@dudousxd/nestjs-durable-store-prisma 0.17.0 → 0.18.1

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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { StateStore, WorkflowRun, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
1
+ import { StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter } from '@dudousxd/nestjs-durable-core';
2
2
 
3
3
  interface RunRow {
4
4
  id: string;
@@ -82,10 +82,23 @@ interface Delegate<Row> {
82
82
  deleteMany(args?: Args): Promise<{
83
83
  count: number;
84
84
  }>;
85
+ count(args?: Args): Promise<number>;
85
86
  /** Aggregate form — used only by `runFacets`, whose rows are `(status, origin, count)` cells, not
86
87
  * model rows, so the result type is stated at the call site rather than derived from `Row`. */
87
88
  groupBy(args: Args): Promise<Record<string, unknown>[]>;
88
89
  }
90
+ interface ScheduleRow {
91
+ id: string;
92
+ namespace: string;
93
+ workflow: string;
94
+ paused: boolean;
95
+ nextFireAt: bigint | null;
96
+ tags: unknown;
97
+ spec: unknown;
98
+ state: unknown;
99
+ createdAt: Date;
100
+ updatedAt: Date;
101
+ }
89
102
  interface DurablePrismaTx {
90
103
  durableWorkflowRun: Delegate<RunRow>;
91
104
  durableStepCheckpoint: Delegate<CheckpointRow>;
@@ -93,6 +106,8 @@ interface DurablePrismaTx {
93
106
  durableSignalWaiter: Delegate<WaiterRow>;
94
107
  durableBufferedSignal: Delegate<BufferedSignalRow>;
95
108
  durableBufferedEvent: Delegate<BufferedEventRow>;
109
+ /** Only needed for persisted schedules (`engine.schedules`) — add the `DurableSchedule` model. */
110
+ durableSchedule?: Delegate<ScheduleRow>;
96
111
  }
97
112
  interface DurablePrismaClient extends DurablePrismaTx {
98
113
  /** Prisma's interactive-transaction form: runs `fn` with a tx-scoped client and commits on resolve. */
@@ -111,13 +126,16 @@ interface DurablePrismaClient extends DurablePrismaTx {
111
126
  declare class PrismaStateStore implements StateStore {
112
127
  private readonly db;
113
128
  constructor(db: DurablePrismaClient);
129
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
114
130
  createRun(run: WorkflowRun): Promise<void>;
115
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
131
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
116
132
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
117
133
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
118
134
  private reindexAttributes;
119
135
  getRun(runId: string): Promise<WorkflowRun | null>;
120
136
  deleteRun(runId: string): Promise<void>;
137
+ deleteRuns(runIds: string[]): Promise<void>;
138
+ pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
121
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
122
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
123
141
  transaction<T>(work: (tx: {
@@ -133,6 +151,7 @@ declare class PrismaStateStore implements StateStore {
133
151
  /** The `where` every run query shares — {@link listRuns} pages it, {@link runFacets} groups it. */
134
152
  private runWhere;
135
153
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
154
+ countRuns(query: Omit<RunQuery, 'limit' | 'offset'>): Promise<number>;
136
155
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
137
156
  * console can show whole-set counts next to a bounded page instead of downloading every run to
138
157
  * count them in the browser. */
@@ -142,6 +161,13 @@ declare class PrismaStateStore implements StateStore {
142
161
  * attribute axes live outside the row (json column / side table) and are counted from a bounded
143
162
  * page of runs instead — see `RunValueFacetOptions.scan`. */
144
163
  runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
164
+ private schedules;
165
+ saveSchedule(record: ScheduleRecord): Promise<void>;
166
+ getSchedule(id: string): Promise<ScheduleRecord | null>;
167
+ /** One conditional `updateMany`: the compare-and-set on `next_fire_at` is a single statement. */
168
+ updateSchedule(id: string, patch: Partial<Omit<ScheduleRecord, 'id' | 'createdAt'>>, expectedNextFireAt?: number | null): Promise<boolean>;
169
+ deleteSchedule(id: string): Promise<boolean>;
170
+ listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
145
171
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
146
172
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
147
173
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StateStore, WorkflowRun, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
1
+ import { StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter } from '@dudousxd/nestjs-durable-core';
2
2
 
3
3
  interface RunRow {
4
4
  id: string;
@@ -82,10 +82,23 @@ interface Delegate<Row> {
82
82
  deleteMany(args?: Args): Promise<{
83
83
  count: number;
84
84
  }>;
85
+ count(args?: Args): Promise<number>;
85
86
  /** Aggregate form — used only by `runFacets`, whose rows are `(status, origin, count)` cells, not
86
87
  * model rows, so the result type is stated at the call site rather than derived from `Row`. */
87
88
  groupBy(args: Args): Promise<Record<string, unknown>[]>;
88
89
  }
90
+ interface ScheduleRow {
91
+ id: string;
92
+ namespace: string;
93
+ workflow: string;
94
+ paused: boolean;
95
+ nextFireAt: bigint | null;
96
+ tags: unknown;
97
+ spec: unknown;
98
+ state: unknown;
99
+ createdAt: Date;
100
+ updatedAt: Date;
101
+ }
89
102
  interface DurablePrismaTx {
90
103
  durableWorkflowRun: Delegate<RunRow>;
91
104
  durableStepCheckpoint: Delegate<CheckpointRow>;
@@ -93,6 +106,8 @@ interface DurablePrismaTx {
93
106
  durableSignalWaiter: Delegate<WaiterRow>;
94
107
  durableBufferedSignal: Delegate<BufferedSignalRow>;
95
108
  durableBufferedEvent: Delegate<BufferedEventRow>;
109
+ /** Only needed for persisted schedules (`engine.schedules`) — add the `DurableSchedule` model. */
110
+ durableSchedule?: Delegate<ScheduleRow>;
96
111
  }
97
112
  interface DurablePrismaClient extends DurablePrismaTx {
98
113
  /** Prisma's interactive-transaction form: runs `fn` with a tx-scoped client and commits on resolve. */
@@ -111,13 +126,16 @@ interface DurablePrismaClient extends DurablePrismaTx {
111
126
  declare class PrismaStateStore implements StateStore {
112
127
  private readonly db;
113
128
  constructor(db: DurablePrismaClient);
129
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
114
130
  createRun(run: WorkflowRun): Promise<void>;
115
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
131
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
116
132
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
117
133
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
118
134
  private reindexAttributes;
119
135
  getRun(runId: string): Promise<WorkflowRun | null>;
120
136
  deleteRun(runId: string): Promise<void>;
137
+ deleteRuns(runIds: string[]): Promise<void>;
138
+ pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
121
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
122
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
123
141
  transaction<T>(work: (tx: {
@@ -133,6 +151,7 @@ declare class PrismaStateStore implements StateStore {
133
151
  /** The `where` every run query shares — {@link listRuns} pages it, {@link runFacets} groups it. */
134
152
  private runWhere;
135
153
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
154
+ countRuns(query: Omit<RunQuery, 'limit' | 'offset'>): Promise<number>;
136
155
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
137
156
  * console can show whole-set counts next to a bounded page instead of downloading every run to
138
157
  * count them in the browser. */
@@ -142,6 +161,13 @@ declare class PrismaStateStore implements StateStore {
142
161
  * attribute axes live outside the row (json column / side table) and are counted from a bounded
143
162
  * page of runs instead — see `RunValueFacetOptions.scan`. */
144
163
  runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
164
+ private schedules;
165
+ saveSchedule(record: ScheduleRecord): Promise<void>;
166
+ getSchedule(id: string): Promise<ScheduleRecord | null>;
167
+ /** One conditional `updateMany`: the compare-and-set on `next_fire_at` is a single statement. */
168
+ updateSchedule(id: string, patch: Partial<Omit<ScheduleRecord, 'id' | 'createdAt'>>, expectedNextFireAt?: number | null): Promise<boolean>;
169
+ deleteSchedule(id: string): Promise<boolean>;
170
+ listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
145
171
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
146
172
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
147
173
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/prisma-state-store.ts
5
- import { RUN_VALUE_FACET_SCAN, attributePredicateOperands, axisIsRunColumn, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, runValueFacetsFromRuns } from "@dudousxd/nestjs-durable-core";
5
+ 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";
6
6
  function namespaceWhere(namespace) {
7
7
  if (namespace === void 0) return {};
8
8
  return {
@@ -18,13 +18,54 @@ var PrismaStateStore = class {
18
18
  constructor(db) {
19
19
  this.db = db;
20
20
  }
21
+ async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
22
+ return this.db.$transaction(async (tx) => {
23
+ const where = {
24
+ workflow,
25
+ status: {
26
+ in: SINGLETON_ACTIVE_STATUSES
27
+ }
28
+ };
29
+ await tx.durableWorkflowRun.updateMany({
30
+ where,
31
+ data: {
32
+ workflow
33
+ }
34
+ });
35
+ const rows = await tx.durableWorkflowRun.findMany({
36
+ where
37
+ });
38
+ const runs = rows.map(fromRunRow);
39
+ const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
40
+ if (!tags) {
41
+ const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
42
+ if (patch) await tx.durableWorkflowRun.update({
43
+ where: {
44
+ id: runId
45
+ },
46
+ data: toRunPatch(patch)
47
+ });
48
+ return false;
49
+ }
50
+ await tx.durableWorkflowRun.update({
51
+ where: {
52
+ id: runId
53
+ },
54
+ data: {
55
+ tags
56
+ }
57
+ });
58
+ return true;
59
+ });
60
+ }
21
61
  async createRun(run) {
22
62
  await this.db.durableWorkflowRun.create({
23
63
  data: toRunData(run)
24
64
  });
25
65
  await this.reindexAttributes(run.id, run.searchAttributes);
26
66
  }
27
- async updateRun(runId, patch) {
67
+ async updateRun(runId, inputPatch) {
68
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.getRun(id)) : inputPatch;
28
69
  await this.db.durableWorkflowRun.update({
29
70
  where: {
30
71
  id: runId
@@ -76,6 +117,99 @@ var PrismaStateStore = class {
76
117
  }
77
118
  });
78
119
  }
120
+ async deleteRuns(runIds) {
121
+ if (runIds.length === 0) return;
122
+ await this.db.$transaction(async (tx) => {
123
+ await tx.durableStepCheckpoint.deleteMany({
124
+ where: {
125
+ runId: {
126
+ in: runIds
127
+ }
128
+ }
129
+ });
130
+ await tx.durableSignalWaiter.deleteMany({
131
+ where: {
132
+ runId: {
133
+ in: runIds
134
+ }
135
+ }
136
+ });
137
+ await tx.durableRunAttribute.deleteMany({
138
+ where: {
139
+ runId: {
140
+ in: runIds
141
+ }
142
+ }
143
+ });
144
+ await tx.durableWorkflowRun.deleteMany({
145
+ where: {
146
+ id: {
147
+ in: runIds
148
+ }
149
+ }
150
+ });
151
+ });
152
+ }
153
+ async pruneTerminalRuns(policy, nowMs, limit) {
154
+ const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
155
+ if (statuses.length === 0 || limit <= 0) return 0;
156
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
157
+ const scoped = this.runWhere({
158
+ ...policy.scope ?? {},
159
+ statuses
160
+ });
161
+ const ids = /* @__PURE__ */ new Set();
162
+ if (policy.maxAge != null) {
163
+ const cutoff = new Date(nowMs - parseDuration(policy.maxAge));
164
+ const rows = await this.db.durableWorkflowRun.findMany({
165
+ where: {
166
+ AND: [
167
+ scoped,
168
+ {
169
+ updatedAt: {
170
+ lt: cutoff
171
+ }
172
+ }
173
+ ]
174
+ },
175
+ select: {
176
+ id: true
177
+ },
178
+ orderBy: {
179
+ updatedAt: "asc"
180
+ },
181
+ take: limit
182
+ });
183
+ for (const r of rows) ids.add(r.id);
184
+ }
185
+ if (policy.maxCount != null && ids.size < limit) {
186
+ const rows = await this.db.durableWorkflowRun.findMany({
187
+ where: scoped,
188
+ select: {
189
+ id: true
190
+ },
191
+ orderBy: [
192
+ {
193
+ updatedAt: "desc"
194
+ },
195
+ {
196
+ id: "desc"
197
+ }
198
+ ],
199
+ skip: policy.maxCount,
200
+ take: limit
201
+ });
202
+ for (const r of rows) {
203
+ if (ids.size >= limit) break;
204
+ ids.add(r.id);
205
+ }
206
+ }
207
+ const doomed = [
208
+ ...ids
209
+ ].slice(0, limit);
210
+ await this.deleteRuns(doomed);
211
+ return doomed.length;
212
+ }
79
213
  async getCheckpoint(runId, seq) {
80
214
  const row = await this.db.durableStepCheckpoint.findUnique({
81
215
  where: {
@@ -259,6 +393,11 @@ var PrismaStateStore = class {
259
393
  });
260
394
  return rows.map(fromRunRow);
261
395
  }
396
+ async countRuns(query) {
397
+ return this.db.durableWorkflowRun.count({
398
+ where: this.runWhere(query)
399
+ });
400
+ }
262
401
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
263
402
  * console can show whole-set counts next to a bounded page instead of downloading every run to
264
403
  * count them in the browser. */
@@ -305,6 +444,101 @@ var PrismaStateStore = class {
305
444
  count: r._count._all
306
445
  })), opts);
307
446
  }
447
+ // ---- persisted schedules ----------------------------------------------------------------------
448
+ schedules() {
449
+ const delegate = this.db.durableSchedule;
450
+ if (!delegate) {
451
+ throw new Error("persisted schedules need the DurableSchedule model \u2014 copy it from prisma/schema.prisma into your schema and re-run `prisma generate`");
452
+ }
453
+ return delegate;
454
+ }
455
+ async saveSchedule(record) {
456
+ const data = toScheduleData(record);
457
+ await this.schedules().upsert({
458
+ where: {
459
+ id: record.id
460
+ },
461
+ create: data,
462
+ update: data
463
+ });
464
+ }
465
+ async getSchedule(id) {
466
+ const row = await this.schedules().findUnique({
467
+ where: {
468
+ id
469
+ }
470
+ });
471
+ return row ? fromScheduleRow(row) : null;
472
+ }
473
+ /** One conditional `updateMany`: the compare-and-set on `next_fire_at` is a single statement. */
474
+ async updateSchedule(id, patch, expectedNextFireAt) {
475
+ const data = {};
476
+ if (patch.namespace !== void 0) data.namespace = patch.namespace;
477
+ if (patch.workflow !== void 0) data.workflow = patch.workflow;
478
+ if (patch.paused !== void 0) data.paused = patch.paused;
479
+ if ("nextFireAt" in patch) data.nextFireAt = patch.nextFireAt == null ? null : BigInt(patch.nextFireAt);
480
+ if ("tags" in patch) data.tags = jsonOrNull(patch.tags);
481
+ if (patch.spec !== void 0) data.spec = patch.spec;
482
+ if (patch.state !== void 0) data.state = patch.state;
483
+ if (patch.updatedAt !== void 0) data.updatedAt = patch.updatedAt;
484
+ if (!Object.keys(data).length) return await this.getSchedule(id) !== null;
485
+ const where = {
486
+ id
487
+ };
488
+ if (expectedNextFireAt !== void 0) where.nextFireAt = expectedNextFireAt === null ? null : BigInt(expectedNextFireAt);
489
+ const { count } = await this.schedules().updateMany({
490
+ where,
491
+ data
492
+ });
493
+ return count > 0;
494
+ }
495
+ async deleteSchedule(id) {
496
+ const { count } = await this.schedules().deleteMany({
497
+ where: {
498
+ id
499
+ }
500
+ });
501
+ return count > 0;
502
+ }
503
+ async listSchedules(query) {
504
+ const where = {};
505
+ if (query.namespace !== void 0) where.namespace = query.namespace;
506
+ if (query.workflow !== void 0) where.workflow = query.workflow;
507
+ if (query.paused !== void 0) where.paused = query.paused;
508
+ if (query.tag !== void 0) where.tags = {
509
+ array_contains: query.tag
510
+ };
511
+ if (query.dueBy !== void 0) {
512
+ where.AND = [
513
+ {
514
+ paused: false
515
+ },
516
+ {
517
+ nextFireAt: {
518
+ lte: BigInt(query.dueBy)
519
+ }
520
+ }
521
+ ];
522
+ }
523
+ const rows = await this.schedules().findMany({
524
+ where,
525
+ // Soonest-due first, never-firing (NULL) last, id as the stable tie-break.
526
+ orderBy: [
527
+ {
528
+ nextFireAt: {
529
+ sort: "asc",
530
+ nulls: "last"
531
+ }
532
+ },
533
+ {
534
+ id: "asc"
535
+ }
536
+ ],
537
+ take: query.limit,
538
+ skip: query.offset
539
+ });
540
+ return rows.map(fromScheduleRow);
541
+ }
308
542
  async listCheckpoints(runId) {
309
543
  const rows = await this.db.durableStepCheckpoint.findMany({
310
544
  where: {
@@ -626,6 +860,39 @@ function fromCheckpointRow(row) {
626
860
  };
627
861
  }
628
862
  __name(fromCheckpointRow, "fromCheckpointRow");
863
+ function toScheduleData(r) {
864
+ return {
865
+ id: r.id,
866
+ namespace: r.namespace,
867
+ workflow: r.workflow,
868
+ paused: r.paused,
869
+ nextFireAt: r.nextFireAt == null ? null : BigInt(r.nextFireAt),
870
+ tags: jsonOrNull(r.tags),
871
+ spec: r.spec,
872
+ state: r.state,
873
+ createdAt: r.createdAt,
874
+ updatedAt: r.updatedAt
875
+ };
876
+ }
877
+ __name(toScheduleData, "toScheduleData");
878
+ function fromScheduleRow(row) {
879
+ const tags = Array.isArray(row.tags) ? row.tags : void 0;
880
+ return {
881
+ id: row.id,
882
+ namespace: row.namespace,
883
+ workflow: row.workflow,
884
+ paused: row.paused,
885
+ nextFireAt: row.nextFireAt == null ? null : Number(row.nextFireAt),
886
+ ...tags?.length ? {
887
+ tags
888
+ } : {},
889
+ spec: row.spec ?? {},
890
+ state: row.state ?? {},
891
+ createdAt: row.createdAt,
892
+ updatedAt: row.updatedAt
893
+ };
894
+ }
895
+ __name(fromScheduleRow, "fromScheduleRow");
629
896
 
630
897
  // src/schema.ts
631
898
  function durableManagedTables() {
@@ -635,7 +902,8 @@ function durableManagedTables() {
635
902
  "durable_run_attributes",
636
903
  "durable_signal_waiters",
637
904
  "durable_buffered_signals",
638
- "durable_buffered_events"
905
+ "durable_buffered_events",
906
+ "durable_schedules"
639
907
  ];
640
908
  }
641
909
  __name(durableManagedTables, "durableManagedTables");