@nodii/saga 0.0.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/admin-service.d.ts +55 -0
  2. package/dist/admin-service.d.ts.map +1 -0
  3. package/dist/admin-service.js +161 -0
  4. package/dist/admin-service.js.map +1 -0
  5. package/dist/async-step.d.ts +43 -0
  6. package/dist/async-step.d.ts.map +1 -0
  7. package/dist/async-step.js +166 -0
  8. package/dist/async-step.js.map +1 -0
  9. package/dist/context.d.ts +18 -0
  10. package/dist/context.d.ts.map +1 -0
  11. package/dist/context.js +28 -0
  12. package/dist/context.js.map +1 -0
  13. package/dist/decorator.d.ts +24 -0
  14. package/dist/decorator.d.ts.map +1 -0
  15. package/dist/decorator.js +69 -0
  16. package/dist/decorator.js.map +1 -0
  17. package/dist/idempotency.d.ts +22 -0
  18. package/dist/idempotency.d.ts.map +1 -0
  19. package/dist/idempotency.js +44 -0
  20. package/dist/idempotency.js.map +1 -0
  21. package/dist/index.d.ts +20 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +34 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/init.d.ts +45 -0
  26. package/dist/init.d.ts.map +1 -0
  27. package/dist/init.js +86 -0
  28. package/dist/init.js.map +1 -0
  29. package/dist/interceptor.d.ts +5 -0
  30. package/dist/interceptor.d.ts.map +1 -0
  31. package/dist/interceptor.js +34 -0
  32. package/dist/interceptor.js.map +1 -0
  33. package/dist/migrations/index.d.ts +19 -0
  34. package/dist/migrations/index.d.ts.map +1 -0
  35. package/dist/migrations/index.js +106 -0
  36. package/dist/migrations/index.js.map +1 -0
  37. package/dist/reaper.d.ts +27 -0
  38. package/dist/reaper.d.ts.map +1 -0
  39. package/dist/reaper.js +79 -0
  40. package/dist/reaper.js.map +1 -0
  41. package/dist/registry.d.ts +7 -0
  42. package/dist/registry.d.ts.map +1 -0
  43. package/dist/registry.js +23 -0
  44. package/dist/registry.js.map +1 -0
  45. package/dist/saga.d.ts +48 -0
  46. package/dist/saga.d.ts.map +1 -0
  47. package/dist/saga.js +384 -0
  48. package/dist/saga.js.map +1 -0
  49. package/dist/signal-bus/index.d.ts +2 -0
  50. package/dist/signal-bus/index.d.ts.map +1 -0
  51. package/dist/signal-bus/index.js +6 -0
  52. package/dist/signal-bus/index.js.map +1 -0
  53. package/dist/signal-bus/redis-stream.d.ts +43 -0
  54. package/dist/signal-bus/redis-stream.d.ts.map +1 -0
  55. package/dist/signal-bus/redis-stream.js +189 -0
  56. package/dist/signal-bus/redis-stream.js.map +1 -0
  57. package/dist/signals.d.ts +29 -0
  58. package/dist/signals.d.ts.map +1 -0
  59. package/dist/signals.js +113 -0
  60. package/dist/signals.js.map +1 -0
  61. package/dist/state-store/index.d.ts +2 -0
  62. package/dist/state-store/index.d.ts.map +1 -0
  63. package/dist/state-store/index.js +6 -0
  64. package/dist/state-store/index.js.map +1 -0
  65. package/dist/state-store/postgres.d.ts +47 -0
  66. package/dist/state-store/postgres.d.ts.map +1 -0
  67. package/dist/state-store/postgres.js +270 -0
  68. package/dist/state-store/postgres.js.map +1 -0
  69. package/dist/test-doubles/in-memory-signal-bus.d.ts +38 -0
  70. package/dist/test-doubles/in-memory-signal-bus.d.ts.map +1 -0
  71. package/dist/test-doubles/in-memory-signal-bus.js +68 -0
  72. package/dist/test-doubles/in-memory-signal-bus.js.map +1 -0
  73. package/dist/test-doubles/in-memory-state-store.d.ts +26 -0
  74. package/dist/test-doubles/in-memory-state-store.d.ts.map +1 -0
  75. package/dist/test-doubles/in-memory-state-store.js +87 -0
  76. package/dist/test-doubles/in-memory-state-store.js.map +1 -0
  77. package/dist/test-doubles/index.d.ts +4 -0
  78. package/dist/test-doubles/index.d.ts.map +1 -0
  79. package/dist/test-doubles/index.js +14 -0
  80. package/dist/test-doubles/index.js.map +1 -0
  81. package/dist/types.d.ts +260 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/types.js +103 -0
  84. package/dist/types.js.map +1 -0
  85. package/dist/uuid.d.ts +3 -0
  86. package/dist/uuid.d.ts.map +1 -0
  87. package/dist/uuid.js +62 -0
  88. package/dist/uuid.js.map +1 -0
  89. package/package.json +33 -7
  90. package/src/migrations/001-saga-state.sql +79 -0
@@ -0,0 +1,55 @@
1
+ import { type SagaAdminServiceOpts, type SagaStateRow } from "./types";
2
+ export interface GetSagaRequest {
3
+ sagaId: string;
4
+ }
5
+ export interface ListSagasRequest {
6
+ tenantId?: string;
7
+ status?: string;
8
+ type?: string;
9
+ limit?: number;
10
+ }
11
+ export interface WriteRpcRequest {
12
+ sagaId: string;
13
+ justification: string;
14
+ actorId?: string;
15
+ }
16
+ export interface RetryStepRequest extends WriteRpcRequest {
17
+ stepName: string;
18
+ }
19
+ export interface SkipStepRequest extends WriteRpcRequest {
20
+ stepName: string;
21
+ }
22
+ export interface ForceCompensateRequest extends WriteRpcRequest {
23
+ fromStep?: string;
24
+ }
25
+ export interface MarkManuallyCompensatedRequest extends WriteRpcRequest {
26
+ resolutionRef?: string;
27
+ }
28
+ export interface CancelSagaRequest extends WriteRpcRequest {
29
+ reason: string;
30
+ }
31
+ export interface SagaAdminServer {
32
+ GetSaga(req: GetSagaRequest): Promise<{
33
+ saga: SagaStateRow | null;
34
+ }>;
35
+ ListSagas(req: ListSagasRequest): Promise<{
36
+ sagas: SagaStateRow[];
37
+ }>;
38
+ RetryStep(req: RetryStepRequest): Promise<{
39
+ ok: true;
40
+ }>;
41
+ SkipStep(req: SkipStepRequest): Promise<{
42
+ ok: true;
43
+ }>;
44
+ ForceCompensate(req: ForceCompensateRequest): Promise<{
45
+ ok: true;
46
+ }>;
47
+ MarkManuallyCompensated(req: MarkManuallyCompensatedRequest): Promise<{
48
+ ok: true;
49
+ }>;
50
+ CancelSaga(req: CancelSagaRequest): Promise<{
51
+ ok: true;
52
+ }>;
53
+ }
54
+ export declare function createSagaAdminServer(opts: SagaAdminServiceOpts): SagaAdminServer;
55
+ //# sourceMappingURL=admin-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-service.d.ts","sourceRoot":"","sources":["../src/admin-service.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EAGlB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAuB,SAAQ,eAAe;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,8BAA+B,SAAQ,eAAe;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACxD,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,eAAe,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACpE,uBAAuB,CACrB,GAAG,EAAE,8BAA8B,GAClC,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACzB,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,oBAAoB,GACzB,eAAe,CAsKjB"}
@@ -0,0 +1,161 @@
1
+ // SagaAdminService factory — § 5.10. v0.1.0 ships the JSON-RPC-style
2
+ // surface; the real gRPC proto + codegen + Pane wiring lands in
3
+ // saga-v0.1.x. Stubs for the 4 write RPCs throw NotImplementedError AFTER
4
+ // validating `justification` — that keeps the audit shape correct in the
5
+ // rejection path.
6
+ //
7
+ // Two read RPCs (`GetSaga`, `ListSagas`) are fully wired against the
8
+ // pluggable SagaStateStore.
9
+ import { JustificationRequired, MIN_JUSTIFICATION_CHARS, } from "./types";
10
+ export function createSagaAdminServer(opts) {
11
+ const { stateStore, audit } = opts;
12
+ const validateJustification = (justification) => {
13
+ if (typeof justification !== "string" ||
14
+ justification.length < MIN_JUSTIFICATION_CHARS) {
15
+ throw new JustificationRequired(typeof justification === "string" ? justification.length : 0);
16
+ }
17
+ };
18
+ const emitAdminAudit = async (action, sagaId, payload) => {
19
+ if (!audit)
20
+ return;
21
+ await audit.emit({
22
+ action,
23
+ target_kind: "saga",
24
+ target_id: sagaId,
25
+ payload,
26
+ });
27
+ };
28
+ return {
29
+ async GetSaga(req) {
30
+ await emitAdminAudit("saga_admin_get_saga", req.sagaId);
31
+ const saga = await stateStore.getSaga(req.sagaId);
32
+ return { saga };
33
+ },
34
+ async ListSagas(req) {
35
+ await emitAdminAudit("saga_admin_list_sagas", req.tenantId ?? "*");
36
+ // Per § 5.5 list() is optional on SagaStateStore — stores that omit it
37
+ // return []. Production-bound stores (Drizzle) MUST implement it.
38
+ const rows = stateStore.list
39
+ ? await stateStore.list({
40
+ tenantId: req.tenantId,
41
+ status: req.status,
42
+ type: req.type,
43
+ })
44
+ : [];
45
+ const limit = req.limit ?? 100;
46
+ return { sagas: rows.slice(0, limit) };
47
+ },
48
+ async RetryStep(req) {
49
+ validateJustification(req.justification);
50
+ await emitAdminAudit("saga_admin_retry_step", req.sagaId, {
51
+ step: req.stepName,
52
+ justification: req.justification,
53
+ });
54
+ const now = new Date().toISOString();
55
+ // Reset failure state + transition back to running. The next
56
+ // saga-runner tick on this row picks up at current_step.
57
+ await stateStore.updateSagaStatus(req.sagaId, {
58
+ status: "active",
59
+ current_step: req.stepName,
60
+ failure_reason: null,
61
+ failure_step: null,
62
+ last_admin_action: "retry_step",
63
+ last_admin_actor_id: req.actorId ?? null,
64
+ last_admin_at: now,
65
+ });
66
+ return { ok: true };
67
+ },
68
+ async SkipStep(req) {
69
+ validateJustification(req.justification);
70
+ await emitAdminAudit("saga_admin_skip_step", req.sagaId, {
71
+ step: req.stepName,
72
+ justification: req.justification,
73
+ });
74
+ const now = new Date().toISOString();
75
+ // Record the skip + transition back to running. The skipped step
76
+ // is marked in step_outputs so the saga handler can branch on it.
77
+ await stateStore.appendStepOutput(req.sagaId, req.stepName, {
78
+ admin_skipped: true,
79
+ skipped_at: now,
80
+ actor_id: req.actorId ?? null,
81
+ justification: req.justification,
82
+ });
83
+ await stateStore.updateSagaStatus(req.sagaId, {
84
+ status: "active",
85
+ current_step: req.stepName,
86
+ failure_reason: null,
87
+ failure_step: null,
88
+ last_admin_action: "skip_step",
89
+ last_admin_actor_id: req.actorId ?? null,
90
+ last_admin_at: now,
91
+ });
92
+ return { ok: true };
93
+ },
94
+ async ForceCompensate(req) {
95
+ validateJustification(req.justification);
96
+ await emitAdminAudit("saga_admin_force_compensate", req.sagaId, {
97
+ from_step: req.fromStep,
98
+ justification: req.justification,
99
+ });
100
+ const now = new Date().toISOString();
101
+ // Transition to compensating. The reaper picks up paused/running →
102
+ // compensating rows and runs undo() handlers in reverse step order
103
+ // from current_step (defaulting to fromStep when provided).
104
+ await stateStore.updateSagaStatus(req.sagaId, {
105
+ status: "compensating",
106
+ current_step: req.fromStep ?? null,
107
+ last_admin_action: "force_compensate",
108
+ last_admin_actor_id: req.actorId ?? null,
109
+ last_admin_at: now,
110
+ });
111
+ return { ok: true };
112
+ },
113
+ async MarkManuallyCompensated(req) {
114
+ validateJustification(req.justification);
115
+ await emitAdminAudit("saga_admin_mark_manually_compensated", req.sagaId, {
116
+ resolution_ref: req.resolutionRef,
117
+ justification: req.justification,
118
+ });
119
+ const now = new Date().toISOString();
120
+ // Terminal: operator confirms out-of-band remediation. Records
121
+ // resolution_ref in the compensation_log for audit traceability.
122
+ await stateStore.appendCompensationLog(req.sagaId, {
123
+ step_name: "<manual>",
124
+ // "undone" is the closest outcome semantic: out-of-band remediation
125
+ // by an operator counts as the compensation having succeeded; the
126
+ // resolution_ref + admin action audit trail provide the manual-
127
+ // override traceability.
128
+ outcome: "undone",
129
+ ts: now,
130
+ error: req.resolutionRef
131
+ ? `manual_resolution_ref=${req.resolutionRef}`
132
+ : "manual_resolution",
133
+ });
134
+ await stateStore.updateSagaStatus(req.sagaId, {
135
+ status: "compensated_manual",
136
+ completed_at: now,
137
+ last_admin_action: "mark_manually_compensated",
138
+ last_admin_actor_id: req.actorId ?? null,
139
+ last_admin_at: now,
140
+ });
141
+ return { ok: true };
142
+ },
143
+ async CancelSaga(req) {
144
+ validateJustification(req.justification);
145
+ await emitAdminAudit("saga_admin_cancel", req.sagaId, {
146
+ reason: req.reason,
147
+ justification: req.justification,
148
+ });
149
+ await stateStore.updateSagaStatus(req.sagaId, {
150
+ status: "cancelled",
151
+ cancel_reason: req.reason,
152
+ completed_at: new Date().toISOString(),
153
+ last_admin_action: "cancel",
154
+ last_admin_actor_id: req.actorId ?? null,
155
+ last_admin_at: new Date().toISOString(),
156
+ });
157
+ return { ok: true };
158
+ },
159
+ };
160
+ }
161
+ //# sourceMappingURL=admin-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-service.js","sourceRoot":"","sources":["../src/admin-service.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,gEAAgE;AAChE,0EAA0E;AAC1E,yEAAyE;AACzE,kBAAkB;AAClB,EAAE;AACF,qEAAqE;AACrE,4BAA4B;AAE5B,OAAO,EAGL,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AAmDjB,MAAM,UAAU,qBAAqB,CACnC,IAA0B;IAE1B,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAEnC,MAAM,qBAAqB,GAAG,CAAC,aAAqB,EAAQ,EAAE;QAC5D,IACE,OAAO,aAAa,KAAK,QAAQ;YACjC,aAAa,CAAC,MAAM,GAAG,uBAAuB,EAC9C,CAAC;YACD,MAAM,IAAI,qBAAqB,CAC7B,OAAO,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAC7D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,KAAK,EAC1B,MAAc,EACd,MAAc,EACd,OAAiC,EAClB,EAAE;QACjB,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,KAAK,CAAC,IAAI,CAAC;YACf,MAAM;YACN,WAAW,EAAE,MAAM;YACnB,SAAS,EAAE,MAAM;YACjB,OAAO;SACR,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,GAAG;YACf,MAAM,cAAc,CAAC,qBAAqB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;YACxD,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,EAAE,IAAI,EAAE,CAAC;QAClB,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,MAAM,cAAc,CAAC,uBAAuB,EAAE,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC;YACnE,uEAAuE;YACvE,kEAAkE;YAClE,MAAM,IAAI,GAAmB,UAAU,CAAC,IAAI;gBAC1C,CAAC,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC;oBACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;oBACtB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;iBACf,CAAC;gBACJ,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC;YAC/B,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QACzC,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACzC,MAAM,cAAc,CAAC,uBAAuB,EAAE,GAAG,CAAC,MAAM,EAAE;gBACxD,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,6DAA6D;YAC7D,yDAAyD;YACzD,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE;gBAC5C,MAAM,EAAE,QAAQ;gBAChB,YAAY,EAAE,GAAG,CAAC,QAAQ;gBAC1B,cAAc,EAAE,IAAI;gBACpB,YAAY,EAAE,IAAI;gBAClB,iBAAiB,EAAE,YAAY;gBAC/B,mBAAmB,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBACxC,aAAa,EAAE,GAAG;aACnB,CAAC,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,GAAG;YAChB,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACzC,MAAM,cAAc,CAAC,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE;gBACvD,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,iEAAiE;YACjE,kEAAkE;YAClE,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,EAAE;gBAC1D,aAAa,EAAE,IAAI;gBACnB,UAAU,EAAE,GAAG;gBACf,QAAQ,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBAC7B,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE;gBAC5C,MAAM,EAAE,QAAQ;gBAChB,YAAY,EAAE,GAAG,CAAC,QAAQ;gBAC1B,cAAc,EAAE,IAAI;gBACpB,YAAY,EAAE,IAAI;gBAClB,iBAAiB,EAAE,WAAW;gBAC9B,mBAAmB,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBACxC,aAAa,EAAE,GAAG;aACnB,CAAC,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,eAAe,CAAC,GAAG;YACvB,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACzC,MAAM,cAAc,CAAC,6BAA6B,EAAE,GAAG,CAAC,MAAM,EAAE;gBAC9D,SAAS,EAAE,GAAG,CAAC,QAAQ;gBACvB,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,mEAAmE;YACnE,mEAAmE;YACnE,4DAA4D;YAC5D,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE;gBAC5C,MAAM,EAAE,cAAc;gBACtB,YAAY,EAAE,GAAG,CAAC,QAAQ,IAAI,IAAI;gBAClC,iBAAiB,EAAE,kBAAkB;gBACrC,mBAAmB,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBACxC,aAAa,EAAE,GAAG;aACnB,CAAC,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,uBAAuB,CAAC,GAAG;YAC/B,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACzC,MAAM,cAAc,CAAC,sCAAsC,EAAE,GAAG,CAAC,MAAM,EAAE;gBACvE,cAAc,EAAE,GAAG,CAAC,aAAa;gBACjC,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,+DAA+D;YAC/D,iEAAiE;YACjE,MAAM,UAAU,CAAC,qBAAqB,CAAC,GAAG,CAAC,MAAM,EAAE;gBACjD,SAAS,EAAE,UAAU;gBACrB,oEAAoE;gBACpE,kEAAkE;gBAClE,gEAAgE;gBAChE,yBAAyB;gBACzB,OAAO,EAAE,QAAQ;gBACjB,EAAE,EAAE,GAAG;gBACP,KAAK,EAAE,GAAG,CAAC,aAAa;oBACtB,CAAC,CAAC,yBAAyB,GAAG,CAAC,aAAa,EAAE;oBAC9C,CAAC,CAAC,mBAAmB;aACxB,CAAC,CAAC;YACH,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE;gBAC5C,MAAM,EAAE,oBAAoB;gBAC5B,YAAY,EAAE,GAAG;gBACjB,iBAAiB,EAAE,2BAA2B;gBAC9C,mBAAmB,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBACxC,aAAa,EAAE,GAAG;aACnB,CAAC,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,GAAG;YAClB,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACzC,MAAM,cAAc,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE;gBACpD,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;YACH,MAAM,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE;gBAC5C,MAAM,EAAE,WAAW;gBACnB,aAAa,EAAE,GAAG,CAAC,MAAM;gBACzB,YAAY,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACtC,iBAAiB,EAAE,QAAQ;gBAC3B,mBAAmB,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;gBACxC,aAAa,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACxC,CAAC,CAAC;YACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { SagaError, type StepOpts } from "./types";
2
+ /** Internal sentinel — thrown to unwind a saga at the async-step boundary
3
+ * without triggering compensation. `createSaga` catches and treats as
4
+ * "saga paused, return current handle". */
5
+ export declare class SagaPausedSentinel extends SagaError {
6
+ readonly sagaId: string;
7
+ readonly stepName: string;
8
+ constructor(sagaId: string, stepName: string);
9
+ }
10
+ /**
11
+ * Begin a Pattern 2 step. Called from `step()` when `opts.async === true`.
12
+ *
13
+ * Side effects:
14
+ * - saga_state.status → 'paused', current_step = name, next_resume_at = now + grace.
15
+ * - saga_outbox row written: { event_type: 'saga.step.<name>.begin', payload }
16
+ * - SagaPausedSentinel thrown — control exits the saga's `fn`.
17
+ */
18
+ export declare function beginAsyncStep<O>(name: string, opts: StepOpts<O>): Promise<O>;
19
+ /**
20
+ * Mark an async (Pattern 2) step as completed. Consumer workers call this
21
+ * when the participant-side completion event arrives.
22
+ *
23
+ * - Persists the step output to saga_state.step_outputs.
24
+ * - Transitions saga_state.status='active' + clears next_resume_at.
25
+ * - Emits `saga_resumed` audit event.
26
+ *
27
+ * The caller is responsible for invoking `withSaga(sagaId, ...)` + the
28
+ * registered saga function afterwards (the lifecycle pattern from § 5.8).
29
+ */
30
+ export declare function completeAsyncStep(args: {
31
+ sagaId: string;
32
+ stepName: string;
33
+ output: unknown;
34
+ }): Promise<void>;
35
+ /**
36
+ * Read the persisted output of a previously-completed async step. Used by
37
+ * a re-entered saga function to retrieve the result inside its step body.
38
+ */
39
+ export declare function getCompletedAsyncOutput(args: {
40
+ sagaId: string;
41
+ stepName: string;
42
+ }): Promise<unknown | null>;
43
+ //# sourceMappingURL=async-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-step.d.ts","sourceRoot":"","sources":["../src/async-step.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAKnD;;4CAE4C;AAC5C,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAQ7C;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAChB,OAAO,CAAC,CAAC,CAAC,CAuEZ;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BhB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAK1B"}
@@ -0,0 +1,166 @@
1
+ // Pattern 2 (async-via-outbox) step impl.
2
+ //
3
+ // Spec § 5.6 + comm doctrine § 4.5. When a step is invoked with
4
+ // `{ async: true }`, the library:
5
+ //
6
+ // 1. Pauses the saga (status='paused', next_resume_at=now+graceMs).
7
+ // 2. Writes a `saga.step.<step_name>.begin` event to saga_outbox in
8
+ // the same DB tx as the saga_state advance.
9
+ // 3. Throws `SagaPausedSentinel` which `createSaga` recognizes as a
10
+ // pause boundary — NOT a step failure. The saga exits cleanly with
11
+ // its handle reported as paused. The outer caller observes
12
+ // paused-state via `getSaga(id).status === 'paused'`.
13
+ //
14
+ // The completion path is driven by `completeAsyncStep(sagaId, stepName,
15
+ // result)`. This is the function a consumer worker (per-service) calls
16
+ // when the participant-side work finishes. It:
17
+ //
18
+ // 1. Persists the step output.
19
+ // 2. Transitions saga_state.status='active'.
20
+ // 3. Emits a `saga.step.<step_name>.completed` audit event.
21
+ // 4. The next `withSaga(sagaId, ...)` invocation can then re-enter the
22
+ // saga and call the same step name; the step short-circuits on
23
+ // idempotency-cached output OR returns the persisted output via
24
+ // `getCompletedAsyncOutput`.
25
+ //
26
+ // For v0.1.0 we keep the implementation deterministic + observable
27
+ // rather than fully transactional — Pattern 2's "same DB tx" mandate is
28
+ // the orchestrator-of-truth's concern; the library writes the outbox row
29
+ // in the same call but does not span participant boundaries.
30
+ import { requireSagaContext } from "./context";
31
+ import { requireSaga } from "./init";
32
+ import { SagaError } from "./types";
33
+ const ASYNC_OUTBOX_EVENT_PREFIX = "saga.step.";
34
+ const PAUSE_DEFAULT_GRACE_MS = 24 * 60 * 60 * 1000; // 24h, per § 5.8
35
+ /** Internal sentinel — thrown to unwind a saga at the async-step boundary
36
+ * without triggering compensation. `createSaga` catches and treats as
37
+ * "saga paused, return current handle". */
38
+ export class SagaPausedSentinel extends SagaError {
39
+ sagaId;
40
+ stepName;
41
+ constructor(sagaId, stepName) {
42
+ super(`saga_paused: ${sagaId} paused at step '${stepName}' awaiting Pattern 2 completion`);
43
+ this.name = "SagaPausedSentinel";
44
+ this.sagaId = sagaId;
45
+ this.stepName = stepName;
46
+ }
47
+ }
48
+ /**
49
+ * Begin a Pattern 2 step. Called from `step()` when `opts.async === true`.
50
+ *
51
+ * Side effects:
52
+ * - saga_state.status → 'paused', current_step = name, next_resume_at = now + grace.
53
+ * - saga_outbox row written: { event_type: 'saga.step.<name>.begin', payload }
54
+ * - SagaPausedSentinel thrown — control exits the saga's `fn`.
55
+ */
56
+ export async function beginAsyncStep(name, opts) {
57
+ const cfg = requireSaga();
58
+ const sagaCtx = requireSagaContext("step(async)");
59
+ const row = sagaCtx.row;
60
+ // Compute the pause window. `opts` doesn't carry it for v0.1.0; we
61
+ // reuse the global default.
62
+ const nextResumeAt = new Date(Date.now() + PAUSE_DEFAULT_GRACE_MS).toISOString();
63
+ await cfg.stateStore.updateSagaStatus(row.id, {
64
+ status: "paused",
65
+ current_step: name,
66
+ next_resume_at: nextResumeAt,
67
+ });
68
+ row.status = "paused";
69
+ row.current_step = name;
70
+ row.next_resume_at = nextResumeAt;
71
+ // Idempotency-key payload mirrors step()'s deterministic serialization
72
+ // so a downstream completion event can correlate.
73
+ let serializedInput;
74
+ try {
75
+ const payload = {
76
+ name,
77
+ persist_fields: [...(opts.persistFields ?? [])].sort(),
78
+ };
79
+ if (opts.input !== undefined)
80
+ payload.input = opts.input;
81
+ serializedInput = JSON.stringify(payload, [
82
+ "input",
83
+ "name",
84
+ "persist_fields",
85
+ ]);
86
+ }
87
+ catch {
88
+ serializedInput = `${name}::nonserializable::${row.id}`;
89
+ }
90
+ // Write outbox row — same call as the state advance. Stores that don't
91
+ // implement outbox (in-memory) get a soft warning + no-op.
92
+ if (cfg.stateStore.writeOutboxBegin) {
93
+ await cfg.stateStore.writeOutboxBegin({
94
+ sagaId: row.id,
95
+ stepName: name,
96
+ eventType: `${ASYNC_OUTBOX_EVENT_PREFIX}${name}.begin`,
97
+ payload: {
98
+ saga_id: row.id,
99
+ step_name: name,
100
+ idempotency_serialized_input: serializedInput,
101
+ tenant_id: row.tenant_id,
102
+ },
103
+ tenantId: row.tenant_id,
104
+ });
105
+ }
106
+ else {
107
+ cfg.logger.warn("async-step: state store does not implement writeOutboxBegin; outbox event not persisted", { saga_id: row.id, step: name });
108
+ }
109
+ if (cfg.telemetryAudit) {
110
+ await cfg.telemetryAudit.emit({
111
+ action: "saga_paused",
112
+ target_kind: "saga",
113
+ target_id: row.id,
114
+ payload: { step: name, pattern: "2" },
115
+ });
116
+ }
117
+ throw new SagaPausedSentinel(row.id, name);
118
+ }
119
+ /**
120
+ * Mark an async (Pattern 2) step as completed. Consumer workers call this
121
+ * when the participant-side completion event arrives.
122
+ *
123
+ * - Persists the step output to saga_state.step_outputs.
124
+ * - Transitions saga_state.status='active' + clears next_resume_at.
125
+ * - Emits `saga_resumed` audit event.
126
+ *
127
+ * The caller is responsible for invoking `withSaga(sagaId, ...)` + the
128
+ * registered saga function afterwards (the lifecycle pattern from § 5.8).
129
+ */
130
+ export async function completeAsyncStep(args) {
131
+ const cfg = requireSaga();
132
+ const row = await cfg.stateStore.getSaga(args.sagaId);
133
+ if (!row) {
134
+ throw new Error(`completeAsyncStep: saga ${args.sagaId} not found`);
135
+ }
136
+ if (row.status !== "paused") {
137
+ // Per comm doctrine § 4.5 step 11: drop on duplicate / late arrivals.
138
+ cfg.logger.warn("completeAsyncStep: saga is not paused; treating completion event as duplicate", { saga_id: args.sagaId, current_status: row.status, step: args.stepName });
139
+ return;
140
+ }
141
+ await cfg.stateStore.appendStepOutput(args.sagaId, args.stepName, args.output);
142
+ await cfg.stateStore.updateSagaStatus(args.sagaId, {
143
+ status: "active",
144
+ next_resume_at: null,
145
+ });
146
+ if (cfg.telemetryAudit) {
147
+ await cfg.telemetryAudit.emit({
148
+ action: "saga_resumed",
149
+ target_kind: "saga",
150
+ target_id: args.sagaId,
151
+ payload: { step: args.stepName, pattern: "2" },
152
+ });
153
+ }
154
+ }
155
+ /**
156
+ * Read the persisted output of a previously-completed async step. Used by
157
+ * a re-entered saga function to retrieve the result inside its step body.
158
+ */
159
+ export async function getCompletedAsyncOutput(args) {
160
+ const cfg = requireSaga();
161
+ const row = await cfg.stateStore.getSaga(args.sagaId);
162
+ if (!row)
163
+ return null;
164
+ return row.step_outputs?.[args.stepName] ?? null;
165
+ }
166
+ //# sourceMappingURL=async-step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-step.js","sourceRoot":"","sources":["../src/async-step.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type ResumeContext, type SagaHandle, type SagaStateRow } from "./types";
2
+ interface SagaContextState {
3
+ handle: SagaHandle;
4
+ row: SagaStateRow;
5
+ resume?: ResumeContext;
6
+ /** Steps that completed in this episode (push-on-complete). */
7
+ completedSteps: {
8
+ name: string;
9
+ output: unknown;
10
+ }[];
11
+ }
12
+ export declare function runInSagaContext<T>(state: SagaContextState, fn: () => Promise<T>): Promise<T>;
13
+ export declare function getSagaContext(): SagaContextState | undefined;
14
+ export declare function requireSagaContext(api: string): SagaContextState;
15
+ export declare function runWithResumeContext<T>(ctx: ResumeContext, fn: () => Promise<T>): Promise<T>;
16
+ export declare function getResumeContext(): ResumeContext | undefined;
17
+ export {};
18
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,YAAY,EAElB,MAAM,SAAS,CAAC;AAEjB,UAAU,gBAAgB;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,+DAA+D;IAC/D,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACrD;AAID,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,gBAAgB,EACvB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,wBAAgB,cAAc,IAAI,gBAAgB,GAAG,SAAS,CAE7D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAMhE;AAMD,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,GAAG,EAAE,aAAa,EAClB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAE5D"}
@@ -0,0 +1,28 @@
1
+ // ALS-based saga context — primes (sagaId, resumeContext, siblings) for
2
+ // `step`, `withSaga`, `emitSagaSignal`, etc.
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { NoActiveSaga, } from "./types";
5
+ const sagaStorage = new AsyncLocalStorage();
6
+ export function runInSagaContext(state, fn) {
7
+ return sagaStorage.run(state, fn);
8
+ }
9
+ export function getSagaContext() {
10
+ return sagaStorage.getStore();
11
+ }
12
+ export function requireSagaContext(api) {
13
+ const ctx = sagaStorage.getStore();
14
+ if (!ctx) {
15
+ throw new NoActiveSaga(api);
16
+ }
17
+ return ctx;
18
+ }
19
+ // Resume context priming — `withSaga()` sets this so the next createSaga
20
+ // detects "we're inside a resume episode" and reloads from the state store.
21
+ const resumeStorage = new AsyncLocalStorage();
22
+ export function runWithResumeContext(ctx, fn) {
23
+ return resumeStorage.run(ctx, fn);
24
+ }
25
+ export function getResumeContext() {
26
+ return resumeStorage.getStore();
27
+ }
28
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,6CAA6C;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAIL,YAAY,GACb,MAAM,SAAS,CAAC;AAUjB,MAAM,WAAW,GAAG,IAAI,iBAAiB,EAAoB,CAAC;AAE9D,MAAM,UAAU,gBAAgB,CAC9B,KAAuB,EACvB,EAAoB;IAEpB,OAAO,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,WAAW,CAAC,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yEAAyE;AACzE,4EAA4E;AAC5E,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAiB,CAAC;AAE7D,MAAM,UAAU,oBAAoB,CAClC,GAAkB,EAClB,EAAoB;IAEpB,OAAO,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,aAAa,CAAC,QAAQ,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { SagaCallableConfig } from "./types";
2
+ interface RegistryEntry {
3
+ className: string;
4
+ methodName: string;
5
+ config: SagaCallableConfig;
6
+ }
7
+ /** Symbol-keyed slot so consumers can read the tag off a method. */
8
+ export declare const SAGA_CALLABLE_METADATA: unique symbol;
9
+ export type SagaCallableMethod = {
10
+ [SAGA_CALLABLE_METADATA]: SagaCallableConfig;
11
+ };
12
+ /**
13
+ * Decorator factory — usable as `@sagaCallable({...})` (TC39 stage-3
14
+ * decorators) and also callable explicitly: `sagaCallable({...})(method)`.
15
+ *
16
+ * Legacy `experimentalDecorators` shape (3-arg) is supported so this works
17
+ * under bun's default tsconfig.
18
+ */
19
+ export declare function sagaCallable(config: SagaCallableConfig): (target: unknown, propertyKeyOrContext?: unknown, descriptor?: PropertyDescriptor) => unknown;
20
+ export declare function readSagaCallableConfig(fn: unknown): SagaCallableConfig | undefined;
21
+ export declare function readSagaCallableRegistry(): readonly RegistryEntry[];
22
+ export declare function _resetDecoratorRegistryForTests(): void;
23
+ export {};
24
+ //# sourceMappingURL=decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,UAAU,aAAa;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAID,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,EAAE,OAAO,MAE3C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,sBAAsB,CAAC,EAAE,kBAAkB,CAAC;CAC9C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,IAEnD,QAAQ,OAAO,EACf,uBAAuB,OAAO,EAC9B,aAAa,kBAAkB,KAC9B,OAAO,CAgDX;AAWD,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,OAAO,GACV,kBAAkB,GAAG,SAAS,CAKhC;AAED,wBAAgB,wBAAwB,IAAI,SAAS,aAAa,EAAE,CAEnE;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
@@ -0,0 +1,69 @@
1
+ // @sagaCallable — D165 six-tag schema with runtime metadata + class registry.
2
+ //
3
+ // Spec § 5.7. v0.1.0 attaches the tag to the method's prototype-bound function
4
+ // and records (className, methodName, config) into a process-local registry
5
+ // the lint plugin + codegen will read at build time (`// TODO saga-v0.1.x:`
6
+ // for the build-time emitters).
7
+ const registry = [];
8
+ /** Symbol-keyed slot so consumers can read the tag off a method. */
9
+ export const SAGA_CALLABLE_METADATA = Symbol.for("@nodii/saga.sagaCallableMetadata");
10
+ /**
11
+ * Decorator factory — usable as `@sagaCallable({...})` (TC39 stage-3
12
+ * decorators) and also callable explicitly: `sagaCallable({...})(method)`.
13
+ *
14
+ * Legacy `experimentalDecorators` shape (3-arg) is supported so this works
15
+ * under bun's default tsconfig.
16
+ */
17
+ export function sagaCallable(config) {
18
+ return function decorate(target, propertyKeyOrContext, descriptor) {
19
+ // Stage-3 decorator: (value, context) returning a replacement function.
20
+ if (typeof target === "function" &&
21
+ propertyKeyOrContext &&
22
+ typeof propertyKeyOrContext === "object" &&
23
+ "kind" in propertyKeyOrContext) {
24
+ const ctx = propertyKeyOrContext;
25
+ const fn = target;
26
+ attachMetadata(fn, config);
27
+ ctx.addInitializer?.(function () {
28
+ const className = this.constructor?.name ??
29
+ "<anonymous>";
30
+ registry.push({ className, methodName: ctx.name, config });
31
+ });
32
+ return fn;
33
+ }
34
+ // Legacy decorator: (target, propertyKey, descriptor).
35
+ if (typeof propertyKeyOrContext === "string" ||
36
+ typeof propertyKeyOrContext === "symbol") {
37
+ const className = target?.constructor?.name ??
38
+ target.name ??
39
+ "<anonymous>";
40
+ const methodName = String(propertyKeyOrContext);
41
+ if (descriptor && typeof descriptor.value === "function") {
42
+ attachMetadata(descriptor.value, config);
43
+ }
44
+ registry.push({ className, methodName, config });
45
+ return descriptor;
46
+ }
47
+ // Explicit call: sagaCallable({...})(fn).
48
+ if (typeof target === "function") {
49
+ attachMetadata(target, config);
50
+ return target;
51
+ }
52
+ return target;
53
+ };
54
+ }
55
+ function attachMetadata(fn, config) {
56
+ fn[SAGA_CALLABLE_METADATA] = config;
57
+ }
58
+ export function readSagaCallableConfig(fn) {
59
+ if (typeof fn !== "function")
60
+ return undefined;
61
+ return fn[SAGA_CALLABLE_METADATA];
62
+ }
63
+ export function readSagaCallableRegistry() {
64
+ return [...registry];
65
+ }
66
+ export function _resetDecoratorRegistryForTests() {
67
+ registry.length = 0;
68
+ }
69
+ //# sourceMappingURL=decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator.js","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,4EAA4E;AAC5E,gCAAgC;AAUhC,MAAM,QAAQ,GAAoB,EAAE,CAAC;AAErC,oEAAoE;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAkB,MAAM,CAAC,GAAG,CAC7D,kCAAkC,CACnC,CAAC;AAMF;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,OAAO,SAAS,QAAQ,CACtB,MAAe,EACf,oBAA8B,EAC9B,UAA+B;QAE/B,wEAAwE;QACxE,IACE,OAAO,MAAM,KAAK,UAAU;YAC5B,oBAAoB;YACpB,OAAO,oBAAoB,KAAK,QAAQ;YACxC,MAAM,IAAK,oBAA+B,EAC1C,CAAC;YACD,MAAM,GAAG,GAAG,oBAIX,CAAC;YACF,MAAM,EAAE,GAAG,MAAyC,CAAC;YACrD,cAAc,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;YAC3B,GAAG,CAAC,cAAc,EAAE,CAAC;gBACnB,MAAM,SAAS,GACZ,IAA4C,CAAC,WAAW,EAAE,IAAI;oBAC/D,aAAa,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC7D,CAAC,CAAC,CAAC;YACH,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,uDAAuD;QACvD,IACE,OAAO,oBAAoB,KAAK,QAAQ;YACxC,OAAO,oBAAoB,KAAK,QAAQ,EACxC,CAAC;YACD,MAAM,SAAS,GACZ,MAA8C,EAAE,WAAW,EAAE,IAAI;gBACjE,MAA4B,CAAC,IAAI;gBAClC,aAAa,CAAC;YAChB,MAAM,UAAU,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAChD,IAAI,UAAU,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;gBACzD,cAAc,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC3C,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,0CAA0C;QAC1C,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,cAAc,CAAC,MAAyC,EAAE,MAAM,CAAC,CAAC;YAClE,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CACrB,EAAmC,EACnC,MAA0B;IAEzB,EAAkE,CACjE,sBAAsB,CACvB,GAAG,MAAM,CAAC;AACb,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,EAAW;IAEX,IAAI,OAAO,EAAE,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAQ,EAAwD,CAC9D,sBAAsB,CACvB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB;IACtC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,+BAA+B;IAC7C,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { IdempotencyShim } from "./types";
2
+ export declare class NoopIdempotency implements IdempotencyShim {
3
+ wrapForSagaStep<O>(args: {
4
+ sagaId: string;
5
+ stepName: string;
6
+ serializedInput: string;
7
+ handler: () => Promise<O>;
8
+ }): Promise<O>;
9
+ }
10
+ export declare class InMemoryIdempotency implements IdempotencyShim {
11
+ private cache;
12
+ wrapForSagaStep<O>(args: {
13
+ sagaId: string;
14
+ stepName: string;
15
+ serializedInput: string;
16
+ handler: () => Promise<O>;
17
+ }): Promise<O>;
18
+ /** Test-only — used by the parity-fence fixtures to assert byte equality. */
19
+ static debugComputeKey(sagaId: string, stepName: string, serializedInput: string): string;
20
+ reset(): void;
21
+ }
22
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,qBAAa,eAAgB,YAAW,eAAe;IAC/C,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE;QAC7B,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,eAAe,EAAE,MAAM,CAAC;QACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;KAC3B,GAAG,OAAO,CAAC,CAAC,CAAC;CAGf;AAED,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,KAAK,CAA8B;IAErC,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE;QAC7B,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,eAAe,EAAE,MAAM,CAAC;QACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;KAC3B,GAAG,OAAO,CAAC,CAAC,CAAC;IAUd,6EAA6E;IAC7E,MAAM,CAAC,eAAe,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,MAAM;IAIT,KAAK,IAAI,IAAI;CAGd"}