@kronos-ts/modelling 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,3 @@
1
+ export { type StateModule, type StateLifecycle, type IdSchema, type InferIdFromSchema, state, } from "./state.js";
2
+ export { type SourcingInfo, type LoadResult, type StateRepository, type StateManager, createStateManager, } from "./state-manager.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,GACN,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { state, } from "./state.js";
2
+ export { createStateManager, } from "./state-manager.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,GACN,MAAM,YAAY,CAAA;AAEnB,OAAO,EAKL,kBAAkB,GACnB,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,41 @@
1
+ import type { EventCriteria } from "@kronos-ts/messaging";
2
+ import type { StateModule } from "./state.js";
3
+ /**
4
+ * Metadata about what was sourced when loading state.
5
+ * Used by the framework to build append conditions.
6
+ */
7
+ export interface SourcingInfo {
8
+ readonly criteria: EventCriteria;
9
+ readonly markerPosition: bigint;
10
+ }
11
+ /**
12
+ * Result of loading state — the state plus sourcing metadata.
13
+ */
14
+ export interface LoadResult<S = unknown> {
15
+ readonly state: S;
16
+ readonly sourcingInfo: SourcingInfo;
17
+ }
18
+ /**
19
+ * A repository that knows how to load state of a specific state module
20
+ * by sourcing events from the event store and folding them through evolvers.
21
+ */
22
+ export interface StateRepository<Id = unknown, S = unknown> {
23
+ readonly stateName: string;
24
+ load(id: Id): Promise<LoadResult<S>>;
25
+ /**
26
+ * Load state, creating the initial state if no events exist.
27
+ * Unlike `load()`, this never fails for a new state — it returns
28
+ * the `create()` state with empty sourcing info.
29
+ */
30
+ loadOrCreate(id: Id): Promise<LoadResult<S>>;
31
+ }
32
+ /**
33
+ * Manages state repositories and provides the `load()` capability
34
+ * to command and event handlers.
35
+ */
36
+ export interface StateManager {
37
+ register<Id, S>(state: StateModule<Id, S>, repository: StateRepository<Id, S>): void;
38
+ load<Id, S>(state: StateModule<Id, S>, id: Id): Promise<LoadResult<S>>;
39
+ }
40
+ export declare function createStateManager(): StateManager;
41
+ //# sourceMappingURL=state-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../src/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;IACjB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,CAAC,EACZ,KAAK,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EACzB,UAAU,EAAE,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,GACjC,IAAI,CAAA;IAEP,IAAI,CAAC,EAAE,EAAE,CAAC,EACR,KAAK,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EACzB,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;CAC1B;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAyBjD"}
@@ -0,0 +1,17 @@
1
+ export function createStateManager() {
2
+ const repositories = new Map();
3
+ return {
4
+ register(state, repository) {
5
+ repositories.set(state.name, repository);
6
+ },
7
+ async load(state, id) {
8
+ const repo = repositories.get(state.name);
9
+ if (!repo) {
10
+ throw new Error(`No repository registered for state "${state.name}". ` +
11
+ `Make sure it is included in the states array.`);
12
+ }
13
+ return repo.load(id);
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=state-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../src/state-manager.ts"],"names":[],"mappings":"AAmDA,MAAM,UAAU,kBAAkB;IAChC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA2B,CAAA;IAEvD,OAAO;QACL,QAAQ,CACN,KAAyB,EACzB,UAAkC;YAElC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,UAA6B,CAAC,CAAA;QAC7D,CAAC;QAED,KAAK,CAAC,IAAI,CACR,KAAyB,EACzB,EAAM;YAEN,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACzC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CACb,uCAAuC,KAAK,CAAC,IAAI,KAAK;oBACtD,+CAA+C,CAChD,CAAA;YACH,CAAC;YACD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAA2B,CAAA;QAChD,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,87 @@
1
+ import type { z } from "zod";
2
+ import type { EventCriteria, EventMessage } from "@kronos-ts/messaging";
3
+ import type { EvolverRegistration } from "@kronos-ts/messaging";
4
+ /**
5
+ * A named record mapping field names to Zod schemas.
6
+ * Used to define state IDs with explicit field names.
7
+ *
8
+ * ```typescript
9
+ * // Simple ID
10
+ * { courseId: z.string() }
11
+ *
12
+ * // Composite ID
13
+ * { courseId: z.string(), studentId: z.string() }
14
+ * ```
15
+ */
16
+ export type IdSchema = Record<string, z.ZodType>;
17
+ /**
18
+ * Infers the runtime type from an ID schema record.
19
+ *
20
+ * `{ courseId: z.string() }` → `{ courseId: string }`
21
+ * `{ courseId: z.string(), studentId: z.string() }` → `{ courseId: string, studentId: string }`
22
+ */
23
+ export type InferIdFromSchema<T extends IdSchema> = {
24
+ [K in keyof T]: z.infer<T[K]>;
25
+ };
26
+ /**
27
+ * Lifecycle hooks for state transitions.
28
+ */
29
+ export interface StateLifecycle<Id = unknown, S = unknown> {
30
+ /** Called when the first event transitions from initial state. */
31
+ onCreate?: (state: S, id: Id) => void | Promise<void>;
32
+ /** Called when the state transitions to a deleted state. */
33
+ onDelete?: (state: S, id: Id) => void | Promise<void>;
34
+ /** Called after each evolver application when state changes. */
35
+ onStateChange?: (from: S, to: S, event: EventMessage, id: Id) => void | Promise<void>;
36
+ /** Predicate that detects deleted state. */
37
+ isDeleted?: (state: S) => boolean;
38
+ }
39
+ /**
40
+ * A state module — a self-contained definition of state sourced from events.
41
+ *
42
+ * The `Id` type is always a named record (e.g., `{ courseId: string }`),
43
+ * enforced at compile time by requiring an {@link IdSchema} definition.
44
+ * This ensures field names are always available for criteria, evolvers,
45
+ * and the initial function.
46
+ */
47
+ export interface StateModule<Id = unknown, S = unknown> {
48
+ readonly kind: "state-module";
49
+ readonly name: string;
50
+ /** The ID schema — maps field names to Zod types. */
51
+ readonly idSchema: IdSchema;
52
+ readonly create: (id: Id) => S;
53
+ readonly criteria: (id: Id) => EventCriteria;
54
+ readonly evolvers: ReadonlyArray<EvolverRegistration<S, any>>;
55
+ readonly lifecycle?: StateLifecycle<Id, S>;
56
+ }
57
+ /**
58
+ * Defines a state module — state sourced from events, scoped by an ID.
59
+ *
60
+ * The `id` parameter must be a named record mapping field names to Zod types.
61
+ * A bare Zod type (e.g., `z.string()`) will not compile — you must name
62
+ * the field (e.g., `{ courseId: z.string() }`).
63
+ *
64
+ * The state type is inferred from the `initial` function's return type —
65
+ * no separate type definition needed.
66
+ *
67
+ * ```typescript
68
+ * const Course = state({
69
+ * name: "Course",
70
+ * id: { courseId: z.string() },
71
+ * initial: () => ({ created: false, name: "", capacity: 0 }),
72
+ * criteria: (id) => EventCriteria.havingTags({ courseId: id.courseId }),
73
+ * evolve: [
74
+ * on(CourseCreated, (s, event) => ({ ...s, created: true })),
75
+ * ],
76
+ * })
77
+ * ```
78
+ */
79
+ export declare function state<IS extends IdSchema, S>(def: {
80
+ name: string;
81
+ id: IS;
82
+ initial: (id: InferIdFromSchema<IS>) => S;
83
+ criteria: (id: InferIdFromSchema<IS>) => EventCriteria;
84
+ evolve: Array<EvolverRegistration<S, any>>;
85
+ lifecycle?: StateLifecycle<InferIdFromSchema<IS>, S>;
86
+ }): StateModule<InferIdFromSchema<IS>, S>;
87
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,QAAQ,IAAI;KACjD,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC9B,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACvD,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrF,4CAA4C;IAC5C,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAA;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW,CAC1B,EAAE,GAAG,OAAO,EACZ,CAAC,GAAG,OAAO;IAEX,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,aAAa,CAAA;IAC5C,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,KAAK,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,EAAE,CAAA;IACN,OAAO,EAAE,CAAC,EAAE,EAAE,iBAAiB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAA;IACzC,QAAQ,EAAE,CAAC,EAAE,EAAE,iBAAiB,CAAC,EAAE,CAAC,KAAK,aAAa,CAAA;IACtD,MAAM,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IAC1C,SAAS,CAAC,EAAE,cAAc,CAAC,iBAAiB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CACrD,GAAG,WAAW,CAAC,iBAAiB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA"}
package/dist/state.js ADDED
@@ -0,0 +1,12 @@
1
+ export function state(def) {
2
+ return {
3
+ kind: "state-module",
4
+ name: def.name,
5
+ idSchema: def.id,
6
+ create: def.initial,
7
+ criteria: def.criteria,
8
+ evolvers: def.evolve,
9
+ lifecycle: def.lifecycle,
10
+ };
11
+ }
12
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AA+FA,MAAM,UAAU,KAAK,CAAyB,GAO7C;IACC,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,QAAQ,EAAE,GAAG,CAAC,EAAE;QAChB,MAAM,EAAE,GAAG,CAAC,OAAO;QACnB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,QAAQ,EAAE,GAAG,CAAC,MAAM;QACpB,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@kronos-ts/modelling",
3
+ "version": "0.1.0",
4
+ "description": "State modelling for Kronos — event-sourced state definitions and evolution.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "author": "Theo Emanuelsson",
8
+ "homepage": "https://github.com/KronosDB/kronos-ts/tree/main/packages/modelling#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/KronosDB/kronos-ts.git",
12
+ "directory": "packages/modelling"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/KronosDB/kronos-ts/issues"
16
+ },
17
+ "keywords": [
18
+ "kronos",
19
+ "event-sourcing",
20
+ "cqrs",
21
+ "dcb",
22
+ "typescript"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "src/index.ts",
26
+ "types": "src/index.ts",
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "!src/**/__tests__",
31
+ "!src/**/*.test.ts",
32
+ "!src/**/*.bench.ts"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "clean": "rm -rf dist *.tsbuildinfo"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "default": "./dist/index.js"
46
+ }
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "@kronos-ts/common": "workspace:*",
51
+ "@kronos-ts/messaging": "workspace:*",
52
+ "zod": "^4.3.6"
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export {
2
+ type StateModule,
3
+ type StateLifecycle,
4
+ type IdSchema,
5
+ type InferIdFromSchema,
6
+ state,
7
+ } from "./state.js"
8
+
9
+ export {
10
+ type SourcingInfo,
11
+ type LoadResult,
12
+ type StateRepository,
13
+ type StateManager,
14
+ createStateManager,
15
+ } from "./state-manager.js"
@@ -0,0 +1,77 @@
1
+ import type { EventCriteria } from "@kronos-ts/messaging"
2
+ import type { StateModule } from "./state.js"
3
+
4
+ /**
5
+ * Metadata about what was sourced when loading state.
6
+ * Used by the framework to build append conditions.
7
+ */
8
+ export interface SourcingInfo {
9
+ readonly criteria: EventCriteria
10
+ readonly markerPosition: bigint
11
+ }
12
+
13
+ /**
14
+ * Result of loading state — the state plus sourcing metadata.
15
+ */
16
+ export interface LoadResult<S = unknown> {
17
+ readonly state: S
18
+ readonly sourcingInfo: SourcingInfo
19
+ }
20
+
21
+ /**
22
+ * A repository that knows how to load state of a specific state module
23
+ * by sourcing events from the event store and folding them through evolvers.
24
+ */
25
+ export interface StateRepository<Id = unknown, S = unknown> {
26
+ readonly stateName: string
27
+ load(id: Id): Promise<LoadResult<S>>
28
+ /**
29
+ * Load state, creating the initial state if no events exist.
30
+ * Unlike `load()`, this never fails for a new state — it returns
31
+ * the `create()` state with empty sourcing info.
32
+ */
33
+ loadOrCreate(id: Id): Promise<LoadResult<S>>
34
+ }
35
+
36
+ /**
37
+ * Manages state repositories and provides the `load()` capability
38
+ * to command and event handlers.
39
+ */
40
+ export interface StateManager {
41
+ register<Id, S>(
42
+ state: StateModule<Id, S>,
43
+ repository: StateRepository<Id, S>,
44
+ ): void
45
+
46
+ load<Id, S>(
47
+ state: StateModule<Id, S>,
48
+ id: Id,
49
+ ): Promise<LoadResult<S>>
50
+ }
51
+
52
+ export function createStateManager(): StateManager {
53
+ const repositories = new Map<string, StateRepository>()
54
+
55
+ return {
56
+ register<Id, S>(
57
+ state: StateModule<Id, S>,
58
+ repository: StateRepository<Id, S>,
59
+ ): void {
60
+ repositories.set(state.name, repository as StateRepository)
61
+ },
62
+
63
+ async load<Id, S>(
64
+ state: StateModule<Id, S>,
65
+ id: Id,
66
+ ): Promise<LoadResult<S>> {
67
+ const repo = repositories.get(state.name)
68
+ if (!repo) {
69
+ throw new Error(
70
+ `No repository registered for state "${state.name}". ` +
71
+ `Make sure it is included in the states array.`,
72
+ )
73
+ }
74
+ return repo.load(id) as Promise<LoadResult<S>>
75
+ },
76
+ }
77
+ }
package/src/state.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type { z } from "zod"
2
+ import type { EventCriteria, EventMessage } from "@kronos-ts/messaging"
3
+ import type { EvolverRegistration } from "@kronos-ts/messaging"
4
+
5
+ /**
6
+ * A named record mapping field names to Zod schemas.
7
+ * Used to define state IDs with explicit field names.
8
+ *
9
+ * ```typescript
10
+ * // Simple ID
11
+ * { courseId: z.string() }
12
+ *
13
+ * // Composite ID
14
+ * { courseId: z.string(), studentId: z.string() }
15
+ * ```
16
+ */
17
+ export type IdSchema = Record<string, z.ZodType>
18
+
19
+ /**
20
+ * Infers the runtime type from an ID schema record.
21
+ *
22
+ * `{ courseId: z.string() }` → `{ courseId: string }`
23
+ * `{ courseId: z.string(), studentId: z.string() }` → `{ courseId: string, studentId: string }`
24
+ */
25
+ export type InferIdFromSchema<T extends IdSchema> = {
26
+ [K in keyof T]: z.infer<T[K]>
27
+ }
28
+
29
+ /**
30
+ * Lifecycle hooks for state transitions.
31
+ */
32
+ export interface StateLifecycle<Id = unknown, S = unknown> {
33
+ /** Called when the first event transitions from initial state. */
34
+ onCreate?: (state: S, id: Id) => void | Promise<void>
35
+ /** Called when the state transitions to a deleted state. */
36
+ onDelete?: (state: S, id: Id) => void | Promise<void>
37
+ /** Called after each evolver application when state changes. */
38
+ onStateChange?: (from: S, to: S, event: EventMessage, id: Id) => void | Promise<void>
39
+ /** Predicate that detects deleted state. */
40
+ isDeleted?: (state: S) => boolean
41
+ }
42
+
43
+ /**
44
+ * A state module — a self-contained definition of state sourced from events.
45
+ *
46
+ * The `Id` type is always a named record (e.g., `{ courseId: string }`),
47
+ * enforced at compile time by requiring an {@link IdSchema} definition.
48
+ * This ensures field names are always available for criteria, evolvers,
49
+ * and the initial function.
50
+ */
51
+ export interface StateModule<
52
+ Id = unknown,
53
+ S = unknown,
54
+ > {
55
+ readonly kind: "state-module"
56
+ readonly name: string
57
+ /** The ID schema — maps field names to Zod types. */
58
+ readonly idSchema: IdSchema
59
+ readonly create: (id: Id) => S
60
+ readonly criteria: (id: Id) => EventCriteria
61
+ readonly evolvers: ReadonlyArray<EvolverRegistration<S, any>>
62
+ readonly lifecycle?: StateLifecycle<Id, S>
63
+ }
64
+
65
+ /**
66
+ * Defines a state module — state sourced from events, scoped by an ID.
67
+ *
68
+ * The `id` parameter must be a named record mapping field names to Zod types.
69
+ * A bare Zod type (e.g., `z.string()`) will not compile — you must name
70
+ * the field (e.g., `{ courseId: z.string() }`).
71
+ *
72
+ * The state type is inferred from the `initial` function's return type —
73
+ * no separate type definition needed.
74
+ *
75
+ * ```typescript
76
+ * const Course = state({
77
+ * name: "Course",
78
+ * id: { courseId: z.string() },
79
+ * initial: () => ({ created: false, name: "", capacity: 0 }),
80
+ * criteria: (id) => EventCriteria.havingTags({ courseId: id.courseId }),
81
+ * evolve: [
82
+ * on(CourseCreated, (s, event) => ({ ...s, created: true })),
83
+ * ],
84
+ * })
85
+ * ```
86
+ */
87
+ export function state<IS extends IdSchema, S>(def: {
88
+ name: string
89
+ id: IS
90
+ initial: (id: InferIdFromSchema<IS>) => S
91
+ criteria: (id: InferIdFromSchema<IS>) => EventCriteria
92
+ evolve: Array<EvolverRegistration<S, any>>
93
+ lifecycle?: StateLifecycle<InferIdFromSchema<IS>, S>
94
+ }): StateModule<InferIdFromSchema<IS>, S>
95
+
96
+ export function state<IS extends IdSchema, S>(def: {
97
+ name: string
98
+ id: IS
99
+ initial: (id: InferIdFromSchema<IS>) => S
100
+ criteria: (id: InferIdFromSchema<IS>) => EventCriteria
101
+ evolve: Array<EvolverRegistration<S, any>>
102
+ lifecycle?: StateLifecycle<InferIdFromSchema<IS>, S>
103
+ }): StateModule<InferIdFromSchema<IS>, S> {
104
+ return {
105
+ kind: "state-module",
106
+ name: def.name,
107
+ idSchema: def.id,
108
+ create: def.initial,
109
+ criteria: def.criteria,
110
+ evolvers: def.evolve,
111
+ lifecycle: def.lifecycle,
112
+ }
113
+ }