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