@nebutra/queue 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -2
- package/dist/factory.d.ts +42 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +156 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/middleware/qstash-verify.d.ts +16 -0
- package/dist/middleware/qstash-verify.d.ts.map +1 -0
- package/dist/middleware/qstash-verify.js +87 -0
- package/dist/providers/bullmq.d.ts +32 -0
- package/dist/providers/bullmq.d.ts.map +1 -0
- package/dist/providers/bullmq.js +258 -0
- package/dist/providers/memory.d.ts +23 -0
- package/dist/providers/memory.d.ts.map +1 -0
- package/dist/providers/memory.js +220 -0
- package/dist/providers/qstash.d.ts +26 -0
- package/dist/providers/qstash.d.ts.map +1 -0
- package/dist/providers/qstash.js +226 -0
- package/dist/providers/sqs.d.ts +18 -0
- package/dist/providers/sqs.d.ts.map +1 -0
- package/dist/providers/sqs.js +197 -0
- package/dist/queuebase-webhook.d.ts +45 -0
- package/dist/queuebase-webhook.d.ts.map +1 -0
- package/dist/queuebase-webhook.js +113 -0
- package/dist/queuebase.d.ts +27 -0
- package/dist/queuebase.d.ts.map +1 -0
- package/dist/queuebase.js +48 -0
- package/dist/scheduled/index.d.ts +15 -0
- package/dist/scheduled/index.d.ts.map +1 -0
- package/dist/scheduled/index.js +25 -0
- package/dist/scheduled/jobs/invitation-cleanup.d.ts +40 -0
- package/dist/scheduled/jobs/invitation-cleanup.d.ts.map +1 -0
- package/dist/scheduled/jobs/invitation-cleanup.js +44 -0
- package/dist/scheduled/jobs/session-cleanup.d.ts +24 -0
- package/dist/scheduled/jobs/session-cleanup.d.ts.map +1 -0
- package/dist/scheduled/jobs/session-cleanup.js +32 -0
- package/dist/scheduled/scheduler.d.ts +44 -0
- package/dist/scheduled/scheduler.d.ts.map +1 -0
- package/dist/scheduled/scheduler.js +52 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/package.json +17 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { queuebaseJobs } from "./queuebase-webhook";
|
|
2
|
+
export { createJobRouter, createQueuebaseWebhookHandler, defineQueueJob, listQueuebaseSchedules, queuebaseJobs, queuebaseWebhookHandler, verifyQueuebaseSignature, } from "./queuebase-webhook";
|
|
3
|
+
export function createJobClient(router, options) {
|
|
4
|
+
return Object.fromEntries(Object.entries(router).map(([jobName, definition]) => [
|
|
5
|
+
jobName,
|
|
6
|
+
{
|
|
7
|
+
enqueue: async (input, enqueueOptions) => {
|
|
8
|
+
const parsed = definition.input.safeParse(input);
|
|
9
|
+
if (!parsed.success) {
|
|
10
|
+
throw new Error(`Invalid input for job ${jobName}: ${parsed.error.message}`);
|
|
11
|
+
}
|
|
12
|
+
const { createJob, getQueue } = await import("./factory");
|
|
13
|
+
const queue = options.queue ?? (await getQueue());
|
|
14
|
+
const result = await queue.enqueue(createJob("queuebase", jobName, parsed.data, {
|
|
15
|
+
...enqueueOptions,
|
|
16
|
+
maxRetries: enqueueOptions?.maxRetries ?? definition.defaults?.retries,
|
|
17
|
+
metadata: {
|
|
18
|
+
...enqueueOptions?.metadata,
|
|
19
|
+
queuebaseJobName: jobName,
|
|
20
|
+
callbackUrl: options.callbackUrl,
|
|
21
|
+
...(options.apiUrl ? { apiUrl: options.apiUrl } : {}),
|
|
22
|
+
...(definition.defaults?.backoff ? { backoff: definition.defaults.backoff } : {}),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
jobName,
|
|
28
|
+
callbackUrl: options.callbackUrl,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
export function getQueuebaseCallbackUrl(baseUrl = getBaseUrlFromEnv()) {
|
|
35
|
+
return `${baseUrl.replace(/\/$/, "")}/api/webhooks/queuebase`;
|
|
36
|
+
}
|
|
37
|
+
export const queuebaseJobClient = createJobClient(queuebaseJobs, {
|
|
38
|
+
apiUrl: process.env.QUEUEBASE_API_URL ?? "http://localhost:3847",
|
|
39
|
+
...(process.env.QUEUEBASE_API_KEY ? { apiKey: process.env.QUEUEBASE_API_KEY } : {}),
|
|
40
|
+
callbackUrl: getQueuebaseCallbackUrl(),
|
|
41
|
+
});
|
|
42
|
+
function getBaseUrlFromEnv() {
|
|
43
|
+
if (process.env.NEXT_PUBLIC_SITE_URL)
|
|
44
|
+
return process.env.NEXT_PUBLIC_SITE_URL;
|
|
45
|
+
if (process.env.VERCEL_URL)
|
|
46
|
+
return `https://${process.env.VERCEL_URL}`;
|
|
47
|
+
return `http://localhost:${process.env.PORT ?? "3001"}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { RunInvitationCleanupOptions } from "./jobs/invitation-cleanup";
|
|
2
|
+
export { invitationCleanup, runInvitationCleanup } from "./jobs/invitation-cleanup";
|
|
3
|
+
export type { RunSessionCleanupOptions } from "./jobs/session-cleanup";
|
|
4
|
+
export { runSessionCleanup, sessionCleanup } from "./jobs/session-cleanup";
|
|
5
|
+
export type { ScheduledJob, ScheduledJobResult } from "./scheduler";
|
|
6
|
+
export { clearScheduledJobs, getScheduledJob, listScheduledJobs, registerScheduledJob, } from "./scheduler";
|
|
7
|
+
/**
|
|
8
|
+
* Register the two built-in production scheduled jobs:
|
|
9
|
+
* - `invitation-cleanup` (every 6h)
|
|
10
|
+
* - `session-cleanup` (daily 00:00 UTC)
|
|
11
|
+
*
|
|
12
|
+
* Idempotent — re-registering overwrites the previous entry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerDefaultScheduledJobs(): void;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduled/index.ts"],"names":[],"mappings":"AASA,YAAY,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACpF,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAMrB;;;;;;GAMG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// @nebutra/queue/scheduled — scheduled (cron) job registry & built-in jobs
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Public re-exports:
|
|
5
|
+
// - The scheduler abstraction: register / get / list / clear scheduled jobs
|
|
6
|
+
// - Built-in production jobs: invitation-cleanup, session-cleanup
|
|
7
|
+
// - `registerDefaultScheduledJobs()` — convenience to register the built-ins
|
|
8
|
+
// =============================================================================
|
|
9
|
+
export { invitationCleanup, runInvitationCleanup } from "./jobs/invitation-cleanup";
|
|
10
|
+
export { runSessionCleanup, sessionCleanup } from "./jobs/session-cleanup";
|
|
11
|
+
export { clearScheduledJobs, getScheduledJob, listScheduledJobs, registerScheduledJob, } from "./scheduler";
|
|
12
|
+
import { invitationCleanup } from "./jobs/invitation-cleanup";
|
|
13
|
+
import { sessionCleanup } from "./jobs/session-cleanup";
|
|
14
|
+
import { registerScheduledJob } from "./scheduler";
|
|
15
|
+
/**
|
|
16
|
+
* Register the two built-in production scheduled jobs:
|
|
17
|
+
* - `invitation-cleanup` (every 6h)
|
|
18
|
+
* - `session-cleanup` (daily 00:00 UTC)
|
|
19
|
+
*
|
|
20
|
+
* Idempotent — re-registering overwrites the previous entry.
|
|
21
|
+
*/
|
|
22
|
+
export function registerDefaultScheduledJobs() {
|
|
23
|
+
registerScheduledJob(invitationCleanup);
|
|
24
|
+
registerScheduledJob(sessionCleanup);
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ScheduledJob, ScheduledJobResult } from "../scheduler";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal subset of the Prisma client surface this job needs.
|
|
4
|
+
* Keeps the test fakes tiny and avoids pulling Prisma's full type into hot paths.
|
|
5
|
+
*/
|
|
6
|
+
interface InvitationCleanupClient {
|
|
7
|
+
organizationInvitation: {
|
|
8
|
+
updateMany(args: {
|
|
9
|
+
where: {
|
|
10
|
+
status: string;
|
|
11
|
+
expiresAt: {
|
|
12
|
+
lt: Date;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
data: {
|
|
16
|
+
status: string;
|
|
17
|
+
};
|
|
18
|
+
}): Promise<{
|
|
19
|
+
count: number;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface RunInvitationCleanupOptions {
|
|
24
|
+
/** Prisma client (or a structural fake in tests). */
|
|
25
|
+
client: InvitationCleanupClient;
|
|
26
|
+
/** Override "now" for deterministic testing. */
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pure handler — flips rows whose `expiresAt < now()` and `status = 'pending'`
|
|
31
|
+
* to `status = 'expired'`. Returns the count of affected rows.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runInvitationCleanup(options: RunInvitationCleanupOptions): Promise<ScheduledJobResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Registry-shaped definition. The handler resolves the real Prisma client
|
|
36
|
+
* lazily via dynamic import so tests don't need a live database.
|
|
37
|
+
*/
|
|
38
|
+
export declare const invitationCleanup: ScheduledJob;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=invitation-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitation-cleanup.d.ts","sourceRoot":"","sources":["../../../src/scheduled/jobs/invitation-cleanup.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAErE;;;GAGG;AACH,UAAU,uBAAuB;IAC/B,sBAAsB,EAAE;QACtB,UAAU,CAAC,IAAI,EAAE;YACf,KAAK,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE;oBAAE,EAAE,EAAE,IAAI,CAAA;iBAAE,CAAA;aAAE,CAAC;YACnD,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAA;aAAE,CAAC;SAC1B,GAAG,OAAO,CAAC;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,2BAA2B;IAC1C,qDAAqD;IACrD,MAAM,EAAE,uBAAuB,CAAC;IAChC,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAiB7B;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAO/B,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// invitation-cleanup — marks expired pending OrganizationInvitation rows
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Scheduled every 6 hours. Idempotent — re-running the same hour is a no-op
|
|
5
|
+
// because rows already flipped to `expired` no longer match the WHERE clause.
|
|
6
|
+
//
|
|
7
|
+
// The job is implemented as two layers:
|
|
8
|
+
// - `runInvitationCleanup({ client })` is the pure async unit, easily
|
|
9
|
+
// testable with an injected fake Prisma client.
|
|
10
|
+
// - `invitationCleanup` is the registry-shaped `ScheduledJob` whose
|
|
11
|
+
// handler resolves the real Prisma client from `@nebutra/db` lazily.
|
|
12
|
+
// =============================================================================
|
|
13
|
+
import { logger } from "@nebutra/logger";
|
|
14
|
+
/**
|
|
15
|
+
* Pure handler — flips rows whose `expiresAt < now()` and `status = 'pending'`
|
|
16
|
+
* to `status = 'expired'`. Returns the count of affected rows.
|
|
17
|
+
*/
|
|
18
|
+
export async function runInvitationCleanup(options) {
|
|
19
|
+
const now = (options.now ?? (() => new Date()))();
|
|
20
|
+
const { count } = await options.client.organizationInvitation.updateMany({
|
|
21
|
+
where: {
|
|
22
|
+
status: "pending",
|
|
23
|
+
expiresAt: { lt: now },
|
|
24
|
+
},
|
|
25
|
+
data: { status: "expired" },
|
|
26
|
+
});
|
|
27
|
+
logger.info("[scheduled:invitation-cleanup] expired pending invitations", {
|
|
28
|
+
expired: count,
|
|
29
|
+
cutoff: now.toISOString(),
|
|
30
|
+
});
|
|
31
|
+
return { ok: true, details: { expired: count } };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Registry-shaped definition. The handler resolves the real Prisma client
|
|
35
|
+
* lazily via dynamic import so tests don't need a live database.
|
|
36
|
+
*/
|
|
37
|
+
export const invitationCleanup = {
|
|
38
|
+
name: "invitation-cleanup",
|
|
39
|
+
cron: "0 */6 * * *",
|
|
40
|
+
handler: async () => {
|
|
41
|
+
const { getSystemDb } = await import("@nebutra/db");
|
|
42
|
+
return runInvitationCleanup({ client: getSystemDb() });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ScheduledJob, ScheduledJobResult } from "../scheduler";
|
|
2
|
+
interface SessionCleanupClient {
|
|
3
|
+
authSession: {
|
|
4
|
+
deleteMany(args: {
|
|
5
|
+
where: {
|
|
6
|
+
expiresAt: {
|
|
7
|
+
lt: Date;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
}): Promise<{
|
|
11
|
+
count: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface RunSessionCleanupOptions {
|
|
16
|
+
/** Prisma client (or a structural fake in tests). */
|
|
17
|
+
client: SessionCleanupClient;
|
|
18
|
+
/** Override "now" for deterministic testing. */
|
|
19
|
+
now?: () => Date;
|
|
20
|
+
}
|
|
21
|
+
export declare function runSessionCleanup(options: RunSessionCleanupOptions): Promise<ScheduledJobResult>;
|
|
22
|
+
export declare const sessionCleanup: ScheduledJob;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=session-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cleanup.d.ts","sourceRoot":"","sources":["../../../src/scheduled/jobs/session-cleanup.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAErE,UAAU,oBAAoB;IAC5B,WAAW,EAAE;QACX,UAAU,CAAC,IAAI,EAAE;YAAE,KAAK,EAAE;gBAAE,SAAS,EAAE;oBAAE,EAAE,EAAE,IAAI,CAAA;iBAAE,CAAA;aAAE,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtF,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,qDAAqD;IACrD,MAAM,EAAE,oBAAoB,CAAC;IAC7B,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED,eAAO,MAAM,cAAc,EAAE,YAO5B,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// session-cleanup — deletes expired auth_sessions rows
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Scheduled daily at midnight UTC. Idempotent — already-deleted rows simply
|
|
5
|
+
// don't match the next run's WHERE clause.
|
|
6
|
+
//
|
|
7
|
+
// Two layers (mirrors invitation-cleanup):
|
|
8
|
+
// - `runSessionCleanup({ client })` — pure async unit, DI-friendly.
|
|
9
|
+
// - `sessionCleanup` — registry-shaped `ScheduledJob`.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
import { logger } from "@nebutra/logger";
|
|
12
|
+
export async function runSessionCleanup(options) {
|
|
13
|
+
const now = (options.now ?? (() => new Date()))();
|
|
14
|
+
const { count } = await options.client.authSession.deleteMany({
|
|
15
|
+
where: {
|
|
16
|
+
expiresAt: { lt: now },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
logger.info("[scheduled:session-cleanup] deleted expired auth sessions", {
|
|
20
|
+
deleted: count,
|
|
21
|
+
cutoff: now.toISOString(),
|
|
22
|
+
});
|
|
23
|
+
return { ok: true, details: { deleted: count } };
|
|
24
|
+
}
|
|
25
|
+
export const sessionCleanup = {
|
|
26
|
+
name: "session-cleanup",
|
|
27
|
+
cron: "0 0 * * *",
|
|
28
|
+
handler: async () => {
|
|
29
|
+
const { getSystemDb } = await import("@nebutra/db");
|
|
30
|
+
return runSessionCleanup({ client: getSystemDb() });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result returned by a scheduled job handler.
|
|
3
|
+
*
|
|
4
|
+
* - `ok` — true on success, false on partial / soft failure
|
|
5
|
+
* - `details` — arbitrary structured payload (counts, IDs) for observability
|
|
6
|
+
*/
|
|
7
|
+
export interface ScheduledJobResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
details?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A scheduled job is a cron-triggered idempotent unit of work.
|
|
13
|
+
*/
|
|
14
|
+
export interface ScheduledJob {
|
|
15
|
+
/** Unique registry key — used by HTTP entry points to look up the handler. */
|
|
16
|
+
name: string;
|
|
17
|
+
/**
|
|
18
|
+
* Cron expression in standard 5-field syntax (minute hour dom month dow).
|
|
19
|
+
* This is metadata only — the registry never schedules anything itself.
|
|
20
|
+
*/
|
|
21
|
+
cron: string;
|
|
22
|
+
/** The async unit of work. Must be idempotent. */
|
|
23
|
+
handler: () => Promise<ScheduledJobResult>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register a scheduled job. Re-registering the same name overwrites the
|
|
27
|
+
* previous entry (useful for tests; production code registers once at boot).
|
|
28
|
+
*/
|
|
29
|
+
export declare function registerScheduledJob(job: ScheduledJob): void;
|
|
30
|
+
/**
|
|
31
|
+
* Look up a registered job by name. Returns `undefined` if not registered.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getScheduledJob(name: string): ScheduledJob | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* List all registered jobs. Returns a snapshot array — mutating it does NOT
|
|
36
|
+
* affect the registry.
|
|
37
|
+
*/
|
|
38
|
+
export declare function listScheduledJobs(): ScheduledJob[];
|
|
39
|
+
/**
|
|
40
|
+
* Remove all registered jobs. Intended for tests; production code should not
|
|
41
|
+
* call this.
|
|
42
|
+
*/
|
|
43
|
+
export declare function clearScheduledJobs(): void;
|
|
44
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/scheduled/scheduler.ts"],"names":[],"mappings":"AAeA;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC5C;AAMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAW5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,YAAY,EAAE,CAElD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Scheduled Job Registry — Provider-agnostic cron abstraction
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// This module defines a pure runtime registry for scheduled (cron) jobs.
|
|
5
|
+
//
|
|
6
|
+
// Schedule activation is decoupled from job definition: jobs are registered
|
|
7
|
+
// here with their cron expression, and an external scheduler (Vercel Cron,
|
|
8
|
+
// QStash schedules, BullMQ repeatable jobs) triggers them via HTTP route or
|
|
9
|
+
// worker.
|
|
10
|
+
//
|
|
11
|
+
// Default activation in this codebase is Vercel Cron — see
|
|
12
|
+
// `apps/web/src/app/api/cron/<name>/route.ts` for the HTTP entry points and
|
|
13
|
+
// the root `vercel.json` `crons` block for schedule activation.
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// ── Registry (module-scoped Map) ────────────────────────────────────────────
|
|
16
|
+
const registry = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Register a scheduled job. Re-registering the same name overwrites the
|
|
19
|
+
* previous entry (useful for tests; production code registers once at boot).
|
|
20
|
+
*/
|
|
21
|
+
export function registerScheduledJob(job) {
|
|
22
|
+
if (!job.name || typeof job.name !== "string") {
|
|
23
|
+
throw new Error("[scheduler] job.name must be a non-empty string");
|
|
24
|
+
}
|
|
25
|
+
if (!job.cron || typeof job.cron !== "string") {
|
|
26
|
+
throw new Error("[scheduler] job.cron must be a non-empty string");
|
|
27
|
+
}
|
|
28
|
+
if (typeof job.handler !== "function") {
|
|
29
|
+
throw new Error("[scheduler] job.handler must be a function");
|
|
30
|
+
}
|
|
31
|
+
registry.set(job.name, job);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Look up a registered job by name. Returns `undefined` if not registered.
|
|
35
|
+
*/
|
|
36
|
+
export function getScheduledJob(name) {
|
|
37
|
+
return registry.get(name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* List all registered jobs. Returns a snapshot array — mutating it does NOT
|
|
41
|
+
* affect the registry.
|
|
42
|
+
*/
|
|
43
|
+
export function listScheduledJobs() {
|
|
44
|
+
return Array.from(registry.values());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Remove all registered jobs. Intended for tests; production code should not
|
|
48
|
+
* call this.
|
|
49
|
+
*/
|
|
50
|
+
export function clearScheduledJobs() {
|
|
51
|
+
registry.clear();
|
|
52
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Supported queue backend providers.
|
|
4
|
+
*
|
|
5
|
+
* - `qstash` — Upstash QStash (serverless, HTTP-based, zero infra)
|
|
6
|
+
* - `bullmq` — BullMQ over self-hosted Redis (full-featured, self-managed)
|
|
7
|
+
* - `sqs` — AWS SQS (managed, pull-based, ideal for AWS-native deployments)
|
|
8
|
+
* - `memory` — In-memory queue for local dev & testing (NOT for production)
|
|
9
|
+
*/
|
|
10
|
+
export type QueueProviderType = "qstash" | "bullmq" | "sqs" | "memory";
|
|
11
|
+
export declare const JobOptionsSchema: z.ZodObject<{
|
|
12
|
+
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
13
|
+
delaySec: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
maxRetries: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
cron: z.ZodOptional<z.ZodString>;
|
|
17
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
18
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export type JobOptions = z.infer<typeof JobOptionsSchema>;
|
|
21
|
+
export declare const JobPayloadSchema: z.ZodObject<{
|
|
22
|
+
id: z.ZodString;
|
|
23
|
+
queue: z.ZodString;
|
|
24
|
+
type: z.ZodString;
|
|
25
|
+
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
26
|
+
options: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
28
|
+
delaySec: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
maxRetries: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
cron: z.ZodOptional<z.ZodString>;
|
|
32
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
33
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
createdAt: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
export type JobPayload = z.infer<typeof JobPayloadSchema>;
|
|
38
|
+
export interface JobResult {
|
|
39
|
+
/** Provider-specific job/message ID */
|
|
40
|
+
jobId: string;
|
|
41
|
+
/** Acknowledged by the provider? */
|
|
42
|
+
accepted: boolean;
|
|
43
|
+
/** Provider name that handled the enqueue */
|
|
44
|
+
provider: QueueProviderType;
|
|
45
|
+
}
|
|
46
|
+
export type JobStatus = "pending" | "active" | "completed" | "failed" | "dead-lettered" | "delayed" | "waiting" | "canceled";
|
|
47
|
+
export type JobLifecycleAction = "enqueued" | "started" | "attempt-failed" | "completed" | "failed" | "dead-lettered" | "canceled" | "retried";
|
|
48
|
+
export interface JobLifecycleEvent {
|
|
49
|
+
id: string;
|
|
50
|
+
jobId: string;
|
|
51
|
+
queue: string;
|
|
52
|
+
action: JobLifecycleAction;
|
|
53
|
+
at: string;
|
|
54
|
+
attempt?: number;
|
|
55
|
+
reason?: string;
|
|
56
|
+
actor?: string;
|
|
57
|
+
metadata?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface JobLifecycleActionResult {
|
|
60
|
+
jobId: string;
|
|
61
|
+
queue: string;
|
|
62
|
+
accepted: boolean;
|
|
63
|
+
action: "cancel" | "retry";
|
|
64
|
+
reason?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface JobStatusInfo {
|
|
67
|
+
id: string;
|
|
68
|
+
queue: string;
|
|
69
|
+
status: JobStatus;
|
|
70
|
+
attempts: number;
|
|
71
|
+
maxRetries: number;
|
|
72
|
+
failedReason?: string;
|
|
73
|
+
canceledReason?: string;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
processedAt?: string;
|
|
76
|
+
completedAt?: string;
|
|
77
|
+
deadLetteredAt?: string;
|
|
78
|
+
canceledAt?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface DeadLetterJob {
|
|
81
|
+
id: string;
|
|
82
|
+
queue: string;
|
|
83
|
+
type: string;
|
|
84
|
+
originalJob: JobPayload;
|
|
85
|
+
attempts: number;
|
|
86
|
+
maxRetries: number;
|
|
87
|
+
failedReason: string;
|
|
88
|
+
provider: QueueProviderType;
|
|
89
|
+
failedAt: string;
|
|
90
|
+
}
|
|
91
|
+
export type QueueDeadLetterRecord = Record<string, unknown>;
|
|
92
|
+
export interface QueueDeadLetterFetchContext {
|
|
93
|
+
token: string;
|
|
94
|
+
endpoint?: string;
|
|
95
|
+
queue?: string;
|
|
96
|
+
}
|
|
97
|
+
export type QueueDeadLetterFetcher = (context: QueueDeadLetterFetchContext) => Promise<QueueDeadLetterRecord[]>;
|
|
98
|
+
/**
|
|
99
|
+
* A job handler receives the payload and returns void on success
|
|
100
|
+
* or throws to trigger a retry.
|
|
101
|
+
*/
|
|
102
|
+
export type JobHandler<T extends Record<string, unknown> = Record<string, unknown>> = (job: JobPayload & {
|
|
103
|
+
data: T;
|
|
104
|
+
}) => Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Every queue backend must implement this interface.
|
|
107
|
+
* The factory function (`createQueue`) returns a `QueueProvider`.
|
|
108
|
+
*/
|
|
109
|
+
export interface QueueProvider {
|
|
110
|
+
readonly name: QueueProviderType;
|
|
111
|
+
/**
|
|
112
|
+
* Enqueue a job for async processing.
|
|
113
|
+
*/
|
|
114
|
+
enqueue(job: JobPayload): Promise<JobResult>;
|
|
115
|
+
/**
|
|
116
|
+
* Enqueue multiple jobs in a single round-trip (where supported).
|
|
117
|
+
* Falls back to sequential enqueue if the provider has no native batch API.
|
|
118
|
+
*/
|
|
119
|
+
enqueueBatch(jobs: JobPayload[]): Promise<JobResult[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Register a handler for a given job type.
|
|
122
|
+
* - BullMQ: starts a Worker that polls Redis.
|
|
123
|
+
* - QStash: returns an HTTP handler to mount in your API route.
|
|
124
|
+
* - Memory: processes inline.
|
|
125
|
+
*/
|
|
126
|
+
registerHandler<T extends Record<string, unknown>>(queue: string, type: string, handler: JobHandler<T>): void;
|
|
127
|
+
/**
|
|
128
|
+
* Query the status of a previously enqueued job (best-effort).
|
|
129
|
+
* Not all providers support this; returns `undefined` if unsupported.
|
|
130
|
+
*/
|
|
131
|
+
getJobStatus?(jobId: string, queue: string): Promise<JobStatusInfo | undefined>;
|
|
132
|
+
/**
|
|
133
|
+
* Inspect jobs that exhausted retries and require operator attention.
|
|
134
|
+
* Providers without a durable DLQ may omit this method.
|
|
135
|
+
*/
|
|
136
|
+
getDeadLetteredJobs?(queue?: string): Promise<DeadLetterJob[]>;
|
|
137
|
+
/**
|
|
138
|
+
* Return provider-neutral lifecycle logs for operator tooling.
|
|
139
|
+
* Providers without durable inspection may omit this method.
|
|
140
|
+
*/
|
|
141
|
+
getJobLogs?(jobId: string, queue: string): Promise<JobLifecycleEvent[]>;
|
|
142
|
+
/**
|
|
143
|
+
* Cancel a pending/delayed job when supported.
|
|
144
|
+
*/
|
|
145
|
+
cancelJob?(jobId: string, queue: string, reason?: string): Promise<JobLifecycleActionResult>;
|
|
146
|
+
/**
|
|
147
|
+
* Replay a failed or dead-lettered job when supported.
|
|
148
|
+
*/
|
|
149
|
+
retryJob?(jobId: string, queue: string, reason?: string): Promise<JobLifecycleActionResult>;
|
|
150
|
+
/**
|
|
151
|
+
* Graceful shutdown — drain in-flight jobs, close connections.
|
|
152
|
+
*/
|
|
153
|
+
close(): Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
export interface QStashProviderConfig {
|
|
156
|
+
provider: "qstash";
|
|
157
|
+
/** QStash REST token (defaults to `process.env.QSTASH_TOKEN`) */
|
|
158
|
+
token?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Base URL of your API that QStash will POST callbacks to.
|
|
161
|
+
* e.g. "https://api.nebutra.com" or "https://my-tunnel.ngrok.io"
|
|
162
|
+
*/
|
|
163
|
+
callbackBaseUrl: string;
|
|
164
|
+
/** Optional signing keys for webhook verification */
|
|
165
|
+
currentSigningKey?: string;
|
|
166
|
+
nextSigningKey?: string;
|
|
167
|
+
/** Optional provider-side DLQ endpoint for injected fetchers. */
|
|
168
|
+
dlqEndpoint?: string;
|
|
169
|
+
/** Injectable provider-side DLQ fetcher. No Upstash SDK DLQ APIs are assumed. */
|
|
170
|
+
dlqFetcher?: QueueDeadLetterFetcher;
|
|
171
|
+
}
|
|
172
|
+
export interface BullMQProviderConfig {
|
|
173
|
+
provider: "bullmq";
|
|
174
|
+
/** Redis connection URL (defaults to `process.env.REDIS_URL`) */
|
|
175
|
+
redisUrl?: string;
|
|
176
|
+
/** Default concurrency per worker (default: 5) */
|
|
177
|
+
concurrency?: number;
|
|
178
|
+
/** Key prefix for Redis keys (default: "nebutra:queue") */
|
|
179
|
+
prefix?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface MemoryProviderConfig {
|
|
182
|
+
provider: "memory";
|
|
183
|
+
}
|
|
184
|
+
export interface SQSProviderConfig {
|
|
185
|
+
provider: "sqs";
|
|
186
|
+
/** AWS region (defaults to `process.env.AWS_REGION`) */
|
|
187
|
+
region?: string;
|
|
188
|
+
/** SQS queue URL (defaults to `process.env.AWS_SQS_QUEUE_URL`) */
|
|
189
|
+
queueUrl?: string;
|
|
190
|
+
/** AWS access key (defaults to `process.env.AWS_ACCESS_KEY_ID`) */
|
|
191
|
+
accessKeyId?: string;
|
|
192
|
+
/** AWS secret key (defaults to `process.env.AWS_SECRET_ACCESS_KEY`) */
|
|
193
|
+
secretAccessKey?: string;
|
|
194
|
+
/** Long-poll wait time in seconds when receiving (1-20, default 20) */
|
|
195
|
+
waitTimeSeconds?: number;
|
|
196
|
+
/** Max messages per receive call (1-10, default 10) */
|
|
197
|
+
maxMessages?: number;
|
|
198
|
+
/** Visibility timeout in seconds — how long a received message is hidden (default 30) */
|
|
199
|
+
visibilityTimeoutSeconds?: number;
|
|
200
|
+
}
|
|
201
|
+
export type QueueConfig = QStashProviderConfig | BullMQProviderConfig | SQSProviderConfig | MemoryProviderConfig;
|
|
202
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ── Job Schema ──────────────────────────────────────────────────────────────
|
|
3
|
+
export const JobOptionsSchema = z.object({
|
|
4
|
+
/** Unique idempotency key — prevents duplicate processing */
|
|
5
|
+
idempotencyKey: z.string().optional(),
|
|
6
|
+
/** Delay before the job becomes visible (in seconds) */
|
|
7
|
+
delaySec: z.number().int().min(0).optional(),
|
|
8
|
+
/** Maximum retry attempts on failure (default: 3) */
|
|
9
|
+
maxRetries: z.number().int().min(0).optional(),
|
|
10
|
+
/** Priority: lower number = higher priority (BullMQ only, ignored by QStash) */
|
|
11
|
+
priority: z.number().int().min(0).optional(),
|
|
12
|
+
/** Cron expression for recurring jobs (e.g. "0 3 * * *") */
|
|
13
|
+
cron: z.string().optional(),
|
|
14
|
+
/** Tenant isolation — jobs can be scoped to a tenant */
|
|
15
|
+
tenantId: z.string().optional(),
|
|
16
|
+
/** Arbitrary metadata passed through to the handler */
|
|
17
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
18
|
+
});
|
|
19
|
+
export const JobPayloadSchema = z.object({
|
|
20
|
+
/** Globally unique job ID (auto-generated if omitted) */
|
|
21
|
+
id: z.string(),
|
|
22
|
+
/** Queue / topic name this job belongs to */
|
|
23
|
+
queue: z.string(),
|
|
24
|
+
/** Fully-qualified handler name (e.g. "email.send", "report.generate") */
|
|
25
|
+
type: z.string(),
|
|
26
|
+
/** Serialisable job data */
|
|
27
|
+
data: z.record(z.string(), z.unknown()),
|
|
28
|
+
/** Job scheduling & retry options */
|
|
29
|
+
options: JobOptionsSchema.optional(),
|
|
30
|
+
/** ISO-8601 creation timestamp */
|
|
31
|
+
createdAt: z.string().datetime(),
|
|
32
|
+
});
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/queue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"nebutra": {
|
|
8
8
|
"status": "foundation",
|
|
9
9
|
"productionReady": false,
|
|
10
10
|
"requires": [
|
|
11
11
|
"QSTASH_TOKEN (+ signing keys) for serverless",
|
|
12
|
+
"Configured QStash dlqFetcher/dlqEndpoint for provider-side DLQ observability",
|
|
12
13
|
"REDIS_URL for BullMQ self-hosted",
|
|
13
14
|
"AWS_SQS_QUEUE_URL (+ AWS credentials) for SQS"
|
|
14
15
|
],
|
|
15
16
|
"gaps": [
|
|
16
|
-
"QStash provider dead letter retrieval not implemented",
|
|
17
17
|
"Worker auto-scaling TODO"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
@@ -35,6 +35,18 @@
|
|
|
35
35
|
"./sqs": {
|
|
36
36
|
"types": "./dist/providers/sqs.d.ts",
|
|
37
37
|
"import": "./dist/providers/sqs.js"
|
|
38
|
+
},
|
|
39
|
+
"./queuebase": {
|
|
40
|
+
"types": "./dist/queuebase.d.ts",
|
|
41
|
+
"import": "./dist/queuebase.js"
|
|
42
|
+
},
|
|
43
|
+
"./queuebase-webhook": {
|
|
44
|
+
"types": "./dist/queuebase-webhook.d.ts",
|
|
45
|
+
"import": "./dist/queuebase-webhook.js"
|
|
46
|
+
},
|
|
47
|
+
"./scheduled": {
|
|
48
|
+
"types": "./dist/scheduled/index.d.ts",
|
|
49
|
+
"import": "./dist/scheduled/index.js"
|
|
38
50
|
}
|
|
39
51
|
},
|
|
40
52
|
"files": [
|
|
@@ -47,7 +59,8 @@
|
|
|
47
59
|
"bullmq": "^5.34.8",
|
|
48
60
|
"ioredis": "^5.4.2",
|
|
49
61
|
"zod": "^4.3.6",
|
|
50
|
-
"@nebutra/logger": "0.1.0"
|
|
62
|
+
"@nebutra/logger": "0.1.0",
|
|
63
|
+
"@nebutra/provider-factory": "0.2.0"
|
|
51
64
|
},
|
|
52
65
|
"peerDependencies": {
|
|
53
66
|
"@nebutra/db": "0.1.0"
|