@directive-run/core 1.17.2 → 1.18.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/adapter-utils.d.cts +1 -1
  2. package/dist/adapter-utils.d.ts +1 -1
  3. package/dist/chunk-3LBPXJO2.cjs +3 -0
  4. package/dist/chunk-3LBPXJO2.cjs.map +1 -0
  5. package/dist/chunk-4QCKAJCX.cjs +2 -0
  6. package/dist/chunk-4QCKAJCX.cjs.map +1 -0
  7. package/dist/chunk-NPKUAG3Q.js +3 -0
  8. package/dist/chunk-NPKUAG3Q.js.map +1 -0
  9. package/dist/{chunk-F5JPF4YX.js → chunk-SGTWS4S2.js} +3 -3
  10. package/dist/chunk-SGTWS4S2.js.map +1 -0
  11. package/dist/chunk-WKCMOU2F.js +2 -0
  12. package/dist/chunk-WKCMOU2F.js.map +1 -0
  13. package/dist/{chunk-SUCLMJSZ.cjs → chunk-YYBU4TCO.cjs} +3 -3
  14. package/dist/chunk-YYBU4TCO.cjs.map +1 -0
  15. package/dist/{index-D_oNDe4l.d.cts → index-DW5awWlo.d.ts} +25 -2
  16. package/dist/{index-c5wGodzh.d.ts → index-uuMlmmrP.d.cts} +25 -2
  17. package/dist/index.cjs +2 -2
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +21 -6
  20. package/dist/index.d.ts +21 -6
  21. package/dist/index.js +2 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/internals.cjs +1 -1
  24. package/dist/internals.d.cts +204 -5
  25. package/dist/internals.d.ts +204 -5
  26. package/dist/internals.js +1 -1
  27. package/dist/plugins/index.cjs +2 -2
  28. package/dist/plugins/index.cjs.map +1 -1
  29. package/dist/plugins/index.d.cts +2 -2
  30. package/dist/plugins/index.d.ts +2 -2
  31. package/dist/plugins/index.js +1 -1
  32. package/dist/plugins/index.js.map +1 -1
  33. package/dist/{plugins-BpNMkJRw.d.ts → plugins-xpm4B7Nt.d.cts} +550 -1
  34. package/dist/{plugins-BpNMkJRw.d.cts → plugins-xpm4B7Nt.d.ts} +550 -1
  35. package/dist/{predicate-BinuVL33.d.ts → predicate-CMGFjIeu.d.cts} +2 -2
  36. package/dist/{predicate-DN1MnDbi.d.cts → predicate-fpUoy77T.d.ts} +2 -2
  37. package/dist/system-6D4C5QZL.cjs +2 -0
  38. package/dist/{system-RHSYFDOB.cjs.map → system-6D4C5QZL.cjs.map} +1 -1
  39. package/dist/system-ATW7RUEU.js +2 -0
  40. package/dist/{system-MMCZKPI2.js.map → system-ATW7RUEU.js.map} +1 -1
  41. package/dist/testing.cjs +1 -1
  42. package/dist/testing.d.cts +1 -1
  43. package/dist/testing.d.ts +1 -1
  44. package/dist/testing.js +1 -1
  45. package/dist/worker.cjs +1 -1
  46. package/dist/worker.d.cts +1 -1
  47. package/dist/worker.d.ts +1 -1
  48. package/dist/worker.js +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-4MTC4BSK.cjs +0 -2
  51. package/dist/chunk-4MTC4BSK.cjs.map +0 -1
  52. package/dist/chunk-F5JPF4YX.js.map +0 -1
  53. package/dist/chunk-Q5UTBMX2.cjs +0 -3
  54. package/dist/chunk-Q5UTBMX2.cjs.map +0 -1
  55. package/dist/chunk-RWJV7QLM.js +0 -3
  56. package/dist/chunk-RWJV7QLM.js.map +0 -1
  57. package/dist/chunk-SUCLMJSZ.cjs.map +0 -1
  58. package/dist/chunk-YYFRZJO2.js +0 -2
  59. package/dist/chunk-YYFRZJO2.js.map +0 -1
  60. package/dist/system-MMCZKPI2.js +0 -2
  61. package/dist/system-RHSYFDOB.cjs +0 -2
@@ -519,6 +519,20 @@ type RequirementPayloadSchema = Record<string, SchemaType<unknown>>;
519
519
  * ```
520
520
  */
521
521
  type RequirementsSchema = Record<string, RequirementPayloadSchema>;
522
+ /**
523
+ * Infer the requirement payload type from a requirement payload schema.
524
+ */
525
+ type InferRequirementPayload<P extends RequirementPayloadSchema> = {
526
+ [K in keyof P]: P[K] extends SchemaType<infer T> ? T : never;
527
+ };
528
+ /**
529
+ * Infer all requirements from a requirements schema as a discriminated union.
530
+ */
531
+ type InferRequirementsFromSchema<R extends RequirementsSchema> = {
532
+ [K in keyof R]: {
533
+ type: K;
534
+ } & InferRequirementPayload<R[K]>;
535
+ }[keyof R];
522
536
  /**
523
537
  * Requirement output from a constraint - can be single, array, or null.
524
538
  * - Single requirement: `{ type: "RESTOCK", sku: "ABC" }`
@@ -689,6 +703,277 @@ interface EffectDef<S extends Schema> {
689
703
  /** Map of effect definitions */
690
704
  type EffectsDef<S extends Schema> = Record<string, EffectDef<S>>;
691
705
 
706
+ /**
707
+ * Source Types — typed external event sources.
708
+ *
709
+ * A `source` is the inbound dual of an {@link EffectDef effect}:
710
+ *
711
+ * - **Effects** are outbound. They observe fact changes and push to the
712
+ * external world (DOM mutation, WebSocket send, logging, analytics).
713
+ * Effects cannot dispatch `system.events`.
714
+ * - **Sources** are inbound. They subscribe to an external event stream
715
+ * (Supabase realtime channel, WebSocket message stream, browser event,
716
+ * polling timer) and publish *into* the system's event queue. Sources
717
+ * never read facts.
718
+ *
719
+ * Sources sit on the same lifecycle plane as effects but mount once per
720
+ * system instance (not on every fact change). `attach` runs at
721
+ * `system.start()`; the returned unsubscribe runs at `system.stop()`.
722
+ *
723
+ * ## Why this exists
724
+ *
725
+ * The "hook-as-bridge" pattern — a `useEffect` that owns an external
726
+ * subscription, maps incoming payloads, and dispatches `sys.events.X()` —
727
+ * shows up in 7+ call sites across the Sizls workspace (Minglingo's
728
+ * `useActiveRoundSystem`, `useBattleRoyaleSystem`, `eventClaims.realtime.ts`,
729
+ * etc.). Each site re-derives the lifecycle plumbing.
730
+ *
731
+ * A `source` declares that plumbing as a first-class module field. The
732
+ * engine owns the lifecycle; the author owns only the subscribe + publish
733
+ * logic. The same shape works in non-React consumers (Workers, node, tests).
734
+ *
735
+ * @example Supabase realtime channel
736
+ * ```typescript
737
+ * sources: {
738
+ * gameUpdates: {
739
+ * attach: (publish) => {
740
+ * const channel = supabase.channel(`game:${gameId}`).on(
741
+ * 'postgres_changes',
742
+ * { event: 'UPDATE', table: 'games', filter: `id=eq.${gameId}` },
743
+ * (payload) => publish('REALTIME_GAME_UPDATE', { gameState: map(payload.new) }),
744
+ * ).subscribe();
745
+ * return () => channel.unsubscribe();
746
+ * },
747
+ * },
748
+ * }
749
+ * ```
750
+ *
751
+ * @example Polling timer
752
+ * ```typescript
753
+ * sources: {
754
+ * heartbeat: {
755
+ * attach: (publish) => {
756
+ * const id = setInterval(() => publish('HEARTBEAT_TICK', undefined), 5000);
757
+ * return () => clearInterval(id);
758
+ * },
759
+ * },
760
+ * }
761
+ * ```
762
+ *
763
+ * @example No-op (test) source — note the `() => () => undefined` shape:
764
+ * `attach` MUST return a function. Returning `() => undefined` (one level)
765
+ * would return `undefined`, which the manager logs as "did not return an
766
+ * unsubscribe function" and skips. Always two levels.
767
+ * ```typescript
768
+ * sources: {
769
+ * ignored: { attach: () => () => undefined },
770
+ * // ^^^^^^^^^^^^^^^^^^^
771
+ * // | the function that's the unsubscribe
772
+ * // the function that's `attach`'s return value
773
+ * }
774
+ * ```
775
+ *
776
+ * @example Typed publish — wrap once for compile-time event-name + payload safety
777
+ * ```typescript
778
+ * import type { SourcePublish } from '@directive-run/core';
779
+ *
780
+ * function createPublisher(publish: SourcePublish) {
781
+ * return {
782
+ * TICK: (delta: number) => publish('TICK', { delta }),
783
+ * HEARTBEAT: () => publish('HEARTBEAT'),
784
+ * };
785
+ * }
786
+ *
787
+ * sources: {
788
+ * ticker: {
789
+ * attach: (publish) => {
790
+ * const p = createPublisher(publish);
791
+ * const id = setInterval(() => p.TICK(1), 1000); // ← typed
792
+ * return () => clearInterval(id);
793
+ * },
794
+ * },
795
+ * }
796
+ * ```
797
+ */
798
+
799
+ /**
800
+ * Cleanup function returned by a source's `attach`.
801
+ *
802
+ * Called once at `system.stop()`. On the next `system.start()` (the manager
803
+ * supports the full start → stop → start lifecycle), the source's `attach`
804
+ * runs again and returns a FRESH unsubscribe. Implementations SHOULD guard
805
+ * repeated teardown defensively, but the manager will only invoke the
806
+ * captured unsubscribe once per attach cycle.
807
+ *
808
+ * The name is intentional — sources are typically wrapping a stateful
809
+ * external subscription (Supabase channel, WebSocket, browser listener), so
810
+ * "unsubscribe" is the verb authors recognise. Note that effects use
811
+ * `EffectCleanup` instead; the asymmetric naming captures the asymmetric
812
+ * concern (an effect cleans up a side effect; a source unsubscribes from
813
+ * an external stream).
814
+ */
815
+ type SourceUnsubscribe = () => void | Promise<void>;
816
+ /**
817
+ * Typed event dispatcher passed to a source's `attach`. Calls into the same
818
+ * dispatch queue used by `system.events.X(payload)`.
819
+ *
820
+ * The string event name is unchecked at this level (kept as `string` to
821
+ * avoid coupling the source primitive to the consuming module's event
822
+ * schema). The authoring module typically wraps `publish` in a typed helper
823
+ * — see the {@link SourceDef} example for the recommended pattern. The
824
+ * runtime semantics:
825
+ *
826
+ * - `publish('KNOWN_EVENT', payload)` — dispatches into the module's event
827
+ * handler. The payload is augmented with `{ type: eventName, ...payload }`
828
+ * before passing to the handler (same shape as `system.events.X(payload)`).
829
+ * - `publish('UNKNOWN_EVENT', ...)` — in development, logs a
830
+ * `[Directive] Unknown event type` warning and drops the dispatch.
831
+ * In production, silently drops. **This is a footgun for sources** —
832
+ * strongly prefer wrapping `publish` in a typed factory (per the example).
833
+ * - `publish(event, undefined)` — dispatches with an empty payload object.
834
+ * - Calling `publish` after `system.destroy()` — silently no-ops. The
835
+ * engine guards against post-destroy dispatch so stale source callbacks
836
+ * cannot mutate a torn-down store.
837
+ */
838
+ /**
839
+ * Type-wrapped as an interface (rather than a bare function type) so
840
+ * additive minors can attach optional methods (`error`, `complete` —
841
+ * see RFC 0008 Observer-protocol posture) without a major bump. The
842
+ * call signature is unchanged: existing `publish('EVENT', payload)`
843
+ * call sites keep working.
844
+ */
845
+ interface SourcePublish {
846
+ (event: string, payload?: unknown): void;
847
+ }
848
+ /**
849
+ * A source definition — attaches an external event stream to the system
850
+ * lifecycle.
851
+ *
852
+ * ## Lifecycle
853
+ *
854
+ * - `attach(publish)` runs once at `system.start()`. It is sync.
855
+ * - The returned `SourceUnsubscribe` runs at `system.stop()`.
856
+ * - Multiple sources per module are supported; ordering is registration
857
+ * order at `attach`, reverse-registration order at unsubscribe.
858
+ *
859
+ * ## Restrictions
860
+ *
861
+ * - `attach` is **synchronous**. Returning a `Promise<Unsubscribe>` does NOT
862
+ * work — the engine discards the Promise and treats it as "no cleanup
863
+ * function returned" (logs an error + skips the source). Do async setup
864
+ * inside the subscription's own internals; `attach` returns immediately
865
+ * with the synchronous unsubscribe function.
866
+ * - Sources cannot subscribe to system facts. Use {@link EffectDef effects}
867
+ * for fact-reactive behavior. If you need to re-subscribe when a fact
868
+ * changes, use `system.registerModule()` to swap the source's owning
869
+ * module, OR drive the subscription via an effect that owns its own
870
+ * channel.
871
+ * - The publish callback dispatches events normally — resolvers, fact
872
+ * handlers, and downstream effects all run. Authors SHOULD throttle /
873
+ * debounce inside the source if the inbound rate may exceed the system's
874
+ * reconciliation budget.
875
+ * - **Don't subscribe to the same external channel from both a source AND
876
+ * an effect.** The effect will re-run on fact changes, the source mounts
877
+ * once — you'll get 2× messages with silent duplicates. Pick one.
878
+ */
879
+ /**
880
+ * Source-side runtime-error reporter. Optional second argument to
881
+ * `attach` per RFC 0008. Authors call this when the underlying stream
882
+ * errors mid-flight (WebSocket disconnect, Supabase channel goes
883
+ * stale, polling fetch throws) instead of publishing magic event names
884
+ * like `STREAM_ERROR`. The manager forwards the error through the same
885
+ * sinks as attach/cleanup failures, with `phase: "runtime"`.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * sources: {
890
+ * ws: {
891
+ * attach: (publish, reportError) => {
892
+ * const sock = new WebSocket(url);
893
+ * sock.addEventListener('error', () => reportError(new Error('WS error')));
894
+ * sock.addEventListener('message', (e) => publish('MSG', JSON.parse(e.data)));
895
+ * return () => sock.close();
896
+ * },
897
+ * },
898
+ * }
899
+ * ```
900
+ */
901
+ type SourceReportError = (error: Error) => void;
902
+ interface SourceDef {
903
+ /**
904
+ * Mount the source against the system. Runs once at `system.start()`.
905
+ *
906
+ * @param publish - dispatch typed events into the system's event queue.
907
+ * @param reportError - report a runtime error from the source's
908
+ * underlying stream. Fires `source.error` observation events with
909
+ * `phase: "runtime"` (distinct from `"attach"` and `"cleanup"`)
910
+ * so observers can attribute the failure correctly. Optional —
911
+ * sources that never error mid-flight don't need it.
912
+ * @returns a cleanup function that runs at `system.stop()`.
913
+ */
914
+ attach: (publish: SourcePublish, reportError?: SourceReportError) => SourceUnsubscribe;
915
+ /** Optional metadata for debugging and devtools (never read on hot path). */
916
+ meta?: DefinitionMeta;
917
+ /**
918
+ * How the manager absorbs publishes that would overwhelm the
919
+ * reconcile loop. Default: `"none"` — every publish dispatches
920
+ * straight through to the engine.
921
+ *
922
+ * Set `"lastWriteWins"` for high-frequency sources (cursor movement,
923
+ * sensor telemetry, channel storms). The manager coalesces publishes
924
+ * with the same event name within a single microtask: only the last
925
+ * payload of the cycle dispatches; earlier ones bump `dropCount` and
926
+ * record `lastDropReason: "coalesced"` per source so operators can
927
+ * see the rate of debouncing on `system.inspect().sources`.
928
+ *
929
+ * `"all"` is a no-op equivalent to `"none"` — it names the intent
930
+ * (no coalesce, every publish counts) for readers.
931
+ *
932
+ * Choose `"none"` (default) for low-frequency lifecycle sources
933
+ * (MCP connect, DO alarm, WebSocket open/close). Choose
934
+ * `"lastWriteWins"` for any source that could publish faster than
935
+ * the reconcile loop can drain. See `RFC 0007` for throughput
936
+ * budgets per coalesce strategy and the rationale.
937
+ *
938
+ * Coalescing applies per-event-name: two different event names from
939
+ * the same source coalesce independently, so a `"priceTick"` storm
940
+ * doesn't drop a one-shot `"connected"` event.
941
+ *
942
+ * **Limitation:** the coalesce STRATEGY is uniform across all event
943
+ * names on a source — you can't mix (e.g. `lastWriteWins` for
944
+ * `priceTick` and `none` for `connected` from the same source).
945
+ * Workaround: split into two source declarations on the module if
946
+ * the strategies must differ. A future RFC may add per-event-name
947
+ * strategy overrides.
948
+ */
949
+ coalesce?: "none" | "lastWriteWins" | "all";
950
+ /**
951
+ * Called when the host runtime signals the isolate is about to be
952
+ * evicted (Cloudflare DO hibernation, Workers memory pressure, etc.).
953
+ * Use this to actively close external subscriptions BEFORE the
954
+ * isolate dies, so the broker / remote service doesn't accumulate
955
+ * ghost subscriptions visible as "phantom presence" bugs at fleet
956
+ * scale.
957
+ *
958
+ * Distinct from `unsubscribe()`: eviction can fire WITHOUT a
959
+ * `system.stop()` having been called. The host runtime invokes this
960
+ * via `system.evict()`, which fires every source's `onEvict()` in
961
+ * registration order, then `destroyAsync()`. The entire call is
962
+ * awaitable up to a runtime-supplied deadline.
963
+ *
964
+ * Optional — sources whose underlying transport is short-lived
965
+ * (browser WebSocket, in-process EventEmitter) don't need it. RFC
966
+ * 0009 documents the full DO-eviction recipe.
967
+ */
968
+ onEvict?: () => void | Promise<void>;
969
+ }
970
+ /**
971
+ * Map of source definitions, keyed by source name. Collision rules match
972
+ * effects/events/constraints: a source name must be unique across all
973
+ * modules in a system.
974
+ */
975
+ type SourcesDef = Record<string, SourceDef>;
976
+
692
977
  /**
693
978
  * Event Types - Type definitions for events and event handlers
694
979
  */
@@ -911,8 +1196,63 @@ interface ResolverDef<S extends Schema, R extends Requirement = Requirement> {
911
1196
  /** Optional metadata for debugging and devtools (never read on hot path). */
912
1197
  meta?: DefinitionMeta;
913
1198
  }
1199
+ /**
1200
+ * Inferred requirement type helper.
1201
+ * Constructs a requirement type from a requirements schema entry.
1202
+ *
1203
+ * @typeParam R - The requirements schema
1204
+ * @typeParam T - The requirement type key
1205
+ *
1206
+ * @example
1207
+ * ```typescript
1208
+ * const requirements = {
1209
+ * FETCH_USER: { userId: t.string() },
1210
+ * SEND_EMAIL: { to: t.string(), subject: t.string() },
1211
+ * };
1212
+ *
1213
+ * // InferredReq<typeof requirements, "FETCH_USER"> = { type: "FETCH_USER"; userId: string }
1214
+ * ```
1215
+ */
1216
+ type InferredReq<R extends RequirementsSchema, T extends keyof R & string> = {
1217
+ type: T;
1218
+ } & InferRequirementPayload<R[T]>;
1219
+ /**
1220
+ * Typed resolver for a specific requirement type.
1221
+ */
1222
+ type TypedResolverForType<S extends Schema, R extends RequirementsSchema, T extends keyof R & string> = {
1223
+ /** Requirement type to handle */
1224
+ requirement: T;
1225
+ /** Custom key function for deduplication */
1226
+ key?: (req: InferredReq<R, T>) => string;
1227
+ /** Retry policy */
1228
+ retry?: RetryPolicy;
1229
+ /** Timeout for resolver execution (ms) */
1230
+ timeout?: number;
1231
+ /** Batch configuration */
1232
+ batch?: BatchConfig;
1233
+ /** Resolve function for single requirement */
1234
+ resolve?: (req: InferredReq<R, T>, ctx: ResolverContext<S>) => Promise<void>;
1235
+ /** Resolve function for batched requirements (all-or-nothing) */
1236
+ resolveBatch?: (reqs: InferredReq<R, T>[], ctx: ResolverContext<S>) => Promise<void>;
1237
+ /** Resolve function for batched requirements with per-item results */
1238
+ resolveBatchWithResults?: (reqs: InferredReq<R, T>[], ctx: ResolverContext<S>) => Promise<BatchResolveResults>;
1239
+ };
1240
+ /**
1241
+ * Union of all typed resolver configurations for all requirement types.
1242
+ * TypeScript narrows based on the `requirement` literal value.
1243
+ */
1244
+ type AnyTypedResolver<S extends Schema, R extends RequirementsSchema> = {
1245
+ [T in keyof R & string]: TypedResolverForType<S, R, T>;
1246
+ }[keyof R & string];
914
1247
  /** Map of resolver definitions */
915
1248
  type ResolversDef<S extends Schema> = Record<string, ResolverDef<S, Requirement>>;
1249
+ /**
1250
+ * Map of typed resolver definitions (schema-based variant).
1251
+ * Each resolver uses `requirement: "TYPE"` with types inferred from the requirements schema.
1252
+ *
1253
+ * @internal Use `TypedResolversDef` from `types/module.ts` for the public module-based API.
1254
+ */
1255
+ type SchemaTypedResolversDef<S extends Schema, R extends RequirementsSchema> = Record<string, AnyTypedResolver<S, R> | ResolverDef<S, Requirement & InferRequirementsFromSchema<R>>>;
916
1256
  /** Resolver status */
917
1257
  type ResolverStatus = {
918
1258
  state: "idle";
@@ -1946,6 +2286,15 @@ interface ModuleDef<M extends ModuleSchema = ModuleSchema> {
1946
2286
  derive?: TypedDerivationsDef<M>;
1947
2287
  events?: TypedEventsDef<M>;
1948
2288
  effects?: EffectsDef<M["facts"]>;
2289
+ /**
2290
+ * Typed external event sources. See {@link SourceDef} for the primitive's
2291
+ * lifecycle + rationale. Each source attaches at `system.start()` and
2292
+ * tears down at `system.stop()`. Use for Supabase realtime channels,
2293
+ * WebSocket message streams, polling timers, browser event listeners —
2294
+ * any inbound external event the module needs to map into its own event
2295
+ * dispatch surface.
2296
+ */
2297
+ sources?: SourcesDef;
1949
2298
  constraints?: TypedConstraintsDef<M>;
1950
2299
  resolvers?: TypedResolversDef<M>;
1951
2300
  hooks?: ModuleHooks<M>;
@@ -2200,6 +2549,78 @@ interface SystemInspection {
2200
2549
  id: string;
2201
2550
  meta?: DefinitionMeta;
2202
2551
  }>;
2552
+ /**
2553
+ * All declared source names with the module that owns each, plus per-source
2554
+ * telemetry surfaced for production-debug ("which source is publishing?",
2555
+ * "when did this source last fire?", "is this source errored?", "is the
2556
+ * engine silently dropping publishes from this source?") without requiring
2557
+ * a custom plugin to be installed first.
2558
+ *
2559
+ * Counters reset at every `system.start()` cycle — a stop → start does not
2560
+ * carry "ghost" counts from the previous cycle. Timestamps are wall-clock
2561
+ * milliseconds (Date.now()); `null` means "never happened in this cycle".
2562
+ *
2563
+ * `dropCount` / `lastDropReason` / `lastDropAt` count publishes the
2564
+ * engine's dispatch guard rejected (post-stop, BLOCKED_PROPS event name,
2565
+ * empty / non-string event name). Without these, attackers — or a buggy
2566
+ * source — could probe BLOCKED_PROPS / the isRunning guard invisibly:
2567
+ * telemetry would never advance and no plugin hook would fire.
2568
+ *
2569
+ * `attachedSourceCount` (below) is the aggregate count of `attached: true`
2570
+ * rows; both must stay in lockstep.
2571
+ */
2572
+ sources: Array<{
2573
+ id: string;
2574
+ moduleId: string;
2575
+ meta?: DefinitionMeta;
2576
+ /** True while the source's attach succeeded and its unsubscribe is held. */
2577
+ attached: boolean;
2578
+ /** Wall-clock ms when the source most recently attached, or null. */
2579
+ attachedAt: number | null;
2580
+ /** Wall-clock ms when the source most recently detached, or null. */
2581
+ detachedAt: number | null;
2582
+ /** Total publish() invocations the engine accepted since the last attachAll. */
2583
+ publishCount: number;
2584
+ /** Wall-clock ms of the most recent accepted publish() call, or null. */
2585
+ lastPublishAt: number | null;
2586
+ /**
2587
+ * Total publish() invocations the engine's dispatch guard rejected this
2588
+ * cycle. Operators monitor this to spot misconfigured sources (wrong
2589
+ * event names) or probing of the BLOCKED_PROPS / isRunning guards.
2590
+ */
2591
+ dropCount: number;
2592
+ /**
2593
+ * Reason for the most recent rejected publish, or `null`.
2594
+ * - `"post-destroy"` / `"post-stop"` — leaked transport firing
2595
+ * outside the running lifecycle window
2596
+ * - `"blocked-event-name"` / `"invalid-event-name"` — engine
2597
+ * dispatch guard rejected the publish (BLOCKED_PROPS / empty)
2598
+ * - `"coalesced"` — manager debounced a same-event-name publish
2599
+ * when `SourceDef.coalesce === "lastWriteWins"`
2600
+ */
2601
+ lastDropReason: "post-destroy" | "post-stop" | "blocked-event-name" | "invalid-event-name" | "coalesced" | null;
2602
+ /** Wall-clock ms of the most recent rejected publish, or `null`. */
2603
+ lastDropAt: number | null;
2604
+ /** Total attach + cleanup errors since the last attachAll. */
2605
+ errorCount: number;
2606
+ /**
2607
+ * The most recent error from this source, or `null`. `message` is
2608
+ * truncated to a fixed maximum length so a source whose `attach()`
2609
+ * throws with a payload-embedded message does not write unbounded data
2610
+ * here AND downstream into the audit ledger. `phase: "runtime"`
2611
+ * (RFC 0008) flags errors that the source reported mid-flight via
2612
+ * the `reportError` callback `attach` receives as its second
2613
+ * argument (distinct from lifecycle `"attach"` / `"cleanup"`
2614
+ * failures).
2615
+ */
2616
+ lastError: {
2617
+ phase: "attach" | "cleanup" | "runtime";
2618
+ message: string;
2619
+ at: number;
2620
+ } | null;
2621
+ }>;
2622
+ /** Number of sources currently attached (i.e. system is in the `attached` phase + their attach() succeeded). */
2623
+ attachedSourceCount: number;
2203
2624
  /** All defined derivation names with optional metadata */
2204
2625
  derivations: Array<{
2205
2626
  id: string;
@@ -2627,6 +3048,48 @@ type ObservationEvent = {
2627
3048
  type: "effect.error";
2628
3049
  id: string;
2629
3050
  error: unknown;
3051
+ }
3052
+ /**
3053
+ * Source attached at `system.start()` (or at registerModule when the
3054
+ * system was already running). Carries the source id + the module that
3055
+ * declared it so plugins can attribute per-module sources.
3056
+ */
3057
+ | {
3058
+ type: "source.attach";
3059
+ id: string;
3060
+ moduleId: string;
3061
+ }
3062
+ /**
3063
+ * A source published an event into the system's event queue. Fires
3064
+ * BEFORE the event handler runs; pair with `fact.change` events to
3065
+ * trace the downstream effect.
3066
+ */
3067
+ | {
3068
+ type: "source.publish";
3069
+ id: string;
3070
+ moduleId: string;
3071
+ eventName: string;
3072
+ }
3073
+ /** Source detached at `system.stop()` (reverse-registration order). */
3074
+ | {
3075
+ type: "source.detach";
3076
+ id: string;
3077
+ moduleId: string;
3078
+ }
3079
+ /**
3080
+ * Source `attach` / unsubscribe threw, OR the source called the
3081
+ * `reportError` callback that `attach` received as its second
3082
+ * argument (RFC 0008) — handled, isolated, observable.
3083
+ * `phase: "runtime"` distinguishes mid-flight stream errors
3084
+ * (WebSocket disconnect, Supabase channel goes stale) from lifecycle
3085
+ * `"attach"` / `"cleanup"` failures so observers attribute correctly.
3086
+ */
3087
+ | {
3088
+ type: "source.error";
3089
+ id: string;
3090
+ moduleId: string;
3091
+ phase: "attach" | "cleanup" | "runtime";
3092
+ error: unknown;
2630
3093
  } | {
2631
3094
  type: "derivation.compute";
2632
3095
  id: string;
@@ -2660,6 +3123,23 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2660
3123
  * Observe all lifecycle events as a typed stream.
2661
3124
  * Returns an unsubscribe function.
2662
3125
  *
3126
+ * ## Timing semantics
3127
+ *
3128
+ * Observers receive events that fire **from the moment they subscribe
3129
+ * onwards**. Past events are NOT replayed. Consequences:
3130
+ *
3131
+ * - An observer registered BEFORE `system.start()` sees the initial
3132
+ * `system.start`, `source.attach`, etc. events emitted during start.
3133
+ * - An observer registered AFTER `system.start()` does NOT see those
3134
+ * initial events. To reconstruct the current state, use
3135
+ * `system.inspect()` (`facts`, `sources`, `constraints`, etc.) at
3136
+ * subscription time, then layer the live observer stream on top.
3137
+ * - The unsubscribe function is idempotent — calling it more than once
3138
+ * is safe.
3139
+ *
3140
+ * This mirrors RxJS Subject + DOM EventTarget conventions; there is no
3141
+ * hidden replay buffer.
3142
+ *
2663
3143
  * @example
2664
3144
  * ```typescript
2665
3145
  * const unsub = system.observe((event) => {
@@ -2668,6 +3148,13 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2668
3148
  * }
2669
3149
  * });
2670
3150
  * ```
3151
+ *
3152
+ * @example Reconstructing state on late subscription
3153
+ * ```typescript
3154
+ * const snapshot = system.inspect();
3155
+ * console.log('active sources:', snapshot.attachedSourceCount);
3156
+ * const unsub = system.observe((event) => { ... }); // forward-only
3157
+ * ```
2671
3158
  */
2672
3159
  observe(observer: (event: ObservationEvent) => void): () => void;
2673
3160
  /** Per-run trace entries (null if trace is not enabled) */
@@ -2677,6 +3164,35 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2677
3164
  start(): void;
2678
3165
  stop(): void;
2679
3166
  destroy(): void;
3167
+ /**
3168
+ * RFC 0009: async-aware variant of `stop()`. Awaits each source's
3169
+ * unsubscribe before resolving. Use when sources have async
3170
+ * unsubscribes (Supabase `channel.unsubscribe()`, Cloudflare DO
3171
+ * storage flushes) and the caller needs to know teardown actually
3172
+ * completed before continuing.
3173
+ */
3174
+ stopAsync(): Promise<void>;
3175
+ /**
3176
+ * RFC 0009: async-aware variant of `destroy()`. Equivalent to
3177
+ * `stopAsync` followed by `destroy`. Use in DO `webSocketClose` /
3178
+ * `alarm` handlers where the caller must let the runtime evict the
3179
+ * isolate cleanly.
3180
+ */
3181
+ destroyAsync(): Promise<void>;
3182
+ /**
3183
+ * RFC 0009: signal the host runtime is about to evict this isolate.
3184
+ * Fires every source's `onEvict()` in registration order (so sources
3185
+ * with downstream dependencies close in the right order), then calls
3186
+ * `destroyAsync()`. Cloudflare DO consumers call this from their
3187
+ * `alarm()` / `webSocketClose()` handlers BEFORE letting the
3188
+ * runtime evict so external brokers don't accumulate ghost
3189
+ * subscriptions.
3190
+ *
3191
+ * @param deadline - Optional wall-clock ms deadline. If supplied,
3192
+ * evicts that have not resolved by `deadline` are abandoned (the
3193
+ * isolate will die anyway). Defaults to no deadline.
3194
+ */
3195
+ evict(deadline?: number): Promise<void>;
2680
3196
  readonly isRunning: boolean;
2681
3197
  readonly isSettled: boolean;
2682
3198
  /** Whether all modules have completed initialization */
@@ -3041,6 +3557,39 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3041
3557
  * @param error - The error that was thrown
3042
3558
  */
3043
3559
  onEffectError?: (id: string, error: unknown) => void;
3560
+ /**
3561
+ * Called when a typed external event source attaches at `system.start()`
3562
+ * (or when a dynamically registered module brings new sources to an
3563
+ * already-running system).
3564
+ * @param id - The source ID (key on the module's `sources:` map).
3565
+ * @param moduleId - The owning module's id.
3566
+ */
3567
+ onSourceAttach?: (id: string, moduleId: string) => void;
3568
+ /**
3569
+ * Called when a source publishes an event into the system's event queue.
3570
+ * Fires BEFORE the event handler runs. Pair with `onFactChange` to trace
3571
+ * the downstream state mutation.
3572
+ * @param id - The source ID.
3573
+ * @param moduleId - The owning module's id.
3574
+ * @param eventName - The dispatched event name (the first arg to `publish`).
3575
+ */
3576
+ onSourcePublish?: (id: string, moduleId: string, eventName: string) => void;
3577
+ /**
3578
+ * Called when a source detaches at `system.stop()` (reverse-registration
3579
+ * order across all modules).
3580
+ * @param id - The source ID.
3581
+ * @param moduleId - The owning module's id.
3582
+ */
3583
+ onSourceDetach?: (id: string, moduleId: string) => void;
3584
+ /**
3585
+ * Called when a source's `attach` or unsubscribe throws. Errors are
3586
+ * isolated (one bad source never blocks others) but observable here.
3587
+ * @param id - The source ID.
3588
+ * @param moduleId - The owning module's id.
3589
+ * @param phase - Which lifecycle stage threw.
3590
+ * @param error - The thrown value (normalized to Error inside the manager).
3591
+ */
3592
+ onSourceError?: (id: string, moduleId: string, phase: "attach" | "cleanup" | "runtime", error: unknown) => void;
3044
3593
  /**
3045
3594
  * Called when a history snapshot is taken.
3046
3595
  * @param snapshot - The snapshot that was captured
@@ -3099,4 +3648,4 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3099
3648
  onTraceComplete?: (entry: TraceEntry) => void;
3100
3649
  }
3101
3650
 
3102
- export { type PatchSpec as $, type AnySystem as A, type BatchConfig as B, type ClauseResult as C, type DefinitionMeta as D, type EffectsDef as E, type FactPredicate as F, type HistoryOption as G, type HistoryAPI as H, type HistoryState as I, type InferDerivations as J, type InferEvents as K, type InferFacts as L, type ModuleSchema as M, type NamespacedSystem as N, type InferRequirementTypes as O, type Plugin as P, type InferRequirements as Q, type RequirementWithId as R, type SchemaType as S, type TypedDerivationsDef as T, type InferSchemaType as U, type InferSelectorState as V, type KeySelector as W, type MetaAccessor as X, type MetaMatch as Y, type ObservationEvent as Z, type OperatorObject as _, type Facts as a, type RequirementOutput as a$, type PatchValue as a0, type PayloadRef as a1, type PredicateClause as a2, type PredicateCombinator as a3, type PredicateCombinatorKey as a4, type PredicateObject as a5, type PredicateOp as a6, type RetryPolicy as a7, type Schema as a8, type Snapshot as a9, type CrossModuleFactsWithSelf as aA, type DerivationKeys as aB, type DerivationReturnType as aC, type DerivationsControl as aD, type DerivationsSchema as aE, type DeriveAccessor as aF, type DispatchEventsFromSchema as aG, type EffectCleanup as aH, type EffectsControl as aI, type EventPayloadSchema as aJ, type EventsAccessor as aK, type EventsAccessorFromSchema as aL, type EventsDef as aM, type EventsSchema as aN, type FactKeys as aO, type FactReturnType as aP, type FlexibleEventHandler as aQ, type HistoryConfig as aR, type InferEventPayloadFromSchema as aS, type InferRequirementPayloadFromSchema as aT, type InferSchema as aU, type MutableNamespacedFacts as aV, type NamespacedDerivations as aW, type NamespacedEventsAccessor as aX, type NamespacedFacts as aY, type ObservableKeys as aZ, type RequirementExplanation as a_, type System as aa, type SystemConfig as ab, type SystemDerived as ac, type SystemFacts as ad, type SystemInspection as ae, type SystemMode as af, type SystemSnapshot as ag, type TraceEntry as ah, isNamespacedSystem as ai, isSingleModuleSystem as aj, type FactsStore as ak, type ConstraintsDef as al, type ConstraintState as am, type ResolversDef as an, type ResolverStatus as ao, type FactChange as ap, type ReconcileResult as aq, type RecoveryStrategy as ar, type ErrorSource as as, type RetryLaterConfig as at, type BatchItemResult as au, type BatchResolveResults as av, type ConstraintsControl as aw, type CrossModuleConstraintDef as ax, type CrossModuleDerivationFn as ay, type CrossModuleEffectDef as az, type TypedEventsDef as b, type RequirementPayloadSchema$1 as b0, type RequirementsSchema as b1, type ResolverContext as b2, type ResolversControl as b3, type SnapshotMeta as b4, type SystemEvent as b5, type TraceConfig as b6, type TypedConstraintDef as b7, type TypedResolverContext as b8, type TypedResolverDef as b9, type UnionEvents as ba, type RequirementOutput$1 as bb, type TypedConstraintsDef as c, type TypedResolversDef as d, type ModuleHooks as e, type CrossModuleDeps as f, type CrossModuleDerivationsDef as g, type CrossModuleEffectsDef as h, type CrossModuleConstraintsDef as i, type ModuleDef as j, type CreateSystemOptionsSingle as k, type SingleModuleSystem as l, type ModulesMap as m, type CreateSystemOptionsNamed as n, type TraceOption as o, type ErrorBoundaryConfig as p, type Requirement as q, type RequirementKeyFn as r, DirectiveError as s, type DistributableSnapshot as t, type DistributableSnapshotOptions as u, type DynamicConstraintDef as v, type DynamicEffectDef as w, type DynamicResolverDef as x, type FactTemplate as y, type FactsSnapshot as z };
3651
+ export { type InferSchemaType as $, type AnySystem as A, type BatchConfig as B, type ClauseResult as C, type DefinitionMeta as D, type EffectsDef as E, type FactPredicate as F, type DynamicConstraintDef as G, type DynamicEffectDef as H, type DynamicResolverDef as I, type EffectDef as J, type EventsDef as K, type FactTemplate as L, type ModuleSchema as M, type NamespacedSystem as N, type FactsSnapshot as O, type Plugin as P, type HistoryAPI as Q, type RequirementWithId as R, type SchemaType as S, type TypedDerivationsDef as T, type HistoryOption as U, type HistoryState as V, type InferDerivations as W, type InferEvents as X, type InferFacts as Y, type InferRequirementTypes as Z, type InferRequirements as _, type Facts as a, type FlexibleEventHandler as a$, type InferSelectorState as a0, type KeySelector as a1, type MetaAccessor as a2, type MetaMatch as a3, type ObservationEvent as a4, type OperatorObject as a5, type PatchSpec as a6, type PatchValue as a7, type PayloadRef as a8, type PredicateClause as a9, type FactsStore as aA, type ConstraintState as aB, type ResolverStatus as aC, type FactChange as aD, type ReconcileResult as aE, type RecoveryStrategy as aF, type ErrorSource as aG, type RetryLaterConfig as aH, type BatchItemResult as aI, type BatchResolveResults as aJ, type ConstraintsControl as aK, type CrossModuleDerivationFn as aL, type CrossModuleFactsWithSelf as aM, type DerivationKeys as aN, type DerivationReturnType as aO, type DerivationsControl as aP, type DerivationsSchema as aQ, type DeriveAccessor as aR, type DispatchEventsFromSchema as aS, type EffectCleanup as aT, type EffectsControl as aU, type EventPayloadSchema as aV, type EventsAccessor as aW, type EventsAccessorFromSchema as aX, type EventsSchema as aY, type FactKeys as aZ, type FactReturnType as a_, type PredicateCombinator as aa, type PredicateCombinatorKey as ab, type PredicateObject as ac, type PredicateOp as ad, type ResolverDef as ae, type ResolversDef as af, type RetryPolicy as ag, type Schema as ah, type SchemaTypedResolversDef as ai, type Snapshot as aj, type SourceDef as ak, type SourcePublish as al, type SourceReportError as am, type SourceUnsubscribe as an, type System as ao, type SystemConfig as ap, type SystemDerived as aq, type SystemFacts as ar, type SystemInspection as as, type SystemMode as at, type SystemSnapshot as au, type TraceEntry as av, type TypedConstraintDef as aw, type TypedResolverDef as ax, isNamespacedSystem as ay, isSingleModuleSystem as az, type TypedEventsDef as b, type HistoryConfig as b0, type InferEventPayloadFromSchema as b1, type InferRequirementPayloadFromSchema as b2, type InferSchema as b3, type MutableNamespacedFacts as b4, type NamespacedDerivations as b5, type NamespacedEventsAccessor as b6, type NamespacedFacts as b7, type ObservableKeys as b8, type RequirementExplanation as b9, type RequirementOutput as ba, type RequirementPayloadSchema$1 as bb, type RequirementsSchema as bc, type ResolverContext as bd, type ResolversControl as be, type SnapshotMeta as bf, type SystemEvent as bg, type TraceConfig as bh, type TypedResolverContext as bi, type UnionEvents as bj, type RequirementOutput$1 as bk, type SourcesDef as c, type TypedConstraintsDef as d, type TypedResolversDef as e, type ModuleHooks as f, type CrossModuleDeps as g, type CrossModuleDerivationsDef as h, type CrossModuleEffectsDef as i, type CrossModuleConstraintsDef as j, type ModuleDef as k, type CreateSystemOptionsSingle as l, type SingleModuleSystem as m, type ModulesMap as n, type CreateSystemOptionsNamed as o, type TraceOption as p, type ErrorBoundaryConfig as q, type Requirement as r, type RequirementKeyFn as s, type ConstraintDef as t, type ConstraintsDef as u, type CrossModuleConstraintDef as v, type CrossModuleEffectDef as w, DirectiveError as x, type DistributableSnapshot as y, type DistributableSnapshotOptions as z };