@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.
Files changed (61) hide show
  1. package/dist/app.d.ts +228 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +519 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/components.d.ts +28 -0
  6. package/dist/components.d.ts.map +1 -0
  7. package/dist/components.js +14 -0
  8. package/dist/components.js.map +1 -0
  9. package/dist/decorator.d.ts +54 -0
  10. package/dist/decorator.d.ts.map +1 -0
  11. package/dist/decorator.js +36 -0
  12. package/dist/decorator.js.map +1 -0
  13. package/dist/defaults-handles.d.ts +25 -0
  14. package/dist/defaults-handles.d.ts.map +1 -0
  15. package/dist/defaults-handles.js +25 -0
  16. package/dist/defaults-handles.js.map +1 -0
  17. package/dist/defaults.d.ts +10 -0
  18. package/dist/defaults.d.ts.map +1 -0
  19. package/dist/defaults.js +49 -0
  20. package/dist/defaults.js.map +1 -0
  21. package/dist/errors.d.ts +37 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +53 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/index.d.ts +17 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/kronos.d.ts +51 -0
  30. package/dist/kronos.d.ts.map +1 -0
  31. package/dist/kronos.js +74 -0
  32. package/dist/kronos.js.map +1 -0
  33. package/dist/lifecycle.d.ts +27 -0
  34. package/dist/lifecycle.d.ts.map +1 -0
  35. package/dist/lifecycle.js +7 -0
  36. package/dist/lifecycle.js.map +1 -0
  37. package/dist/resolved.d.ts +18 -0
  38. package/dist/resolved.d.ts.map +1 -0
  39. package/dist/resolved.js +43 -0
  40. package/dist/resolved.js.map +1 -0
  41. package/dist/slot-registry.d.ts +35 -0
  42. package/dist/slot-registry.d.ts.map +1 -0
  43. package/dist/slot-registry.js +50 -0
  44. package/dist/slot-registry.js.map +1 -0
  45. package/dist/warnings.d.ts +19 -0
  46. package/dist/warnings.d.ts.map +1 -0
  47. package/dist/warnings.js +14 -0
  48. package/dist/warnings.js.map +1 -0
  49. package/package.json +59 -0
  50. package/src/app.ts +795 -0
  51. package/src/components.ts +48 -0
  52. package/src/decorator.ts +88 -0
  53. package/src/defaults-handles.ts +29 -0
  54. package/src/defaults.ts +64 -0
  55. package/src/errors.ts +62 -0
  56. package/src/index.ts +38 -0
  57. package/src/kronos.ts +117 -0
  58. package/src/lifecycle.ts +33 -0
  59. package/src/resolved.ts +54 -0
  60. package/src/slot-registry.ts +89 -0
  61. 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
+ }