@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
@@ -0,0 +1,48 @@
1
+ import type { EventStore, SnapshotStore, TagResolver } from "@kronos-ts/eventsourcing"
2
+ import type {
3
+ CommandBus,
4
+ QueryBus,
5
+ EventBus,
6
+ UoWRunner,
7
+ TokenStore,
8
+ TransactionManager,
9
+ } from "@kronos-ts/messaging"
10
+ import type { Serializer } from "@kronos-ts/common"
11
+
12
+ /**
13
+ * Fixed slot interface. SLT-01: enumerates ALL 10 framework slots.
14
+ * No declaration merging, no string tokens — closed contract.
15
+ *
16
+ * Plan 09-01 (D-84): tokenStore + transactionManager added so persistence
17
+ * extensions (KronosDB, etc.) replace typed slots instead of routing through
18
+ * the deleted configurer's componentRegistry.
19
+ */
20
+ export interface KronosComponents {
21
+ eventStore: EventStore
22
+ snapshotStore: SnapshotStore
23
+ commandBus: CommandBus
24
+ queryBus: QueryBus
25
+ eventBus: EventBus
26
+ serializer: Serializer
27
+ unitOfWorkFactory: UoWRunner
28
+ tagResolver: TagResolver
29
+ tokenStore: TokenStore
30
+ transactionManager: TransactionManager
31
+ }
32
+
33
+ /** Type-level: keyof KronosComponents — for verb signatures. */
34
+ export type SlotName = keyof KronosComponents
35
+
36
+ /** Sentinel listing every slot name; used by .start() to iterate slots and emit startup warnings. Order is stable for deterministic warning emission. */
37
+ export const ALL_SLOTS: readonly SlotName[] = [
38
+ "eventStore",
39
+ "snapshotStore",
40
+ "commandBus",
41
+ "queryBus",
42
+ "eventBus",
43
+ "serializer",
44
+ "unitOfWorkFactory",
45
+ "tagResolver",
46
+ "tokenStore",
47
+ "transactionManager",
48
+ ] as const
@@ -0,0 +1,88 @@
1
+ import type { Resolved } from "./resolved.js"
2
+ import type { KronosComponents, SlotName } from "./components.js"
3
+
4
+ /**
5
+ * Slot-typed handle returned by `app.decorate()`. The `__slot` brand prevents
6
+ * cross-slot removal at compile time (DEC-03). `__id` is the runtime identity
7
+ * key used by `removeDecorator()` to locate the registration.
8
+ *
9
+ * Framework defaults expose pre-allocated handle constants on `Defaults` so
10
+ * users can write `app.removeDecorator(Defaults.commandBus.intercepting)`.
11
+ */
12
+ export interface DecoratorHandle<K extends SlotName> {
13
+ readonly __slot: K
14
+ readonly __id: symbol
15
+ readonly __name: string
16
+ }
17
+
18
+ /**
19
+ * A decorator factory wraps the slot's resolved value. Polymorphic over the
20
+ * slot interface (DEC-04) — receives `KronosComponents[K]` and returns
21
+ * `KronosComponents[K]`. The `resolved` proxy is the same lazy proxy passed
22
+ * to slot factories (Phase 5 D-52); decorators may pull sibling slots through
23
+ * it (cycle detection covers this).
24
+ */
25
+ export type DecoratorFactory<K extends SlotName> = (
26
+ inner: KronosComponents[K],
27
+ resolved: Resolved,
28
+ ) => KronosComponents[K]
29
+
30
+ /**
31
+ * Internal accumulator entry. Stored on `AppState.decoratorRegistrations` in
32
+ * registration order — pipeline order at `.start()` is left-to-right
33
+ * (last registered = outermost wrap, per D-61 / DESIGN.md §8).
34
+ *
35
+ * `frameworkDefault` distinguishes framework-registered defaults (Plan 02)
36
+ * from user-registered decorators (D-62 — user decorators wrap OUTSIDE
37
+ * framework defaults). The `.start()` decoration step partitions on this
38
+ * field and applies framework defaults first (innermost), then user
39
+ * decorators (outermost).
40
+ */
41
+ export interface DecoratorEntry<K extends SlotName = SlotName> {
42
+ readonly handle: DecoratorHandle<K>
43
+ readonly factory: DecoratorFactory<K>
44
+ readonly frameworkDefault: boolean
45
+ }
46
+
47
+ /**
48
+ * @internal — used by `Defaults` and by `kronos()` bootstrap (Plan 02) to
49
+ * mint pre-allocated framework-default handles with stable identity.
50
+ */
51
+ export function makeFrameworkHandle<K extends SlotName>(
52
+ slot: K,
53
+ name: string,
54
+ ): DecoratorHandle<K> {
55
+ return Object.freeze({
56
+ __slot: slot,
57
+ __id: Symbol(`${slot}.${name}`),
58
+ __name: name,
59
+ }) as DecoratorHandle<K>
60
+ }
61
+
62
+ /**
63
+ * Apply all decorator registrations for a given slot in two passes:
64
+ * 1. Framework defaults first (innermost, handler-adjacent)
65
+ * 2. User decorators after (outer; last .decorate() = outermost wrap)
66
+ *
67
+ * Both passes iterate `registrations` in registration order so a
68
+ * left-to-right reduce composes correctly (each factory wraps the current value).
69
+ */
70
+ export function applyDecorators<K extends SlotName>(
71
+ slot: K,
72
+ base: KronosComponents[K],
73
+ registrations: ReadonlyArray<DecoratorEntry>,
74
+ resolved: Resolved,
75
+ ): KronosComponents[K] {
76
+ let current = base
77
+ // Pass 1: framework defaults (innermost)
78
+ for (const reg of registrations) {
79
+ if (reg.handle.__slot !== slot || !reg.frameworkDefault) continue
80
+ current = (reg.factory as unknown as DecoratorFactory<K>)(current, resolved)
81
+ }
82
+ // Pass 2: user decorators (outer)
83
+ for (const reg of registrations) {
84
+ if (reg.handle.__slot !== slot || reg.frameworkDefault) continue
85
+ current = (reg.factory as unknown as DecoratorFactory<K>)(current, resolved)
86
+ }
87
+ return current
88
+ }
@@ -0,0 +1,29 @@
1
+ import { makeFrameworkHandle, type DecoratorHandle } from "./decorator.js"
2
+
3
+ /**
4
+ * Global static module export — frozen handle identities for framework-default
5
+ * decorators (D-54). Per-app decorator state lives on `AppState`; `Defaults`
6
+ * only carries handle *identities*.
7
+ *
8
+ * Plan 02 wires the actual factories in `kronos()` bootstrap, keyed by these
9
+ * handles. Plan 01 ships the handle constants only — having them in place
10
+ * lets users write `app.removeDecorator(Defaults.commandBus.intercepting)`
11
+ * even before Plan 02 registers the factories (the call will throw
12
+ * `UnknownDecoratorHandleError` until Plan 02 lands, which is correct
13
+ * behavior for now and ratchets to the real removal once Plan 02 ships).
14
+ */
15
+ export const Defaults = Object.freeze({
16
+ commandBus: Object.freeze({
17
+ intercepting: makeFrameworkHandle("commandBus", "intercepting"),
18
+ }),
19
+ queryBus: Object.freeze({
20
+ intercepting: makeFrameworkHandle("queryBus", "intercepting"),
21
+ }),
22
+ eventBus: Object.freeze({
23
+ intercepting: makeFrameworkHandle("eventBus", "intercepting"),
24
+ }),
25
+ }) as {
26
+ readonly commandBus: { readonly intercepting: DecoratorHandle<"commandBus"> }
27
+ readonly queryBus: { readonly intercepting: DecoratorHandle<"queryBus"> }
28
+ readonly eventBus: { readonly intercepting: DecoratorHandle<"eventBus"> }
29
+ }
@@ -0,0 +1,64 @@
1
+ import type { App } from "./app.js"
2
+ import type { EventBus } from "@kronos-ts/messaging"
3
+ import {
4
+ createInMemoryEventStore,
5
+ createInMemorySnapshotStore,
6
+ descriptorBasedTagResolver,
7
+ } from "@kronos-ts/eventsourcing"
8
+ import {
9
+ createSimpleCommandBus,
10
+ createSimpleQueryBus,
11
+ jsonSerializer,
12
+ runInNewUoW,
13
+ createInMemoryTokenStore,
14
+ noTransactionManager,
15
+ } from "@kronos-ts/messaging"
16
+
17
+ /**
18
+ * Register the 8 in-memory defaults (SLT-04, D-51).
19
+ * Slots flagged inMemory:true emit a startup warning at .start() unless the slot
20
+ * was overridden via .set/.forceSet (or another .setDefault won — but setDefault
21
+ * is ifAbsent, so only the FIRST setDefault wins; user setDefault calls on already-defaulted
22
+ * slots are no-ops, which is the intended SLT-02 semantics).
23
+ */
24
+ export function registerInMemoryDefaults(app: App): void {
25
+ // Durability-flagged defaults (SLT-04 warning fires unless overridden):
26
+ app.setDefault("eventStore", () => createInMemoryEventStore(), {
27
+ inMemory: true,
28
+ warning: "[kronos] eventStore: in-memory — not durable, configure an extension for production",
29
+ })
30
+ app.setDefault("snapshotStore", () => createInMemorySnapshotStore(), {
31
+ inMemory: true,
32
+ warning: "[kronos] snapshotStore: in-memory — not durable, configure an extension for production",
33
+ })
34
+ app.setDefault("commandBus", () => createSimpleCommandBus(), {
35
+ inMemory: true,
36
+ warning: "[kronos] commandBus: in-memory — single-process only, configure an extension for distribution",
37
+ })
38
+ app.setDefault("queryBus", () => createSimpleQueryBus(), {
39
+ inMemory: true,
40
+ warning: "[kronos] queryBus: in-memory — single-process only, configure an extension for distribution",
41
+ })
42
+ // EventBus default: in ES setups EventStore IS the EventBus (configurer mirrors this at line 796)
43
+ app.setDefault("eventBus", ({ eventStore }) => eventStore as unknown as EventBus, {
44
+ inMemory: true,
45
+ warning: "[kronos] eventBus: in-memory — single-process only",
46
+ })
47
+
48
+ // Stateless / non-durability-implied defaults — NO inMemory flag, no warning emission:
49
+ app.setDefault("serializer", () => jsonSerializer())
50
+ app.setDefault("unitOfWorkFactory", () => runInNewUoW)
51
+ app.setDefault("tagResolver", () => descriptorBasedTagResolver())
52
+
53
+ // Plan 09-01 (D-84): typed slots for token persistence + transactional wrapping.
54
+ // Both default to in-memory — extensions (KronosDB, Drizzle/Knex/Kysely token stores,
55
+ // user-supplied TransactionManagers) override via app.set('tokenStore', ...) etc.
56
+ app.setDefault("tokenStore", () => createInMemoryTokenStore(), {
57
+ inMemory: true,
58
+ warning: "[kronos] tokenStore: in-memory — not durable, configure a persistence extension for production",
59
+ })
60
+ app.setDefault("transactionManager", () => noTransactionManager(), {
61
+ inMemory: true,
62
+ warning: "[kronos] transactionManager: in-memory — pass-through, configure a transactional extension for production",
63
+ })
64
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { SlotName } from "./components.js"
2
+ import type { DecoratorHandle } from "./decorator.js"
3
+
4
+ /**
5
+ * Thrown when buildResolved detects a cycle: a factory's getter on Resolved re-enters
6
+ * a slot that is currently being resolved. (D-52 cycle-detection contract.)
7
+ */
8
+ export class CircularSlotDependencyError extends Error {
9
+ readonly slot: SlotName
10
+ readonly chain: readonly SlotName[]
11
+ constructor(slot: SlotName, chain: readonly SlotName[]) {
12
+ super(`[kronos] Circular slot dependency: "${slot}" via [${chain.join(" → ")}]`)
13
+ this.name = "CircularSlotDependencyError"
14
+ this.slot = slot
15
+ this.chain = chain
16
+ }
17
+ }
18
+
19
+ /** Thrown when a Resolved getter touches a slot that has no registered factory. */
20
+ export class SlotNotRegisteredError extends Error {
21
+ readonly slot: SlotName
22
+ constructor(slot: SlotName) {
23
+ super(`[kronos] Slot "${slot}" has no registered factory.`)
24
+ this.name = "SlotNotRegisteredError"
25
+ this.slot = slot
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Thrown when `app.commandGateway` or `app.queryGateway` is accessed before
31
+ * the `register` lifecycle stage completes during `.start()`. Available inside
32
+ * `onStart('warmup'|'register'|'processors'|'serve', fn)` hooks AFTER register
33
+ * completes, and after `.start()` resolves. (Plan 08-01.)
34
+ */
35
+ export class AppNotStartedError extends Error {
36
+ constructor(accessor: string) {
37
+ super(
38
+ `[kronos] App not started: ${accessor} is only accessible after register-stage completes during .start().`,
39
+ )
40
+ this.name = "AppNotStartedError"
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Thrown by `app.removeDecorator(handle)` when the handle is not found in the
46
+ * app's registration list (D-59). Catches typos like
47
+ * `Defaults.commandBus.interceptingg` and removal of a handle that was never
48
+ * registered (e.g., framework default whose factory hasn't been wired yet).
49
+ */
50
+ export class UnknownDecoratorHandleError extends Error {
51
+ readonly slot: SlotName
52
+ readonly handleName: string
53
+ constructor(handle: DecoratorHandle<SlotName>) {
54
+ super(
55
+ `[kronos] Unknown decorator handle "${handle.__name}" for slot "${handle.__slot}". ` +
56
+ `Either it was never registered or it was already removed.`,
57
+ )
58
+ this.name = "UnknownDecoratorHandleError"
59
+ this.slot = handle.__slot
60
+ this.handleName = handle.__name
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ export type { KronosComponents, SlotName } from "./components.js"
2
+ export { ALL_SLOTS } from "./components.js"
3
+ export { CircularSlotDependencyError, SlotNotRegisteredError } from "./errors.js"
4
+ export { SlotRegistry, type SlotMeta, type SlotEntry, type SlotFactory } from "./slot-registry.js"
5
+ export { buildResolved, type Resolved } from "./resolved.js"
6
+ export {
7
+ createWarningChannel,
8
+ type WarningLogger,
9
+ type WarningChannelOptions,
10
+ type WarningChannel,
11
+ } from "./warnings.js"
12
+ export {
13
+ AppImpl,
14
+ AppAlreadyStartedError,
15
+ type App,
16
+ type RunningApp,
17
+ type Extension,
18
+ type AppState,
19
+ type AppImplOptions,
20
+ type KronosIdentity,
21
+ } from "./app.js"
22
+ export { registerInMemoryDefaults } from "./defaults.js"
23
+ export { kronos, type KronosPartialConfig } from "./kronos.js"
24
+ export {
25
+ type DecoratorHandle,
26
+ type DecoratorFactory,
27
+ type DecoratorEntry,
28
+ applyDecorators,
29
+ } from "./decorator.js"
30
+ export { Defaults } from "./defaults-handles.js"
31
+ export { UnknownDecoratorHandleError, AppNotStartedError } from "./errors.js"
32
+ export type { LifecycleStage, LifecycleHook } from "./lifecycle.js"
33
+ // Plan 09-01 Task 1: re-export of typed-slot interfaces so extension packages don't
34
+ // need to deep-import from @kronos-ts/messaging just to set/replace slots.
35
+ export type { TokenStore, TransactionManager } from "@kronos-ts/messaging"
36
+ // Plan 09-01 Task 2: handler enhancer + entities() tuple-shape types.
37
+ export type { HandlerEnhancerDefinition } from "@kronos-ts/messaging"
38
+ export type { StateOptions, StatesArg } from "./app.js"
package/src/kronos.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { StateModule } from "@kronos-ts/modelling"
2
+ import type {
3
+ CommandHandlerDefinition,
4
+ QueryHandlerDefinition,
5
+ EventProcessorModule,
6
+ } from "@kronos-ts/messaging"
7
+ import {
8
+ createInterceptingCommandBus,
9
+ createInterceptingQueryBus,
10
+ createInterceptingEventBus,
11
+ } from "@kronos-ts/messaging"
12
+ import { AppImpl, type App } from "./app.js"
13
+ import { registerInMemoryDefaults } from "./defaults.js"
14
+ import { createWarningChannel, type WarningLogger } from "./warnings.js"
15
+ import { Defaults } from "./defaults-handles.js"
16
+
17
+ /**
18
+ * Partial-config shorthand options for kronos(). APP-02.
19
+ *
20
+ * Domain registrations passed here are appended to the same internal accumulators
21
+ * as fluent .states()/.commands()/etc. calls. quiet/logger configure the warning
22
+ * channel BEFORE in-memory defaults are registered.
23
+ */
24
+ export interface KronosPartialConfig {
25
+ states?: StateModule[]
26
+ commands?: CommandHandlerDefinition<any, any>[]
27
+ queries?: QueryHandlerDefinition[]
28
+ processors?: EventProcessorModule[]
29
+ quiet?: boolean
30
+ logger?: WarningLogger
31
+ /** Stable logical service/application name. Same across replicas. */
32
+ serviceName?: string
33
+ /** Unique physical runtime instance id. Different per process/pod. */
34
+ instanceId?: string
35
+ /**
36
+ * Per-stage timeout (ms) for native lifecycle execution (D-77).
37
+ * If a single stage exceeds this, AppImpl emits a warning and continues
38
+ * to the next stage WITHOUT cancelling the slow hooks (warn-then-continue).
39
+ *
40
+ * Default: 5000.
41
+ */
42
+ stageTimeoutMs?: number
43
+ }
44
+
45
+ /**
46
+ * Create a new Kronos App.
47
+ *
48
+ * ```typescript
49
+ * const app = await kronos()
50
+ * .states(Course)
51
+ * .commands(createCourseHandler)
52
+ * .start()
53
+ *
54
+ * await app.commandGateway.send(CreateCourse, { courseId: "cs-101", name: "Intro" }, emptyMetadata())
55
+ * ```
56
+ *
57
+ * Or with a partial config (APP-02):
58
+ *
59
+ * ```typescript
60
+ * const app = await kronos({ states: [Course], commands: [createCourseHandler], quiet: true }).start()
61
+ * ```
62
+ */
63
+ export function kronos(partial?: KronosPartialConfig): App {
64
+ const warningChannel = createWarningChannel({ quiet: partial?.quiet, logger: partial?.logger })
65
+ const app = new AppImpl({
66
+ warningChannel,
67
+ stageTimeoutMs: partial?.stageTimeoutMs,
68
+ serviceName: partial?.serviceName,
69
+ instanceId: partial?.instanceId,
70
+ })
71
+
72
+ // Register in-memory defaults FIRST so user partial-config / fluent calls override them
73
+ // via set/forceSet (setDefault is ifAbsent — first registration wins).
74
+ registerInMemoryDefaults(app)
75
+
76
+ // Register framework-default `intercepting` decorators for the 3 buses (DEC-02, D-57).
77
+ // Each closure reads `app._state.{bus}DispatchInterceptors` + `app._state.handlerInterceptors`
78
+ // at decoration time (i.e., during .start()) — extensions and user code populate these
79
+ // arrays before .start() runs, so the snapshot is complete by the time the closure fires.
80
+ // Removable via `removeDecorator(Defaults.{bus}.intercepting)`.
81
+ app._registerFrameworkDefaultDecorator(
82
+ Defaults.commandBus.intercepting,
83
+ (inner, _resolved) => {
84
+ const wrapped = createInterceptingCommandBus(inner)
85
+ for (const fn of app._state.commandDispatchInterceptors) wrapped.registerDispatchInterceptor(fn)
86
+ for (const fn of app._state.handlerInterceptors) wrapped.registerHandlerInterceptor(fn)
87
+ return wrapped
88
+ },
89
+ )
90
+ app._registerFrameworkDefaultDecorator(
91
+ Defaults.queryBus.intercepting,
92
+ (inner, _resolved) => {
93
+ const wrapped = createInterceptingQueryBus(inner)
94
+ for (const fn of app._state.queryDispatchInterceptors) wrapped.registerDispatchInterceptor(fn)
95
+ for (const fn of app._state.handlerInterceptors) wrapped.registerHandlerInterceptor(fn)
96
+ return wrapped
97
+ },
98
+ )
99
+ app._registerFrameworkDefaultDecorator(
100
+ Defaults.eventBus.intercepting,
101
+ (inner, _resolved) => {
102
+ // EventBus intercepting wrapper takes its dispatch interceptors at construction time;
103
+ // it has no register* methods and no handler interceptor concept (verified
104
+ // packages/messaging/src/intercepting-event-bus.ts — no handlerInterceptors field).
105
+ // Snapshot the array at decoration time; mutations after .start() are blocked
106
+ // by the AppAlreadyStartedError guard on `eventDispatchInterceptor()`.
107
+ return createInterceptingEventBus(inner, [...app._state.eventDispatchInterceptors])
108
+ },
109
+ )
110
+
111
+ if (partial?.states) app.states(...partial.states)
112
+ if (partial?.commands) app.commands(...partial.commands)
113
+ if (partial?.queries) app.queries(...partial.queries)
114
+ if (partial?.processors) app.processors(...partial.processors)
115
+
116
+ return app
117
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed lifecycle stages for app-level startup/shutdown hooks (LIF-01).
3
+ *
4
+ * Forward order on `.start()`: connect → warmup → register → processors → serve.
5
+ * Reverse order on `.stop()`: serve → processors → register → warmup → connect.
6
+ *
7
+ * Within a single stage, hooks execute in registration order (no `{stage, order}`
8
+ * tiebreaker — D-70 defers it until a real consumer asks).
9
+ *
10
+ * Closed string union by design (LIF-01). NOT extensible via declaration merging.
11
+ */
12
+ export type LifecycleStage = "connect" | "register" | "warmup" | "processors" | "serve"
13
+
14
+ /**
15
+ * Hook signature for `app.onStart(stage, fn)` / `app.onStop(stage, fn)` (LIF-02, D-69).
16
+ *
17
+ * Bare zero-arg shape. Extensions close over their own state from the enclosing
18
+ * `(app: App) => void` extension scope (see DESIGN.md §12 KronosDB pattern).
19
+ *
20
+ * Why no `Resolved` arg: the Resolved proxy exists for slot-factory composition
21
+ * (Phase 5 D-52); lifecycle hooks are a different concern.
22
+ *
23
+ * Why no `Configuration` arg: matching the legacy `LifecycleHandler(config?: any)`
24
+ * signature would couple this public type to the configurer's `Configuration` —
25
+ * a Phase 8 deletion target.
26
+ */
27
+ export type LifecycleHook = () => void | Promise<void>
28
+
29
+ // Plan 08-03a (D-77): the legacy numeric-phase mapping is deleted. Native
30
+ // AppImpl.start() executes typed-stage hooks directly off
31
+ // AppState.startHooks/stopHooks — no numeric-phase bridge to the legacy
32
+ // LifecycleRegistry. Plan 03b's enhancer-bridge keeps a private inverted copy
33
+ // locally for the D-81 fallback path.
@@ -0,0 +1,54 @@
1
+ import type { KronosComponents, SlotName } from "./components.js"
2
+ import { CircularSlotDependencyError, SlotNotRegisteredError } from "./errors.js"
3
+ import type { SlotRegistry } from "./slot-registry.js"
4
+
5
+ /**
6
+ * Resolved is structurally identical to KronosComponents — the destructured arg
7
+ * passed to factories. We re-export the alias so user code reads `Resolved` not `KronosComponents`.
8
+ */
9
+ export type Resolved = KronosComponents
10
+
11
+ /**
12
+ * Build a lazy + memoized Resolved Proxy backed by the registry. (D-52.)
13
+ *
14
+ * - Each property access invokes the slot's factory once and memoizes
15
+ * - Factories receive the same proxy as their argument so dependency chains compose
16
+ * - A `resolving` Set detects cycles: if a getter is hit while its slot is mid-resolution,
17
+ * throw CircularSlotDependencyError with the offending chain
18
+ * - try/finally ensures `resolving` is cleaned even after errors so subsequent accesses still work
19
+ */
20
+ export function buildResolved(registry: SlotRegistry): Resolved {
21
+ const cache = new Map<SlotName, unknown>()
22
+ const resolving = new Set<SlotName>()
23
+ const resolvingOrder: SlotName[] = []
24
+
25
+ const handler: ProxyHandler<object> = {
26
+ get(_target, prop) {
27
+ const slot = prop as SlotName
28
+ if (cache.has(slot)) return cache.get(slot)
29
+
30
+ if (resolving.has(slot)) {
31
+ throw new CircularSlotDependencyError(slot, [...resolvingOrder, slot])
32
+ }
33
+
34
+ const entry = registry.getEntry(slot)
35
+ if (!entry) {
36
+ throw new SlotNotRegisteredError(slot)
37
+ }
38
+
39
+ resolving.add(slot)
40
+ resolvingOrder.push(slot)
41
+ try {
42
+ const value = entry.factory(proxy as Resolved)
43
+ cache.set(slot, value)
44
+ return value
45
+ } finally {
46
+ resolving.delete(slot)
47
+ resolvingOrder.pop()
48
+ }
49
+ },
50
+ }
51
+
52
+ const proxy = new Proxy({}, handler) as Resolved
53
+ return proxy
54
+ }
@@ -0,0 +1,89 @@
1
+ import type { KronosComponents, SlotName } from "./components.js"
2
+
3
+ /**
4
+ * Optional metadata attached to a slot entry. Today the only field is
5
+ * `inMemory` + `warning` (D-51) — used by Plan 02 to emit startup warnings
6
+ * for any slot still using a flagged in-memory default at .start() time.
7
+ */
8
+ export interface SlotMeta {
9
+ inMemory?: true
10
+ warning?: string
11
+ }
12
+
13
+ /** Each slot's resolution callable: a factory that takes the destructured Resolved and returns the component. */
14
+ export type SlotFactory<K extends SlotName> = (resolved: KronosComponents) => KronosComponents[K]
15
+
16
+ /** A registered slot entry: normalized factory + optional meta. */
17
+ export interface SlotEntry<K extends SlotName = SlotName> {
18
+ factory: SlotFactory<K>
19
+ meta?: SlotMeta
20
+ }
21
+
22
+ /**
23
+ * Normalize a factory-or-instance argument into a SlotFactory.
24
+ * SLT-03: plain instances (non-function values) become `() => instance` internally.
25
+ */
26
+ function normalizeFactory<K extends SlotName>(
27
+ factoryOrInstance: SlotFactory<K> | KronosComponents[K],
28
+ ): SlotFactory<K> {
29
+ if (typeof factoryOrInstance === "function") {
30
+ return factoryOrInstance as SlotFactory<K>
31
+ }
32
+ const instance = factoryOrInstance
33
+ return () => instance
34
+ }
35
+
36
+ /**
37
+ * SlotRegistry — the storage backing the App's three verbs.
38
+ * setDefault: ifAbsent (no-op if occupied)
39
+ * set: warn on double-set
40
+ * forceSet: silent overwrite
41
+ * (DESIGN.md §6, REQUIREMENTS.md SLT-02.)
42
+ */
43
+ export class SlotRegistry {
44
+ private readonly slots = new Map<SlotName, SlotEntry>()
45
+
46
+ setDefault<K extends SlotName>(
47
+ slot: K,
48
+ factoryOrInstance: SlotFactory<K> | KronosComponents[K],
49
+ meta?: SlotMeta,
50
+ ): void {
51
+ if (this.slots.has(slot)) return
52
+ this.slots.set(slot, { factory: normalizeFactory(factoryOrInstance) as SlotFactory<SlotName>, meta })
53
+ }
54
+
55
+ set<K extends SlotName>(
56
+ slot: K,
57
+ factoryOrInstance: SlotFactory<K> | KronosComponents[K],
58
+ ): void {
59
+ const existing = this.slots.get(slot)
60
+ // Provenance check: setDefault always writes a `meta` key (even when its value
61
+ // is undefined for stateless defaults like serializer / unitOfWorkFactory / tagResolver);
62
+ // set / forceSet never write the key at all. So `"meta" in existing` identifies a prior
63
+ // setDefault — those overrides are the expected "extension overrides in-memory default" path
64
+ // and should NOT warn. Genuine collisions (two set/forceSet calls on the same slot) DO warn.
65
+ if (existing && !("meta" in existing)) {
66
+ console.warn(
67
+ `[kronos] slot "${slot}" override: already set. Use forceSet() to suppress this warning.`,
68
+ )
69
+ }
70
+ this.slots.set(slot, { factory: normalizeFactory(factoryOrInstance) as SlotFactory<SlotName> })
71
+ }
72
+
73
+ forceSet<K extends SlotName>(
74
+ slot: K,
75
+ factoryOrInstance: SlotFactory<K> | KronosComponents[K],
76
+ ): void {
77
+ this.slots.set(slot, { factory: normalizeFactory(factoryOrInstance) as SlotFactory<SlotName> })
78
+ }
79
+
80
+ /** @internal — used by buildResolved and by Plan 02's warning emitter. */
81
+ getEntry<K extends SlotName>(slot: K): SlotEntry<K> | undefined {
82
+ return this.slots.get(slot) as SlotEntry<K> | undefined
83
+ }
84
+
85
+ /** @internal — used by Plan 02 to iterate registered slots for startup warnings. */
86
+ has(slot: SlotName): boolean {
87
+ return this.slots.has(slot)
88
+ }
89
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Smallest viable logger interface (D-51) — just `warn(msg: string)`.
3
+ * Phase 5 only emits startup warnings for in-memory defaults; richer levels
4
+ * are intentionally NOT in scope.
5
+ */
6
+ export interface WarningLogger {
7
+ warn(msg: string): void
8
+ }
9
+
10
+ export interface WarningChannelOptions {
11
+ /** When true, suppress all warnings (kronos({ quiet: true })). */
12
+ quiet?: boolean
13
+ /** When set, route warnings here instead of console.warn. `quiet` takes precedence over `logger`. */
14
+ logger?: WarningLogger
15
+ }
16
+
17
+ export interface WarningChannel {
18
+ emit(msg: string): void
19
+ }
20
+
21
+ export function createWarningChannel(options: WarningChannelOptions = {}): WarningChannel {
22
+ return {
23
+ emit(msg: string): void {
24
+ if (options.quiet) return
25
+ if (options.logger) {
26
+ options.logger.warn(msg)
27
+ return
28
+ }
29
+ console.warn(msg)
30
+ },
31
+ }
32
+ }