@directive-run/core 1.17.2 → 1.19.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 (63) hide show
  1. package/dist/adapter-utils.d.cts +1 -1
  2. package/dist/adapter-utils.d.ts +1 -1
  3. package/dist/chunk-43YXBRQC.js +16 -0
  4. package/dist/chunk-43YXBRQC.js.map +1 -0
  5. package/dist/chunk-4QCKAJCX.cjs +2 -0
  6. package/dist/chunk-4QCKAJCX.cjs.map +1 -0
  7. package/dist/chunk-LC5SSD5D.cjs +3 -0
  8. package/dist/chunk-LC5SSD5D.cjs.map +1 -0
  9. package/dist/chunk-NEKZLX66.js +3 -0
  10. package/dist/chunk-NEKZLX66.js.map +1 -0
  11. package/dist/chunk-OSXI7JFB.cjs +16 -0
  12. package/dist/chunk-OSXI7JFB.cjs.map +1 -0
  13. package/dist/chunk-WKCMOU2F.js +2 -0
  14. package/dist/chunk-WKCMOU2F.js.map +1 -0
  15. package/dist/{index-D_oNDe4l.d.cts → index-BjyEUCQ9.d.ts} +25 -2
  16. package/dist/{index-c5wGodzh.d.ts → index-UVB8lHFn.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-yuRFvXJD.d.cts} +600 -1
  34. package/dist/{plugins-BpNMkJRw.d.cts → plugins-yuRFvXJD.d.ts} +600 -1
  35. package/dist/{predicate-DN1MnDbi.d.cts → predicate-BVPgRy9T.d.ts} +2 -2
  36. package/dist/{predicate-BinuVL33.d.ts → predicate-Blbvhuxm.d.cts} +2 -2
  37. package/dist/system-SLMLXFKK.cjs +2 -0
  38. package/dist/{system-RHSYFDOB.cjs.map → system-SLMLXFKK.cjs.map} +1 -1
  39. package/dist/system-U63MK6X4.js +2 -0
  40. package/dist/{system-MMCZKPI2.js.map → system-U63MK6X4.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 +0 -16
  53. package/dist/chunk-F5JPF4YX.js.map +0 -1
  54. package/dist/chunk-Q5UTBMX2.cjs +0 -3
  55. package/dist/chunk-Q5UTBMX2.cjs.map +0 -1
  56. package/dist/chunk-RWJV7QLM.js +0 -3
  57. package/dist/chunk-RWJV7QLM.js.map +0 -1
  58. package/dist/chunk-SUCLMJSZ.cjs +0 -16
  59. package/dist/chunk-SUCLMJSZ.cjs.map +0 -1
  60. package/dist/chunk-YYFRZJO2.js +0 -2
  61. package/dist/chunk-YYFRZJO2.js.map +0 -1
  62. package/dist/system-MMCZKPI2.js +0 -2
  63. 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";
@@ -1178,8 +1518,30 @@ interface NamespacedSystem<Modules extends ModulesMap> {
1178
1518
  start(): void;
1179
1519
  /** Stop the system (cancel resolvers, stop reconciliation) */
1180
1520
  stop(): void;
1521
+ /**
1522
+ * Async-aware stop (RFC 0009). Awaits source unsubscribes so external
1523
+ * transports (Supabase channel.unsubscribe, DO storage flush) settle
1524
+ * before the caller continues. Use this instead of `stop()` when any
1525
+ * declared source returns a Promise from its unsubscribe.
1526
+ */
1527
+ stopAsync(): Promise<void>;
1181
1528
  /** Destroy the system (stop and cleanup) */
1182
1529
  destroy(): void;
1530
+ /**
1531
+ * Async-aware destroy (RFC 0009). Awaits `stopAsync()` first so source
1532
+ * teardown finishes before facts/derivations/effects are wiped.
1533
+ */
1534
+ destroyAsync(): Promise<void>;
1535
+ /**
1536
+ * Signal that the host runtime is about to evict this system (RFC 0009).
1537
+ * Fires every source's `onEvict` in registration order, then races
1538
+ * teardown against `deadline` (ms epoch). With no deadline the full
1539
+ * teardown is awaited. With `deadline <= now` the eviction is
1540
+ * detached so the caller returns immediately. Use for Cloudflare DO
1541
+ * hibernation, Worker eviction, and any runtime that imposes a hard
1542
+ * shutdown wall clock.
1543
+ */
1544
+ evict(deadline?: number): Promise<void>;
1183
1545
  /** Whether the system is currently running */
1184
1546
  readonly isRunning: boolean;
1185
1547
  /** Whether all resolvers have completed */
@@ -1419,8 +1781,30 @@ interface SingleModuleSystem<S extends ModuleSchema> {
1419
1781
  start(): void;
1420
1782
  /** Stop the system (cancel resolvers, stop reconciliation) */
1421
1783
  stop(): void;
1784
+ /**
1785
+ * Async-aware stop (RFC 0009). Awaits source unsubscribes so external
1786
+ * transports (Supabase channel.unsubscribe, DO storage flush) settle
1787
+ * before the caller continues. Use this instead of `stop()` when any
1788
+ * declared source returns a Promise from its unsubscribe.
1789
+ */
1790
+ stopAsync(): Promise<void>;
1422
1791
  /** Destroy the system (stop and cleanup) */
1423
1792
  destroy(): void;
1793
+ /**
1794
+ * Async-aware destroy (RFC 0009). Awaits `stopAsync()` first so source
1795
+ * teardown finishes before facts/derivations/effects are wiped.
1796
+ */
1797
+ destroyAsync(): Promise<void>;
1798
+ /**
1799
+ * Signal that the host runtime is about to evict this system (RFC 0009).
1800
+ * Fires every source's `onEvict` in registration order, then races
1801
+ * teardown against `deadline` (ms epoch). With no deadline the full
1802
+ * teardown is awaited. With `deadline <= now` the eviction is
1803
+ * detached so the caller returns immediately. Use for Cloudflare DO
1804
+ * hibernation, Worker eviction, and any runtime that imposes a hard
1805
+ * shutdown wall clock.
1806
+ */
1807
+ evict(deadline?: number): Promise<void>;
1424
1808
  /** Whether the system is currently running */
1425
1809
  readonly isRunning: boolean;
1426
1810
  /** Whether all resolvers have completed */
@@ -1557,7 +1941,13 @@ interface AnySystem {
1557
1941
  initialize(): void;
1558
1942
  start(): void;
1559
1943
  stop(): void;
1944
+ /** RFC 0009: async-aware stop — awaits source unsubscribes. */
1945
+ stopAsync(): Promise<void>;
1560
1946
  destroy(): void;
1947
+ /** RFC 0009: async-aware destroy. */
1948
+ destroyAsync(): Promise<void>;
1949
+ /** RFC 0009: runtime-eviction signal with optional deadline. */
1950
+ evict(deadline?: number): Promise<void>;
1561
1951
  }
1562
1952
  /**
1563
1953
  * Check if a system is a single module system.
@@ -1946,6 +2336,15 @@ interface ModuleDef<M extends ModuleSchema = ModuleSchema> {
1946
2336
  derive?: TypedDerivationsDef<M>;
1947
2337
  events?: TypedEventsDef<M>;
1948
2338
  effects?: EffectsDef<M["facts"]>;
2339
+ /**
2340
+ * Typed external event sources. See {@link SourceDef} for the primitive's
2341
+ * lifecycle + rationale. Each source attaches at `system.start()` and
2342
+ * tears down at `system.stop()`. Use for Supabase realtime channels,
2343
+ * WebSocket message streams, polling timers, browser event listeners —
2344
+ * any inbound external event the module needs to map into its own event
2345
+ * dispatch surface.
2346
+ */
2347
+ sources?: SourcesDef;
1949
2348
  constraints?: TypedConstraintsDef<M>;
1950
2349
  resolvers?: TypedResolversDef<M>;
1951
2350
  hooks?: ModuleHooks<M>;
@@ -2200,6 +2599,78 @@ interface SystemInspection {
2200
2599
  id: string;
2201
2600
  meta?: DefinitionMeta;
2202
2601
  }>;
2602
+ /**
2603
+ * All declared source names with the module that owns each, plus per-source
2604
+ * telemetry surfaced for production-debug ("which source is publishing?",
2605
+ * "when did this source last fire?", "is this source errored?", "is the
2606
+ * engine silently dropping publishes from this source?") without requiring
2607
+ * a custom plugin to be installed first.
2608
+ *
2609
+ * Counters reset at every `system.start()` cycle — a stop → start does not
2610
+ * carry "ghost" counts from the previous cycle. Timestamps are wall-clock
2611
+ * milliseconds (Date.now()); `null` means "never happened in this cycle".
2612
+ *
2613
+ * `dropCount` / `lastDropReason` / `lastDropAt` count publishes the
2614
+ * engine's dispatch guard rejected (post-stop, BLOCKED_PROPS event name,
2615
+ * empty / non-string event name). Without these, attackers — or a buggy
2616
+ * source — could probe BLOCKED_PROPS / the isRunning guard invisibly:
2617
+ * telemetry would never advance and no plugin hook would fire.
2618
+ *
2619
+ * `attachedSourceCount` (below) is the aggregate count of `attached: true`
2620
+ * rows; both must stay in lockstep.
2621
+ */
2622
+ sources: Array<{
2623
+ id: string;
2624
+ moduleId: string;
2625
+ meta?: DefinitionMeta;
2626
+ /** True while the source's attach succeeded and its unsubscribe is held. */
2627
+ attached: boolean;
2628
+ /** Wall-clock ms when the source most recently attached, or null. */
2629
+ attachedAt: number | null;
2630
+ /** Wall-clock ms when the source most recently detached, or null. */
2631
+ detachedAt: number | null;
2632
+ /** Total publish() invocations the engine accepted since the last attachAll. */
2633
+ publishCount: number;
2634
+ /** Wall-clock ms of the most recent accepted publish() call, or null. */
2635
+ lastPublishAt: number | null;
2636
+ /**
2637
+ * Total publish() invocations the engine's dispatch guard rejected this
2638
+ * cycle. Operators monitor this to spot misconfigured sources (wrong
2639
+ * event names) or probing of the BLOCKED_PROPS / isRunning guards.
2640
+ */
2641
+ dropCount: number;
2642
+ /**
2643
+ * Reason for the most recent rejected publish, or `null`.
2644
+ * - `"post-destroy"` / `"post-stop"` — leaked transport firing
2645
+ * outside the running lifecycle window
2646
+ * - `"blocked-event-name"` / `"invalid-event-name"` — engine
2647
+ * dispatch guard rejected the publish (BLOCKED_PROPS / empty)
2648
+ * - `"coalesced"` — manager debounced a same-event-name publish
2649
+ * when `SourceDef.coalesce === "lastWriteWins"`
2650
+ */
2651
+ lastDropReason: "post-destroy" | "post-stop" | "blocked-event-name" | "invalid-event-name" | "coalesced" | null;
2652
+ /** Wall-clock ms of the most recent rejected publish, or `null`. */
2653
+ lastDropAt: number | null;
2654
+ /** Total attach + cleanup errors since the last attachAll. */
2655
+ errorCount: number;
2656
+ /**
2657
+ * The most recent error from this source, or `null`. `message` is
2658
+ * truncated to a fixed maximum length so a source whose `attach()`
2659
+ * throws with a payload-embedded message does not write unbounded data
2660
+ * here AND downstream into the audit ledger. `phase: "runtime"`
2661
+ * (RFC 0008) flags errors that the source reported mid-flight via
2662
+ * the `reportError` callback `attach` receives as its second
2663
+ * argument (distinct from lifecycle `"attach"` / `"cleanup"`
2664
+ * failures).
2665
+ */
2666
+ lastError: {
2667
+ phase: "attach" | "cleanup" | "runtime";
2668
+ message: string;
2669
+ at: number;
2670
+ } | null;
2671
+ }>;
2672
+ /** Number of sources currently attached (i.e. system is in the `attached` phase + their attach() succeeded). */
2673
+ attachedSourceCount: number;
2203
2674
  /** All defined derivation names with optional metadata */
2204
2675
  derivations: Array<{
2205
2676
  id: string;
@@ -2627,6 +3098,48 @@ type ObservationEvent = {
2627
3098
  type: "effect.error";
2628
3099
  id: string;
2629
3100
  error: unknown;
3101
+ }
3102
+ /**
3103
+ * Source attached at `system.start()` (or at registerModule when the
3104
+ * system was already running). Carries the source id + the module that
3105
+ * declared it so plugins can attribute per-module sources.
3106
+ */
3107
+ | {
3108
+ type: "source.attach";
3109
+ id: string;
3110
+ moduleId: string;
3111
+ }
3112
+ /**
3113
+ * A source published an event into the system's event queue. Fires
3114
+ * BEFORE the event handler runs; pair with `fact.change` events to
3115
+ * trace the downstream effect.
3116
+ */
3117
+ | {
3118
+ type: "source.publish";
3119
+ id: string;
3120
+ moduleId: string;
3121
+ eventName: string;
3122
+ }
3123
+ /** Source detached at `system.stop()` (reverse-registration order). */
3124
+ | {
3125
+ type: "source.detach";
3126
+ id: string;
3127
+ moduleId: string;
3128
+ }
3129
+ /**
3130
+ * Source `attach` / unsubscribe threw, OR the source called the
3131
+ * `reportError` callback that `attach` received as its second
3132
+ * argument (RFC 0008) — handled, isolated, observable.
3133
+ * `phase: "runtime"` distinguishes mid-flight stream errors
3134
+ * (WebSocket disconnect, Supabase channel goes stale) from lifecycle
3135
+ * `"attach"` / `"cleanup"` failures so observers attribute correctly.
3136
+ */
3137
+ | {
3138
+ type: "source.error";
3139
+ id: string;
3140
+ moduleId: string;
3141
+ phase: "attach" | "cleanup" | "runtime";
3142
+ error: unknown;
2630
3143
  } | {
2631
3144
  type: "derivation.compute";
2632
3145
  id: string;
@@ -2660,6 +3173,23 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2660
3173
  * Observe all lifecycle events as a typed stream.
2661
3174
  * Returns an unsubscribe function.
2662
3175
  *
3176
+ * ## Timing semantics
3177
+ *
3178
+ * Observers receive events that fire **from the moment they subscribe
3179
+ * onwards**. Past events are NOT replayed. Consequences:
3180
+ *
3181
+ * - An observer registered BEFORE `system.start()` sees the initial
3182
+ * `system.start`, `source.attach`, etc. events emitted during start.
3183
+ * - An observer registered AFTER `system.start()` does NOT see those
3184
+ * initial events. To reconstruct the current state, use
3185
+ * `system.inspect()` (`facts`, `sources`, `constraints`, etc.) at
3186
+ * subscription time, then layer the live observer stream on top.
3187
+ * - The unsubscribe function is idempotent — calling it more than once
3188
+ * is safe.
3189
+ *
3190
+ * This mirrors RxJS Subject + DOM EventTarget conventions; there is no
3191
+ * hidden replay buffer.
3192
+ *
2663
3193
  * @example
2664
3194
  * ```typescript
2665
3195
  * const unsub = system.observe((event) => {
@@ -2668,6 +3198,13 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2668
3198
  * }
2669
3199
  * });
2670
3200
  * ```
3201
+ *
3202
+ * @example Reconstructing state on late subscription
3203
+ * ```typescript
3204
+ * const snapshot = system.inspect();
3205
+ * console.log('active sources:', snapshot.attachedSourceCount);
3206
+ * const unsub = system.observe((event) => { ... }); // forward-only
3207
+ * ```
2671
3208
  */
2672
3209
  observe(observer: (event: ObservationEvent) => void): () => void;
2673
3210
  /** Per-run trace entries (null if trace is not enabled) */
@@ -2677,6 +3214,35 @@ interface System<M extends ModuleSchema = ModuleSchema> {
2677
3214
  start(): void;
2678
3215
  stop(): void;
2679
3216
  destroy(): void;
3217
+ /**
3218
+ * RFC 0009: async-aware variant of `stop()`. Awaits each source's
3219
+ * unsubscribe before resolving. Use when sources have async
3220
+ * unsubscribes (Supabase `channel.unsubscribe()`, Cloudflare DO
3221
+ * storage flushes) and the caller needs to know teardown actually
3222
+ * completed before continuing.
3223
+ */
3224
+ stopAsync(): Promise<void>;
3225
+ /**
3226
+ * RFC 0009: async-aware variant of `destroy()`. Equivalent to
3227
+ * `stopAsync` followed by `destroy`. Use in DO `webSocketClose` /
3228
+ * `alarm` handlers where the caller must let the runtime evict the
3229
+ * isolate cleanly.
3230
+ */
3231
+ destroyAsync(): Promise<void>;
3232
+ /**
3233
+ * RFC 0009: signal the host runtime is about to evict this isolate.
3234
+ * Fires every source's `onEvict()` in registration order (so sources
3235
+ * with downstream dependencies close in the right order), then calls
3236
+ * `destroyAsync()`. Cloudflare DO consumers call this from their
3237
+ * `alarm()` / `webSocketClose()` handlers BEFORE letting the
3238
+ * runtime evict so external brokers don't accumulate ghost
3239
+ * subscriptions.
3240
+ *
3241
+ * @param deadline - Optional wall-clock ms deadline. If supplied,
3242
+ * evicts that have not resolved by `deadline` are abandoned (the
3243
+ * isolate will die anyway). Defaults to no deadline.
3244
+ */
3245
+ evict(deadline?: number): Promise<void>;
2680
3246
  readonly isRunning: boolean;
2681
3247
  readonly isSettled: boolean;
2682
3248
  /** Whether all modules have completed initialization */
@@ -3041,6 +3607,39 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3041
3607
  * @param error - The error that was thrown
3042
3608
  */
3043
3609
  onEffectError?: (id: string, error: unknown) => void;
3610
+ /**
3611
+ * Called when a typed external event source attaches at `system.start()`
3612
+ * (or when a dynamically registered module brings new sources to an
3613
+ * already-running system).
3614
+ * @param id - The source ID (key on the module's `sources:` map).
3615
+ * @param moduleId - The owning module's id.
3616
+ */
3617
+ onSourceAttach?: (id: string, moduleId: string) => void;
3618
+ /**
3619
+ * Called when a source publishes an event into the system's event queue.
3620
+ * Fires BEFORE the event handler runs. Pair with `onFactChange` to trace
3621
+ * the downstream state mutation.
3622
+ * @param id - The source ID.
3623
+ * @param moduleId - The owning module's id.
3624
+ * @param eventName - The dispatched event name (the first arg to `publish`).
3625
+ */
3626
+ onSourcePublish?: (id: string, moduleId: string, eventName: string) => void;
3627
+ /**
3628
+ * Called when a source detaches at `system.stop()` (reverse-registration
3629
+ * order across all modules).
3630
+ * @param id - The source ID.
3631
+ * @param moduleId - The owning module's id.
3632
+ */
3633
+ onSourceDetach?: (id: string, moduleId: string) => void;
3634
+ /**
3635
+ * Called when a source's `attach` or unsubscribe throws. Errors are
3636
+ * isolated (one bad source never blocks others) but observable here.
3637
+ * @param id - The source ID.
3638
+ * @param moduleId - The owning module's id.
3639
+ * @param phase - Which lifecycle stage threw.
3640
+ * @param error - The thrown value (normalized to Error inside the manager).
3641
+ */
3642
+ onSourceError?: (id: string, moduleId: string, phase: "attach" | "cleanup" | "runtime", error: unknown) => void;
3044
3643
  /**
3045
3644
  * Called when a history snapshot is taken.
3046
3645
  * @param snapshot - The snapshot that was captured
@@ -3099,4 +3698,4 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3099
3698
  onTraceComplete?: (entry: TraceEntry) => void;
3100
3699
  }
3101
3700
 
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 };
3701
+ 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 };