@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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/state-manager.d.ts +41 -0
- package/dist/state-manager.d.ts.map +1 -0
- package/dist/state-manager.js +17 -0
- package/dist/state-manager.js.map +1 -0
- package/dist/state.d.ts +87 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +12 -0
- package/dist/state.js.map +1 -0
- package/package.json +54 -0
- package/src/index.ts +15 -0
- package/src/state-manager.ts +77 -0
- package/src/state.ts +113 -0
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/state.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|