@fabricorg/platform-temporal 4.1.1 → 4.2.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/CHANGELOG.md +26 -0
- package/README.md +35 -0
- package/dist/index.cjs +52 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d5fa7c2: Saga signals can now carry a stable identity, and a new receiver drops redeliveries (issue #23).
|
|
8
|
+
|
|
9
|
+
**Release note: new dedup semantics.** A sender that retries `client.signal()` after a timeout could previously cause a saga to act on the same signal twice. A workflow that receives signals through the new inbox now acts on each `signalId` once.
|
|
10
|
+
|
|
11
|
+
- `SagaSignalEnvelope` gains the optional `signalId` field. It identifies one logical signal, and senders should keep it the same on each redelivery, for example a webhook delivery ID or an approval decision ID. No existing field changed.
|
|
12
|
+
- `createSagaSignalDispatcher()` still forwards the whole envelope. It now throws synchronously on an envelope whose `signalId` is present but blank, before calling the client. It stays synchronous, so a synchronous throw from the injected client still propagates as before.
|
|
13
|
+
- New export `createSagaSignalInbox(options?)`, with the types `SagaSignalInbox`, `SagaSignalInboxOptions` and `SagaSignalInboxSnapshot`. It is a deterministic receiver for workflow code with no `@temporalio/workflow` import, and the package gains no new peer dependency.
|
|
14
|
+
- `receive(envelope)` buffers FIFO per signal name. It returns `false` and drops the envelope when its `signalId` was already received, under any signal name.
|
|
15
|
+
- Envelopes without a `signalId` are not deduplicated.
|
|
16
|
+
- `take(signalName)` dequeues the oldest buffered envelope, and `pending(signalName)` counts them, for use with `condition()`.
|
|
17
|
+
- Dedup state is per inbox instance, so it lasts for one workflow run. To carry it across continue-as-new, pass `snapshot()` into the next run and hand it back to `createSagaSignalInbox()`. The snapshot holds the seen IDs and any accepted-but-untaken envelopes, so neither is lost.
|
|
18
|
+
- The seen-ID set is not pruned and grows with each distinct `signalId`, including across continue-as-new.
|
|
19
|
+
- Workflows that do not use the inbox behave as before.
|
|
20
|
+
|
|
21
|
+
## 4.1.2
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- fd1f88f: Publish the current packaged documentation, product names and resolved workspace dependency minimums under new patch versions. The release audit found retained package versions whose consumer-visible metadata differs from their registry tarballs. Runtime source is unchanged apart from regenerated generator version stamps.
|
|
26
|
+
|
|
27
|
+
The release guard now retains nested manifest fields while canonicalizing key order, so dependency, peer and export changes cannot disappear from the comparison. Development-only dependencies remain excluded.
|
|
28
|
+
|
|
3
29
|
## 4.1.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,3 +18,38 @@ optional occurrence discriminator for callers constructing those identities dire
|
|
|
18
18
|
identities to make a retried or replayed activity idempotent. `SAGA_SIGNALS` declares
|
|
19
19
|
the stable signal names (`saga.proceed`, `saga.cancel`, `saga.approval`) a workflow may register
|
|
20
20
|
handlers for.
|
|
21
|
+
|
|
22
|
+
## Signal identity and the saga signal inbox
|
|
23
|
+
|
|
24
|
+
A sender may deliver the same signal more than once, for example when it retries
|
|
25
|
+
`client.signal()` after a timeout. `SagaSignalEnvelope` has an optional `signalId` that
|
|
26
|
+
identifies one logical signal; reuse the same value on every redelivery, such as a
|
|
27
|
+
webhook delivery ID or an approval decision ID. `createSagaSignalDispatcher()` forwards the
|
|
28
|
+
whole envelope and throws on a blank `signalId` before sending.
|
|
29
|
+
|
|
30
|
+
`createSagaSignalInbox()` is the receiving side. It has no Temporal import and reads no
|
|
31
|
+
clock, randomness or process-global state, so it can run inside workflow code:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import * as wf from "@temporalio/workflow";
|
|
35
|
+
import { createSagaSignalInbox, SAGA_SIGNALS, type SagaSignalEnvelope } from "@fabricorg/platform-temporal";
|
|
36
|
+
|
|
37
|
+
const inbox = createSagaSignalInbox(input.inbox); // the previous run's inbox.snapshot(), if any
|
|
38
|
+
wf.setHandler(wf.defineSignal<[SagaSignalEnvelope]>(SAGA_SIGNALS.approval), (envelope) => {
|
|
39
|
+
inbox.receive(envelope);
|
|
40
|
+
});
|
|
41
|
+
if (await wf.condition(() => inbox.pending(SAGA_SIGNALS.approval) > 0, "1 day")) {
|
|
42
|
+
const approval = inbox.take(SAGA_SIGNALS.approval);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `receive()` buffers envelopes FIFO per signal name and returns `false`, dropping the
|
|
47
|
+
envelope, when its `signalId` was already received under any signal name.
|
|
48
|
+
- Envelopes without a `signalId` are buffered and not deduplicated.
|
|
49
|
+
- Dedup state belongs to the inbox instance, so it lasts for one workflow run. To keep it
|
|
50
|
+
across continue-as-new, pass `inbox.snapshot()` into the next run and hand it back to
|
|
51
|
+
`createSagaSignalInbox()`. The snapshot carries both the seen IDs and any envelope that was
|
|
52
|
+
accepted but not yet taken; carrying only `seenSignalIds()` would drop those envelopes and
|
|
53
|
+
then reject their redelivery.
|
|
54
|
+
- The remembered ID set grows with each distinct `signalId` and is not pruned, so
|
|
55
|
+
continue-as-new does not shrink it. Forgetting an ID would let its redelivery through.
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
createGovernedActionActivities: () => createGovernedActionActivities,
|
|
29
29
|
createSagaParentActivities: () => createSagaParentActivities,
|
|
30
30
|
createSagaSignalDispatcher: () => createSagaSignalDispatcher,
|
|
31
|
+
createSagaSignalInbox: () => createSagaSignalInbox,
|
|
31
32
|
createTemporalSagaDispatcher: () => createTemporalSagaDispatcher,
|
|
32
33
|
runCompensatedSaga: () => runCompensatedSaga,
|
|
33
34
|
sagaChildCausation: () => sagaChildCausation,
|
|
@@ -59,7 +60,56 @@ function createGovernedActionActivities(invoker) {
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
function createSagaSignalDispatcher(client) {
|
|
62
|
-
return (workflowId, envelope) =>
|
|
63
|
+
return (workflowId, envelope) => {
|
|
64
|
+
assertSignalIdentity(envelope);
|
|
65
|
+
return client.signal(workflowId, envelope.signal, envelope);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function assertSignalIdentity(envelope) {
|
|
69
|
+
if (envelope.signalId === void 0) return;
|
|
70
|
+
if (typeof envelope.signalId !== "string" || !envelope.signalId.trim()) {
|
|
71
|
+
throw new Error(`Saga signal "${envelope.signal}" for saga "${envelope.sagaId}" has a blank signalId; omit it or supply a stable non-blank identity.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function signalIdentity(envelope) {
|
|
75
|
+
return typeof envelope.signalId === "string" && envelope.signalId.trim() ? envelope.signalId : void 0;
|
|
76
|
+
}
|
|
77
|
+
function createSagaSignalInbox(options = {}) {
|
|
78
|
+
const seen = new Set(options.seenSignalIds ?? []);
|
|
79
|
+
const queues = /* @__PURE__ */ new Map();
|
|
80
|
+
const enqueue = (envelope) => {
|
|
81
|
+
const queue = queues.get(envelope.signal);
|
|
82
|
+
if (queue) queue.push(envelope);
|
|
83
|
+
else queues.set(envelope.signal, [envelope]);
|
|
84
|
+
};
|
|
85
|
+
for (const envelope of options.pending ?? []) {
|
|
86
|
+
const id = signalIdentity(envelope);
|
|
87
|
+
if (id !== void 0) seen.add(id);
|
|
88
|
+
enqueue(envelope);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
receive(envelope) {
|
|
92
|
+
const id = signalIdentity(envelope);
|
|
93
|
+
if (id !== void 0) {
|
|
94
|
+
if (seen.has(id)) return false;
|
|
95
|
+
seen.add(id);
|
|
96
|
+
}
|
|
97
|
+
enqueue(envelope);
|
|
98
|
+
return true;
|
|
99
|
+
},
|
|
100
|
+
take(signalName) {
|
|
101
|
+
return queues.get(signalName)?.shift();
|
|
102
|
+
},
|
|
103
|
+
pending(signalName) {
|
|
104
|
+
return queues.get(signalName)?.length ?? 0;
|
|
105
|
+
},
|
|
106
|
+
seenSignalIds() {
|
|
107
|
+
return [...seen];
|
|
108
|
+
},
|
|
109
|
+
snapshot() {
|
|
110
|
+
return { seenSignalIds: [...seen], pending: [...queues.values()].flat() };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
63
113
|
}
|
|
64
114
|
function startGovernedWorkflow(client, input) {
|
|
65
115
|
return client.start({ ...input, idConflictPolicy: "use_existing" });
|
|
@@ -279,6 +329,7 @@ async function runCompensatedSaga(activity, input) {
|
|
|
279
329
|
createGovernedActionActivities,
|
|
280
330
|
createSagaParentActivities,
|
|
281
331
|
createSagaSignalDispatcher,
|
|
332
|
+
createSagaSignalInbox,
|
|
282
333
|
createTemporalSagaDispatcher,
|
|
283
334
|
runCompensatedSaga,
|
|
284
335
|
sagaChildCausation,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import type { ActorType } from \"@fabricorg/platform\";\nimport type { WorkflowClient } from \"@temporalio/client\";\n\n/** Serializable input passed from deterministic workflows to a Platform activity. */\nexport interface GovernedActionActivityInput {\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tworkflowId: string;\n\trunId: string;\n\tstepId: string;\n\tparentInvocationId?: string;\n}\n\nexport interface GovernedActionInvoker {\n\tinvoke(input: {\n\t\tactionId: string; parameters: Record<string, unknown>;\n\t\ttenantId: string; spaceId: string; actorId: string; actorType: ActorType;\n\t\tidempotencyKey: string; correlationId: string; causationId?: string;\n\t\texecutionEvidence: { temporalWorkflowId: string; temporalRunId: string; temporalStepId: string };\n\t}): Promise<unknown>;\n}\n\nexport function temporalInvocationIdentity(input: Pick<GovernedActionActivityInput, \"workflowId\" | \"stepId\">) {\n\treturn { idempotencyKey: `temporal:${input.workflowId}:${input.stepId}`, correlationId: `temporal:${input.workflowId}` };\n}\n\n/** Activity implementation. It always enters through the governed Host; handlers are never called directly. */\nexport function createGovernedActionActivities(invoker: GovernedActionInvoker) {\n\treturn {\n\t\tinvokeGovernedAction: async (input: GovernedActionActivityInput) => {\n\t\t\tconst identity = temporalInvocationIdentity(input);\n\t\t\treturn invoker.invoke({\n\t\t\t\tactionId: input.actionId, parameters: input.parameters,\n\t\t\t\ttenantId: input.tenantId, spaceId: input.spaceId,\n\t\t\t\tactorId: input.actorId, actorType: input.actorType,\n\t\t\t\t...identity,\n\t\t\t\t...(input.parentInvocationId ? { causationId: input.parentInvocationId } : {}),\n\t\t\t\texecutionEvidence: { temporalWorkflowId: input.workflowId, temporalRunId: input.runId, temporalStepId: input.stepId },\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface SagaSignalEnvelope<T = unknown> { sagaId: string; signal: string; payload: T; actorId: string; receivedAt: string }\nexport interface TemporalSignalClient { signal(workflowId: string, signalName: string, payload: unknown): Promise<void> }\nexport function createSagaSignalDispatcher(client: TemporalSignalClient) {\n\treturn (workflowId: string, envelope: SagaSignalEnvelope) => client.signal(workflowId, envelope.signal, envelope);\n}\n\nexport interface WorkflowStartClient<THandle = unknown> {\n\tstart(input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[]; idConflictPolicy: \"use_existing\" }): Promise<THandle>;\n}\nexport function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THandle>, input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[] }) {\n\treturn client.start({ ...input, idConflictPolicy: \"use_existing\" });\n}\n\nexport interface SagaWorkflowBinding {\n\tactionId: `${string}.${string}`;\n\tworkflowType: string;\n\ttaskQueue: string;\n}\n\n/** Instance-scoped registry; registration order never changes resolution. */\nexport class SagaWorkflowRegistry {\n\tprivate readonly bindings = new Map<string, SagaWorkflowBinding>();\n\n\tconstructor(bindings: readonly SagaWorkflowBinding[] = []) {\n\t\tfor (const binding of bindings) this.register(binding);\n\t}\n\n\tregister(binding: SagaWorkflowBinding): void {\n\t\tif (this.bindings.has(binding.actionId)) {\n\t\t\tthrow new Error(`Saga workflow binding already registered: ${binding.actionId}`);\n\t\t}\n\t\tif (!binding.workflowType.trim() || !binding.taskQueue.trim()) {\n\t\t\tthrow new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);\n\t\t}\n\t\tthis.bindings.set(binding.actionId, { ...binding });\n\t}\n\n\trequire(actionId: string): SagaWorkflowBinding {\n\t\tconst binding = this.bindings.get(actionId);\n\t\tif (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);\n\t\treturn { ...binding };\n\t}\n}\n\nexport interface TemporalSagaDispatchInput {\n\tactionInvocationId: string;\n\tactionId: `${string}.${string}`;\n\ttenantId: string;\n\tspaceId: string;\n\tworkflowId: string;\n}\n\n/** Starts the registered parent workflow with an invocation-only durable identity. */\nexport function createTemporalSagaDispatcher<THandle>(\n\tclient: WorkflowStartClient<THandle>,\n\tregistry: SagaWorkflowRegistry,\n) {\n\treturn {\n\t\tdispatch: async (input: TemporalSagaDispatchInput) => {\n\t\t\tconst binding = registry.require(input.actionId);\n\t\t\tconst handle = await startGovernedWorkflow(client, {\n\t\t\t\tworkflowType: binding.workflowType,\n\t\t\t\tworkflowId: input.workflowId,\n\t\t\t\ttaskQueue: binding.taskQueue,\n\t\t\t\targs: [{\n\t\t\t\t\tparentInvocationId: input.actionInvocationId,\n\t\t\t\t\tactionId: input.actionId,\n\t\t\t\t\ttenantId: input.tenantId,\n\t\t\t\t\tspaceId: input.spaceId,\n\t\t\t\t}],\n\t\t\t});\n\t\t\tconst runId = runIdFromHandle(handle);\n\t\t\treturn { workflowId: input.workflowId, ...(runId ? { runId } : {}) };\n\t\t},\n\t};\n}\n\nfunction runIdFromHandle(handle: unknown): string | undefined {\n\tif (!handle || typeof handle !== \"object\") return undefined;\n\tconst candidate = (handle as { firstExecutionRunId?: unknown }).firstExecutionRunId;\n\treturn typeof candidate === \"string\" && candidate ? candidate : undefined;\n}\n\n/** Durable record of a single step completing within a saga parent. */\nexport interface SagaParentProgressRecord {\n\t/** The step that just completed. */\n\tstepId: string;\n\t/** Zero-based position of this step in the declared sequence. */\n\tindex: number;\n\t/** Total steps declared for this saga. */\n\ttotal: number;\n\t/** ISO-8601 timestamp the progress was recorded. */\n\tcompletedAt: string;\n}\n\nexport interface SagaParentProgressInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\tprogress: SagaParentProgressRecord;\n}\n\nexport interface SagaParentApprovalInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\t/** The step at which the saga paused for human approval. */\n\tstepId: string;\n\t/** Stable identifier for the approval gate, so a later signal can resume it. */\n\tapprovalId: string;\n\treason: string;\n}\n\nexport interface SagaParentCancellationInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\treason: string;\n\t/** Actor requesting the cancellation, for audit attribution. */\n\tactorId: string;\n}\n\nexport interface SagaParentLifecycle {\n\t/** Record durable progress as each step completes. */\n\trecordProgress(input: SagaParentProgressInput): Promise<void>;\n\t/** Park the parent for human approval at a specific step. */\n\tparkApproval(input: SagaParentApprovalInput): Promise<void>;\n\t/** Cancel the parent saga with an attributable reason. */\n\tcancel(input: SagaParentCancellationInput): Promise<void>;\n\t/** Complete the parent saga with its final result. */\n\tcomplete(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; result: Record<string, unknown> }): Promise<void>;\n\t/** Fail the parent saga, surfacing any compensation records. */\n\tfail(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; error: string; compensation: CompensationRecord[] }): Promise<void>;\n}\n\n/** Parent lifecycle remains explicit durable activities, never workflow-local state. */\nexport function createSagaParentActivities(lifecycle: SagaParentLifecycle) {\n\treturn {\n\t\trecordSagaProgress: (input: SagaParentProgressInput) => lifecycle.recordProgress({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"progress\",\n\t\t\t\t`${input.progress.stepId}:${input.progress.index}`,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tparkSagaApproval: (input: SagaParentApprovalInput) => lifecycle.parkApproval({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"approval\",\n\t\t\t\tinput.approvalId,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcancelSagaParent: (input: SagaParentCancellationInput) => lifecycle.cancel({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"cancel\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcompleteSagaParent: (input: Parameters<SagaParentLifecycle[\"complete\"]>[0]) =>\n\t\t\tlifecycle.complete({\n\t\t\t\t...input,\n\t\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\t\"complete\",\n\t\t\t\t).idempotencyKey,\n\t\t\t}),\n\t\tfailSagaParent: (input: Parameters<SagaParentLifecycle[\"fail\"]>[0]) => lifecycle.fail({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"fail\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t};\n}\n\n// ── Stable parent and child causation identities ───────────────────────────\n\n/**\n * Stable identity for a saga parent's own lifecycle activity (progress,\n * cancellation, approval parking, completion, failure). The activity stepId\n * is namespaced under `parent:` so it can never collide with a forward step or\n * a `compensate:` reversal.\n */\nexport function sagaParentActivityIdentity(\n\tworkflowId: string,\n\tactivity: SagaParentActivityKind,\n\tdiscriminator?: string,\n) {\n\treturn temporalInvocationIdentity({\n\t\tworkflowId,\n\t\tstepId: `parent:${activity}${discriminator ? `:${discriminator}` : \"\"}`,\n\t});\n}\n\nexport type SagaParentActivityKind = \"progress\" | \"approval\" | \"cancel\" | \"complete\" | \"fail\";\n\n/**\n * Stable causation identity linking a child step to its parent invocation.\n * The parent invocation ID becomes the causation ID; the step keeps its own\n * deterministic idempotency key and shares the workflow correlation ID.\n */\nexport function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string) {\n\treturn { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };\n}\n\n// ── Saga signal names ───────────────────────────────────────────────────────\n\n/** Stable signal names a saga workflow may register handlers for. */\nexport const SAGA_SIGNALS = {\n\tproceed: \"saga.proceed\",\n\tcancel: \"saga.cancel\",\n\tapproval: \"saga.approval\",\n} as const;\n\n/** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */\nexport function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient {\n\treturn {\n\t\tstart: ({ workflowType, workflowId, taskQueue, args }) =>\n\t\t\tclient.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: \"USE_EXISTING\" }),\n\t};\n}\n\nexport interface WorkerVersionEvidence { buildId: string; deploymentName: string; rampPercent?: number }\nexport function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void {\n\tif (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error(\"Temporal worker buildId and deploymentName are required for rollover evidence.\");\n}\n\n// ── Declared compensation ───────────────────────────────────────────────────\n\nexport interface CompensatedStep {\n\t/** Workflow-local step identity. Must match the compensation declared in the module manifest. */\n\tstepId: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\t/**\n\t * Governed action that reverses this step's effect. Omit for a step with\n\t * nothing to undo; a step whose effect is externally visible should declare one.\n\t */\n\tcompensateWith?: { actionId: string; parameters?: Record<string, unknown> };\n}\n\nexport interface CompensatedSagaInput {\n\tworkflowId: string;\n\trunId: string;\n\tsteps: CompensatedStep[];\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tparentInvocationId?: string;\n\t/** When supplied, every step's reversal must match what the manifest declares. */\n\tworkflow?: DeclaredWorkflow;\n}\n\nexport interface CompensationRecord {\n\tstepId: string;\n\tactionId: string;\n\t/** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */\n\terror?: string;\n}\n\nexport interface CompensatedSagaResult {\n\tcompleted: string[];\n\tcompensated: CompensationRecord[];\n\tfailedStep?: { stepId: string; error: string };\n}\n\n/** Reversals are governed invocations too, so they carry their own stable identity. */\nexport function compensationInvocationIdentity(input: { workflowId: string; stepId: string }) {\n\treturn temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });\n}\n\nexport class SagaCompensationError extends Error {\n\toverride readonly name = \"SagaCompensationError\";\n\tconstructor(readonly result: CompensatedSagaResult) {\n\t\tsuper(`Saga step \"${result.failedStep?.stepId}\" failed: ${result.failedStep?.error}`);\n\t}\n}\n\ntype GovernedActivity = ReturnType<typeof createGovernedActionActivities>[\"invokeGovernedAction\"];\n\n/**\n * The governed Host resolves for outcomes that are not success — a blocked policy, a\n * failed validation, a reconciliation, an approval park. Treating a resolved promise\n * as a completed step would silently skip the unwind, so the status is inspected.\n */\nconst SUCCESSFUL_STATUS = \"completed\";\n\nfunction stepFailure(result: unknown, label: string): string | undefined {\n\tif (!result || typeof result !== \"object\") return undefined;\n\tconst status = (result as { status?: unknown }).status;\n\tif (typeof status !== \"string\" || status === SUCCESSFUL_STATUS) return undefined;\n\tconst error = (result as { error?: unknown }).error;\n\treturn `${label} resolved with status \"${status}\"${typeof error === \"string\" && error ? `: ${error}` : \"\"}`;\n}\n\nfunction assertDistinctSteps(steps: CompensatedStep[]): void {\n\tconst seen = new Set<string>();\n\tfor (const step of steps) {\n\t\tif (!step.stepId.trim()) throw new Error(\"Every saga step requires a stepId.\");\n\t\t// A forward step called \"compensate:x\" would share an identity with the reversal of \"x\".\n\t\tif (step.stepId.startsWith(\"compensate:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"compensate:\" prefix.`);\n\t\t// A forward step called \"parent:x\" would share an identity with a parent lifecycle activity.\n\t\tif (step.stepId.startsWith(\"parent:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"parent:\" prefix.`);\n\t\tif (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step \"${step.stepId}\"; step identity must be unique within a workflow.`);\n\t\tseen.add(step.stepId);\n\t}\n}\n\n/** The manifest declaration this saga is expected to honour, when one is supplied. */\nexport interface DeclaredWorkflow {\n\tname: string;\n\tcompensations?: Array<{ step: string; actionId: string }>;\n}\n\nfunction assertMatchesDeclaration(steps: CompensatedStep[], workflow: DeclaredWorkflow): void {\n\tconst declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));\n\tfor (const step of steps) {\n\t\tconst expected = declared.get(step.stepId);\n\t\tif (expected === undefined) {\n\t\t\tif (step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" supplies a reversal the manifest does not declare.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" omits the reversal \"${expected}\" its manifest declares.`);\n\t\tif (step.compensateWith.actionId !== expected) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" reverses with \"${step.compensateWith.actionId}\" but its manifest declares \"${expected}\".`);\n\t\t}\n\t}\n\tfor (const [step, actionId] of declared) {\n\t\tif (!steps.some((candidate) => candidate.stepId === step)) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" declares a reversal \"${actionId}\" for step \"${step}\", which this saga never runs.`);\n\t\t}\n\t}\n}\n\n/**\n * Runs steps in order, accumulating reversals as each succeeds. On failure the\n * accumulated reversals run newest-first, so an unwind never leaves an orphan.\n * Every forward step and every reversal enters through the governed Host with a\n * deterministic idempotency key, so a retried or replayed unwind is not double-applied.\n */\nexport async function runCompensatedSaga(\n\tactivity: GovernedActivity,\n\tinput: CompensatedSagaInput,\n): Promise<CompensatedSagaResult> {\n\tconst scope = {\n\t\ttenantId: input.tenantId,\n\t\tspaceId: input.spaceId,\n\t\tactorId: input.actorId,\n\t\tactorType: input.actorType,\n\t\tworkflowId: input.workflowId,\n\t\trunId: input.runId,\n\t\t...(input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}),\n\t};\n\tassertDistinctSteps(input.steps);\n\tif (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);\n\n\tconst completed: string[] = [];\n\tconst reversals: CompensatedStep[] = [];\n\n\tconst unwind = async (failedStep: { stepId: string; error: string }): Promise<never> => {\n\t\tconst compensated: CompensationRecord[] = [];\n\t\t// Newest-first: the last effect applied is the first one withdrawn.\n\t\tfor (const applied of [...reversals].reverse()) {\n\t\t\tconst reversal = applied.compensateWith!;\n\t\t\tconst record: CompensationRecord = { stepId: applied.stepId, actionId: reversal.actionId };\n\t\t\ttry {\n\t\t\t\tconst result = await activity({\n\t\t\t\t\t...scope,\n\t\t\t\t\tactionId: reversal.actionId,\n\t\t\t\t\tparameters: reversal.parameters ?? applied.parameters,\n\t\t\t\t\tstepId: `compensate:${applied.stepId}`,\n\t\t\t\t});\n\t\t\t\tconst refused = stepFailure(result, `reversal \"${reversal.actionId}\"`);\n\t\t\t\tif (refused) record.error = refused;\n\t\t\t} catch (compensationError) {\n\t\t\t\trecord.error = compensationError instanceof Error ? compensationError.message : String(compensationError);\n\t\t\t}\n\t\t\tcompensated.push(record);\n\t\t}\n\t\tthrow new SagaCompensationError({ completed, compensated, failedStep });\n\t};\n\n\tfor (const step of input.steps) {\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tresult = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });\n\t\t} catch (error) {\n\t\t\treturn await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t\tconst refused = stepFailure(result, `step \"${step.stepId}\"`);\n\t\tif (refused) return await unwind({ stepId: step.stepId, error: refused });\n\t\tcompleted.push(step.stepId);\n\t\tif (step.compensateWith) reversals.push(step);\n\t}\n\n\treturn { completed, compensated: [] };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import type { ActorType } from \"@fabricorg/platform\";\nimport type { WorkflowClient } from \"@temporalio/client\";\n\n/** Serializable input passed from deterministic workflows to a Platform activity. */\nexport interface GovernedActionActivityInput {\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tworkflowId: string;\n\trunId: string;\n\tstepId: string;\n\tparentInvocationId?: string;\n}\n\nexport interface GovernedActionInvoker {\n\tinvoke(input: {\n\t\tactionId: string; parameters: Record<string, unknown>;\n\t\ttenantId: string; spaceId: string; actorId: string; actorType: ActorType;\n\t\tidempotencyKey: string; correlationId: string; causationId?: string;\n\t\texecutionEvidence: { temporalWorkflowId: string; temporalRunId: string; temporalStepId: string };\n\t}): Promise<unknown>;\n}\n\nexport function temporalInvocationIdentity(input: Pick<GovernedActionActivityInput, \"workflowId\" | \"stepId\">) {\n\treturn { idempotencyKey: `temporal:${input.workflowId}:${input.stepId}`, correlationId: `temporal:${input.workflowId}` };\n}\n\n/** Activity implementation. It always enters through the governed Host; handlers are never called directly. */\nexport function createGovernedActionActivities(invoker: GovernedActionInvoker) {\n\treturn {\n\t\tinvokeGovernedAction: async (input: GovernedActionActivityInput) => {\n\t\t\tconst identity = temporalInvocationIdentity(input);\n\t\t\treturn invoker.invoke({\n\t\t\t\tactionId: input.actionId, parameters: input.parameters,\n\t\t\t\ttenantId: input.tenantId, spaceId: input.spaceId,\n\t\t\t\tactorId: input.actorId, actorType: input.actorType,\n\t\t\t\t...identity,\n\t\t\t\t...(input.parentInvocationId ? { causationId: input.parentInvocationId } : {}),\n\t\t\t\texecutionEvidence: { temporalWorkflowId: input.workflowId, temporalRunId: input.runId, temporalStepId: input.stepId },\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface SagaSignalEnvelope<T = unknown> {\n\tsagaId: string;\n\tsignal: string;\n\tpayload: T;\n\tactorId: string;\n\treceivedAt: string;\n\t/**\n\t * Identifies one logical signal. A sender should reuse the same value on each\n\t * redelivery of that signal, for example a webhook delivery ID or an approval\n\t * decision ID, so a retried `client.signal()` after a timeout carries the\n\t * same `signalId` as the first attempt.\n\t *\n\t * A receiver built on {@link createSagaSignalInbox} drops an envelope whose\n\t * `signalId` it has already received. Envelopes without a `signalId` are not\n\t * deduplicated. {@link createSagaSignalDispatcher} throws on a blank value\n\t * before sending.\n\t */\n\tsignalId?: string;\n}\nexport interface TemporalSignalClient { signal(workflowId: string, signalName: string, payload: unknown): Promise<void> }\nexport function createSagaSignalDispatcher(client: TemporalSignalClient) {\n\t// Not async: a synchronous throw from the client keeps propagating synchronously, as before.\n\treturn (workflowId: string, envelope: SagaSignalEnvelope) => {\n\t\tassertSignalIdentity(envelope);\n\t\treturn client.signal(workflowId, envelope.signal, envelope);\n\t};\n}\n\nfunction assertSignalIdentity(envelope: SagaSignalEnvelope): void {\n\tif (envelope.signalId === undefined) return;\n\tif (typeof envelope.signalId !== \"string\" || !envelope.signalId.trim()) {\n\t\tthrow new Error(`Saga signal \"${envelope.signal}\" for saga \"${envelope.sagaId}\" has a blank signalId; omit it or supply a stable non-blank identity.`);\n\t}\n}\n\n/** A stable identity a receiver can deduplicate on, or undefined when the envelope carries none. */\nfunction signalIdentity(envelope: SagaSignalEnvelope): string | undefined {\n\treturn typeof envelope.signalId === \"string\" && envelope.signalId.trim() ? envelope.signalId : undefined;\n}\n\n/** Serializable inbox state, as returned by {@link SagaSignalInbox.snapshot}. */\nexport interface SagaSignalInboxSnapshot {\n\t/** Signal IDs already received, so a redelivery after the handover is still dropped. */\n\tseenSignalIds: string[];\n\t/** Envelopes received but not yet taken, FIFO within each signal name. */\n\tpending: SagaSignalEnvelope[];\n}\n\nexport interface SagaSignalInboxOptions {\n\t/**\n\t * Signal IDs already received by an earlier run of this saga. Prefer passing a\n\t * whole {@link SagaSignalInbox.snapshot}: carrying IDs without `pending` drops\n\t * any envelope that was accepted but not yet taken.\n\t */\n\tseenSignalIds?: readonly string[];\n\t/**\n\t * Envelopes an earlier run accepted but did not take. They are buffered again\n\t * in order without a dedup check, since they were already accepted once.\n\t */\n\tpending?: readonly SagaSignalEnvelope[];\n}\n\nexport interface SagaSignalInbox {\n\t/**\n\t * Buffers the envelope at the back of the queue for its signal name and returns\n\t * `true`. Returns `false` and drops the envelope when its `signalId` was already\n\t * received by this inbox under any signal name. An envelope without a `signalId`\n\t * (or with a blank one) is buffered and not remembered.\n\t */\n\treceive(envelope: SagaSignalEnvelope): boolean;\n\t/** Removes and returns the oldest buffered envelope for `signalName`, or `undefined` when none is buffered. */\n\ttake<T = unknown>(signalName: string): SagaSignalEnvelope<T> | undefined;\n\t/** Number of envelopes buffered for `signalName`, for use in `condition(() => inbox.pending(name) > 0, timeout)`. */\n\tpending(signalName: string): number;\n\t/** Signal IDs received so far, carried-forward ones first, in the order they were first received. */\n\tseenSignalIds(): string[];\n\t/**\n\t * Copies the seen IDs and every buffered envelope. Pass it into the next run across\n\t * continue-as-new and hand it back to `createSagaSignalInbox()`, so neither a\n\t * redelivery nor an accepted-but-untaken signal is lost.\n\t */\n\tsnapshot(): SagaSignalInboxSnapshot;\n}\n\n/**\n * Deterministic receiver for saga signals. It has no Temporal dependency and reads\n * no clock, randomness or process-global state, so it can run inside the workflow\n * sandbox. Wire it with\n * `setHandler(defineSignal(SAGA_SIGNALS.approval), (envelope) => { inbox.receive(envelope); })`.\n *\n * Buffering is FIFO per signal name. Deduplication is keyed on `signalId` across\n * every signal name received by this inbox. Dedup state lives in this instance, so\n * it lasts for one workflow run unless the caller carries\n * {@link SagaSignalInbox.snapshot} into the next run. The remembered set grows\n * with each distinct `signalId` and is not pruned, including across\n * continue-as-new, because forgetting an ID would let its redelivery through.\n */\nexport function createSagaSignalInbox(options: SagaSignalInboxOptions = {}): SagaSignalInbox {\n\tconst seen = new Set<string>(options.seenSignalIds ?? []);\n\tconst queues = new Map<string, SagaSignalEnvelope[]>();\n\tconst enqueue = (envelope: SagaSignalEnvelope) => {\n\t\tconst queue = queues.get(envelope.signal);\n\t\tif (queue) queue.push(envelope);\n\t\telse queues.set(envelope.signal, [envelope]);\n\t};\n\tfor (const envelope of options.pending ?? []) {\n\t\tconst id = signalIdentity(envelope);\n\t\tif (id !== undefined) seen.add(id);\n\t\tenqueue(envelope);\n\t}\n\treturn {\n\t\treceive(envelope) {\n\t\t\tconst id = signalIdentity(envelope);\n\t\t\tif (id !== undefined) {\n\t\t\t\tif (seen.has(id)) return false;\n\t\t\t\tseen.add(id);\n\t\t\t}\n\t\t\tenqueue(envelope);\n\t\t\treturn true;\n\t\t},\n\t\ttake<T = unknown>(signalName: string) {\n\t\t\treturn queues.get(signalName)?.shift() as SagaSignalEnvelope<T> | undefined;\n\t\t},\n\t\tpending(signalName) {\n\t\t\treturn queues.get(signalName)?.length ?? 0;\n\t\t},\n\t\tseenSignalIds() {\n\t\t\treturn [...seen];\n\t\t},\n\t\tsnapshot() {\n\t\t\treturn { seenSignalIds: [...seen], pending: [...queues.values()].flat() };\n\t\t},\n\t};\n}\n\nexport interface WorkflowStartClient<THandle = unknown> {\n\tstart(input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[]; idConflictPolicy: \"use_existing\" }): Promise<THandle>;\n}\nexport function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THandle>, input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[] }) {\n\treturn client.start({ ...input, idConflictPolicy: \"use_existing\" });\n}\n\nexport interface SagaWorkflowBinding {\n\tactionId: `${string}.${string}`;\n\tworkflowType: string;\n\ttaskQueue: string;\n}\n\n/** Instance-scoped registry; registration order never changes resolution. */\nexport class SagaWorkflowRegistry {\n\tprivate readonly bindings = new Map<string, SagaWorkflowBinding>();\n\n\tconstructor(bindings: readonly SagaWorkflowBinding[] = []) {\n\t\tfor (const binding of bindings) this.register(binding);\n\t}\n\n\tregister(binding: SagaWorkflowBinding): void {\n\t\tif (this.bindings.has(binding.actionId)) {\n\t\t\tthrow new Error(`Saga workflow binding already registered: ${binding.actionId}`);\n\t\t}\n\t\tif (!binding.workflowType.trim() || !binding.taskQueue.trim()) {\n\t\t\tthrow new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);\n\t\t}\n\t\tthis.bindings.set(binding.actionId, { ...binding });\n\t}\n\n\trequire(actionId: string): SagaWorkflowBinding {\n\t\tconst binding = this.bindings.get(actionId);\n\t\tif (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);\n\t\treturn { ...binding };\n\t}\n}\n\nexport interface TemporalSagaDispatchInput {\n\tactionInvocationId: string;\n\tactionId: `${string}.${string}`;\n\ttenantId: string;\n\tspaceId: string;\n\tworkflowId: string;\n}\n\n/** Starts the registered parent workflow with an invocation-only durable identity. */\nexport function createTemporalSagaDispatcher<THandle>(\n\tclient: WorkflowStartClient<THandle>,\n\tregistry: SagaWorkflowRegistry,\n) {\n\treturn {\n\t\tdispatch: async (input: TemporalSagaDispatchInput) => {\n\t\t\tconst binding = registry.require(input.actionId);\n\t\t\tconst handle = await startGovernedWorkflow(client, {\n\t\t\t\tworkflowType: binding.workflowType,\n\t\t\t\tworkflowId: input.workflowId,\n\t\t\t\ttaskQueue: binding.taskQueue,\n\t\t\t\targs: [{\n\t\t\t\t\tparentInvocationId: input.actionInvocationId,\n\t\t\t\t\tactionId: input.actionId,\n\t\t\t\t\ttenantId: input.tenantId,\n\t\t\t\t\tspaceId: input.spaceId,\n\t\t\t\t}],\n\t\t\t});\n\t\t\tconst runId = runIdFromHandle(handle);\n\t\t\treturn { workflowId: input.workflowId, ...(runId ? { runId } : {}) };\n\t\t},\n\t};\n}\n\nfunction runIdFromHandle(handle: unknown): string | undefined {\n\tif (!handle || typeof handle !== \"object\") return undefined;\n\tconst candidate = (handle as { firstExecutionRunId?: unknown }).firstExecutionRunId;\n\treturn typeof candidate === \"string\" && candidate ? candidate : undefined;\n}\n\n/** Durable record of a single step completing within a saga parent. */\nexport interface SagaParentProgressRecord {\n\t/** The step that just completed. */\n\tstepId: string;\n\t/** Zero-based position of this step in the declared sequence. */\n\tindex: number;\n\t/** Total steps declared for this saga. */\n\ttotal: number;\n\t/** ISO-8601 timestamp the progress was recorded. */\n\tcompletedAt: string;\n}\n\nexport interface SagaParentProgressInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\tprogress: SagaParentProgressRecord;\n}\n\nexport interface SagaParentApprovalInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\t/** The step at which the saga paused for human approval. */\n\tstepId: string;\n\t/** Stable identifier for the approval gate, so a later signal can resume it. */\n\tapprovalId: string;\n\treason: string;\n}\n\nexport interface SagaParentCancellationInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\treason: string;\n\t/** Actor requesting the cancellation, for audit attribution. */\n\tactorId: string;\n}\n\nexport interface SagaParentLifecycle {\n\t/** Record durable progress as each step completes. */\n\trecordProgress(input: SagaParentProgressInput): Promise<void>;\n\t/** Park the parent for human approval at a specific step. */\n\tparkApproval(input: SagaParentApprovalInput): Promise<void>;\n\t/** Cancel the parent saga with an attributable reason. */\n\tcancel(input: SagaParentCancellationInput): Promise<void>;\n\t/** Complete the parent saga with its final result. */\n\tcomplete(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; result: Record<string, unknown> }): Promise<void>;\n\t/** Fail the parent saga, surfacing any compensation records. */\n\tfail(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; error: string; compensation: CompensationRecord[] }): Promise<void>;\n}\n\n/** Parent lifecycle remains explicit durable activities, never workflow-local state. */\nexport function createSagaParentActivities(lifecycle: SagaParentLifecycle) {\n\treturn {\n\t\trecordSagaProgress: (input: SagaParentProgressInput) => lifecycle.recordProgress({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"progress\",\n\t\t\t\t`${input.progress.stepId}:${input.progress.index}`,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tparkSagaApproval: (input: SagaParentApprovalInput) => lifecycle.parkApproval({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"approval\",\n\t\t\t\tinput.approvalId,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcancelSagaParent: (input: SagaParentCancellationInput) => lifecycle.cancel({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"cancel\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcompleteSagaParent: (input: Parameters<SagaParentLifecycle[\"complete\"]>[0]) =>\n\t\t\tlifecycle.complete({\n\t\t\t\t...input,\n\t\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\t\"complete\",\n\t\t\t\t).idempotencyKey,\n\t\t\t}),\n\t\tfailSagaParent: (input: Parameters<SagaParentLifecycle[\"fail\"]>[0]) => lifecycle.fail({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"fail\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t};\n}\n\n// ── Stable parent and child causation identities ───────────────────────────\n\n/**\n * Stable identity for a saga parent's own lifecycle activity (progress,\n * cancellation, approval parking, completion, failure). The activity stepId\n * is namespaced under `parent:` so it can never collide with a forward step or\n * a `compensate:` reversal.\n */\nexport function sagaParentActivityIdentity(\n\tworkflowId: string,\n\tactivity: SagaParentActivityKind,\n\tdiscriminator?: string,\n) {\n\treturn temporalInvocationIdentity({\n\t\tworkflowId,\n\t\tstepId: `parent:${activity}${discriminator ? `:${discriminator}` : \"\"}`,\n\t});\n}\n\nexport type SagaParentActivityKind = \"progress\" | \"approval\" | \"cancel\" | \"complete\" | \"fail\";\n\n/**\n * Stable causation identity linking a child step to its parent invocation.\n * The parent invocation ID becomes the causation ID; the step keeps its own\n * deterministic idempotency key and shares the workflow correlation ID.\n */\nexport function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string) {\n\treturn { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };\n}\n\n// ── Saga signal names ───────────────────────────────────────────────────────\n\n/** Stable signal names a saga workflow may register handlers for. */\nexport const SAGA_SIGNALS = {\n\tproceed: \"saga.proceed\",\n\tcancel: \"saga.cancel\",\n\tapproval: \"saga.approval\",\n} as const;\n\n/** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */\nexport function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient {\n\treturn {\n\t\tstart: ({ workflowType, workflowId, taskQueue, args }) =>\n\t\t\tclient.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: \"USE_EXISTING\" }),\n\t};\n}\n\nexport interface WorkerVersionEvidence { buildId: string; deploymentName: string; rampPercent?: number }\nexport function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void {\n\tif (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error(\"Temporal worker buildId and deploymentName are required for rollover evidence.\");\n}\n\n// ── Declared compensation ───────────────────────────────────────────────────\n\nexport interface CompensatedStep {\n\t/** Workflow-local step identity. Must match the compensation declared in the module manifest. */\n\tstepId: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\t/**\n\t * Governed action that reverses this step's effect. Omit for a step with\n\t * nothing to undo; a step whose effect is externally visible should declare one.\n\t */\n\tcompensateWith?: { actionId: string; parameters?: Record<string, unknown> };\n}\n\nexport interface CompensatedSagaInput {\n\tworkflowId: string;\n\trunId: string;\n\tsteps: CompensatedStep[];\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tparentInvocationId?: string;\n\t/** When supplied, every step's reversal must match what the manifest declares. */\n\tworkflow?: DeclaredWorkflow;\n}\n\nexport interface CompensationRecord {\n\tstepId: string;\n\tactionId: string;\n\t/** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */\n\terror?: string;\n}\n\nexport interface CompensatedSagaResult {\n\tcompleted: string[];\n\tcompensated: CompensationRecord[];\n\tfailedStep?: { stepId: string; error: string };\n}\n\n/** Reversals are governed invocations too, so they carry their own stable identity. */\nexport function compensationInvocationIdentity(input: { workflowId: string; stepId: string }) {\n\treturn temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });\n}\n\nexport class SagaCompensationError extends Error {\n\toverride readonly name = \"SagaCompensationError\";\n\tconstructor(readonly result: CompensatedSagaResult) {\n\t\tsuper(`Saga step \"${result.failedStep?.stepId}\" failed: ${result.failedStep?.error}`);\n\t}\n}\n\ntype GovernedActivity = ReturnType<typeof createGovernedActionActivities>[\"invokeGovernedAction\"];\n\n/**\n * The governed Host resolves for outcomes that are not success — a blocked policy, a\n * failed validation, a reconciliation, an approval park. Treating a resolved promise\n * as a completed step would silently skip the unwind, so the status is inspected.\n */\nconst SUCCESSFUL_STATUS = \"completed\";\n\nfunction stepFailure(result: unknown, label: string): string | undefined {\n\tif (!result || typeof result !== \"object\") return undefined;\n\tconst status = (result as { status?: unknown }).status;\n\tif (typeof status !== \"string\" || status === SUCCESSFUL_STATUS) return undefined;\n\tconst error = (result as { error?: unknown }).error;\n\treturn `${label} resolved with status \"${status}\"${typeof error === \"string\" && error ? `: ${error}` : \"\"}`;\n}\n\nfunction assertDistinctSteps(steps: CompensatedStep[]): void {\n\tconst seen = new Set<string>();\n\tfor (const step of steps) {\n\t\tif (!step.stepId.trim()) throw new Error(\"Every saga step requires a stepId.\");\n\t\t// A forward step called \"compensate:x\" would share an identity with the reversal of \"x\".\n\t\tif (step.stepId.startsWith(\"compensate:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"compensate:\" prefix.`);\n\t\t// A forward step called \"parent:x\" would share an identity with a parent lifecycle activity.\n\t\tif (step.stepId.startsWith(\"parent:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"parent:\" prefix.`);\n\t\tif (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step \"${step.stepId}\"; step identity must be unique within a workflow.`);\n\t\tseen.add(step.stepId);\n\t}\n}\n\n/** The manifest declaration this saga is expected to honour, when one is supplied. */\nexport interface DeclaredWorkflow {\n\tname: string;\n\tcompensations?: Array<{ step: string; actionId: string }>;\n}\n\nfunction assertMatchesDeclaration(steps: CompensatedStep[], workflow: DeclaredWorkflow): void {\n\tconst declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));\n\tfor (const step of steps) {\n\t\tconst expected = declared.get(step.stepId);\n\t\tif (expected === undefined) {\n\t\t\tif (step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" supplies a reversal the manifest does not declare.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" omits the reversal \"${expected}\" its manifest declares.`);\n\t\tif (step.compensateWith.actionId !== expected) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" reverses with \"${step.compensateWith.actionId}\" but its manifest declares \"${expected}\".`);\n\t\t}\n\t}\n\tfor (const [step, actionId] of declared) {\n\t\tif (!steps.some((candidate) => candidate.stepId === step)) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" declares a reversal \"${actionId}\" for step \"${step}\", which this saga never runs.`);\n\t\t}\n\t}\n}\n\n/**\n * Runs steps in order, accumulating reversals as each succeeds. On failure the\n * accumulated reversals run newest-first, so an unwind never leaves an orphan.\n * Every forward step and every reversal enters through the governed Host with a\n * deterministic idempotency key, so a retried or replayed unwind is not double-applied.\n */\nexport async function runCompensatedSaga(\n\tactivity: GovernedActivity,\n\tinput: CompensatedSagaInput,\n): Promise<CompensatedSagaResult> {\n\tconst scope = {\n\t\ttenantId: input.tenantId,\n\t\tspaceId: input.spaceId,\n\t\tactorId: input.actorId,\n\t\tactorType: input.actorType,\n\t\tworkflowId: input.workflowId,\n\t\trunId: input.runId,\n\t\t...(input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}),\n\t};\n\tassertDistinctSteps(input.steps);\n\tif (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);\n\n\tconst completed: string[] = [];\n\tconst reversals: CompensatedStep[] = [];\n\n\tconst unwind = async (failedStep: { stepId: string; error: string }): Promise<never> => {\n\t\tconst compensated: CompensationRecord[] = [];\n\t\t// Newest-first: the last effect applied is the first one withdrawn.\n\t\tfor (const applied of [...reversals].reverse()) {\n\t\t\tconst reversal = applied.compensateWith!;\n\t\t\tconst record: CompensationRecord = { stepId: applied.stepId, actionId: reversal.actionId };\n\t\t\ttry {\n\t\t\t\tconst result = await activity({\n\t\t\t\t\t...scope,\n\t\t\t\t\tactionId: reversal.actionId,\n\t\t\t\t\tparameters: reversal.parameters ?? applied.parameters,\n\t\t\t\t\tstepId: `compensate:${applied.stepId}`,\n\t\t\t\t});\n\t\t\t\tconst refused = stepFailure(result, `reversal \"${reversal.actionId}\"`);\n\t\t\t\tif (refused) record.error = refused;\n\t\t\t} catch (compensationError) {\n\t\t\t\trecord.error = compensationError instanceof Error ? compensationError.message : String(compensationError);\n\t\t\t}\n\t\t\tcompensated.push(record);\n\t\t}\n\t\tthrow new SagaCompensationError({ completed, compensated, failedStep });\n\t};\n\n\tfor (const step of input.steps) {\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tresult = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });\n\t\t} catch (error) {\n\t\t\treturn await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t\tconst refused = stepFailure(result, `step \"${step.stepId}\"`);\n\t\tif (refused) return await unwind({ stepId: step.stepId, error: refused });\n\t\tcompleted.push(step.stepId);\n\t\tif (step.compensateWith) reversals.push(step);\n\t}\n\n\treturn { completed, compensated: [] };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -46,11 +46,78 @@ interface SagaSignalEnvelope<T = unknown> {
|
|
|
46
46
|
payload: T;
|
|
47
47
|
actorId: string;
|
|
48
48
|
receivedAt: string;
|
|
49
|
+
/**
|
|
50
|
+
* Identifies one logical signal. A sender should reuse the same value on each
|
|
51
|
+
* redelivery of that signal, for example a webhook delivery ID or an approval
|
|
52
|
+
* decision ID, so a retried `client.signal()` after a timeout carries the
|
|
53
|
+
* same `signalId` as the first attempt.
|
|
54
|
+
*
|
|
55
|
+
* A receiver built on {@link createSagaSignalInbox} drops an envelope whose
|
|
56
|
+
* `signalId` it has already received. Envelopes without a `signalId` are not
|
|
57
|
+
* deduplicated. {@link createSagaSignalDispatcher} throws on a blank value
|
|
58
|
+
* before sending.
|
|
59
|
+
*/
|
|
60
|
+
signalId?: string;
|
|
49
61
|
}
|
|
50
62
|
interface TemporalSignalClient {
|
|
51
63
|
signal(workflowId: string, signalName: string, payload: unknown): Promise<void>;
|
|
52
64
|
}
|
|
53
65
|
declare function createSagaSignalDispatcher(client: TemporalSignalClient): (workflowId: string, envelope: SagaSignalEnvelope) => Promise<void>;
|
|
66
|
+
/** Serializable inbox state, as returned by {@link SagaSignalInbox.snapshot}. */
|
|
67
|
+
interface SagaSignalInboxSnapshot {
|
|
68
|
+
/** Signal IDs already received, so a redelivery after the handover is still dropped. */
|
|
69
|
+
seenSignalIds: string[];
|
|
70
|
+
/** Envelopes received but not yet taken, FIFO within each signal name. */
|
|
71
|
+
pending: SagaSignalEnvelope[];
|
|
72
|
+
}
|
|
73
|
+
interface SagaSignalInboxOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Signal IDs already received by an earlier run of this saga. Prefer passing a
|
|
76
|
+
* whole {@link SagaSignalInbox.snapshot}: carrying IDs without `pending` drops
|
|
77
|
+
* any envelope that was accepted but not yet taken.
|
|
78
|
+
*/
|
|
79
|
+
seenSignalIds?: readonly string[];
|
|
80
|
+
/**
|
|
81
|
+
* Envelopes an earlier run accepted but did not take. They are buffered again
|
|
82
|
+
* in order without a dedup check, since they were already accepted once.
|
|
83
|
+
*/
|
|
84
|
+
pending?: readonly SagaSignalEnvelope[];
|
|
85
|
+
}
|
|
86
|
+
interface SagaSignalInbox {
|
|
87
|
+
/**
|
|
88
|
+
* Buffers the envelope at the back of the queue for its signal name and returns
|
|
89
|
+
* `true`. Returns `false` and drops the envelope when its `signalId` was already
|
|
90
|
+
* received by this inbox under any signal name. An envelope without a `signalId`
|
|
91
|
+
* (or with a blank one) is buffered and not remembered.
|
|
92
|
+
*/
|
|
93
|
+
receive(envelope: SagaSignalEnvelope): boolean;
|
|
94
|
+
/** Removes and returns the oldest buffered envelope for `signalName`, or `undefined` when none is buffered. */
|
|
95
|
+
take<T = unknown>(signalName: string): SagaSignalEnvelope<T> | undefined;
|
|
96
|
+
/** Number of envelopes buffered for `signalName`, for use in `condition(() => inbox.pending(name) > 0, timeout)`. */
|
|
97
|
+
pending(signalName: string): number;
|
|
98
|
+
/** Signal IDs received so far, carried-forward ones first, in the order they were first received. */
|
|
99
|
+
seenSignalIds(): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Copies the seen IDs and every buffered envelope. Pass it into the next run across
|
|
102
|
+
* continue-as-new and hand it back to `createSagaSignalInbox()`, so neither a
|
|
103
|
+
* redelivery nor an accepted-but-untaken signal is lost.
|
|
104
|
+
*/
|
|
105
|
+
snapshot(): SagaSignalInboxSnapshot;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Deterministic receiver for saga signals. It has no Temporal dependency and reads
|
|
109
|
+
* no clock, randomness or process-global state, so it can run inside the workflow
|
|
110
|
+
* sandbox. Wire it with
|
|
111
|
+
* `setHandler(defineSignal(SAGA_SIGNALS.approval), (envelope) => { inbox.receive(envelope); })`.
|
|
112
|
+
*
|
|
113
|
+
* Buffering is FIFO per signal name. Deduplication is keyed on `signalId` across
|
|
114
|
+
* every signal name received by this inbox. Dedup state lives in this instance, so
|
|
115
|
+
* it lasts for one workflow run unless the caller carries
|
|
116
|
+
* {@link SagaSignalInbox.snapshot} into the next run. The remembered set grows
|
|
117
|
+
* with each distinct `signalId` and is not pruned, including across
|
|
118
|
+
* continue-as-new, because forgetting an ID would let its redelivery through.
|
|
119
|
+
*/
|
|
120
|
+
declare function createSagaSignalInbox(options?: SagaSignalInboxOptions): SagaSignalInbox;
|
|
54
121
|
interface WorkflowStartClient<THandle = unknown> {
|
|
55
122
|
start(input: {
|
|
56
123
|
workflowType: string;
|
|
@@ -271,4 +338,4 @@ interface DeclaredWorkflow {
|
|
|
271
338
|
*/
|
|
272
339
|
declare function runCompensatedSaga(activity: GovernedActivity, input: CompensatedSagaInput): Promise<CompensatedSagaResult>;
|
|
273
340
|
|
|
274
|
-
export { type CompensatedSagaInput, type CompensatedSagaResult, type CompensatedStep, type CompensationRecord, type DeclaredWorkflow, type GovernedActionActivityInput, type GovernedActionInvoker, SAGA_SIGNALS, SagaCompensationError, type SagaParentActivityKind, type SagaParentApprovalInput, type SagaParentCancellationInput, type SagaParentLifecycle, type SagaParentProgressInput, type SagaParentProgressRecord, type SagaSignalEnvelope, type SagaWorkflowBinding, SagaWorkflowRegistry, type TemporalSagaDispatchInput, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, compensationInvocationIdentity, createGovernedActionActivities, createSagaParentActivities, createSagaSignalDispatcher, createTemporalSagaDispatcher, runCompensatedSaga, sagaChildCausation, sagaParentActivityIdentity, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
|
|
341
|
+
export { type CompensatedSagaInput, type CompensatedSagaResult, type CompensatedStep, type CompensationRecord, type DeclaredWorkflow, type GovernedActionActivityInput, type GovernedActionInvoker, SAGA_SIGNALS, SagaCompensationError, type SagaParentActivityKind, type SagaParentApprovalInput, type SagaParentCancellationInput, type SagaParentLifecycle, type SagaParentProgressInput, type SagaParentProgressRecord, type SagaSignalEnvelope, type SagaSignalInbox, type SagaSignalInboxOptions, type SagaSignalInboxSnapshot, type SagaWorkflowBinding, SagaWorkflowRegistry, type TemporalSagaDispatchInput, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, compensationInvocationIdentity, createGovernedActionActivities, createSagaParentActivities, createSagaSignalDispatcher, createSagaSignalInbox, createTemporalSagaDispatcher, runCompensatedSaga, sagaChildCausation, sagaParentActivityIdentity, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -46,11 +46,78 @@ interface SagaSignalEnvelope<T = unknown> {
|
|
|
46
46
|
payload: T;
|
|
47
47
|
actorId: string;
|
|
48
48
|
receivedAt: string;
|
|
49
|
+
/**
|
|
50
|
+
* Identifies one logical signal. A sender should reuse the same value on each
|
|
51
|
+
* redelivery of that signal, for example a webhook delivery ID or an approval
|
|
52
|
+
* decision ID, so a retried `client.signal()` after a timeout carries the
|
|
53
|
+
* same `signalId` as the first attempt.
|
|
54
|
+
*
|
|
55
|
+
* A receiver built on {@link createSagaSignalInbox} drops an envelope whose
|
|
56
|
+
* `signalId` it has already received. Envelopes without a `signalId` are not
|
|
57
|
+
* deduplicated. {@link createSagaSignalDispatcher} throws on a blank value
|
|
58
|
+
* before sending.
|
|
59
|
+
*/
|
|
60
|
+
signalId?: string;
|
|
49
61
|
}
|
|
50
62
|
interface TemporalSignalClient {
|
|
51
63
|
signal(workflowId: string, signalName: string, payload: unknown): Promise<void>;
|
|
52
64
|
}
|
|
53
65
|
declare function createSagaSignalDispatcher(client: TemporalSignalClient): (workflowId: string, envelope: SagaSignalEnvelope) => Promise<void>;
|
|
66
|
+
/** Serializable inbox state, as returned by {@link SagaSignalInbox.snapshot}. */
|
|
67
|
+
interface SagaSignalInboxSnapshot {
|
|
68
|
+
/** Signal IDs already received, so a redelivery after the handover is still dropped. */
|
|
69
|
+
seenSignalIds: string[];
|
|
70
|
+
/** Envelopes received but not yet taken, FIFO within each signal name. */
|
|
71
|
+
pending: SagaSignalEnvelope[];
|
|
72
|
+
}
|
|
73
|
+
interface SagaSignalInboxOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Signal IDs already received by an earlier run of this saga. Prefer passing a
|
|
76
|
+
* whole {@link SagaSignalInbox.snapshot}: carrying IDs without `pending` drops
|
|
77
|
+
* any envelope that was accepted but not yet taken.
|
|
78
|
+
*/
|
|
79
|
+
seenSignalIds?: readonly string[];
|
|
80
|
+
/**
|
|
81
|
+
* Envelopes an earlier run accepted but did not take. They are buffered again
|
|
82
|
+
* in order without a dedup check, since they were already accepted once.
|
|
83
|
+
*/
|
|
84
|
+
pending?: readonly SagaSignalEnvelope[];
|
|
85
|
+
}
|
|
86
|
+
interface SagaSignalInbox {
|
|
87
|
+
/**
|
|
88
|
+
* Buffers the envelope at the back of the queue for its signal name and returns
|
|
89
|
+
* `true`. Returns `false` and drops the envelope when its `signalId` was already
|
|
90
|
+
* received by this inbox under any signal name. An envelope without a `signalId`
|
|
91
|
+
* (or with a blank one) is buffered and not remembered.
|
|
92
|
+
*/
|
|
93
|
+
receive(envelope: SagaSignalEnvelope): boolean;
|
|
94
|
+
/** Removes and returns the oldest buffered envelope for `signalName`, or `undefined` when none is buffered. */
|
|
95
|
+
take<T = unknown>(signalName: string): SagaSignalEnvelope<T> | undefined;
|
|
96
|
+
/** Number of envelopes buffered for `signalName`, for use in `condition(() => inbox.pending(name) > 0, timeout)`. */
|
|
97
|
+
pending(signalName: string): number;
|
|
98
|
+
/** Signal IDs received so far, carried-forward ones first, in the order they were first received. */
|
|
99
|
+
seenSignalIds(): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Copies the seen IDs and every buffered envelope. Pass it into the next run across
|
|
102
|
+
* continue-as-new and hand it back to `createSagaSignalInbox()`, so neither a
|
|
103
|
+
* redelivery nor an accepted-but-untaken signal is lost.
|
|
104
|
+
*/
|
|
105
|
+
snapshot(): SagaSignalInboxSnapshot;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Deterministic receiver for saga signals. It has no Temporal dependency and reads
|
|
109
|
+
* no clock, randomness or process-global state, so it can run inside the workflow
|
|
110
|
+
* sandbox. Wire it with
|
|
111
|
+
* `setHandler(defineSignal(SAGA_SIGNALS.approval), (envelope) => { inbox.receive(envelope); })`.
|
|
112
|
+
*
|
|
113
|
+
* Buffering is FIFO per signal name. Deduplication is keyed on `signalId` across
|
|
114
|
+
* every signal name received by this inbox. Dedup state lives in this instance, so
|
|
115
|
+
* it lasts for one workflow run unless the caller carries
|
|
116
|
+
* {@link SagaSignalInbox.snapshot} into the next run. The remembered set grows
|
|
117
|
+
* with each distinct `signalId` and is not pruned, including across
|
|
118
|
+
* continue-as-new, because forgetting an ID would let its redelivery through.
|
|
119
|
+
*/
|
|
120
|
+
declare function createSagaSignalInbox(options?: SagaSignalInboxOptions): SagaSignalInbox;
|
|
54
121
|
interface WorkflowStartClient<THandle = unknown> {
|
|
55
122
|
start(input: {
|
|
56
123
|
workflowType: string;
|
|
@@ -271,4 +338,4 @@ interface DeclaredWorkflow {
|
|
|
271
338
|
*/
|
|
272
339
|
declare function runCompensatedSaga(activity: GovernedActivity, input: CompensatedSagaInput): Promise<CompensatedSagaResult>;
|
|
273
340
|
|
|
274
|
-
export { type CompensatedSagaInput, type CompensatedSagaResult, type CompensatedStep, type CompensationRecord, type DeclaredWorkflow, type GovernedActionActivityInput, type GovernedActionInvoker, SAGA_SIGNALS, SagaCompensationError, type SagaParentActivityKind, type SagaParentApprovalInput, type SagaParentCancellationInput, type SagaParentLifecycle, type SagaParentProgressInput, type SagaParentProgressRecord, type SagaSignalEnvelope, type SagaWorkflowBinding, SagaWorkflowRegistry, type TemporalSagaDispatchInput, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, compensationInvocationIdentity, createGovernedActionActivities, createSagaParentActivities, createSagaSignalDispatcher, createTemporalSagaDispatcher, runCompensatedSaga, sagaChildCausation, sagaParentActivityIdentity, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
|
|
341
|
+
export { type CompensatedSagaInput, type CompensatedSagaResult, type CompensatedStep, type CompensationRecord, type DeclaredWorkflow, type GovernedActionActivityInput, type GovernedActionInvoker, SAGA_SIGNALS, SagaCompensationError, type SagaParentActivityKind, type SagaParentApprovalInput, type SagaParentCancellationInput, type SagaParentLifecycle, type SagaParentProgressInput, type SagaParentProgressRecord, type SagaSignalEnvelope, type SagaSignalInbox, type SagaSignalInboxOptions, type SagaSignalInboxSnapshot, type SagaWorkflowBinding, SagaWorkflowRegistry, type TemporalSagaDispatchInput, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, compensationInvocationIdentity, createGovernedActionActivities, createSagaParentActivities, createSagaSignalDispatcher, createSagaSignalInbox, createTemporalSagaDispatcher, runCompensatedSaga, sagaChildCausation, sagaParentActivityIdentity, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,56 @@ function createGovernedActionActivities(invoker) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
function createSagaSignalDispatcher(client) {
|
|
24
|
-
return (workflowId, envelope) =>
|
|
24
|
+
return (workflowId, envelope) => {
|
|
25
|
+
assertSignalIdentity(envelope);
|
|
26
|
+
return client.signal(workflowId, envelope.signal, envelope);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function assertSignalIdentity(envelope) {
|
|
30
|
+
if (envelope.signalId === void 0) return;
|
|
31
|
+
if (typeof envelope.signalId !== "string" || !envelope.signalId.trim()) {
|
|
32
|
+
throw new Error(`Saga signal "${envelope.signal}" for saga "${envelope.sagaId}" has a blank signalId; omit it or supply a stable non-blank identity.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function signalIdentity(envelope) {
|
|
36
|
+
return typeof envelope.signalId === "string" && envelope.signalId.trim() ? envelope.signalId : void 0;
|
|
37
|
+
}
|
|
38
|
+
function createSagaSignalInbox(options = {}) {
|
|
39
|
+
const seen = new Set(options.seenSignalIds ?? []);
|
|
40
|
+
const queues = /* @__PURE__ */ new Map();
|
|
41
|
+
const enqueue = (envelope) => {
|
|
42
|
+
const queue = queues.get(envelope.signal);
|
|
43
|
+
if (queue) queue.push(envelope);
|
|
44
|
+
else queues.set(envelope.signal, [envelope]);
|
|
45
|
+
};
|
|
46
|
+
for (const envelope of options.pending ?? []) {
|
|
47
|
+
const id = signalIdentity(envelope);
|
|
48
|
+
if (id !== void 0) seen.add(id);
|
|
49
|
+
enqueue(envelope);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
receive(envelope) {
|
|
53
|
+
const id = signalIdentity(envelope);
|
|
54
|
+
if (id !== void 0) {
|
|
55
|
+
if (seen.has(id)) return false;
|
|
56
|
+
seen.add(id);
|
|
57
|
+
}
|
|
58
|
+
enqueue(envelope);
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
take(signalName) {
|
|
62
|
+
return queues.get(signalName)?.shift();
|
|
63
|
+
},
|
|
64
|
+
pending(signalName) {
|
|
65
|
+
return queues.get(signalName)?.length ?? 0;
|
|
66
|
+
},
|
|
67
|
+
seenSignalIds() {
|
|
68
|
+
return [...seen];
|
|
69
|
+
},
|
|
70
|
+
snapshot() {
|
|
71
|
+
return { seenSignalIds: [...seen], pending: [...queues.values()].flat() };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
25
74
|
}
|
|
26
75
|
function startGovernedWorkflow(client, input) {
|
|
27
76
|
return client.start({ ...input, idConflictPolicy: "use_existing" });
|
|
@@ -240,6 +289,7 @@ export {
|
|
|
240
289
|
createGovernedActionActivities,
|
|
241
290
|
createSagaParentActivities,
|
|
242
291
|
createSagaSignalDispatcher,
|
|
292
|
+
createSagaSignalInbox,
|
|
243
293
|
createTemporalSagaDispatcher,
|
|
244
294
|
runCompensatedSaga,
|
|
245
295
|
sagaChildCausation,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import type { ActorType } from \"@fabricorg/platform\";\nimport type { WorkflowClient } from \"@temporalio/client\";\n\n/** Serializable input passed from deterministic workflows to a Platform activity. */\nexport interface GovernedActionActivityInput {\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tworkflowId: string;\n\trunId: string;\n\tstepId: string;\n\tparentInvocationId?: string;\n}\n\nexport interface GovernedActionInvoker {\n\tinvoke(input: {\n\t\tactionId: string; parameters: Record<string, unknown>;\n\t\ttenantId: string; spaceId: string; actorId: string; actorType: ActorType;\n\t\tidempotencyKey: string; correlationId: string; causationId?: string;\n\t\texecutionEvidence: { temporalWorkflowId: string; temporalRunId: string; temporalStepId: string };\n\t}): Promise<unknown>;\n}\n\nexport function temporalInvocationIdentity(input: Pick<GovernedActionActivityInput, \"workflowId\" | \"stepId\">) {\n\treturn { idempotencyKey: `temporal:${input.workflowId}:${input.stepId}`, correlationId: `temporal:${input.workflowId}` };\n}\n\n/** Activity implementation. It always enters through the governed Host; handlers are never called directly. */\nexport function createGovernedActionActivities(invoker: GovernedActionInvoker) {\n\treturn {\n\t\tinvokeGovernedAction: async (input: GovernedActionActivityInput) => {\n\t\t\tconst identity = temporalInvocationIdentity(input);\n\t\t\treturn invoker.invoke({\n\t\t\t\tactionId: input.actionId, parameters: input.parameters,\n\t\t\t\ttenantId: input.tenantId, spaceId: input.spaceId,\n\t\t\t\tactorId: input.actorId, actorType: input.actorType,\n\t\t\t\t...identity,\n\t\t\t\t...(input.parentInvocationId ? { causationId: input.parentInvocationId } : {}),\n\t\t\t\texecutionEvidence: { temporalWorkflowId: input.workflowId, temporalRunId: input.runId, temporalStepId: input.stepId },\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface SagaSignalEnvelope<T = unknown> { sagaId: string; signal: string; payload: T; actorId: string; receivedAt: string }\nexport interface TemporalSignalClient { signal(workflowId: string, signalName: string, payload: unknown): Promise<void> }\nexport function createSagaSignalDispatcher(client: TemporalSignalClient) {\n\treturn (workflowId: string, envelope: SagaSignalEnvelope) => client.signal(workflowId, envelope.signal, envelope);\n}\n\nexport interface WorkflowStartClient<THandle = unknown> {\n\tstart(input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[]; idConflictPolicy: \"use_existing\" }): Promise<THandle>;\n}\nexport function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THandle>, input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[] }) {\n\treturn client.start({ ...input, idConflictPolicy: \"use_existing\" });\n}\n\nexport interface SagaWorkflowBinding {\n\tactionId: `${string}.${string}`;\n\tworkflowType: string;\n\ttaskQueue: string;\n}\n\n/** Instance-scoped registry; registration order never changes resolution. */\nexport class SagaWorkflowRegistry {\n\tprivate readonly bindings = new Map<string, SagaWorkflowBinding>();\n\n\tconstructor(bindings: readonly SagaWorkflowBinding[] = []) {\n\t\tfor (const binding of bindings) this.register(binding);\n\t}\n\n\tregister(binding: SagaWorkflowBinding): void {\n\t\tif (this.bindings.has(binding.actionId)) {\n\t\t\tthrow new Error(`Saga workflow binding already registered: ${binding.actionId}`);\n\t\t}\n\t\tif (!binding.workflowType.trim() || !binding.taskQueue.trim()) {\n\t\t\tthrow new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);\n\t\t}\n\t\tthis.bindings.set(binding.actionId, { ...binding });\n\t}\n\n\trequire(actionId: string): SagaWorkflowBinding {\n\t\tconst binding = this.bindings.get(actionId);\n\t\tif (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);\n\t\treturn { ...binding };\n\t}\n}\n\nexport interface TemporalSagaDispatchInput {\n\tactionInvocationId: string;\n\tactionId: `${string}.${string}`;\n\ttenantId: string;\n\tspaceId: string;\n\tworkflowId: string;\n}\n\n/** Starts the registered parent workflow with an invocation-only durable identity. */\nexport function createTemporalSagaDispatcher<THandle>(\n\tclient: WorkflowStartClient<THandle>,\n\tregistry: SagaWorkflowRegistry,\n) {\n\treturn {\n\t\tdispatch: async (input: TemporalSagaDispatchInput) => {\n\t\t\tconst binding = registry.require(input.actionId);\n\t\t\tconst handle = await startGovernedWorkflow(client, {\n\t\t\t\tworkflowType: binding.workflowType,\n\t\t\t\tworkflowId: input.workflowId,\n\t\t\t\ttaskQueue: binding.taskQueue,\n\t\t\t\targs: [{\n\t\t\t\t\tparentInvocationId: input.actionInvocationId,\n\t\t\t\t\tactionId: input.actionId,\n\t\t\t\t\ttenantId: input.tenantId,\n\t\t\t\t\tspaceId: input.spaceId,\n\t\t\t\t}],\n\t\t\t});\n\t\t\tconst runId = runIdFromHandle(handle);\n\t\t\treturn { workflowId: input.workflowId, ...(runId ? { runId } : {}) };\n\t\t},\n\t};\n}\n\nfunction runIdFromHandle(handle: unknown): string | undefined {\n\tif (!handle || typeof handle !== \"object\") return undefined;\n\tconst candidate = (handle as { firstExecutionRunId?: unknown }).firstExecutionRunId;\n\treturn typeof candidate === \"string\" && candidate ? candidate : undefined;\n}\n\n/** Durable record of a single step completing within a saga parent. */\nexport interface SagaParentProgressRecord {\n\t/** The step that just completed. */\n\tstepId: string;\n\t/** Zero-based position of this step in the declared sequence. */\n\tindex: number;\n\t/** Total steps declared for this saga. */\n\ttotal: number;\n\t/** ISO-8601 timestamp the progress was recorded. */\n\tcompletedAt: string;\n}\n\nexport interface SagaParentProgressInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\tprogress: SagaParentProgressRecord;\n}\n\nexport interface SagaParentApprovalInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\t/** The step at which the saga paused for human approval. */\n\tstepId: string;\n\t/** Stable identifier for the approval gate, so a later signal can resume it. */\n\tapprovalId: string;\n\treason: string;\n}\n\nexport interface SagaParentCancellationInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\treason: string;\n\t/** Actor requesting the cancellation, for audit attribution. */\n\tactorId: string;\n}\n\nexport interface SagaParentLifecycle {\n\t/** Record durable progress as each step completes. */\n\trecordProgress(input: SagaParentProgressInput): Promise<void>;\n\t/** Park the parent for human approval at a specific step. */\n\tparkApproval(input: SagaParentApprovalInput): Promise<void>;\n\t/** Cancel the parent saga with an attributable reason. */\n\tcancel(input: SagaParentCancellationInput): Promise<void>;\n\t/** Complete the parent saga with its final result. */\n\tcomplete(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; result: Record<string, unknown> }): Promise<void>;\n\t/** Fail the parent saga, surfacing any compensation records. */\n\tfail(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; error: string; compensation: CompensationRecord[] }): Promise<void>;\n}\n\n/** Parent lifecycle remains explicit durable activities, never workflow-local state. */\nexport function createSagaParentActivities(lifecycle: SagaParentLifecycle) {\n\treturn {\n\t\trecordSagaProgress: (input: SagaParentProgressInput) => lifecycle.recordProgress({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"progress\",\n\t\t\t\t`${input.progress.stepId}:${input.progress.index}`,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tparkSagaApproval: (input: SagaParentApprovalInput) => lifecycle.parkApproval({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"approval\",\n\t\t\t\tinput.approvalId,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcancelSagaParent: (input: SagaParentCancellationInput) => lifecycle.cancel({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"cancel\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcompleteSagaParent: (input: Parameters<SagaParentLifecycle[\"complete\"]>[0]) =>\n\t\t\tlifecycle.complete({\n\t\t\t\t...input,\n\t\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\t\"complete\",\n\t\t\t\t).idempotencyKey,\n\t\t\t}),\n\t\tfailSagaParent: (input: Parameters<SagaParentLifecycle[\"fail\"]>[0]) => lifecycle.fail({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"fail\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t};\n}\n\n// ── Stable parent and child causation identities ───────────────────────────\n\n/**\n * Stable identity for a saga parent's own lifecycle activity (progress,\n * cancellation, approval parking, completion, failure). The activity stepId\n * is namespaced under `parent:` so it can never collide with a forward step or\n * a `compensate:` reversal.\n */\nexport function sagaParentActivityIdentity(\n\tworkflowId: string,\n\tactivity: SagaParentActivityKind,\n\tdiscriminator?: string,\n) {\n\treturn temporalInvocationIdentity({\n\t\tworkflowId,\n\t\tstepId: `parent:${activity}${discriminator ? `:${discriminator}` : \"\"}`,\n\t});\n}\n\nexport type SagaParentActivityKind = \"progress\" | \"approval\" | \"cancel\" | \"complete\" | \"fail\";\n\n/**\n * Stable causation identity linking a child step to its parent invocation.\n * The parent invocation ID becomes the causation ID; the step keeps its own\n * deterministic idempotency key and shares the workflow correlation ID.\n */\nexport function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string) {\n\treturn { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };\n}\n\n// ── Saga signal names ───────────────────────────────────────────────────────\n\n/** Stable signal names a saga workflow may register handlers for. */\nexport const SAGA_SIGNALS = {\n\tproceed: \"saga.proceed\",\n\tcancel: \"saga.cancel\",\n\tapproval: \"saga.approval\",\n} as const;\n\n/** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */\nexport function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient {\n\treturn {\n\t\tstart: ({ workflowType, workflowId, taskQueue, args }) =>\n\t\t\tclient.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: \"USE_EXISTING\" }),\n\t};\n}\n\nexport interface WorkerVersionEvidence { buildId: string; deploymentName: string; rampPercent?: number }\nexport function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void {\n\tif (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error(\"Temporal worker buildId and deploymentName are required for rollover evidence.\");\n}\n\n// ── Declared compensation ───────────────────────────────────────────────────\n\nexport interface CompensatedStep {\n\t/** Workflow-local step identity. Must match the compensation declared in the module manifest. */\n\tstepId: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\t/**\n\t * Governed action that reverses this step's effect. Omit for a step with\n\t * nothing to undo; a step whose effect is externally visible should declare one.\n\t */\n\tcompensateWith?: { actionId: string; parameters?: Record<string, unknown> };\n}\n\nexport interface CompensatedSagaInput {\n\tworkflowId: string;\n\trunId: string;\n\tsteps: CompensatedStep[];\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tparentInvocationId?: string;\n\t/** When supplied, every step's reversal must match what the manifest declares. */\n\tworkflow?: DeclaredWorkflow;\n}\n\nexport interface CompensationRecord {\n\tstepId: string;\n\tactionId: string;\n\t/** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */\n\terror?: string;\n}\n\nexport interface CompensatedSagaResult {\n\tcompleted: string[];\n\tcompensated: CompensationRecord[];\n\tfailedStep?: { stepId: string; error: string };\n}\n\n/** Reversals are governed invocations too, so they carry their own stable identity. */\nexport function compensationInvocationIdentity(input: { workflowId: string; stepId: string }) {\n\treturn temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });\n}\n\nexport class SagaCompensationError extends Error {\n\toverride readonly name = \"SagaCompensationError\";\n\tconstructor(readonly result: CompensatedSagaResult) {\n\t\tsuper(`Saga step \"${result.failedStep?.stepId}\" failed: ${result.failedStep?.error}`);\n\t}\n}\n\ntype GovernedActivity = ReturnType<typeof createGovernedActionActivities>[\"invokeGovernedAction\"];\n\n/**\n * The governed Host resolves for outcomes that are not success — a blocked policy, a\n * failed validation, a reconciliation, an approval park. Treating a resolved promise\n * as a completed step would silently skip the unwind, so the status is inspected.\n */\nconst SUCCESSFUL_STATUS = \"completed\";\n\nfunction stepFailure(result: unknown, label: string): string | undefined {\n\tif (!result || typeof result !== \"object\") return undefined;\n\tconst status = (result as { status?: unknown }).status;\n\tif (typeof status !== \"string\" || status === SUCCESSFUL_STATUS) return undefined;\n\tconst error = (result as { error?: unknown }).error;\n\treturn `${label} resolved with status \"${status}\"${typeof error === \"string\" && error ? `: ${error}` : \"\"}`;\n}\n\nfunction assertDistinctSteps(steps: CompensatedStep[]): void {\n\tconst seen = new Set<string>();\n\tfor (const step of steps) {\n\t\tif (!step.stepId.trim()) throw new Error(\"Every saga step requires a stepId.\");\n\t\t// A forward step called \"compensate:x\" would share an identity with the reversal of \"x\".\n\t\tif (step.stepId.startsWith(\"compensate:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"compensate:\" prefix.`);\n\t\t// A forward step called \"parent:x\" would share an identity with a parent lifecycle activity.\n\t\tif (step.stepId.startsWith(\"parent:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"parent:\" prefix.`);\n\t\tif (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step \"${step.stepId}\"; step identity must be unique within a workflow.`);\n\t\tseen.add(step.stepId);\n\t}\n}\n\n/** The manifest declaration this saga is expected to honour, when one is supplied. */\nexport interface DeclaredWorkflow {\n\tname: string;\n\tcompensations?: Array<{ step: string; actionId: string }>;\n}\n\nfunction assertMatchesDeclaration(steps: CompensatedStep[], workflow: DeclaredWorkflow): void {\n\tconst declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));\n\tfor (const step of steps) {\n\t\tconst expected = declared.get(step.stepId);\n\t\tif (expected === undefined) {\n\t\t\tif (step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" supplies a reversal the manifest does not declare.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" omits the reversal \"${expected}\" its manifest declares.`);\n\t\tif (step.compensateWith.actionId !== expected) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" reverses with \"${step.compensateWith.actionId}\" but its manifest declares \"${expected}\".`);\n\t\t}\n\t}\n\tfor (const [step, actionId] of declared) {\n\t\tif (!steps.some((candidate) => candidate.stepId === step)) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" declares a reversal \"${actionId}\" for step \"${step}\", which this saga never runs.`);\n\t\t}\n\t}\n}\n\n/**\n * Runs steps in order, accumulating reversals as each succeeds. On failure the\n * accumulated reversals run newest-first, so an unwind never leaves an orphan.\n * Every forward step and every reversal enters through the governed Host with a\n * deterministic idempotency key, so a retried or replayed unwind is not double-applied.\n */\nexport async function runCompensatedSaga(\n\tactivity: GovernedActivity,\n\tinput: CompensatedSagaInput,\n): Promise<CompensatedSagaResult> {\n\tconst scope = {\n\t\ttenantId: input.tenantId,\n\t\tspaceId: input.spaceId,\n\t\tactorId: input.actorId,\n\t\tactorType: input.actorType,\n\t\tworkflowId: input.workflowId,\n\t\trunId: input.runId,\n\t\t...(input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}),\n\t};\n\tassertDistinctSteps(input.steps);\n\tif (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);\n\n\tconst completed: string[] = [];\n\tconst reversals: CompensatedStep[] = [];\n\n\tconst unwind = async (failedStep: { stepId: string; error: string }): Promise<never> => {\n\t\tconst compensated: CompensationRecord[] = [];\n\t\t// Newest-first: the last effect applied is the first one withdrawn.\n\t\tfor (const applied of [...reversals].reverse()) {\n\t\t\tconst reversal = applied.compensateWith!;\n\t\t\tconst record: CompensationRecord = { stepId: applied.stepId, actionId: reversal.actionId };\n\t\t\ttry {\n\t\t\t\tconst result = await activity({\n\t\t\t\t\t...scope,\n\t\t\t\t\tactionId: reversal.actionId,\n\t\t\t\t\tparameters: reversal.parameters ?? applied.parameters,\n\t\t\t\t\tstepId: `compensate:${applied.stepId}`,\n\t\t\t\t});\n\t\t\t\tconst refused = stepFailure(result, `reversal \"${reversal.actionId}\"`);\n\t\t\t\tif (refused) record.error = refused;\n\t\t\t} catch (compensationError) {\n\t\t\t\trecord.error = compensationError instanceof Error ? compensationError.message : String(compensationError);\n\t\t\t}\n\t\t\tcompensated.push(record);\n\t\t}\n\t\tthrow new SagaCompensationError({ completed, compensated, failedStep });\n\t};\n\n\tfor (const step of input.steps) {\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tresult = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });\n\t\t} catch (error) {\n\t\t\treturn await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t\tconst refused = stepFailure(result, `step \"${step.stepId}\"`);\n\t\tif (refused) return await unwind({ stepId: step.stepId, error: refused });\n\t\tcompleted.push(step.stepId);\n\t\tif (step.compensateWith) reversals.push(step);\n\t}\n\n\treturn { completed, compensated: [] };\n}\n"],"mappings":";AA0BO,SAAS,2BAA2B,OAAmE;AAC7G,SAAO,EAAE,gBAAgB,YAAY,MAAM,UAAU,IAAI,MAAM,MAAM,IAAI,eAAe,YAAY,MAAM,UAAU,GAAG;AACxH;AAGO,SAAS,+BAA+B,SAAgC;AAC9E,SAAO;AAAA,IACN,sBAAsB,OAAO,UAAuC;AACnE,YAAM,WAAW,2BAA2B,KAAK;AACjD,aAAO,QAAQ,OAAO;AAAA,QACrB,UAAU,MAAM;AAAA,QAAU,YAAY,MAAM;AAAA,QAC5C,UAAU,MAAM;AAAA,QAAU,SAAS,MAAM;AAAA,QACzC,SAAS,MAAM;AAAA,QAAS,WAAW,MAAM;AAAA,QACzC,GAAG;AAAA,QACH,GAAI,MAAM,qBAAqB,EAAE,aAAa,MAAM,mBAAmB,IAAI,CAAC;AAAA,QAC5E,mBAAmB,EAAE,oBAAoB,MAAM,YAAY,eAAe,MAAM,OAAO,gBAAgB,MAAM,OAAO;AAAA,MACrH,CAAC;AAAA,IACF;AAAA,EACD;AACD;AAIO,SAAS,2BAA2B,QAA8B;AACxE,SAAO,CAAC,YAAoB,aAAiC,OAAO,OAAO,YAAY,SAAS,QAAQ,QAAQ;AACjH;AAKO,SAAS,sBAA+B,QAAsC,OAAyF;AAC7K,SAAO,OAAO,MAAM,EAAE,GAAG,OAAO,kBAAkB,eAAe,CAAC;AACnE;AASO,IAAM,uBAAN,MAA2B;AAAA,EAChB,WAAW,oBAAI,IAAiC;AAAA,EAEjE,YAAY,WAA2C,CAAC,GAAG;AAC1D,eAAW,WAAW,SAAU,MAAK,SAAS,OAAO;AAAA,EACtD;AAAA,EAEA,SAAS,SAAoC;AAC5C,QAAI,KAAK,SAAS,IAAI,QAAQ,QAAQ,GAAG;AACxC,YAAM,IAAI,MAAM,6CAA6C,QAAQ,QAAQ,EAAE;AAAA,IAChF;AACA,QAAI,CAAC,QAAQ,aAAa,KAAK,KAAK,CAAC,QAAQ,UAAU,KAAK,GAAG;AAC9D,YAAM,IAAI,MAAM,yBAAyB,QAAQ,QAAQ,sCAAsC;AAAA,IAChG;AACA,SAAK,SAAS,IAAI,QAAQ,UAAU,EAAE,GAAG,QAAQ,CAAC;AAAA,EACnD;AAAA,EAEA,QAAQ,UAAuC;AAC9C,UAAM,UAAU,KAAK,SAAS,IAAI,QAAQ;AAC1C,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,2CAA2C,QAAQ,EAAE;AACnF,WAAO,EAAE,GAAG,QAAQ;AAAA,EACrB;AACD;AAWO,SAAS,6BACf,QACA,UACC;AACD,SAAO;AAAA,IACN,UAAU,OAAO,UAAqC;AACrD,YAAM,UAAU,SAAS,QAAQ,MAAM,QAAQ;AAC/C,YAAM,SAAS,MAAM,sBAAsB,QAAQ;AAAA,QAClD,cAAc,QAAQ;AAAA,QACtB,YAAY,MAAM;AAAA,QAClB,WAAW,QAAQ;AAAA,QACnB,MAAM,CAAC;AAAA,UACN,oBAAoB,MAAM;AAAA,UAC1B,UAAU,MAAM;AAAA,UAChB,UAAU,MAAM;AAAA,UAChB,SAAS,MAAM;AAAA,QAChB,CAAC;AAAA,MACF,CAAC;AACD,YAAM,QAAQ,gBAAgB,MAAM;AACpC,aAAO,EAAE,YAAY,MAAM,YAAY,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAG;AAAA,IACpE;AAAA,EACD;AACD;AAEA,SAAS,gBAAgB,QAAqC;AAC7D,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,YAAa,OAA6C;AAChE,SAAO,OAAO,cAAc,YAAY,YAAY,YAAY;AACjE;AA6DO,SAAS,2BAA2B,WAAgC;AAC1E,SAAO;AAAA,IACN,oBAAoB,CAAC,UAAmC,UAAU,eAAe;AAAA,MAChF,GAAG;AAAA,MACH,aAAa,MAAM,eAAe;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,QACA,GAAG,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK;AAAA,MACjD,EAAE;AAAA,IACH,CAAC;AAAA,IACD,kBAAkB,CAAC,UAAmC,UAAU,aAAa;AAAA,MAC5E,GAAG;AAAA,MACH,aAAa,MAAM,eAAe;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,QACA,MAAM;AAAA,MACP,EAAE;AAAA,IACH,CAAC;AAAA,IACD,kBAAkB,CAAC,UAAuC,UAAU,OAAO;AAAA,MAC1E,GAAG;AAAA,MACH,aAAa,MAAM,eAAe;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,MACD,EAAE;AAAA,IACH,CAAC;AAAA,IACD,oBAAoB,CAAC,UACpB,UAAU,SAAS;AAAA,MAClB,GAAG;AAAA,MACH,aAAa,MAAM,eAAe;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,MACD,EAAE;AAAA,IACH,CAAC;AAAA,IACF,gBAAgB,CAAC,UAAsD,UAAU,KAAK;AAAA,MACrF,GAAG;AAAA,MACH,aAAa,MAAM,eAAe;AAAA,QACjC,MAAM;AAAA,QACN;AAAA,MACD,EAAE;AAAA,IACH,CAAC;AAAA,EACF;AACD;AAUO,SAAS,2BACf,YACA,UACA,eACC;AACD,SAAO,2BAA2B;AAAA,IACjC;AAAA,IACA,QAAQ,UAAU,QAAQ,GAAG,gBAAgB,IAAI,aAAa,KAAK,EAAE;AAAA,EACtE,CAAC;AACF;AASO,SAAS,mBAAmB,YAAoB,oBAA4B,QAAgB;AAClG,SAAO,EAAE,GAAG,2BAA2B,EAAE,YAAY,OAAO,CAAC,GAAG,aAAa,mBAAmB;AACjG;AAKO,IAAM,eAAe;AAAA,EAC3B,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AACX;AAGO,SAAS,4BAA4B,QAA6C;AACxF,SAAO;AAAA,IACN,OAAO,CAAC,EAAE,cAAc,YAAY,WAAW,KAAK,MACnD,OAAO,MAAM,cAAc,EAAE,YAAY,WAAW,MAAM,0BAA0B,eAAe,CAAC;AAAA,EACtG;AACD;AAGO,SAAS,4BAA4B,OAAoC;AAC/E,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,MAAM,eAAe,KAAK,EAAG,OAAM,IAAI,MAAM,gFAAgF;AAC5J;AA2CO,SAAS,+BAA+B,OAA+C;AAC7F,SAAO,2BAA2B,EAAE,YAAY,MAAM,YAAY,QAAQ,cAAc,MAAM,MAAM,GAAG,CAAC;AACzG;AAEO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAEhD,YAAqB,QAA+B;AACnD,UAAM,cAAc,OAAO,YAAY,MAAM,aAAa,OAAO,YAAY,KAAK,EAAE;AADhE;AAAA,EAErB;AAAA,EAFqB;AAAA,EADH,OAAO;AAI1B;AASA,IAAM,oBAAoB;AAE1B,SAAS,YAAY,QAAiB,OAAmC;AACxE,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,SAAU,OAAgC;AAChD,MAAI,OAAO,WAAW,YAAY,WAAW,kBAAmB,QAAO;AACvE,QAAM,QAAS,OAA+B;AAC9C,SAAO,GAAG,KAAK,0BAA0B,MAAM,IAAI,OAAO,UAAU,YAAY,QAAQ,KAAK,KAAK,KAAK,EAAE;AAC1G;AAEA,SAAS,oBAAoB,OAAgC;AAC5D,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,QAAQ,OAAO;AACzB,QAAI,CAAC,KAAK,OAAO,KAAK,EAAG,OAAM,IAAI,MAAM,oCAAoC;AAE7E,QAAI,KAAK,OAAO,WAAW,aAAa,EAAG,OAAM,IAAI,MAAM,cAAc,KAAK,MAAM,mDAAmD;AAEvI,QAAI,KAAK,OAAO,WAAW,SAAS,EAAG,OAAM,IAAI,MAAM,cAAc,KAAK,MAAM,+CAA+C;AAC/H,QAAI,KAAK,IAAI,KAAK,MAAM,EAAG,OAAM,IAAI,MAAM,iCAAiC,KAAK,MAAM,oDAAoD;AAC3I,SAAK,IAAI,KAAK,MAAM;AAAA,EACrB;AACD;AAQA,SAAS,yBAAyB,OAA0B,UAAkC;AAC7F,QAAM,WAAW,IAAI,KAAK,SAAS,iBAAiB,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,MAAM,MAAM,QAAQ,CAAC,CAAC;AACpG,aAAW,QAAQ,OAAO;AACzB,UAAM,WAAW,SAAS,IAAI,KAAK,MAAM;AACzC,QAAI,aAAa,QAAW;AAC3B,UAAI,KAAK,eAAgB,OAAM,IAAI,MAAM,aAAa,SAAS,IAAI,WAAW,KAAK,MAAM,sDAAsD;AAC/I;AAAA,IACD;AACA,QAAI,CAAC,KAAK,eAAgB,OAAM,IAAI,MAAM,aAAa,SAAS,IAAI,WAAW,KAAK,MAAM,yBAAyB,QAAQ,0BAA0B;AACrJ,QAAI,KAAK,eAAe,aAAa,UAAU;AAC9C,YAAM,IAAI,MAAM,aAAa,SAAS,IAAI,WAAW,KAAK,MAAM,oBAAoB,KAAK,eAAe,QAAQ,gCAAgC,QAAQ,IAAI;AAAA,IAC7J;AAAA,EACD;AACA,aAAW,CAAC,MAAM,QAAQ,KAAK,UAAU;AACxC,QAAI,CAAC,MAAM,KAAK,CAAC,cAAc,UAAU,WAAW,IAAI,GAAG;AAC1D,YAAM,IAAI,MAAM,aAAa,SAAS,IAAI,0BAA0B,QAAQ,eAAe,IAAI,gCAAgC;AAAA,IAChI;AAAA,EACD;AACD;AAQA,eAAsB,mBACrB,UACA,OACiC;AACjC,QAAM,QAAQ;AAAA,IACb,UAAU,MAAM;AAAA,IAChB,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,WAAW,MAAM;AAAA,IACjB,YAAY,MAAM;AAAA,IAClB,OAAO,MAAM;AAAA,IACb,GAAI,MAAM,qBAAqB,EAAE,oBAAoB,MAAM,mBAAmB,IAAI,CAAC;AAAA,EACpF;AACA,sBAAoB,MAAM,KAAK;AAC/B,MAAI,MAAM,SAAU,0BAAyB,MAAM,OAAO,MAAM,QAAQ;AAExE,QAAM,YAAsB,CAAC;AAC7B,QAAM,YAA+B,CAAC;AAEtC,QAAM,SAAS,OAAO,eAAkE;AACvF,UAAM,cAAoC,CAAC;AAE3C,eAAW,WAAW,CAAC,GAAG,SAAS,EAAE,QAAQ,GAAG;AAC/C,YAAM,WAAW,QAAQ;AACzB,YAAM,SAA6B,EAAE,QAAQ,QAAQ,QAAQ,UAAU,SAAS,SAAS;AACzF,UAAI;AACH,cAAM,SAAS,MAAM,SAAS;AAAA,UAC7B,GAAG;AAAA,UACH,UAAU,SAAS;AAAA,UACnB,YAAY,SAAS,cAAc,QAAQ;AAAA,UAC3C,QAAQ,cAAc,QAAQ,MAAM;AAAA,QACrC,CAAC;AACD,cAAM,UAAU,YAAY,QAAQ,aAAa,SAAS,QAAQ,GAAG;AACrE,YAAI,QAAS,QAAO,QAAQ;AAAA,MAC7B,SAAS,mBAAmB;AAC3B,eAAO,QAAQ,6BAA6B,QAAQ,kBAAkB,UAAU,OAAO,iBAAiB;AAAA,MACzG;AACA,kBAAY,KAAK,MAAM;AAAA,IACxB;AACA,UAAM,IAAI,sBAAsB,EAAE,WAAW,aAAa,WAAW,CAAC;AAAA,EACvE;AAEA,aAAW,QAAQ,MAAM,OAAO;AAC/B,QAAI;AACJ,QAAI;AACH,eAAS,MAAM,SAAS,EAAE,GAAG,OAAO,UAAU,KAAK,UAAU,YAAY,KAAK,YAAY,QAAQ,KAAK,OAAO,CAAC;AAAA,IAChH,SAAS,OAAO;AACf,aAAO,MAAM,OAAO,EAAE,QAAQ,KAAK,QAAQ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAC3G;AACA,UAAM,UAAU,YAAY,QAAQ,SAAS,KAAK,MAAM,GAAG;AAC3D,QAAI,QAAS,QAAO,MAAM,OAAO,EAAE,QAAQ,KAAK,QAAQ,OAAO,QAAQ,CAAC;AACxE,cAAU,KAAK,KAAK,MAAM;AAC1B,QAAI,KAAK,eAAgB,WAAU,KAAK,IAAI;AAAA,EAC7C;AAEA,SAAO,EAAE,WAAW,aAAa,CAAC,EAAE;AACrC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import type { ActorType } from \"@fabricorg/platform\";\nimport type { WorkflowClient } from \"@temporalio/client\";\n\n/** Serializable input passed from deterministic workflows to a Platform activity. */\nexport interface GovernedActionActivityInput {\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tworkflowId: string;\n\trunId: string;\n\tstepId: string;\n\tparentInvocationId?: string;\n}\n\nexport interface GovernedActionInvoker {\n\tinvoke(input: {\n\t\tactionId: string; parameters: Record<string, unknown>;\n\t\ttenantId: string; spaceId: string; actorId: string; actorType: ActorType;\n\t\tidempotencyKey: string; correlationId: string; causationId?: string;\n\t\texecutionEvidence: { temporalWorkflowId: string; temporalRunId: string; temporalStepId: string };\n\t}): Promise<unknown>;\n}\n\nexport function temporalInvocationIdentity(input: Pick<GovernedActionActivityInput, \"workflowId\" | \"stepId\">) {\n\treturn { idempotencyKey: `temporal:${input.workflowId}:${input.stepId}`, correlationId: `temporal:${input.workflowId}` };\n}\n\n/** Activity implementation. It always enters through the governed Host; handlers are never called directly. */\nexport function createGovernedActionActivities(invoker: GovernedActionInvoker) {\n\treturn {\n\t\tinvokeGovernedAction: async (input: GovernedActionActivityInput) => {\n\t\t\tconst identity = temporalInvocationIdentity(input);\n\t\t\treturn invoker.invoke({\n\t\t\t\tactionId: input.actionId, parameters: input.parameters,\n\t\t\t\ttenantId: input.tenantId, spaceId: input.spaceId,\n\t\t\t\tactorId: input.actorId, actorType: input.actorType,\n\t\t\t\t...identity,\n\t\t\t\t...(input.parentInvocationId ? { causationId: input.parentInvocationId } : {}),\n\t\t\t\texecutionEvidence: { temporalWorkflowId: input.workflowId, temporalRunId: input.runId, temporalStepId: input.stepId },\n\t\t\t});\n\t\t},\n\t};\n}\n\nexport interface SagaSignalEnvelope<T = unknown> {\n\tsagaId: string;\n\tsignal: string;\n\tpayload: T;\n\tactorId: string;\n\treceivedAt: string;\n\t/**\n\t * Identifies one logical signal. A sender should reuse the same value on each\n\t * redelivery of that signal, for example a webhook delivery ID or an approval\n\t * decision ID, so a retried `client.signal()` after a timeout carries the\n\t * same `signalId` as the first attempt.\n\t *\n\t * A receiver built on {@link createSagaSignalInbox} drops an envelope whose\n\t * `signalId` it has already received. Envelopes without a `signalId` are not\n\t * deduplicated. {@link createSagaSignalDispatcher} throws on a blank value\n\t * before sending.\n\t */\n\tsignalId?: string;\n}\nexport interface TemporalSignalClient { signal(workflowId: string, signalName: string, payload: unknown): Promise<void> }\nexport function createSagaSignalDispatcher(client: TemporalSignalClient) {\n\t// Not async: a synchronous throw from the client keeps propagating synchronously, as before.\n\treturn (workflowId: string, envelope: SagaSignalEnvelope) => {\n\t\tassertSignalIdentity(envelope);\n\t\treturn client.signal(workflowId, envelope.signal, envelope);\n\t};\n}\n\nfunction assertSignalIdentity(envelope: SagaSignalEnvelope): void {\n\tif (envelope.signalId === undefined) return;\n\tif (typeof envelope.signalId !== \"string\" || !envelope.signalId.trim()) {\n\t\tthrow new Error(`Saga signal \"${envelope.signal}\" for saga \"${envelope.sagaId}\" has a blank signalId; omit it or supply a stable non-blank identity.`);\n\t}\n}\n\n/** A stable identity a receiver can deduplicate on, or undefined when the envelope carries none. */\nfunction signalIdentity(envelope: SagaSignalEnvelope): string | undefined {\n\treturn typeof envelope.signalId === \"string\" && envelope.signalId.trim() ? envelope.signalId : undefined;\n}\n\n/** Serializable inbox state, as returned by {@link SagaSignalInbox.snapshot}. */\nexport interface SagaSignalInboxSnapshot {\n\t/** Signal IDs already received, so a redelivery after the handover is still dropped. */\n\tseenSignalIds: string[];\n\t/** Envelopes received but not yet taken, FIFO within each signal name. */\n\tpending: SagaSignalEnvelope[];\n}\n\nexport interface SagaSignalInboxOptions {\n\t/**\n\t * Signal IDs already received by an earlier run of this saga. Prefer passing a\n\t * whole {@link SagaSignalInbox.snapshot}: carrying IDs without `pending` drops\n\t * any envelope that was accepted but not yet taken.\n\t */\n\tseenSignalIds?: readonly string[];\n\t/**\n\t * Envelopes an earlier run accepted but did not take. They are buffered again\n\t * in order without a dedup check, since they were already accepted once.\n\t */\n\tpending?: readonly SagaSignalEnvelope[];\n}\n\nexport interface SagaSignalInbox {\n\t/**\n\t * Buffers the envelope at the back of the queue for its signal name and returns\n\t * `true`. Returns `false` and drops the envelope when its `signalId` was already\n\t * received by this inbox under any signal name. An envelope without a `signalId`\n\t * (or with a blank one) is buffered and not remembered.\n\t */\n\treceive(envelope: SagaSignalEnvelope): boolean;\n\t/** Removes and returns the oldest buffered envelope for `signalName`, or `undefined` when none is buffered. */\n\ttake<T = unknown>(signalName: string): SagaSignalEnvelope<T> | undefined;\n\t/** Number of envelopes buffered for `signalName`, for use in `condition(() => inbox.pending(name) > 0, timeout)`. */\n\tpending(signalName: string): number;\n\t/** Signal IDs received so far, carried-forward ones first, in the order they were first received. */\n\tseenSignalIds(): string[];\n\t/**\n\t * Copies the seen IDs and every buffered envelope. Pass it into the next run across\n\t * continue-as-new and hand it back to `createSagaSignalInbox()`, so neither a\n\t * redelivery nor an accepted-but-untaken signal is lost.\n\t */\n\tsnapshot(): SagaSignalInboxSnapshot;\n}\n\n/**\n * Deterministic receiver for saga signals. It has no Temporal dependency and reads\n * no clock, randomness or process-global state, so it can run inside the workflow\n * sandbox. Wire it with\n * `setHandler(defineSignal(SAGA_SIGNALS.approval), (envelope) => { inbox.receive(envelope); })`.\n *\n * Buffering is FIFO per signal name. Deduplication is keyed on `signalId` across\n * every signal name received by this inbox. Dedup state lives in this instance, so\n * it lasts for one workflow run unless the caller carries\n * {@link SagaSignalInbox.snapshot} into the next run. The remembered set grows\n * with each distinct `signalId` and is not pruned, including across\n * continue-as-new, because forgetting an ID would let its redelivery through.\n */\nexport function createSagaSignalInbox(options: SagaSignalInboxOptions = {}): SagaSignalInbox {\n\tconst seen = new Set<string>(options.seenSignalIds ?? []);\n\tconst queues = new Map<string, SagaSignalEnvelope[]>();\n\tconst enqueue = (envelope: SagaSignalEnvelope) => {\n\t\tconst queue = queues.get(envelope.signal);\n\t\tif (queue) queue.push(envelope);\n\t\telse queues.set(envelope.signal, [envelope]);\n\t};\n\tfor (const envelope of options.pending ?? []) {\n\t\tconst id = signalIdentity(envelope);\n\t\tif (id !== undefined) seen.add(id);\n\t\tenqueue(envelope);\n\t}\n\treturn {\n\t\treceive(envelope) {\n\t\t\tconst id = signalIdentity(envelope);\n\t\t\tif (id !== undefined) {\n\t\t\t\tif (seen.has(id)) return false;\n\t\t\t\tseen.add(id);\n\t\t\t}\n\t\t\tenqueue(envelope);\n\t\t\treturn true;\n\t\t},\n\t\ttake<T = unknown>(signalName: string) {\n\t\t\treturn queues.get(signalName)?.shift() as SagaSignalEnvelope<T> | undefined;\n\t\t},\n\t\tpending(signalName) {\n\t\t\treturn queues.get(signalName)?.length ?? 0;\n\t\t},\n\t\tseenSignalIds() {\n\t\t\treturn [...seen];\n\t\t},\n\t\tsnapshot() {\n\t\t\treturn { seenSignalIds: [...seen], pending: [...queues.values()].flat() };\n\t\t},\n\t};\n}\n\nexport interface WorkflowStartClient<THandle = unknown> {\n\tstart(input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[]; idConflictPolicy: \"use_existing\" }): Promise<THandle>;\n}\nexport function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THandle>, input: { workflowType: string; workflowId: string; taskQueue: string; args: unknown[] }) {\n\treturn client.start({ ...input, idConflictPolicy: \"use_existing\" });\n}\n\nexport interface SagaWorkflowBinding {\n\tactionId: `${string}.${string}`;\n\tworkflowType: string;\n\ttaskQueue: string;\n}\n\n/** Instance-scoped registry; registration order never changes resolution. */\nexport class SagaWorkflowRegistry {\n\tprivate readonly bindings = new Map<string, SagaWorkflowBinding>();\n\n\tconstructor(bindings: readonly SagaWorkflowBinding[] = []) {\n\t\tfor (const binding of bindings) this.register(binding);\n\t}\n\n\tregister(binding: SagaWorkflowBinding): void {\n\t\tif (this.bindings.has(binding.actionId)) {\n\t\t\tthrow new Error(`Saga workflow binding already registered: ${binding.actionId}`);\n\t\t}\n\t\tif (!binding.workflowType.trim() || !binding.taskQueue.trim()) {\n\t\t\tthrow new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);\n\t\t}\n\t\tthis.bindings.set(binding.actionId, { ...binding });\n\t}\n\n\trequire(actionId: string): SagaWorkflowBinding {\n\t\tconst binding = this.bindings.get(actionId);\n\t\tif (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);\n\t\treturn { ...binding };\n\t}\n}\n\nexport interface TemporalSagaDispatchInput {\n\tactionInvocationId: string;\n\tactionId: `${string}.${string}`;\n\ttenantId: string;\n\tspaceId: string;\n\tworkflowId: string;\n}\n\n/** Starts the registered parent workflow with an invocation-only durable identity. */\nexport function createTemporalSagaDispatcher<THandle>(\n\tclient: WorkflowStartClient<THandle>,\n\tregistry: SagaWorkflowRegistry,\n) {\n\treturn {\n\t\tdispatch: async (input: TemporalSagaDispatchInput) => {\n\t\t\tconst binding = registry.require(input.actionId);\n\t\t\tconst handle = await startGovernedWorkflow(client, {\n\t\t\t\tworkflowType: binding.workflowType,\n\t\t\t\tworkflowId: input.workflowId,\n\t\t\t\ttaskQueue: binding.taskQueue,\n\t\t\t\targs: [{\n\t\t\t\t\tparentInvocationId: input.actionInvocationId,\n\t\t\t\t\tactionId: input.actionId,\n\t\t\t\t\ttenantId: input.tenantId,\n\t\t\t\t\tspaceId: input.spaceId,\n\t\t\t\t}],\n\t\t\t});\n\t\t\tconst runId = runIdFromHandle(handle);\n\t\t\treturn { workflowId: input.workflowId, ...(runId ? { runId } : {}) };\n\t\t},\n\t};\n}\n\nfunction runIdFromHandle(handle: unknown): string | undefined {\n\tif (!handle || typeof handle !== \"object\") return undefined;\n\tconst candidate = (handle as { firstExecutionRunId?: unknown }).firstExecutionRunId;\n\treturn typeof candidate === \"string\" && candidate ? candidate : undefined;\n}\n\n/** Durable record of a single step completing within a saga parent. */\nexport interface SagaParentProgressRecord {\n\t/** The step that just completed. */\n\tstepId: string;\n\t/** Zero-based position of this step in the declared sequence. */\n\tindex: number;\n\t/** Total steps declared for this saga. */\n\ttotal: number;\n\t/** ISO-8601 timestamp the progress was recorded. */\n\tcompletedAt: string;\n}\n\nexport interface SagaParentProgressInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\tprogress: SagaParentProgressRecord;\n}\n\nexport interface SagaParentApprovalInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\t/** The step at which the saga paused for human approval. */\n\tstepId: string;\n\t/** Stable identifier for the approval gate, so a later signal can resume it. */\n\tapprovalId: string;\n\treason: string;\n}\n\nexport interface SagaParentCancellationInput {\n\tparentInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\t/** Stable identity shared by every retry of this lifecycle transition. */\n\tlifecycleId?: string;\n\treason: string;\n\t/** Actor requesting the cancellation, for audit attribution. */\n\tactorId: string;\n}\n\nexport interface SagaParentLifecycle {\n\t/** Record durable progress as each step completes. */\n\trecordProgress(input: SagaParentProgressInput): Promise<void>;\n\t/** Park the parent for human approval at a specific step. */\n\tparkApproval(input: SagaParentApprovalInput): Promise<void>;\n\t/** Cancel the parent saga with an attributable reason. */\n\tcancel(input: SagaParentCancellationInput): Promise<void>;\n\t/** Complete the parent saga with its final result. */\n\tcomplete(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; result: Record<string, unknown> }): Promise<void>;\n\t/** Fail the parent saga, surfacing any compensation records. */\n\tfail(input: { parentInvocationId: string; tenantId: string; spaceId: string; lifecycleId?: string; error: string; compensation: CompensationRecord[] }): Promise<void>;\n}\n\n/** Parent lifecycle remains explicit durable activities, never workflow-local state. */\nexport function createSagaParentActivities(lifecycle: SagaParentLifecycle) {\n\treturn {\n\t\trecordSagaProgress: (input: SagaParentProgressInput) => lifecycle.recordProgress({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"progress\",\n\t\t\t\t`${input.progress.stepId}:${input.progress.index}`,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tparkSagaApproval: (input: SagaParentApprovalInput) => lifecycle.parkApproval({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"approval\",\n\t\t\t\tinput.approvalId,\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcancelSagaParent: (input: SagaParentCancellationInput) => lifecycle.cancel({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"cancel\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t\tcompleteSagaParent: (input: Parameters<SagaParentLifecycle[\"complete\"]>[0]) =>\n\t\t\tlifecycle.complete({\n\t\t\t\t...input,\n\t\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\t\"complete\",\n\t\t\t\t).idempotencyKey,\n\t\t\t}),\n\t\tfailSagaParent: (input: Parameters<SagaParentLifecycle[\"fail\"]>[0]) => lifecycle.fail({\n\t\t\t...input,\n\t\t\tlifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(\n\t\t\t\tinput.parentInvocationId,\n\t\t\t\t\"fail\",\n\t\t\t).idempotencyKey,\n\t\t}),\n\t};\n}\n\n// ── Stable parent and child causation identities ───────────────────────────\n\n/**\n * Stable identity for a saga parent's own lifecycle activity (progress,\n * cancellation, approval parking, completion, failure). The activity stepId\n * is namespaced under `parent:` so it can never collide with a forward step or\n * a `compensate:` reversal.\n */\nexport function sagaParentActivityIdentity(\n\tworkflowId: string,\n\tactivity: SagaParentActivityKind,\n\tdiscriminator?: string,\n) {\n\treturn temporalInvocationIdentity({\n\t\tworkflowId,\n\t\tstepId: `parent:${activity}${discriminator ? `:${discriminator}` : \"\"}`,\n\t});\n}\n\nexport type SagaParentActivityKind = \"progress\" | \"approval\" | \"cancel\" | \"complete\" | \"fail\";\n\n/**\n * Stable causation identity linking a child step to its parent invocation.\n * The parent invocation ID becomes the causation ID; the step keeps its own\n * deterministic idempotency key and shares the workflow correlation ID.\n */\nexport function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string) {\n\treturn { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };\n}\n\n// ── Saga signal names ───────────────────────────────────────────────────────\n\n/** Stable signal names a saga workflow may register handlers for. */\nexport const SAGA_SIGNALS = {\n\tproceed: \"saga.proceed\",\n\tcancel: \"saga.cancel\",\n\tapproval: \"saga.approval\",\n} as const;\n\n/** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */\nexport function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient {\n\treturn {\n\t\tstart: ({ workflowType, workflowId, taskQueue, args }) =>\n\t\t\tclient.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: \"USE_EXISTING\" }),\n\t};\n}\n\nexport interface WorkerVersionEvidence { buildId: string; deploymentName: string; rampPercent?: number }\nexport function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void {\n\tif (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error(\"Temporal worker buildId and deploymentName are required for rollover evidence.\");\n}\n\n// ── Declared compensation ───────────────────────────────────────────────────\n\nexport interface CompensatedStep {\n\t/** Workflow-local step identity. Must match the compensation declared in the module manifest. */\n\tstepId: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n\t/**\n\t * Governed action that reverses this step's effect. Omit for a step with\n\t * nothing to undo; a step whose effect is externally visible should declare one.\n\t */\n\tcompensateWith?: { actionId: string; parameters?: Record<string, unknown> };\n}\n\nexport interface CompensatedSagaInput {\n\tworkflowId: string;\n\trunId: string;\n\tsteps: CompensatedStep[];\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tparentInvocationId?: string;\n\t/** When supplied, every step's reversal must match what the manifest declares. */\n\tworkflow?: DeclaredWorkflow;\n}\n\nexport interface CompensationRecord {\n\tstepId: string;\n\tactionId: string;\n\t/** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */\n\terror?: string;\n}\n\nexport interface CompensatedSagaResult {\n\tcompleted: string[];\n\tcompensated: CompensationRecord[];\n\tfailedStep?: { stepId: string; error: string };\n}\n\n/** Reversals are governed invocations too, so they carry their own stable identity. */\nexport function compensationInvocationIdentity(input: { workflowId: string; stepId: string }) {\n\treturn temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });\n}\n\nexport class SagaCompensationError extends Error {\n\toverride readonly name = \"SagaCompensationError\";\n\tconstructor(readonly result: CompensatedSagaResult) {\n\t\tsuper(`Saga step \"${result.failedStep?.stepId}\" failed: ${result.failedStep?.error}`);\n\t}\n}\n\ntype GovernedActivity = ReturnType<typeof createGovernedActionActivities>[\"invokeGovernedAction\"];\n\n/**\n * The governed Host resolves for outcomes that are not success — a blocked policy, a\n * failed validation, a reconciliation, an approval park. Treating a resolved promise\n * as a completed step would silently skip the unwind, so the status is inspected.\n */\nconst SUCCESSFUL_STATUS = \"completed\";\n\nfunction stepFailure(result: unknown, label: string): string | undefined {\n\tif (!result || typeof result !== \"object\") return undefined;\n\tconst status = (result as { status?: unknown }).status;\n\tif (typeof status !== \"string\" || status === SUCCESSFUL_STATUS) return undefined;\n\tconst error = (result as { error?: unknown }).error;\n\treturn `${label} resolved with status \"${status}\"${typeof error === \"string\" && error ? `: ${error}` : \"\"}`;\n}\n\nfunction assertDistinctSteps(steps: CompensatedStep[]): void {\n\tconst seen = new Set<string>();\n\tfor (const step of steps) {\n\t\tif (!step.stepId.trim()) throw new Error(\"Every saga step requires a stepId.\");\n\t\t// A forward step called \"compensate:x\" would share an identity with the reversal of \"x\".\n\t\tif (step.stepId.startsWith(\"compensate:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"compensate:\" prefix.`);\n\t\t// A forward step called \"parent:x\" would share an identity with a parent lifecycle activity.\n\t\tif (step.stepId.startsWith(\"parent:\")) throw new Error(`Saga step \"${step.stepId}\" must not use the reserved \"parent:\" prefix.`);\n\t\tif (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step \"${step.stepId}\"; step identity must be unique within a workflow.`);\n\t\tseen.add(step.stepId);\n\t}\n}\n\n/** The manifest declaration this saga is expected to honour, when one is supplied. */\nexport interface DeclaredWorkflow {\n\tname: string;\n\tcompensations?: Array<{ step: string; actionId: string }>;\n}\n\nfunction assertMatchesDeclaration(steps: CompensatedStep[], workflow: DeclaredWorkflow): void {\n\tconst declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));\n\tfor (const step of steps) {\n\t\tconst expected = declared.get(step.stepId);\n\t\tif (expected === undefined) {\n\t\t\tif (step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" supplies a reversal the manifest does not declare.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!step.compensateWith) throw new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" omits the reversal \"${expected}\" its manifest declares.`);\n\t\tif (step.compensateWith.actionId !== expected) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" step \"${step.stepId}\" reverses with \"${step.compensateWith.actionId}\" but its manifest declares \"${expected}\".`);\n\t\t}\n\t}\n\tfor (const [step, actionId] of declared) {\n\t\tif (!steps.some((candidate) => candidate.stepId === step)) {\n\t\t\tthrow new Error(`Workflow \"${workflow.name}\" declares a reversal \"${actionId}\" for step \"${step}\", which this saga never runs.`);\n\t\t}\n\t}\n}\n\n/**\n * Runs steps in order, accumulating reversals as each succeeds. On failure the\n * accumulated reversals run newest-first, so an unwind never leaves an orphan.\n * Every forward step and every reversal enters through the governed Host with a\n * deterministic idempotency key, so a retried or replayed unwind is not double-applied.\n */\nexport async function runCompensatedSaga(\n\tactivity: GovernedActivity,\n\tinput: CompensatedSagaInput,\n): Promise<CompensatedSagaResult> {\n\tconst scope = {\n\t\ttenantId: input.tenantId,\n\t\tspaceId: input.spaceId,\n\t\tactorId: input.actorId,\n\t\tactorType: input.actorType,\n\t\tworkflowId: input.workflowId,\n\t\trunId: input.runId,\n\t\t...(input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}),\n\t};\n\tassertDistinctSteps(input.steps);\n\tif (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);\n\n\tconst completed: string[] = [];\n\tconst reversals: CompensatedStep[] = [];\n\n\tconst unwind = async (failedStep: { stepId: string; error: string }): Promise<never> => {\n\t\tconst compensated: CompensationRecord[] = [];\n\t\t// Newest-first: the last effect applied is the first one withdrawn.\n\t\tfor (const applied of [...reversals].reverse()) {\n\t\t\tconst reversal = applied.compensateWith!;\n\t\t\tconst record: CompensationRecord = { stepId: applied.stepId, actionId: reversal.actionId };\n\t\t\ttry {\n\t\t\t\tconst result = await activity({\n\t\t\t\t\t...scope,\n\t\t\t\t\tactionId: reversal.actionId,\n\t\t\t\t\tparameters: reversal.parameters ?? applied.parameters,\n\t\t\t\t\tstepId: `compensate:${applied.stepId}`,\n\t\t\t\t});\n\t\t\t\tconst refused = stepFailure(result, `reversal \"${reversal.actionId}\"`);\n\t\t\t\tif (refused) record.error = refused;\n\t\t\t} catch (compensationError) {\n\t\t\t\trecord.error = compensationError instanceof Error ? compensationError.message : String(compensationError);\n\t\t\t}\n\t\t\tcompensated.push(record);\n\t\t}\n\t\tthrow new SagaCompensationError({ completed, compensated, failedStep });\n\t};\n\n\tfor (const step of input.steps) {\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tresult = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });\n\t\t} catch (error) {\n\t\t\treturn await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t\tconst refused = stepFailure(result, `step \"${step.stepId}\"`);\n\t\tif (refused) return await unwind({ stepId: step.stepId, error: refused });\n\t\tcompleted.push(step.stepId);\n\t\tif (step.compensateWith) reversals.push(step);\n\t}\n\n\treturn { completed, compensated: [] };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabricorg/platform-temporal",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Temporal binding contracts for governed
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "Temporal binding contracts for governed TechFabric Platform actions and sagas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/index.cjs",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"tsup": "^8.5.0",
|
|
38
38
|
"typescript": "^5.7.3",
|
|
39
39
|
"vitest": "^4.1.5",
|
|
40
|
-
"@fabricorg/platform": "1.
|
|
41
|
-
"@fabricorg/platform-host": "7.
|
|
40
|
+
"@fabricorg/platform": "1.8.0",
|
|
41
|
+
"@fabricorg/platform-host": "7.4.0"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=20"
|