effect-mq 0.1.0 → 0.3.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/README.md +305 -25
- package/dist/Job.d.ts +118 -5
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +119 -4
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +260 -9
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +115 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +38 -6
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +351 -47
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts +5 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +117 -10
- package/dist/Worker.js.map +1 -1
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +35 -2
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
- package/dist/drizzle-postgres/DrizzleJobStore.js +941 -0
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle-postgres/index.d.ts.map +1 -0
- package/dist/drizzle-postgres/index.js.map +1 -0
- package/dist/drizzle-postgres/schema.d.ts +670 -0
- package/dist/drizzle-postgres/schema.d.ts.map +1 -0
- package/dist/drizzle-postgres/schema.js +150 -0
- package/dist/drizzle-postgres/schema.js.map +1 -0
- package/dist/redis/RedisJobStore.d.ts +58 -0
- package/dist/redis/RedisJobStore.d.ts.map +1 -0
- package/dist/redis/RedisJobStore.js +424 -0
- package/dist/redis/RedisJobStore.js.map +1 -0
- package/dist/redis/index.d.ts +9 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +9 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/scripts.d.ts +181 -0
- package/dist/redis/scripts.d.ts.map +1 -0
- package/dist/redis/scripts.js +940 -0
- package/dist/redis/scripts.js.map +1 -0
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +502 -8
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +8 -4
- package/src/Job.ts +301 -10
- package/src/JobStore.ts +373 -9
- package/src/MemoryJobStore.ts +440 -53
- package/src/Worker.ts +153 -10
- package/src/drizzle-postgres/DrizzleJobStore.ts +1311 -0
- package/src/drizzle-postgres/schema.ts +279 -0
- package/src/redis/RedisJobStore.ts +652 -0
- package/src/redis/index.ts +8 -0
- package/src/redis/scripts.ts +1055 -0
- package/src/testing/conformance.ts +665 -8
- package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
- package/dist/drizzle/DrizzleJobStore.js +0 -426
- package/dist/drizzle/DrizzleJobStore.js.map +0 -1
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/schema.d.ts +0 -464
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js +0 -68
- package/dist/drizzle/schema.js.map +0 -1
- package/src/drizzle/DrizzleJobStore.ts +0 -599
- package/src/drizzle/schema.ts +0 -116
- /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
- /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
- /package/src/{drizzle → drizzle-postgres}/index.ts +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
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-postgres"
|
|
11
|
+
*
|
|
12
|
+
* type DurableJobs = typeof GenerateInvoice._tag | typeof GenerateReport._tag
|
|
13
|
+
* export const jobs = mqJobs<DurableJobs>()
|
|
14
|
+
* export const jobAttempts = mqJobAttempts(jobs)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Every factory accepts an `extraConfig` callback — the same shape as
|
|
18
|
+
* drizzle's own third `pgTable` argument — to add your own indexes (or
|
|
19
|
+
* checks/policies) on top of the built-in ones:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* export const jobs = mqJobs<DurableJobs>("effect_mq_jobs", {
|
|
23
|
+
* extraConfig: (t) => [index("jobs_name_recent_idx").on(t.name, t.enqueuedAt.desc())]
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Then `drizzle-kit generate` emits the CREATE TABLE migrations into your
|
|
28
|
+
* pipeline like any other table. Reads through drizzle are encouraged;
|
|
29
|
+
* writes must go through the `JobStore` (e.g. `store.retry`) so locking and
|
|
30
|
+
* wake-up invariants hold.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
*/
|
|
34
|
+
import type * as JobStore from "../JobStore.ts"
|
|
35
|
+
import { sql } from "drizzle-orm"
|
|
36
|
+
import {
|
|
37
|
+
type AnyPgColumnBuilder,
|
|
38
|
+
bigint,
|
|
39
|
+
boolean,
|
|
40
|
+
index,
|
|
41
|
+
integer,
|
|
42
|
+
jsonb,
|
|
43
|
+
type PgBuildExtraConfigColumns,
|
|
44
|
+
pgTable,
|
|
45
|
+
type PgTableExtraConfigValue,
|
|
46
|
+
primaryKey,
|
|
47
|
+
text,
|
|
48
|
+
timestamp
|
|
49
|
+
} from "drizzle-orm/pg-core"
|
|
50
|
+
|
|
51
|
+
type JobId = JobStore.JobId
|
|
52
|
+
type QueueName = JobStore.QueueName
|
|
53
|
+
type ScheduleKey = JobStore.ScheduleKey
|
|
54
|
+
type JobState = JobStore.JobState
|
|
55
|
+
type BackoffPolicy = JobStore.BackoffPolicy
|
|
56
|
+
type KeepPolicy = JobStore.KeepPolicy
|
|
57
|
+
type AttemptOutcome = JobStore.AttemptRecord["outcome"]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Table-factory options: `extraConfig` receives the table's columns (exactly
|
|
61
|
+
* like drizzle's third `pgTable` argument) and returns additional indexes,
|
|
62
|
+
* checks, or policies, appended after the built-in ones.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.2.1
|
|
65
|
+
*/
|
|
66
|
+
export interface MqTableOptions<Columns extends Record<string, AnyPgColumnBuilder>> {
|
|
67
|
+
readonly extraConfig?:
|
|
68
|
+
| ((table: PgBuildExtraConfigColumns<Columns>) => Array<PgTableExtraConfigValue>)
|
|
69
|
+
| undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const jobsColumns = <JobName extends string>() => ({
|
|
73
|
+
id: text("id").primaryKey().$type<JobId>(),
|
|
74
|
+
name: text("name").notNull().$type<JobName>(),
|
|
75
|
+
queue: text("queue").notNull().$type<QueueName>(),
|
|
76
|
+
state: text("state").notNull().$type<JobState>(),
|
|
77
|
+
priority: integer("priority").notNull().default(0),
|
|
78
|
+
/** FIFO order within a priority; bumped on retry so retries go to the tail. */
|
|
79
|
+
seq: bigint("seq", { mode: "number" }).notNull().generatedByDefaultAsIdentity(),
|
|
80
|
+
payload: jsonb("payload"),
|
|
81
|
+
metadata: jsonb("metadata").notNull().default({}).$type<Record<string, string>>(),
|
|
82
|
+
attemptsMax: integer("attempts_max").notNull(),
|
|
83
|
+
attemptsMade: integer("attempts_made").notNull().default(0),
|
|
84
|
+
stalledCount: integer("stalled_count").notNull().default(0),
|
|
85
|
+
backoff: jsonb("backoff").$type<BackoffPolicy>(),
|
|
86
|
+
keep: jsonb("keep").$type<KeepPolicy>(),
|
|
87
|
+
timeoutMs: bigint("timeout_ms", { mode: "number" }),
|
|
88
|
+
cancelRequested: boolean("cancel_requested").notNull().default(false),
|
|
89
|
+
dedupeKey: text("dedupe_key"),
|
|
90
|
+
runAt: timestamp("run_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
91
|
+
enqueuedAt: timestamp("enqueued_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
92
|
+
processedAt: timestamp("processed_at", { withTimezone: true, mode: "date" }),
|
|
93
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }),
|
|
94
|
+
exit: jsonb("exit"),
|
|
95
|
+
failedReason: text("failed_reason"),
|
|
96
|
+
lockToken: text("lock_token"),
|
|
97
|
+
lockExpiresAt: timestamp("lock_expires_at", { withTimezone: true, mode: "date" })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The jobs table factory. `JobName` types the `name` column — derive it from
|
|
102
|
+
* your job definitions: `mqJobs<typeof GenerateInvoice._tag | typeof Report._tag>()`.
|
|
103
|
+
*
|
|
104
|
+
* `extend` adds your own columns (tenant ids, object ids, ...) to the table.
|
|
105
|
+
* At enqueue the Postgres store fills each extended column from the job's
|
|
106
|
+
* `metadata` entry with the same TS key (NULL when absent); override the
|
|
107
|
+
* mapping with the store's `extraValues` option. Extended columns are yours
|
|
108
|
+
* to read with plain drizzle queries — FKs, RLS policies, and `extraConfig`
|
|
109
|
+
* indexes all work:
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* export const jobs = mqJobs<JobNames>("effect_mq_jobs", {
|
|
113
|
+
* extend: {
|
|
114
|
+
* companyId: text("company_id").notNull(),
|
|
115
|
+
* objectId: text("object_id")
|
|
116
|
+
* },
|
|
117
|
+
* extraConfig: (t) => [index("jobs_company_idx").on(t.companyId, t.state)]
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @since 0.1.0
|
|
122
|
+
*/
|
|
123
|
+
export const mqJobs = <
|
|
124
|
+
JobName extends string = string,
|
|
125
|
+
Extend extends Record<string, AnyPgColumnBuilder> = Record<never, never>
|
|
126
|
+
>(
|
|
127
|
+
tableName = "effect_mq_jobs",
|
|
128
|
+
options?: MqTableOptions<ReturnType<typeof jobsColumns<JobName>> & Extend> & {
|
|
129
|
+
/** Extra columns appended to the factory's own (see the JSDoc example). */
|
|
130
|
+
readonly extend?: Extend | undefined
|
|
131
|
+
}
|
|
132
|
+
) =>
|
|
133
|
+
pgTable(tableName, {
|
|
134
|
+
...jobsColumns<JobName>(),
|
|
135
|
+
// SAFETY: when `extend` is absent, `Extend` was never inferred from a
|
|
136
|
+
// value and stays at its empty-record default, which {} satisfies.
|
|
137
|
+
...options?.extend ?? ({} as Extend)
|
|
138
|
+
}, (table) => [
|
|
139
|
+
// Claim path: pop highest priority, FIFO within it.
|
|
140
|
+
index(`${tableName}_ready_idx`)
|
|
141
|
+
.on(table.queue, table.priority.desc(), table.seq.asc())
|
|
142
|
+
.where(sql`${table.state} = 'waiting'`),
|
|
143
|
+
// Delayed promotion + nextRunAt.
|
|
144
|
+
index(`${tableName}_delayed_idx`)
|
|
145
|
+
.on(table.queue, table.runAt)
|
|
146
|
+
.where(sql`${table.state} = 'delayed'`),
|
|
147
|
+
// Stalled sweep.
|
|
148
|
+
index(`${tableName}_active_idx`)
|
|
149
|
+
.on(table.lockExpiresAt)
|
|
150
|
+
.where(sql`${table.state} = 'active'`),
|
|
151
|
+
// History/retention queries (leading `name` also serves name-only filters).
|
|
152
|
+
index(`${tableName}_history_idx`).on(table.name, table.state, table.finishedAt),
|
|
153
|
+
// Listing (newest first, keyset pagination).
|
|
154
|
+
index(`${tableName}_listing_idx`).on(table.enqueuedAt.desc(), table.id.desc()),
|
|
155
|
+
// Metadata containment queries.
|
|
156
|
+
index(`${tableName}_metadata_idx`).using("gin", table.metadata.op("jsonb_path_ops")),
|
|
157
|
+
...options?.extraConfig?.(table) ?? []
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
const attemptsColumns = (jobs: MqJobsTable) => ({
|
|
161
|
+
jobId: text("job_id").notNull().references(() => jobs.id, { onDelete: "cascade" }).$type<JobId>(),
|
|
162
|
+
attempt: integer("attempt").notNull(),
|
|
163
|
+
outcome: text("outcome").notNull().$type<AttemptOutcome>(),
|
|
164
|
+
startedAt: timestamp("started_at", { withTimezone: true, mode: "date" }),
|
|
165
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }).notNull(),
|
|
166
|
+
exit: jsonb("exit")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The job run-ledger table factory (one row per attempt, including
|
|
171
|
+
* successes and stall recoveries).
|
|
172
|
+
*
|
|
173
|
+
* @since 0.1.0
|
|
174
|
+
*/
|
|
175
|
+
export const mqJobAttempts = (
|
|
176
|
+
jobs: ReturnType<typeof mqJobs<any>>,
|
|
177
|
+
tableName = "effect_mq_job_attempts",
|
|
178
|
+
options?: MqTableOptions<ReturnType<typeof attemptsColumns>>
|
|
179
|
+
) =>
|
|
180
|
+
pgTable(tableName, attemptsColumns(jobs), (table) => [
|
|
181
|
+
primaryKey({ columns: [table.jobId, table.attempt] }),
|
|
182
|
+
...options?.extraConfig?.(table) ?? []
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
const schedulesColumns = () => ({
|
|
186
|
+
key: text("key").primaryKey().$type<ScheduleKey>(),
|
|
187
|
+
jobName: text("job_name").notNull(),
|
|
188
|
+
queue: text("queue").notNull().$type<QueueName>(),
|
|
189
|
+
cron: text("cron"),
|
|
190
|
+
tz: text("tz"),
|
|
191
|
+
everyMs: bigint("every_ms", { mode: "number" }),
|
|
192
|
+
payload: jsonb("payload"),
|
|
193
|
+
metadata: jsonb("metadata").notNull().default({}).$type<Record<string, string>>(),
|
|
194
|
+
priority: integer("priority").notNull().default(0),
|
|
195
|
+
attemptsMax: integer("attempts_max").notNull(),
|
|
196
|
+
backoff: jsonb("backoff").$type<BackoffPolicy>(),
|
|
197
|
+
keep: jsonb("keep").$type<KeepPolicy>(),
|
|
198
|
+
timeoutMs: bigint("timeout_ms", { mode: "number" }),
|
|
199
|
+
nextRunAt: timestamp("next_run_at", { withTimezone: true, mode: "date" }).notNull()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Repeatable-job schedules (one row per `Job.schedule` key).
|
|
204
|
+
*
|
|
205
|
+
* @since 0.2.0
|
|
206
|
+
*/
|
|
207
|
+
export const mqSchedules = (
|
|
208
|
+
tableName = "effect_mq_schedules",
|
|
209
|
+
options?: MqTableOptions<ReturnType<typeof schedulesColumns>>
|
|
210
|
+
) =>
|
|
211
|
+
pgTable(tableName, schedulesColumns(), (table) => [
|
|
212
|
+
index(`${tableName}_due_idx`).on(table.nextRunAt),
|
|
213
|
+
...options?.extraConfig?.(table) ?? []
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
const dedupeColumns = () => ({
|
|
217
|
+
name: text("name").notNull(),
|
|
218
|
+
key: text("key").notNull(),
|
|
219
|
+
jobId: text("job_id").notNull().$type<JobId>(),
|
|
220
|
+
/** Set for ttl/throttle windows; NULL rows live as long as their job is pending. */
|
|
221
|
+
windowExpiresAt: timestamp("window_expires_at", { withTimezone: true, mode: "date" })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Dedup-key registry (one row per job name + dedup key; see `DedupePolicy`).
|
|
226
|
+
*
|
|
227
|
+
* @since 0.3.0
|
|
228
|
+
*/
|
|
229
|
+
export const mqDedupe = (
|
|
230
|
+
tableName = "effect_mq_dedupe",
|
|
231
|
+
options?: MqTableOptions<ReturnType<typeof dedupeColumns>>
|
|
232
|
+
) =>
|
|
233
|
+
pgTable(tableName, dedupeColumns(), (table) => [
|
|
234
|
+
primaryKey({ columns: [table.name, table.key] }),
|
|
235
|
+
...options?.extraConfig?.(table) ?? []
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
const queueControlColumns = () => ({
|
|
239
|
+
queue: text("queue").primaryKey().$type<QueueName>(),
|
|
240
|
+
paused: boolean("paused").notNull().default(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Durable queue control flags (pause/resume).
|
|
245
|
+
*
|
|
246
|
+
* @since 0.2.0
|
|
247
|
+
*/
|
|
248
|
+
export const mqQueueControl = (
|
|
249
|
+
tableName = "effect_mq_queue_control",
|
|
250
|
+
options?: MqTableOptions<ReturnType<typeof queueControlColumns>>
|
|
251
|
+
) =>
|
|
252
|
+
pgTable(tableName, queueControlColumns(), (table) => [
|
|
253
|
+
...options?.extraConfig?.(table) ?? []
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @since 0.1.0
|
|
258
|
+
*/
|
|
259
|
+
export type MqJobsTable = ReturnType<typeof mqJobs<any>>
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @since 0.1.0
|
|
263
|
+
*/
|
|
264
|
+
export type MqJobAttemptsTable = ReturnType<typeof mqJobAttempts>
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @since 0.2.0
|
|
268
|
+
*/
|
|
269
|
+
export type MqSchedulesTable = ReturnType<typeof mqSchedules>
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @since 0.2.0
|
|
273
|
+
*/
|
|
274
|
+
export type MqQueueControlTable = ReturnType<typeof mqQueueControl>
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @since 0.3.0
|
|
278
|
+
*/
|
|
279
|
+
export type MqDedupeTable = ReturnType<typeof mqDedupe>
|