@kronos-ts/app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +228 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +519 -0
- package/dist/app.js.map +1 -0
- package/dist/components.d.ts +28 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +14 -0
- package/dist/components.js.map +1 -0
- package/dist/decorator.d.ts +54 -0
- package/dist/decorator.d.ts.map +1 -0
- package/dist/decorator.js +36 -0
- package/dist/decorator.js.map +1 -0
- package/dist/defaults-handles.d.ts +25 -0
- package/dist/defaults-handles.d.ts.map +1 -0
- package/dist/defaults-handles.js +25 -0
- package/dist/defaults-handles.js.map +1 -0
- package/dist/defaults.d.ts +10 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +49 -0
- package/dist/defaults.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/kronos.d.ts +51 -0
- package/dist/kronos.d.ts.map +1 -0
- package/dist/kronos.js +74 -0
- package/dist/kronos.js.map +1 -0
- package/dist/lifecycle.d.ts +27 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +7 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/resolved.d.ts +18 -0
- package/dist/resolved.d.ts.map +1 -0
- package/dist/resolved.js +43 -0
- package/dist/resolved.js.map +1 -0
- package/dist/slot-registry.d.ts +35 -0
- package/dist/slot-registry.d.ts.map +1 -0
- package/dist/slot-registry.js +50 -0
- package/dist/slot-registry.js.map +1 -0
- package/dist/warnings.d.ts +19 -0
- package/dist/warnings.d.ts.map +1 -0
- package/dist/warnings.js +14 -0
- package/dist/warnings.js.map +1 -0
- package/package.json +59 -0
- package/src/app.ts +795 -0
- package/src/components.ts +48 -0
- package/src/decorator.ts +88 -0
- package/src/defaults-handles.ts +29 -0
- package/src/defaults.ts +64 -0
- package/src/errors.ts +62 -0
- package/src/index.ts +38 -0
- package/src/kronos.ts +117 -0
- package/src/lifecycle.ts +33 -0
- package/src/resolved.ts +54 -0
- package/src/slot-registry.ts +89 -0
- package/src/warnings.ts +32 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import type { StateModule } from "@kronos-ts/modelling"
|
|
2
|
+
import { createStateManager, type StateManager } from "@kronos-ts/modelling"
|
|
3
|
+
import type {
|
|
4
|
+
CommandHandlerDefinition,
|
|
5
|
+
QueryHandlerDefinition,
|
|
6
|
+
EventProcessorModule,
|
|
7
|
+
CommandGateway,
|
|
8
|
+
QueryGateway,
|
|
9
|
+
CommandMessage,
|
|
10
|
+
QueryMessage,
|
|
11
|
+
EventMessage,
|
|
12
|
+
DispatchInterceptor,
|
|
13
|
+
HandlerInterceptor,
|
|
14
|
+
HandlerEnhancerDefinition,
|
|
15
|
+
TrackingEventProcessor,
|
|
16
|
+
SubscribingEventProcessor,
|
|
17
|
+
StreamableEventSource,
|
|
18
|
+
SubscribableEventSource,
|
|
19
|
+
} from "@kronos-ts/messaging"
|
|
20
|
+
import {
|
|
21
|
+
registerCommandHandlersNatively,
|
|
22
|
+
registerQueryHandlersNatively,
|
|
23
|
+
createCommandGateway,
|
|
24
|
+
createQueryGateway,
|
|
25
|
+
createTrackingEventProcessor,
|
|
26
|
+
createSubscribingEventProcessor,
|
|
27
|
+
multiHandlerEnhancerDefinition,
|
|
28
|
+
} from "@kronos-ts/messaging"
|
|
29
|
+
import { createEventSourcedRepository } from "@kronos-ts/eventsourcing"
|
|
30
|
+
import type { SnapshotPolicy, SnapshotStore } from "@kronos-ts/eventsourcing"
|
|
31
|
+
import type { MinimalConfiguration } from "@kronos-ts/messaging"
|
|
32
|
+
import { ALL_SLOTS, type KronosComponents, type SlotName } from "./components.js"
|
|
33
|
+
import { SlotRegistry, type SlotFactory, type SlotMeta } from "./slot-registry.js"
|
|
34
|
+
import { buildResolved } from "./resolved.js"
|
|
35
|
+
import type { WarningChannel } from "./warnings.js"
|
|
36
|
+
import type { DecoratorEntry, DecoratorFactory, DecoratorHandle } from "./decorator.js"
|
|
37
|
+
import { applyDecorators } from "./decorator.js"
|
|
38
|
+
import { AppNotStartedError, UnknownDecoratorHandleError } from "./errors.js"
|
|
39
|
+
import type { LifecycleStage, LifecycleHook } from "./lifecycle.js"
|
|
40
|
+
|
|
41
|
+
/** Thrown when the App's mutating methods are called after .start() (D-50 footgun closure). */
|
|
42
|
+
export class AppAlreadyStartedError extends Error {
|
|
43
|
+
constructor() {
|
|
44
|
+
super("[kronos] App has already been started; configuration is immutable after start().")
|
|
45
|
+
this.name = "AppAlreadyStartedError"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extension shape in Phase 5 (D-50). Phase 6/7/9 will widen this; Phase 5 keeps it minimal.
|
|
51
|
+
*/
|
|
52
|
+
export type Extension = (app: App) => void | Promise<void>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Plan 09-01 (D-88): per-state options accepted in App.states() tuple form.
|
|
56
|
+
* Both fields optional — extensions or user code that wants snapshotting on a
|
|
57
|
+
* particular state passes one or both, and the value flows through into
|
|
58
|
+
* createEventSourcedRepository at start-time Step 5a.
|
|
59
|
+
*/
|
|
60
|
+
export interface StateOptions {
|
|
61
|
+
readonly snapshotPolicy?: SnapshotPolicy
|
|
62
|
+
readonly snapshotStore?: SnapshotStore
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Plan 09-01 (D-88): argument shape accepted by App.states() — either a bare
|
|
67
|
+
* StateModule or a [module, options] tuple. Mixed lists are fine.
|
|
68
|
+
*/
|
|
69
|
+
// StateModule<any, any>: the Id parameter sits in a contravariant position
|
|
70
|
+
// (`create`/`criteria` accept it), so a concrete StateModule<{courseId:string},…>
|
|
71
|
+
// is NOT assignable to StateModule<unknown,unknown>. `any` accepts any module.
|
|
72
|
+
export type StatesArg =
|
|
73
|
+
| StateModule<any, any>
|
|
74
|
+
| readonly [StateModule<any, any>, StateOptions]
|
|
75
|
+
|
|
76
|
+
export interface KronosIdentity {
|
|
77
|
+
/** Stable logical service/application name. Same across replicas. */
|
|
78
|
+
readonly serviceName: string
|
|
79
|
+
/** Unique physical runtime instance id. Different per process/pod. */
|
|
80
|
+
readonly instanceId: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface App {
|
|
84
|
+
readonly identity: KronosIdentity
|
|
85
|
+
states(...args: StatesArg[]): App
|
|
86
|
+
commands(...handlers: CommandHandlerDefinition<any, any>[]): App
|
|
87
|
+
queries(...handlers: QueryHandlerDefinition[]): App
|
|
88
|
+
/**
|
|
89
|
+
* Read accessor (Plan 09-01, D-103): when called with zero arguments,
|
|
90
|
+
* returns the registered EventProcessorModule[] in registration order.
|
|
91
|
+
* Returned array is a frozen view; mutations have no effect on app state.
|
|
92
|
+
* Consumed by extensions (e.g. Axon Server) inside `onStart('connect', ...)`
|
|
93
|
+
* to build a fan-out registration list before processors start.
|
|
94
|
+
*/
|
|
95
|
+
processors(): readonly EventProcessorModule[]
|
|
96
|
+
/** Writer overload (D-103): appends EventProcessorModule registrations. */
|
|
97
|
+
processors(...modules: EventProcessorModule[]): App
|
|
98
|
+
/** D-73: register an Extension (function) — runs during start() before slot resolution. */
|
|
99
|
+
use(extension: Extension): App
|
|
100
|
+
setDefault<K extends SlotName>(
|
|
101
|
+
slot: K,
|
|
102
|
+
factory: SlotFactory<K> | KronosComponents[K],
|
|
103
|
+
meta?: SlotMeta,
|
|
104
|
+
): App
|
|
105
|
+
set<K extends SlotName>(slot: K, factory: SlotFactory<K> | KronosComponents[K]): App
|
|
106
|
+
forceSet<K extends SlotName>(slot: K, factory: SlotFactory<K> | KronosComponents[K]): App
|
|
107
|
+
decorate<K extends SlotName>(slot: K, factory: DecoratorFactory<K>): DecoratorHandle<K>
|
|
108
|
+
removeDecorator<K extends SlotName>(handle: DecoratorHandle<K>): App
|
|
109
|
+
/**
|
|
110
|
+
* Register a dispatch interceptor for the command bus. The interceptor runs as
|
|
111
|
+
* part of the framework `intercepting` default decorator (Defaults.commandBus.intercepting).
|
|
112
|
+
* If that default has been removed via `removeDecorator()`, registered interceptors
|
|
113
|
+
* have no effect on dispatch.
|
|
114
|
+
*/
|
|
115
|
+
commandDispatchInterceptor(fn: DispatchInterceptor<CommandMessage>): App
|
|
116
|
+
/** Same as commandDispatchInterceptor, scoped to the query bus. */
|
|
117
|
+
queryDispatchInterceptor(fn: DispatchInterceptor<QueryMessage>): App
|
|
118
|
+
/** Same as commandDispatchInterceptor, scoped to the event bus. */
|
|
119
|
+
eventDispatchInterceptor(fn: DispatchInterceptor<EventMessage>): App
|
|
120
|
+
/**
|
|
121
|
+
* Register a bus-agnostic handler interceptor. Wired into the framework `intercepting`
|
|
122
|
+
* defaults for commandBus and queryBus only — eventBus has no handler-interceptor concept
|
|
123
|
+
* in the existing intercepting wrapper.
|
|
124
|
+
*/
|
|
125
|
+
handlerInterceptor(fn: HandlerInterceptor): App
|
|
126
|
+
/**
|
|
127
|
+
* Plan 09-01 (D-86): accumulator for HandlerEnhancerDefinition. Mirrors the
|
|
128
|
+
* Phase 6 dispatch-interceptor accumulator pattern. Multiple registrations
|
|
129
|
+
* compose left-to-right via multiHandlerEnhancerDefinition (first registered
|
|
130
|
+
* wraps outermost). Composed at start-time and threaded through:
|
|
131
|
+
* - registerCommandHandlersNatively (Step 5c)
|
|
132
|
+
* - registerQueryHandlersNatively (Step 5d, RESEARCH Open Question #4)
|
|
133
|
+
* - createTrackingEventProcessor / createSubscribingEventProcessor (Step 5e)
|
|
134
|
+
*
|
|
135
|
+
* Returns App for chaining. Throws AppAlreadyStartedError after .start().
|
|
136
|
+
*/
|
|
137
|
+
handlerEnhancer(def: HandlerEnhancerDefinition): App
|
|
138
|
+
/**
|
|
139
|
+
* Live CommandGateway. Throws AppNotStartedError if accessed before the
|
|
140
|
+
* `register` lifecycle stage completes during `.start()`. Available inside
|
|
141
|
+
* `onStart('warmup'|'register'|'processors'|'serve', fn)` hooks (after register)
|
|
142
|
+
* and after `.start()` resolves. (Plan 08-01.)
|
|
143
|
+
*/
|
|
144
|
+
readonly commandGateway: CommandGateway
|
|
145
|
+
/**
|
|
146
|
+
* Live QueryGateway. Throws AppNotStartedError if accessed before the
|
|
147
|
+
* `register` lifecycle stage completes during `.start()`. Available inside
|
|
148
|
+
* `onStart('warmup'|'register'|'processors'|'serve', fn)` hooks (after register)
|
|
149
|
+
* and after `.start()` resolves. (Plan 08-01.)
|
|
150
|
+
*/
|
|
151
|
+
readonly queryGateway: QueryGateway
|
|
152
|
+
/**
|
|
153
|
+
* Register a hook that runs at the given lifecycle stage during `.start()`.
|
|
154
|
+
* Stages execute in forward order (connect → warmup → register → processors → serve).
|
|
155
|
+
* Within a stage, hooks execute in registration order. Throws AppAlreadyStartedError
|
|
156
|
+
* if called after `.start()`. (LIF-02, D-68, D-69, D-70)
|
|
157
|
+
*/
|
|
158
|
+
onStart(stage: LifecycleStage, fn: LifecycleHook): App
|
|
159
|
+
/**
|
|
160
|
+
* Register a hook that runs at the given lifecycle stage during `.stop()`.
|
|
161
|
+
* Stages execute in reverse order (serve → processors → register → warmup → connect).
|
|
162
|
+
* Within a stage, hooks execute in registration order. Throws AppAlreadyStartedError
|
|
163
|
+
* if called after `.start()`. (LIF-02, D-68, D-69, D-70)
|
|
164
|
+
*/
|
|
165
|
+
onStop(stage: LifecycleStage, fn: LifecycleHook): App
|
|
166
|
+
start(): Promise<RunningApp>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface RunningApp {
|
|
170
|
+
readonly identity: KronosIdentity
|
|
171
|
+
readonly commandGateway: CommandGateway
|
|
172
|
+
readonly queryGateway: QueryGateway
|
|
173
|
+
stop(): Promise<void>
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Internal accumulators populated by fluent methods; consumed by .start(). */
|
|
177
|
+
export interface AppState {
|
|
178
|
+
readonly slotRegistry: SlotRegistry
|
|
179
|
+
/** Plan 09-01 (D-88): replaces flat `states: StateModule[]` to carry per-state options. */
|
|
180
|
+
readonly stateEntries: Array<{ module: StateModule; options: StateOptions }>
|
|
181
|
+
readonly commandHandlers: CommandHandlerDefinition<any, any>[]
|
|
182
|
+
readonly queryHandlers: QueryHandlerDefinition[]
|
|
183
|
+
readonly processors: EventProcessorModule[]
|
|
184
|
+
readonly extensions: Extension[]
|
|
185
|
+
readonly warningChannel: WarningChannel
|
|
186
|
+
readonly decoratorRegistrations: DecoratorEntry[] // NEW: per-app registration order; pipeline = left-to-right
|
|
187
|
+
readonly commandDispatchInterceptors: DispatchInterceptor<CommandMessage>[]
|
|
188
|
+
readonly queryDispatchInterceptors: DispatchInterceptor<QueryMessage>[]
|
|
189
|
+
readonly eventDispatchInterceptors: DispatchInterceptor<EventMessage>[]
|
|
190
|
+
readonly handlerInterceptors: HandlerInterceptor[]
|
|
191
|
+
/** Plan 09-01 (D-86): accumulator composed via multiHandlerEnhancerDefinition at start. */
|
|
192
|
+
readonly handlerEnhancers: HandlerEnhancerDefinition[]
|
|
193
|
+
readonly startHooks: Array<{ stage: LifecycleStage; fn: LifecycleHook }>
|
|
194
|
+
readonly stopHooks: Array<{ stage: LifecycleStage; fn: LifecycleHook }>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface AppImplOptions {
|
|
198
|
+
warningChannel: WarningChannel
|
|
199
|
+
/** Stable logical service/application name. Same across replicas. */
|
|
200
|
+
serviceName?: string
|
|
201
|
+
/** Unique physical runtime instance id. Different per process/pod. */
|
|
202
|
+
instanceId?: string
|
|
203
|
+
/** Per-stage timeout (ms) for native lifecycle execution (D-77). Default: 5000. */
|
|
204
|
+
stageTimeoutMs?: number
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export class AppImpl implements App {
|
|
208
|
+
readonly _state: AppState
|
|
209
|
+
private _started = false
|
|
210
|
+
readonly identity: KronosIdentity
|
|
211
|
+
private _commandGateway: CommandGateway | undefined = undefined
|
|
212
|
+
private _queryGateway: QueryGateway | undefined = undefined
|
|
213
|
+
/** D-77: per-stage timeout for native lifecycle execution. */
|
|
214
|
+
private readonly _stageTimeoutMs: number
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Live CommandGateway. Throws AppNotStartedError if accessed before the
|
|
218
|
+
* `register` lifecycle stage completes during `.start()`. (Plan 08-01.)
|
|
219
|
+
*/
|
|
220
|
+
get commandGateway(): CommandGateway {
|
|
221
|
+
if (!this._commandGateway) throw new AppNotStartedError("commandGateway")
|
|
222
|
+
return this._commandGateway
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Live QueryGateway. Throws AppNotStartedError if accessed before the
|
|
227
|
+
* `register` lifecycle stage completes during `.start()`. (Plan 08-01.)
|
|
228
|
+
*/
|
|
229
|
+
get queryGateway(): QueryGateway {
|
|
230
|
+
if (!this._queryGateway) throw new AppNotStartedError("queryGateway")
|
|
231
|
+
return this._queryGateway
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
constructor(options: AppImplOptions) {
|
|
235
|
+
this._stageTimeoutMs = options.stageTimeoutMs ?? 5000
|
|
236
|
+
this.identity = {
|
|
237
|
+
serviceName: options.serviceName ?? "kronos-app",
|
|
238
|
+
instanceId: options.instanceId ?? createDefaultInstanceId(),
|
|
239
|
+
}
|
|
240
|
+
this._state = {
|
|
241
|
+
slotRegistry: new SlotRegistry(),
|
|
242
|
+
stateEntries: [],
|
|
243
|
+
commandHandlers: [],
|
|
244
|
+
queryHandlers: [],
|
|
245
|
+
processors: [],
|
|
246
|
+
extensions: [],
|
|
247
|
+
warningChannel: options.warningChannel,
|
|
248
|
+
decoratorRegistrations: [],
|
|
249
|
+
commandDispatchInterceptors: [],
|
|
250
|
+
queryDispatchInterceptors: [],
|
|
251
|
+
eventDispatchInterceptors: [],
|
|
252
|
+
handlerInterceptors: [],
|
|
253
|
+
handlerEnhancers: [],
|
|
254
|
+
startHooks: [],
|
|
255
|
+
stopHooks: [],
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @internal — used by tests + by registerInMemoryDefaults indirectly through setDefault. */
|
|
260
|
+
getRegistry(): SlotRegistry {
|
|
261
|
+
return this._state.slotRegistry
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @internal — used by Task 2's start() implementation. */
|
|
265
|
+
isStarted(): boolean {
|
|
266
|
+
return this._started
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @internal — set just before .start() returns; Task 2 toggles this. */
|
|
270
|
+
markStarted(): void {
|
|
271
|
+
this._started = true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private guard(): void {
|
|
275
|
+
if (this._started) throw new AppAlreadyStartedError()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
states(...args: StatesArg[]): App {
|
|
279
|
+
this.guard()
|
|
280
|
+
for (const arg of args) {
|
|
281
|
+
if (Array.isArray(arg)) {
|
|
282
|
+
const [module, options] = arg as readonly [StateModule, StateOptions]
|
|
283
|
+
this._state.stateEntries.push({ module, options })
|
|
284
|
+
} else {
|
|
285
|
+
this._state.stateEntries.push({ module: arg as StateModule, options: {} })
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return this
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
commands(...handlers: CommandHandlerDefinition<any, any>[]): App {
|
|
292
|
+
this.guard()
|
|
293
|
+
this._state.commandHandlers.push(...handlers)
|
|
294
|
+
return this
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
queries(...handlers: QueryHandlerDefinition[]): App {
|
|
298
|
+
this.guard()
|
|
299
|
+
this._state.queryHandlers.push(...handlers)
|
|
300
|
+
return this
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Plan 09-01 (D-103): dual-overload processors() — read accessor + writer.
|
|
304
|
+
processors(): readonly EventProcessorModule[]
|
|
305
|
+
processors(...modules: EventProcessorModule[]): App
|
|
306
|
+
processors(
|
|
307
|
+
...modules: EventProcessorModule[]
|
|
308
|
+
): App | readonly EventProcessorModule[] {
|
|
309
|
+
if (modules.length === 0) {
|
|
310
|
+
// Frozen view so accidental .push() on the returned array doesn't smuggle
|
|
311
|
+
// mutations into _state. The underlying array is still mutable for the
|
|
312
|
+
// writer overload below.
|
|
313
|
+
return Object.freeze([...this._state.processors]) as readonly EventProcessorModule[]
|
|
314
|
+
}
|
|
315
|
+
this.guard()
|
|
316
|
+
this._state.processors.push(...modules)
|
|
317
|
+
return this
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
use(extension: Extension): App {
|
|
321
|
+
this.guard()
|
|
322
|
+
this._state.extensions.push(extension)
|
|
323
|
+
return this
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setDefault<K extends SlotName>(
|
|
327
|
+
slot: K,
|
|
328
|
+
factory: SlotFactory<K> | KronosComponents[K],
|
|
329
|
+
meta?: SlotMeta,
|
|
330
|
+
): App {
|
|
331
|
+
this.guard()
|
|
332
|
+
this._state.slotRegistry.setDefault(slot, factory, meta)
|
|
333
|
+
return this
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
set<K extends SlotName>(slot: K, factory: SlotFactory<K> | KronosComponents[K]): App {
|
|
337
|
+
this.guard()
|
|
338
|
+
this._state.slotRegistry.set(slot, factory)
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
forceSet<K extends SlotName>(slot: K, factory: SlotFactory<K> | KronosComponents[K]): App {
|
|
343
|
+
this.guard()
|
|
344
|
+
this._state.slotRegistry.forceSet(slot, factory)
|
|
345
|
+
return this
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
decorate<K extends SlotName>(
|
|
349
|
+
slot: K,
|
|
350
|
+
factory: DecoratorFactory<K>,
|
|
351
|
+
): DecoratorHandle<K> {
|
|
352
|
+
this.guard()
|
|
353
|
+
const handle: DecoratorHandle<K> = Object.freeze({
|
|
354
|
+
__slot: slot,
|
|
355
|
+
__id: Symbol(`user.${slot}`),
|
|
356
|
+
__name: `user.${slot}.${this._state.decoratorRegistrations.length}`,
|
|
357
|
+
}) as DecoratorHandle<K>
|
|
358
|
+
this._state.decoratorRegistrations.push({
|
|
359
|
+
handle: handle as DecoratorHandle<SlotName>,
|
|
360
|
+
factory: factory as unknown as DecoratorFactory<SlotName>,
|
|
361
|
+
frameworkDefault: false,
|
|
362
|
+
})
|
|
363
|
+
return handle
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
removeDecorator<K extends SlotName>(handle: DecoratorHandle<K>): App {
|
|
367
|
+
this.guard()
|
|
368
|
+
const idx = this._state.decoratorRegistrations.findIndex(
|
|
369
|
+
(entry) => entry.handle.__id === handle.__id,
|
|
370
|
+
)
|
|
371
|
+
if (idx < 0) {
|
|
372
|
+
throw new UnknownDecoratorHandleError(handle as DecoratorHandle<SlotName>)
|
|
373
|
+
}
|
|
374
|
+
this._state.decoratorRegistrations.splice(idx, 1)
|
|
375
|
+
return this
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
commandDispatchInterceptor(fn: DispatchInterceptor<CommandMessage>): App {
|
|
379
|
+
this.guard()
|
|
380
|
+
this._state.commandDispatchInterceptors.push(fn)
|
|
381
|
+
return this
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
queryDispatchInterceptor(fn: DispatchInterceptor<QueryMessage>): App {
|
|
385
|
+
this.guard()
|
|
386
|
+
this._state.queryDispatchInterceptors.push(fn)
|
|
387
|
+
return this
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
eventDispatchInterceptor(fn: DispatchInterceptor<EventMessage>): App {
|
|
391
|
+
this.guard()
|
|
392
|
+
this._state.eventDispatchInterceptors.push(fn)
|
|
393
|
+
return this
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
handlerInterceptor(fn: HandlerInterceptor): App {
|
|
397
|
+
this.guard()
|
|
398
|
+
this._state.handlerInterceptors.push(fn)
|
|
399
|
+
return this
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
handlerEnhancer(def: HandlerEnhancerDefinition): App {
|
|
403
|
+
this.guard()
|
|
404
|
+
this._state.handlerEnhancers.push(def)
|
|
405
|
+
return this
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
onStart(stage: LifecycleStage, fn: LifecycleHook): App {
|
|
409
|
+
this.guard()
|
|
410
|
+
this._state.startHooks.push({ stage, fn })
|
|
411
|
+
return this
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
onStop(stage: LifecycleStage, fn: LifecycleHook): App {
|
|
415
|
+
this.guard()
|
|
416
|
+
this._state.stopHooks.push({ stage, fn })
|
|
417
|
+
return this
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @internal — used by `kronos()` bootstrap (Plan 02) to register framework-default
|
|
422
|
+
* decorators with pre-allocated handle identities from `Defaults`. Distinguished
|
|
423
|
+
* from user `.decorate()` registrations by `frameworkDefault: true` — at `.start()`,
|
|
424
|
+
* framework defaults wrap innermost (handler-adjacent) and user decorators wrap
|
|
425
|
+
* outside them (D-62, DESIGN.md §8 line 248).
|
|
426
|
+
*
|
|
427
|
+
* Called once per slot per kronos() invocation. Idempotent guard not added here
|
|
428
|
+
* because kronos() bootstrap controls call sites.
|
|
429
|
+
*/
|
|
430
|
+
_registerFrameworkDefaultDecorator<K extends SlotName>(
|
|
431
|
+
handle: DecoratorHandle<K>,
|
|
432
|
+
factory: DecoratorFactory<K>,
|
|
433
|
+
): void {
|
|
434
|
+
this._state.decoratorRegistrations.push({
|
|
435
|
+
handle: handle as DecoratorHandle<SlotName>,
|
|
436
|
+
factory: factory as unknown as DecoratorFactory<SlotName>,
|
|
437
|
+
frameworkDefault: true,
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async start(): Promise<RunningApp> {
|
|
442
|
+
if (this._started) throw new AppAlreadyStartedError()
|
|
443
|
+
|
|
444
|
+
// 1. Run extensions FIRST so they can mutate the slot registry / accumulators (D-50).
|
|
445
|
+
for (const ext of this._state.extensions) {
|
|
446
|
+
const result = ext(this)
|
|
447
|
+
if (result instanceof Promise) await result
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 2. Mark started AFTER extensions ran (so extensions can mutate) but BEFORE slot resolution
|
|
451
|
+
// (so resolved factories can't trigger fluent calls — defensive).
|
|
452
|
+
this._started = true
|
|
453
|
+
|
|
454
|
+
// 3. Build the lazy Resolved proxy and EAGERLY resolve all 8 slots up-front
|
|
455
|
+
// (Pitfall 1 — interleaving slot resolution with configurer registration creates stale-cache hazards).
|
|
456
|
+
const resolved = buildResolved(this._state.slotRegistry)
|
|
457
|
+
const built: { -readonly [K in SlotName]: KronosComponents[K] } = {
|
|
458
|
+
eventStore: resolved.eventStore,
|
|
459
|
+
snapshotStore: resolved.snapshotStore,
|
|
460
|
+
commandBus: resolved.commandBus,
|
|
461
|
+
queryBus: resolved.queryBus,
|
|
462
|
+
eventBus: resolved.eventBus,
|
|
463
|
+
serializer: resolved.serializer,
|
|
464
|
+
unitOfWorkFactory: resolved.unitOfWorkFactory,
|
|
465
|
+
tagResolver: resolved.tagResolver,
|
|
466
|
+
// Plan 09-01 (D-84): two new typed slots — eagerly resolved so decorator
|
|
467
|
+
// application + processor wiring see the same instance.
|
|
468
|
+
tokenStore: resolved.tokenStore,
|
|
469
|
+
transactionManager: resolved.transactionManager,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 3b. Apply decorators in two passes per slot (D-62, D-64, DESIGN.md §8):
|
|
473
|
+
// - framework defaults first (innermost, handler-adjacent)
|
|
474
|
+
// - user decorators after (outer; last .decorate() = outermost wrap)
|
|
475
|
+
// Pipeline visualization for slot K (outer → inner):
|
|
476
|
+
// [user decorators in registration order, last=outermost]
|
|
477
|
+
// [framework defaults in registration order, last=outermost]
|
|
478
|
+
// [base = resolved[K]]
|
|
479
|
+
const writableBuilt = built as Record<SlotName, unknown>
|
|
480
|
+
for (const slot of ALL_SLOTS) {
|
|
481
|
+
writableBuilt[slot] = applyDecorators(slot, built[slot], this._state.decoratorRegistrations, resolved)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 4. Emit startup warnings for any slot still using a flagged in-memory default (SLT-04).
|
|
485
|
+
// Iterate ALL_SLOTS for deterministic order. Warning emission goes through the channel
|
|
486
|
+
// so quiet:true / logger options route correctly (D-51).
|
|
487
|
+
for (const slot of ALL_SLOTS) {
|
|
488
|
+
const entry = this._state.slotRegistry.getEntry(slot)
|
|
489
|
+
if (entry?.meta?.inMemory && entry.meta.warning) {
|
|
490
|
+
this._state.warningChannel.emit(entry.meta.warning)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ----------------------------------------------------------------------
|
|
495
|
+
// 5. Native wiring (Plan 08-03a — Configurer chain deleted).
|
|
496
|
+
// Build StateManager from registered entities + resolved eventStore, then
|
|
497
|
+
// subscribe command/query handlers and event-handler subscribing processors
|
|
498
|
+
// directly off the resolved buses. No legacy configurer chain, no Module
|
|
499
|
+
// initialize() shells, no LifecycleRegistry numeric-phase bridge.
|
|
500
|
+
// ----------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
// 5a. Construct StateManager from registered entities (mirrors the configurer's
|
|
503
|
+
// registerDefaults() pattern at eventsourcing-configurer.ts ~line 755).
|
|
504
|
+
// StateModule has no .initialize() — state modules are wired purely via
|
|
505
|
+
// repository registration on the StateManager.
|
|
506
|
+
// Plan 09-01 (D-88): per-state tuple options (snapshotPolicy, snapshotStore)
|
|
507
|
+
// thread through into createEventSourcedRepository.
|
|
508
|
+
const stateManager: StateManager = createStateManager()
|
|
509
|
+
for (const { module, options } of this._state.stateEntries) {
|
|
510
|
+
stateManager.register(
|
|
511
|
+
module,
|
|
512
|
+
createEventSourcedRepository(
|
|
513
|
+
module,
|
|
514
|
+
built.eventStore,
|
|
515
|
+
options.snapshotStore ?? built.snapshotStore,
|
|
516
|
+
options.snapshotPolicy,
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 5b. Build the minimal Configuration shim that createCommandInvocation reads
|
|
522
|
+
// during dispatch (D-82). Surface kept narrow on purpose — anything outside
|
|
523
|
+
// the documented set throws loudly so misuse is obvious.
|
|
524
|
+
const configShim = createConfigShim(built, stateManager)
|
|
525
|
+
|
|
526
|
+
// Plan 09-01 (D-86, RESEARCH Open Question #4): compose the accumulated
|
|
527
|
+
// handlerEnhancers ONCE and thread the composed definition into command,
|
|
528
|
+
// query, tracking, and subscribing handler registration so cross-cutting
|
|
529
|
+
// (tracing, timing, security) wraps every handler kind uniformly.
|
|
530
|
+
const composedHandlerEnhancer: HandlerEnhancerDefinition | undefined =
|
|
531
|
+
this._state.handlerEnhancers.length > 0
|
|
532
|
+
? multiHandlerEnhancerDefinition(this._state.handlerEnhancers)
|
|
533
|
+
: undefined
|
|
534
|
+
|
|
535
|
+
// 5c. Subscribe command handlers natively. createCommandInvocation seeds the
|
|
536
|
+
// ALS three-key set (STATE_MANAGER + COMMAND_BUS + QUERY_BUS) and registers
|
|
537
|
+
// the onPrepareCommit event-flush — verbatim from D-82.
|
|
538
|
+
registerCommandHandlersNatively(this._state.commandHandlers, {
|
|
539
|
+
commandBus: built.commandBus,
|
|
540
|
+
config: configShim,
|
|
541
|
+
moduleName: "commands",
|
|
542
|
+
handlerEnhancer: composedHandlerEnhancer,
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
// 5d. Subscribe query handlers directly onto the queryBus.
|
|
546
|
+
// Plan 09-01: query handlers receive the same enhancer treatment as
|
|
547
|
+
// commands (closes RESEARCH Open Question #4).
|
|
548
|
+
registerQueryHandlersNatively(this._state.queryHandlers, {
|
|
549
|
+
queryBus: built.queryBus,
|
|
550
|
+
moduleName: "queries",
|
|
551
|
+
handlerEnhancer: composedHandlerEnhancer,
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
// 5e. Build event processors from explicit `.processors(...)` modules.
|
|
555
|
+
// Users wanting a subscribing processor write
|
|
556
|
+
// `subscribingProcessor(name).eventHandlers(...).build()` and pass it
|
|
557
|
+
// to `app.processors(...)` — there is no implicit shortcut.
|
|
558
|
+
const builtProcessors: Array<TrackingEventProcessor | SubscribingEventProcessor> = []
|
|
559
|
+
for (const proc of this._state.processors) {
|
|
560
|
+
if (proc.kind === "subscribing") {
|
|
561
|
+
const subscribable = built.eventStore as unknown as SubscribableEventSource
|
|
562
|
+
if (!subscribable.subscribe) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`Event source does not support subscription. ` +
|
|
565
|
+
`Cannot create subscribing processor "${proc.name}".`,
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
builtProcessors.push(
|
|
569
|
+
createSubscribingEventProcessor({
|
|
570
|
+
name: proc.name,
|
|
571
|
+
eventSource: subscribable,
|
|
572
|
+
eventHandlers: proc.eventHandlers,
|
|
573
|
+
stateManager,
|
|
574
|
+
commandBus: built.commandBus,
|
|
575
|
+
queryBus: built.queryBus,
|
|
576
|
+
unitOfWorkRunner: proc.unitOfWorkRunner ?? built.unitOfWorkFactory,
|
|
577
|
+
errorHandler: proc.errorHandler,
|
|
578
|
+
handlerEnhancer: composedHandlerEnhancer,
|
|
579
|
+
}),
|
|
580
|
+
)
|
|
581
|
+
} else {
|
|
582
|
+
builtProcessors.push(
|
|
583
|
+
createTrackingEventProcessor({
|
|
584
|
+
name: proc.name,
|
|
585
|
+
eventSource: built.eventStore as unknown as StreamableEventSource,
|
|
586
|
+
eventHandlers: proc.eventHandlers,
|
|
587
|
+
stateManager,
|
|
588
|
+
commandBus: built.commandBus,
|
|
589
|
+
queryBus: built.queryBus,
|
|
590
|
+
unitOfWorkRunner: proc.unitOfWorkRunner ?? built.unitOfWorkFactory,
|
|
591
|
+
// Plan 09-01 (D-84): per-processor override wins, otherwise fall
|
|
592
|
+
// back to the resolved tokenStore slot so the default in-memory
|
|
593
|
+
// store (or any extension-supplied replacement) drives position
|
|
594
|
+
// persistence — the slot is the single source of truth.
|
|
595
|
+
tokenStore: proc.tokenStore ?? built.tokenStore,
|
|
596
|
+
batchSize: proc.batchSize,
|
|
597
|
+
pollingIntervalMs: proc.pollingIntervalMs,
|
|
598
|
+
errorHandler: proc.errorHandler,
|
|
599
|
+
handlerEnhancer: composedHandlerEnhancer,
|
|
600
|
+
// Plan 11-02: onReset lives on the tracking processor module.
|
|
601
|
+
// Tracking processors support reset; subscribing processors don't.
|
|
602
|
+
onReset: proc.onReset,
|
|
603
|
+
}),
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 5f. Build CommandGateway / QueryGateway from resolved buses, threading the
|
|
609
|
+
// configured UoW runner through so transactional wrappers span the dispatch
|
|
610
|
+
// boundary (CTX-04 / D-34). Gateways are constructed eagerly but the
|
|
611
|
+
// `app.commandGateway` / `app.queryGateway` accessors are only populated at
|
|
612
|
+
// the `register` stage — preserving the AppNotStartedError contract for
|
|
613
|
+
// pre-register hooks (Plan 08-01).
|
|
614
|
+
const commandGateway = createCommandGateway(built.commandBus, built.unitOfWorkFactory)
|
|
615
|
+
const queryGateway = createQueryGateway(built.queryBus, built.unitOfWorkFactory)
|
|
616
|
+
|
|
617
|
+
// 5g. Run typed-stage start hooks in forward order with D-77 warn-then-continue
|
|
618
|
+
// per-stage timeout. Hooks within a stage run concurrently via Promise.all.
|
|
619
|
+
// At the `register` stage, populate the live-gateway accessors so any
|
|
620
|
+
// register/processors/serve-stage hooks (and downstream handlers) see them.
|
|
621
|
+
for (const stage of FORWARD_STAGES) {
|
|
622
|
+
if (stage === "register") {
|
|
623
|
+
this._commandGateway = commandGateway
|
|
624
|
+
this._queryGateway = queryGateway
|
|
625
|
+
}
|
|
626
|
+
const hooks = this._state.startHooks.filter((h) => h.stage === stage)
|
|
627
|
+
if (hooks.length === 0) continue
|
|
628
|
+
await this._runStageWithTimeout(
|
|
629
|
+
stage,
|
|
630
|
+
hooks.map((h) => h.fn),
|
|
631
|
+
this._stageTimeoutMs,
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 5h. Start event processors AFTER processors-stage hooks have run — mirrors
|
|
636
|
+
// the configurer's old sequencing (eventsourcing-configurer.ts lines 670+).
|
|
637
|
+
for (const proc of builtProcessors) {
|
|
638
|
+
await proc.start()
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 5i. Build the RunningApp. stop() reverses: processors first, then user stop
|
|
642
|
+
// hooks in reverse stage order, again with the warn-then-continue timeout.
|
|
643
|
+
const runStageWithTimeout = this._runStageWithTimeout.bind(this)
|
|
644
|
+
const stageTimeoutMs = this._stageTimeoutMs
|
|
645
|
+
const stopHooks = this._state.stopHooks
|
|
646
|
+
const identity = this.identity
|
|
647
|
+
return {
|
|
648
|
+
get identity(): KronosIdentity {
|
|
649
|
+
return identity
|
|
650
|
+
},
|
|
651
|
+
get commandGateway(): CommandGateway {
|
|
652
|
+
return commandGateway
|
|
653
|
+
},
|
|
654
|
+
get queryGateway(): QueryGateway {
|
|
655
|
+
return queryGateway
|
|
656
|
+
},
|
|
657
|
+
async stop() {
|
|
658
|
+
// Stop processors first (mirrors legacy shutdown order).
|
|
659
|
+
for (const proc of builtProcessors) {
|
|
660
|
+
proc.stop()
|
|
661
|
+
}
|
|
662
|
+
// Reverse stage order for stop hooks.
|
|
663
|
+
for (const stage of REVERSE_STAGES) {
|
|
664
|
+
const hooks = stopHooks.filter((h) => h.stage === stage)
|
|
665
|
+
if (hooks.length === 0) continue
|
|
666
|
+
await runStageWithTimeout(
|
|
667
|
+
stage,
|
|
668
|
+
hooks.map((h) => h.fn),
|
|
669
|
+
stageTimeoutMs,
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* D-77 native lifecycle execution: per-stage Promise.all + Promise.race with
|
|
678
|
+
* warn-then-continue. If the stage exceeds `timeoutMs`, log a warning and
|
|
679
|
+
* STOP WAITING — the slow hooks continue to pend in the background; they are
|
|
680
|
+
* NOT cancelled. Reproduces createLifecycleRegistry's per-phase semantics
|
|
681
|
+
* verbatim, but over typed stages instead of numeric phases.
|
|
682
|
+
*/
|
|
683
|
+
private async _runStageWithTimeout(
|
|
684
|
+
stage: LifecycleStage,
|
|
685
|
+
fns: LifecycleHook[],
|
|
686
|
+
timeoutMs: number,
|
|
687
|
+
): Promise<void> {
|
|
688
|
+
const stageWork = Promise.all(fns.map((fn) => Promise.resolve(fn())))
|
|
689
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
690
|
+
const timeout = new Promise<"timeout">((resolve) => {
|
|
691
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs)
|
|
692
|
+
})
|
|
693
|
+
// Swallow background rejections from the slow hooks if the stage has already
|
|
694
|
+
// returned via the timeout branch — without this, an unhandled rejection
|
|
695
|
+
// could surface long after start() resolved (the hook is intentionally not
|
|
696
|
+
// cancelled per D-77, but its eventual rejection is no longer observable).
|
|
697
|
+
stageWork.catch(() => {
|
|
698
|
+
/* warn-then-continue: failures after the timeout are intentionally dropped */
|
|
699
|
+
})
|
|
700
|
+
const result = await Promise.race([
|
|
701
|
+
stageWork.then(() => "done" as const),
|
|
702
|
+
timeout,
|
|
703
|
+
])
|
|
704
|
+
if (timer) clearTimeout(timer)
|
|
705
|
+
if (result === "timeout") {
|
|
706
|
+
this._state.warningChannel.emit(
|
|
707
|
+
`[kronos] Lifecycle stage '${stage}' exceeded ${timeoutMs}ms timeout — continuing without waiting for completion (warn-then-continue per D-77).`,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// Module-private helpers (Plan 08-03a native execution)
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
function createDefaultInstanceId(): string {
|
|
718
|
+
const randomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto)
|
|
719
|
+
if (randomUUID) return randomUUID()
|
|
720
|
+
return `instance-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Forward typed-stage order for `.start()` execution (D-77, LIF-01). */
|
|
724
|
+
const FORWARD_STAGES: ReadonlyArray<LifecycleStage> = [
|
|
725
|
+
"connect",
|
|
726
|
+
"warmup",
|
|
727
|
+
"register",
|
|
728
|
+
"processors",
|
|
729
|
+
"serve",
|
|
730
|
+
] as const
|
|
731
|
+
|
|
732
|
+
/** Reverse typed-stage order for `.stop()` execution (D-77, LIF-01). */
|
|
733
|
+
const REVERSE_STAGES: ReadonlyArray<LifecycleStage> = [
|
|
734
|
+
"serve",
|
|
735
|
+
"processors",
|
|
736
|
+
"register",
|
|
737
|
+
"warmup",
|
|
738
|
+
"connect",
|
|
739
|
+
] as const
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Construct a minimal Configuration shim for createCommandInvocation (D-82).
|
|
743
|
+
*
|
|
744
|
+
* The shim implements ONLY the methods createCommandInvocation invokes:
|
|
745
|
+
* - hasComponent / getComponent for STATE_MANAGER, COMMAND_BUS, QUERY_BUS
|
|
746
|
+
* (the three keys seeded into ALS at command-invocation entry per D-82)
|
|
747
|
+
* - hasComponent / getComponent for EVENT_STORE (read inside the
|
|
748
|
+
* onPrepareCommit closure when flushing buffered events)
|
|
749
|
+
* - getOptionalComponent for TAG_RESOLVER (read inside onPrepareCommit when
|
|
750
|
+
* enriching events with tags)
|
|
751
|
+
*
|
|
752
|
+
* Everything else (decorators, modules, factories, getComponents, getParent)
|
|
753
|
+
* throws or returns empty — the configurer-era surface is gone. Plan 04 will
|
|
754
|
+
* delete the Configuration interface entirely; this shim is the bridge.
|
|
755
|
+
*/
|
|
756
|
+
function createConfigShim(
|
|
757
|
+
built: { -readonly [K in SlotName]: KronosComponents[K] },
|
|
758
|
+
stateManager: StateManager,
|
|
759
|
+
): MinimalConfiguration {
|
|
760
|
+
// Inline string-literal keys mirror the kronos() framework defaults that
|
|
761
|
+
// createCommandInvocation reads (STATE_MANAGER, COMMAND_BUS, QUERY_BUS,
|
|
762
|
+
// EVENT_STORE, TAG_RESOLVER) plus the additional slot mirrors carried
|
|
763
|
+
// forward for parity with the previous Configuration shim.
|
|
764
|
+
const components: Record<string, unknown> = {
|
|
765
|
+
stateManager,
|
|
766
|
+
commandBus: built.commandBus,
|
|
767
|
+
queryBus: built.queryBus,
|
|
768
|
+
eventStore: built.eventStore,
|
|
769
|
+
eventBus: built.eventBus,
|
|
770
|
+
snapshotStore: built.snapshotStore,
|
|
771
|
+
serializer: built.serializer,
|
|
772
|
+
unitOfWorkFactory: built.unitOfWorkFactory,
|
|
773
|
+
tagResolver: built.tagResolver,
|
|
774
|
+
// Plan 09-01: shim mirrors of the two new typed slots so legacy
|
|
775
|
+
// enhancers / probes that look them up via the Configuration shape
|
|
776
|
+
// see the resolved instance.
|
|
777
|
+
tokenStore: built.tokenStore,
|
|
778
|
+
transactionManager: built.transactionManager,
|
|
779
|
+
}
|
|
780
|
+
const config: MinimalConfiguration = {
|
|
781
|
+
hasComponent(type: string, _name?: string): boolean {
|
|
782
|
+
return type in components
|
|
783
|
+
},
|
|
784
|
+
getComponent<T>(type: string, _name?: string): T {
|
|
785
|
+
if (!(type in components)) {
|
|
786
|
+
throw new Error(`[kronos] Configuration shim does not provide "${type}"`)
|
|
787
|
+
}
|
|
788
|
+
return components[type] as T
|
|
789
|
+
},
|
|
790
|
+
getOptionalComponent<T>(type: string, _name?: string): T | undefined {
|
|
791
|
+
return components[type] as T | undefined
|
|
792
|
+
},
|
|
793
|
+
}
|
|
794
|
+
return config
|
|
795
|
+
}
|