effect-mq 0.1.0
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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/Job.d.ts +222 -0
- package/dist/Job.d.ts.map +1 -0
- package/dist/Job.js +218 -0
- package/dist/Job.js.map +1 -0
- package/dist/JobStore.d.ts +401 -0
- package/dist/JobStore.d.ts.map +1 -0
- package/dist/JobStore.js +89 -0
- package/dist/JobStore.js.map +1 -0
- package/dist/MemoryJobStore.d.ts +34 -0
- package/dist/MemoryJobStore.d.ts.map +1 -0
- package/dist/MemoryJobStore.js +381 -0
- package/dist/MemoryJobStore.js.map +1 -0
- package/dist/Worker.d.ts +127 -0
- package/dist/Worker.d.ts.map +1 -0
- package/dist/Worker.js +274 -0
- package/dist/Worker.js.map +1 -0
- package/dist/drizzle/DrizzleJobStore.d.ts +59 -0
- package/dist/drizzle/DrizzleJobStore.d.ts.map +1 -0
- package/dist/drizzle/DrizzleJobStore.js +426 -0
- package/dist/drizzle/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle/index.d.ts +19 -0
- package/dist/drizzle/index.d.ts.map +1 -0
- package/dist/drizzle/index.js +19 -0
- package/dist/drizzle/index.js.map +1 -0
- package/dist/drizzle/schema.d.ts +464 -0
- package/dist/drizzle/schema.d.ts.map +1 -0
- package/dist/drizzle/schema.js +68 -0
- package/dist/drizzle/schema.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/conformance.d.ts +27 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +451 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +71 -0
- package/src/Job.ts +606 -0
- package/src/JobStore.ts +446 -0
- package/src/MemoryJobStore.ts +467 -0
- package/src/Worker.ts +514 -0
- package/src/drizzle/DrizzleJobStore.ts +599 -0
- package/src/drizzle/index.ts +20 -0
- package/src/drizzle/schema.ts +116 -0
- package/src/index.ts +33 -0
- package/src/testing/conformance.ts +654 -0
- package/src/testing/index.ts +7 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema factories for effect-mq's Postgres tables.
|
|
3
|
+
*
|
|
4
|
+
* Re-export these from your own drizzle schema so drizzle-kit owns the
|
|
5
|
+
* migrations and your queries are fully typed — including the job `name`
|
|
6
|
+
* column typed to the union of your job tags:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // schema.ts
|
|
10
|
+
* import { mqJobAttempts, mqJobs } from "effect-mq/drizzle"
|
|
11
|
+
*
|
|
12
|
+
* type DurableJobs = typeof SyncBenefits._tag | typeof GenerateReport._tag
|
|
13
|
+
* export const jobs = mqJobs<DurableJobs>()
|
|
14
|
+
* export const jobAttempts = mqJobAttempts(jobs)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Then `drizzle-kit generate` emits the CREATE TABLE migrations into your
|
|
18
|
+
* pipeline like any other table. Reads through drizzle are encouraged;
|
|
19
|
+
* writes must go through the `JobStore` (e.g. `store.retry`) so locking and
|
|
20
|
+
* wake-up invariants hold.
|
|
21
|
+
*
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
import type * as JobStore from "../JobStore.ts"
|
|
25
|
+
import { sql } from "drizzle-orm"
|
|
26
|
+
import { bigint, index, integer, jsonb, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core"
|
|
27
|
+
|
|
28
|
+
type JobId = JobStore.JobId
|
|
29
|
+
type QueueName = JobStore.QueueName
|
|
30
|
+
type JobState = JobStore.JobState
|
|
31
|
+
type BackoffPolicy = JobStore.BackoffPolicy
|
|
32
|
+
type KeepPolicy = JobStore.KeepPolicy
|
|
33
|
+
type AttemptOutcome = JobStore.AttemptRecord["outcome"]
|
|
34
|
+
/**
|
|
35
|
+
* The jobs table factory. `JobName` types the `name` column — derive it from
|
|
36
|
+
* your job definitions: `mqJobs<typeof SyncBenefits._tag | typeof Report._tag>()`.
|
|
37
|
+
*
|
|
38
|
+
* @since 0.1.0
|
|
39
|
+
*/
|
|
40
|
+
export const mqJobs = <JobName extends string = string>(
|
|
41
|
+
tableName = "effect_mq_jobs"
|
|
42
|
+
) =>
|
|
43
|
+
pgTable(tableName, {
|
|
44
|
+
id: text("id").primaryKey().$type<JobId>(),
|
|
45
|
+
name: text("name").notNull().$type<JobName>(),
|
|
46
|
+
queue: text("queue").notNull().$type<QueueName>(),
|
|
47
|
+
state: text("state").notNull().$type<JobState>(),
|
|
48
|
+
priority: integer("priority").notNull().default(0),
|
|
49
|
+
/** FIFO order within a priority; bumped on retry so retries go to the tail. */
|
|
50
|
+
seq: bigint("seq", { mode: "number" }).notNull().generatedByDefaultAsIdentity(),
|
|
51
|
+
payload: jsonb("payload"),
|
|
52
|
+
metadata: jsonb("metadata").notNull().default({}).$type<Record<string, string>>(),
|
|
53
|
+
attemptsMax: integer("attempts_max").notNull(),
|
|
54
|
+
attemptsMade: integer("attempts_made").notNull().default(0),
|
|
55
|
+
stalledCount: integer("stalled_count").notNull().default(0),
|
|
56
|
+
backoff: jsonb("backoff").$type<BackoffPolicy>(),
|
|
57
|
+
keep: jsonb("keep").$type<KeepPolicy>(),
|
|
58
|
+
runAt: timestamp("run_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
59
|
+
enqueuedAt: timestamp("enqueued_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
60
|
+
processedAt: timestamp("processed_at", { withTimezone: true, mode: "date" }),
|
|
61
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }),
|
|
62
|
+
exit: jsonb("exit"),
|
|
63
|
+
failedReason: text("failed_reason"),
|
|
64
|
+
lockToken: text("lock_token"),
|
|
65
|
+
lockExpiresAt: timestamp("lock_expires_at", { withTimezone: true, mode: "date" })
|
|
66
|
+
}, (table) => [
|
|
67
|
+
// Claim path: pop highest priority, FIFO within it.
|
|
68
|
+
index(`${tableName}_ready_idx`)
|
|
69
|
+
.on(table.queue, table.priority.desc(), table.seq.asc())
|
|
70
|
+
.where(sql`${table.state} = 'waiting'`),
|
|
71
|
+
// Delayed promotion + nextRunAt.
|
|
72
|
+
index(`${tableName}_delayed_idx`)
|
|
73
|
+
.on(table.queue, table.runAt)
|
|
74
|
+
.where(sql`${table.state} = 'delayed'`),
|
|
75
|
+
// Stalled sweep.
|
|
76
|
+
index(`${tableName}_active_idx`)
|
|
77
|
+
.on(table.lockExpiresAt)
|
|
78
|
+
.where(sql`${table.state} = 'active'`),
|
|
79
|
+
// History/retention queries.
|
|
80
|
+
index(`${tableName}_history_idx`).on(table.name, table.state, table.finishedAt),
|
|
81
|
+
// Listing (newest first, keyset pagination).
|
|
82
|
+
index(`${tableName}_listing_idx`).on(table.enqueuedAt.desc(), table.id.desc()),
|
|
83
|
+
// Metadata containment queries.
|
|
84
|
+
index(`${tableName}_metadata_idx`).using("gin", table.metadata.op("jsonb_path_ops"))
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The job run-ledger table factory (one row per attempt, including
|
|
89
|
+
* successes and stall recoveries).
|
|
90
|
+
*
|
|
91
|
+
* @since 0.1.0
|
|
92
|
+
*/
|
|
93
|
+
export const mqJobAttempts = (
|
|
94
|
+
jobs: ReturnType<typeof mqJobs<any>>,
|
|
95
|
+
tableName = "effect_mq_job_attempts"
|
|
96
|
+
) =>
|
|
97
|
+
pgTable(tableName, {
|
|
98
|
+
jobId: text("job_id").notNull().references(() => jobs.id, { onDelete: "cascade" }).$type<JobId>(),
|
|
99
|
+
attempt: integer("attempt").notNull(),
|
|
100
|
+
outcome: text("outcome").notNull().$type<AttemptOutcome>(),
|
|
101
|
+
startedAt: timestamp("started_at", { withTimezone: true, mode: "date" }),
|
|
102
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
103
|
+
exit: jsonb("exit")
|
|
104
|
+
}, (table) => [
|
|
105
|
+
primaryKey({ columns: [table.jobId, table.attempt] })
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @since 0.1.0
|
|
110
|
+
*/
|
|
111
|
+
export type MqJobsTable = ReturnType<typeof mqJobs<any>>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @since 0.1.0
|
|
115
|
+
*/
|
|
116
|
+
export type MqJobAttemptsTable = ReturnType<typeof mqJobAttempts>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-native, storage-agnostic background jobs.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schema-first job definitions: `Job.make`, `enqueue`, `toLayer`.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*/
|
|
12
|
+
export * as Job from "./Job.ts"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The storage seam: the `JobStore` service, job records and typed errors.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
export * as JobStore from "./JobStore.ts"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The reference in-memory `JobStore` driver (Effect primitives only).
|
|
23
|
+
*
|
|
24
|
+
* @since 0.1.0
|
|
25
|
+
*/
|
|
26
|
+
export * as MemoryJobStore from "./MemoryJobStore.ts"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The worker runtime: `Worker.layer` and handler registration.
|
|
30
|
+
*
|
|
31
|
+
* @since 0.1.0
|
|
32
|
+
*/
|
|
33
|
+
export * as Worker from "./Worker.ts"
|