@fabricorg/platform-temporal 3.0.0 → 4.1.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 CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 293561d: Close adapter and saga correctness gaps: deliver actual deadline and AbortSignal context to adapters, require the governed Host to classify thrown adapter failures before retrying, reject contradictory successful ambiguous outcomes, reserve parent lifecycle identity prefixes, and provide a durable Platform Host parent lifecycle adapter tested through public store seams.
8
+ - 4e585a4: Close final production-maturity correctness gaps: expired leases can no longer be renewed,
9
+ saga parent transitions are serialized, atomic, terminal-safe, and retry-idempotent by lifecycle identity, hydrated
10
+ projections must meet requested checkpoints, and SDUI releases accept components only from
11
+ resolved document packs. The release gate now verifies every package and public subpath in
12
+ isolated ESM, CommonJS, and TypeScript consumers.
13
+ - fb14d67: Add versioned PostgreSQL migrations and live PostgreSQL 16 certification. Fence and renew invocation
14
+ and outbox leases, bound publisher calls, reject stale finalization, and add deterministic Temporal
15
+ saga workflow binding with explicit parent lifecycle activities.
16
+ - eefd1eb: Complete the saga parent lifecycle: add durable progress, approval-parking, and cancellation activity
17
+ contracts alongside the existing completion and failure seams. Add stable parent and child causation
18
+ identity helpers and stable saga signal names. Cover duplicate dispatch, cancellation, approval,
19
+ compensation failure, and worker rollover through public Temporal seams.
20
+
21
+ ### Additional release notes
22
+
23
+ ### Minor Changes
24
+
25
+ - Add an instance-scoped saga workflow registry, stable Temporal dispatcher, and explicit durable
26
+ parent completion and compensation-failure activity seams.
27
+
28
+ - Complete the saga parent lifecycle: add durable progress, approval-parking, and cancellation
29
+ activity contracts alongside the existing completion and failure seams. Add stable parent and
30
+ child causation identity helpers (`sagaParentActivityIdentity`, `sagaChildCausation`) and stable
31
+ saga signal names (`SAGA_SIGNALS`). Cover duplicate dispatch, cancellation, approval, compensation
32
+ failure, and worker rollover through public Temporal seams.
33
+ - Reserve the `parent:` identity prefix in `assertDistinctSteps` so a forward step cannot collide
34
+ with a parent lifecycle activity identity.
35
+ - Attach stable occurrence-specific lifecycle identities in `createSagaParentActivities` so
36
+ activity retries map to one durable parent transition.
37
+
38
+ ## 4.0.0
39
+
40
+ ### Major Changes
41
+
42
+ - c504d0a: Make durability a declared, diffable contract.
43
+
44
+ `CapabilityMetadata` gains `workflows`: each declares its task queue, the event that starts it, the
45
+ durable deadlines it carries (resolved from configuration keys, never literals), and which governed
46
+ action reverses each compensatable step. The field is additive, so `MODULE_MANIFEST_VERSION` stays
47
+ at 2.
48
+
49
+ `diffModuleManifests` classifies this plane instead of reporting one blanket capability change:
50
+ adding a workflow, timer or reversal is compatible; moving a task queue, changing a trigger, dropping
51
+ a deadline or repointing a reversal is potentially breaking; removing a workflow or reversal is
52
+ breaking.
53
+
54
+ The compiler validates serialized workflow declarations, which never pass through a registry, and
55
+ rejects an action declaring `completion: "immediate"` while `kind: "saga"` — a manifest contradiction
56
+ that now fails the build rather than a running host.
57
+
58
+ `platform-temporal` executes declared reversals. `runCompensatedSaga` unwinds newest-first, routes
59
+ every reversal through the governed Host under a distinct deterministic
60
+ `compensationInvocationIdentity`, and surfaces a reversal that itself failed on
61
+ `SagaCompensationError` rather than swallowing it. A step that _resolves_ with a non-successful
62
+ status — blocked, failed, parked for approval, sent to reconciliation — now triggers the unwind
63
+ instead of being recorded as completed. Step identities must be unique and may not use the reserved
64
+ `compensate:` prefix, and when a `workflow` declaration is supplied every reversal is checked against
65
+ it.
66
+
3
67
  ## 3.0.0
4
68
 
5
69
  ### Patch Changes
package/README.md CHANGED
@@ -3,3 +3,18 @@
3
3
  Host-side Temporal binding for governed Platform actions. Activities invoke the public governed Host boundary with stable workflow/step idempotency, carry workflow evidence, and map saga/approval messages to signals. Temporal supplies durable orchestration; Platform invocation and event records remain canonical audit evidence. This package does not call handlers or make Platform a workflow authority.
4
4
 
5
5
  `temporalWorkflowStartClient()` adapts the official `@temporalio/client` `WorkflowClient`; all direct Temporal packages in an application must remain on the family-supported version line.
6
+
7
+ `SagaWorkflowRegistry` maps saga action IDs to workflow types and task queues without process-global
8
+ registration. `createTemporalSagaDispatcher()` starts the parent under the Host's stable workflow ID
9
+ with Temporal's use-existing conflict policy. `createSagaParentActivities()` exposes explicit durable
10
+ progress, approval-parking, cancellation, completion, and compensation-failure callbacks; workflows
11
+ must finalize the canonical parent invocation through those activities rather than treating Temporal
12
+ history as the audit ledger.
13
+
14
+ `createSagaParentActivities()` attaches a distinct stable `lifecycleId` to each progress step,
15
+ approval gate, cancellation, completion, and failure. `sagaParentActivityIdentity()` accepts an
16
+ optional occurrence discriminator for callers constructing those identities directly, while
17
+ `sagaChildCausation()` produces stable child causation. A durable lifecycle adapter uses these
18
+ identities to make a retried or replayed activity idempotent. `SAGA_SIGNALS` declares
19
+ the stable signal names (`saga.proceed`, `saga.cancel`, `saga.approval`) a workflow may register
20
+ handlers for.
package/dist/index.cjs CHANGED
@@ -20,9 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ SAGA_SIGNALS: () => SAGA_SIGNALS,
24
+ SagaCompensationError: () => SagaCompensationError,
25
+ SagaWorkflowRegistry: () => SagaWorkflowRegistry,
23
26
  assertWorkerVersionEvidence: () => assertWorkerVersionEvidence,
27
+ compensationInvocationIdentity: () => compensationInvocationIdentity,
24
28
  createGovernedActionActivities: () => createGovernedActionActivities,
29
+ createSagaParentActivities: () => createSagaParentActivities,
25
30
  createSagaSignalDispatcher: () => createSagaSignalDispatcher,
31
+ createTemporalSagaDispatcher: () => createTemporalSagaDispatcher,
32
+ runCompensatedSaga: () => runCompensatedSaga,
33
+ sagaChildCausation: () => sagaChildCausation,
34
+ sagaParentActivityIdentity: () => sagaParentActivityIdentity,
26
35
  startGovernedWorkflow: () => startGovernedWorkflow,
27
36
  temporalInvocationIdentity: () => temporalInvocationIdentity,
28
37
  temporalWorkflowStartClient: () => temporalWorkflowStartClient
@@ -55,6 +64,106 @@ function createSagaSignalDispatcher(client) {
55
64
  function startGovernedWorkflow(client, input) {
56
65
  return client.start({ ...input, idConflictPolicy: "use_existing" });
57
66
  }
67
+ var SagaWorkflowRegistry = class {
68
+ bindings = /* @__PURE__ */ new Map();
69
+ constructor(bindings = []) {
70
+ for (const binding of bindings) this.register(binding);
71
+ }
72
+ register(binding) {
73
+ if (this.bindings.has(binding.actionId)) {
74
+ throw new Error(`Saga workflow binding already registered: ${binding.actionId}`);
75
+ }
76
+ if (!binding.workflowType.trim() || !binding.taskQueue.trim()) {
77
+ throw new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);
78
+ }
79
+ this.bindings.set(binding.actionId, { ...binding });
80
+ }
81
+ require(actionId) {
82
+ const binding = this.bindings.get(actionId);
83
+ if (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);
84
+ return { ...binding };
85
+ }
86
+ };
87
+ function createTemporalSagaDispatcher(client, registry) {
88
+ return {
89
+ dispatch: async (input) => {
90
+ const binding = registry.require(input.actionId);
91
+ const handle = await startGovernedWorkflow(client, {
92
+ workflowType: binding.workflowType,
93
+ workflowId: input.workflowId,
94
+ taskQueue: binding.taskQueue,
95
+ args: [{
96
+ parentInvocationId: input.actionInvocationId,
97
+ actionId: input.actionId,
98
+ tenantId: input.tenantId,
99
+ spaceId: input.spaceId
100
+ }]
101
+ });
102
+ const runId = runIdFromHandle(handle);
103
+ return { workflowId: input.workflowId, ...runId ? { runId } : {} };
104
+ }
105
+ };
106
+ }
107
+ function runIdFromHandle(handle) {
108
+ if (!handle || typeof handle !== "object") return void 0;
109
+ const candidate = handle.firstExecutionRunId;
110
+ return typeof candidate === "string" && candidate ? candidate : void 0;
111
+ }
112
+ function createSagaParentActivities(lifecycle) {
113
+ return {
114
+ recordSagaProgress: (input) => lifecycle.recordProgress({
115
+ ...input,
116
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
117
+ input.parentInvocationId,
118
+ "progress",
119
+ `${input.progress.stepId}:${input.progress.index}`
120
+ ).idempotencyKey
121
+ }),
122
+ parkSagaApproval: (input) => lifecycle.parkApproval({
123
+ ...input,
124
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
125
+ input.parentInvocationId,
126
+ "approval",
127
+ input.approvalId
128
+ ).idempotencyKey
129
+ }),
130
+ cancelSagaParent: (input) => lifecycle.cancel({
131
+ ...input,
132
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
133
+ input.parentInvocationId,
134
+ "cancel"
135
+ ).idempotencyKey
136
+ }),
137
+ completeSagaParent: (input) => lifecycle.complete({
138
+ ...input,
139
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
140
+ input.parentInvocationId,
141
+ "complete"
142
+ ).idempotencyKey
143
+ }),
144
+ failSagaParent: (input) => lifecycle.fail({
145
+ ...input,
146
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
147
+ input.parentInvocationId,
148
+ "fail"
149
+ ).idempotencyKey
150
+ })
151
+ };
152
+ }
153
+ function sagaParentActivityIdentity(workflowId, activity, discriminator) {
154
+ return temporalInvocationIdentity({
155
+ workflowId,
156
+ stepId: `parent:${activity}${discriminator ? `:${discriminator}` : ""}`
157
+ });
158
+ }
159
+ function sagaChildCausation(workflowId, parentInvocationId, stepId) {
160
+ return { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };
161
+ }
162
+ var SAGA_SIGNALS = {
163
+ proceed: "saga.proceed",
164
+ cancel: "saga.cancel",
165
+ approval: "saga.approval"
166
+ };
58
167
  function temporalWorkflowStartClient(client) {
59
168
  return {
60
169
  start: ({ workflowType, workflowId, taskQueue, args }) => client.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: "USE_EXISTING" })
@@ -63,11 +172,117 @@ function temporalWorkflowStartClient(client) {
63
172
  function assertWorkerVersionEvidence(value) {
64
173
  if (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error("Temporal worker buildId and deploymentName are required for rollover evidence.");
65
174
  }
175
+ function compensationInvocationIdentity(input) {
176
+ return temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });
177
+ }
178
+ var SagaCompensationError = class extends Error {
179
+ constructor(result) {
180
+ super(`Saga step "${result.failedStep?.stepId}" failed: ${result.failedStep?.error}`);
181
+ this.result = result;
182
+ }
183
+ result;
184
+ name = "SagaCompensationError";
185
+ };
186
+ var SUCCESSFUL_STATUS = "completed";
187
+ function stepFailure(result, label) {
188
+ if (!result || typeof result !== "object") return void 0;
189
+ const status = result.status;
190
+ if (typeof status !== "string" || status === SUCCESSFUL_STATUS) return void 0;
191
+ const error = result.error;
192
+ return `${label} resolved with status "${status}"${typeof error === "string" && error ? `: ${error}` : ""}`;
193
+ }
194
+ function assertDistinctSteps(steps) {
195
+ const seen = /* @__PURE__ */ new Set();
196
+ for (const step of steps) {
197
+ if (!step.stepId.trim()) throw new Error("Every saga step requires a stepId.");
198
+ if (step.stepId.startsWith("compensate:")) throw new Error(`Saga step "${step.stepId}" must not use the reserved "compensate:" prefix.`);
199
+ if (step.stepId.startsWith("parent:")) throw new Error(`Saga step "${step.stepId}" must not use the reserved "parent:" prefix.`);
200
+ if (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step "${step.stepId}"; step identity must be unique within a workflow.`);
201
+ seen.add(step.stepId);
202
+ }
203
+ }
204
+ function assertMatchesDeclaration(steps, workflow) {
205
+ const declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));
206
+ for (const step of steps) {
207
+ const expected = declared.get(step.stepId);
208
+ if (expected === void 0) {
209
+ if (step.compensateWith) throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" supplies a reversal the manifest does not declare.`);
210
+ continue;
211
+ }
212
+ if (!step.compensateWith) throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" omits the reversal "${expected}" its manifest declares.`);
213
+ if (step.compensateWith.actionId !== expected) {
214
+ throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" reverses with "${step.compensateWith.actionId}" but its manifest declares "${expected}".`);
215
+ }
216
+ }
217
+ for (const [step, actionId] of declared) {
218
+ if (!steps.some((candidate) => candidate.stepId === step)) {
219
+ throw new Error(`Workflow "${workflow.name}" declares a reversal "${actionId}" for step "${step}", which this saga never runs.`);
220
+ }
221
+ }
222
+ }
223
+ async function runCompensatedSaga(activity, input) {
224
+ const scope = {
225
+ tenantId: input.tenantId,
226
+ spaceId: input.spaceId,
227
+ actorId: input.actorId,
228
+ actorType: input.actorType,
229
+ workflowId: input.workflowId,
230
+ runId: input.runId,
231
+ ...input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}
232
+ };
233
+ assertDistinctSteps(input.steps);
234
+ if (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);
235
+ const completed = [];
236
+ const reversals = [];
237
+ const unwind = async (failedStep) => {
238
+ const compensated = [];
239
+ for (const applied of [...reversals].reverse()) {
240
+ const reversal = applied.compensateWith;
241
+ const record = { stepId: applied.stepId, actionId: reversal.actionId };
242
+ try {
243
+ const result = await activity({
244
+ ...scope,
245
+ actionId: reversal.actionId,
246
+ parameters: reversal.parameters ?? applied.parameters,
247
+ stepId: `compensate:${applied.stepId}`
248
+ });
249
+ const refused = stepFailure(result, `reversal "${reversal.actionId}"`);
250
+ if (refused) record.error = refused;
251
+ } catch (compensationError) {
252
+ record.error = compensationError instanceof Error ? compensationError.message : String(compensationError);
253
+ }
254
+ compensated.push(record);
255
+ }
256
+ throw new SagaCompensationError({ completed, compensated, failedStep });
257
+ };
258
+ for (const step of input.steps) {
259
+ let result;
260
+ try {
261
+ result = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });
262
+ } catch (error) {
263
+ return await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });
264
+ }
265
+ const refused = stepFailure(result, `step "${step.stepId}"`);
266
+ if (refused) return await unwind({ stepId: step.stepId, error: refused });
267
+ completed.push(step.stepId);
268
+ if (step.compensateWith) reversals.push(step);
269
+ }
270
+ return { completed, compensated: [] };
271
+ }
66
272
  // Annotate the CommonJS export names for ESM import in node:
67
273
  0 && (module.exports = {
274
+ SAGA_SIGNALS,
275
+ SagaCompensationError,
276
+ SagaWorkflowRegistry,
68
277
  assertWorkerVersionEvidence,
278
+ compensationInvocationIdentity,
69
279
  createGovernedActionActivities,
280
+ createSagaParentActivities,
70
281
  createSagaSignalDispatcher,
282
+ createTemporalSagaDispatcher,
283
+ runCompensatedSaga,
284
+ sagaChildCausation,
285
+ sagaParentActivityIdentity,
71
286
  startGovernedWorkflow,
72
287
  temporalInvocationIdentity,
73
288
  temporalWorkflowStartClient
@@ -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\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;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;","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> { 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":[]}
package/dist/index.d.cts CHANGED
@@ -66,6 +66,133 @@ declare function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THan
66
66
  taskQueue: string;
67
67
  args: unknown[];
68
68
  }): Promise<THandle>;
69
+ interface SagaWorkflowBinding {
70
+ actionId: `${string}.${string}`;
71
+ workflowType: string;
72
+ taskQueue: string;
73
+ }
74
+ /** Instance-scoped registry; registration order never changes resolution. */
75
+ declare class SagaWorkflowRegistry {
76
+ private readonly bindings;
77
+ constructor(bindings?: readonly SagaWorkflowBinding[]);
78
+ register(binding: SagaWorkflowBinding): void;
79
+ require(actionId: string): SagaWorkflowBinding;
80
+ }
81
+ interface TemporalSagaDispatchInput {
82
+ actionInvocationId: string;
83
+ actionId: `${string}.${string}`;
84
+ tenantId: string;
85
+ spaceId: string;
86
+ workflowId: string;
87
+ }
88
+ /** Starts the registered parent workflow with an invocation-only durable identity. */
89
+ declare function createTemporalSagaDispatcher<THandle>(client: WorkflowStartClient<THandle>, registry: SagaWorkflowRegistry): {
90
+ dispatch: (input: TemporalSagaDispatchInput) => Promise<{
91
+ runId?: string | undefined;
92
+ workflowId: string;
93
+ }>;
94
+ };
95
+ /** Durable record of a single step completing within a saga parent. */
96
+ interface SagaParentProgressRecord {
97
+ /** The step that just completed. */
98
+ stepId: string;
99
+ /** Zero-based position of this step in the declared sequence. */
100
+ index: number;
101
+ /** Total steps declared for this saga. */
102
+ total: number;
103
+ /** ISO-8601 timestamp the progress was recorded. */
104
+ completedAt: string;
105
+ }
106
+ interface SagaParentProgressInput {
107
+ parentInvocationId: string;
108
+ tenantId: string;
109
+ spaceId: string;
110
+ /** Stable identity shared by every retry of this lifecycle transition. */
111
+ lifecycleId?: string;
112
+ progress: SagaParentProgressRecord;
113
+ }
114
+ interface SagaParentApprovalInput {
115
+ parentInvocationId: string;
116
+ tenantId: string;
117
+ spaceId: string;
118
+ /** Stable identity shared by every retry of this lifecycle transition. */
119
+ lifecycleId?: string;
120
+ /** The step at which the saga paused for human approval. */
121
+ stepId: string;
122
+ /** Stable identifier for the approval gate, so a later signal can resume it. */
123
+ approvalId: string;
124
+ reason: string;
125
+ }
126
+ interface SagaParentCancellationInput {
127
+ parentInvocationId: string;
128
+ tenantId: string;
129
+ spaceId: string;
130
+ /** Stable identity shared by every retry of this lifecycle transition. */
131
+ lifecycleId?: string;
132
+ reason: string;
133
+ /** Actor requesting the cancellation, for audit attribution. */
134
+ actorId: string;
135
+ }
136
+ interface SagaParentLifecycle {
137
+ /** Record durable progress as each step completes. */
138
+ recordProgress(input: SagaParentProgressInput): Promise<void>;
139
+ /** Park the parent for human approval at a specific step. */
140
+ parkApproval(input: SagaParentApprovalInput): Promise<void>;
141
+ /** Cancel the parent saga with an attributable reason. */
142
+ cancel(input: SagaParentCancellationInput): Promise<void>;
143
+ /** Complete the parent saga with its final result. */
144
+ complete(input: {
145
+ parentInvocationId: string;
146
+ tenantId: string;
147
+ spaceId: string;
148
+ lifecycleId?: string;
149
+ result: Record<string, unknown>;
150
+ }): Promise<void>;
151
+ /** Fail the parent saga, surfacing any compensation records. */
152
+ fail(input: {
153
+ parentInvocationId: string;
154
+ tenantId: string;
155
+ spaceId: string;
156
+ lifecycleId?: string;
157
+ error: string;
158
+ compensation: CompensationRecord[];
159
+ }): Promise<void>;
160
+ }
161
+ /** Parent lifecycle remains explicit durable activities, never workflow-local state. */
162
+ declare function createSagaParentActivities(lifecycle: SagaParentLifecycle): {
163
+ recordSagaProgress: (input: SagaParentProgressInput) => Promise<void>;
164
+ parkSagaApproval: (input: SagaParentApprovalInput) => Promise<void>;
165
+ cancelSagaParent: (input: SagaParentCancellationInput) => Promise<void>;
166
+ completeSagaParent: (input: Parameters<SagaParentLifecycle["complete"]>[0]) => Promise<void>;
167
+ failSagaParent: (input: Parameters<SagaParentLifecycle["fail"]>[0]) => Promise<void>;
168
+ };
169
+ /**
170
+ * Stable identity for a saga parent's own lifecycle activity (progress,
171
+ * cancellation, approval parking, completion, failure). The activity stepId
172
+ * is namespaced under `parent:` so it can never collide with a forward step or
173
+ * a `compensate:` reversal.
174
+ */
175
+ declare function sagaParentActivityIdentity(workflowId: string, activity: SagaParentActivityKind, discriminator?: string): {
176
+ idempotencyKey: string;
177
+ correlationId: string;
178
+ };
179
+ type SagaParentActivityKind = "progress" | "approval" | "cancel" | "complete" | "fail";
180
+ /**
181
+ * Stable causation identity linking a child step to its parent invocation.
182
+ * The parent invocation ID becomes the causation ID; the step keeps its own
183
+ * deterministic idempotency key and shares the workflow correlation ID.
184
+ */
185
+ declare function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string): {
186
+ causationId: string;
187
+ idempotencyKey: string;
188
+ correlationId: string;
189
+ };
190
+ /** Stable signal names a saga workflow may register handlers for. */
191
+ declare const SAGA_SIGNALS: {
192
+ readonly proceed: "saga.proceed";
193
+ readonly cancel: "saga.cancel";
194
+ readonly approval: "saga.approval";
195
+ };
69
196
  /** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */
70
197
  declare function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient;
71
198
  interface WorkerVersionEvidence {
@@ -74,5 +201,74 @@ interface WorkerVersionEvidence {
74
201
  rampPercent?: number;
75
202
  }
76
203
  declare function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void;
204
+ interface CompensatedStep {
205
+ /** Workflow-local step identity. Must match the compensation declared in the module manifest. */
206
+ stepId: string;
207
+ actionId: string;
208
+ parameters: Record<string, unknown>;
209
+ /**
210
+ * Governed action that reverses this step's effect. Omit for a step with
211
+ * nothing to undo; a step whose effect is externally visible should declare one.
212
+ */
213
+ compensateWith?: {
214
+ actionId: string;
215
+ parameters?: Record<string, unknown>;
216
+ };
217
+ }
218
+ interface CompensatedSagaInput {
219
+ workflowId: string;
220
+ runId: string;
221
+ steps: CompensatedStep[];
222
+ tenantId: string;
223
+ spaceId: string;
224
+ actorId: string;
225
+ actorType: ActorType;
226
+ parentInvocationId?: string;
227
+ /** When supplied, every step's reversal must match what the manifest declares. */
228
+ workflow?: DeclaredWorkflow;
229
+ }
230
+ interface CompensationRecord {
231
+ stepId: string;
232
+ actionId: string;
233
+ /** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */
234
+ error?: string;
235
+ }
236
+ interface CompensatedSagaResult {
237
+ completed: string[];
238
+ compensated: CompensationRecord[];
239
+ failedStep?: {
240
+ stepId: string;
241
+ error: string;
242
+ };
243
+ }
244
+ /** Reversals are governed invocations too, so they carry their own stable identity. */
245
+ declare function compensationInvocationIdentity(input: {
246
+ workflowId: string;
247
+ stepId: string;
248
+ }): {
249
+ idempotencyKey: string;
250
+ correlationId: string;
251
+ };
252
+ declare class SagaCompensationError extends Error {
253
+ readonly result: CompensatedSagaResult;
254
+ readonly name = "SagaCompensationError";
255
+ constructor(result: CompensatedSagaResult);
256
+ }
257
+ type GovernedActivity = ReturnType<typeof createGovernedActionActivities>["invokeGovernedAction"];
258
+ /** The manifest declaration this saga is expected to honour, when one is supplied. */
259
+ interface DeclaredWorkflow {
260
+ name: string;
261
+ compensations?: Array<{
262
+ step: string;
263
+ actionId: string;
264
+ }>;
265
+ }
266
+ /**
267
+ * Runs steps in order, accumulating reversals as each succeeds. On failure the
268
+ * accumulated reversals run newest-first, so an unwind never leaves an orphan.
269
+ * Every forward step and every reversal enters through the governed Host with a
270
+ * deterministic idempotency key, so a retried or replayed unwind is not double-applied.
271
+ */
272
+ declare function runCompensatedSaga(activity: GovernedActivity, input: CompensatedSagaInput): Promise<CompensatedSagaResult>;
77
273
 
78
- export { type GovernedActionActivityInput, type GovernedActionInvoker, type SagaSignalEnvelope, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, createGovernedActionActivities, createSagaSignalDispatcher, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
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 };
package/dist/index.d.ts CHANGED
@@ -66,6 +66,133 @@ declare function startGovernedWorkflow<THandle>(client: WorkflowStartClient<THan
66
66
  taskQueue: string;
67
67
  args: unknown[];
68
68
  }): Promise<THandle>;
69
+ interface SagaWorkflowBinding {
70
+ actionId: `${string}.${string}`;
71
+ workflowType: string;
72
+ taskQueue: string;
73
+ }
74
+ /** Instance-scoped registry; registration order never changes resolution. */
75
+ declare class SagaWorkflowRegistry {
76
+ private readonly bindings;
77
+ constructor(bindings?: readonly SagaWorkflowBinding[]);
78
+ register(binding: SagaWorkflowBinding): void;
79
+ require(actionId: string): SagaWorkflowBinding;
80
+ }
81
+ interface TemporalSagaDispatchInput {
82
+ actionInvocationId: string;
83
+ actionId: `${string}.${string}`;
84
+ tenantId: string;
85
+ spaceId: string;
86
+ workflowId: string;
87
+ }
88
+ /** Starts the registered parent workflow with an invocation-only durable identity. */
89
+ declare function createTemporalSagaDispatcher<THandle>(client: WorkflowStartClient<THandle>, registry: SagaWorkflowRegistry): {
90
+ dispatch: (input: TemporalSagaDispatchInput) => Promise<{
91
+ runId?: string | undefined;
92
+ workflowId: string;
93
+ }>;
94
+ };
95
+ /** Durable record of a single step completing within a saga parent. */
96
+ interface SagaParentProgressRecord {
97
+ /** The step that just completed. */
98
+ stepId: string;
99
+ /** Zero-based position of this step in the declared sequence. */
100
+ index: number;
101
+ /** Total steps declared for this saga. */
102
+ total: number;
103
+ /** ISO-8601 timestamp the progress was recorded. */
104
+ completedAt: string;
105
+ }
106
+ interface SagaParentProgressInput {
107
+ parentInvocationId: string;
108
+ tenantId: string;
109
+ spaceId: string;
110
+ /** Stable identity shared by every retry of this lifecycle transition. */
111
+ lifecycleId?: string;
112
+ progress: SagaParentProgressRecord;
113
+ }
114
+ interface SagaParentApprovalInput {
115
+ parentInvocationId: string;
116
+ tenantId: string;
117
+ spaceId: string;
118
+ /** Stable identity shared by every retry of this lifecycle transition. */
119
+ lifecycleId?: string;
120
+ /** The step at which the saga paused for human approval. */
121
+ stepId: string;
122
+ /** Stable identifier for the approval gate, so a later signal can resume it. */
123
+ approvalId: string;
124
+ reason: string;
125
+ }
126
+ interface SagaParentCancellationInput {
127
+ parentInvocationId: string;
128
+ tenantId: string;
129
+ spaceId: string;
130
+ /** Stable identity shared by every retry of this lifecycle transition. */
131
+ lifecycleId?: string;
132
+ reason: string;
133
+ /** Actor requesting the cancellation, for audit attribution. */
134
+ actorId: string;
135
+ }
136
+ interface SagaParentLifecycle {
137
+ /** Record durable progress as each step completes. */
138
+ recordProgress(input: SagaParentProgressInput): Promise<void>;
139
+ /** Park the parent for human approval at a specific step. */
140
+ parkApproval(input: SagaParentApprovalInput): Promise<void>;
141
+ /** Cancel the parent saga with an attributable reason. */
142
+ cancel(input: SagaParentCancellationInput): Promise<void>;
143
+ /** Complete the parent saga with its final result. */
144
+ complete(input: {
145
+ parentInvocationId: string;
146
+ tenantId: string;
147
+ spaceId: string;
148
+ lifecycleId?: string;
149
+ result: Record<string, unknown>;
150
+ }): Promise<void>;
151
+ /** Fail the parent saga, surfacing any compensation records. */
152
+ fail(input: {
153
+ parentInvocationId: string;
154
+ tenantId: string;
155
+ spaceId: string;
156
+ lifecycleId?: string;
157
+ error: string;
158
+ compensation: CompensationRecord[];
159
+ }): Promise<void>;
160
+ }
161
+ /** Parent lifecycle remains explicit durable activities, never workflow-local state. */
162
+ declare function createSagaParentActivities(lifecycle: SagaParentLifecycle): {
163
+ recordSagaProgress: (input: SagaParentProgressInput) => Promise<void>;
164
+ parkSagaApproval: (input: SagaParentApprovalInput) => Promise<void>;
165
+ cancelSagaParent: (input: SagaParentCancellationInput) => Promise<void>;
166
+ completeSagaParent: (input: Parameters<SagaParentLifecycle["complete"]>[0]) => Promise<void>;
167
+ failSagaParent: (input: Parameters<SagaParentLifecycle["fail"]>[0]) => Promise<void>;
168
+ };
169
+ /**
170
+ * Stable identity for a saga parent's own lifecycle activity (progress,
171
+ * cancellation, approval parking, completion, failure). The activity stepId
172
+ * is namespaced under `parent:` so it can never collide with a forward step or
173
+ * a `compensate:` reversal.
174
+ */
175
+ declare function sagaParentActivityIdentity(workflowId: string, activity: SagaParentActivityKind, discriminator?: string): {
176
+ idempotencyKey: string;
177
+ correlationId: string;
178
+ };
179
+ type SagaParentActivityKind = "progress" | "approval" | "cancel" | "complete" | "fail";
180
+ /**
181
+ * Stable causation identity linking a child step to its parent invocation.
182
+ * The parent invocation ID becomes the causation ID; the step keeps its own
183
+ * deterministic idempotency key and shares the workflow correlation ID.
184
+ */
185
+ declare function sagaChildCausation(workflowId: string, parentInvocationId: string, stepId: string): {
186
+ causationId: string;
187
+ idempotencyKey: string;
188
+ correlationId: string;
189
+ };
190
+ /** Stable signal names a saga workflow may register handlers for. */
191
+ declare const SAGA_SIGNALS: {
192
+ readonly proceed: "saga.proceed";
193
+ readonly cancel: "saga.cancel";
194
+ readonly approval: "saga.approval";
195
+ };
69
196
  /** Adapts the official Temporal TypeScript `WorkflowClient` to the narrow testable binding seam. */
70
197
  declare function temporalWorkflowStartClient(client: WorkflowClient): WorkflowStartClient;
71
198
  interface WorkerVersionEvidence {
@@ -74,5 +201,74 @@ interface WorkerVersionEvidence {
74
201
  rampPercent?: number;
75
202
  }
76
203
  declare function assertWorkerVersionEvidence(value: WorkerVersionEvidence): void;
204
+ interface CompensatedStep {
205
+ /** Workflow-local step identity. Must match the compensation declared in the module manifest. */
206
+ stepId: string;
207
+ actionId: string;
208
+ parameters: Record<string, unknown>;
209
+ /**
210
+ * Governed action that reverses this step's effect. Omit for a step with
211
+ * nothing to undo; a step whose effect is externally visible should declare one.
212
+ */
213
+ compensateWith?: {
214
+ actionId: string;
215
+ parameters?: Record<string, unknown>;
216
+ };
217
+ }
218
+ interface CompensatedSagaInput {
219
+ workflowId: string;
220
+ runId: string;
221
+ steps: CompensatedStep[];
222
+ tenantId: string;
223
+ spaceId: string;
224
+ actorId: string;
225
+ actorType: ActorType;
226
+ parentInvocationId?: string;
227
+ /** When supplied, every step's reversal must match what the manifest declares. */
228
+ workflow?: DeclaredWorkflow;
229
+ }
230
+ interface CompensationRecord {
231
+ stepId: string;
232
+ actionId: string;
233
+ /** Present when the reversal itself failed; the saga surfaces it rather than swallowing it. */
234
+ error?: string;
235
+ }
236
+ interface CompensatedSagaResult {
237
+ completed: string[];
238
+ compensated: CompensationRecord[];
239
+ failedStep?: {
240
+ stepId: string;
241
+ error: string;
242
+ };
243
+ }
244
+ /** Reversals are governed invocations too, so they carry their own stable identity. */
245
+ declare function compensationInvocationIdentity(input: {
246
+ workflowId: string;
247
+ stepId: string;
248
+ }): {
249
+ idempotencyKey: string;
250
+ correlationId: string;
251
+ };
252
+ declare class SagaCompensationError extends Error {
253
+ readonly result: CompensatedSagaResult;
254
+ readonly name = "SagaCompensationError";
255
+ constructor(result: CompensatedSagaResult);
256
+ }
257
+ type GovernedActivity = ReturnType<typeof createGovernedActionActivities>["invokeGovernedAction"];
258
+ /** The manifest declaration this saga is expected to honour, when one is supplied. */
259
+ interface DeclaredWorkflow {
260
+ name: string;
261
+ compensations?: Array<{
262
+ step: string;
263
+ actionId: string;
264
+ }>;
265
+ }
266
+ /**
267
+ * Runs steps in order, accumulating reversals as each succeeds. On failure the
268
+ * accumulated reversals run newest-first, so an unwind never leaves an orphan.
269
+ * Every forward step and every reversal enters through the governed Host with a
270
+ * deterministic idempotency key, so a retried or replayed unwind is not double-applied.
271
+ */
272
+ declare function runCompensatedSaga(activity: GovernedActivity, input: CompensatedSagaInput): Promise<CompensatedSagaResult>;
77
273
 
78
- export { type GovernedActionActivityInput, type GovernedActionInvoker, type SagaSignalEnvelope, type TemporalSignalClient, type WorkerVersionEvidence, type WorkflowStartClient, assertWorkerVersionEvidence, createGovernedActionActivities, createSagaSignalDispatcher, startGovernedWorkflow, temporalInvocationIdentity, temporalWorkflowStartClient };
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 };
package/dist/index.js CHANGED
@@ -26,6 +26,106 @@ function createSagaSignalDispatcher(client) {
26
26
  function startGovernedWorkflow(client, input) {
27
27
  return client.start({ ...input, idConflictPolicy: "use_existing" });
28
28
  }
29
+ var SagaWorkflowRegistry = class {
30
+ bindings = /* @__PURE__ */ new Map();
31
+ constructor(bindings = []) {
32
+ for (const binding of bindings) this.register(binding);
33
+ }
34
+ register(binding) {
35
+ if (this.bindings.has(binding.actionId)) {
36
+ throw new Error(`Saga workflow binding already registered: ${binding.actionId}`);
37
+ }
38
+ if (!binding.workflowType.trim() || !binding.taskQueue.trim()) {
39
+ throw new Error(`Saga workflow binding ${binding.actionId} requires workflowType and taskQueue`);
40
+ }
41
+ this.bindings.set(binding.actionId, { ...binding });
42
+ }
43
+ require(actionId) {
44
+ const binding = this.bindings.get(actionId);
45
+ if (!binding) throw new Error(`No saga workflow binding registered for ${actionId}`);
46
+ return { ...binding };
47
+ }
48
+ };
49
+ function createTemporalSagaDispatcher(client, registry) {
50
+ return {
51
+ dispatch: async (input) => {
52
+ const binding = registry.require(input.actionId);
53
+ const handle = await startGovernedWorkflow(client, {
54
+ workflowType: binding.workflowType,
55
+ workflowId: input.workflowId,
56
+ taskQueue: binding.taskQueue,
57
+ args: [{
58
+ parentInvocationId: input.actionInvocationId,
59
+ actionId: input.actionId,
60
+ tenantId: input.tenantId,
61
+ spaceId: input.spaceId
62
+ }]
63
+ });
64
+ const runId = runIdFromHandle(handle);
65
+ return { workflowId: input.workflowId, ...runId ? { runId } : {} };
66
+ }
67
+ };
68
+ }
69
+ function runIdFromHandle(handle) {
70
+ if (!handle || typeof handle !== "object") return void 0;
71
+ const candidate = handle.firstExecutionRunId;
72
+ return typeof candidate === "string" && candidate ? candidate : void 0;
73
+ }
74
+ function createSagaParentActivities(lifecycle) {
75
+ return {
76
+ recordSagaProgress: (input) => lifecycle.recordProgress({
77
+ ...input,
78
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
79
+ input.parentInvocationId,
80
+ "progress",
81
+ `${input.progress.stepId}:${input.progress.index}`
82
+ ).idempotencyKey
83
+ }),
84
+ parkSagaApproval: (input) => lifecycle.parkApproval({
85
+ ...input,
86
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
87
+ input.parentInvocationId,
88
+ "approval",
89
+ input.approvalId
90
+ ).idempotencyKey
91
+ }),
92
+ cancelSagaParent: (input) => lifecycle.cancel({
93
+ ...input,
94
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
95
+ input.parentInvocationId,
96
+ "cancel"
97
+ ).idempotencyKey
98
+ }),
99
+ completeSagaParent: (input) => lifecycle.complete({
100
+ ...input,
101
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
102
+ input.parentInvocationId,
103
+ "complete"
104
+ ).idempotencyKey
105
+ }),
106
+ failSagaParent: (input) => lifecycle.fail({
107
+ ...input,
108
+ lifecycleId: input.lifecycleId ?? sagaParentActivityIdentity(
109
+ input.parentInvocationId,
110
+ "fail"
111
+ ).idempotencyKey
112
+ })
113
+ };
114
+ }
115
+ function sagaParentActivityIdentity(workflowId, activity, discriminator) {
116
+ return temporalInvocationIdentity({
117
+ workflowId,
118
+ stepId: `parent:${activity}${discriminator ? `:${discriminator}` : ""}`
119
+ });
120
+ }
121
+ function sagaChildCausation(workflowId, parentInvocationId, stepId) {
122
+ return { ...temporalInvocationIdentity({ workflowId, stepId }), causationId: parentInvocationId };
123
+ }
124
+ var SAGA_SIGNALS = {
125
+ proceed: "saga.proceed",
126
+ cancel: "saga.cancel",
127
+ approval: "saga.approval"
128
+ };
29
129
  function temporalWorkflowStartClient(client) {
30
130
  return {
31
131
  start: ({ workflowType, workflowId, taskQueue, args }) => client.start(workflowType, { workflowId, taskQueue, args, workflowIdConflictPolicy: "USE_EXISTING" })
@@ -34,10 +134,116 @@ function temporalWorkflowStartClient(client) {
34
134
  function assertWorkerVersionEvidence(value) {
35
135
  if (!value.buildId.trim() || !value.deploymentName.trim()) throw new Error("Temporal worker buildId and deploymentName are required for rollover evidence.");
36
136
  }
137
+ function compensationInvocationIdentity(input) {
138
+ return temporalInvocationIdentity({ workflowId: input.workflowId, stepId: `compensate:${input.stepId}` });
139
+ }
140
+ var SagaCompensationError = class extends Error {
141
+ constructor(result) {
142
+ super(`Saga step "${result.failedStep?.stepId}" failed: ${result.failedStep?.error}`);
143
+ this.result = result;
144
+ }
145
+ result;
146
+ name = "SagaCompensationError";
147
+ };
148
+ var SUCCESSFUL_STATUS = "completed";
149
+ function stepFailure(result, label) {
150
+ if (!result || typeof result !== "object") return void 0;
151
+ const status = result.status;
152
+ if (typeof status !== "string" || status === SUCCESSFUL_STATUS) return void 0;
153
+ const error = result.error;
154
+ return `${label} resolved with status "${status}"${typeof error === "string" && error ? `: ${error}` : ""}`;
155
+ }
156
+ function assertDistinctSteps(steps) {
157
+ const seen = /* @__PURE__ */ new Set();
158
+ for (const step of steps) {
159
+ if (!step.stepId.trim()) throw new Error("Every saga step requires a stepId.");
160
+ if (step.stepId.startsWith("compensate:")) throw new Error(`Saga step "${step.stepId}" must not use the reserved "compensate:" prefix.`);
161
+ if (step.stepId.startsWith("parent:")) throw new Error(`Saga step "${step.stepId}" must not use the reserved "parent:" prefix.`);
162
+ if (seen.has(step.stepId)) throw new Error(`Saga declares duplicate step "${step.stepId}"; step identity must be unique within a workflow.`);
163
+ seen.add(step.stepId);
164
+ }
165
+ }
166
+ function assertMatchesDeclaration(steps, workflow) {
167
+ const declared = new Map((workflow.compensations ?? []).map((entry) => [entry.step, entry.actionId]));
168
+ for (const step of steps) {
169
+ const expected = declared.get(step.stepId);
170
+ if (expected === void 0) {
171
+ if (step.compensateWith) throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" supplies a reversal the manifest does not declare.`);
172
+ continue;
173
+ }
174
+ if (!step.compensateWith) throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" omits the reversal "${expected}" its manifest declares.`);
175
+ if (step.compensateWith.actionId !== expected) {
176
+ throw new Error(`Workflow "${workflow.name}" step "${step.stepId}" reverses with "${step.compensateWith.actionId}" but its manifest declares "${expected}".`);
177
+ }
178
+ }
179
+ for (const [step, actionId] of declared) {
180
+ if (!steps.some((candidate) => candidate.stepId === step)) {
181
+ throw new Error(`Workflow "${workflow.name}" declares a reversal "${actionId}" for step "${step}", which this saga never runs.`);
182
+ }
183
+ }
184
+ }
185
+ async function runCompensatedSaga(activity, input) {
186
+ const scope = {
187
+ tenantId: input.tenantId,
188
+ spaceId: input.spaceId,
189
+ actorId: input.actorId,
190
+ actorType: input.actorType,
191
+ workflowId: input.workflowId,
192
+ runId: input.runId,
193
+ ...input.parentInvocationId ? { parentInvocationId: input.parentInvocationId } : {}
194
+ };
195
+ assertDistinctSteps(input.steps);
196
+ if (input.workflow) assertMatchesDeclaration(input.steps, input.workflow);
197
+ const completed = [];
198
+ const reversals = [];
199
+ const unwind = async (failedStep) => {
200
+ const compensated = [];
201
+ for (const applied of [...reversals].reverse()) {
202
+ const reversal = applied.compensateWith;
203
+ const record = { stepId: applied.stepId, actionId: reversal.actionId };
204
+ try {
205
+ const result = await activity({
206
+ ...scope,
207
+ actionId: reversal.actionId,
208
+ parameters: reversal.parameters ?? applied.parameters,
209
+ stepId: `compensate:${applied.stepId}`
210
+ });
211
+ const refused = stepFailure(result, `reversal "${reversal.actionId}"`);
212
+ if (refused) record.error = refused;
213
+ } catch (compensationError) {
214
+ record.error = compensationError instanceof Error ? compensationError.message : String(compensationError);
215
+ }
216
+ compensated.push(record);
217
+ }
218
+ throw new SagaCompensationError({ completed, compensated, failedStep });
219
+ };
220
+ for (const step of input.steps) {
221
+ let result;
222
+ try {
223
+ result = await activity({ ...scope, actionId: step.actionId, parameters: step.parameters, stepId: step.stepId });
224
+ } catch (error) {
225
+ return await unwind({ stepId: step.stepId, error: error instanceof Error ? error.message : String(error) });
226
+ }
227
+ const refused = stepFailure(result, `step "${step.stepId}"`);
228
+ if (refused) return await unwind({ stepId: step.stepId, error: refused });
229
+ completed.push(step.stepId);
230
+ if (step.compensateWith) reversals.push(step);
231
+ }
232
+ return { completed, compensated: [] };
233
+ }
37
234
  export {
235
+ SAGA_SIGNALS,
236
+ SagaCompensationError,
237
+ SagaWorkflowRegistry,
38
238
  assertWorkerVersionEvidence,
239
+ compensationInvocationIdentity,
39
240
  createGovernedActionActivities,
241
+ createSagaParentActivities,
40
242
  createSagaSignalDispatcher,
243
+ createTemporalSagaDispatcher,
244
+ runCompensatedSaga,
245
+ sagaChildCausation,
246
+ sagaParentActivityIdentity,
41
247
  startGovernedWorkflow,
42
248
  temporalInvocationIdentity,
43
249
  temporalWorkflowStartClient
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\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"],"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;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;","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> { 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/platform-temporal",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Temporal binding contracts for governed Fabric Platform actions and sagas.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -26,16 +26,19 @@
26
26
  ],
27
27
  "peerDependencies": {
28
28
  "@fabricorg/platform": "^1.0.0",
29
- "@temporalio/client": "^1.20.3"
29
+ "@temporalio/client": "^1.23.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@temporalio/client": "^1.20.3",
32
+ "@temporalio/client": "^1.23.0",
33
+ "@temporalio/testing": "^1.23.0",
34
+ "@temporalio/worker": "^1.23.0",
35
+ "@temporalio/workflow": "^1.23.0",
33
36
  "@types/node": "^22.10.0",
34
37
  "tsup": "^8.5.0",
35
38
  "typescript": "^5.7.3",
36
39
  "vitest": "^4.1.5",
37
- "@fabricorg/platform-host": "4.0.0",
38
- "@fabricorg/platform": "1.0.0"
40
+ "@fabricorg/platform": "1.2.0",
41
+ "@fabricorg/platform-host": "6.0.0"
39
42
  },
40
43
  "engines": {
41
44
  "node": ">=20"