@pikku/mongodb 0.12.6 → 0.12.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @pikku/mongodb
2
2
 
3
+ ## 0.12.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 92cd5b1: feat(workflow): workflow-owned step retries + stable invocationId
8
+
9
+ The workflow — not the queue — now owns step retry policy, and each step
10
+ invocation gets a stable idempotency key.
11
+ - **Default `retries: 5` with exponential backoff.** A step with no `retries`
12
+ previously inherited the queue's bare default (e.g. pg-boss `retry_limit 2`,
13
+ no backoff) so retries fired instantly and couldn't outlast a transient
14
+ outage. Retries now default to 5 with backoff, resolved at the workflow layer.
15
+ - **`retries: 0` is honored.** Dispatch previously passed `undefined` options
16
+ for `retries: 0`, letting the queue re-run a non-idempotent step up to its own
17
+ default. The resolved policy now always sets `attempts` (`retries: 0` →
18
+ `attempts: 1`), so the queue never second-guesses the workflow. The persisted
19
+ step retries and the dispatched `attempts` are resolved together so
20
+ "retries exhausted" and "no more redeliveries" are the same event.
21
+ - **`workflowStep.invocationId`** — a deterministic, dependency-free
22
+ `uuidv5(runId:stepName)` handed to every step. Unlike `stepId` (minted per
23
+ attempt), it is identical across retries, so a step can dedupe on it
24
+ (`ON CONFLICT (invocationId)`, Stripe idempotency keys, etc.).
25
+ - **queue-bullmq**: `mapPikkuJobToBull` now maps `backoff` (previously dropped,
26
+ so a step's backoff silently never applied on Redis), and `registerQueues`
27
+ throws a clear error when no logger is available (matching queue-pg-boss).
28
+ - **Dispatch failures are recoverable, not fatal.** A step is now marked
29
+ `scheduled` only _after_ it is successfully handed to its transport (queue or
30
+ scheduler) — a failed hand-off leaves it `pending` so a replay re-dispatches
31
+ it, instead of stranding it in `scheduled` (replay would pause forever on a
32
+ job that was never enqueued). A transport outage (e.g. pg-boss momentarily
33
+ down) is surfaced as a new `WorkflowDispatchException`, which the orchestrator
34
+ treats as transient: the run is left running and the orchestrator job is
35
+ rethrown for redelivery (it replays idempotently from the snapshot) rather
36
+ than the whole run being marked `failed`. The orchestrator job now also
37
+ carries its own retry policy, so this holds even when the orchestrator queue
38
+ is configured `retry_limit 0`. A genuine step error still fails the run.
39
+ - **Same step name can be invoked multiple times in one run.** Step rows are now
40
+ keyed per _invocation_: the Nth reach of a step name in a replay resolves to a
41
+ physical key (`name` for the first, `name#N` for repeats), so a literal
42
+ duplicate name no longer clobbers the earlier step's state. The first reach
43
+ keeps the bare name, so existing rows, graph-node matching and `invocationId`s
44
+ are unchanged. Ordinals are derived deterministically from DSL execution order
45
+ and reset each replay.
46
+ - **Step provenance (`fromStepName`) + graph cycles.** Every step now records
47
+ the predecessor it was scheduled from (`fromStepName`; entry steps have none),
48
+ persisted on the step row across all stores (in-memory, kysely, redis,
49
+ mongodb, cloudflare DO) and carried in the queued payload. The DSL wire
50
+ exposes the derived `fromInvocationId` (`uuidv5(runId:fromStepName)`) so
51
+ consumers get the stable predecessor key without a second persisted id —
52
+ `fromStepName` is the source of truth (it is replay-deterministic; `stepId`,
53
+ minted per row, is not). This makes the walked path reconstructable even when
54
+ a node is reached more than once: in `a → b → a → c` the second `a` is a
55
+ distinct ordinal instance (`a#1`) whose `fromStepName` is `b`.
56
+ The graph runner now supports **cycles**: a forward edge into an
57
+ already-started node still collapses to a single run (joins/diamonds are
58
+ unchanged), but a _back-edge_ — one whose target can reach its source — fires
59
+ a fresh ordinal instance, so a node can loop back to itself. Termination is
60
+ the graph's responsibility (branch routing must converge); the engine enforces
61
+ no visit cap.
62
+
63
+ - Updated dependencies [92cd5b1]
64
+ - @pikku/core@0.12.38
65
+
3
66
  ## 0.12.6
4
67
 
5
68
  ### Patch Changes
@@ -25,6 +25,6 @@ export declare class MongoDBSecretService implements SecretService {
25
25
  hasSecret(key: string): Promise<boolean>;
26
26
  setSecret(key: string, value: unknown): Promise<void>;
27
27
  deleteSecret(key: string): Promise<void>;
28
- getSecrets(keys: string[]): Promise<Record<string, unknown>>;
28
+ getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<T>;
29
29
  rotateKEK(): Promise<number>;
30
30
  }
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '@pikku/core';
2
- import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
2
+ import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
3
3
  import type { Db } from 'mongodb';
4
4
  export declare class MongoDBWorkflowService extends PikkuWorkflowService {
5
5
  private initialized;
@@ -19,7 +19,7 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
19
19
  protected insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: {
20
20
  retries?: number;
21
21
  retryDelay?: string | number;
22
- }): Promise<StepState>;
22
+ }, fromStepName?: string): Promise<StepState>;
23
23
  getStepState(runId: string, stepName: string): Promise<StepState>;
24
24
  getRunHistory(runId: string): Promise<Array<StepState & {
25
25
  stepName: string;
@@ -40,6 +40,11 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
40
40
  branchKeys: Record<string, string>;
41
41
  }>;
42
42
  getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
43
+ getStepInstances(runId: string): Promise<Array<{
44
+ stepName: string;
45
+ status: StepStatus;
46
+ fromStepName?: string;
47
+ }>>;
43
48
  getNodeResults(runId: string, nodeIds: string[]): Promise<Record<string, any>>;
44
49
  protected setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
45
50
  protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
@@ -62,7 +62,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
62
62
  },
63
63
  });
64
64
  }
65
- async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions) {
65
+ async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions, fromStepName) {
66
66
  const stepId = crypto.randomUUID();
67
67
  const now = new Date();
68
68
  await this.steps.insertOne({
@@ -77,6 +77,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
77
77
  branchTaken: null,
78
78
  retries: stepOptions?.retries ?? null,
79
79
  retryDelay: stepOptions?.retryDelay?.toString() ?? null,
80
+ fromStepName: fromStepName ?? null,
80
81
  createdAt: now,
81
82
  updatedAt: now,
82
83
  });
@@ -89,6 +90,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
89
90
  attemptCount: 1,
90
91
  retries: stepOptions?.retries,
91
92
  retryDelay: stepOptions?.retryDelay?.toString(),
93
+ fromStepName,
92
94
  createdAt: now,
93
95
  updatedAt: now,
94
96
  };
@@ -112,6 +114,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
112
114
  attemptCount,
113
115
  retries: row.retries != null ? Number(row.retries) : undefined,
114
116
  retryDelay: row.retryDelay ?? undefined,
117
+ fromStepName: row.fromStepName ?? undefined,
115
118
  createdAt: new Date(row.createdAt),
116
119
  updatedAt: new Date(row.updatedAt),
117
120
  };
@@ -240,6 +243,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
240
243
  attemptCount,
241
244
  retries: row.retries != null ? Number(row.retries) : undefined,
242
245
  retryDelay: row.retryDelay ?? undefined,
246
+ fromStepName: row.fromStepName ?? undefined,
243
247
  createdAt: new Date(row.createdAt),
244
248
  updatedAt: new Date(row.updatedAt),
245
249
  };
@@ -293,6 +297,17 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
293
297
  const existingStepNames = new Set(result.map((r) => r.stepName));
294
298
  return nodeIds.filter((id) => !existingStepNames.has(id));
295
299
  }
300
+ async getStepInstances(runId) {
301
+ const rows = await this.steps
302
+ .find({ workflowRunId: runId })
303
+ .project({ stepName: 1, status: 1, fromStepName: 1 })
304
+ .toArray();
305
+ return rows.map((r) => ({
306
+ stepName: r.stepName,
307
+ status: r.status,
308
+ fromStepName: r.fromStepName ?? undefined,
309
+ }));
310
+ }
296
311
  async getNodeResults(runId, nodeIds) {
297
312
  if (nodeIds.length === 0)
298
313
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/mongodb",
3
- "version": "0.12.6",
3
+ "version": "0.12.7",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -17,7 +17,7 @@
17
17
  "prepublishOnly": "yarn build"
18
18
  },
19
19
  "peerDependencies": {
20
- "@pikku/core": "^0.12.27"
20
+ "@pikku/core": "^0.12.38"
21
21
  },
22
22
  "dependencies": {
23
23
  "mongodb": "^6.12.0"
@@ -131,7 +131,9 @@ export class MongoDBSecretService implements SecretService {
131
131
  await this.logAudit(key, 'delete')
132
132
  }
133
133
 
134
- async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
134
+ async getSecrets<
135
+ T extends Record<string, unknown> = Record<string, unknown>,
136
+ >(keys: (keyof T & string)[]): Promise<T> {
135
137
  const rows = await this.secrets
136
138
  .find({ _id: { $in: keys } } as any)
137
139
  .toArray()
@@ -148,7 +150,7 @@ export class MongoDBSecretService implements SecretService {
148
150
  // skip secrets that fail to decrypt
149
151
  }
150
152
  }
151
- return out
153
+ return out as T
152
154
  }
153
155
 
154
156
  async rotateKEK(): Promise<number> {
@@ -5,6 +5,7 @@ import {
5
5
  type WorkflowRun,
6
6
  type WorkflowRunWire,
7
7
  type StepState,
8
+ type StepStatus,
8
9
  type WorkflowStatus,
9
10
  type WorkflowVersionStatus,
10
11
  } from '@pikku/core/workflow'
@@ -41,6 +42,7 @@ interface WorkflowStepDoc {
41
42
  branchTaken: string | null
42
43
  retries: number | null
43
44
  retryDelay: string | null
45
+ fromStepName?: string | null
44
46
  createdAt: Date
45
47
  updatedAt: Date
46
48
  }
@@ -172,7 +174,8 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
172
174
  stepName: string,
173
175
  rpcName: string | null,
174
176
  data: any,
175
- stepOptions?: { retries?: number; retryDelay?: string | number }
177
+ stepOptions?: { retries?: number; retryDelay?: string | number },
178
+ fromStepName?: string
176
179
  ): Promise<StepState> {
177
180
  const stepId = crypto.randomUUID()
178
181
  const now = new Date()
@@ -189,6 +192,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
189
192
  branchTaken: null,
190
193
  retries: stepOptions?.retries ?? null,
191
194
  retryDelay: stepOptions?.retryDelay?.toString() ?? null,
195
+ fromStepName: fromStepName ?? null,
192
196
  createdAt: now,
193
197
  updatedAt: now,
194
198
  })
@@ -203,6 +207,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
203
207
  attemptCount: 1,
204
208
  retries: stepOptions?.retries,
205
209
  retryDelay: stepOptions?.retryDelay?.toString(),
210
+ fromStepName,
206
211
  createdAt: now,
207
212
  updatedAt: now,
208
213
  }
@@ -232,6 +237,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
232
237
  attemptCount,
233
238
  retries: row.retries != null ? Number(row.retries) : undefined,
234
239
  retryDelay: row.retryDelay ?? undefined,
240
+ fromStepName: row.fromStepName ?? undefined,
235
241
  createdAt: new Date(row.createdAt),
236
242
  updatedAt: new Date(row.updatedAt),
237
243
  }
@@ -427,6 +433,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
427
433
  attemptCount,
428
434
  retries: row.retries != null ? Number(row.retries) : undefined,
429
435
  retryDelay: row.retryDelay ?? undefined,
436
+ fromStepName: row.fromStepName ?? undefined,
430
437
  createdAt: new Date(row.createdAt),
431
438
  updatedAt: new Date(row.updatedAt),
432
439
  }
@@ -500,6 +507,20 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
500
507
  return nodeIds.filter((id) => !existingStepNames.has(id))
501
508
  }
502
509
 
510
+ async getStepInstances(runId: string): Promise<
511
+ Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
512
+ > {
513
+ const rows = await this.steps
514
+ .find({ workflowRunId: runId })
515
+ .project({ stepName: 1, status: 1, fromStepName: 1 })
516
+ .toArray()
517
+ return rows.map((r: any) => ({
518
+ stepName: r.stepName,
519
+ status: r.status as StepStatus,
520
+ fromStepName: r.fromStepName ?? undefined,
521
+ }))
522
+ }
523
+
503
524
  async getNodeResults(
504
525
  runId: string,
505
526
  nodeIds: string[]