@directive-run/core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/adapter-utils.cjs +2 -0
- package/dist/adapter-utils.cjs.map +1 -0
- package/dist/adapter-utils.d.cts +230 -0
- package/dist/adapter-utils.d.ts +230 -0
- package/dist/adapter-utils.js +2 -0
- package/dist/adapter-utils.js.map +1 -0
- package/dist/index.cjs +35 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2016 -0
- package/dist/index.d.ts +2016 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/migration.cjs +25 -0
- package/dist/migration.cjs.map +1 -0
- package/dist/migration.d.cts +109 -0
- package/dist/migration.d.ts +109 -0
- package/dist/migration.js +25 -0
- package/dist/migration.js.map +1 -0
- package/dist/plugins/index.cjs +3 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +697 -0
- package/dist/plugins/index.d.ts +697 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins-CcwEXXMS.d.cts +1876 -0
- package/dist/plugins-CcwEXXMS.d.ts +1876 -0
- package/dist/testing.cjs +12 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +235 -0
- package/dist/testing.d.ts +235 -0
- package/dist/testing.js +12 -0
- package/dist/testing.js.map +1 -0
- package/dist/utils-4JrY5fk9.d.cts +198 -0
- package/dist/utils-4JrY5fk9.d.ts +198 -0
- package/dist/worker.cjs +12 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.cts +241 -0
- package/dist/worker.d.ts +241 -0
- package/dist/worker.js +12 -0
- package/dist/worker.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,1876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Types - Type definitions for fact schemas and consolidated module schemas
|
|
3
|
+
*/
|
|
4
|
+
/** Primitive type definitions for schema */
|
|
5
|
+
interface SchemaType<T> {
|
|
6
|
+
readonly _type: T;
|
|
7
|
+
readonly _validators: Array<(value: any) => boolean>;
|
|
8
|
+
validate(fn: (value: T) => boolean): SchemaType<T>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Schema definition mapping keys to types.
|
|
12
|
+
* Supports both:
|
|
13
|
+
* - Schema builders: `{ count: t.number() }`
|
|
14
|
+
* - Type assertions: `{} as { count: number }`
|
|
15
|
+
*/
|
|
16
|
+
type Schema = Record<string, SchemaType<unknown> | unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Infer a single type from a SchemaType, Zod schema, or plain type.
|
|
19
|
+
* - If it has `_type` (our SchemaType), extract it
|
|
20
|
+
* - If it has `_output` (Zod schema), extract it
|
|
21
|
+
* - Otherwise use the type directly (type assertion)
|
|
22
|
+
*/
|
|
23
|
+
type InferSchemaType<T> = T extends SchemaType<infer U> ? U : T extends {
|
|
24
|
+
_output: infer Z;
|
|
25
|
+
} ? Z : T;
|
|
26
|
+
/** Extract the TypeScript type from a schema (removes readonly from const type params) */
|
|
27
|
+
type InferSchema<S extends Schema> = {
|
|
28
|
+
-readonly [K in keyof S]: InferSchemaType<S[K]>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Event payload schema - maps property names to their types.
|
|
32
|
+
* Empty object `{}` means no payload.
|
|
33
|
+
* Supports both `t.*()` builders and plain types.
|
|
34
|
+
*/
|
|
35
|
+
type EventPayloadSchema = Record<string, SchemaType<unknown> | unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Events schema - maps event names to their payload schemas.
|
|
38
|
+
* Supports type assertion: `{} as { eventName: { prop: Type } }`
|
|
39
|
+
*/
|
|
40
|
+
type EventsSchema = Record<string, EventPayloadSchema>;
|
|
41
|
+
/**
|
|
42
|
+
* Derivations schema - maps derivation names to their return types.
|
|
43
|
+
* Supports both:
|
|
44
|
+
* - `{ doubled: t.number() }`
|
|
45
|
+
* - `{} as { doubled: number }`
|
|
46
|
+
*/
|
|
47
|
+
type DerivationsSchema = Record<string, SchemaType<unknown> | unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* Requirement payload schema - maps property names to their types.
|
|
50
|
+
* Supports both `t.*()` builders and plain types.
|
|
51
|
+
*/
|
|
52
|
+
type RequirementPayloadSchema$1 = Record<string, SchemaType<unknown> | unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Requirements schema - maps requirement type names to their payload schemas.
|
|
55
|
+
* Supports type assertion: `{} as { REQ_NAME: { prop: Type } }`
|
|
56
|
+
*/
|
|
57
|
+
type RequirementsSchema$1 = Record<string, RequirementPayloadSchema$1>;
|
|
58
|
+
/**
|
|
59
|
+
* Consolidated module schema - single source of truth for all types.
|
|
60
|
+
*
|
|
61
|
+
* Only `facts` is required. Other sections default to empty:
|
|
62
|
+
* - `derivations` - Omit if no computed values
|
|
63
|
+
* - `events` - Omit if no event handlers
|
|
64
|
+
* - `requirements` - Omit if no constraints/resolvers
|
|
65
|
+
*
|
|
66
|
+
* Supports two patterns for defining types:
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // Pattern 1: Schema builders (with optional runtime validation)
|
|
71
|
+
* createModule("counter", {
|
|
72
|
+
* schema: {
|
|
73
|
+
* facts: { count: t.number(), phase: t.string<"a" | "b">() },
|
|
74
|
+
* derivations: { doubled: t.number() },
|
|
75
|
+
* events: { increment: {}, setPhase: { phase: t.string<"a" | "b">() } },
|
|
76
|
+
* requirements: { FETCH: { id: t.string() } },
|
|
77
|
+
* },
|
|
78
|
+
* // ...
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* // Pattern 2: Type assertions (type-only, no validation)
|
|
82
|
+
* createModule("counter", {
|
|
83
|
+
* schema: {
|
|
84
|
+
* facts: {} as { count: number; phase: "a" | "b" },
|
|
85
|
+
* derivations: {} as { doubled: number },
|
|
86
|
+
* events: {} as { increment: {}; setPhase: { phase: "a" | "b" } },
|
|
87
|
+
* requirements: {} as { FETCH: { id: string } },
|
|
88
|
+
* },
|
|
89
|
+
* // ...
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
interface ModuleSchema {
|
|
94
|
+
/** Facts (state) schema - required */
|
|
95
|
+
facts: Schema;
|
|
96
|
+
/** Derivation return types - optional, defaults to {} */
|
|
97
|
+
derivations?: DerivationsSchema;
|
|
98
|
+
/** Event payload schemas - optional, defaults to {} */
|
|
99
|
+
events?: EventsSchema;
|
|
100
|
+
/** Requirement payload schemas - optional, defaults to {} */
|
|
101
|
+
requirements?: RequirementsSchema$1;
|
|
102
|
+
}
|
|
103
|
+
/** Helper to get derivations, defaulting to empty */
|
|
104
|
+
type GetDerivations<M extends ModuleSchema> = M["derivations"] extends DerivationsSchema ? M["derivations"] : Record<string, never>;
|
|
105
|
+
/** Helper to get events, defaulting to empty */
|
|
106
|
+
type GetEvents<M extends ModuleSchema> = M["events"] extends EventsSchema ? M["events"] : Record<string, never>;
|
|
107
|
+
/** Helper to get requirements, defaulting to empty */
|
|
108
|
+
type GetRequirements<M extends ModuleSchema> = M["requirements"] extends RequirementsSchema$1 ? M["requirements"] : Record<string, never>;
|
|
109
|
+
/**
|
|
110
|
+
* Infer the facts type from a module schema.
|
|
111
|
+
*/
|
|
112
|
+
type InferFacts<M extends ModuleSchema> = InferSchema<M["facts"]>;
|
|
113
|
+
/**
|
|
114
|
+
* Infer derivation values from a module schema.
|
|
115
|
+
* Each key maps to the return type declared in schema.derivations.
|
|
116
|
+
*/
|
|
117
|
+
type InferDerivations<M extends ModuleSchema> = {
|
|
118
|
+
readonly [K in keyof GetDerivations<M>]: InferSchemaType<GetDerivations<M>[K]>;
|
|
119
|
+
};
|
|
120
|
+
/** Combined facts + derivations — matches the useSelector proxy at runtime. */
|
|
121
|
+
type InferSelectorState<M extends ModuleSchema> = InferFacts<M> & InferDerivations<M>;
|
|
122
|
+
/**
|
|
123
|
+
* Infer event payload type from an event payload schema.
|
|
124
|
+
*/
|
|
125
|
+
type InferEventPayloadFromSchema<P extends EventPayloadSchema> = {
|
|
126
|
+
[K in keyof P]: InferSchemaType<P[K]>;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Infer all events from a module schema as a discriminated union.
|
|
130
|
+
*/
|
|
131
|
+
type InferEvents<M extends ModuleSchema> = {
|
|
132
|
+
[K in keyof GetEvents<M>]: keyof GetEvents<M>[K] extends never ? {
|
|
133
|
+
type: K;
|
|
134
|
+
} : {
|
|
135
|
+
type: K;
|
|
136
|
+
} & InferEventPayloadFromSchema<GetEvents<M>[K]>;
|
|
137
|
+
}[keyof GetEvents<M>];
|
|
138
|
+
/**
|
|
139
|
+
* Infer requirement payload type from a requirement payload schema.
|
|
140
|
+
*/
|
|
141
|
+
type InferRequirementPayloadFromSchema<P extends RequirementPayloadSchema$1> = {
|
|
142
|
+
[K in keyof P]: InferSchemaType<P[K]>;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Infer all requirements from a module schema as a discriminated union.
|
|
146
|
+
*/
|
|
147
|
+
type InferRequirements<M extends ModuleSchema> = {
|
|
148
|
+
[K in keyof GetRequirements<M>]: {
|
|
149
|
+
type: K;
|
|
150
|
+
} & InferRequirementPayloadFromSchema<GetRequirements<M>[K]>;
|
|
151
|
+
}[keyof GetRequirements<M>];
|
|
152
|
+
/**
|
|
153
|
+
* Infer requirement type names from a module schema.
|
|
154
|
+
*/
|
|
155
|
+
type InferRequirementTypes<M extends ModuleSchema> = keyof GetRequirements<M> & string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Facts Types - Type definitions for facts store and accessor
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/** Read-only snapshot of facts */
|
|
162
|
+
interface FactsSnapshot<S extends Schema = Schema> {
|
|
163
|
+
get<K extends keyof InferSchema<S>>(key: K): InferSchema<S>[K] | undefined;
|
|
164
|
+
has(key: keyof InferSchema<S>): boolean;
|
|
165
|
+
}
|
|
166
|
+
/** Mutable facts store */
|
|
167
|
+
interface FactsStore<S extends Schema = Schema> extends FactsSnapshot<S> {
|
|
168
|
+
set<K extends keyof InferSchema<S>>(key: K, value: InferSchema<S>[K]): void;
|
|
169
|
+
delete(key: keyof InferSchema<S>): void;
|
|
170
|
+
batch(fn: () => void): void;
|
|
171
|
+
subscribe(keys: Array<keyof InferSchema<S>>, listener: () => void): () => void;
|
|
172
|
+
subscribeAll(listener: () => void): () => void;
|
|
173
|
+
/** Get all facts as a plain object (for serialization/time-travel) */
|
|
174
|
+
toObject(): Record<string, unknown>;
|
|
175
|
+
}
|
|
176
|
+
/** Proxy-based facts accessor (cleaner API) */
|
|
177
|
+
type Facts<S extends Schema = Schema> = InferSchema<S> & {
|
|
178
|
+
readonly $store: FactsStore<S>;
|
|
179
|
+
readonly $snapshot: () => FactsSnapshot<S>;
|
|
180
|
+
};
|
|
181
|
+
/** Fact change record */
|
|
182
|
+
interface FactChange {
|
|
183
|
+
key: string;
|
|
184
|
+
value: unknown;
|
|
185
|
+
prev: unknown;
|
|
186
|
+
type: "set" | "delete";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Requirement Types - Type definitions for requirements and constraints
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
/** Base requirement structure */
|
|
194
|
+
interface Requirement {
|
|
195
|
+
readonly type: string;
|
|
196
|
+
readonly [key: string]: unknown;
|
|
197
|
+
}
|
|
198
|
+
/** Requirement with computed identity */
|
|
199
|
+
interface RequirementWithId {
|
|
200
|
+
readonly requirement: Requirement;
|
|
201
|
+
readonly id: string;
|
|
202
|
+
readonly fromConstraint: string;
|
|
203
|
+
}
|
|
204
|
+
/** Requirement key function for custom deduplication */
|
|
205
|
+
type RequirementKeyFn<R extends Requirement = Requirement> = (req: R) => string;
|
|
206
|
+
/**
|
|
207
|
+
* Requirement payload schema - maps property names to their types.
|
|
208
|
+
*/
|
|
209
|
+
type RequirementPayloadSchema = Record<string, SchemaType<unknown>>;
|
|
210
|
+
/**
|
|
211
|
+
* Requirements schema definition - maps requirement type names to their payload schemas.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const module = createModule("inventory", {
|
|
216
|
+
* requirements: {
|
|
217
|
+
* RESTOCK: { sku: t.string(), quantity: t.number() },
|
|
218
|
+
* ALERT: { message: t.string() },
|
|
219
|
+
* },
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
type RequirementsSchema = Record<string, RequirementPayloadSchema>;
|
|
224
|
+
/**
|
|
225
|
+
* Requirement output from a constraint - can be single, array, or null.
|
|
226
|
+
* - Single requirement: `{ type: "RESTOCK", sku: "ABC" }`
|
|
227
|
+
* - Multiple requirements: `[{ type: "RESTOCK", sku: "ABC" }, { type: "NOTIFY", message: "Low stock" }]`
|
|
228
|
+
* - No requirements: `null` or `[]`
|
|
229
|
+
*/
|
|
230
|
+
type RequirementOutput$1<R extends Requirement = Requirement> = R | R[] | null;
|
|
231
|
+
/** Constraint definition */
|
|
232
|
+
interface ConstraintDef<S extends Schema, R extends Requirement = Requirement> {
|
|
233
|
+
/** Priority for ordering (higher runs first) */
|
|
234
|
+
priority?: number;
|
|
235
|
+
/** Mark this constraint as async (avoids runtime detection) */
|
|
236
|
+
async?: boolean;
|
|
237
|
+
/** Condition function (sync or async) */
|
|
238
|
+
when: (facts: Facts<S>) => boolean | Promise<boolean>;
|
|
239
|
+
/**
|
|
240
|
+
* Requirement(s) to produce when condition is met.
|
|
241
|
+
* - Single requirement: `{ type: "RESTOCK", sku: "ABC" }`
|
|
242
|
+
* - Multiple requirements: `[{ type: "RESTOCK", sku: "ABC" }, { type: "NOTIFY", message: "Low" }]`
|
|
243
|
+
* - Function returning requirements: `(facts) => ({ type: "RESTOCK", sku: facts.sku })`
|
|
244
|
+
* - Function returning null/empty array for conditional no-op: `(facts) => facts.critical ? [...] : null`
|
|
245
|
+
*/
|
|
246
|
+
require: RequirementOutput$1<R> | ((facts: Facts<S>) => RequirementOutput$1<R>);
|
|
247
|
+
/** Timeout for async constraints (ms) */
|
|
248
|
+
timeout?: number;
|
|
249
|
+
/**
|
|
250
|
+
* Constraint IDs whose resolvers must complete before this constraint is evaluated.
|
|
251
|
+
* If a dependency's `when()` returns false (no requirements), this constraint proceeds.
|
|
252
|
+
* If a dependency's resolver fails, this constraint remains blocked.
|
|
253
|
+
* Cross-module: use "moduleName.constraintName" format.
|
|
254
|
+
*/
|
|
255
|
+
after?: string[];
|
|
256
|
+
/**
|
|
257
|
+
* Explicit fact dependencies for this constraint.
|
|
258
|
+
* Required for async constraints to enable dependency tracking (auto-tracking
|
|
259
|
+
* cannot work across async boundaries). Also works for sync constraints to
|
|
260
|
+
* bypass auto-tracking overhead.
|
|
261
|
+
*/
|
|
262
|
+
deps?: string[];
|
|
263
|
+
}
|
|
264
|
+
/** Map of constraint definitions (generic) */
|
|
265
|
+
type ConstraintsDef<S extends Schema> = Record<string, ConstraintDef<S, Requirement>>;
|
|
266
|
+
/** Internal constraint state */
|
|
267
|
+
interface ConstraintState {
|
|
268
|
+
id: string;
|
|
269
|
+
priority: number;
|
|
270
|
+
isAsync: boolean;
|
|
271
|
+
lastResult: boolean | null;
|
|
272
|
+
isEvaluating: boolean;
|
|
273
|
+
error: Error | null;
|
|
274
|
+
/** Timestamp when this constraint's resolver(s) last completed successfully */
|
|
275
|
+
lastResolvedAt: number | null;
|
|
276
|
+
/** Constraint IDs this constraint is waiting on (from `after` property) */
|
|
277
|
+
after: string[];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Effect Types - Type definitions for effects
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A cleanup function returned by an effect's `run()`.
|
|
286
|
+
* Called before the effect re-runs (when deps change) or when the system stops/destroys.
|
|
287
|
+
* Use for teardown: closing WebSocket connections, clearing intervals, removing DOM listeners, etc.
|
|
288
|
+
*/
|
|
289
|
+
type EffectCleanup = () => void;
|
|
290
|
+
/**
|
|
291
|
+
* Effect definition - side effects with optional cleanup.
|
|
292
|
+
*
|
|
293
|
+
* ## Effects vs Constraints
|
|
294
|
+
*
|
|
295
|
+
* Use **Effects** for:
|
|
296
|
+
* - Logging and analytics
|
|
297
|
+
* - DOM manipulation (scrolling, focus)
|
|
298
|
+
* - External notifications (toasts, alerts)
|
|
299
|
+
* - Syncing to localStorage/sessionStorage
|
|
300
|
+
* - WebSocket connections, intervals, DOM listeners (return cleanup)
|
|
301
|
+
* - Any side effect that doesn't need tracking or retry
|
|
302
|
+
*
|
|
303
|
+
* Use **Constraints** for:
|
|
304
|
+
* - Data fetching (API calls)
|
|
305
|
+
* - Async operations that may fail and need retry
|
|
306
|
+
* - Operations that produce requirements to be resolved
|
|
307
|
+
* - Anything that needs cancellation support
|
|
308
|
+
* - Operations where you need to know completion status
|
|
309
|
+
*
|
|
310
|
+
* Key differences:
|
|
311
|
+
* - Effects run and are forgotten - no retry, no cancellation, no status tracking
|
|
312
|
+
* - Constraints produce requirements that resolvers fulfill with full lifecycle management
|
|
313
|
+
* - Effects are synchronous in the reconciliation loop
|
|
314
|
+
* - Constraints/resolvers can be async with timeout, retry, and batching
|
|
315
|
+
*
|
|
316
|
+
* ## Cleanup
|
|
317
|
+
*
|
|
318
|
+
* Return a cleanup function from `run()` to tear down resources:
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* effects: {
|
|
323
|
+
* websocket: {
|
|
324
|
+
* deps: ["userId"],
|
|
325
|
+
* run: (facts) => {
|
|
326
|
+
* const ws = new WebSocket(`/ws/${facts.userId}`);
|
|
327
|
+
* return () => ws.close(); // Cleanup when userId changes or system stops
|
|
328
|
+
* },
|
|
329
|
+
* },
|
|
330
|
+
* interval: {
|
|
331
|
+
* run: (facts) => {
|
|
332
|
+
* const id = setInterval(() => sync(facts), 5000);
|
|
333
|
+
* return () => clearInterval(id);
|
|
334
|
+
* },
|
|
335
|
+
* },
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
interface EffectDef<S extends Schema> {
|
|
340
|
+
run(facts: Facts<S>, prev: FactsSnapshot<S> | null): void | EffectCleanup | Promise<void | EffectCleanup>;
|
|
341
|
+
/** Optional explicit dependencies for optimization */
|
|
342
|
+
deps?: Array<keyof InferSchema<S>>;
|
|
343
|
+
}
|
|
344
|
+
/** Map of effect definitions */
|
|
345
|
+
type EffectsDef<S extends Schema> = Record<string, EffectDef<S>>;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Event Types - Type definitions for events and event handlers
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
/** Helper to get events schema, defaulting to empty */
|
|
352
|
+
type GetEventsSchema$1<M extends ModuleSchema> = M["events"] extends EventsSchema ? M["events"] : Record<string, never>;
|
|
353
|
+
/**
|
|
354
|
+
* Events accessor type from a module schema.
|
|
355
|
+
* Provides typed dispatch functions for each event.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* type Accessor = EventsAccessorFromSchema<MySchema>;
|
|
360
|
+
* // {
|
|
361
|
+
* // increment: () => void;
|
|
362
|
+
* // setPhase: (payload: { phase: "red" | "green" }) => void;
|
|
363
|
+
* // }
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
type EventsAccessorFromSchema<M extends ModuleSchema> = {
|
|
367
|
+
readonly [K in keyof GetEventsSchema$1<M>]: keyof GetEventsSchema$1<M>[K] extends never ? () => void : (payload: InferEventPayloadFromSchema<GetEventsSchema$1<M>[K]>) => void;
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Dispatch events union type from a module schema.
|
|
371
|
+
* Used for system.dispatch() type.
|
|
372
|
+
*/
|
|
373
|
+
type DispatchEventsFromSchema<M extends ModuleSchema> = InferEvents<M>;
|
|
374
|
+
/** System event */
|
|
375
|
+
interface SystemEvent {
|
|
376
|
+
type: string;
|
|
377
|
+
[key: string]: unknown;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Flexible event handler that accepts either:
|
|
381
|
+
* - Simple handler: `(facts) => void`
|
|
382
|
+
* - Typed payload handler: `(facts, { field }: { field: Type }) => void`
|
|
383
|
+
* - Generic handler: `(facts, event: SystemEvent) => void`
|
|
384
|
+
*/
|
|
385
|
+
type FlexibleEventHandler<S extends Schema> = (facts: Facts<S>, event?: any) => void;
|
|
386
|
+
/** Events definition - accepts any event handler signature */
|
|
387
|
+
type EventsDef<S extends Schema> = Record<string, FlexibleEventHandler<S>>;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolver Types - Type definitions for resolvers
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
/** Retry policy configuration */
|
|
394
|
+
interface RetryPolicy$1 {
|
|
395
|
+
/** Maximum number of attempts */
|
|
396
|
+
attempts: number;
|
|
397
|
+
/** Backoff strategy */
|
|
398
|
+
backoff: "none" | "linear" | "exponential";
|
|
399
|
+
/** Initial delay in ms */
|
|
400
|
+
initialDelay?: number;
|
|
401
|
+
/** Maximum delay in ms */
|
|
402
|
+
maxDelay?: number;
|
|
403
|
+
/**
|
|
404
|
+
* Optional predicate to decide whether to retry after an error.
|
|
405
|
+
* Return `true` to retry, `false` to stop immediately.
|
|
406
|
+
* If omitted, all errors are retried (up to `attempts`).
|
|
407
|
+
*
|
|
408
|
+
* @param error - The error that occurred
|
|
409
|
+
* @param attempt - The attempt number that just failed (1-based)
|
|
410
|
+
*/
|
|
411
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
412
|
+
}
|
|
413
|
+
/** Batch configuration */
|
|
414
|
+
interface BatchConfig {
|
|
415
|
+
/** Enable batching */
|
|
416
|
+
enabled: boolean;
|
|
417
|
+
/** Time window to collect requirements (ms) */
|
|
418
|
+
windowMs: number;
|
|
419
|
+
/** Maximum batch size (default: unlimited) */
|
|
420
|
+
maxSize?: number;
|
|
421
|
+
/** Per-batch timeout in ms (overrides resolver timeout for batches) */
|
|
422
|
+
timeoutMs?: number;
|
|
423
|
+
/**
|
|
424
|
+
* Failure strategy for partial batch failures:
|
|
425
|
+
* - "all-or-nothing" (default): If resolveBatch throws, all requirements fail
|
|
426
|
+
* - "per-item": Use resolveBatchWithResults to get per-item results
|
|
427
|
+
*/
|
|
428
|
+
failureStrategy?: "all-or-nothing" | "per-item";
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Result for a single item in a batch resolution.
|
|
432
|
+
*/
|
|
433
|
+
interface BatchItemResult<T = unknown> {
|
|
434
|
+
/** Whether this item succeeded */
|
|
435
|
+
success: boolean;
|
|
436
|
+
/** Error if the item failed */
|
|
437
|
+
error?: Error;
|
|
438
|
+
/** Optional result value if the item succeeded */
|
|
439
|
+
value?: T;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Results from batch resolution with per-item status.
|
|
443
|
+
* The array order must match the order of requirements passed in.
|
|
444
|
+
*/
|
|
445
|
+
type BatchResolveResults<T = unknown> = Array<BatchItemResult<T>>;
|
|
446
|
+
/** Resolver context passed to resolve function */
|
|
447
|
+
interface ResolverContext<S extends Schema = Schema> {
|
|
448
|
+
readonly facts: Facts<S>;
|
|
449
|
+
readonly signal: AbortSignal;
|
|
450
|
+
readonly snapshot: () => FactsSnapshot<S>;
|
|
451
|
+
}
|
|
452
|
+
/** Single resolver definition (untyped - use TypedResolversDef for type safety) */
|
|
453
|
+
interface ResolverDef<S extends Schema, R extends Requirement = Requirement> {
|
|
454
|
+
/**
|
|
455
|
+
* Requirement type to handle.
|
|
456
|
+
* - String: matches `req.type` directly (e.g., `requirement: "FETCH_USER"`)
|
|
457
|
+
* - Function: type guard predicate (e.g., `requirement: (req) => req.type === "FETCH_USER"`)
|
|
458
|
+
*/
|
|
459
|
+
requirement: string | ((req: Requirement) => req is R);
|
|
460
|
+
/** Custom key function for deduplication */
|
|
461
|
+
key?: RequirementKeyFn<R>;
|
|
462
|
+
/** Retry policy */
|
|
463
|
+
retry?: RetryPolicy$1;
|
|
464
|
+
/** Timeout for resolver execution (ms) */
|
|
465
|
+
timeout?: number;
|
|
466
|
+
/** Batch configuration (mutually exclusive with regular resolve) */
|
|
467
|
+
batch?: BatchConfig;
|
|
468
|
+
/** Resolve function for single requirement */
|
|
469
|
+
resolve?: (req: R, ctx: ResolverContext<S>) => Promise<void>;
|
|
470
|
+
/**
|
|
471
|
+
* Resolve function for batched requirements (all-or-nothing).
|
|
472
|
+
* If this throws, all requirements in the batch fail.
|
|
473
|
+
*/
|
|
474
|
+
resolveBatch?: (reqs: R[], ctx: ResolverContext<S>) => Promise<void>;
|
|
475
|
+
/**
|
|
476
|
+
* Resolve function for batched requirements with per-item results.
|
|
477
|
+
* Use this when you need to handle partial failures.
|
|
478
|
+
* The returned array must match the order of input requirements.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* resolveBatchWithResults: async (reqs, ctx) => {
|
|
483
|
+
* return Promise.all(reqs.map(async (req) => {
|
|
484
|
+
* try {
|
|
485
|
+
* await processItem(req);
|
|
486
|
+
* return { success: true };
|
|
487
|
+
* } catch (error) {
|
|
488
|
+
* return { success: false, error };
|
|
489
|
+
* }
|
|
490
|
+
* }));
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
resolveBatchWithResults?: (reqs: R[], ctx: ResolverContext<S>) => Promise<BatchResolveResults>;
|
|
495
|
+
}
|
|
496
|
+
/** Map of resolver definitions */
|
|
497
|
+
type ResolversDef<S extends Schema> = Record<string, ResolverDef<S, Requirement>>;
|
|
498
|
+
/** Resolver status */
|
|
499
|
+
type ResolverStatus = {
|
|
500
|
+
state: "idle";
|
|
501
|
+
} | {
|
|
502
|
+
state: "pending";
|
|
503
|
+
requirementId: string;
|
|
504
|
+
startedAt: number;
|
|
505
|
+
} | {
|
|
506
|
+
state: "running";
|
|
507
|
+
requirementId: string;
|
|
508
|
+
startedAt: number;
|
|
509
|
+
attempt: number;
|
|
510
|
+
} | {
|
|
511
|
+
state: "success";
|
|
512
|
+
requirementId: string;
|
|
513
|
+
completedAt: number;
|
|
514
|
+
duration: number;
|
|
515
|
+
} | {
|
|
516
|
+
state: "error";
|
|
517
|
+
requirementId: string;
|
|
518
|
+
error: Error;
|
|
519
|
+
failedAt: number;
|
|
520
|
+
attempts: number;
|
|
521
|
+
} | {
|
|
522
|
+
state: "canceled";
|
|
523
|
+
requirementId: string;
|
|
524
|
+
canceledAt: number;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Error Types - Type definitions for error handling
|
|
529
|
+
*/
|
|
530
|
+
/** Error source types */
|
|
531
|
+
type ErrorSource = "constraint" | "resolver" | "effect" | "derivation" | "system";
|
|
532
|
+
/**
|
|
533
|
+
* Extended Error class with source tracking, recovery metadata, and
|
|
534
|
+
* arbitrary context for structured error handling within Directive.
|
|
535
|
+
*
|
|
536
|
+
* Thrown or returned by the error boundary manager. The `source` and
|
|
537
|
+
* `sourceId` fields identify where the error originated, and `recoverable`
|
|
538
|
+
* indicates whether the engine can apply a recovery strategy.
|
|
539
|
+
*
|
|
540
|
+
* @param message - Human-readable error description
|
|
541
|
+
* @param source - Which subsystem produced the error (`"constraint"`, `"resolver"`, `"effect"`, `"derivation"`, or `"system"`)
|
|
542
|
+
* @param sourceId - The ID of the specific constraint, resolver, effect, or derivation that failed
|
|
543
|
+
* @param context - Optional arbitrary data for debugging (e.g., the requirement that triggered a resolver error)
|
|
544
|
+
* @param recoverable - Whether the error boundary can apply a recovery strategy (default `true`; `false` for system errors)
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* try {
|
|
549
|
+
* await system.settle();
|
|
550
|
+
* } catch (err) {
|
|
551
|
+
* if (err instanceof DirectiveError) {
|
|
552
|
+
* console.log(err.source); // "resolver"
|
|
553
|
+
* console.log(err.sourceId); // "fetchUser"
|
|
554
|
+
* console.log(err.recoverable); // true
|
|
555
|
+
* }
|
|
556
|
+
* }
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
declare class DirectiveError extends Error {
|
|
560
|
+
readonly source: ErrorSource;
|
|
561
|
+
readonly sourceId: string;
|
|
562
|
+
readonly context?: unknown | undefined;
|
|
563
|
+
readonly recoverable: boolean;
|
|
564
|
+
constructor(message: string, source: ErrorSource, sourceId: string, context?: unknown | undefined, recoverable?: boolean);
|
|
565
|
+
}
|
|
566
|
+
/** Recovery strategy for errors */
|
|
567
|
+
type RecoveryStrategy = "skip" | "retry" | "retry-later" | "disable" | "throw";
|
|
568
|
+
/**
|
|
569
|
+
* Configuration for retry-later strategy.
|
|
570
|
+
* When an error occurs, the system will wait for `delayMs` before retrying.
|
|
571
|
+
*/
|
|
572
|
+
interface RetryLaterConfig {
|
|
573
|
+
/** Delay in milliseconds before retrying (default: 1000) */
|
|
574
|
+
delayMs?: number;
|
|
575
|
+
/** Maximum retries before giving up (default: 3) */
|
|
576
|
+
maxRetries?: number;
|
|
577
|
+
/** Backoff multiplier for each retry (default: 2) */
|
|
578
|
+
backoffMultiplier?: number;
|
|
579
|
+
/** Maximum delay in milliseconds (default: 30000) */
|
|
580
|
+
maxDelayMs?: number;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Circuit breaker configuration for automatic failure protection.
|
|
584
|
+
* After `failureThreshold` consecutive failures, the circuit opens
|
|
585
|
+
* and all requests fail fast for `resetTimeoutMs`.
|
|
586
|
+
*/
|
|
587
|
+
interface CircuitBreakerConfig {
|
|
588
|
+
/** Number of consecutive failures before opening the circuit (default: 5) */
|
|
589
|
+
failureThreshold?: number;
|
|
590
|
+
/** Time in milliseconds before attempting to close the circuit (default: 60000) */
|
|
591
|
+
resetTimeoutMs?: number;
|
|
592
|
+
/** Number of successful requests needed to close a half-open circuit (default: 1) */
|
|
593
|
+
successThreshold?: number;
|
|
594
|
+
}
|
|
595
|
+
/** Circuit breaker state */
|
|
596
|
+
type CircuitBreakerState = "closed" | "open" | "half-open";
|
|
597
|
+
/** Error boundary configuration */
|
|
598
|
+
interface ErrorBoundaryConfig {
|
|
599
|
+
onConstraintError?: RecoveryStrategy | ((error: Error, constraint: string) => void);
|
|
600
|
+
onResolverError?: RecoveryStrategy | ((error: Error, resolver: string) => void);
|
|
601
|
+
onEffectError?: RecoveryStrategy | ((error: Error, effect: string) => void);
|
|
602
|
+
onDerivationError?: RecoveryStrategy | ((error: Error, derivation: string) => void);
|
|
603
|
+
onError?: (error: DirectiveError) => void;
|
|
604
|
+
/** Configuration for retry-later strategy */
|
|
605
|
+
retryLater?: RetryLaterConfig;
|
|
606
|
+
/** Circuit breaker configuration (applies to resolvers only) */
|
|
607
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Composition Types - Type definitions for single and multi-module systems
|
|
612
|
+
*
|
|
613
|
+
* Single module = direct access (no namespace):
|
|
614
|
+
* @example
|
|
615
|
+
* ```typescript
|
|
616
|
+
* const system = createSystem({ modules: counterModule });
|
|
617
|
+
* system.facts.count // Direct access
|
|
618
|
+
* system.events.increment() // Direct events
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* Multiple modules = namespaced access:
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* const system = createSystem({
|
|
625
|
+
* modules: { auth: authModule, data: dataModule },
|
|
626
|
+
* });
|
|
627
|
+
* system.facts.auth.token // Namespaced access
|
|
628
|
+
* system.derive.data.userCount // Namespaced derivations
|
|
629
|
+
* system.events.auth.login() // Namespaced events
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Extract the schema type from a module definition.
|
|
635
|
+
*/
|
|
636
|
+
type ExtractSchema<M> = M extends ModuleDef<infer S> ? S : never;
|
|
637
|
+
/**
|
|
638
|
+
* Map of module name to module definition (object form).
|
|
639
|
+
*
|
|
640
|
+
* Uses `ModuleDef<any>` instead of `ModuleDef<ModuleSchema>` to preserve
|
|
641
|
+
* specific schema types during inference. The actual schema types are
|
|
642
|
+
* extracted via `ExtractSchema<M>` where needed.
|
|
643
|
+
*/
|
|
644
|
+
type ModulesMap = Record<string, ModuleDef<any>>;
|
|
645
|
+
/**
|
|
646
|
+
* Map of namespace to schema for cross-module dependencies.
|
|
647
|
+
* Used in module config to declare type-safe access to other modules' facts.
|
|
648
|
+
*/
|
|
649
|
+
type CrossModuleDeps = Record<string, ModuleSchema>;
|
|
650
|
+
/**
|
|
651
|
+
* Cross-module facts type using "self" for own module.
|
|
652
|
+
* Own module accessed via `facts.self.*`, dependencies via `facts.{dep}.*`.
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```typescript
|
|
656
|
+
* // For a "data" module with crossModuleDeps: { auth: authSchema }
|
|
657
|
+
* facts.self.users // ✅ own module via "self"
|
|
658
|
+
* facts.auth.isAuthenticated // ✅ cross-module via namespace
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
661
|
+
type CrossModuleFactsWithSelf<OwnSchema extends ModuleSchema, Deps extends CrossModuleDeps> = {
|
|
662
|
+
self: InferFacts<OwnSchema>;
|
|
663
|
+
} & {
|
|
664
|
+
[K in keyof Deps]: InferFacts<Deps[K]>;
|
|
665
|
+
};
|
|
666
|
+
/**
|
|
667
|
+
* Namespace facts under module keys.
|
|
668
|
+
* `facts.auth.token` instead of `facts.auth_token`
|
|
669
|
+
*/
|
|
670
|
+
type NamespacedFacts<Modules extends ModulesMap> = {
|
|
671
|
+
readonly [K in keyof Modules]: InferFacts<ExtractSchema<Modules[K]>>;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Mutable version for constraint/resolver callbacks.
|
|
675
|
+
*/
|
|
676
|
+
type MutableNamespacedFacts<Modules extends ModulesMap> = {
|
|
677
|
+
[K in keyof Modules]: InferFacts<ExtractSchema<Modules[K]>>;
|
|
678
|
+
};
|
|
679
|
+
/**
|
|
680
|
+
* Namespace derivations under module keys.
|
|
681
|
+
* `derive.auth.status` instead of `derive.auth_status`
|
|
682
|
+
*/
|
|
683
|
+
type NamespacedDerivations<Modules extends ModulesMap> = {
|
|
684
|
+
readonly [K in keyof Modules]: InferDerivations<ExtractSchema<Modules[K]>>;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Union of all module events (not namespaced).
|
|
688
|
+
* Events stay as discriminated union for dispatch.
|
|
689
|
+
*/
|
|
690
|
+
type UnionEvents<Modules extends ModulesMap> = {
|
|
691
|
+
[K in keyof Modules]: InferEvents<ExtractSchema<Modules[K]>>;
|
|
692
|
+
}[keyof Modules];
|
|
693
|
+
/**
|
|
694
|
+
* Options for createSystem with object modules (namespaced mode).
|
|
695
|
+
*/
|
|
696
|
+
interface CreateSystemOptionsNamed<Modules extends ModulesMap> {
|
|
697
|
+
/** Modules as object = namespaced access */
|
|
698
|
+
modules: Modules;
|
|
699
|
+
/** Plugins to register */
|
|
700
|
+
plugins?: Array<Plugin<ModuleSchema>>;
|
|
701
|
+
/** Debug configuration */
|
|
702
|
+
debug?: DebugConfig;
|
|
703
|
+
/** Error boundary configuration */
|
|
704
|
+
errorBoundary?: ErrorBoundaryConfig;
|
|
705
|
+
/**
|
|
706
|
+
* Tick interval for time-based systems (ms).
|
|
707
|
+
*/
|
|
708
|
+
tickMs?: number;
|
|
709
|
+
/**
|
|
710
|
+
* Enable zero-config mode with sensible defaults.
|
|
711
|
+
*/
|
|
712
|
+
zeroConfig?: boolean;
|
|
713
|
+
/**
|
|
714
|
+
* Initial facts to set after module init (namespaced format).
|
|
715
|
+
* Applied after all module `init()` functions but before reconciliation.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* createSystem({
|
|
720
|
+
* modules: { auth, data },
|
|
721
|
+
* initialFacts: {
|
|
722
|
+
* auth: { token: "restored-token", user: cachedUser },
|
|
723
|
+
* data: { users: preloadedUsers },
|
|
724
|
+
* },
|
|
725
|
+
* });
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
initialFacts?: Partial<{
|
|
729
|
+
[K in keyof Modules]: Partial<InferFacts<ExtractSchema<Modules[K]>>>;
|
|
730
|
+
}>;
|
|
731
|
+
/**
|
|
732
|
+
* Init order strategy:
|
|
733
|
+
* - "auto" (default): Sort by crossModuleDeps topology
|
|
734
|
+
* - "declaration": Use object key order (current behavior)
|
|
735
|
+
* - string[]: Explicit order by namespace
|
|
736
|
+
*/
|
|
737
|
+
initOrder?: "auto" | "declaration" | Array<keyof Modules & string>;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* System interface for namespaced modules.
|
|
741
|
+
* Facts and derivations are accessed via module namespaces.
|
|
742
|
+
*/
|
|
743
|
+
interface NamespacedSystem<Modules extends ModulesMap> {
|
|
744
|
+
/** System mode discriminator for type guards */
|
|
745
|
+
readonly _mode: "namespaced";
|
|
746
|
+
/** Namespaced facts accessor: system.facts.auth.token */
|
|
747
|
+
readonly facts: MutableNamespacedFacts<Modules>;
|
|
748
|
+
/** Time-travel debugging API (if enabled) */
|
|
749
|
+
readonly debug: TimeTravelAPI | null;
|
|
750
|
+
/** Namespaced derivations accessor: system.derive.auth.status */
|
|
751
|
+
readonly derive: NamespacedDerivations<Modules>;
|
|
752
|
+
/** Events accessor (union of all module events) */
|
|
753
|
+
readonly events: NamespacedEventsAccessor<Modules>;
|
|
754
|
+
/** Start the system (initialize modules, begin reconciliation) */
|
|
755
|
+
start(): void;
|
|
756
|
+
/** Stop the system (cancel resolvers, stop reconciliation) */
|
|
757
|
+
stop(): void;
|
|
758
|
+
/** Destroy the system (stop and cleanup) */
|
|
759
|
+
destroy(): void;
|
|
760
|
+
/** Whether the system is currently running */
|
|
761
|
+
readonly isRunning: boolean;
|
|
762
|
+
/** Whether all resolvers have completed */
|
|
763
|
+
readonly isSettled: boolean;
|
|
764
|
+
/** Whether all modules have completed initialization */
|
|
765
|
+
readonly isInitialized: boolean;
|
|
766
|
+
/** Whether system has completed first reconciliation */
|
|
767
|
+
readonly isReady: boolean;
|
|
768
|
+
/** Wait for system to be fully ready (after first reconciliation) */
|
|
769
|
+
whenReady(): Promise<void>;
|
|
770
|
+
/**
|
|
771
|
+
* Hydrate facts from async source (call before start).
|
|
772
|
+
* Useful for restoring state from localStorage, API, etc.
|
|
773
|
+
*
|
|
774
|
+
* @example
|
|
775
|
+
* ```typescript
|
|
776
|
+
* const system = createSystem({ modules: { auth, data } });
|
|
777
|
+
* await system.hydrate(async () => {
|
|
778
|
+
* const stored = localStorage.getItem("app-state");
|
|
779
|
+
* return stored ? JSON.parse(stored) : {};
|
|
780
|
+
* });
|
|
781
|
+
* system.start();
|
|
782
|
+
* ```
|
|
783
|
+
*/
|
|
784
|
+
hydrate(loader: () => Promise<Partial<{
|
|
785
|
+
[K in keyof Modules]: Partial<InferFacts<ExtractSchema<Modules[K]>>>;
|
|
786
|
+
}>> | Partial<{
|
|
787
|
+
[K in keyof Modules]: Partial<InferFacts<ExtractSchema<Modules[K]>>>;
|
|
788
|
+
}>): Promise<void>;
|
|
789
|
+
/** Dispatch an event (union of all module events) */
|
|
790
|
+
dispatch(event: UnionEvents<Modules>): void;
|
|
791
|
+
/** Batch multiple fact changes */
|
|
792
|
+
batch(fn: () => void): void;
|
|
793
|
+
/**
|
|
794
|
+
* Subscribe to settlement state changes.
|
|
795
|
+
* Called whenever the system's settled state may have changed
|
|
796
|
+
* (resolver starts/completes, reconcile starts/ends).
|
|
797
|
+
*/
|
|
798
|
+
onSettledChange(listener: () => void): () => void;
|
|
799
|
+
/** Subscribe to time-travel state changes (snapshot taken, navigation). */
|
|
800
|
+
onTimeTravelChange(listener: () => void): () => void;
|
|
801
|
+
/**
|
|
802
|
+
* Read a derivation value by namespaced key.
|
|
803
|
+
* Accepts "namespace.key" format (e.g., "auth.status").
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* system.read("auth.status") // → "authenticated"
|
|
807
|
+
* system.read("data.count") // → 5
|
|
808
|
+
*/
|
|
809
|
+
read<T = unknown>(derivationId: string): T;
|
|
810
|
+
/**
|
|
811
|
+
* Subscribe to fact or derivation changes using namespaced keys.
|
|
812
|
+
* Keys are auto-detected — pass any mix of fact keys and derivation keys.
|
|
813
|
+
* Accepts "namespace.key" format (e.g., "auth.status", "auth.token").
|
|
814
|
+
* Supports wildcard "namespace.*" to subscribe to all keys in a module.
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* system.subscribe(["auth.token", "data.count"], () => {
|
|
818
|
+
* console.log("Auth or data changed");
|
|
819
|
+
* });
|
|
820
|
+
*
|
|
821
|
+
* @example Wildcard
|
|
822
|
+
* system.subscribe(["game.*"], () => render());
|
|
823
|
+
*/
|
|
824
|
+
subscribe(ids: string[], listener: () => void): () => void;
|
|
825
|
+
/**
|
|
826
|
+
* Subscribe to ALL fact and derivation changes in a module namespace.
|
|
827
|
+
* Shorthand for subscribing to every key in a module.
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* system.subscribeModule("game", () => render());
|
|
831
|
+
* system.subscribeModule("chat", () => render());
|
|
832
|
+
*/
|
|
833
|
+
subscribeModule(namespace: keyof Modules & string, listener: () => void): () => void;
|
|
834
|
+
/**
|
|
835
|
+
* Watch a fact or derivation for changes using namespaced key.
|
|
836
|
+
* The key is auto-detected -- works with both fact keys and derivation keys.
|
|
837
|
+
* Accepts "namespace.key" format (e.g., "auth.status", "auth.token").
|
|
838
|
+
* Pass `options.equalityFn` for custom comparison (e.g., shallow equality for objects).
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* system.watch("auth.token", (newVal, oldVal) => {
|
|
842
|
+
* console.log(`Token changed from ${oldVal} to ${newVal}`);
|
|
843
|
+
* });
|
|
844
|
+
*/
|
|
845
|
+
watch<T = unknown>(id: string, callback: (newValue: T, previousValue: T | undefined) => void, options?: {
|
|
846
|
+
equalityFn?: (a: T, b: T | undefined) => boolean;
|
|
847
|
+
}): () => void;
|
|
848
|
+
/**
|
|
849
|
+
* Returns a promise that resolves when the predicate becomes true.
|
|
850
|
+
* The predicate is evaluated against current facts and re-evaluated on every change.
|
|
851
|
+
* Uses namespaced facts: `facts.auth.token`, `facts.data.count`, etc.
|
|
852
|
+
* Optionally pass a timeout in ms -- rejects with an error if exceeded.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* await system.when((facts) => facts.auth.token !== null);
|
|
856
|
+
* await system.when((facts) => facts.auth.token !== null, { timeout: 5000 });
|
|
857
|
+
*/
|
|
858
|
+
when(predicate: (facts: Readonly<MutableNamespacedFacts<Modules>>) => boolean, options?: {
|
|
859
|
+
timeout?: number;
|
|
860
|
+
}): Promise<void>;
|
|
861
|
+
/** Inspect system state */
|
|
862
|
+
inspect(): SystemInspection;
|
|
863
|
+
/** Wait for system to settle (all resolvers complete) */
|
|
864
|
+
settle(maxWait?: number): Promise<void>;
|
|
865
|
+
/** Explain why a requirement exists */
|
|
866
|
+
explain(requirementId: string): string | null;
|
|
867
|
+
/** Get serializable snapshot of system state */
|
|
868
|
+
getSnapshot(): SystemSnapshot;
|
|
869
|
+
/** Restore system state from snapshot */
|
|
870
|
+
restore(snapshot: SystemSnapshot): void;
|
|
871
|
+
/**
|
|
872
|
+
* Register a new module into a running system.
|
|
873
|
+
* The module is initialized, wired into constraint/resolver/derivation graphs,
|
|
874
|
+
* and reconciliation is triggered.
|
|
875
|
+
*
|
|
876
|
+
* @example
|
|
877
|
+
* ```typescript
|
|
878
|
+
* // Lazy-load a module
|
|
879
|
+
* const chatModule = await import('./modules/chat');
|
|
880
|
+
* system.registerModule("chat", chatModule.default);
|
|
881
|
+
* ```
|
|
882
|
+
*/
|
|
883
|
+
registerModule<S extends ModuleSchema>(namespace: string, moduleDef: ModuleDef<S>): void;
|
|
884
|
+
/**
|
|
885
|
+
* Get a distributable snapshot of computed derivations.
|
|
886
|
+
* Use "namespace.key" format for derivation keys.
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* ```typescript
|
|
890
|
+
* const snapshot = system.getDistributableSnapshot({
|
|
891
|
+
* includeDerivations: ['auth.effectivePlan', 'auth.canUseFeature'],
|
|
892
|
+
* ttlSeconds: 3600,
|
|
893
|
+
* });
|
|
894
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
895
|
+
* ```
|
|
896
|
+
*/
|
|
897
|
+
getDistributableSnapshot<T = Record<string, unknown>>(options?: DistributableSnapshotOptions): DistributableSnapshot<T>;
|
|
898
|
+
/**
|
|
899
|
+
* Watch for changes to distributable snapshot derivations.
|
|
900
|
+
* Calls the callback whenever any of the included derivations change.
|
|
901
|
+
* Use "namespace.key" format for derivation keys.
|
|
902
|
+
* Returns an unsubscribe function.
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```typescript
|
|
906
|
+
* const unsubscribe = system.watchDistributableSnapshot(
|
|
907
|
+
* { includeDerivations: ['auth.effectivePlan', 'auth.canUseFeature'] },
|
|
908
|
+
* (snapshot) => {
|
|
909
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
910
|
+
* }
|
|
911
|
+
* );
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
watchDistributableSnapshot<T = Record<string, unknown>>(options: DistributableSnapshotOptions, callback: (snapshot: DistributableSnapshot<T>) => void): () => void;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Events accessor that groups event dispatchers by module namespace.
|
|
918
|
+
*/
|
|
919
|
+
type NamespacedEventsAccessor<Modules extends ModulesMap> = {
|
|
920
|
+
readonly [K in keyof Modules]: EventsDispatcherForModule<Modules[K]>;
|
|
921
|
+
};
|
|
922
|
+
/**
|
|
923
|
+
* Event dispatcher functions for a single module.
|
|
924
|
+
*/
|
|
925
|
+
type EventsDispatcherForModule<M> = M extends ModuleDef<infer S> ? S extends ModuleSchema ? S["events"] extends Record<string, unknown> ? {
|
|
926
|
+
[E in keyof S["events"]]: S["events"][E] extends Record<string, unknown> ? keyof S["events"][E] extends never ? () => void : (payload: InferEventPayload<S["events"][E]>) => void : () => void;
|
|
927
|
+
} : Record<string, never> : Record<string, never> : Record<string, never>;
|
|
928
|
+
/**
|
|
929
|
+
* Infer event payload from event schema.
|
|
930
|
+
*/
|
|
931
|
+
type InferEventPayload<E> = E extends Record<string, unknown> ? {
|
|
932
|
+
[K in keyof E]: E[K] extends {
|
|
933
|
+
_type: infer T;
|
|
934
|
+
} ? T : E[K];
|
|
935
|
+
} : never;
|
|
936
|
+
/**
|
|
937
|
+
* Options for createSystem with a single module (no namespacing).
|
|
938
|
+
*/
|
|
939
|
+
interface CreateSystemOptionsSingle<S extends ModuleSchema> {
|
|
940
|
+
/** Single module = direct access (use `modules` for multiple) */
|
|
941
|
+
module: ModuleDef<S>;
|
|
942
|
+
/** Plugins to register */
|
|
943
|
+
plugins?: Array<Plugin<ModuleSchema>>;
|
|
944
|
+
/** Debug configuration */
|
|
945
|
+
debug?: DebugConfig;
|
|
946
|
+
/** Error boundary configuration */
|
|
947
|
+
errorBoundary?: ErrorBoundaryConfig;
|
|
948
|
+
/** Tick interval for time-based systems (ms) */
|
|
949
|
+
tickMs?: number;
|
|
950
|
+
/** Enable zero-config mode with sensible defaults */
|
|
951
|
+
zeroConfig?: boolean;
|
|
952
|
+
/**
|
|
953
|
+
* Initial facts to set after module init.
|
|
954
|
+
* Applied after module `init()` but before reconciliation.
|
|
955
|
+
*/
|
|
956
|
+
initialFacts?: Partial<InferFacts<S>>;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* System interface for a single module (no namespace).
|
|
960
|
+
* Facts, derivations, and events are accessed directly.
|
|
961
|
+
*/
|
|
962
|
+
interface SingleModuleSystem<S extends ModuleSchema> {
|
|
963
|
+
/** System mode discriminator for type guards */
|
|
964
|
+
readonly _mode: "single";
|
|
965
|
+
/** Direct facts accessor: system.facts.count */
|
|
966
|
+
readonly facts: Facts<S["facts"]>;
|
|
967
|
+
/** Time-travel debugging API (if enabled) */
|
|
968
|
+
readonly debug: TimeTravelAPI | null;
|
|
969
|
+
/** Direct derivations accessor: system.derive.doubled */
|
|
970
|
+
readonly derive: InferDerivations<S>;
|
|
971
|
+
/** Direct events accessor: system.events.increment() */
|
|
972
|
+
readonly events: SingleModuleEvents<S>;
|
|
973
|
+
/** Start the system (initialize modules, begin reconciliation) */
|
|
974
|
+
start(): void;
|
|
975
|
+
/** Stop the system (cancel resolvers, stop reconciliation) */
|
|
976
|
+
stop(): void;
|
|
977
|
+
/** Destroy the system (stop and cleanup) */
|
|
978
|
+
destroy(): void;
|
|
979
|
+
/** Whether the system is currently running */
|
|
980
|
+
readonly isRunning: boolean;
|
|
981
|
+
/** Whether all resolvers have completed */
|
|
982
|
+
readonly isSettled: boolean;
|
|
983
|
+
/** Whether module has completed initialization */
|
|
984
|
+
readonly isInitialized: boolean;
|
|
985
|
+
/** Whether system has completed first reconciliation */
|
|
986
|
+
readonly isReady: boolean;
|
|
987
|
+
/** Wait for system to be fully ready (after first reconciliation) */
|
|
988
|
+
whenReady(): Promise<void>;
|
|
989
|
+
/**
|
|
990
|
+
* Hydrate facts from async source (call before start).
|
|
991
|
+
*/
|
|
992
|
+
hydrate(loader: () => Promise<Partial<InferFacts<S>>> | Partial<InferFacts<S>>): Promise<void>;
|
|
993
|
+
/** Dispatch an event */
|
|
994
|
+
dispatch(event: InferEvents<S>): void;
|
|
995
|
+
/** Batch multiple fact changes */
|
|
996
|
+
batch(fn: () => void): void;
|
|
997
|
+
/**
|
|
998
|
+
* Subscribe to settlement state changes.
|
|
999
|
+
* Called whenever the system's settled state may have changed
|
|
1000
|
+
* (resolver starts/completes, reconcile starts/ends).
|
|
1001
|
+
*/
|
|
1002
|
+
onSettledChange(listener: () => void): () => void;
|
|
1003
|
+
/** Subscribe to time-travel state changes (snapshot taken, navigation). */
|
|
1004
|
+
onTimeTravelChange(listener: () => void): () => void;
|
|
1005
|
+
/**
|
|
1006
|
+
* Read a derivation value by key.
|
|
1007
|
+
* @example system.read("doubled")
|
|
1008
|
+
*/
|
|
1009
|
+
read<T = unknown>(derivationId: string): T;
|
|
1010
|
+
/**
|
|
1011
|
+
* Subscribe to fact or derivation changes.
|
|
1012
|
+
* Keys are auto-detected -- pass any mix of fact keys and derivation keys.
|
|
1013
|
+
* @example system.subscribe(["count", "doubled"], () => { ... })
|
|
1014
|
+
*/
|
|
1015
|
+
subscribe(ids: string[], listener: () => void): () => void;
|
|
1016
|
+
/**
|
|
1017
|
+
* Watch a fact or derivation for value changes.
|
|
1018
|
+
* The key is auto-detected -- works with both fact keys and derivation keys.
|
|
1019
|
+
* Pass `options.equalityFn` for custom comparison (e.g., shallow equality for objects).
|
|
1020
|
+
* @example system.watch("count", (newVal, oldVal) => { ... })
|
|
1021
|
+
* @example system.watch("derived", cb, { equalityFn: shallowEqual })
|
|
1022
|
+
*/
|
|
1023
|
+
watch<T = unknown>(id: string, callback: (newValue: T, previousValue: T | undefined) => void, options?: {
|
|
1024
|
+
equalityFn?: (a: T, b: T | undefined) => boolean;
|
|
1025
|
+
}): () => void;
|
|
1026
|
+
/**
|
|
1027
|
+
* Returns a promise that resolves when the predicate becomes true.
|
|
1028
|
+
* The predicate is evaluated against current facts and re-evaluated on every change.
|
|
1029
|
+
* Optionally pass a timeout in ms -- rejects with an error if exceeded.
|
|
1030
|
+
*
|
|
1031
|
+
* @example
|
|
1032
|
+
* await system.when((facts) => facts.count > 10);
|
|
1033
|
+
* await system.when((facts) => facts.count > 10, { timeout: 5000 });
|
|
1034
|
+
*/
|
|
1035
|
+
when(predicate: (facts: Readonly<InferFacts<S>>) => boolean, options?: {
|
|
1036
|
+
timeout?: number;
|
|
1037
|
+
}): Promise<void>;
|
|
1038
|
+
/** Inspect system state */
|
|
1039
|
+
inspect(): SystemInspection;
|
|
1040
|
+
/** Wait for system to settle (all resolvers complete) */
|
|
1041
|
+
settle(maxWait?: number): Promise<void>;
|
|
1042
|
+
/** Explain why a requirement exists */
|
|
1043
|
+
explain(requirementId: string): string | null;
|
|
1044
|
+
/** Get serializable snapshot of system state */
|
|
1045
|
+
getSnapshot(): SystemSnapshot;
|
|
1046
|
+
/** Restore system state from snapshot */
|
|
1047
|
+
restore(snapshot: SystemSnapshot): void;
|
|
1048
|
+
/**
|
|
1049
|
+
* Register a new module into a running system.
|
|
1050
|
+
* Module facts, derivations, effects, constraints, and resolvers are merged
|
|
1051
|
+
* into the existing engine and reconciliation is triggered.
|
|
1052
|
+
*
|
|
1053
|
+
* @example
|
|
1054
|
+
* ```typescript
|
|
1055
|
+
* const analyticsModule = await import('./modules/analytics');
|
|
1056
|
+
* system.registerModule(analyticsModule.default);
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
registerModule<S2 extends ModuleSchema>(moduleDef: ModuleDef<S2>): void;
|
|
1060
|
+
/**
|
|
1061
|
+
* Get a distributable snapshot of computed derivations.
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```typescript
|
|
1065
|
+
* const snapshot = system.getDistributableSnapshot({
|
|
1066
|
+
* includeDerivations: ['effectivePlan', 'canUseFeature'],
|
|
1067
|
+
* ttlSeconds: 3600,
|
|
1068
|
+
* });
|
|
1069
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
1072
|
+
getDistributableSnapshot<T = Record<string, unknown>>(options?: DistributableSnapshotOptions): DistributableSnapshot<T>;
|
|
1073
|
+
/**
|
|
1074
|
+
* Watch for changes to distributable snapshot derivations.
|
|
1075
|
+
* Calls the callback whenever any of the included derivations change.
|
|
1076
|
+
* Returns an unsubscribe function.
|
|
1077
|
+
*
|
|
1078
|
+
* @example
|
|
1079
|
+
* ```typescript
|
|
1080
|
+
* const unsubscribe = system.watchDistributableSnapshot(
|
|
1081
|
+
* { includeDerivations: ['effectivePlan', 'canUseFeature'] },
|
|
1082
|
+
* (snapshot) => {
|
|
1083
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
1084
|
+
* }
|
|
1085
|
+
* );
|
|
1086
|
+
* ```
|
|
1087
|
+
*/
|
|
1088
|
+
watchDistributableSnapshot<T = Record<string, unknown>>(options: DistributableSnapshotOptions, callback: (snapshot: DistributableSnapshot<T>) => void): () => void;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Events dispatcher for a single module (direct access).
|
|
1092
|
+
*/
|
|
1093
|
+
type SingleModuleEvents<S extends ModuleSchema> = S["events"] extends Record<string, unknown> ? {
|
|
1094
|
+
[E in keyof S["events"]]: S["events"][E] extends Record<string, unknown> ? keyof S["events"][E] extends never ? () => void : (payload: InferEventPayload<S["events"][E]>) => void : () => void;
|
|
1095
|
+
} : Record<string, never>;
|
|
1096
|
+
/**
|
|
1097
|
+
* System mode discriminator.
|
|
1098
|
+
* - "single": Single module with direct access (`system.facts.count`)
|
|
1099
|
+
* - "namespaced": Multiple modules with namespaced access (`system.facts.auth.token`)
|
|
1100
|
+
*/
|
|
1101
|
+
type SystemMode = "single" | "namespaced";
|
|
1102
|
+
/**
|
|
1103
|
+
* Base system type for type guards.
|
|
1104
|
+
* Use this for functions that accept either system type.
|
|
1105
|
+
*/
|
|
1106
|
+
interface AnySystem {
|
|
1107
|
+
readonly _mode: SystemMode;
|
|
1108
|
+
readonly isRunning: boolean;
|
|
1109
|
+
readonly isSettled: boolean;
|
|
1110
|
+
readonly isInitialized: boolean;
|
|
1111
|
+
readonly isReady: boolean;
|
|
1112
|
+
start(): void;
|
|
1113
|
+
stop(): void;
|
|
1114
|
+
destroy(): void;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Check if a system is a single module system.
|
|
1118
|
+
* Returns true if the system was created with `module:` (singular).
|
|
1119
|
+
*
|
|
1120
|
+
* @example
|
|
1121
|
+
* ```typescript
|
|
1122
|
+
* const system = createSystem({ module: counterModule });
|
|
1123
|
+
*
|
|
1124
|
+
* if (isSingleModuleSystem(system)) {
|
|
1125
|
+
* // system._mode === "single"
|
|
1126
|
+
* console.log(system.facts.count);
|
|
1127
|
+
* }
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
declare function isSingleModuleSystem(system: AnySystem): boolean;
|
|
1131
|
+
/**
|
|
1132
|
+
* Check if a system is a namespaced (multi-module) system.
|
|
1133
|
+
* Returns true if the system was created with `modules:` (plural, object).
|
|
1134
|
+
*
|
|
1135
|
+
* @example
|
|
1136
|
+
* ```typescript
|
|
1137
|
+
* const system = createSystem({ modules: { auth, data } });
|
|
1138
|
+
*
|
|
1139
|
+
* if (isNamespacedSystem(system)) {
|
|
1140
|
+
* // system._mode === "namespaced"
|
|
1141
|
+
* console.log(system.facts.auth.token);
|
|
1142
|
+
* }
|
|
1143
|
+
* ```
|
|
1144
|
+
*/
|
|
1145
|
+
declare function isNamespacedSystem(system: AnySystem): boolean;
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Module Types - Type definitions for modules with consolidated schema
|
|
1149
|
+
*/
|
|
1150
|
+
|
|
1151
|
+
/** Lifecycle hooks for modules */
|
|
1152
|
+
interface ModuleHooks<_M extends ModuleSchema> {
|
|
1153
|
+
onInit?: (system: System<any>) => void;
|
|
1154
|
+
onStart?: (system: System<any>) => void;
|
|
1155
|
+
onStop?: (system: System<any>) => void;
|
|
1156
|
+
onError?: (error: DirectiveError, context: unknown) => void;
|
|
1157
|
+
}
|
|
1158
|
+
/** Helper to get derivations schema, defaulting to empty */
|
|
1159
|
+
type GetDerivationsSchema<M extends ModuleSchema> = M["derivations"] extends DerivationsSchema ? M["derivations"] : Record<string, never>;
|
|
1160
|
+
/** Helper to get events schema, defaulting to empty */
|
|
1161
|
+
type GetEventsSchema<M extends ModuleSchema> = M["events"] extends EventsSchema ? M["events"] : Record<string, never>;
|
|
1162
|
+
/** Helper to get requirements schema, defaulting to empty */
|
|
1163
|
+
type GetRequirementsSchema<M extends ModuleSchema> = M["requirements"] extends RequirementsSchema$1 ? M["requirements"] : Record<string, never>;
|
|
1164
|
+
/**
|
|
1165
|
+
* Derivation function with typed facts and derive accessor.
|
|
1166
|
+
* The derive accessor is typed from schema.derivations.
|
|
1167
|
+
* Supports both t.*() builders and type assertion {} as {} patterns.
|
|
1168
|
+
*/
|
|
1169
|
+
type TypedDerivationFn<M extends ModuleSchema, K extends keyof GetDerivationsSchema<M>> = (facts: Facts<M["facts"]>, derive: InferDerivations<M>) => InferSchemaType<GetDerivationsSchema<M>[K]>;
|
|
1170
|
+
/**
|
|
1171
|
+
* Typed derivations definition using the module schema.
|
|
1172
|
+
* Each derivation key must match schema.derivations and return the declared type.
|
|
1173
|
+
*/
|
|
1174
|
+
type TypedDerivationsDef<M extends ModuleSchema> = {
|
|
1175
|
+
[K in keyof GetDerivationsSchema<M>]: TypedDerivationFn<M, K>;
|
|
1176
|
+
};
|
|
1177
|
+
/**
|
|
1178
|
+
* Event handler function with typed facts and payload.
|
|
1179
|
+
* Payload is typed from schema.events[K].
|
|
1180
|
+
*/
|
|
1181
|
+
type TypedEventHandlerFn<M extends ModuleSchema, K extends keyof GetEventsSchema<M>> = keyof GetEventsSchema<M>[K] extends never ? (facts: Facts<M["facts"]>) => void : (facts: Facts<M["facts"]>, payload: InferEventPayloadFromSchema<GetEventsSchema<M>[K]>) => void;
|
|
1182
|
+
/**
|
|
1183
|
+
* Typed events definition using the module schema.
|
|
1184
|
+
* Each event key must match schema.events with the correct payload type.
|
|
1185
|
+
*/
|
|
1186
|
+
type TypedEventsDef<M extends ModuleSchema> = {
|
|
1187
|
+
[K in keyof GetEventsSchema<M>]: TypedEventHandlerFn<M, K>;
|
|
1188
|
+
};
|
|
1189
|
+
/**
|
|
1190
|
+
* Requirement output from a constraint.
|
|
1191
|
+
*/
|
|
1192
|
+
type RequirementOutput<R> = R | R[] | null;
|
|
1193
|
+
/**
|
|
1194
|
+
* Constraint definition with typed requirements.
|
|
1195
|
+
*/
|
|
1196
|
+
interface TypedConstraintDef<M extends ModuleSchema> {
|
|
1197
|
+
/** Priority for ordering (higher runs first) */
|
|
1198
|
+
priority?: number;
|
|
1199
|
+
/** Mark this constraint as async */
|
|
1200
|
+
async?: boolean;
|
|
1201
|
+
/** Condition function */
|
|
1202
|
+
when: (facts: Facts<M["facts"]>) => boolean | Promise<boolean>;
|
|
1203
|
+
/**
|
|
1204
|
+
* Requirement(s) to produce when condition is met.
|
|
1205
|
+
*/
|
|
1206
|
+
require: RequirementOutput<InferRequirements<M>> | ((facts: Facts<M["facts"]>) => RequirementOutput<InferRequirements<M>>);
|
|
1207
|
+
/** Timeout for async constraints (ms) */
|
|
1208
|
+
timeout?: number;
|
|
1209
|
+
/**
|
|
1210
|
+
* Constraint IDs whose resolvers must complete before this constraint is evaluated.
|
|
1211
|
+
* If a dependency's `when()` returns false (no requirements), this constraint proceeds.
|
|
1212
|
+
* If a dependency's resolver fails, this constraint remains blocked.
|
|
1213
|
+
* Cross-module: use "moduleName.constraintName" format.
|
|
1214
|
+
*/
|
|
1215
|
+
after?: string[];
|
|
1216
|
+
/**
|
|
1217
|
+
* Explicit fact dependencies for this constraint.
|
|
1218
|
+
* Required for async constraints to enable dependency tracking.
|
|
1219
|
+
*/
|
|
1220
|
+
deps?: string[];
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Typed constraints definition using the module schema.
|
|
1224
|
+
*/
|
|
1225
|
+
type TypedConstraintsDef<M extends ModuleSchema> = Record<string, TypedConstraintDef<M>>;
|
|
1226
|
+
/**
|
|
1227
|
+
* Constraint definition with cross-module typed facts.
|
|
1228
|
+
* Used when a module declares crossModuleDeps for type-safe access to other modules.
|
|
1229
|
+
*
|
|
1230
|
+
* At runtime, constraints receive facts with:
|
|
1231
|
+
* - `facts.self.*` for own module's facts
|
|
1232
|
+
* - `facts.{dep}.*` for cross-module facts
|
|
1233
|
+
*/
|
|
1234
|
+
interface CrossModuleConstraintDef<M extends ModuleSchema, Deps extends CrossModuleDeps> {
|
|
1235
|
+
/** Priority for ordering (higher runs first) */
|
|
1236
|
+
priority?: number;
|
|
1237
|
+
/** Mark this constraint as async */
|
|
1238
|
+
async?: boolean;
|
|
1239
|
+
/** Condition function with cross-module facts access */
|
|
1240
|
+
when: (facts: CrossModuleFactsWithSelf<M, Deps>) => boolean | Promise<boolean>;
|
|
1241
|
+
/**
|
|
1242
|
+
* Requirement(s) to produce when condition is met.
|
|
1243
|
+
*/
|
|
1244
|
+
require: RequirementOutput<InferRequirements<M>> | ((facts: CrossModuleFactsWithSelf<M, Deps>) => RequirementOutput<InferRequirements<M>>);
|
|
1245
|
+
/** Timeout for async constraints (ms) */
|
|
1246
|
+
timeout?: number;
|
|
1247
|
+
/**
|
|
1248
|
+
* Constraint IDs whose resolvers must complete before this constraint is evaluated.
|
|
1249
|
+
* If a dependency's `when()` returns false (no requirements), this constraint proceeds.
|
|
1250
|
+
* If a dependency's resolver fails, this constraint remains blocked.
|
|
1251
|
+
* Cross-module: use "moduleName.constraintName" format.
|
|
1252
|
+
*/
|
|
1253
|
+
after?: string[];
|
|
1254
|
+
/**
|
|
1255
|
+
* Explicit fact dependencies for this constraint.
|
|
1256
|
+
* Required for async constraints to enable dependency tracking.
|
|
1257
|
+
*/
|
|
1258
|
+
deps?: string[];
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Cross-module constraints definition.
|
|
1262
|
+
*/
|
|
1263
|
+
type CrossModuleConstraintsDef<M extends ModuleSchema, Deps extends CrossModuleDeps> = Record<string, CrossModuleConstraintDef<M, Deps>>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Effect definition with cross-module typed facts.
|
|
1266
|
+
* Used when a module declares crossModuleDeps for type-safe access to other modules.
|
|
1267
|
+
*
|
|
1268
|
+
* At runtime, effects receive facts with:
|
|
1269
|
+
* - `facts.self.*` for own module's facts
|
|
1270
|
+
* - `facts.{dep}.*` for cross-module facts
|
|
1271
|
+
*/
|
|
1272
|
+
interface CrossModuleEffectDef<M extends ModuleSchema, Deps extends CrossModuleDeps> {
|
|
1273
|
+
/** Effect function with cross-module facts access. Return a cleanup function for teardown. */
|
|
1274
|
+
run: (facts: CrossModuleFactsWithSelf<M, Deps>, prev: CrossModuleFactsWithSelf<M, Deps> | undefined) => void | EffectCleanup | Promise<void | EffectCleanup>;
|
|
1275
|
+
/** Optional dependency keys to filter when effect runs */
|
|
1276
|
+
deps?: string[];
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Cross-module effects definition.
|
|
1280
|
+
*/
|
|
1281
|
+
type CrossModuleEffectsDef<M extends ModuleSchema, Deps extends CrossModuleDeps> = Record<string, CrossModuleEffectDef<M, Deps>>;
|
|
1282
|
+
/**
|
|
1283
|
+
* Derivation function with cross-module typed facts.
|
|
1284
|
+
* Used when a module declares crossModuleDeps for type-safe access to other modules' facts.
|
|
1285
|
+
*
|
|
1286
|
+
* At runtime, derivations receive facts with:
|
|
1287
|
+
* - `facts.self.*` for own module's facts
|
|
1288
|
+
* - `facts.{dep}.*` for cross-module facts (read-only)
|
|
1289
|
+
*/
|
|
1290
|
+
type CrossModuleDerivationFn<M extends ModuleSchema, Deps extends CrossModuleDeps, K extends keyof GetDerivationsSchema<M>> = (facts: CrossModuleFactsWithSelf<M, Deps>, derive: InferDerivations<M>) => InferSchemaType<GetDerivationsSchema<M>[K]>;
|
|
1291
|
+
/**
|
|
1292
|
+
* Cross-module derivations definition.
|
|
1293
|
+
*/
|
|
1294
|
+
type CrossModuleDerivationsDef<M extends ModuleSchema, Deps extends CrossModuleDeps> = {
|
|
1295
|
+
[K in keyof GetDerivationsSchema<M>]: CrossModuleDerivationFn<M, Deps, K>;
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* Retry policy configuration.
|
|
1299
|
+
*/
|
|
1300
|
+
interface RetryPolicy {
|
|
1301
|
+
attempts: number;
|
|
1302
|
+
backoff: "none" | "linear" | "exponential";
|
|
1303
|
+
initialDelay?: number;
|
|
1304
|
+
maxDelay?: number;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolver context with typed facts.
|
|
1308
|
+
*/
|
|
1309
|
+
interface TypedResolverContext<M extends ModuleSchema> {
|
|
1310
|
+
readonly facts: Facts<M["facts"]>;
|
|
1311
|
+
readonly signal: AbortSignal;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Helper to extract a specific requirement type from the schema.
|
|
1315
|
+
*/
|
|
1316
|
+
type ExtractRequirement<M extends ModuleSchema, T extends keyof GetRequirementsSchema<M>> = {
|
|
1317
|
+
type: T;
|
|
1318
|
+
} & InferRequirementPayloadFromSchema<GetRequirementsSchema<M>[T]>;
|
|
1319
|
+
/**
|
|
1320
|
+
* Typed resolver definition for a specific requirement type.
|
|
1321
|
+
*/
|
|
1322
|
+
interface TypedResolverDef<M extends ModuleSchema, T extends keyof GetRequirementsSchema<M> & string> {
|
|
1323
|
+
/** Requirement type to handle */
|
|
1324
|
+
requirement: T;
|
|
1325
|
+
/** Custom key function for deduplication */
|
|
1326
|
+
key?: (req: ExtractRequirement<M, T>) => string;
|
|
1327
|
+
/** Retry policy */
|
|
1328
|
+
retry?: RetryPolicy;
|
|
1329
|
+
/** Timeout for resolver execution (ms) */
|
|
1330
|
+
timeout?: number;
|
|
1331
|
+
/** Resolve function */
|
|
1332
|
+
resolve: (req: ExtractRequirement<M, T>, ctx: TypedResolverContext<M>) => Promise<void>;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Union of all typed resolver definitions for all requirement types.
|
|
1336
|
+
*/
|
|
1337
|
+
type AnyTypedResolverDef<M extends ModuleSchema> = {
|
|
1338
|
+
[T in keyof GetRequirementsSchema<M> & string]: TypedResolverDef<M, T>;
|
|
1339
|
+
}[keyof GetRequirementsSchema<M> & string];
|
|
1340
|
+
/**
|
|
1341
|
+
* Typed resolvers definition using the module schema.
|
|
1342
|
+
*/
|
|
1343
|
+
type TypedResolversDef<M extends ModuleSchema> = Record<string, AnyTypedResolverDef<M>>;
|
|
1344
|
+
/**
|
|
1345
|
+
* Module definition using consolidated schema.
|
|
1346
|
+
* This provides full type inference for all module components.
|
|
1347
|
+
*
|
|
1348
|
+
* derive and events are optional when the schema has no derivations/events.
|
|
1349
|
+
*/
|
|
1350
|
+
interface ModuleDef<M extends ModuleSchema = ModuleSchema> {
|
|
1351
|
+
id: string;
|
|
1352
|
+
schema: M;
|
|
1353
|
+
init?: (facts: Facts<M["facts"]>) => void;
|
|
1354
|
+
derive?: TypedDerivationsDef<M>;
|
|
1355
|
+
events?: TypedEventsDef<M>;
|
|
1356
|
+
effects?: EffectsDef<M["facts"]>;
|
|
1357
|
+
constraints?: TypedConstraintsDef<M>;
|
|
1358
|
+
resolvers?: TypedResolversDef<M>;
|
|
1359
|
+
hooks?: ModuleHooks<M>;
|
|
1360
|
+
/**
|
|
1361
|
+
* Cross-module dependencies (runtime marker).
|
|
1362
|
+
* When present, constraints/effects receive `facts.self.*` + `facts.{dep}.*`.
|
|
1363
|
+
* @internal
|
|
1364
|
+
*/
|
|
1365
|
+
crossModuleDeps?: CrossModuleDeps;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* System Types - Type definitions for the system
|
|
1370
|
+
*/
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Derive accessor from module schema.
|
|
1374
|
+
*/
|
|
1375
|
+
type DeriveAccessor<M extends ModuleSchema> = InferDerivations<M>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Fact keys from module schema.
|
|
1378
|
+
*/
|
|
1379
|
+
type FactKeys<M extends ModuleSchema> = keyof M["facts"] & string;
|
|
1380
|
+
/**
|
|
1381
|
+
* Get fact return type from module schema.
|
|
1382
|
+
*/
|
|
1383
|
+
type FactReturnType<M extends ModuleSchema, K extends keyof M["facts"]> = InferSchemaType<M["facts"][K]>;
|
|
1384
|
+
/**
|
|
1385
|
+
* Derivation keys from module schema.
|
|
1386
|
+
*/
|
|
1387
|
+
type DerivationKeys<M extends ModuleSchema> = keyof M["derivations"] & string;
|
|
1388
|
+
/**
|
|
1389
|
+
* Get derivation return type from module schema.
|
|
1390
|
+
*/
|
|
1391
|
+
type DerivationReturnType<M extends ModuleSchema, K extends keyof M["derivations"]> = InferSchemaType<M["derivations"][K]>;
|
|
1392
|
+
/**
|
|
1393
|
+
* All observable keys (facts + derivations) from module schema.
|
|
1394
|
+
*/
|
|
1395
|
+
type ObservableKeys<M extends ModuleSchema> = FactKeys<M> | DerivationKeys<M>;
|
|
1396
|
+
/**
|
|
1397
|
+
* Events accessor from module schema.
|
|
1398
|
+
*/
|
|
1399
|
+
type EventsAccessor<M extends ModuleSchema> = EventsAccessorFromSchema<M>;
|
|
1400
|
+
/** Debug configuration */
|
|
1401
|
+
interface DebugConfig {
|
|
1402
|
+
timeTravel?: boolean;
|
|
1403
|
+
maxSnapshots?: number;
|
|
1404
|
+
}
|
|
1405
|
+
/** Time-travel API */
|
|
1406
|
+
interface TimeTravelAPI {
|
|
1407
|
+
readonly snapshots: Snapshot[];
|
|
1408
|
+
readonly currentIndex: number;
|
|
1409
|
+
readonly isPaused: boolean;
|
|
1410
|
+
goBack(steps?: number): void;
|
|
1411
|
+
goForward(steps?: number): void;
|
|
1412
|
+
goTo(snapshotId: number): void;
|
|
1413
|
+
replay(): void;
|
|
1414
|
+
export(): string;
|
|
1415
|
+
import(json: string): void;
|
|
1416
|
+
beginChangeset(label: string): void;
|
|
1417
|
+
endChangeset(): void;
|
|
1418
|
+
pause(): void;
|
|
1419
|
+
resume(): void;
|
|
1420
|
+
}
|
|
1421
|
+
/** Lightweight snapshot metadata (no facts data — keeps re-renders cheap) */
|
|
1422
|
+
interface SnapshotMeta {
|
|
1423
|
+
id: number;
|
|
1424
|
+
timestamp: number;
|
|
1425
|
+
trigger: string;
|
|
1426
|
+
}
|
|
1427
|
+
/** Reactive time-travel state for framework hooks */
|
|
1428
|
+
interface TimeTravelState {
|
|
1429
|
+
canUndo: boolean;
|
|
1430
|
+
canRedo: boolean;
|
|
1431
|
+
undo: () => void;
|
|
1432
|
+
redo: () => void;
|
|
1433
|
+
currentIndex: number;
|
|
1434
|
+
totalSnapshots: number;
|
|
1435
|
+
snapshots: SnapshotMeta[];
|
|
1436
|
+
getSnapshotFacts: (id: number) => Record<string, unknown> | null;
|
|
1437
|
+
goTo: (snapshotId: number) => void;
|
|
1438
|
+
goBack: (steps: number) => void;
|
|
1439
|
+
goForward: (steps: number) => void;
|
|
1440
|
+
replay: () => void;
|
|
1441
|
+
exportSession: () => string;
|
|
1442
|
+
importSession: (json: string) => void;
|
|
1443
|
+
beginChangeset: (label: string) => void;
|
|
1444
|
+
endChangeset: () => void;
|
|
1445
|
+
isPaused: boolean;
|
|
1446
|
+
pause: () => void;
|
|
1447
|
+
resume: () => void;
|
|
1448
|
+
}
|
|
1449
|
+
/** System inspection result */
|
|
1450
|
+
interface SystemInspection {
|
|
1451
|
+
unmet: RequirementWithId[];
|
|
1452
|
+
inflight: Array<{
|
|
1453
|
+
id: string;
|
|
1454
|
+
resolverId: string;
|
|
1455
|
+
startedAt: number;
|
|
1456
|
+
}>;
|
|
1457
|
+
constraints: Array<{
|
|
1458
|
+
id: string;
|
|
1459
|
+
active: boolean;
|
|
1460
|
+
priority: number;
|
|
1461
|
+
}>;
|
|
1462
|
+
resolvers: Record<string, ResolverStatus>;
|
|
1463
|
+
}
|
|
1464
|
+
/** Explanation of why a requirement exists */
|
|
1465
|
+
interface RequirementExplanation {
|
|
1466
|
+
requirementId: string;
|
|
1467
|
+
requirementType: string;
|
|
1468
|
+
constraintId: string;
|
|
1469
|
+
constraintPriority: number;
|
|
1470
|
+
relevantFacts: Record<string, unknown>;
|
|
1471
|
+
resolverStatus: ResolverStatus;
|
|
1472
|
+
}
|
|
1473
|
+
/** Serializable system snapshot for SSR/persistence */
|
|
1474
|
+
interface SystemSnapshot {
|
|
1475
|
+
facts: Record<string, unknown>;
|
|
1476
|
+
version?: number;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Options for creating a distributable snapshot.
|
|
1480
|
+
* Distributable snapshots contain computed derivation values that can be
|
|
1481
|
+
* serialized and distributed (JWT, Redis, edge KV) for use outside the runtime.
|
|
1482
|
+
*/
|
|
1483
|
+
interface DistributableSnapshotOptions {
|
|
1484
|
+
/** Derivation keys to include (default: all) */
|
|
1485
|
+
includeDerivations?: string[];
|
|
1486
|
+
/** Derivation keys to exclude */
|
|
1487
|
+
excludeDerivations?: string[];
|
|
1488
|
+
/** Fact keys to include (default: none) */
|
|
1489
|
+
includeFacts?: string[];
|
|
1490
|
+
/** TTL in seconds */
|
|
1491
|
+
ttlSeconds?: number;
|
|
1492
|
+
/** Custom metadata */
|
|
1493
|
+
metadata?: Record<string, unknown>;
|
|
1494
|
+
/** Include version hash for cache invalidation */
|
|
1495
|
+
includeVersion?: boolean;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* A distributable snapshot containing computed state.
|
|
1499
|
+
* This is a serializable object that can be stored in Redis, JWT, etc.
|
|
1500
|
+
*
|
|
1501
|
+
* @example
|
|
1502
|
+
* ```typescript
|
|
1503
|
+
* const snapshot = system.getDistributableSnapshot({
|
|
1504
|
+
* includeDerivations: ['effectivePlan', 'canUseFeature', 'limits'],
|
|
1505
|
+
* ttlSeconds: 3600,
|
|
1506
|
+
* });
|
|
1507
|
+
* // { data: { effectivePlan: "pro", canUseFeature: {...} }, createdAt: ..., expiresAt: ... }
|
|
1508
|
+
*
|
|
1509
|
+
* // Store in Redis
|
|
1510
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
1511
|
+
*
|
|
1512
|
+
* // Later, in an API route (no Directive runtime needed)
|
|
1513
|
+
* const cached = JSON.parse(await redis.get(`entitlements:${userId}`));
|
|
1514
|
+
* if (!cached.data.canUseFeature.api) throw new ForbiddenError();
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
interface DistributableSnapshot<T = Record<string, unknown>> {
|
|
1518
|
+
/** The computed derivation values and optionally included facts */
|
|
1519
|
+
data: T;
|
|
1520
|
+
/** Timestamp when this snapshot was created (ms since epoch) */
|
|
1521
|
+
createdAt: number;
|
|
1522
|
+
/** Timestamp when this snapshot expires (ms since epoch), if TTL was specified */
|
|
1523
|
+
expiresAt?: number;
|
|
1524
|
+
/** Version hash for cache invalidation, if includeVersion was true */
|
|
1525
|
+
version?: string;
|
|
1526
|
+
/** Custom metadata passed in options */
|
|
1527
|
+
metadata?: Record<string, unknown>;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* System interface using consolidated module schema.
|
|
1531
|
+
* Provides full type inference for facts, derivations, events, and dispatch.
|
|
1532
|
+
*/
|
|
1533
|
+
/** Runtime control for constraints */
|
|
1534
|
+
interface ConstraintsControl {
|
|
1535
|
+
/** Disable a constraint by ID — it will be excluded from evaluation */
|
|
1536
|
+
disable(id: string): void;
|
|
1537
|
+
/** Enable a previously disabled constraint — it will be re-evaluated on the next cycle */
|
|
1538
|
+
enable(id: string): void;
|
|
1539
|
+
}
|
|
1540
|
+
/** Runtime control for effects */
|
|
1541
|
+
interface EffectsControl {
|
|
1542
|
+
/** Disable an effect by ID — it will be skipped during reconciliation */
|
|
1543
|
+
disable(id: string): void;
|
|
1544
|
+
/** Enable a previously disabled effect */
|
|
1545
|
+
enable(id: string): void;
|
|
1546
|
+
/** Check if an effect is currently enabled */
|
|
1547
|
+
isEnabled(id: string): boolean;
|
|
1548
|
+
}
|
|
1549
|
+
interface System<M extends ModuleSchema = ModuleSchema> {
|
|
1550
|
+
readonly facts: Facts<M["facts"]>;
|
|
1551
|
+
readonly debug: TimeTravelAPI | null;
|
|
1552
|
+
readonly derive: InferDerivations<M>;
|
|
1553
|
+
readonly events: EventsAccessorFromSchema<M>;
|
|
1554
|
+
readonly constraints: ConstraintsControl;
|
|
1555
|
+
readonly effects: EffectsControl;
|
|
1556
|
+
start(): void;
|
|
1557
|
+
stop(): void;
|
|
1558
|
+
destroy(): void;
|
|
1559
|
+
readonly isRunning: boolean;
|
|
1560
|
+
readonly isSettled: boolean;
|
|
1561
|
+
/** Whether all modules have completed initialization */
|
|
1562
|
+
readonly isInitialized: boolean;
|
|
1563
|
+
/** Whether system has completed first reconciliation */
|
|
1564
|
+
readonly isReady: boolean;
|
|
1565
|
+
/** Wait for system to be fully ready (after first reconciliation) */
|
|
1566
|
+
whenReady(): Promise<void>;
|
|
1567
|
+
dispatch(event: InferEvents<M>): void;
|
|
1568
|
+
dispatch(event: SystemEvent): void;
|
|
1569
|
+
batch(fn: () => void): void;
|
|
1570
|
+
/**
|
|
1571
|
+
* Subscribe to settlement state changes.
|
|
1572
|
+
* Called whenever the system's settled state may have changed
|
|
1573
|
+
* (resolver starts/completes, reconcile starts/ends).
|
|
1574
|
+
*/
|
|
1575
|
+
onSettledChange(listener: () => void): () => void;
|
|
1576
|
+
/**
|
|
1577
|
+
* Subscribe to time-travel state changes.
|
|
1578
|
+
* Called whenever a snapshot is taken or time-travel navigation occurs.
|
|
1579
|
+
* Returns an unsubscribe function.
|
|
1580
|
+
*/
|
|
1581
|
+
onTimeTravelChange(listener: () => void): () => void;
|
|
1582
|
+
read<K extends DerivationKeys<M>>(derivationId: K): DerivationReturnType<M, K>;
|
|
1583
|
+
read<T = unknown>(derivationId: string): T;
|
|
1584
|
+
/**
|
|
1585
|
+
* Subscribe to fact or derivation changes.
|
|
1586
|
+
* Keys are auto-detected -- pass any mix of fact keys and derivation keys.
|
|
1587
|
+
* @example system.subscribe(["count", "doubled"], () => { ... })
|
|
1588
|
+
*/
|
|
1589
|
+
subscribe(ids: Array<ObservableKeys<M>>, listener: () => void): () => void;
|
|
1590
|
+
/**
|
|
1591
|
+
* Watch a fact or derivation for value changes.
|
|
1592
|
+
* The key is auto-detected -- works with both fact keys and derivation keys.
|
|
1593
|
+
* Pass `options.equalityFn` for custom comparison (e.g., shallow equality for objects).
|
|
1594
|
+
* @example system.watch("count", (newVal, oldVal) => { ... })
|
|
1595
|
+
* @example system.watch("derived", cb, { equalityFn: shallowEqual })
|
|
1596
|
+
*/
|
|
1597
|
+
watch<K extends DerivationKeys<M>>(id: K, callback: (newValue: DerivationReturnType<M, K>, previousValue: DerivationReturnType<M, K> | undefined) => void, options?: {
|
|
1598
|
+
equalityFn?: (a: DerivationReturnType<M, K>, b: DerivationReturnType<M, K> | undefined) => boolean;
|
|
1599
|
+
}): () => void;
|
|
1600
|
+
watch<K extends FactKeys<M>>(id: K, callback: (newValue: FactReturnType<M, K>, previousValue: FactReturnType<M, K> | undefined) => void, options?: {
|
|
1601
|
+
equalityFn?: (a: FactReturnType<M, K>, b: FactReturnType<M, K> | undefined) => boolean;
|
|
1602
|
+
}): () => void;
|
|
1603
|
+
watch<T = unknown>(id: string, callback: (newValue: T, previousValue: T | undefined) => void, options?: {
|
|
1604
|
+
equalityFn?: (a: T, b: T | undefined) => boolean;
|
|
1605
|
+
}): () => void;
|
|
1606
|
+
/**
|
|
1607
|
+
* Returns a promise that resolves when the predicate becomes true.
|
|
1608
|
+
* The predicate is evaluated against current facts and re-evaluated on every change.
|
|
1609
|
+
* Optionally pass a timeout in ms -- rejects with an error if exceeded.
|
|
1610
|
+
*
|
|
1611
|
+
* @example
|
|
1612
|
+
* await system.when((facts) => facts.phase === "ready");
|
|
1613
|
+
* @example
|
|
1614
|
+
* await system.when((facts) => facts.count > 10, { timeout: 5000 });
|
|
1615
|
+
*/
|
|
1616
|
+
when(predicate: (facts: Readonly<InferFacts<M>>) => boolean, options?: {
|
|
1617
|
+
timeout?: number;
|
|
1618
|
+
}): Promise<void>;
|
|
1619
|
+
inspect(): SystemInspection;
|
|
1620
|
+
settle(maxWait?: number): Promise<void>;
|
|
1621
|
+
explain(requirementId: string): string | null;
|
|
1622
|
+
getSnapshot(): SystemSnapshot;
|
|
1623
|
+
restore(snapshot: SystemSnapshot): void;
|
|
1624
|
+
/**
|
|
1625
|
+
* Get a distributable snapshot of computed derivations.
|
|
1626
|
+
* This creates a serializable object that can be stored in Redis, JWT, etc.
|
|
1627
|
+
* for use outside the Directive runtime.
|
|
1628
|
+
*
|
|
1629
|
+
* @example
|
|
1630
|
+
* ```typescript
|
|
1631
|
+
* const snapshot = system.getDistributableSnapshot({
|
|
1632
|
+
* includeDerivations: ['effectivePlan', 'canUseFeature'],
|
|
1633
|
+
* ttlSeconds: 3600,
|
|
1634
|
+
* });
|
|
1635
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
1636
|
+
* ```
|
|
1637
|
+
*/
|
|
1638
|
+
getDistributableSnapshot<T = Record<string, unknown>>(options?: DistributableSnapshotOptions): DistributableSnapshot<T>;
|
|
1639
|
+
/**
|
|
1640
|
+
* Watch for changes to distributable snapshot derivations.
|
|
1641
|
+
* Calls the callback whenever any of the included derivations change.
|
|
1642
|
+
* Returns an unsubscribe function.
|
|
1643
|
+
*
|
|
1644
|
+
* @example
|
|
1645
|
+
* ```typescript
|
|
1646
|
+
* const unsubscribe = system.watchDistributableSnapshot(
|
|
1647
|
+
* { includeDerivations: ['effectivePlan', 'canUseFeature'] },
|
|
1648
|
+
* (snapshot) => {
|
|
1649
|
+
* // Snapshot changed - push to Redis/edge cache
|
|
1650
|
+
* await redis.setex(`entitlements:${userId}`, 3600, JSON.stringify(snapshot));
|
|
1651
|
+
* }
|
|
1652
|
+
* );
|
|
1653
|
+
*
|
|
1654
|
+
* // Later, cleanup
|
|
1655
|
+
* unsubscribe();
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|
|
1658
|
+
watchDistributableSnapshot<T = Record<string, unknown>>(options: DistributableSnapshotOptions, callback: (snapshot: DistributableSnapshot<T>) => void): () => void;
|
|
1659
|
+
}
|
|
1660
|
+
/** System configuration */
|
|
1661
|
+
interface SystemConfig<M extends ModuleSchema = ModuleSchema> {
|
|
1662
|
+
modules: Array<ModuleDef<M>>;
|
|
1663
|
+
plugins?: Array<Plugin<any>>;
|
|
1664
|
+
debug?: DebugConfig;
|
|
1665
|
+
errorBoundary?: ErrorBoundaryConfig;
|
|
1666
|
+
/**
|
|
1667
|
+
* Callback invoked after module inits but before first reconcile.
|
|
1668
|
+
* Used by system wrapper to apply initialFacts/hydrate at the right time.
|
|
1669
|
+
* @internal
|
|
1670
|
+
*/
|
|
1671
|
+
onAfterModuleInit?: () => void;
|
|
1672
|
+
tickMs?: number;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Plugin Types - Type definitions for plugins
|
|
1677
|
+
*/
|
|
1678
|
+
|
|
1679
|
+
/** Reconcile result */
|
|
1680
|
+
interface ReconcileResult {
|
|
1681
|
+
unmet: RequirementWithId[];
|
|
1682
|
+
inflight: Array<{
|
|
1683
|
+
id: string;
|
|
1684
|
+
resolverId: string;
|
|
1685
|
+
startedAt: number;
|
|
1686
|
+
}>;
|
|
1687
|
+
completed: Array<{
|
|
1688
|
+
id: string;
|
|
1689
|
+
resolverId: string;
|
|
1690
|
+
duration: number;
|
|
1691
|
+
}>;
|
|
1692
|
+
canceled: Array<{
|
|
1693
|
+
id: string;
|
|
1694
|
+
resolverId: string;
|
|
1695
|
+
}>;
|
|
1696
|
+
}
|
|
1697
|
+
/** Snapshot for time-travel */
|
|
1698
|
+
interface Snapshot {
|
|
1699
|
+
id: number;
|
|
1700
|
+
timestamp: number;
|
|
1701
|
+
facts: Record<string, unknown>;
|
|
1702
|
+
trigger: string;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Plugin interface for extending Directive functionality.
|
|
1706
|
+
*
|
|
1707
|
+
* Plugins receive lifecycle hooks at every stage of the system's operation.
|
|
1708
|
+
* All hooks except `onInit` are synchronous - use them for logging, metrics,
|
|
1709
|
+
* or triggering external effects, not for async operations that should block.
|
|
1710
|
+
*/
|
|
1711
|
+
interface Plugin<M extends ModuleSchema = ModuleSchema> {
|
|
1712
|
+
/** Unique name for this plugin (used in error messages and debugging) */
|
|
1713
|
+
name: string;
|
|
1714
|
+
/**
|
|
1715
|
+
* Called once when the system is created, before start().
|
|
1716
|
+
* This is the only async hook - use it for async initialization.
|
|
1717
|
+
* @param system - The system instance
|
|
1718
|
+
*/
|
|
1719
|
+
onInit?: (system: System<M>) => void | Promise<void>;
|
|
1720
|
+
/**
|
|
1721
|
+
* Called when system.start() is invoked.
|
|
1722
|
+
* Module init functions have already run at this point.
|
|
1723
|
+
* @param system - The system instance
|
|
1724
|
+
*/
|
|
1725
|
+
onStart?: (system: System<M>) => void;
|
|
1726
|
+
/**
|
|
1727
|
+
* Called when system.stop() is invoked.
|
|
1728
|
+
* All resolvers have been canceled at this point.
|
|
1729
|
+
* @param system - The system instance
|
|
1730
|
+
*/
|
|
1731
|
+
onStop?: (system: System<M>) => void;
|
|
1732
|
+
/**
|
|
1733
|
+
* Called when system.destroy() is invoked.
|
|
1734
|
+
* Use for final cleanup (closing connections, etc.).
|
|
1735
|
+
* @param system - The system instance
|
|
1736
|
+
*/
|
|
1737
|
+
onDestroy?: (system: System<M>) => void;
|
|
1738
|
+
/**
|
|
1739
|
+
* Called when a single fact is set (not during batch).
|
|
1740
|
+
* @param key - The fact key that changed
|
|
1741
|
+
* @param value - The new value
|
|
1742
|
+
* @param prev - The previous value (undefined if new)
|
|
1743
|
+
*/
|
|
1744
|
+
onFactSet?: (key: string, value: unknown, prev: unknown) => void;
|
|
1745
|
+
/**
|
|
1746
|
+
* Called when a fact is deleted.
|
|
1747
|
+
* @param key - The fact key that was deleted
|
|
1748
|
+
* @param prev - The previous value
|
|
1749
|
+
*/
|
|
1750
|
+
onFactDelete?: (key: string, prev: unknown) => void;
|
|
1751
|
+
/**
|
|
1752
|
+
* Called after a batch of fact changes completes.
|
|
1753
|
+
* Use this instead of onFactSet for batched operations.
|
|
1754
|
+
* @param changes - Array of all changes in the batch
|
|
1755
|
+
*/
|
|
1756
|
+
onFactsBatch?: (changes: FactChange[]) => void;
|
|
1757
|
+
/**
|
|
1758
|
+
* Called when a derivation is computed (or recomputed).
|
|
1759
|
+
* @param id - The derivation ID
|
|
1760
|
+
* @param value - The computed value
|
|
1761
|
+
* @param deps - Array of fact keys this derivation depends on
|
|
1762
|
+
*/
|
|
1763
|
+
onDerivationCompute?: (id: string, value: unknown, deps: string[]) => void;
|
|
1764
|
+
/**
|
|
1765
|
+
* Called when a derivation is invalidated (marked stale).
|
|
1766
|
+
* The derivation will be recomputed on next access.
|
|
1767
|
+
* @param id - The derivation ID
|
|
1768
|
+
*/
|
|
1769
|
+
onDerivationInvalidate?: (id: string) => void;
|
|
1770
|
+
/**
|
|
1771
|
+
* Called at the start of each reconciliation loop.
|
|
1772
|
+
* @param snapshot - Read-only snapshot of current facts
|
|
1773
|
+
*/
|
|
1774
|
+
onReconcileStart?: (snapshot: FactsSnapshot<M["facts"]>) => void;
|
|
1775
|
+
/**
|
|
1776
|
+
* Called at the end of each reconciliation loop.
|
|
1777
|
+
* @param result - Summary of what happened (unmet, inflight, completed, canceled)
|
|
1778
|
+
*/
|
|
1779
|
+
onReconcileEnd?: (result: ReconcileResult) => void;
|
|
1780
|
+
/**
|
|
1781
|
+
* Called after a constraint's `when` function is evaluated.
|
|
1782
|
+
* @param id - The constraint ID
|
|
1783
|
+
* @param active - Whether the constraint is active (when returned true)
|
|
1784
|
+
*/
|
|
1785
|
+
onConstraintEvaluate?: (id: string, active: boolean) => void;
|
|
1786
|
+
/**
|
|
1787
|
+
* Called when a constraint's `when` function throws an error.
|
|
1788
|
+
* @param id - The constraint ID
|
|
1789
|
+
* @param error - The error that was thrown
|
|
1790
|
+
*/
|
|
1791
|
+
onConstraintError?: (id: string, error: unknown) => void;
|
|
1792
|
+
/**
|
|
1793
|
+
* Called when a new requirement is created by a constraint.
|
|
1794
|
+
* @param req - The requirement with its computed ID
|
|
1795
|
+
*/
|
|
1796
|
+
onRequirementCreated?: (req: RequirementWithId) => void;
|
|
1797
|
+
/**
|
|
1798
|
+
* Called when a requirement is fulfilled by a resolver.
|
|
1799
|
+
* @param req - The requirement that was met
|
|
1800
|
+
* @param byResolver - The ID of the resolver that fulfilled it
|
|
1801
|
+
*/
|
|
1802
|
+
onRequirementMet?: (req: RequirementWithId, byResolver: string) => void;
|
|
1803
|
+
/**
|
|
1804
|
+
* Called when a requirement is canceled (constraint no longer active).
|
|
1805
|
+
* @param req - The requirement that was canceled
|
|
1806
|
+
*/
|
|
1807
|
+
onRequirementCanceled?: (req: RequirementWithId) => void;
|
|
1808
|
+
/**
|
|
1809
|
+
* Called when a resolver starts processing a requirement.
|
|
1810
|
+
* @param resolver - The resolver ID
|
|
1811
|
+
* @param req - The requirement being resolved
|
|
1812
|
+
*/
|
|
1813
|
+
onResolverStart?: (resolver: string, req: RequirementWithId) => void;
|
|
1814
|
+
/**
|
|
1815
|
+
* Called when a resolver successfully completes.
|
|
1816
|
+
* @param resolver - The resolver ID
|
|
1817
|
+
* @param req - The requirement that was resolved
|
|
1818
|
+
* @param duration - Time in ms to complete
|
|
1819
|
+
*/
|
|
1820
|
+
onResolverComplete?: (resolver: string, req: RequirementWithId, duration: number) => void;
|
|
1821
|
+
/**
|
|
1822
|
+
* Called when a resolver fails (after all retries exhausted).
|
|
1823
|
+
* @param resolver - The resolver ID
|
|
1824
|
+
* @param req - The requirement that failed
|
|
1825
|
+
* @param error - The final error
|
|
1826
|
+
*/
|
|
1827
|
+
onResolverError?: (resolver: string, req: RequirementWithId, error: unknown) => void;
|
|
1828
|
+
/**
|
|
1829
|
+
* Called when a resolver is about to retry after failure.
|
|
1830
|
+
* @param resolver - The resolver ID
|
|
1831
|
+
* @param req - The requirement being retried
|
|
1832
|
+
* @param attempt - The attempt number (2 for first retry, etc.)
|
|
1833
|
+
*/
|
|
1834
|
+
onResolverRetry?: (resolver: string, req: RequirementWithId, attempt: number) => void;
|
|
1835
|
+
/**
|
|
1836
|
+
* Called when a resolver is canceled (requirement no longer needed).
|
|
1837
|
+
* @param resolver - The resolver ID
|
|
1838
|
+
* @param req - The requirement that was canceled
|
|
1839
|
+
*/
|
|
1840
|
+
onResolverCancel?: (resolver: string, req: RequirementWithId) => void;
|
|
1841
|
+
/**
|
|
1842
|
+
* Called when an effect runs.
|
|
1843
|
+
* @param id - The effect ID
|
|
1844
|
+
*/
|
|
1845
|
+
onEffectRun?: (id: string) => void;
|
|
1846
|
+
/**
|
|
1847
|
+
* Called when an effect throws an error.
|
|
1848
|
+
* @param id - The effect ID
|
|
1849
|
+
* @param error - The error that was thrown
|
|
1850
|
+
*/
|
|
1851
|
+
onEffectError?: (id: string, error: unknown) => void;
|
|
1852
|
+
/**
|
|
1853
|
+
* Called when a time-travel snapshot is taken.
|
|
1854
|
+
* @param snapshot - The snapshot that was captured
|
|
1855
|
+
*/
|
|
1856
|
+
onSnapshot?: (snapshot: Snapshot) => void;
|
|
1857
|
+
/**
|
|
1858
|
+
* Called when time-travel navigation occurs.
|
|
1859
|
+
* @param from - The index we navigated from
|
|
1860
|
+
* @param to - The index we navigated to
|
|
1861
|
+
*/
|
|
1862
|
+
onTimeTravel?: (from: number, to: number) => void;
|
|
1863
|
+
/**
|
|
1864
|
+
* Called when any error occurs in the system.
|
|
1865
|
+
* @param error - The DirectiveError with source and context
|
|
1866
|
+
*/
|
|
1867
|
+
onError?: (error: DirectiveError) => void;
|
|
1868
|
+
/**
|
|
1869
|
+
* Called when error recovery is attempted.
|
|
1870
|
+
* @param error - The error that triggered recovery
|
|
1871
|
+
* @param strategy - The recovery strategy used
|
|
1872
|
+
*/
|
|
1873
|
+
onErrorRecovery?: (error: DirectiveError, strategy: RecoveryStrategy) => void;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
export { type BatchItemResult as $, type ConstraintState as A, type BatchConfig as B, type CrossModuleDeps as C, type DerivationsSchema as D, type EffectsDef as E, type Facts as F, type ResolversDef as G, type ResolverStatus as H, type InferRequirements as I, type System as J, type FactChange as K, type FactsSnapshot as L, type ModuleSchema as M, type NamespacedSystem as N, type ReconcileResult as O, type Plugin as P, type Snapshot as Q, type Requirement as R, type Schema as S, type TypedDerivationsDef as T, DirectiveError as U, type RecoveryStrategy as V, type ErrorSource as W, type RetryLaterConfig as X, type TimeTravelAPI as Y, type SystemConfig as Z, type AnySystem as _, type RequirementOutput$1 as a, type BatchResolveResults as a0, type CircuitBreakerConfig as a1, type CircuitBreakerState as a2, type CrossModuleConstraintDef as a3, type CrossModuleDerivationFn as a4, type CrossModuleEffectDef as a5, type CrossModuleFactsWithSelf as a6, type DerivationKeys as a7, type DerivationReturnType as a8, type DeriveAccessor as a9, type RequirementPayloadSchema$1 as aA, type RequirementsSchema as aB, type SnapshotMeta as aC, type SystemEvent as aD, type SystemInspection as aE, type SystemMode as aF, type SystemSnapshot as aG, type TimeTravelState as aH, type TypedResolverContext as aI, type TypedResolverDef as aJ, type UnionEvents as aK, isNamespacedSystem as aL, isSingleModuleSystem as aM, type DispatchEventsFromSchema as aa, type DistributableSnapshot as ab, type DistributableSnapshotOptions as ac, type EffectCleanup as ad, type EventPayloadSchema as ae, type EventsAccessor as af, type EventsAccessorFromSchema as ag, type EventsDef as ah, type EventsSchema as ai, type FactKeys as aj, type FactReturnType as ak, type FlexibleEventHandler as al, type InferDerivations as am, type InferEventPayloadFromSchema as an, type InferEvents as ao, type InferRequirementPayloadFromSchema as ap, type InferRequirementTypes as aq, type InferSchema as ar, type InferSchemaType as as, type InferSelectorState as at, type MutableNamespacedFacts as au, type NamespacedDerivations as av, type NamespacedEventsAccessor as aw, type NamespacedFacts as ax, type ObservableKeys as ay, type RequirementExplanation as az, type RetryPolicy$1 as b, type ResolverContext as c, type SchemaType as d, type FactsStore as e, type TypedEventsDef as f, type TypedConstraintsDef as g, type TypedResolversDef as h, type ModuleHooks as i, type CrossModuleDerivationsDef as j, type CrossModuleEffectsDef as k, type CrossModuleConstraintsDef as l, type ModuleDef as m, type CreateSystemOptionsSingle as n, type SingleModuleSystem as o, type ModulesMap as p, type CreateSystemOptionsNamed as q, type TypedConstraintDef as r, type RequirementOutput as s, type DebugConfig as t, type ErrorBoundaryConfig as u, type InferFacts as v, type ExtractSchema as w, type RequirementWithId as x, type RequirementKeyFn as y, type ConstraintsDef as z };
|