Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- name: Install dependencies (skip postinstall)
run: |
pnpm install --ignore-scripts --frozen-lockfile
pnpm install --ignore-scripts --no-frozen-lockfile

- name: Build Packages
run: |
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import semver from 'semver'
type AServiceProvider = (new (_app: Application) => ServiceProvider) & Partial<ServiceProvider>

export class Application extends Container implements IApplication {
private static instance: Application;
public paths = new PathLoader()
private tries: number = 0
private booted = false
Expand All @@ -39,6 +40,8 @@ export class Application extends Container implements IApplication {
constructor(basePath: string) {
super()

Application.instance = this;

dotenvExpand.expand(dotenv.config({ quiet: true }))

this.basePath = basePath
Expand All @@ -48,6 +51,10 @@ export class Application extends Container implements IApplication {
Registerer.register(this)
}

public static getInstance(): Application {
return Application.instance;
}

/**
* Register core bindings into the container
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/queue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,31 @@ The H3ravel framework is open-sourced software licensed under the [MIT license](
[lini]: https://img.shields.io/github/license/h3ravel/framework
[tel]: https://github.com/h3ravel/framework/actions/workflows/test.yml
[tei]: https://github.com/h3ravel/framework/actions/workflows/test.yml/badge.svg


# Queue Package

This package provides background job processing with multiple drivers. Inspired by Laravel queues.

## Features
- Dispatch jobs to different drivers (memory, database, redis stub).
- Worker processes jobs and supports retry/backoff.
- Familiar developer experience with some unique twists.

## Usage

### Creating a Job
```ts
import { JobContract } from "@h3ravel/queue";

export class SendEmailJob implements JobContract {
constructor(private email: string) {}

async handle() {
console.log(`Sending email to ${this.email}`);
}

serialize() {
return { email: this.email };
}
}
14 changes: 14 additions & 0 deletions packages/queue/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createDefaultPreset } from "ts-jest";

const tsJestTransformCfg = createDefaultPreset().transform;

/** @type {import("jest").Config} **/
export default {
testEnvironment: "node",
transform: {
...tsJestTransformCfg,
},
moduleNameMapper: {
'^(\.{1,2}/.*)\.js$': '$1',
},
};
6 changes: 5 additions & 1 deletion packages/queue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@
"test": "jest --passWithNoTests",
"version-patch": "pnpm version patch"
},
"peerDependencies": {
"dependencies": {
"@h3ravel/core": "workspace:^"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"jest": "^30.1.3",
"ts-jest": "^29.4.4",
"typescript": "^5.4.0"
}
}
3 changes: 3 additions & 0 deletions packages/queue/src/Contracts/Job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Job {
handle(): Promise<void> | void;
}
41 changes: 41 additions & 0 deletions packages/queue/src/Contracts/JobContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface JobContract {
/**
* The number of times the job may be attempted.
*/
tries?: number;

/**
* The maximum number of exceptions to allow before failing.
*/
maxExceptions?: number;

/**
* The number of seconds to wait before retrying the job.
*/
backoff?: number;

/**
* The number of seconds to wait before timing out the job.
*/
timeout?: number;

/**
* The number of times the job has been attempted.
*/
attempts?: number;

/**
* Process the job.
*/
handle(): Promise<void> | void;

/**
* The job's serialized data.
*/
serialize(): any;

/**
* The method to call when the job fails.
*/
failed?(error: Error): void;
}
9 changes: 9 additions & 0 deletions packages/queue/src/Contracts/Queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Job } from "./Job";

export interface Queue {
push(job: Job, payload?: any, queue?: string): Promise<any>;
later(delay: number, job: Job, payload?: any, queue?: string): Promise<any>;
size(queue?: string): Promise<number>;
pop(queue?: string): Promise<any>;
release(job: any, delay: number): Promise<void>;
}
8 changes: 8 additions & 0 deletions packages/queue/src/Contracts/QueueDriverContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { JobContract } from '../Contracts/JobContract';

export interface QueueDriverContract {
push(job: JobContract): any;
pop(queue?: string): Promise<JobContract | null> | JobContract | null;
size(queue?: string): Promise<number> | number;
release(job: JobContract, delay?: number): void;
}
37 changes: 37 additions & 0 deletions packages/queue/src/Drivers/ArrayDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Job } from "../Contracts/Job";
import { Queue } from "../Contracts/Queue";

export class ArrayDriver implements Queue {
protected jobs: any[] = [];

public async push(job: Job, payload?: any, queue?: string): Promise<any> {
this.jobs.push({ job, payload, queue });
return Promise.resolve();
}

public async later(delay: number, job: Job, payload?: any, queue?: string): Promise<any> {
return new Promise(resolve => {
setTimeout(async () => {
await this.push(job, payload, queue);
resolve(undefined);
}, delay);
});
}

public async size(queue?: string): Promise<number> {
return this.jobs.length;
}

public async pop(queue?: string): Promise<any> {
return this.jobs.shift();
}

public async release(job: any, delay: number): Promise<void> {
return new Promise(resolve => {
setTimeout(async () => {
this.jobs.unshift(job);
resolve(undefined);
}, delay);
});
}
}
19 changes: 19 additions & 0 deletions packages/queue/src/Drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { JobContract } from '../Contracts/JobContract';
import { QueueDriverContract } from '../Contracts/QueueDriverContract';

export class DatabaseDriver implements QueueDriverContract {
public push(job: JobContract) {
// TODO: Implement database logic
return null;
}

public pop(): Promise<JobContract | null> {
// TODO: Implement database logic
return Promise.resolve(null);
}

public size(): Promise<number> {
// TODO: Implement database logic
return Promise.resolve(0);
}
}
1 change: 0 additions & 1 deletion packages/queue/src/Drivers/MemoryDriver.ts

This file was deleted.

20 changes: 19 additions & 1 deletion packages/queue/src/Drivers/RedisDriver.ts
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
export default class { }
import { JobContract } from '../Contracts/JobContract';
import { QueueDriverContract } from '../Contracts/QueueDriverContract';

export class RedisDriver implements QueueDriverContract {
public push(job: JobContract) {
// TODO: Implement redis logic
return null;
}

public pop(): Promise<JobContract | null> {
// TODO: Implement redis logic
return Promise.resolve(null);
}

public size(): Promise<number> {
// TODO: Implement redis logic
return Promise.resolve(0);
}
}
39 changes: 39 additions & 0 deletions packages/queue/src/Drivers/SyncDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Job } from "../Contracts/Job";
import { Queue } from "../Contracts/Queue";

export class SyncDriver implements Queue {
public async push(job: Job, payload?: any, queue?: string): Promise<any> {
const jobInstance = this.resolveJob(job, payload);
await jobInstance.handle();
}

public async later(delay: number, job: Job, payload?: any, queue?: string): Promise<any> {
await new Promise(resolve => setTimeout(resolve, delay));
return this.push(job, payload, queue);
}

public async size(queue?: string): Promise<number> {
return Promise.resolve(0);
}

public async pop(queue?: string): Promise<any> {
return Promise.resolve(undefined);
}

public async release(job: any, delay: number): Promise<void> {
return Promise.resolve();
}

protected resolveJob(job: any, payload: any): Job {
if (typeof job === 'object') {
return job;
}

// Here you might want to resolve the job from a container or instantiate it
// For now, we'll assume the job is a class constructor
const jobInstance = new job();
// You can pass the payload to the job instance if it has a constructor that accepts it
// Object.assign(jobInstance, payload);
return jobInstance;
}
}
13 changes: 13 additions & 0 deletions packages/queue/src/Jobs/SendEmailJobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { JobContract } from "../Contracts/JobContract";

export class SendEmailJob implements JobContract {
constructor(private email: string) {}

async handle(): Promise<void> {
console.log(`📧 Sending email to ${this.email}`);
}

serialize() {
return { email: this.email, name: "SendEmailJob" };
}
}
24 changes: 8 additions & 16 deletions packages/queue/src/Providers/QueueServiceProvider.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { ServiceProvider } from '@h3ravel/core'
import { ServiceProvider } from "@h3ravel/core";
import { QueueManager } from "../QueueManager";

/**
* Queues and workers.
*
* Register QueueManager.
* Load drivers (Redis, in-memory).
* Register job dispatcher and workers.
*
* Auto-Registered if @h3ravel/queue is installed
*/
export class QueueServiceProvider extends ServiceProvider {
public static priority = 991

register () {
// Core bindings
}
}
public register(): void {
this.app.singleton("queue", (app) => {
return new QueueManager(app);
});
}
}
Loading
Loading