@dudousxd/nestjs-durable-store-drizzle 0.20.0 → 0.22.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/dist/index.cjs +115 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +116 -3
- package/dist/index.js.map +1 -1
- package/dist/pg/index.cjs +234 -99
- package/dist/pg/index.cjs.map +1 -1
- package/dist/pg/index.d.cts +15 -2
- package/dist/pg/index.d.ts +15 -2
- package/dist/pg/index.js +182 -48
- package/dist/pg/index.js.map +1 -1
- package/package.json +4 -4
package/dist/pg/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
2
2
|
import { PgDatabase } from 'drizzle-orm/pg-core';
|
|
3
|
-
import { StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, StoreTransaction, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
3
|
+
import { StateStore, WorkflowRun, RunStatus, RetentionPolicy, StepCheckpoint, StoreTransaction, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
4
4
|
import { SQL } from 'drizzle-orm';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -79,6 +79,7 @@ declare class DrizzlePgStateStore implements StateStore {
|
|
|
79
79
|
tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
|
|
80
80
|
createRun(run: WorkflowRun): Promise<void>;
|
|
81
81
|
updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
|
|
82
|
+
updateRunIf(runId: string, expectedStatuses: RunStatus[], inputPatch: Partial<WorkflowRun>): Promise<boolean>;
|
|
82
83
|
getRun(runId: string): Promise<WorkflowRun | null>;
|
|
83
84
|
deleteRun(runId: string): Promise<void>;
|
|
84
85
|
deleteRuns(runIds: string[]): Promise<void>;
|
|
@@ -86,9 +87,11 @@ declare class DrizzlePgStateStore implements StateStore {
|
|
|
86
87
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
87
88
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
88
89
|
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
90
|
+
saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
|
|
89
91
|
transaction<T>(work: (tx: StoreTransaction) => Promise<T>): Promise<T>;
|
|
90
92
|
/** Crash recovery. Namespace-scoped: a worker re-drives only its own tenant's runs. */
|
|
91
93
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
94
|
+
listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
92
95
|
/** FIFO pick-up, served by `durable_workflow_runs_namespace_status_idx`. The engine then leases
|
|
93
96
|
* each run with {@link tryLockRun}; the listing itself takes no locks (it runs outside a tx). */
|
|
94
97
|
listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
@@ -118,6 +121,14 @@ declare class DrizzlePgStateStore implements StateStore {
|
|
|
118
121
|
getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
|
|
119
122
|
countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
|
|
120
123
|
listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
|
|
124
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
125
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
126
|
+
checkpointStats(runId: string, opts?: {
|
|
127
|
+
group?: string | undefined;
|
|
128
|
+
}): Promise<CheckpointStat[]>;
|
|
129
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
130
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
131
|
+
queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
|
|
121
132
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
122
133
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
123
134
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
@@ -159,6 +170,8 @@ interface DrizzlePgSqlExecutor {
|
|
|
159
170
|
/** Turn a `$1`-placeholder SQL string + params into a parameterized drizzle `SQL` object, so it runs
|
|
160
171
|
* through whatever driver the drizzle db wraps (node-postgres, postgres-js, PGlite, Neon, …). */
|
|
161
172
|
declare function toDrizzleSql(text: string, params?: unknown[]): SQL;
|
|
173
|
+
/** node-postgres / PGlite hand back `{ rows }`; postgres-js hands back the row array itself. */
|
|
174
|
+
declare function rowsOf<T>(result: unknown): T[];
|
|
162
175
|
/**
|
|
163
176
|
* Build a `DbTransport` executor from your drizzle Postgres db — the broker-less, DBOS-style
|
|
164
177
|
* transport (remote steps as rows, claimed with `SELECT … FOR UPDATE SKIP LOCKED`) on the SAME
|
|
@@ -2384,4 +2397,4 @@ declare const durablePgSchema: {
|
|
|
2384
2397
|
*/
|
|
2385
2398
|
declare function durablePgManagedTables(): string[];
|
|
2386
2399
|
|
|
2387
|
-
export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, drizzlePgExecutor, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, toDrizzleSql };
|
|
2400
|
+
export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, drizzlePgExecutor, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, rowsOf, toDrizzleSql };
|
package/dist/pg/index.js
CHANGED
|
@@ -86,6 +86,7 @@ var DURABLE_PG_DDL = [
|
|
|
86
86
|
`CREATE INDEX IF NOT EXISTS "durable_workflow_runs_namespace_status_idx" ON "durable_workflow_runs" USING btree ("namespace","status","created_at")`,
|
|
87
87
|
`CREATE INDEX IF NOT EXISTS "durable_runs_tags_gin_idx" ON "durable_workflow_runs" USING gin ("tags" jsonb_path_ops)`,
|
|
88
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_step_checkpoints_group_idx" ON "durable_step_checkpoints" USING btree ("run_id","parallel_group","seq")`,
|
|
89
90
|
`CREATE INDEX IF NOT EXISTS "durable_run_attributes_num_idx" ON "durable_run_attributes" USING btree ("key","num_value")`,
|
|
90
91
|
`CREATE INDEX IF NOT EXISTS "durable_run_attributes_str_idx" ON "durable_run_attributes" USING btree ("key","str_value")`,
|
|
91
92
|
`CREATE INDEX IF NOT EXISTS "durable_signal_waiters_run_id_idx" ON "durable_signal_waiters" USING btree ("run_id")`,
|
|
@@ -104,7 +105,39 @@ __name(ensureDrizzlePgDurableSchema, "ensureDrizzlePgDurableSchema");
|
|
|
104
105
|
|
|
105
106
|
// src/pg/drizzle-pg-state-store.ts
|
|
106
107
|
import { RUN_VALUE_FACET_SCAN, SINGLETON_ACTIVE_STATUSES, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, clearTerminalSingletonAdmission, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns, singletonAdmissionDenialPatch, singletonAdmissionTags } from "@dudousxd/nestjs-durable-core";
|
|
107
|
-
import { and, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, like, lt, lte, or, sql as
|
|
108
|
+
import { and, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, like, lt, lte, notInArray, or, sql as sql3 } from "drizzle-orm";
|
|
109
|
+
|
|
110
|
+
// src/pg/executor.ts
|
|
111
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
112
|
+
function toDrizzleSql(text2, params = []) {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
let last = 0;
|
|
115
|
+
for (const match of text2.matchAll(/\$(\d+)/g)) {
|
|
116
|
+
const index2 = Number(match[1]) - 1;
|
|
117
|
+
if (index2 < 0 || index2 >= params.length) throw new Error(`placeholder ${match[0]} has no bound parameter`);
|
|
118
|
+
chunks.push(sql2.raw(text2.slice(last, match.index)), sql2`${params[index2]}`);
|
|
119
|
+
last = (match.index ?? 0) + match[0].length;
|
|
120
|
+
}
|
|
121
|
+
chunks.push(sql2.raw(text2.slice(last)));
|
|
122
|
+
return sql2.join(chunks);
|
|
123
|
+
}
|
|
124
|
+
__name(toDrizzleSql, "toDrizzleSql");
|
|
125
|
+
function rowsOf(result) {
|
|
126
|
+
if (Array.isArray(result)) return result;
|
|
127
|
+
return result?.rows ?? [];
|
|
128
|
+
}
|
|
129
|
+
__name(rowsOf, "rowsOf");
|
|
130
|
+
function drizzlePgExecutor(db) {
|
|
131
|
+
return {
|
|
132
|
+
dialect: "postgres",
|
|
133
|
+
escapeId: /* @__PURE__ */ __name((id) => `"${id.replace(/"/g, '""')}"`, "escapeId"),
|
|
134
|
+
query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await db.execute(toDrizzleSql(text2, params))), "query"),
|
|
135
|
+
transaction: /* @__PURE__ */ __name((fn) => db.transaction((tx) => fn({
|
|
136
|
+
query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await tx.execute(toDrizzleSql(text2, params))), "query")
|
|
137
|
+
})), "transaction")
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
__name(drizzlePgExecutor, "drizzlePgExecutor");
|
|
108
141
|
|
|
109
142
|
// src/pg/schema.ts
|
|
110
143
|
import { getTableName } from "drizzle-orm";
|
|
@@ -182,7 +215,9 @@ var durableStepCheckpoints = pgTable(
|
|
|
182
215
|
t.runId,
|
|
183
216
|
t.seq
|
|
184
217
|
]
|
|
185
|
-
})
|
|
218
|
+
}),
|
|
219
|
+
// A run detail reads one fan at a time: checkpointStats / queryCheckpoints by group, seq-paged.
|
|
220
|
+
index("durable_step_checkpoints_group_idx").on(t.runId, t.parallelGroup, t.seq)
|
|
186
221
|
]
|
|
187
222
|
);
|
|
188
223
|
var durableRunAttributes = pgTable("durable_run_attributes", {
|
|
@@ -271,7 +306,7 @@ __name(durablePgManagedTables, "durablePgManagedTables");
|
|
|
271
306
|
// src/pg/drizzle-pg-state-store.ts
|
|
272
307
|
function anyOf(conditions) {
|
|
273
308
|
const present = conditions.filter((c) => c !== void 0);
|
|
274
|
-
return present.length ? or(...present) :
|
|
309
|
+
return present.length ? or(...present) : sql3`1 = 0`;
|
|
275
310
|
}
|
|
276
311
|
__name(anyOf, "anyOf");
|
|
277
312
|
function namespaceFilter(namespace) {
|
|
@@ -279,7 +314,7 @@ function namespaceFilter(namespace) {
|
|
|
279
314
|
}
|
|
280
315
|
__name(namespaceFilter, "namespaceFilter");
|
|
281
316
|
function hasTag(tag) {
|
|
282
|
-
return
|
|
317
|
+
return sql3`${durableWorkflowRuns.tags} @> ${JSON.stringify([
|
|
283
318
|
tag
|
|
284
319
|
])}::jsonb`;
|
|
285
320
|
}
|
|
@@ -351,6 +386,22 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
351
386
|
if (reindex) await reindexAttributes(tx, runId, patch.searchAttributes);
|
|
352
387
|
});
|
|
353
388
|
}
|
|
389
|
+
async updateRunIf(runId, expectedStatuses, inputPatch) {
|
|
390
|
+
if (expectedStatuses.length === 0) return false;
|
|
391
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.withScope({}).getRun(id)) : inputPatch;
|
|
392
|
+
const row = toRunPatch(patch);
|
|
393
|
+
const reindex = "searchAttributes" in patch;
|
|
394
|
+
const matches = and(eq(durableWorkflowRuns.id, runId), inArray(durableWorkflowRuns.status, expectedStatuses));
|
|
395
|
+
return this.db.transaction(async (tx) => {
|
|
396
|
+
const applied = Object.keys(row).length ? (await tx.update(durableWorkflowRuns).set(row).where(matches).returning({
|
|
397
|
+
id: durableWorkflowRuns.id
|
|
398
|
+
})).length === 1 : (await tx.select({
|
|
399
|
+
id: durableWorkflowRuns.id
|
|
400
|
+
}).from(durableWorkflowRuns).where(matches).for("update")).length === 1;
|
|
401
|
+
if (applied && reindex) await reindexAttributes(tx, runId, patch.searchAttributes);
|
|
402
|
+
return applied;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
354
405
|
async getRun(runId) {
|
|
355
406
|
const rows = await this.db.select().from(durableWorkflowRuns).where(and(eq(durableWorkflowRuns.id, runId), this.scope())).limit(1);
|
|
356
407
|
return rows[0] ? fromRunRow(rows[0]) : null;
|
|
@@ -420,6 +471,30 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
420
471
|
});
|
|
421
472
|
return rows.length === 1;
|
|
422
473
|
}
|
|
474
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
475
|
+
const wrote = checkpoints.map(() => false);
|
|
476
|
+
if (checkpoints.length === 0) return wrote;
|
|
477
|
+
await this.db.transaction(async (tx) => {
|
|
478
|
+
for (const round of distinctRounds(checkpoints)) {
|
|
479
|
+
for (let at = 0; at < round.length; at += CHECKPOINT_BATCH_ROWS) {
|
|
480
|
+
const chunk = round.slice(at, at + CHECKPOINT_BATCH_ROWS);
|
|
481
|
+
const updated = rowsOf(await tx.execute(updateIfPendingFrom(chunk.map((i) => checkpoints[i]))));
|
|
482
|
+
const byKey = new Map(chunk.map((i) => [
|
|
483
|
+
rowKey(checkpoints[i]),
|
|
484
|
+
i
|
|
485
|
+
]));
|
|
486
|
+
for (const row of updated) {
|
|
487
|
+
const i = byKey.get(rowKey({
|
|
488
|
+
runId: row.run_id,
|
|
489
|
+
seq: Number(row.seq)
|
|
490
|
+
}));
|
|
491
|
+
if (i !== void 0) wrote[i] = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return wrote;
|
|
497
|
+
}
|
|
423
498
|
async transaction(work) {
|
|
424
499
|
return this.db.transaction(async (tx) => work({
|
|
425
500
|
raw: tx,
|
|
@@ -434,6 +509,14 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
434
509
|
]), namespaceFilter(namespace), this.scope()));
|
|
435
510
|
return rows.map(fromRunRow);
|
|
436
511
|
}
|
|
512
|
+
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
513
|
+
if (limit <= 0) return [];
|
|
514
|
+
const rows = await this.db.select().from(durableWorkflowRuns).where(and(inArray(durableWorkflowRuns.status, [
|
|
515
|
+
"running",
|
|
516
|
+
"cancelling"
|
|
517
|
+
]), or(isNull(durableWorkflowRuns.lockedUntil), lte(durableWorkflowRuns.lockedUntil, new Date(nowMs))), namespaceFilter(namespace), this.scope())).orderBy(asc(durableWorkflowRuns.createdAt)).limit(limit);
|
|
518
|
+
return rows.map(fromRunRow);
|
|
519
|
+
}
|
|
437
520
|
/** FIFO pick-up, served by `durable_workflow_runs_namespace_status_idx`. The engine then leases
|
|
438
521
|
* each run with {@link tryLockRun}; the listing itself takes no locks (it runs outside a tx). */
|
|
439
522
|
async listPendingRuns(limit, namespace) {
|
|
@@ -473,16 +556,16 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
473
556
|
return [
|
|
474
557
|
this.scope(),
|
|
475
558
|
query.workflow ? eq(durableWorkflowRuns.workflow, query.workflow) : void 0,
|
|
476
|
-
query.workflows ? query.workflows.length ? inArray(durableWorkflowRuns.workflow, query.workflows) :
|
|
559
|
+
query.workflows ? query.workflows.length ? inArray(durableWorkflowRuns.workflow, query.workflows) : sql3`1 = 0` : void 0,
|
|
477
560
|
// The tenant boundary. Also the engine's execution-timeout sweep path
|
|
478
561
|
// (`listRuns({ workflow, status, namespace })`) — unscoped, it would cancel another tenant's runs.
|
|
479
562
|
namespaceFilter(query.namespace),
|
|
480
|
-
query.namespaces ? query.namespaces.length ? inArray(durableWorkflowRuns.namespace, query.namespaces) :
|
|
563
|
+
query.namespaces ? query.namespaces.length ? inArray(durableWorkflowRuns.namespace, query.namespaces) : sql3`1 = 0` : void 0,
|
|
481
564
|
// `null` selects the unattributed bucket; a value never matches a NULL origin.
|
|
482
565
|
query.origin === null ? isNull(durableWorkflowRuns.origin) : query.origin !== void 0 ? eq(durableWorkflowRuns.origin, query.origin) : void 0,
|
|
483
566
|
query.origins ? anyOf(query.origins.map((o) => o === null ? isNull(durableWorkflowRuns.origin) : eq(durableWorkflowRuns.origin, o))) : void 0,
|
|
484
567
|
query.status ? eq(durableWorkflowRuns.status, query.status) : void 0,
|
|
485
|
-
query.statuses ? query.statuses.length ? inArray(durableWorkflowRuns.status, query.statuses) :
|
|
568
|
+
query.statuses ? query.statuses.length ? inArray(durableWorkflowRuns.status, query.statuses) : sql3`1 = 0` : void 0,
|
|
486
569
|
query.tag ? hasTag(query.tag) : void 0,
|
|
487
570
|
query.tags ? anyOf(query.tags.map(hasTag)) : void 0,
|
|
488
571
|
// One EXISTS per attribute predicate against the normalized side table (ANDed).
|
|
@@ -500,7 +583,7 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
500
583
|
async countRuns(query) {
|
|
501
584
|
const filters = this.runFilters(query);
|
|
502
585
|
const rows = await this.db.select({
|
|
503
|
-
count:
|
|
586
|
+
count: sql3`count(*)`.mapWith(Number)
|
|
504
587
|
}).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0);
|
|
505
588
|
return rows[0]?.count ?? 0;
|
|
506
589
|
}
|
|
@@ -509,7 +592,7 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
509
592
|
const rows = await this.db.select({
|
|
510
593
|
status: durableWorkflowRuns.status,
|
|
511
594
|
origin: durableWorkflowRuns.origin,
|
|
512
|
-
count:
|
|
595
|
+
count: sql3`count(*)`.mapWith(Number)
|
|
513
596
|
}).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0).groupBy(durableWorkflowRuns.status, durableWorkflowRuns.origin);
|
|
514
597
|
return mergeRunFacetRows(rows.map((r) => ({
|
|
515
598
|
status: r.status,
|
|
@@ -531,7 +614,7 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
531
614
|
const filters = this.runFilters(query);
|
|
532
615
|
const rows = await this.db.select({
|
|
533
616
|
value: column,
|
|
534
|
-
count:
|
|
617
|
+
count: sql3`count(*)`.mapWith(Number)
|
|
535
618
|
}).from(durableWorkflowRuns).where(filters.length ? and(...filters) : void 0).groupBy(column);
|
|
536
619
|
return mergeRunValueFacetRows(rows.map((r) => ({
|
|
537
620
|
value: r.value ?? null,
|
|
@@ -542,9 +625,9 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
542
625
|
* excludes runs lacking the key (EXISTS requires the row). Numbers compare `num_value`, strings
|
|
543
626
|
* and booleans `str_value`; an `in` set ORs its members inside the one EXISTS. */
|
|
544
627
|
attributeExists(f) {
|
|
545
|
-
const comparisons = attributePredicateOperands(f).map(({ column, comparator, operand }) =>
|
|
628
|
+
const comparisons = attributePredicateOperands(f).map(({ column, comparator, operand }) => sql3`${column === "numValue" ? durableRunAttributes.numValue : durableRunAttributes.strValue} ${sql3.raw(comparator)} ${operand}`);
|
|
546
629
|
return exists(this.db.select({
|
|
547
|
-
one:
|
|
630
|
+
one: sql3`1`
|
|
548
631
|
}).from(durableRunAttributes).where(and(eq(durableRunAttributes.runId, durableWorkflowRuns.id), eq(durableRunAttributes.key, f.key), anyOf(comparisons))));
|
|
549
632
|
}
|
|
550
633
|
// ---- persisted schedules ----------------------------------------------------------------------
|
|
@@ -575,9 +658,9 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
575
658
|
return rows.length > 0;
|
|
576
659
|
}
|
|
577
660
|
async listSchedules(query) {
|
|
578
|
-
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 ?
|
|
661
|
+
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 ? sql3`${durableSchedules.tags} @> ${JSON.stringify([
|
|
579
662
|
query.tag
|
|
580
|
-
])}::jsonb` : void 0, query.dueBy !== void 0 ? and(eq(durableSchedules.paused, false), lte(durableSchedules.nextFireAt, new Date(query.dueBy))) : void 0)).orderBy(
|
|
663
|
+
])}::jsonb` : void 0, query.dueBy !== void 0 ? and(eq(durableSchedules.paused, false), lte(durableSchedules.nextFireAt, new Date(query.dueBy))) : void 0)).orderBy(sql3`${durableSchedules.nextFireAt} ASC NULLS LAST`, asc(durableSchedules.id)).$dynamic();
|
|
581
664
|
if (query.limit != null) base.limit(query.limit);
|
|
582
665
|
if (query.offset != null) base.offset(query.offset);
|
|
583
666
|
return (await base).map(fromScheduleRow);
|
|
@@ -593,7 +676,7 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
593
676
|
async countCheckpoints(runId, fromSeq, toSeq, statuses) {
|
|
594
677
|
if (statuses.length === 0) return 0;
|
|
595
678
|
const rows = await this.db.select({
|
|
596
|
-
count:
|
|
679
|
+
count: sql3`count(*)`.mapWith(Number)
|
|
597
680
|
}).from(durableStepCheckpoints).where(and(eq(durableStepCheckpoints.runId, runId), gte(durableStepCheckpoints.seq, fromSeq), lte(durableStepCheckpoints.seq, toSeq), inArray(durableStepCheckpoints.status, statuses)));
|
|
598
681
|
return rows[0]?.count ?? 0;
|
|
599
682
|
}
|
|
@@ -602,6 +685,53 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
|
|
|
602
685
|
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));
|
|
603
686
|
return rows.map(fromCheckpointRow);
|
|
604
687
|
}
|
|
688
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
689
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
690
|
+
async checkpointStats(runId, opts) {
|
|
691
|
+
const c = durableStepCheckpoints;
|
|
692
|
+
const nameKind = sql3`CASE
|
|
693
|
+
WHEN ${c.name} LIKE 'signal:child:%' THEN 'child'
|
|
694
|
+
WHEN ${c.name} LIKE 'fan:%' THEN 'folded'
|
|
695
|
+
WHEN ${c.name} LIKE 'spawn:%' THEN 'spawn'
|
|
696
|
+
ELSE 'step' END`.as("name_kind");
|
|
697
|
+
const rows = await this.db.select({
|
|
698
|
+
group: c.parallelGroup,
|
|
699
|
+
status: c.status,
|
|
700
|
+
nameKind,
|
|
701
|
+
count: sql3`count(*)`.mapWith(Number),
|
|
702
|
+
minSeq: sql3`min(${c.seq})`.mapWith(Number),
|
|
703
|
+
maxSeq: sql3`max(${c.seq})`.mapWith(Number),
|
|
704
|
+
minName: sql3`min(${c.name})`,
|
|
705
|
+
maxName: sql3`max(${c.name})`,
|
|
706
|
+
minEnqueuedAt: sql3`min(coalesce(${c.enqueuedAt}, ${c.startedAt}))`.mapWith(c.startedAt),
|
|
707
|
+
minStartedAt: sql3`min(${c.startedAt})`.mapWith(c.startedAt),
|
|
708
|
+
maxStartedAt: sql3`max(${c.startedAt})`.mapWith(c.startedAt),
|
|
709
|
+
maxFinishedAt: sql3`max(${c.finishedAt})`.mapWith(c.finishedAt),
|
|
710
|
+
sumAttempts: sql3`sum(${c.attempts})`.mapWith(Number)
|
|
711
|
+
}).from(c).where(and(eq(c.runId, runId), opts?.group !== void 0 ? eq(c.parallelGroup, opts.group) : void 0)).groupBy(c.parallelGroup, c.status, sql3`name_kind`);
|
|
712
|
+
return rows.map((r) => ({
|
|
713
|
+
...r,
|
|
714
|
+
group: r.group ?? null,
|
|
715
|
+
status: r.status
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
719
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
720
|
+
async queryCheckpoints(runId, query) {
|
|
721
|
+
if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
|
|
722
|
+
const c = durableStepCheckpoints;
|
|
723
|
+
const base = this.db.select().from(c).where(and(
|
|
724
|
+
eq(c.runId, runId),
|
|
725
|
+
query.group !== void 0 ? eq(c.parallelGroup, query.group) : void 0,
|
|
726
|
+
// `NOT IN` is never true for a NULL group, so ungrouped rows are kept explicitly.
|
|
727
|
+
query.excludeGroups?.length ? or(isNull(c.parallelGroup), notInArray(c.parallelGroup, query.excludeGroups)) : void 0,
|
|
728
|
+
query.statuses ? inArray(c.status, query.statuses) : void 0,
|
|
729
|
+
query.namePrefixes ? or(...query.namePrefixes.map((p) => like(c.name, `${escapeLike(p)}%`))) : void 0
|
|
730
|
+
)).orderBy(asc(c.seq)).$dynamic();
|
|
731
|
+
if (query.limit != null) base.limit(query.limit);
|
|
732
|
+
if (query.offset != null) base.offset(query.offset);
|
|
733
|
+
return (await base).map(fromCheckpointRow);
|
|
734
|
+
}
|
|
605
735
|
async putSignalWaiter(waiter) {
|
|
606
736
|
const set = {
|
|
607
737
|
runId: waiter.runId,
|
|
@@ -717,6 +847,41 @@ function escapeLike(value) {
|
|
|
717
847
|
return value.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
718
848
|
}
|
|
719
849
|
__name(escapeLike, "escapeLike");
|
|
850
|
+
var CHECKPOINT_BATCH_ROWS = 500;
|
|
851
|
+
var rowKey = /* @__PURE__ */ __name((cp) => `${cp.seq}:${cp.runId}`, "rowKey");
|
|
852
|
+
function distinctRounds(checkpoints) {
|
|
853
|
+
const rounds = [];
|
|
854
|
+
const seen = /* @__PURE__ */ new Map();
|
|
855
|
+
checkpoints.forEach((cp, i) => {
|
|
856
|
+
const key = rowKey(cp);
|
|
857
|
+
const round = seen.get(key) ?? 0;
|
|
858
|
+
seen.set(key, round + 1);
|
|
859
|
+
const indexes = rounds[round] ?? [];
|
|
860
|
+
rounds[round] = indexes;
|
|
861
|
+
indexes.push(i);
|
|
862
|
+
});
|
|
863
|
+
return rounds;
|
|
864
|
+
}
|
|
865
|
+
__name(distinctRounds, "distinctRounds");
|
|
866
|
+
function updateIfPendingFrom(checkpoints) {
|
|
867
|
+
const json = /* @__PURE__ */ __name((v) => v == null ? null : JSON.stringify(v), "json");
|
|
868
|
+
const ts = /* @__PURE__ */ __name((v) => v == null ? null : v.toISOString(), "ts");
|
|
869
|
+
const values = checkpoints.map((cp) => {
|
|
870
|
+
const r = toCheckpointRow(cp);
|
|
871
|
+
return sql3`(${r.runId}::text, ${r.seq}::integer, ${r.name}::text, ${r.kind}::text, ${r.stepId}::text, ${r.status}::text, ${json(r.input)}::jsonb, ${json(r.output)}::jsonb, ${json(r.error)}::jsonb, ${json(r.events)}::jsonb, ${r.attempts}::integer, ${r.workerGroup}::text, ${r.parallelGroup}::text, ${ts(r.wakeAt)}::timestamptz, ${ts(r.enqueuedAt)}::timestamptz, ${ts(r.startedAt)}::timestamptz, ${ts(r.finishedAt)}::timestamptz)`;
|
|
872
|
+
});
|
|
873
|
+
return sql3`UPDATE ${durableStepCheckpoints} AS c SET
|
|
874
|
+
name = v.name, kind = v.kind, step_id = v.step_id, status = v.status, input = v.input,
|
|
875
|
+
output = v.output, error = v.error, events = v.events, attempts = v.attempts,
|
|
876
|
+
worker_group = v.worker_group, parallel_group = v.parallel_group, wake_at = v.wake_at,
|
|
877
|
+
enqueued_at = v.enqueued_at, started_at = v.started_at, finished_at = v.finished_at
|
|
878
|
+
FROM (VALUES ${sql3.join(values, sql3`, `)}) AS v(run_id, seq, name, kind, step_id, status, input,
|
|
879
|
+
output, error, events, attempts, worker_group, parallel_group, wake_at, enqueued_at, started_at,
|
|
880
|
+
finished_at)
|
|
881
|
+
WHERE c.run_id = v.run_id AND c.seq = v.seq AND c.status = 'pending'
|
|
882
|
+
RETURNING c.run_id, c.seq`;
|
|
883
|
+
}
|
|
884
|
+
__name(updateIfPendingFrom, "updateIfPendingFrom");
|
|
720
885
|
async function upsertCheckpoint(db, cp) {
|
|
721
886
|
const row = toCheckpointRow(cp);
|
|
722
887
|
await db.insert(durableStepCheckpoints).values(row).onConflictDoUpdate({
|
|
@@ -914,42 +1079,10 @@ __name(fromScheduleRow, "fromScheduleRow");
|
|
|
914
1079
|
function excludedSet(table, only) {
|
|
915
1080
|
return Object.fromEntries(Object.entries(getTableColumns(table)).filter(([key]) => !only || only.includes(key)).map(([key, column]) => [
|
|
916
1081
|
key,
|
|
917
|
-
|
|
1082
|
+
sql3.raw(`excluded."${column.name}"`)
|
|
918
1083
|
]));
|
|
919
1084
|
}
|
|
920
1085
|
__name(excludedSet, "excludedSet");
|
|
921
|
-
|
|
922
|
-
// src/pg/executor.ts
|
|
923
|
-
import { sql as sql3 } from "drizzle-orm";
|
|
924
|
-
function toDrizzleSql(text2, params = []) {
|
|
925
|
-
const chunks = [];
|
|
926
|
-
let last = 0;
|
|
927
|
-
for (const match of text2.matchAll(/\$(\d+)/g)) {
|
|
928
|
-
const index2 = Number(match[1]) - 1;
|
|
929
|
-
if (index2 < 0 || index2 >= params.length) throw new Error(`placeholder ${match[0]} has no bound parameter`);
|
|
930
|
-
chunks.push(sql3.raw(text2.slice(last, match.index)), sql3`${params[index2]}`);
|
|
931
|
-
last = (match.index ?? 0) + match[0].length;
|
|
932
|
-
}
|
|
933
|
-
chunks.push(sql3.raw(text2.slice(last)));
|
|
934
|
-
return sql3.join(chunks);
|
|
935
|
-
}
|
|
936
|
-
__name(toDrizzleSql, "toDrizzleSql");
|
|
937
|
-
function rowsOf(result) {
|
|
938
|
-
if (Array.isArray(result)) return result;
|
|
939
|
-
return result?.rows ?? [];
|
|
940
|
-
}
|
|
941
|
-
__name(rowsOf, "rowsOf");
|
|
942
|
-
function drizzlePgExecutor(db) {
|
|
943
|
-
return {
|
|
944
|
-
dialect: "postgres",
|
|
945
|
-
escapeId: /* @__PURE__ */ __name((id) => `"${id.replace(/"/g, '""')}"`, "escapeId"),
|
|
946
|
-
query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await db.execute(toDrizzleSql(text2, params))), "query"),
|
|
947
|
-
transaction: /* @__PURE__ */ __name((fn) => db.transaction((tx) => fn({
|
|
948
|
-
query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await tx.execute(toDrizzleSql(text2, params))), "query")
|
|
949
|
-
})), "transaction")
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
__name(drizzlePgExecutor, "drizzlePgExecutor");
|
|
953
1086
|
export {
|
|
954
1087
|
DEFAULT_NAMESPACE,
|
|
955
1088
|
DURABLE_PG_DDL,
|
|
@@ -965,6 +1098,7 @@ export {
|
|
|
965
1098
|
durableStepCheckpoints,
|
|
966
1099
|
durableWorkflowRuns,
|
|
967
1100
|
ensureDrizzlePgDurableSchema,
|
|
1101
|
+
rowsOf,
|
|
968
1102
|
toDrizzleSql
|
|
969
1103
|
};
|
|
970
1104
|
//# sourceMappingURL=index.js.map
|