@dudousxd/nestjs-durable-store-drizzle 0.16.0 → 0.18.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.
@@ -0,0 +1,885 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/pg/ddl.ts
5
+ import { sql } from "drizzle-orm";
6
+ var DURABLE_PG_DDL = [
7
+ `CREATE TABLE IF NOT EXISTS "durable_workflow_runs" (
8
+ "id" text PRIMARY KEY NOT NULL,
9
+ "workflow" text NOT NULL,
10
+ "workflow_version" text NOT NULL,
11
+ "status" text NOT NULL,
12
+ "input" jsonb,
13
+ "output" jsonb,
14
+ "error" jsonb,
15
+ "wake_at" timestamp with time zone,
16
+ "locked_by" text,
17
+ "locked_until" timestamp with time zone,
18
+ "awaiting_decision_task_id" text,
19
+ "recovery_attempts" integer,
20
+ "tags" jsonb,
21
+ "search_attributes" jsonb,
22
+ "priority" integer,
23
+ "namespace" text DEFAULT 'default' NOT NULL,
24
+ "origin" text,
25
+ "created_at" timestamp with time zone NOT NULL,
26
+ "updated_at" timestamp with time zone NOT NULL
27
+ )`,
28
+ `CREATE TABLE IF NOT EXISTS "durable_step_checkpoints" (
29
+ "run_id" text NOT NULL,
30
+ "seq" integer NOT NULL,
31
+ "name" text NOT NULL,
32
+ "kind" text NOT NULL,
33
+ "step_id" text NOT NULL,
34
+ "status" text NOT NULL,
35
+ "input" jsonb,
36
+ "output" jsonb,
37
+ "error" jsonb,
38
+ "events" jsonb,
39
+ "attempts" integer NOT NULL,
40
+ "worker_group" text,
41
+ "parallel_group" text,
42
+ "wake_at" timestamp with time zone,
43
+ "enqueued_at" timestamp with time zone,
44
+ "started_at" timestamp with time zone NOT NULL,
45
+ "finished_at" timestamp with time zone NOT NULL,
46
+ CONSTRAINT "durable_step_checkpoints_run_id_seq_pk" PRIMARY KEY("run_id","seq")
47
+ )`,
48
+ `CREATE TABLE IF NOT EXISTS "durable_run_attributes" (
49
+ "run_id" text NOT NULL,
50
+ "key" text NOT NULL,
51
+ "str_value" text,
52
+ "num_value" double precision,
53
+ CONSTRAINT "durable_run_attributes_run_id_key_pk" PRIMARY KEY("run_id","key")
54
+ )`,
55
+ `CREATE TABLE IF NOT EXISTS "durable_signal_waiters" (
56
+ "token" text PRIMARY KEY NOT NULL,
57
+ "run_id" text NOT NULL,
58
+ "seq" integer NOT NULL,
59
+ "parallel_group" text
60
+ )`,
61
+ `CREATE TABLE IF NOT EXISTS "durable_buffered_signals" (
62
+ "id" bigserial PRIMARY KEY NOT NULL,
63
+ "token" text NOT NULL,
64
+ "payload" jsonb
65
+ )`,
66
+ `CREATE TABLE IF NOT EXISTS "durable_buffered_events" (
67
+ "id" text PRIMARY KEY NOT NULL,
68
+ "name" text NOT NULL,
69
+ "payload" jsonb,
70
+ "published_at" timestamp with time zone NOT NULL
71
+ )`,
72
+ `CREATE TABLE IF NOT EXISTS "durable_schedules" (
73
+ "id" text PRIMARY KEY NOT NULL,
74
+ "namespace" text DEFAULT 'default' NOT NULL,
75
+ "workflow" text NOT NULL,
76
+ "paused" boolean DEFAULT false NOT NULL,
77
+ "next_fire_at" timestamp with time zone,
78
+ "tags" jsonb,
79
+ "spec" jsonb NOT NULL,
80
+ "state" jsonb NOT NULL,
81
+ "created_at" timestamp with time zone NOT NULL,
82
+ "updated_at" timestamp with time zone NOT NULL
83
+ )`,
84
+ `CREATE INDEX IF NOT EXISTS "durable_runs_status_idx" ON "durable_workflow_runs" USING btree ("status","wake_at")`,
85
+ `CREATE INDEX IF NOT EXISTS "durable_runs_workflow_status_idx" ON "durable_workflow_runs" USING btree ("workflow","status")`,
86
+ `CREATE INDEX IF NOT EXISTS "durable_workflow_runs_namespace_status_idx" ON "durable_workflow_runs" USING btree ("namespace","status","created_at")`,
87
+ `CREATE INDEX IF NOT EXISTS "durable_runs_tags_gin_idx" ON "durable_workflow_runs" USING gin ("tags" jsonb_path_ops)`,
88
+ `CREATE INDEX IF NOT EXISTS "durable_runs_status_updated_idx" ON "durable_workflow_runs" USING btree ("status","updated_at")`,
89
+ `CREATE INDEX IF NOT EXISTS "durable_run_attributes_num_idx" ON "durable_run_attributes" USING btree ("key","num_value")`,
90
+ `CREATE INDEX IF NOT EXISTS "durable_run_attributes_str_idx" ON "durable_run_attributes" USING btree ("key","str_value")`,
91
+ `CREATE INDEX IF NOT EXISTS "durable_signal_waiters_run_id_idx" ON "durable_signal_waiters" USING btree ("run_id")`,
92
+ `CREATE INDEX IF NOT EXISTS "durable_buffered_signals_token_idx" ON "durable_buffered_signals" USING btree ("token","id")`,
93
+ `CREATE INDEX IF NOT EXISTS "durable_buffered_events_name_published_at_idx" ON "durable_buffered_events" USING btree ("name","published_at")`,
94
+ `CREATE INDEX IF NOT EXISTS "durable_schedules_due_idx" ON "durable_schedules" USING btree ("paused","next_fire_at")`
95
+ ];
96
+ var SCHEMA_LOCK_KEY = "durable_schema";
97
+ async function ensureDrizzlePgDurableSchema(db) {
98
+ await db.transaction(async (tx) => {
99
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${SCHEMA_LOCK_KEY}))`);
100
+ for (const statement of DURABLE_PG_DDL) await tx.execute(sql.raw(statement));
101
+ });
102
+ }
103
+ __name(ensureDrizzlePgDurableSchema, "ensureDrizzlePgDurableSchema");
104
+
105
+ // src/pg/drizzle-pg-state-store.ts
106
+ import { RUN_VALUE_FACET_SCAN, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns } from "@dudousxd/nestjs-durable-core";
107
+ import { and, asc, desc, eq, exists, inArray, isNotNull, isNull, like, lt, lte, or, sql as sql2 } from "drizzle-orm";
108
+
109
+ // src/pg/schema.ts
110
+ import { getTableName } from "drizzle-orm";
111
+ import { bigserial, boolean, doublePrecision, index, integer, jsonb, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
112
+ var DEFAULT_NAMESPACE = "default";
113
+ var tstz = /* @__PURE__ */ __name((name) => timestamp(name, {
114
+ withTimezone: true,
115
+ mode: "date"
116
+ }), "tstz");
117
+ var durableWorkflowRuns = pgTable("durable_workflow_runs", {
118
+ id: text("id").primaryKey(),
119
+ workflow: text("workflow").notNull(),
120
+ workflowVersion: text("workflow_version").notNull(),
121
+ status: text("status").notNull(),
122
+ input: jsonb("input"),
123
+ output: jsonb("output"),
124
+ error: jsonb("error"),
125
+ wakeAt: tstz("wake_at"),
126
+ lockedBy: text("locked_by"),
127
+ lockedUntil: tstz("locked_until"),
128
+ // REMOTE turn the engine suspended on awaiting a decision; matched by completeRemoteDecision so
129
+ // only the currently-awaited turn's decision is applied. Nullable: cleared when a decision lands.
130
+ awaitingDecisionTaskId: text("awaiting_decision_task_id"),
131
+ recoveryAttempts: integer("recovery_attempts"),
132
+ tags: jsonb("tags").$type(),
133
+ searchAttributes: jsonb("search_attributes").$type(),
134
+ priority: integer("priority"),
135
+ // The worker-pool partition (tenant) — THE isolation boundary every poll predicate filters on.
136
+ // NOT NULL DEFAULT 'default' so a row can never fall outside every worker's namespace.
137
+ namespace: text("namespace").notNull().default(DEFAULT_NAMESPACE),
138
+ // Which package declared the workflow. Nullable, no default: NULL = unknown, never guessed.
139
+ origin: text("origin"),
140
+ createdAt: tstz("created_at").notNull(),
141
+ updatedAt: tstz("updated_at").notNull()
142
+ }, (t) => [
143
+ // Timer poller (`listDueTimers`) + recovery (`listIncompleteRuns`). Same name as the TypeORM DDL.
144
+ index("durable_runs_status_idx").on(t.status, t.wakeAt),
145
+ // Dashboard `listRuns({ workflow, status })` and the singleton gate's per-workflow scans.
146
+ index("durable_runs_workflow_status_idx").on(t.workflow, t.status),
147
+ // Every namespaced poll tick: `namespace = $1 AND status = $2 ORDER BY created_at` (FIFO pick-up).
148
+ // Name matches the MikroORM adapter + the SQLite drizzle schema so a store swap keeps the index.
149
+ index("durable_workflow_runs_namespace_status_idx").on(t.namespace, t.status, t.createdAt),
150
+ // `listRuns({ tag })` compiles to `tags @> '["tag"]'`, which a `jsonb_path_ops` GIN index serves —
151
+ // this is what keeps the singleton gate (`tag: singleton:<key>`, e.g. a per-tenant concurrency
152
+ // key) an index probe instead of a scan of every run the workflow ever had.
153
+ index("durable_runs_tags_gin_idx").using("gin", t.tags.op("jsonb_path_ops")),
154
+ // Retention (`pruneTerminalRuns`) orders a status set by `updated_at`.
155
+ index("durable_runs_status_updated_idx").on(t.status, t.updatedAt)
156
+ ]);
157
+ var durableStepCheckpoints = pgTable(
158
+ "durable_step_checkpoints",
159
+ {
160
+ runId: text("run_id").notNull(),
161
+ seq: integer("seq").notNull(),
162
+ name: text("name").notNull(),
163
+ kind: text("kind").notNull(),
164
+ stepId: text("step_id").notNull(),
165
+ status: text("status").notNull(),
166
+ input: jsonb("input"),
167
+ output: jsonb("output"),
168
+ error: jsonb("error"),
169
+ events: jsonb("events"),
170
+ attempts: integer("attempts").notNull(),
171
+ workerGroup: text("worker_group"),
172
+ parallelGroup: text("parallel_group"),
173
+ wakeAt: tstz("wake_at"),
174
+ enqueuedAt: tstz("enqueued_at"),
175
+ startedAt: tstz("started_at").notNull(),
176
+ finishedAt: tstz("finished_at").notNull()
177
+ },
178
+ // (run_id, seq) is the primary key, so listCheckpoints / getEvent already hit an index.
179
+ (t) => [
180
+ primaryKey({
181
+ columns: [
182
+ t.runId,
183
+ t.seq
184
+ ]
185
+ })
186
+ ]
187
+ );
188
+ var durableRunAttributes = pgTable("durable_run_attributes", {
189
+ runId: text("run_id").notNull(),
190
+ key: text("key").notNull(),
191
+ strValue: text("str_value"),
192
+ numValue: doublePrecision("num_value")
193
+ }, (t) => [
194
+ primaryKey({
195
+ columns: [
196
+ t.runId,
197
+ t.key
198
+ ]
199
+ }),
200
+ index("durable_run_attributes_num_idx").on(t.key, t.numValue),
201
+ index("durable_run_attributes_str_idx").on(t.key, t.strValue)
202
+ ]);
203
+ var durableSignalWaiters = pgTable(
204
+ "durable_signal_waiters",
205
+ {
206
+ token: text("token").primaryKey(),
207
+ runId: text("run_id").notNull(),
208
+ seq: integer("seq").notNull(),
209
+ parallelGroup: text("parallel_group")
210
+ },
211
+ // deleteRun / pruneTerminalRuns delete a run's waiters by run_id.
212
+ (t) => [
213
+ index("durable_signal_waiters_run_id_idx").on(t.runId)
214
+ ]
215
+ );
216
+ var durableBufferedSignals = pgTable(
217
+ "durable_buffered_signals",
218
+ {
219
+ id: bigserial("id", {
220
+ mode: "number"
221
+ }).primaryKey(),
222
+ token: text("token").notNull(),
223
+ payload: jsonb("payload")
224
+ },
225
+ // Taken FIFO per token (smallest id first).
226
+ (t) => [
227
+ index("durable_buffered_signals_token_idx").on(t.token, t.id)
228
+ ]
229
+ );
230
+ var durableBufferedEvents = pgTable("durable_buffered_events", {
231
+ id: text("id").primaryKey(),
232
+ name: text("name").notNull(),
233
+ payload: jsonb("payload"),
234
+ publishedAt: tstz("published_at").notNull()
235
+ }, (t) => [
236
+ index("durable_buffered_events_name_published_at_idx").on(t.name, t.publishedAt)
237
+ ]);
238
+ var durableSchedules = pgTable(
239
+ "durable_schedules",
240
+ {
241
+ id: text("id").primaryKey(),
242
+ namespace: text("namespace").notNull().default(DEFAULT_NAMESPACE),
243
+ workflow: text("workflow").notNull(),
244
+ paused: boolean("paused").notNull().default(false),
245
+ nextFireAt: tstz("next_fire_at"),
246
+ tags: jsonb("tags").$type(),
247
+ spec: jsonb("spec").$type().notNull(),
248
+ state: jsonb("state").$type().notNull(),
249
+ createdAt: tstz("created_at").notNull(),
250
+ updatedAt: tstz("updated_at").notNull()
251
+ },
252
+ // The poll's "due by T" scan: `paused = false AND next_fire_at <= T ORDER BY next_fire_at`.
253
+ (t) => [
254
+ index("durable_schedules_due_idx").on(t.paused, t.nextFireAt)
255
+ ]
256
+ );
257
+ var durablePgSchema = {
258
+ durableWorkflowRuns,
259
+ durableStepCheckpoints,
260
+ durableRunAttributes,
261
+ durableSignalWaiters,
262
+ durableBufferedSignals,
263
+ durableBufferedEvents,
264
+ durableSchedules
265
+ };
266
+ function durablePgManagedTables() {
267
+ return Object.values(durablePgSchema).map((table) => getTableName(table));
268
+ }
269
+ __name(durablePgManagedTables, "durablePgManagedTables");
270
+
271
+ // src/pg/drizzle-pg-state-store.ts
272
+ function anyOf(conditions) {
273
+ const present = conditions.filter((c) => c !== void 0);
274
+ return present.length ? or(...present) : sql2`1 = 0`;
275
+ }
276
+ __name(anyOf, "anyOf");
277
+ function namespaceFilter(namespace) {
278
+ return namespace === void 0 ? void 0 : eq(durableWorkflowRuns.namespace, namespace);
279
+ }
280
+ __name(namespaceFilter, "namespaceFilter");
281
+ function hasTag(tag) {
282
+ return sql2`${durableWorkflowRuns.tags} @> ${JSON.stringify([
283
+ tag
284
+ ])}::jsonb`;
285
+ }
286
+ __name(hasTag, "hasTag");
287
+ var date = /* @__PURE__ */ __name((ms2) => ms2 == null ? null : new Date(ms2), "date");
288
+ var ms = /* @__PURE__ */ __name((d) => d == null ? void 0 : d.getTime(), "ms");
289
+ var DrizzlePgStateStore = class _DrizzlePgStateStore {
290
+ static {
291
+ __name(this, "DrizzlePgStateStore");
292
+ }
293
+ db;
294
+ options;
295
+ scopeNamespace;
296
+ constructor(db, options = {}) {
297
+ this.db = db;
298
+ this.options = options;
299
+ this.scopeNamespace = options.scope?.namespace;
300
+ }
301
+ /** A store confined to `scope.namespace`, sharing this store's db (no new connection). Used by the
302
+ * NestJS module's `scopeReads`; `{ namespace: undefined }` is the unscoped operator view. */
303
+ withScope(scope) {
304
+ return new _DrizzlePgStateStore(this.db, {
305
+ ...this.options,
306
+ scope
307
+ });
308
+ }
309
+ /** Idempotent CREATE TABLE / INDEX IF NOT EXISTS under an advisory lock — called on boot when the
310
+ * module's `autoSchema` is on. Turn `autoSchema` off if drizzle-kit owns these tables. */
311
+ async ensureSchema() {
312
+ await ensureDrizzlePgDurableSchema(this.db);
313
+ }
314
+ /** The read-scope predicate on the runs table (undefined when the store is unscoped). */
315
+ scope() {
316
+ return namespaceFilter(this.scopeNamespace);
317
+ }
318
+ async createRun(run) {
319
+ await this.db.transaction(async (tx) => {
320
+ await tx.insert(durableWorkflowRuns).values(toRunRow(run));
321
+ await reindexAttributes(tx, run.id, run.searchAttributes);
322
+ });
323
+ }
324
+ async updateRun(runId, patch) {
325
+ const row = toRunPatch(patch);
326
+ const reindex = "searchAttributes" in patch;
327
+ if (!Object.keys(row).length && !reindex) return;
328
+ await this.db.transaction(async (tx) => {
329
+ if (Object.keys(row).length) await tx.update(durableWorkflowRuns).set(row).where(eq(durableWorkflowRuns.id, runId));
330
+ if (reindex) await reindexAttributes(tx, runId, patch.searchAttributes);
331
+ });
332
+ }
333
+ async getRun(runId) {
334
+ const rows = await this.db.select().from(durableWorkflowRuns).where(and(eq(durableWorkflowRuns.id, runId), this.scope())).limit(1);
335
+ return rows[0] ? fromRunRow(rows[0]) : null;
336
+ }
337
+ async deleteRun(runId) {
338
+ await this.db.transaction(async (tx) => {
339
+ await deleteRunsCascade(tx, [
340
+ runId
341
+ ]);
342
+ });
343
+ }
344
+ async deleteRuns(runIds) {
345
+ if (runIds.length === 0) return;
346
+ await this.db.transaction(async (tx) => {
347
+ await deleteRunsCascade(tx, runIds);
348
+ });
349
+ }
350
+ async pruneTerminalRuns(policy, nowMs, limit) {
351
+ const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
352
+ if (statuses.length === 0 || limit <= 0) return 0;
353
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
354
+ const scoped = this.runFilters({
355
+ ...policy.scope ?? {},
356
+ statuses
357
+ });
358
+ return this.db.transaction(async (tx) => {
359
+ const ids = /* @__PURE__ */ new Set();
360
+ if (policy.maxAge != null) {
361
+ const cutoff = new Date(nowMs - parseDuration(policy.maxAge));
362
+ const rows = await tx.select({
363
+ id: durableWorkflowRuns.id
364
+ }).from(durableWorkflowRuns).where(and(...scoped, lt(durableWorkflowRuns.updatedAt, cutoff))).orderBy(asc(durableWorkflowRuns.updatedAt)).limit(limit).for("update", {
365
+ skipLocked: true
366
+ });
367
+ for (const r of rows) ids.add(r.id);
368
+ }
369
+ if (policy.maxCount != null && ids.size < limit) {
370
+ const rows = await tx.select({
371
+ id: durableWorkflowRuns.id
372
+ }).from(durableWorkflowRuns).where(and(...scoped)).orderBy(desc(durableWorkflowRuns.updatedAt), desc(durableWorkflowRuns.id)).limit(limit).offset(policy.maxCount).for("update", {
373
+ skipLocked: true
374
+ });
375
+ for (const r of rows) {
376
+ if (ids.size >= limit) break;
377
+ ids.add(r.id);
378
+ }
379
+ }
380
+ if (ids.size === 0) return 0;
381
+ const idList = [
382
+ ...ids
383
+ ].slice(0, limit);
384
+ await deleteRunsCascade(tx, idList);
385
+ return idList.length;
386
+ });
387
+ }
388
+ async getCheckpoint(runId, seq) {
389
+ const rows = await this.db.select().from(durableStepCheckpoints).where(and(eq(durableStepCheckpoints.runId, runId), eq(durableStepCheckpoints.seq, seq))).limit(1);
390
+ return rows[0] ? fromCheckpointRow(rows[0]) : null;
391
+ }
392
+ async saveCheckpoint(checkpoint) {
393
+ await upsertCheckpoint(this.db, checkpoint);
394
+ }
395
+ async transaction(work) {
396
+ return this.db.transaction(async (tx) => work({
397
+ raw: tx,
398
+ saveCheckpoint: /* @__PURE__ */ __name((cp) => upsertCheckpoint(tx, cp), "saveCheckpoint")
399
+ }));
400
+ }
401
+ /** Crash recovery. Namespace-scoped: a worker re-drives only its own tenant's runs. */
402
+ async listIncompleteRuns(namespace) {
403
+ const rows = await this.db.select().from(durableWorkflowRuns).where(and(inArray(durableWorkflowRuns.status, [
404
+ "running",
405
+ "cancelling"
406
+ ]), namespaceFilter(namespace), this.scope()));
407
+ return rows.map(fromRunRow);
408
+ }
409
+ /** FIFO pick-up, served by `durable_workflow_runs_namespace_status_idx`. The engine then leases
410
+ * each run with {@link tryLockRun}; the listing itself takes no locks (it runs outside a tx). */
411
+ async listPendingRuns(limit, namespace) {
412
+ const rows = await this.db.select().from(durableWorkflowRuns).where(and(eq(durableWorkflowRuns.status, "pending"), namespaceFilter(namespace), this.scope())).orderBy(asc(durableWorkflowRuns.createdAt)).limit(limit);
413
+ return rows.map(fromRunRow);
414
+ }
415
+ async listDueTimers(nowMs, namespace) {
416
+ const rows = await this.db.select().from(durableWorkflowRuns).where(and(eq(durableWorkflowRuns.status, "suspended"), isNotNull(durableWorkflowRuns.wakeAt), lte(durableWorkflowRuns.wakeAt, new Date(nowMs)), namespaceFilter(namespace), this.scope()));
417
+ return rows.map(fromRunRow);
418
+ }
419
+ async tryLockRun(runId, owner, leaseUntilMs, nowMs) {
420
+ const rows = await this.db.update(durableWorkflowRuns).set({
421
+ lockedBy: owner,
422
+ lockedUntil: new Date(leaseUntilMs)
423
+ }).where(and(eq(durableWorkflowRuns.id, runId), or(isNull(durableWorkflowRuns.lockedUntil), lte(durableWorkflowRuns.lockedUntil, new Date(nowMs))))).returning({
424
+ id: durableWorkflowRuns.id
425
+ });
426
+ return rows.length === 1;
427
+ }
428
+ async releaseRunLock(runId) {
429
+ await this.db.update(durableWorkflowRuns).set({
430
+ lockedBy: null,
431
+ lockedUntil: null
432
+ }).where(eq(durableWorkflowRuns.id, runId));
433
+ }
434
+ async renewRunLock(runId, owner, leaseUntilMs) {
435
+ const rows = await this.db.update(durableWorkflowRuns).set({
436
+ lockedUntil: new Date(leaseUntilMs)
437
+ }).where(and(eq(durableWorkflowRuns.id, runId), eq(durableWorkflowRuns.lockedBy, owner))).returning({
438
+ id: durableWorkflowRuns.id
439
+ });
440
+ return rows.length === 1;
441
+ }
442
+ /** The predicates every run query shares — {@link listRuns} pages them, the facets group them. */
443
+ runFilters(query) {
444
+ return [
445
+ this.scope(),
446
+ query.workflow ? eq(durableWorkflowRuns.workflow, query.workflow) : void 0,
447
+ query.workflows ? query.workflows.length ? inArray(durableWorkflowRuns.workflow, query.workflows) : sql2`1 = 0` : void 0,
448
+ // The tenant boundary. Also the engine's execution-timeout sweep path
449
+ // (`listRuns({ workflow, status, namespace })`) — unscoped, it would cancel another tenant's runs.
450
+ namespaceFilter(query.namespace),
451
+ query.namespaces ? query.namespaces.length ? inArray(durableWorkflowRuns.namespace, query.namespaces) : sql2`1 = 0` : void 0,
452
+ // `null` selects the unattributed bucket; a value never matches a NULL origin.
453
+ query.origin === null ? isNull(durableWorkflowRuns.origin) : query.origin !== void 0 ? eq(durableWorkflowRuns.origin, query.origin) : void 0,
454
+ query.origins ? anyOf(query.origins.map((o) => o === null ? isNull(durableWorkflowRuns.origin) : eq(durableWorkflowRuns.origin, o))) : void 0,
455
+ query.status ? eq(durableWorkflowRuns.status, query.status) : void 0,
456
+ query.statuses ? query.statuses.length ? inArray(durableWorkflowRuns.status, query.statuses) : sql2`1 = 0` : void 0,
457
+ query.tag ? hasTag(query.tag) : void 0,
458
+ query.tags ? anyOf(query.tags.map(hasTag)) : void 0,
459
+ // One EXISTS per attribute predicate against the normalized side table (ANDed).
460
+ ...query.attributes?.map((f) => this.attributeExists(f)) ?? []
461
+ ].filter((f) => f !== void 0);
462
+ }
463
+ async listRuns(query) {
464
+ const filters = this.runFilters(query);
465
+ const base = this.db.select().from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0).orderBy(desc(durableWorkflowRuns.createdAt), desc(durableWorkflowRuns.id)).$dynamic();
466
+ if (query.limit != null) base.limit(query.limit);
467
+ if (query.offset != null) base.offset(query.offset);
468
+ const rows = await base;
469
+ return rows.map(fromRunRow);
470
+ }
471
+ async countRuns(query) {
472
+ const filters = this.runFilters(query);
473
+ const rows = await this.db.select({
474
+ count: sql2`count(*)`.mapWith(Number)
475
+ }).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0);
476
+ return rows[0]?.count ?? 0;
477
+ }
478
+ async runFacets(query) {
479
+ const filters = this.runFilters(query);
480
+ const rows = await this.db.select({
481
+ status: durableWorkflowRuns.status,
482
+ origin: durableWorkflowRuns.origin,
483
+ count: sql2`count(*)`.mapWith(Number)
484
+ }).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0).groupBy(durableWorkflowRuns.status, durableWorkflowRuns.origin);
485
+ return mergeRunFacetRows(rows.map((r) => ({
486
+ status: r.status,
487
+ origin: r.origin,
488
+ count: r.count
489
+ })));
490
+ }
491
+ /** Run-table axes are an exact `GROUP BY`; the tag / attribute axes are counted over a bounded
492
+ * page of runs (see `RunValueFacetOptions.scan`), like the TypeORM and SQLite Drizzle adapters. */
493
+ async runValueFacets(axis, query, opts) {
494
+ if (!axisIsRunColumn(axis)) {
495
+ const runs = await this.listRuns({
496
+ ...query,
497
+ limit: opts?.scan ?? RUN_VALUE_FACET_SCAN
498
+ });
499
+ return runValueFacetsFromRuns(runs, axis, opts);
500
+ }
501
+ const column = durableWorkflowRuns[axis.field];
502
+ const filters = this.runFilters(query);
503
+ const rows = await this.db.select({
504
+ value: column,
505
+ count: sql2`count(*)`.mapWith(Number)
506
+ }).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0).groupBy(column);
507
+ return mergeRunValueFacetRows(rows.map((r) => ({
508
+ value: r.value ?? null,
509
+ count: r.count
510
+ })), opts);
511
+ }
512
+ /** One attribute predicate as an EXISTS on the side table, correlated to the outer run. `ne` also
513
+ * excludes runs lacking the key (EXISTS requires the row). Numbers compare `num_value`, strings
514
+ * and booleans `str_value`; an `in` set ORs its members inside the one EXISTS. */
515
+ attributeExists(f) {
516
+ const comparisons = attributePredicateOperands(f).map(({ column, comparator, operand }) => sql2`${column === "numValue" ? durableRunAttributes.numValue : durableRunAttributes.strValue} ${sql2.raw(comparator)} ${operand}`);
517
+ return exists(this.db.select({
518
+ one: sql2`1`
519
+ }).from(durableRunAttributes).where(and(eq(durableRunAttributes.runId, durableWorkflowRuns.id), eq(durableRunAttributes.key, f.key), anyOf(comparisons))));
520
+ }
521
+ // ---- persisted schedules ----------------------------------------------------------------------
522
+ async saveSchedule(record) {
523
+ const row = toScheduleRow(record);
524
+ await this.db.insert(durableSchedules).values(row).onConflictDoUpdate({
525
+ target: durableSchedules.id,
526
+ set: row
527
+ });
528
+ }
529
+ async getSchedule(id) {
530
+ const rows = await this.db.select().from(durableSchedules).where(eq(durableSchedules.id, id)).limit(1);
531
+ return rows[0] ? fromScheduleRow(rows[0]) : null;
532
+ }
533
+ /** A single conditional `UPDATE … RETURNING`: the compare-and-set on `next_fire_at` is atomic. */
534
+ async updateSchedule(id, patch, expectedNextFireAt) {
535
+ const set = toSchedulePatch(patch);
536
+ if (!Object.keys(set).length) return await this.getSchedule(id) !== null;
537
+ const rows = await this.db.update(durableSchedules).set(set).where(and(eq(durableSchedules.id, id), expectedNextFireAt === void 0 ? void 0 : expectedNextFireAt === null ? isNull(durableSchedules.nextFireAt) : eq(durableSchedules.nextFireAt, new Date(expectedNextFireAt)))).returning({
538
+ id: durableSchedules.id
539
+ });
540
+ return rows.length > 0;
541
+ }
542
+ async deleteSchedule(id) {
543
+ const rows = await this.db.delete(durableSchedules).where(eq(durableSchedules.id, id)).returning({
544
+ id: durableSchedules.id
545
+ });
546
+ return rows.length > 0;
547
+ }
548
+ async listSchedules(query) {
549
+ const base = this.db.select().from(durableSchedules).where(and(query.namespace !== void 0 ? eq(durableSchedules.namespace, query.namespace) : void 0, query.workflow !== void 0 ? eq(durableSchedules.workflow, query.workflow) : void 0, query.paused !== void 0 ? eq(durableSchedules.paused, query.paused) : void 0, query.tag !== void 0 ? sql2`${durableSchedules.tags} @> ${JSON.stringify([
550
+ query.tag
551
+ ])}::jsonb` : void 0, query.dueBy !== void 0 ? and(eq(durableSchedules.paused, false), lte(durableSchedules.nextFireAt, new Date(query.dueBy))) : void 0)).orderBy(sql2`${durableSchedules.nextFireAt} ASC NULLS LAST`, asc(durableSchedules.id)).$dynamic();
552
+ if (query.limit != null) base.limit(query.limit);
553
+ if (query.offset != null) base.offset(query.offset);
554
+ return (await base).map(fromScheduleRow);
555
+ }
556
+ async listCheckpoints(runId) {
557
+ const rows = await this.db.select().from(durableStepCheckpoints).where(eq(durableStepCheckpoints.runId, runId)).orderBy(asc(durableStepCheckpoints.seq));
558
+ return rows.map(fromCheckpointRow);
559
+ }
560
+ async getLatestCheckpointByName(runId, name) {
561
+ const rows = await this.db.select().from(durableStepCheckpoints).where(and(eq(durableStepCheckpoints.runId, runId), eq(durableStepCheckpoints.name, name))).orderBy(desc(durableStepCheckpoints.seq)).limit(1);
562
+ return rows[0] ? fromCheckpointRow(rows[0]) : void 0;
563
+ }
564
+ async listCheckpointsByNamePrefix(runId, prefixes) {
565
+ if (prefixes.length === 0) return [];
566
+ const rows = await this.db.select().from(durableStepCheckpoints).where(and(eq(durableStepCheckpoints.runId, runId), or(...prefixes.map((p) => like(durableStepCheckpoints.name, `${escapeLike(p)}%`))))).orderBy(asc(durableStepCheckpoints.seq));
567
+ return rows.map(fromCheckpointRow);
568
+ }
569
+ async putSignalWaiter(waiter) {
570
+ const set = {
571
+ runId: waiter.runId,
572
+ seq: waiter.seq,
573
+ parallelGroup: waiter.parallelGroup ?? null
574
+ };
575
+ await this.db.insert(durableSignalWaiters).values({
576
+ token: waiter.token,
577
+ ...set
578
+ }).onConflictDoUpdate({
579
+ target: durableSignalWaiters.token,
580
+ set
581
+ });
582
+ }
583
+ async takeSignalWaiter(token) {
584
+ const rows = await this.db.delete(durableSignalWaiters).where(eq(durableSignalWaiters.token, token)).returning();
585
+ const row = rows[0];
586
+ return row ? fromWaiterRow(row) : null;
587
+ }
588
+ async listSignalWaiters(prefix) {
589
+ const rows = await this.db.select().from(durableSignalWaiters).where(like(durableSignalWaiters.token, `${escapeLike(prefix)}%`));
590
+ return rows.map(fromWaiterRow);
591
+ }
592
+ async removeSignalWaiter(waiter) {
593
+ await this.db.delete(durableSignalWaiters).where(and(eq(durableSignalWaiters.token, waiter.token), eq(durableSignalWaiters.runId, waiter.runId), eq(durableSignalWaiters.seq, waiter.seq)));
594
+ }
595
+ async bufferSignal(token, payload) {
596
+ await this.db.insert(durableBufferedSignals).values({
597
+ token,
598
+ payload: payload ?? null
599
+ });
600
+ }
601
+ async takeBufferedSignal(token) {
602
+ const oldest = this.db.select({
603
+ id: durableBufferedSignals.id
604
+ }).from(durableBufferedSignals).where(eq(durableBufferedSignals.token, token)).orderBy(asc(durableBufferedSignals.id)).limit(1).for("update", {
605
+ skipLocked: true
606
+ });
607
+ const rows = await this.db.delete(durableBufferedSignals).where(inArray(durableBufferedSignals.id, oldest)).returning({
608
+ payload: durableBufferedSignals.payload
609
+ });
610
+ const row = rows[0];
611
+ return row ? {
612
+ payload: row.payload ?? void 0
613
+ } : null;
614
+ }
615
+ async bufferEvent(input) {
616
+ await this.db.insert(durableBufferedEvents).values({
617
+ id: input.id,
618
+ name: input.name,
619
+ payload: input.payload ?? null,
620
+ publishedAt: new Date(input.publishedAt)
621
+ });
622
+ }
623
+ async listBufferedEvents(name, limit) {
624
+ const rows = await this.db.select().from(durableBufferedEvents).where(eq(durableBufferedEvents.name, name)).orderBy(asc(durableBufferedEvents.publishedAt), asc(durableBufferedEvents.id)).limit(limit);
625
+ return rows.map((r) => ({
626
+ id: r.id,
627
+ payload: r.payload ?? void 0,
628
+ publishedAt: r.publishedAt.getTime()
629
+ }));
630
+ }
631
+ async removeBufferedEvent(id) {
632
+ const rows = await this.db.delete(durableBufferedEvents).where(eq(durableBufferedEvents.id, id)).returning({
633
+ id: durableBufferedEvents.id
634
+ });
635
+ return rows.length === 1;
636
+ }
637
+ };
638
+ function escapeLike(value) {
639
+ return value.replace(/[\\%_]/g, (c) => `\\${c}`);
640
+ }
641
+ __name(escapeLike, "escapeLike");
642
+ async function upsertCheckpoint(db, cp) {
643
+ const row = toCheckpointRow(cp);
644
+ await db.insert(durableStepCheckpoints).values(row).onConflictDoUpdate({
645
+ target: [
646
+ durableStepCheckpoints.runId,
647
+ durableStepCheckpoints.seq
648
+ ],
649
+ set: row
650
+ });
651
+ }
652
+ __name(upsertCheckpoint, "upsertCheckpoint");
653
+ async function reindexAttributes(db, runId, attributes) {
654
+ await db.delete(durableRunAttributes).where(eq(durableRunAttributes.runId, runId));
655
+ const rows = normalizeAttributeRows(runId, attributes);
656
+ if (rows.length) await db.insert(durableRunAttributes).values(rows);
657
+ }
658
+ __name(reindexAttributes, "reindexAttributes");
659
+ async function deleteRunsCascade(db, runIds) {
660
+ await db.delete(durableStepCheckpoints).where(inArray(durableStepCheckpoints.runId, runIds));
661
+ await db.delete(durableSignalWaiters).where(inArray(durableSignalWaiters.runId, runIds));
662
+ await db.delete(durableRunAttributes).where(inArray(durableRunAttributes.runId, runIds));
663
+ await db.delete(durableWorkflowRuns).where(inArray(durableWorkflowRuns.id, runIds));
664
+ }
665
+ __name(deleteRunsCascade, "deleteRunsCascade");
666
+ function fromWaiterRow(row) {
667
+ return {
668
+ token: row.token,
669
+ runId: row.runId,
670
+ seq: row.seq,
671
+ parallelGroup: row.parallelGroup ?? void 0
672
+ };
673
+ }
674
+ __name(fromWaiterRow, "fromWaiterRow");
675
+ function toRunRow(run) {
676
+ return {
677
+ id: run.id,
678
+ workflow: run.workflow,
679
+ workflowVersion: run.workflowVersion,
680
+ status: run.status,
681
+ input: run.input ?? null,
682
+ output: run.output ?? null,
683
+ error: run.error ?? null,
684
+ wakeAt: date(run.wakeAt),
685
+ lockedBy: run.lockedBy ?? null,
686
+ lockedUntil: date(run.lockedUntil),
687
+ awaitingDecisionTaskId: run.awaitingDecisionTaskId ?? null,
688
+ recoveryAttempts: run.recoveryAttempts ?? null,
689
+ tags: run.tags ?? null,
690
+ searchAttributes: run.searchAttributes ?? null,
691
+ priority: run.priority ?? null,
692
+ namespace: run.namespace ?? DEFAULT_NAMESPACE,
693
+ origin: run.origin ?? null,
694
+ createdAt: run.createdAt,
695
+ updatedAt: run.updatedAt
696
+ };
697
+ }
698
+ __name(toRunRow, "toRunRow");
699
+ function toRunPatch(patch) {
700
+ const row = {};
701
+ if (patch.workflow !== void 0) row.workflow = patch.workflow;
702
+ if (patch.workflowVersion !== void 0) row.workflowVersion = patch.workflowVersion;
703
+ if (patch.status !== void 0) row.status = patch.status;
704
+ if ("input" in patch) row.input = patch.input ?? null;
705
+ if ("output" in patch) row.output = patch.output ?? null;
706
+ if ("error" in patch) row.error = patch.error ?? null;
707
+ if ("wakeAt" in patch) row.wakeAt = date(patch.wakeAt);
708
+ if ("lockedBy" in patch) row.lockedBy = patch.lockedBy ?? null;
709
+ if ("lockedUntil" in patch) row.lockedUntil = date(patch.lockedUntil);
710
+ if ("awaitingDecisionTaskId" in patch) row.awaitingDecisionTaskId = patch.awaitingDecisionTaskId ?? null;
711
+ if ("recoveryAttempts" in patch) row.recoveryAttempts = patch.recoveryAttempts ?? null;
712
+ if ("tags" in patch) row.tags = patch.tags ?? null;
713
+ if ("searchAttributes" in patch) row.searchAttributes = patch.searchAttributes ?? null;
714
+ if ("priority" in patch) row.priority = patch.priority ?? null;
715
+ if ("namespace" in patch) row.namespace = patch.namespace ?? DEFAULT_NAMESPACE;
716
+ if ("origin" in patch) row.origin = patch.origin ?? null;
717
+ if (patch.createdAt != null) row.createdAt = patch.createdAt;
718
+ if (patch.updatedAt != null) row.updatedAt = patch.updatedAt;
719
+ return row;
720
+ }
721
+ __name(toRunPatch, "toRunPatch");
722
+ function fromRunRow(row) {
723
+ return {
724
+ id: row.id,
725
+ workflow: row.workflow,
726
+ workflowVersion: row.workflowVersion,
727
+ status: row.status,
728
+ input: row.input ?? void 0,
729
+ output: row.output ?? void 0,
730
+ error: row.error ?? void 0,
731
+ wakeAt: ms(row.wakeAt),
732
+ lockedBy: row.lockedBy ?? void 0,
733
+ lockedUntil: ms(row.lockedUntil),
734
+ awaitingDecisionTaskId: row.awaitingDecisionTaskId ?? void 0,
735
+ recoveryAttempts: row.recoveryAttempts ?? void 0,
736
+ tags: row.tags ?? void 0,
737
+ searchAttributes: row.searchAttributes ?? void 0,
738
+ priority: row.priority ?? void 0,
739
+ namespace: row.namespace,
740
+ origin: row.origin ?? void 0,
741
+ createdAt: row.createdAt,
742
+ updatedAt: row.updatedAt
743
+ };
744
+ }
745
+ __name(fromRunRow, "fromRunRow");
746
+ function toCheckpointRow(cp) {
747
+ return {
748
+ runId: cp.runId,
749
+ seq: cp.seq,
750
+ name: cp.name,
751
+ kind: cp.kind,
752
+ stepId: cp.stepId,
753
+ status: cp.status,
754
+ input: cp.input ?? null,
755
+ output: cp.output ?? null,
756
+ error: cp.error ?? null,
757
+ events: cp.events ?? null,
758
+ attempts: cp.attempts,
759
+ workerGroup: cp.workerGroup ?? null,
760
+ parallelGroup: cp.parallelGroup ?? null,
761
+ wakeAt: date(cp.wakeAt),
762
+ enqueuedAt: cp.enqueuedAt ?? cp.startedAt,
763
+ startedAt: cp.startedAt,
764
+ finishedAt: cp.finishedAt
765
+ };
766
+ }
767
+ __name(toCheckpointRow, "toCheckpointRow");
768
+ function fromCheckpointRow(row) {
769
+ return {
770
+ runId: row.runId,
771
+ seq: row.seq,
772
+ name: row.name,
773
+ kind: row.kind,
774
+ stepId: row.stepId,
775
+ status: row.status,
776
+ input: row.input ?? void 0,
777
+ output: row.output ?? void 0,
778
+ error: row.error ?? void 0,
779
+ events: row.events ?? void 0,
780
+ attempts: row.attempts,
781
+ workerGroup: row.workerGroup ?? void 0,
782
+ parallelGroup: row.parallelGroup ?? void 0,
783
+ wakeAt: ms(row.wakeAt),
784
+ // Older rows may lack enqueued_at; treat the start as enqueue time (queue-wait reads zero).
785
+ enqueuedAt: row.enqueuedAt ?? row.startedAt,
786
+ startedAt: row.startedAt,
787
+ finishedAt: row.finishedAt
788
+ };
789
+ }
790
+ __name(fromCheckpointRow, "fromCheckpointRow");
791
+ function toScheduleRow(r) {
792
+ return {
793
+ id: r.id,
794
+ namespace: r.namespace,
795
+ workflow: r.workflow,
796
+ paused: r.paused,
797
+ nextFireAt: r.nextFireAt == null ? null : new Date(r.nextFireAt),
798
+ tags: r.tags ?? null,
799
+ spec: r.spec,
800
+ state: r.state,
801
+ createdAt: r.createdAt,
802
+ updatedAt: r.updatedAt
803
+ };
804
+ }
805
+ __name(toScheduleRow, "toScheduleRow");
806
+ function toSchedulePatch(patch) {
807
+ const set = {};
808
+ if (patch.namespace !== void 0) set.namespace = patch.namespace;
809
+ if (patch.workflow !== void 0) set.workflow = patch.workflow;
810
+ if (patch.paused !== void 0) set.paused = patch.paused;
811
+ if ("nextFireAt" in patch) set.nextFireAt = patch.nextFireAt == null ? null : new Date(patch.nextFireAt);
812
+ if ("tags" in patch) set.tags = patch.tags ?? null;
813
+ if (patch.spec !== void 0) set.spec = patch.spec;
814
+ if (patch.state !== void 0) set.state = patch.state;
815
+ if (patch.updatedAt !== void 0) set.updatedAt = patch.updatedAt;
816
+ return set;
817
+ }
818
+ __name(toSchedulePatch, "toSchedulePatch");
819
+ function fromScheduleRow(row) {
820
+ return {
821
+ id: row.id,
822
+ namespace: row.namespace,
823
+ workflow: row.workflow,
824
+ paused: row.paused,
825
+ nextFireAt: row.nextFireAt ? row.nextFireAt.getTime() : null,
826
+ ...row.tags ? {
827
+ tags: row.tags
828
+ } : {},
829
+ spec: row.spec ?? {},
830
+ state: row.state ?? {},
831
+ createdAt: row.createdAt,
832
+ updatedAt: row.updatedAt
833
+ };
834
+ }
835
+ __name(fromScheduleRow, "fromScheduleRow");
836
+
837
+ // src/pg/executor.ts
838
+ import { sql as sql3 } from "drizzle-orm";
839
+ function toDrizzleSql(text2, params = []) {
840
+ const chunks = [];
841
+ let last = 0;
842
+ for (const match of text2.matchAll(/\$(\d+)/g)) {
843
+ const index2 = Number(match[1]) - 1;
844
+ if (index2 < 0 || index2 >= params.length) throw new Error(`placeholder ${match[0]} has no bound parameter`);
845
+ chunks.push(sql3.raw(text2.slice(last, match.index)), sql3`${params[index2]}`);
846
+ last = (match.index ?? 0) + match[0].length;
847
+ }
848
+ chunks.push(sql3.raw(text2.slice(last)));
849
+ return sql3.join(chunks);
850
+ }
851
+ __name(toDrizzleSql, "toDrizzleSql");
852
+ function rowsOf(result) {
853
+ if (Array.isArray(result)) return result;
854
+ return result?.rows ?? [];
855
+ }
856
+ __name(rowsOf, "rowsOf");
857
+ function drizzlePgExecutor(db) {
858
+ return {
859
+ dialect: "postgres",
860
+ escapeId: /* @__PURE__ */ __name((id) => `"${id.replace(/"/g, '""')}"`, "escapeId"),
861
+ query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await db.execute(toDrizzleSql(text2, params))), "query"),
862
+ transaction: /* @__PURE__ */ __name((fn) => db.transaction((tx) => fn({
863
+ query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await tx.execute(toDrizzleSql(text2, params))), "query")
864
+ })), "transaction")
865
+ };
866
+ }
867
+ __name(drizzlePgExecutor, "drizzlePgExecutor");
868
+ export {
869
+ DEFAULT_NAMESPACE,
870
+ DURABLE_PG_DDL,
871
+ DrizzlePgStateStore,
872
+ drizzlePgExecutor,
873
+ durableBufferedEvents,
874
+ durableBufferedSignals,
875
+ durablePgManagedTables,
876
+ durablePgSchema,
877
+ durableRunAttributes,
878
+ durableSchedules,
879
+ durableSignalWaiters,
880
+ durableStepCheckpoints,
881
+ durableWorkflowRuns,
882
+ ensureDrizzlePgDurableSchema,
883
+ toDrizzleSql
884
+ };
885
+ //# sourceMappingURL=index.js.map