@nodii/saga 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-service.d.ts +55 -0
- package/dist/admin-service.d.ts.map +1 -0
- package/dist/admin-service.js +161 -0
- package/dist/admin-service.js.map +1 -0
- package/dist/async-step.d.ts +43 -0
- package/dist/async-step.d.ts.map +1 -0
- package/dist/async-step.js +166 -0
- package/dist/async-step.js.map +1 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +28 -0
- package/dist/context.js.map +1 -0
- package/dist/decorator.d.ts +24 -0
- package/dist/decorator.d.ts.map +1 -0
- package/dist/decorator.js +69 -0
- package/dist/decorator.js.map +1 -0
- package/dist/idempotency.d.ts +22 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +44 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +45 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +86 -0
- package/dist/init.js.map +1 -0
- package/dist/interceptor.d.ts +5 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +34 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/migrations/index.d.ts +19 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +106 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/reaper.d.ts +27 -0
- package/dist/reaper.d.ts.map +1 -0
- package/dist/reaper.js +79 -0
- package/dist/reaper.js.map +1 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +23 -0
- package/dist/registry.js.map +1 -0
- package/dist/saga.d.ts +48 -0
- package/dist/saga.d.ts.map +1 -0
- package/dist/saga.js +384 -0
- package/dist/saga.js.map +1 -0
- package/dist/signal-bus/index.d.ts +2 -0
- package/dist/signal-bus/index.d.ts.map +1 -0
- package/dist/signal-bus/index.js +6 -0
- package/dist/signal-bus/index.js.map +1 -0
- package/dist/signal-bus/redis-stream.d.ts +43 -0
- package/dist/signal-bus/redis-stream.d.ts.map +1 -0
- package/dist/signal-bus/redis-stream.js +189 -0
- package/dist/signal-bus/redis-stream.js.map +1 -0
- package/dist/signals.d.ts +29 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +113 -0
- package/dist/signals.js.map +1 -0
- package/dist/state-store/index.d.ts +2 -0
- package/dist/state-store/index.d.ts.map +1 -0
- package/dist/state-store/index.js +6 -0
- package/dist/state-store/index.js.map +1 -0
- package/dist/state-store/postgres.d.ts +47 -0
- package/dist/state-store/postgres.d.ts.map +1 -0
- package/dist/state-store/postgres.js +270 -0
- package/dist/state-store/postgres.js.map +1 -0
- package/dist/test-doubles/in-memory-signal-bus.d.ts +38 -0
- package/dist/test-doubles/in-memory-signal-bus.d.ts.map +1 -0
- package/dist/test-doubles/in-memory-signal-bus.js +68 -0
- package/dist/test-doubles/in-memory-signal-bus.js.map +1 -0
- package/dist/test-doubles/in-memory-state-store.d.ts +26 -0
- package/dist/test-doubles/in-memory-state-store.d.ts.map +1 -0
- package/dist/test-doubles/in-memory-state-store.js +87 -0
- package/dist/test-doubles/in-memory-state-store.js.map +1 -0
- package/dist/test-doubles/index.d.ts +4 -0
- package/dist/test-doubles/index.d.ts.map +1 -0
- package/dist/test-doubles/index.js +14 -0
- package/dist/test-doubles/index.js.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +103 -0
- package/dist/types.js.map +1 -0
- package/dist/uuid.d.ts +3 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +62 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +33 -7
- package/src/migrations/001-saga-state.sql +79 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/** D33-locked status enum (§ 5.5 + comm doctrine § 3.3). */
|
|
2
|
+
export type SagaStatus = "active" | "paused" | "compensating" | "compensated" | "compensation_failed" | "compensated_with_errors" | "compensated_manual" | "technical_failure" | "completed" | "cancelled";
|
|
3
|
+
/** § 5.5 column shape — full 21-col schema including reaper hooks. */
|
|
4
|
+
export interface SagaStateRow {
|
|
5
|
+
id: string;
|
|
6
|
+
tenant_id: string | null;
|
|
7
|
+
type: string;
|
|
8
|
+
status: SagaStatus;
|
|
9
|
+
current_step: string | null;
|
|
10
|
+
step_outputs: Record<string, unknown>;
|
|
11
|
+
episode: number;
|
|
12
|
+
trigger_trace_id: string;
|
|
13
|
+
trigger_span_id: string;
|
|
14
|
+
trigger_request_id: string;
|
|
15
|
+
parent_saga_id: string | null;
|
|
16
|
+
parent_relationship: "child" | "compensation_of";
|
|
17
|
+
children: string[];
|
|
18
|
+
started_at: string;
|
|
19
|
+
completed_at: string | null;
|
|
20
|
+
/** Reaper hook — § 5.8. When status='paused', the reaper compares
|
|
21
|
+
* `next_resume_at + grace_period` to now() and finalizes if elapsed. */
|
|
22
|
+
next_resume_at: string | null;
|
|
23
|
+
failure_reason: string | null;
|
|
24
|
+
failure_step: string | null;
|
|
25
|
+
compensation_log: CompensationLogEntry[];
|
|
26
|
+
last_admin_action: string | null;
|
|
27
|
+
last_admin_actor_id: string | null;
|
|
28
|
+
last_admin_at: string | null;
|
|
29
|
+
input: unknown;
|
|
30
|
+
cancel_reason: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface CompensationLogEntry {
|
|
33
|
+
step_name: string;
|
|
34
|
+
outcome: "undone" | "undo_failed" | "skipped_no_undo";
|
|
35
|
+
error?: string;
|
|
36
|
+
ts: string;
|
|
37
|
+
}
|
|
38
|
+
/** § 5.4 — saga owns inputs only; @nodii/idempotency computes the key. */
|
|
39
|
+
export interface IdempotencyShim {
|
|
40
|
+
/**
|
|
41
|
+
* Spec § 5.4 — saga supplies (saga_id, step_name, serialized_input);
|
|
42
|
+
* idempotency lib (or shim) computes the hash + runs the contract +
|
|
43
|
+
* caches the result. v0.1.0 ships `NoopIdempotency` that simply runs
|
|
44
|
+
* the handler.
|
|
45
|
+
*/
|
|
46
|
+
wrapForSagaStep<O>(args: {
|
|
47
|
+
sagaId: string;
|
|
48
|
+
stepName: string;
|
|
49
|
+
serializedInput: string;
|
|
50
|
+
handler: () => Promise<O>;
|
|
51
|
+
}): Promise<O>;
|
|
52
|
+
}
|
|
53
|
+
/** Filter applied by `SagaStateStore.list` — surfaced via the admin
|
|
54
|
+
* `ListSagas` RPC. All fields are optional; undefined means "no filter". */
|
|
55
|
+
export interface ListSagasFilter {
|
|
56
|
+
tenantId?: string;
|
|
57
|
+
status?: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
}
|
|
60
|
+
/** § 5.5 — pluggable saga_state store.
|
|
61
|
+
*
|
|
62
|
+
* `list` is optional + async (Promise-returning) to match the Python +
|
|
63
|
+
* Go ports. Implementations that omit `list` are honored by the admin
|
|
64
|
+
* server as "no enumerable rows" (it returns []).
|
|
65
|
+
*/
|
|
66
|
+
export interface SagaStateStore {
|
|
67
|
+
createSaga(row: SagaStateRow): Promise<void>;
|
|
68
|
+
getSaga(sagaId: string): Promise<SagaStateRow | null>;
|
|
69
|
+
updateSagaStatus(sagaId: string, update: Partial<Pick<SagaStateRow, "status" | "current_step" | "completed_at" | "failure_reason" | "failure_step" | "last_admin_action" | "last_admin_actor_id" | "last_admin_at" | "cancel_reason" | "next_resume_at">>): Promise<void>;
|
|
70
|
+
appendStepOutput(sagaId: string, stepName: string, output: unknown): Promise<void>;
|
|
71
|
+
appendCompensationLog(sagaId: string, entry: CompensationLogEntry): Promise<void>;
|
|
72
|
+
/** Optional enumeration — production-bound stores (Postgres) MUST
|
|
73
|
+
* implement this; tests can omit. Returns a defensive snapshot. */
|
|
74
|
+
list?(filter?: ListSagasFilter): Promise<SagaStateRow[]>;
|
|
75
|
+
/** Reaper-side scan (§ 5.8) — production-bound stores MUST implement
|
|
76
|
+
* this. Returns paused sagas whose `next_resume_at < cutoffIso`. */
|
|
77
|
+
listStalePaused?(cutoffIso: string): Promise<SagaStateRow[]>;
|
|
78
|
+
/** Pattern 2 outbox writer (§ 5.6). Optional on test doubles. */
|
|
79
|
+
writeOutboxBegin?(args: {
|
|
80
|
+
sagaId: string;
|
|
81
|
+
stepName: string;
|
|
82
|
+
eventType: string;
|
|
83
|
+
payload: unknown;
|
|
84
|
+
tenantId: string | null;
|
|
85
|
+
}): Promise<void>;
|
|
86
|
+
/** Pattern 2 outbox reader — used by tests + reaper. Optional. */
|
|
87
|
+
readOutboxForSaga?(sagaId: string): Promise<{
|
|
88
|
+
stepName: string;
|
|
89
|
+
eventType: string;
|
|
90
|
+
payload: unknown;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
}[]>;
|
|
93
|
+
}
|
|
94
|
+
/** § 5.9 — pluggable saga signal bus (Redis stream in production, in-memory
|
|
95
|
+
* in tests). All implementations carry an optional `close()` so wired
|
|
96
|
+
* services can release connections cleanly at shutdown. */
|
|
97
|
+
export interface SagaSignalBus {
|
|
98
|
+
emit(args: {
|
|
99
|
+
sagaId: string;
|
|
100
|
+
eventType: string;
|
|
101
|
+
payload: unknown;
|
|
102
|
+
scope?: "siblings" | "global";
|
|
103
|
+
to?: string;
|
|
104
|
+
}): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Polled by `awaitSagaSignal`. Returns a matching signal payload if
|
|
107
|
+
* one is available, otherwise null. The in-memory bus immediately
|
|
108
|
+
* resolves; the Redis-stream bus blocks for `blockMs` per call.
|
|
109
|
+
*/
|
|
110
|
+
consume(args: {
|
|
111
|
+
sagaId: string;
|
|
112
|
+
eventType: string;
|
|
113
|
+
from?: string;
|
|
114
|
+
predicate?: (payload: unknown) => boolean;
|
|
115
|
+
}): Promise<unknown | null>;
|
|
116
|
+
list(args: {
|
|
117
|
+
sagaId: string;
|
|
118
|
+
eventType?: string;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
sagaId: string;
|
|
121
|
+
eventType: string;
|
|
122
|
+
payload: unknown;
|
|
123
|
+
}[]>;
|
|
124
|
+
close?(): Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/** Telemetry audit emitter — matches `@nodii/telemetry.audit.emit` signature. */
|
|
127
|
+
export interface TelemetryAuditEmitter {
|
|
128
|
+
emit(event: {
|
|
129
|
+
action: string;
|
|
130
|
+
target_kind: string;
|
|
131
|
+
target_id: string | null;
|
|
132
|
+
payload?: Record<string, unknown>;
|
|
133
|
+
}): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
/** § 5.7 — six-tag schema attached by `@sagaCallable`. */
|
|
136
|
+
export interface SagaCallableConfig {
|
|
137
|
+
require_saga_context: boolean;
|
|
138
|
+
compensable: boolean;
|
|
139
|
+
compensators?: readonly string[];
|
|
140
|
+
idempotent: boolean;
|
|
141
|
+
on_finalize_safe: boolean;
|
|
142
|
+
persistFields?: readonly string[];
|
|
143
|
+
}
|
|
144
|
+
/** § 5.2 step options. */
|
|
145
|
+
export interface StepOpts<O> {
|
|
146
|
+
do: () => Promise<O>;
|
|
147
|
+
undo?: ((output: O) => Promise<void>) | {
|
|
148
|
+
fn: (output: O) => Promise<void>;
|
|
149
|
+
mode: "best-effort";
|
|
150
|
+
};
|
|
151
|
+
/** Pattern 2 marker — throws at runtime in v0.1.0 (see Non-goals). */
|
|
152
|
+
async?: boolean;
|
|
153
|
+
/** Override idempotency key — opaque to saga, passed straight to idempotency lib. */
|
|
154
|
+
idempotencyKey?: string;
|
|
155
|
+
/** Runtime input to the step. Folded into the idempotency-key serialization
|
|
156
|
+
* (alongside step name + persistFields) so two calls with the same `name`
|
|
157
|
+
* but distinct `input` resolve to distinct cache keys. Spec § 5.4 keys
|
|
158
|
+
* off serialized_input, NOT serialized_opts. */
|
|
159
|
+
input?: unknown;
|
|
160
|
+
/** Field allow-list for step_outputs persistence (level 1 of D166 resolution). */
|
|
161
|
+
persistFields?: readonly string[];
|
|
162
|
+
/** Per-step compensation mode override. */
|
|
163
|
+
mode?: "strict" | "best-effort";
|
|
164
|
+
}
|
|
165
|
+
/** § 5.2 createSaga options. */
|
|
166
|
+
export interface CreateSagaOpts {
|
|
167
|
+
type: string;
|
|
168
|
+
input: unknown;
|
|
169
|
+
tenantId?: string;
|
|
170
|
+
triggerTraceId?: string;
|
|
171
|
+
triggerSpanId?: string;
|
|
172
|
+
triggerRequestId?: string;
|
|
173
|
+
parent?: {
|
|
174
|
+
sagaId: string;
|
|
175
|
+
traceId: string;
|
|
176
|
+
} | null;
|
|
177
|
+
parentRelationship?: "child" | "compensation_of";
|
|
178
|
+
/** Override for testing — defaults to library uuidv7 generator. */
|
|
179
|
+
sagaId?: string;
|
|
180
|
+
}
|
|
181
|
+
/** § 5.8 — ResumeContext synthesized at withSaga boundary. */
|
|
182
|
+
export interface ResumeContext {
|
|
183
|
+
isResume: boolean;
|
|
184
|
+
sagaId: string;
|
|
185
|
+
}
|
|
186
|
+
/** Public-facing saga handle (returned by createSaga). */
|
|
187
|
+
export interface SagaHandle {
|
|
188
|
+
readonly id: string;
|
|
189
|
+
readonly type: string;
|
|
190
|
+
readonly tenantId: string | null;
|
|
191
|
+
cancel(reason: string): Promise<void>;
|
|
192
|
+
}
|
|
193
|
+
/** § 5.7 sagaContext interceptor config. */
|
|
194
|
+
export interface SagaContextInterceptorOpts {
|
|
195
|
+
/** Set of method names tagged `require_saga_context: true`. */
|
|
196
|
+
requireSagaContextMethods: ReadonlySet<string>;
|
|
197
|
+
/** D165 'warn' lets the call through; 'reject' returns FAILED_PRECONDITION. */
|
|
198
|
+
enforce?: "warn" | "reject";
|
|
199
|
+
/** Optional logger override; defaults to console.warn. */
|
|
200
|
+
logger?: {
|
|
201
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** Minimal call shape the sagaContext interceptor reads. */
|
|
205
|
+
export interface SagaInterceptorCall {
|
|
206
|
+
metadata: {
|
|
207
|
+
get(key: string): readonly unknown[];
|
|
208
|
+
};
|
|
209
|
+
request: unknown;
|
|
210
|
+
}
|
|
211
|
+
/** SagaAdminService factory opts. */
|
|
212
|
+
export interface SagaAdminServiceOpts {
|
|
213
|
+
stateStore: SagaStateStore;
|
|
214
|
+
audit?: TelemetryAuditEmitter;
|
|
215
|
+
}
|
|
216
|
+
/** § 5.10 — minimum justification length per D164. */
|
|
217
|
+
export declare const MIN_JUSTIFICATION_CHARS = 20;
|
|
218
|
+
/** Locked global saga admin permission strings (§ 5.10). */
|
|
219
|
+
export declare const SAGA_ADMIN_PERMISSIONS: {
|
|
220
|
+
readonly READ: "saga:admin:read";
|
|
221
|
+
readonly RETRY: "saga:admin:retry";
|
|
222
|
+
readonly SKIP: "saga:admin:skip";
|
|
223
|
+
readonly COMPENSATE: "saga:admin:compensate";
|
|
224
|
+
readonly MANUAL_RESOLVE: "saga:admin:manual_resolve";
|
|
225
|
+
};
|
|
226
|
+
export type SagaAdminPermission = (typeof SAGA_ADMIN_PERMISSIONS)[keyof typeof SAGA_ADMIN_PERMISSIONS];
|
|
227
|
+
export declare class SagaError extends Error {
|
|
228
|
+
constructor(message: string);
|
|
229
|
+
}
|
|
230
|
+
export declare class SagaNotInitialized extends SagaError {
|
|
231
|
+
constructor();
|
|
232
|
+
}
|
|
233
|
+
export declare class SagaContextRequired extends SagaError {
|
|
234
|
+
readonly methodName: string;
|
|
235
|
+
constructor(methodName: string);
|
|
236
|
+
}
|
|
237
|
+
export declare class SagaStepFailed extends SagaError {
|
|
238
|
+
readonly stepName: string;
|
|
239
|
+
readonly cause: unknown;
|
|
240
|
+
constructor(stepName: string, cause: unknown);
|
|
241
|
+
}
|
|
242
|
+
export declare class SagaCompensationFailed extends SagaError {
|
|
243
|
+
readonly stepName: string;
|
|
244
|
+
readonly cause: unknown;
|
|
245
|
+
constructor(stepName: string, cause: unknown);
|
|
246
|
+
}
|
|
247
|
+
export declare class JustificationRequired extends SagaError {
|
|
248
|
+
constructor(actual: number);
|
|
249
|
+
}
|
|
250
|
+
export declare class SagaTypeNotRegistered extends SagaError {
|
|
251
|
+
readonly sagaType: string;
|
|
252
|
+
constructor(sagaType: string);
|
|
253
|
+
}
|
|
254
|
+
export declare class NoActiveSaga extends SagaError {
|
|
255
|
+
constructor(api: string);
|
|
256
|
+
}
|
|
257
|
+
export declare class NotImplementedError extends SagaError {
|
|
258
|
+
constructor(message: string);
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Core type surface for @nodii/saga v0.1.0.
|
|
2
|
+
//
|
|
3
|
+
// Spec: planning hub feature_doc serviceId=nodii-libs docKey=saga
|
|
4
|
+
// (D158–D169 + comm doctrine §§ 3–9, 12.3–12.4, 13.1).
|
|
5
|
+
//
|
|
6
|
+
// v0.1.0 ships:
|
|
7
|
+
// - Public lifecycle (createSaga / step / stepParallel / withSaga / registerSaga)
|
|
8
|
+
// - Cross-saga primitives (emitSagaSignal / awaitSagaSignal / awaitChildren /
|
|
9
|
+
// awaitAnySibling / getSiblings) over a pluggable SagaSignalBus
|
|
10
|
+
// - Discipline tag (@sagaCallable + class registry; D165 six-tag schema)
|
|
11
|
+
// - sagaContext interceptor (enforces x-nodii-saga-id; D33 violation event)
|
|
12
|
+
// - SagaAdminService 7-RPC factory (stubs throw NotImplementedError; mandatory
|
|
13
|
+
// justification validation runs FIRST so audit shapes stay correct)
|
|
14
|
+
// - Typed-error hierarchy rooted at SagaError
|
|
15
|
+
// - Pluggable I/O: SagaStateStore, SagaSignalBus, IdempotencyShim, TelemetryAudit
|
|
16
|
+
//
|
|
17
|
+
// Deferred (`// TODO saga-v0.1.x:`):
|
|
18
|
+
// - Real Drizzle saga_state schema + migrations + RLS fragments
|
|
19
|
+
// - Real Redis-stream SagaSignalBus
|
|
20
|
+
// - Reaper / resume-grace-period orchestration
|
|
21
|
+
// - Real gRPC SagaAdminService wiring + proto generation
|
|
22
|
+
// - Codegen of sagas.generated.json + lint plugin
|
|
23
|
+
// - Pattern 2 async-via-outbox (`step({ async: true })`) — throws at runtime
|
|
24
|
+
// - Cross-saga compensation walking (compensation as library-spawned child saga)
|
|
25
|
+
/** § 5.10 — minimum justification length per D164. */
|
|
26
|
+
export const MIN_JUSTIFICATION_CHARS = 20;
|
|
27
|
+
/** Locked global saga admin permission strings (§ 5.10). */
|
|
28
|
+
export const SAGA_ADMIN_PERMISSIONS = {
|
|
29
|
+
READ: "saga:admin:read",
|
|
30
|
+
RETRY: "saga:admin:retry",
|
|
31
|
+
SKIP: "saga:admin:skip",
|
|
32
|
+
COMPENSATE: "saga:admin:compensate",
|
|
33
|
+
MANUAL_RESOLVE: "saga:admin:manual_resolve",
|
|
34
|
+
};
|
|
35
|
+
// ----------------------------------------------------------------------------
|
|
36
|
+
// Typed-error hierarchy.
|
|
37
|
+
export class SagaError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "SagaError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class SagaNotInitialized extends SagaError {
|
|
44
|
+
constructor() {
|
|
45
|
+
super("@nodii/saga is not initialized — call initSaga({ stateStore, signalBus, idempotency }) at process start.");
|
|
46
|
+
this.name = "SagaNotInitialized";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class SagaContextRequired extends SagaError {
|
|
50
|
+
methodName;
|
|
51
|
+
constructor(methodName) {
|
|
52
|
+
super(`saga_context_required: method ${methodName} requires x-nodii-saga-id header.`);
|
|
53
|
+
this.name = "SagaContextRequired";
|
|
54
|
+
this.methodName = methodName;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class SagaStepFailed extends SagaError {
|
|
58
|
+
stepName;
|
|
59
|
+
cause;
|
|
60
|
+
constructor(stepName, cause) {
|
|
61
|
+
super(`saga step '${stepName}' failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
62
|
+
this.name = "SagaStepFailed";
|
|
63
|
+
this.stepName = stepName;
|
|
64
|
+
this.cause = cause;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class SagaCompensationFailed extends SagaError {
|
|
68
|
+
stepName;
|
|
69
|
+
cause;
|
|
70
|
+
constructor(stepName, cause) {
|
|
71
|
+
super(`saga compensation for step '${stepName}' failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
72
|
+
this.name = "SagaCompensationFailed";
|
|
73
|
+
this.stepName = stepName;
|
|
74
|
+
this.cause = cause;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export class JustificationRequired extends SagaError {
|
|
78
|
+
constructor(actual) {
|
|
79
|
+
super(`justification_required: SagaAdminService write RPCs require a justification of at least ${MIN_JUSTIFICATION_CHARS} chars (got ${actual}).`);
|
|
80
|
+
this.name = "JustificationRequired";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export class SagaTypeNotRegistered extends SagaError {
|
|
84
|
+
sagaType;
|
|
85
|
+
constructor(sagaType) {
|
|
86
|
+
super(`saga_type_not_registered: ${sagaType}`);
|
|
87
|
+
this.name = "SagaTypeNotRegistered";
|
|
88
|
+
this.sagaType = sagaType;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class NoActiveSaga extends SagaError {
|
|
92
|
+
constructor(api) {
|
|
93
|
+
super(`no_active_saga: ${api} must be called inside createSaga/withSaga.`);
|
|
94
|
+
this.name = "NoActiveSaga";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class NotImplementedError extends SagaError {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "NotImplementedError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/uuid.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AA6CA,wBAAgB,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAwB3C;AAKD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C"}
|
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// UUIDv7 generator — matches @nodii/telemetry's uuid.ts surface (D158 saga_id
|
|
2
|
+
// shape). RFC 9562 §5.7: 48-bit Unix-millisecond timestamp + 12-bit subsec
|
|
3
|
+
// rand + 62-bit rand. Random bits sourced from a CSPRNG (matches Python's
|
|
4
|
+
// `secrets` + Go's `crypto/rand`) — saga ids flow into trigger_trace_id and
|
|
5
|
+
// audit correlation, so they MUST be unpredictable.
|
|
6
|
+
//
|
|
7
|
+
// Uses Node's `crypto.randomFillSync` when available; falls back to the
|
|
8
|
+
// Web Crypto `crypto.getRandomValues` for non-Node runtimes (Bun/Deno/
|
|
9
|
+
// browsers). Math.random() is intentionally NOT used.
|
|
10
|
+
// We resolve the random source lazily on first call so importing this module
|
|
11
|
+
// in an environment without crypto (extremely rare) still loads — the call
|
|
12
|
+
// site will then throw a clear error.
|
|
13
|
+
let _fillRandom = null;
|
|
14
|
+
function fillRandom(buf) {
|
|
15
|
+
if (_fillRandom == null) {
|
|
16
|
+
// Prefer Web Crypto (cross-runtime). Node ≥ 19 + Bun + Deno + browsers
|
|
17
|
+
// all expose globalThis.crypto.getRandomValues.
|
|
18
|
+
const g = globalThis;
|
|
19
|
+
if (g.crypto && typeof g.crypto.getRandomValues === "function") {
|
|
20
|
+
const wc = g.crypto;
|
|
21
|
+
_fillRandom = (b) => {
|
|
22
|
+
wc.getRandomValues?.(b);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Node-only fallback (older Node). require() is intentionally avoided —
|
|
27
|
+
// a dynamic require would break ESM bundlers. If neither is present,
|
|
28
|
+
// throw a loud error rather than silently degrade to Math.random().
|
|
29
|
+
throw new Error("uuidv7: no CSPRNG available — globalThis.crypto.getRandomValues missing");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
_fillRandom(buf);
|
|
33
|
+
}
|
|
34
|
+
function rand16() {
|
|
35
|
+
const buf = new Uint8Array(2);
|
|
36
|
+
fillRandom(buf);
|
|
37
|
+
return ((buf[0] ?? 0) << 8) | (buf[1] ?? 0);
|
|
38
|
+
}
|
|
39
|
+
export function uuidv7(now) {
|
|
40
|
+
const ms = now ?? Date.now();
|
|
41
|
+
// 48-bit timestamp.
|
|
42
|
+
const tsHex = ms.toString(16).padStart(12, "0");
|
|
43
|
+
// 12-bit rand_a — placed in lower 12 bits of the time_hi_and_version field.
|
|
44
|
+
const randA = rand16() & 0x0fff;
|
|
45
|
+
const versionHex = (0x7000 | randA).toString(16).padStart(4, "0");
|
|
46
|
+
// 2-bit variant (10) + 14-bit rand_b (high half) packed into clock_seq_hi+lo.
|
|
47
|
+
const rb1 = rand16() & 0x3fff;
|
|
48
|
+
const variantHex = (0x8000 | rb1).toString(16).padStart(4, "0");
|
|
49
|
+
// 48-bit rand_b (remainder) for the node id field.
|
|
50
|
+
const rb2 = rand16();
|
|
51
|
+
const rb3 = rand16();
|
|
52
|
+
const rb4 = rand16();
|
|
53
|
+
const nodeHex = (rb2.toString(16).padStart(4, "0") +
|
|
54
|
+
rb3.toString(16).padStart(4, "0") +
|
|
55
|
+
rb4.toString(16).padStart(4, "0")).slice(0, 12);
|
|
56
|
+
return `${tsHex.slice(0, 8)}-${tsHex.slice(8, 12)}-${versionHex}-${variantHex}-${nodeHex}`;
|
|
57
|
+
}
|
|
58
|
+
const UUIDV7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
59
|
+
export function isUuidv7(value) {
|
|
60
|
+
return UUIDV7_REGEX.test(value);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=uuid.js.map
|
package/dist/uuid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.js","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodii/saga",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Saga orchestration library for the Nodii microservice stack — createSaga + step + stepParallel + cross-saga signals + sagaContext interceptor + SagaAdminService factory + @sagaCallable discipline tag + real PostgresSagaStateStore + RedisSignalBus + Pattern 2 async-step + reaper. Polyglot per D159 (TS + Python + Go).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -10,24 +10,50 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./test-doubles": {
|
|
15
|
+
"types": "./dist/test-doubles/index.d.ts",
|
|
16
|
+
"default": "./dist/test-doubles/index.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"src/migrations/001-saga-state.sql"
|
|
23
|
+
],
|
|
16
24
|
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
18
|
-
"provenance": true
|
|
25
|
+
"access": "public"
|
|
19
26
|
},
|
|
20
27
|
"scripts": {
|
|
21
28
|
"build": "tsc",
|
|
22
29
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "
|
|
30
|
+
"test": "bun test"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@nodii/grpc-auth": "workspace:*",
|
|
34
|
+
"@nodii/telemetry": "workspace:*",
|
|
35
|
+
"ioredis": "^5.4.0",
|
|
36
|
+
"postgres": "^3.4.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"ioredis": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"postgres": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
24
45
|
},
|
|
25
46
|
"devDependencies": {
|
|
47
|
+
"@nodii/grpc-auth": "workspace:*",
|
|
48
|
+
"@nodii/telemetry": "workspace:*",
|
|
49
|
+
"@types/bun": "^1.3.13",
|
|
50
|
+
"ioredis": "^5.4.0",
|
|
51
|
+
"postgres": "^3.4.0",
|
|
26
52
|
"typescript": "^5.9.3"
|
|
27
53
|
},
|
|
28
54
|
"repository": {
|
|
29
55
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/cognion-nucleus/nodii-libs.git",
|
|
56
|
+
"url": "git+https://github.com/cognion-nucleus/nodii-libs.git",
|
|
31
57
|
"directory": "ts/saga"
|
|
32
58
|
}
|
|
33
59
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
-- @nodii/saga v0.1.x — saga_state schema + outbox + indexes.
|
|
2
|
+
--
|
|
3
|
+
-- Spec § 5.5 — 21 user-facing columns (we add next_resume_at since spec §
|
|
4
|
+
-- 5.5 lists it under "lifecycle" alongside completed_at). Plus a small
|
|
5
|
+
-- saga_outbox for Pattern 2 begin/completion events.
|
|
6
|
+
--
|
|
7
|
+
-- This file is the canonical source for the per-service migration; the
|
|
8
|
+
-- `migrate-gen` helper (`src/migrate-gen.ts`) copies the contents into a
|
|
9
|
+
-- service's migrations folder verbatim.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS saga_state (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
tenant_id TEXT,
|
|
14
|
+
type TEXT NOT NULL,
|
|
15
|
+
status TEXT NOT NULL,
|
|
16
|
+
current_step TEXT,
|
|
17
|
+
step_outputs JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
18
|
+
episode INT NOT NULL DEFAULT 1,
|
|
19
|
+
|
|
20
|
+
-- D7 trace coupling
|
|
21
|
+
trigger_trace_id TEXT NOT NULL,
|
|
22
|
+
trigger_span_id TEXT NOT NULL,
|
|
23
|
+
trigger_request_id TEXT NOT NULL,
|
|
24
|
+
|
|
25
|
+
-- Cross-saga (D168)
|
|
26
|
+
parent_saga_id TEXT,
|
|
27
|
+
parent_relationship TEXT NOT NULL DEFAULT 'child',
|
|
28
|
+
children JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
29
|
+
|
|
30
|
+
-- Lifecycle
|
|
31
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
32
|
+
completed_at TIMESTAMPTZ,
|
|
33
|
+
next_resume_at TIMESTAMPTZ,
|
|
34
|
+
failure_reason TEXT,
|
|
35
|
+
failure_step TEXT,
|
|
36
|
+
|
|
37
|
+
-- Compensation
|
|
38
|
+
compensation_log JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
39
|
+
|
|
40
|
+
-- Admin recovery (D164)
|
|
41
|
+
last_admin_action TEXT,
|
|
42
|
+
last_admin_actor_id TEXT,
|
|
43
|
+
last_admin_at TIMESTAMPTZ,
|
|
44
|
+
|
|
45
|
+
-- Retroactive amendments
|
|
46
|
+
input JSONB NOT NULL,
|
|
47
|
+
cancel_reason TEXT
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- Spec § 5.5 — four indexes.
|
|
51
|
+
CREATE UNIQUE INDEX IF NOT EXISTS saga_state_trigger_trace_id_uniq
|
|
52
|
+
ON saga_state (trigger_trace_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS saga_state_dead_letter_idx
|
|
54
|
+
ON saga_state (tenant_id, status, started_at);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS saga_state_reaper_idx
|
|
56
|
+
ON saga_state (next_resume_at)
|
|
57
|
+
WHERE next_resume_at IS NOT NULL;
|
|
58
|
+
CREATE INDEX IF NOT EXISTS saga_state_parent_idx
|
|
59
|
+
ON saga_state (parent_saga_id)
|
|
60
|
+
WHERE parent_saga_id IS NOT NULL;
|
|
61
|
+
|
|
62
|
+
-- Pattern 2 outbox table. Persisted in the same DB tx as saga_state advance
|
|
63
|
+
-- per § 5.6. The orchestrator consumer worker reads + relays into the
|
|
64
|
+
-- service's broker; the reaper falls back on this table when a
|
|
65
|
+
-- next_resume_at expires.
|
|
66
|
+
CREATE TABLE IF NOT EXISTS saga_outbox (
|
|
67
|
+
id UUID PRIMARY KEY,
|
|
68
|
+
saga_id TEXT NOT NULL,
|
|
69
|
+
step_name TEXT NOT NULL,
|
|
70
|
+
event_type TEXT NOT NULL,
|
|
71
|
+
payload JSONB NOT NULL,
|
|
72
|
+
tenant_id TEXT,
|
|
73
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
74
|
+
delivered_at TIMESTAMPTZ
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS saga_outbox_saga_idx ON saga_outbox (saga_id, created_at);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS saga_outbox_undelivered_idx ON saga_outbox (delivered_at)
|
|
79
|
+
WHERE delivered_at IS NULL;
|