@sekiban/dcb-domain 0.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.
@@ -0,0 +1,24 @@
1
+ import type { z } from "zod";
2
+ import { DomainAuthoringError, type DomainBoundary, type ParsedAt } from "./types.js";
3
+ export declare class BoundaryParseError extends DomainAuthoringError {
4
+ readonly boundary: DomainBoundary;
5
+ readonly finding: string;
6
+ constructor(boundary: DomainBoundary, finding: string, message: string, options?: ErrorOptions);
7
+ }
8
+ export declare function parseAt<Boundary extends DomainBoundary, Schema extends z.ZodTypeAny>(boundary: Boundary, schema: Schema, value: unknown): ParsedAt<Boundary, z.infer<Schema>>;
9
+ export declare function isParsedAt(value: unknown): boolean;
10
+ export declare function assertParsedAt<Boundary extends DomainBoundary, Value>(boundary: Boundary, value: Value): ParsedAt<Boundary, Value>;
11
+ export declare const parseHttpCommandInput: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"http-command", z.core.output<Schema>>;
12
+ export declare const parseQueueMessage: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"queue", z.core.output<Schema>>;
13
+ export declare const parseStoredEvent: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"stored-event", z.core.output<Schema>>;
14
+ export declare const parseExternalQueryInput: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"external-query", z.core.output<Schema>>;
15
+ export interface WasmRestoreDecoder<Schema extends z.ZodTypeAny> {
16
+ readonly boundary: "wasm-restore";
17
+ readonly decode: (bytes: Uint8Array | string) => ParsedAt<"wasm-restore", z.infer<Schema>>;
18
+ }
19
+ export declare function createWasmRestoreDecoder<Schema extends z.ZodTypeAny>(schema: Schema): WasmRestoreDecoder<Schema>;
20
+ export declare const wasmRestoreDecoder: typeof createWasmRestoreDecoder;
21
+ export declare const parseCommandIngress: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"http-command", z.core.output<Schema>>;
22
+ export declare const parseQueueIngress: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"queue", z.core.output<Schema>>;
23
+ export declare const parseStoredEventMaterialization: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"stored-event", z.core.output<Schema>>;
24
+ export declare const parseQueryInput: <Schema extends z.ZodTypeAny>(schema: Schema, value: unknown) => ParsedAt<"external-query", z.core.output<Schema>>;
package/dist/parse.js ADDED
@@ -0,0 +1,71 @@
1
+ import { DomainAuthoringError, } from "./types";
2
+ const parsedValues = new Map();
3
+ const parsedPrimitives = new Map();
4
+ export class BoundaryParseError extends DomainAuthoringError {
5
+ boundary;
6
+ finding;
7
+ constructor(boundary, finding, message, options) {
8
+ super("BOUNDARY_PARSE_FAILED", message, options);
9
+ this.name = "BoundaryParseError";
10
+ this.boundary = boundary;
11
+ this.finding = finding;
12
+ }
13
+ }
14
+ export function parseAt(boundary, schema, value) {
15
+ try {
16
+ const parsed = schema.parse(value);
17
+ if (typeof parsed === "object" && parsed !== null) {
18
+ const values = parsedValues.get(boundary) ?? new WeakSet();
19
+ values.add(parsed);
20
+ parsedValues.set(boundary, values);
21
+ }
22
+ else {
23
+ const values = parsedPrimitives.get(boundary) ?? new Set();
24
+ values.add(parsed);
25
+ parsedPrimitives.set(boundary, values);
26
+ }
27
+ return parsed;
28
+ }
29
+ catch (error) {
30
+ throw new BoundaryParseError(boundary, `${boundary}-parse`, `Input failed the ${boundary} parse boundary`, { cause: error });
31
+ }
32
+ }
33
+ export function isParsedAt(value) {
34
+ if (typeof value !== "object" || value === null)
35
+ return false;
36
+ for (const values of parsedValues.values())
37
+ if (values.has(value))
38
+ return true;
39
+ return false;
40
+ }
41
+ export function assertParsedAt(boundary, value) {
42
+ if ((typeof value === "object" && value !== null && parsedValues.get(boundary)?.has(value) === true) || parsedPrimitives.get(boundary)?.has(value) === true) {
43
+ return value;
44
+ }
45
+ throw new BoundaryParseError(boundary, `${boundary}-parse-bypass`, `Value did not come from the ${boundary} parser`);
46
+ }
47
+ export const parseHttpCommandInput = (schema, value) => parseAt("http-command", schema, value);
48
+ export const parseQueueMessage = (schema, value) => parseAt("queue", schema, value);
49
+ export const parseStoredEvent = (schema, value) => parseAt("stored-event", schema, value);
50
+ export const parseExternalQueryInput = (schema, value) => parseAt("external-query", schema, value);
51
+ export function createWasmRestoreDecoder(schema) {
52
+ return Object.freeze({
53
+ boundary: "wasm-restore",
54
+ decode: (bytes) => {
55
+ let value;
56
+ try {
57
+ const text = typeof bytes === "string" ? bytes : new TextDecoder().decode(bytes);
58
+ value = JSON.parse(text);
59
+ }
60
+ catch (error) {
61
+ throw new BoundaryParseError("wasm-restore", "wasm-restore-malformed-bytes", "WASM restore bytes were not valid JSON", { cause: error });
62
+ }
63
+ return parseAt("wasm-restore", schema, value);
64
+ },
65
+ });
66
+ }
67
+ export const wasmRestoreDecoder = createWasmRestoreDecoder;
68
+ export const parseCommandIngress = parseHttpCommandInput;
69
+ export const parseQueueIngress = parseQueueMessage;
70
+ export const parseStoredEventMaterialization = parseStoredEvent;
71
+ export const parseQueryInput = parseExternalQueryInput;
@@ -0,0 +1,108 @@
1
+ import { DomainAuthoringError, type CandidateEnvelope, type DecisionLog, type Done, type EventOf, type FixedNow, type None, type PortableSnapshot, type ReadClaim, type ReadSet, type SnapshotReader, type Tag, type TerminalDecision, type TimeProvider } from "./types.js";
2
+ import { type CommandContext, type CommandDefinition, type StagedEvent } from "./command.js";
3
+ import type { EventDefinition } from "./event.js";
4
+ import type { ProjectorDefinition } from "./state.js";
5
+ export type SessionStatus = "OPEN" | "SEALED" | "DISCARDED";
6
+ export type TagPropagationPoint = "staged-log" | "eligible-cells" | "claim-candidate-preflight" | "sealed-envelope";
7
+ export interface TagPropagationObservation {
8
+ readonly point: TagPropagationPoint;
9
+ readonly eventType?: string;
10
+ readonly tags: readonly Tag[];
11
+ }
12
+ export declare class SessionStateError extends DomainAuthoringError {
13
+ constructor(message: string);
14
+ }
15
+ export declare class UndeclaredReadError extends DomainAuthoringError {
16
+ readonly projectorId?: string;
17
+ readonly tag: Tag;
18
+ constructor(projectorId: string | undefined, tag: Tag);
19
+ }
20
+ export declare class IncoherentSnapshotError extends DomainAuthoringError {
21
+ constructor(tag: Tag, firstHead: string | null, secondHead: string | null);
22
+ }
23
+ export interface SessionOptions {
24
+ readonly now: FixedNow;
25
+ readonly readSet: ReadSet;
26
+ readonly snapshots?: SnapshotReader;
27
+ readonly onPropagation?: (observation: TagPropagationObservation) => void;
28
+ }
29
+ export interface PortableSnapshotWire {
30
+ readonly projectorId: string;
31
+ readonly tag: string;
32
+ readonly head: string | null;
33
+ readonly state: unknown;
34
+ readonly exists: boolean;
35
+ }
36
+ export declare function serializePortableSnapshot(snapshot: PortableSnapshot): string;
37
+ export declare function deserializePortableSnapshot(serialized: string): PortableSnapshot;
38
+ export declare class Session {
39
+ readonly now: FixedNow;
40
+ private statusValue;
41
+ private readonly readSet;
42
+ private readonly snapshots?;
43
+ private readonly onPropagation?;
44
+ private readonly snapshotByCell;
45
+ private readonly overlayByCell;
46
+ private readonly headByTag;
47
+ private readonly claimsByKey;
48
+ private readonly staged;
49
+ private readonly observations;
50
+ constructor(options: SessionOptions);
51
+ get status(): SessionStatus;
52
+ get stagedEvents(): readonly StagedEvent[];
53
+ get propagation(): readonly TagPropagationObservation[];
54
+ get readClaims(): readonly ReadClaim[];
55
+ private observe;
56
+ private assertOpen;
57
+ private assertDeclared;
58
+ private rememberClaim;
59
+ private loadSnapshot;
60
+ private stateFor;
61
+ private existsFor;
62
+ private snapshotByCellHasTag;
63
+ preload(): Promise<void>;
64
+ private findProjector;
65
+ private readonly readSetProjectors;
66
+ private attachProjector;
67
+ attachProjectors(projectors: readonly ProjectorDefinition[]): void;
68
+ context(projectors?: readonly ProjectorDefinition[]): CommandContext;
69
+ append<Event extends EventDefinition>(event: Event, payload: EventOf<Event>): void;
70
+ private candidateTags;
71
+ seal(decision: Done): CandidateEnvelope;
72
+ discard(reason?: string): None | TerminalDecision;
73
+ finish(decision: TerminalDecision): CandidateEnvelope | undefined;
74
+ decisionLog(decision: TerminalDecision): DecisionLog;
75
+ }
76
+ export type CommitAttemptResult = {
77
+ readonly kind: "accepted";
78
+ } | {
79
+ readonly kind: "consistency-conflict";
80
+ } | {
81
+ readonly kind: "unknown";
82
+ readonly error?: unknown;
83
+ } | {
84
+ readonly kind: "rejected";
85
+ readonly error?: unknown;
86
+ };
87
+ export interface ExecuteCommandOptions {
88
+ readonly timeProvider?: TimeProvider;
89
+ readonly snapshots?: SnapshotReader;
90
+ readonly commit?: (envelope: CandidateEnvelope) => Promise<unknown> | unknown;
91
+ readonly maxConflictRetries?: number;
92
+ readonly onPropagation?: (observation: TagPropagationObservation) => void;
93
+ }
94
+ export interface ExecuteCommandResult {
95
+ readonly status: "accepted" | "discarded" | "unknown" | "rejected";
96
+ readonly attempts: number;
97
+ readonly now: FixedNow;
98
+ readonly decision: TerminalDecision;
99
+ readonly envelope?: CandidateEnvelope;
100
+ readonly log: DecisionLog;
101
+ readonly session: Session;
102
+ readonly error?: unknown;
103
+ }
104
+ export declare function executeCommand<Command extends CommandDefinition>(command: Command, input: unknown, options?: ExecuteCommandOptions): Promise<ExecuteCommandResult>;
105
+ export declare const runCommand: typeof executeCommand;
106
+ export declare const runSession: typeof executeCommand;
107
+ export declare const executePortableCommand: typeof executeCommand;
108
+ export declare function serializeDecisionLog(log: DecisionLog): string;
@@ -0,0 +1,381 @@
1
+ import { assertJsonValue, DomainAuthoringError, cloneAndFreeze, normalizeTag, } from "./types";
2
+ import { none, } from "./command";
3
+ export class SessionStateError extends DomainAuthoringError {
4
+ constructor(message) {
5
+ super("SESSION_STATE_INVALID", message);
6
+ this.name = "SessionStateError";
7
+ }
8
+ }
9
+ export class UndeclaredReadError extends DomainAuthoringError {
10
+ projectorId;
11
+ tag;
12
+ constructor(projectorId, tag) {
13
+ super("UNDECLARED_DYNAMIC_READ", `Read of ${projectorId ?? "exists"}/${tag.id} was not declared`);
14
+ this.name = "UndeclaredReadError";
15
+ this.projectorId = projectorId;
16
+ this.tag = tag;
17
+ }
18
+ }
19
+ export class IncoherentSnapshotError extends DomainAuthoringError {
20
+ constructor(tag, firstHead, secondHead) {
21
+ super("INCOHERENT_SNAPSHOT", `Tag ${tag.id} was supplied with heads ${firstHead ?? "null"} and ${secondHead ?? "null"}`);
22
+ this.name = "IncoherentSnapshotError";
23
+ }
24
+ }
25
+ export function serializePortableSnapshot(snapshot) {
26
+ const state = assertJsonValue(snapshot.state, "snapshot-serialization");
27
+ return JSON.stringify({
28
+ projectorId: snapshot.projectorId,
29
+ tag: snapshot.tag.id,
30
+ head: snapshot.head,
31
+ state,
32
+ exists: snapshot.exists,
33
+ });
34
+ }
35
+ export function deserializePortableSnapshot(serialized) {
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(serialized);
39
+ }
40
+ catch (error) {
41
+ throw new DomainAuthoringError("SNAPSHOT_SERIALIZATION_INVALID", "Portable snapshot was not valid JSON", { cause: error });
42
+ }
43
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
44
+ throw new DomainAuthoringError("SNAPSHOT_SERIALIZATION_INVALID", "Portable snapshot must be an object");
45
+ }
46
+ const record = parsed;
47
+ if (typeof record.projectorId !== "string" || typeof record.tag !== "string" ||
48
+ (record.head !== null && typeof record.head !== "string") || typeof record.exists !== "boolean") {
49
+ throw new DomainAuthoringError("SNAPSHOT_SERIALIZATION_INVALID", "Portable snapshot fields were invalid");
50
+ }
51
+ return Object.freeze({
52
+ projectorId: record.projectorId,
53
+ tag: normalizeTag(record.tag),
54
+ head: record.head,
55
+ state: assertJsonValue(record.state, "snapshot-deserialization"),
56
+ exists: record.exists,
57
+ });
58
+ }
59
+ function cellKey(projector, tag) {
60
+ return `${projector.id}\u0000${tag.id}`;
61
+ }
62
+ function initialState(projector) {
63
+ return typeof projector.initialState === "function"
64
+ ? projector.initialState()
65
+ : projector.initialState;
66
+ }
67
+ function eventEligible(projector, cellTag, event) {
68
+ return event.tags.some((tag) => tag.id === cellTag.id && tag.family === projector.tag.family)
69
+ && projector.subscribes(event.eventType);
70
+ }
71
+ function asRecord(event) {
72
+ return Object.freeze({
73
+ eventType: event.eventType,
74
+ eventName: event.event.eventPayloadName,
75
+ payload: event.payload,
76
+ tags: Object.freeze([...event.tags]),
77
+ ordinal: event.ordinal,
78
+ });
79
+ }
80
+ export class Session {
81
+ now;
82
+ statusValue = "OPEN";
83
+ readSet;
84
+ snapshots;
85
+ onPropagation;
86
+ snapshotByCell = new Map();
87
+ overlayByCell = new Map();
88
+ headByTag = new Map();
89
+ claimsByKey = new Map();
90
+ staged = [];
91
+ observations = [];
92
+ constructor(options) {
93
+ this.now = options.now;
94
+ this.readSet = options.readSet;
95
+ this.snapshots = options.snapshots;
96
+ this.onPropagation = options.onPropagation;
97
+ }
98
+ get status() {
99
+ return this.statusValue;
100
+ }
101
+ get stagedEvents() {
102
+ return Object.freeze([...this.staged]);
103
+ }
104
+ get propagation() {
105
+ return Object.freeze([...this.observations]);
106
+ }
107
+ get readClaims() {
108
+ return Object.freeze([...this.claimsByKey.values()]);
109
+ }
110
+ observe(observation) {
111
+ const frozen = Object.freeze({ ...observation, tags: Object.freeze([...observation.tags]) });
112
+ this.observations.push(frozen);
113
+ this.onPropagation?.(frozen);
114
+ }
115
+ assertOpen() {
116
+ if (this.statusValue !== "OPEN")
117
+ throw new SessionStateError(`Session is ${this.statusValue}`);
118
+ }
119
+ assertDeclared(kind, projectorId, tag) {
120
+ if (!this.readSet.has(kind, projectorId, tag))
121
+ throw new UndeclaredReadError(projectorId, tag);
122
+ }
123
+ rememberClaim(declaration, head) {
124
+ const key = `${declaration.kind}\u0000${declaration.projectorId ?? ""}\u0000${declaration.tag.id}`;
125
+ this.claimsByKey.set(key, Object.freeze({
126
+ kind: declaration.kind,
127
+ ...(declaration.projectorId === undefined ? {} : { projectorId: declaration.projectorId }),
128
+ tag: declaration.tag,
129
+ head,
130
+ }));
131
+ }
132
+ async loadSnapshot(projector, tag) {
133
+ const key = cellKey(projector, tag);
134
+ const cached = this.snapshotByCell.get(key);
135
+ if (cached !== undefined)
136
+ return cached;
137
+ const supplied = this.snapshots === undefined
138
+ ? { projectorId: projector.id, tag, head: null, state: initialState(projector), exists: false }
139
+ : await this.snapshots.read(projector, tag);
140
+ const suppliedTag = normalizeTag(supplied.tag);
141
+ if (suppliedTag.id !== tag.id || supplied.projectorId !== projector.id) {
142
+ throw new DomainAuthoringError("SNAPSHOT_IDENTITY_INVALID", `Snapshot identity did not match ${projector.id}/${tag.id}`);
143
+ }
144
+ const suppliedHead = supplied.head ?? "";
145
+ const existingHead = this.headByTag.get(tag.id);
146
+ if (existingHead !== undefined && existingHead !== suppliedHead) {
147
+ throw new IncoherentSnapshotError(tag, existingHead, suppliedHead);
148
+ }
149
+ this.headByTag.set(tag.id, suppliedHead);
150
+ this.snapshotByCell.set(key, supplied);
151
+ return supplied;
152
+ }
153
+ async stateFor(projector, tag) {
154
+ this.assertOpen();
155
+ this.assertDeclared("state", projector.id, tag);
156
+ this.attachProjector(projector);
157
+ const key = cellKey(projector, tag);
158
+ const overlay = this.overlayByCell.get(key);
159
+ if (overlay !== undefined)
160
+ return overlay;
161
+ const snapshot = await this.loadSnapshot(projector, tag);
162
+ let state = snapshot.state;
163
+ for (const staged of this.staged) {
164
+ const record = asRecord(staged);
165
+ if (!eventEligible(projector, tag, record))
166
+ continue;
167
+ state = projector.apply(state, record);
168
+ this.observe({ point: "eligible-cells", eventType: record.eventType, tags: record.tags });
169
+ }
170
+ this.overlayByCell.set(key, state);
171
+ this.rememberClaim({ kind: "state", projectorId: projector.id, tag }, snapshot.head ?? "");
172
+ return state;
173
+ }
174
+ async existsFor(tag) {
175
+ this.assertOpen();
176
+ this.assertDeclared("exists", undefined, tag);
177
+ const snapshotExists = this.snapshots?.exists === undefined ? undefined : await this.snapshots.exists(tag);
178
+ const stagedExists = this.staged.some((event) => event.tags.some((candidate) => candidate.id === tag.id));
179
+ // A host-provided `exists=false` is the base snapshot result, not a veto
180
+ // over an event already staged in this session.
181
+ const result = (snapshotExists ?? this.snapshotByCellHasTag(tag)) || stagedExists;
182
+ let head = this.headByTag.get(tag.id);
183
+ if (snapshotExists === false) {
184
+ // `exists=false` is an observed empty-head fact. Keep it distinct from
185
+ // an exists-only read that did not provide an exact head.
186
+ head = "";
187
+ this.headByTag.set(tag.id, head);
188
+ }
189
+ else if (snapshotExists === true && this.snapshots?.head !== undefined) {
190
+ head = await this.snapshots.head(tag);
191
+ this.headByTag.set(tag.id, head);
192
+ }
193
+ this.rememberClaim({ kind: "exists", tag }, head ?? null);
194
+ return result;
195
+ }
196
+ snapshotByCellHasTag(tag) {
197
+ return [...this.snapshotByCell.values()].some((snapshot) => snapshot.tag.id === tag.id && snapshot.exists);
198
+ }
199
+ async preload() {
200
+ this.assertOpen();
201
+ for (const declaration of this.readSet.claims) {
202
+ if (declaration.kind === "state") {
203
+ const projector = this.findProjector(declaration.projectorId);
204
+ await this.stateFor(projector, declaration.tag);
205
+ }
206
+ else {
207
+ await this.existsFor(declaration.tag);
208
+ }
209
+ }
210
+ }
211
+ findProjector(projectorId) {
212
+ if (projectorId === undefined)
213
+ throw new DomainAuthoringError("PROJECTOR_ID_REQUIRED", "A state read requires a projector");
214
+ const declaration = this.readSet.claims.find((claim) => claim.projectorId === projectorId && claim.projector !== undefined);
215
+ if (declaration?.projector !== undefined)
216
+ return declaration.projector;
217
+ throw new DomainAuthoringError("PROJECTOR_NOT_IN_READ_SET", `Projector ${projectorId} was not attached to this session`);
218
+ }
219
+ readSetProjectors = new Map();
220
+ attachProjector(projector) {
221
+ this.readSetProjectors.set(projector.id, {
222
+ id: projector.id,
223
+ tag: projector.tag,
224
+ subscribes: projector.subscribes,
225
+ apply: (state, event) => projector.apply(state, event),
226
+ });
227
+ }
228
+ attachProjectors(projectors) {
229
+ for (const projector of projectors)
230
+ this.attachProjector(projector);
231
+ }
232
+ context(projectors = []) {
233
+ this.attachProjectors(projectors);
234
+ return Object.freeze({
235
+ state: (projector, tag) => this.stateFor(projector, tag),
236
+ exists: (tag) => this.existsFor(tag),
237
+ now: () => this.now,
238
+ append: (event, payload) => this.append(event, payload),
239
+ });
240
+ }
241
+ append(event, payload) {
242
+ this.assertOpen();
243
+ const parsed = event.make(payload);
244
+ const derivedTags = event.tags(parsed).map(normalizeTag);
245
+ const ordinal = String(this.staged.length);
246
+ const staged = Object.freeze({
247
+ event,
248
+ eventType: event.eventType,
249
+ payload: parsed,
250
+ tags: Object.freeze(derivedTags),
251
+ ordinal,
252
+ });
253
+ this.staged.push(staged);
254
+ this.observe({ point: "staged-log", eventType: event.eventType, tags: derivedTags });
255
+ const record = asRecord(staged);
256
+ for (const [key, snapshot] of this.snapshotByCell) {
257
+ const separator = key.indexOf("\u0000");
258
+ const projectorId = key.slice(0, separator);
259
+ const tagId = key.slice(separator + 1);
260
+ if (!record.tags.some((tag) => tag.id === tagId))
261
+ continue;
262
+ const projector = this.readSetProjectors.get(projectorId);
263
+ if (projector === undefined || !eventEligible(projector, snapshot.tag, record))
264
+ continue;
265
+ const previous = this.overlayByCell.get(key) ?? snapshot.state;
266
+ const next = projector.apply(previous, record);
267
+ this.overlayByCell.set(key, next);
268
+ this.observe({ point: "eligible-cells", eventType: event.eventType, tags: record.tags });
269
+ }
270
+ }
271
+ candidateTags() {
272
+ const tags = new Map();
273
+ for (const event of this.staged)
274
+ for (const tag of event.tags)
275
+ tags.set(tag.id, tag);
276
+ for (const tag of this.readSet.tags)
277
+ tags.set(tag.id, tag);
278
+ return Object.freeze([...tags.values()]);
279
+ }
280
+ seal(decision) {
281
+ this.assertOpen();
282
+ const tags = this.candidateTags();
283
+ this.observe({ point: "claim-candidate-preflight", tags });
284
+ const envelope = Object.freeze({
285
+ kind: "candidate-envelope",
286
+ now: this.now,
287
+ events: Object.freeze(this.staged.map(asRecord)),
288
+ tags,
289
+ readClaims: this.readClaims,
290
+ decision,
291
+ });
292
+ this.observe({ point: "sealed-envelope", tags });
293
+ this.statusValue = "SEALED";
294
+ return envelope;
295
+ }
296
+ discard(reason = "discarded") {
297
+ this.assertOpen();
298
+ this.staged.splice(0, this.staged.length);
299
+ this.overlayByCell.clear();
300
+ this.statusValue = "DISCARDED";
301
+ return reason.length === 0 ? none() : none(reason);
302
+ }
303
+ finish(decision) {
304
+ if (decision.kind === "done")
305
+ return this.seal(decision);
306
+ this.discard(decision.kind === "none" ? decision.reason ?? "none" : decision.reason);
307
+ return undefined;
308
+ }
309
+ decisionLog(decision) {
310
+ return Object.freeze({
311
+ now: this.now,
312
+ events: Object.freeze(this.staged.map(asRecord)),
313
+ readClaims: this.readClaims,
314
+ terminal: decision,
315
+ });
316
+ }
317
+ }
318
+ function classifyCommitResult(value) {
319
+ if (value === undefined || value === true)
320
+ return { kind: "accepted" };
321
+ if (typeof value !== "object" || value === null)
322
+ return { kind: "accepted" };
323
+ const record = value;
324
+ if (record.kind === "consistency-conflict" || record.kind === "conflict" || record.code === "consistency_conflict") {
325
+ return { kind: "consistency-conflict" };
326
+ }
327
+ if (record.kind === "unknown" || record.kind === "timeout" || record.code === "unknown_outcome") {
328
+ return { kind: "unknown", error: value };
329
+ }
330
+ if (record.kind === "rejected" || record.kind === "invalid")
331
+ return { kind: "rejected", error: value };
332
+ return { kind: "accepted" };
333
+ }
334
+ export async function executeCommand(command, input, options = {}) {
335
+ const fixedNow = options.timeProvider?.now() ?? 0;
336
+ const maxRetries = Math.max(0, Math.floor(options.maxConflictRetries ?? 1));
337
+ const parsed = command.parseInput(input);
338
+ let attempts = 0;
339
+ for (;;) {
340
+ attempts += 1;
341
+ const readSet = command.reads(parsed);
342
+ const session = new Session({
343
+ now: fixedNow,
344
+ readSet,
345
+ snapshots: options.snapshots,
346
+ onPropagation: options.onPropagation,
347
+ });
348
+ try {
349
+ await session.preload();
350
+ const context = session.context([]);
351
+ const decision = await command.handle(parsed, context);
352
+ const envelope = session.finish(decision);
353
+ const log = session.decisionLog(decision);
354
+ if (envelope === undefined) {
355
+ return Object.freeze({ status: decision.kind === "reject" ? "rejected" : "discarded", attempts, now: fixedNow, decision, log, session });
356
+ }
357
+ if (options.commit === undefined) {
358
+ return Object.freeze({ status: "accepted", attempts, now: fixedNow, decision, envelope, log, session });
359
+ }
360
+ const commitResult = classifyCommitResult(await options.commit(envelope));
361
+ if (commitResult.kind === "consistency-conflict" && attempts <= maxRetries)
362
+ continue;
363
+ if (commitResult.kind === "unknown")
364
+ return Object.freeze({ status: "unknown", attempts, now: fixedNow, decision, envelope, log, session, error: commitResult.error });
365
+ if (commitResult.kind === "rejected")
366
+ return Object.freeze({ status: "rejected", attempts, now: fixedNow, decision, envelope, log, session, error: commitResult.error });
367
+ return Object.freeze({ status: "accepted", attempts, now: fixedNow, decision, envelope, log, session });
368
+ }
369
+ catch (error) {
370
+ if (session.status === "OPEN")
371
+ session.discard("throw");
372
+ throw error;
373
+ }
374
+ }
375
+ }
376
+ export const runCommand = executeCommand;
377
+ export const runSession = executeCommand;
378
+ export const executePortableCommand = executeCommand;
379
+ export function serializeDecisionLog(log) {
380
+ return JSON.stringify(cloneAndFreeze(log));
381
+ }
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import { type EventOf, type EventRecord, type Tag, type ProjectorLike, type TagFamily } from "./types.js";
3
+ import type { EventDefinition } from "./event.js";
4
+ declare const projectorFamilyInvariant: unique symbol;
5
+ export interface StateUnion<Schema extends z.ZodTypeAny = z.ZodTypeAny> {
6
+ readonly kind: "state-union";
7
+ readonly discriminator: string;
8
+ readonly schema: Schema;
9
+ readonly parse: (value: unknown) => z.infer<Schema>;
10
+ readonly initial: z.infer<Schema> | (() => z.infer<Schema>);
11
+ }
12
+ export declare function stateUnion<Schema extends z.ZodTypeAny>(schema: Schema, options: {
13
+ readonly discriminator?: string;
14
+ readonly initial: z.infer<Schema> | (() => z.infer<Schema>);
15
+ }): StateUnion<Schema>;
16
+ export declare const state: typeof stateUnion;
17
+ export declare function states<const Variants extends readonly [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]>(variants: Variants, options: {
18
+ readonly discriminator?: string;
19
+ readonly initial: z.infer<Variants[number]> | (() => z.infer<Variants[number]>);
20
+ }): StateUnion<z.ZodType<z.infer<Variants[number]>>>;
21
+ export type StateOf<Definition extends StateUnion> = z.infer<Definition["schema"]>;
22
+ export type ValidateResult<Kind extends string = string, Details = unknown> = {
23
+ readonly kind: "reject";
24
+ readonly rejectKind: Kind;
25
+ readonly reason: string;
26
+ readonly details?: Details;
27
+ } | undefined;
28
+ export declare function validationReject<Kind extends string, Details = unknown>(rejectKind: Kind, reason: string, details?: Details): Exclude<ValidateResult<Kind, Details>, undefined>;
29
+ export type PureValidator<State, Args, Kind extends string = string, Details = unknown> = (state: State, args: Args) => ValidateResult<Kind, Details>;
30
+ export type PureEvolver<State, Event extends EventDefinition = EventDefinition> = (state: State, event: ProjectorEvent<Event>) => State;
31
+ export interface DeciderModule<State, Args, Event extends EventDefinition = EventDefinition, Kind extends string = string> {
32
+ readonly validate: PureValidator<State, Args, Kind>;
33
+ readonly evolve: PureEvolver<State, Event>;
34
+ }
35
+ export declare function validate<State, Args, Kind extends string = string, Details = unknown>(fn: PureValidator<State, Args, Kind, Details>): PureValidator<State, Args, Kind, Details>;
36
+ export declare function evolve<State, Event extends EventDefinition>(fn: PureEvolver<State, Event>): PureEvolver<State, Event>;
37
+ export declare function decider<State, Args, Event extends EventDefinition, Kind extends string = string>(module: DeciderModule<State, Args, Event, Kind>): DeciderModule<State, Args, Event, Kind>;
38
+ export interface ProjectorEvent<Event extends EventDefinition = EventDefinition> {
39
+ readonly definition: Event;
40
+ readonly eventType: string;
41
+ readonly payload: EventOf<Event>;
42
+ readonly tags: readonly Tag[];
43
+ }
44
+ export type ProjectorHandler<State, Event extends EventDefinition = EventDefinition> = (state: State, event: ProjectorEvent<Event>) => State;
45
+ export interface ProjectorDefinition<State = unknown, Family extends string = string, Events extends readonly EventDefinition[] = readonly EventDefinition[]> extends ProjectorLike {
46
+ readonly id: string;
47
+ readonly version: number;
48
+ readonly tag: TagFamily<Family>;
49
+ readonly [projectorFamilyInvariant]: (value: Family) => Family;
50
+ readonly events: Events;
51
+ readonly eventTypes: readonly string[];
52
+ readonly initialState: State | (() => State);
53
+ readonly handlers: Readonly<Record<string, ProjectorHandler<State, Events[number]>>>;
54
+ readonly apply: (state: State, event: EventRecord) => State;
55
+ readonly reduce: (state: State, event: EventRecord) => State;
56
+ readonly validateState: (state: unknown) => State;
57
+ readonly serializeState: (state: State) => string;
58
+ readonly deserializeState: (serialized: string) => State;
59
+ }
60
+ export type ProjectorOptions<State, Family extends string, Events extends readonly EventDefinition[]> = {
61
+ readonly id: string;
62
+ readonly version?: number;
63
+ readonly tag: TagFamily<Family>;
64
+ readonly state?: StateUnion<z.ZodType<State>>;
65
+ readonly source?: EventUnionLike<Events>;
66
+ readonly events?: Events;
67
+ readonly initialState?: State | (() => State);
68
+ readonly initial?: State | (() => State);
69
+ readonly handlers: Readonly<Record<string, ProjectorHandler<State, Events[number]>>>;
70
+ readonly serializeState?: (state: State) => string;
71
+ readonly deserializeState?: (serialized: string) => State;
72
+ };
73
+ export interface EventUnionLike<Events extends readonly EventDefinition[]> {
74
+ readonly events: Events;
75
+ readonly eventTypes: readonly string[];
76
+ }
77
+ export declare function projector<const State, const Family extends string, const Events extends readonly EventDefinition[]>(options: ProjectorOptions<State, Family, Events>): ProjectorDefinition<State, Family, Events>;
78
+ export declare function projectorInitialState<State>(definition: ProjectorDefinition<State>): State;
79
+ export declare function projectorFamily<Definition extends ProjectorDefinition>(definition: Definition): Definition["tag"];
80
+ export type ProjectorState<Definition extends ProjectorDefinition> = Definition extends ProjectorDefinition<infer State> ? State : never;
81
+ export {};