@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 +63 -0
- package/dist/src/mongodb-secret-service.d.ts +1 -1
- package/dist/src/mongodb-workflow-service.d.ts +7 -2
- package/dist/src/mongodb-workflow-service.js +16 -1
- package/package.json +2 -2
- package/src/mongodb-secret-service.ts +4 -2
- package/src/mongodb-workflow-service.ts +22 -1
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<
|
|
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.
|
|
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.
|
|
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
|
|
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[]
|