@nisli/core 0.55.0 → 0.57.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 (114) hide show
  1. package/README.md +58 -1
  2. package/dist/bundler/component.js +409 -0
  3. package/dist/bundler/component.js.map +7 -0
  4. package/dist/bundler/context.js +27 -0
  5. package/dist/bundler/context.js.map +7 -0
  6. package/dist/bundler/diagnostics.js +55 -0
  7. package/dist/bundler/diagnostics.js.map +7 -0
  8. package/dist/bundler/disposable.js +9 -0
  9. package/dist/bundler/disposable.js.map +7 -0
  10. package/dist/bundler/element-context.js +47 -0
  11. package/dist/bundler/element-context.js.map +7 -0
  12. package/dist/bundler/emitter.js +70 -0
  13. package/dist/bundler/emitter.js.map +7 -0
  14. package/dist/bundler/hmr/registry.js +72 -0
  15. package/dist/bundler/hmr/registry.js.map +7 -0
  16. package/dist/bundler/hmr/transform.js +30 -0
  17. package/dist/bundler/hmr/transform.js.map +7 -0
  18. package/dist/bundler/index.js +103 -0
  19. package/dist/bundler/index.js.map +7 -0
  20. package/dist/bundler/injector.js +75 -0
  21. package/dist/bundler/injector.js.map +7 -0
  22. package/dist/bundler/lifecycle.js +56 -0
  23. package/dist/bundler/lifecycle.js.map +7 -0
  24. package/dist/bundler/projection.js +92 -0
  25. package/dist/bundler/projection.js.map +7 -0
  26. package/dist/bundler/query.js +302 -0
  27. package/dist/bundler/query.js.map +7 -0
  28. package/dist/bundler/ref.js +16 -0
  29. package/dist/bundler/ref.js.map +7 -0
  30. package/dist/bundler/resource.js +99 -0
  31. package/dist/bundler/resource.js.map +7 -0
  32. package/dist/bundler/settle.js +43 -0
  33. package/dist/bundler/settle.js.map +7 -0
  34. package/dist/bundler/signal.js +403 -0
  35. package/dist/bundler/signal.js.map +7 -0
  36. package/dist/bundler/template-audit.js +162 -0
  37. package/dist/bundler/template-audit.js.map +7 -0
  38. package/dist/bundler/template.js +847 -0
  39. package/dist/bundler/template.js.map +7 -0
  40. package/dist/bundler/view-transition.js +20 -0
  41. package/dist/bundler/view-transition.js.map +7 -0
  42. package/dist/bundler/vite-hmr/plugin.js +32 -0
  43. package/dist/bundler/vite-hmr/plugin.js.map +7 -0
  44. package/dist/bundler/vite-hmr/runtime.js +8 -0
  45. package/dist/bundler/vite-hmr/runtime.js.map +7 -0
  46. package/dist/index.js +2607 -44
  47. package/dist/index.js.map +7 -1
  48. package/dist/index.production.js +2 -0
  49. package/dist/index.production.js.map +6 -0
  50. package/dist/{diagnostics.d.ts → types/diagnostics.d.ts} +8 -15
  51. package/dist/{index.d.ts → types/index.d.ts} +1 -0
  52. package/dist/{template-audit.d.ts → types/template-audit.d.ts} +4 -5
  53. package/dist/{template.d.ts → types/template.d.ts} +3 -15
  54. package/package.json +12 -9
  55. package/dist/component.js +0 -633
  56. package/dist/component.js.map +0 -1
  57. package/dist/context.js +0 -55
  58. package/dist/context.js.map +0 -1
  59. package/dist/diagnostics.js +0 -118
  60. package/dist/diagnostics.js.map +0 -1
  61. package/dist/disposable.js +0 -8
  62. package/dist/disposable.js.map +0 -1
  63. package/dist/element-context.js +0 -108
  64. package/dist/element-context.js.map +0 -1
  65. package/dist/emitter.js +0 -91
  66. package/dist/emitter.js.map +0 -1
  67. package/dist/hmr/registry.js +0 -136
  68. package/dist/hmr/registry.js.map +0 -1
  69. package/dist/hmr/transform.js +0 -52
  70. package/dist/hmr/transform.js.map +0 -1
  71. package/dist/injector.js +0 -118
  72. package/dist/injector.js.map +0 -1
  73. package/dist/lifecycle.js +0 -117
  74. package/dist/lifecycle.js.map +0 -1
  75. package/dist/projection.js +0 -179
  76. package/dist/projection.js.map +0 -1
  77. package/dist/query.js +0 -429
  78. package/dist/query.js.map +0 -1
  79. package/dist/ref.js +0 -43
  80. package/dist/ref.js.map +0 -1
  81. package/dist/resource.js +0 -129
  82. package/dist/resource.js.map +0 -1
  83. package/dist/settle.js +0 -106
  84. package/dist/settle.js.map +0 -1
  85. package/dist/signal.js +0 -645
  86. package/dist/signal.js.map +0 -1
  87. package/dist/template-audit.js +0 -246
  88. package/dist/template-audit.js.map +0 -1
  89. package/dist/template.js +0 -1218
  90. package/dist/template.js.map +0 -1
  91. package/dist/view-transition.js +0 -98
  92. package/dist/view-transition.js.map +0 -1
  93. package/dist/vite-hmr/plugin.js +0 -62
  94. package/dist/vite-hmr/plugin.js.map +0 -1
  95. package/dist/vite-hmr/runtime.js +0 -17
  96. package/dist/vite-hmr/runtime.js.map +0 -1
  97. /package/dist/{component.d.ts → types/component.d.ts} +0 -0
  98. /package/dist/{context.d.ts → types/context.d.ts} +0 -0
  99. /package/dist/{disposable.d.ts → types/disposable.d.ts} +0 -0
  100. /package/dist/{element-context.d.ts → types/element-context.d.ts} +0 -0
  101. /package/dist/{emitter.d.ts → types/emitter.d.ts} +0 -0
  102. /package/dist/{hmr → types/hmr}/registry.d.ts +0 -0
  103. /package/dist/{hmr → types/hmr}/transform.d.ts +0 -0
  104. /package/dist/{injector.d.ts → types/injector.d.ts} +0 -0
  105. /package/dist/{lifecycle.d.ts → types/lifecycle.d.ts} +0 -0
  106. /package/dist/{projection.d.ts → types/projection.d.ts} +0 -0
  107. /package/dist/{query.d.ts → types/query.d.ts} +0 -0
  108. /package/dist/{ref.d.ts → types/ref.d.ts} +0 -0
  109. /package/dist/{resource.d.ts → types/resource.d.ts} +0 -0
  110. /package/dist/{settle.d.ts → types/settle.d.ts} +0 -0
  111. /package/dist/{signal.d.ts → types/signal.d.ts} +0 -0
  112. /package/dist/{view-transition.d.ts → types/view-transition.d.ts} +0 -0
  113. /package/dist/{vite-hmr → types/vite-hmr}/plugin.d.ts +0 -0
  114. /package/dist/{vite-hmr → types/vite-hmr}/runtime.d.ts +0 -0
@@ -0,0 +1,99 @@
1
+ import { getCurrentComponent, hasContext } from "./context.js";
2
+ import { disposable } from "./disposable.js";
3
+ import { __enrollPending } from "./settle.js";
4
+ import { effect, signal } from "./signal.js";
5
+ function resource(source, loader) {
6
+ const data = signal(void 0);
7
+ const loading = signal(false);
8
+ const error = signal(null);
9
+ const refreshVersion = signal(0);
10
+ let generation = 0;
11
+ let disposed = false;
12
+ let stopEffect = () => {
13
+ };
14
+ const dispose = () => {
15
+ if (disposed) return;
16
+ disposed = true;
17
+ generation++;
18
+ stopEffect();
19
+ loading.value = false;
20
+ };
21
+ if (hasContext()) {
22
+ getCurrentComponent().addDisposer(dispose);
23
+ }
24
+ stopEffect = effect(() => {
25
+ refreshVersion.value;
26
+ const currentGeneration = ++generation;
27
+ let sourceValue;
28
+ try {
29
+ sourceValue = source();
30
+ } catch (cause) {
31
+ loading.value = false;
32
+ error.value = toError(cause);
33
+ return;
34
+ }
35
+ if (sourceValue === void 0) {
36
+ data.value = void 0;
37
+ loading.value = false;
38
+ error.value = null;
39
+ return;
40
+ }
41
+ const controller = new AbortController();
42
+ loading.value = true;
43
+ error.value = null;
44
+ const enabledSource = sourceValue;
45
+ const isStale = () => disposed || controller.signal.aborted || currentGeneration !== generation;
46
+ const settled = __enrollPending();
47
+ void Promise.resolve().then(async () => {
48
+ if (isStale()) {
49
+ settled();
50
+ return;
51
+ }
52
+ try {
53
+ const value = await loader(enabledSource, controller.signal);
54
+ if (isStale()) {
55
+ settled();
56
+ return;
57
+ }
58
+ data.value = value;
59
+ loading.value = false;
60
+ settled();
61
+ } catch (cause) {
62
+ if (isStale()) {
63
+ settled();
64
+ return;
65
+ }
66
+ error.value = toError(cause);
67
+ loading.value = false;
68
+ settled();
69
+ }
70
+ });
71
+ return () => {
72
+ if (currentGeneration === generation) generation++;
73
+ controller.abort();
74
+ settled();
75
+ };
76
+ });
77
+ const result = {
78
+ data,
79
+ loading,
80
+ error,
81
+ refresh() {
82
+ if (!disposed) refreshVersion.value++;
83
+ },
84
+ dispose
85
+ };
86
+ return disposable(result, dispose);
87
+ }
88
+ function toError(cause) {
89
+ if (cause instanceof Error) return cause;
90
+ try {
91
+ return new Error(String(cause));
92
+ } catch {
93
+ return new Error("Unknown error");
94
+ }
95
+ }
96
+ export {
97
+ resource
98
+ };
99
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/resource.ts"],
4
+ "sourcesContent": ["/**\n * resource.ts \u2014 Local async derivations with reactive source tracking.\n *\n * `resource()` is the async counterpart to a local computed value: source\n * dependencies are tracked synchronously, while the loader runs outside the\n * reactive observer and commits only the newest live generation.\n */\n\nimport { getCurrentComponent, hasContext } from './context.js';\nimport { disposable, type DisposableResource } from './disposable.js';\nimport { __enrollPending } from './settle.js';\nimport { effect, signal, type ReadonlySignal } from './signal.js';\n\nexport interface ResourceResult<T> extends DisposableResource {\n /** Latest successful value. Retained while a newer load is pending. */\n readonly data: ReadonlySignal<T | undefined>;\n /** True while the current generation is loading. */\n readonly loading: ReadonlySignal<boolean>;\n /** Error from the current generation, or null. */\n readonly error: ReadonlySignal<Error | null>;\n /** Re-run the loader for the current source value. */\n refresh(): void;\n /** Stop source tracking and invalidate/abort outstanding work. */\n dispose(): void;\n}\n\n/**\n * Derive local async state from a synchronously tracked source.\n *\n * Only signal reads inside `source` are dependencies. `loader` starts in a\n * microtask, outside the active observer, so signal reads before or after its\n * first await never accidentally widen the source set.\n */\nexport function resource<S, T>(\n source: () => S | undefined,\n loader: (source: S, signal: AbortSignal) => PromiseLike<T>,\n): ResourceResult<T> {\n const data = signal<T | undefined>(undefined);\n const loading = signal(false);\n const error = signal<Error | null>(null);\n const refreshVersion = signal(0);\n\n let generation = 0;\n let disposed = false;\n let stopEffect: () => void = () => {};\n\n const dispose = () => {\n if (disposed) return;\n disposed = true;\n generation++;\n stopEffect();\n loading.value = false;\n };\n\n // Register the full resource disposer before effect() registers its own\n // lower-level disposer, so component teardown first invalidates generations\n // and then performs the idempotent raw effect cleanup.\n if (hasContext()) {\n getCurrentComponent().addDisposer(dispose);\n }\n\n stopEffect = effect(() => {\n refreshVersion.value;\n const currentGeneration = ++generation;\n\n let sourceValue: S | undefined;\n try {\n sourceValue = source();\n } catch (cause) {\n loading.value = false;\n error.value = toError(cause);\n return;\n }\n\n // `undefined` is the single disabled sentinel. It covers the markdown\n // consumer's empty-content branch without growing an options surface.\n if (sourceValue === undefined) {\n data.value = undefined;\n loading.value = false;\n error.value = null;\n return;\n }\n\n const controller = new AbortController();\n loading.value = true;\n error.value = null;\n const enabledSource = sourceValue as S;\n const isStale = () => (\n disposed\n || controller.signal.aborted\n || currentGeneration !== generation\n );\n\n // settle() enrollment (ADR 0030.2 T2): this generation is a pending\n // logical request from here until COMMIT (data/error write) or ABORT\n // (the cleanup below) \u2014 never raw loader settlement, so an\n // abort-ignoring loader cannot wedge settle(). Idempotent.\n const settled = __enrollPending();\n\n // The microtask boundary is deliberate: loader signal reads are NOT source\n // dependencies, and synchronous loader throws become normal rejections.\n void Promise.resolve().then(async () => {\n if (isStale()) { settled(); return; }\n\n try {\n const value = await loader(enabledSource, controller.signal);\n if (isStale()) { settled(); return; }\n data.value = value;\n loading.value = false;\n settled();\n } catch (cause) {\n if (isStale()) { settled(); return; }\n error.value = toError(cause);\n loading.value = false;\n settled();\n }\n });\n\n return () => {\n if (currentGeneration === generation) generation++;\n controller.abort();\n settled();\n };\n });\n\n const result = {\n data,\n loading,\n error,\n refresh() {\n if (!disposed) refreshVersion.value++;\n },\n dispose,\n };\n return disposable(result, dispose);\n}\n\nfunction toError(cause: unknown): Error {\n if (cause instanceof Error) return cause;\n\n try {\n return new Error(String(cause));\n } catch {\n return new Error('Unknown error');\n }\n}\n"],
5
+ "mappings": "AAQA,SAAS,qBAAqB,kBAAkB;AAChD,SAAS,kBAA2C;AACpD,SAAS,uBAAuB;AAChC,SAAS,QAAQ,cAAmC;AAsB7C,SAAS,SACd,QACA,QACmB;AACnB,QAAM,OAAO,OAAsB,MAAS;AAC5C,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,QAAQ,OAAqB,IAAI;AACvC,QAAM,iBAAiB,OAAO,CAAC;AAE/B,MAAI,aAAa;AACjB,MAAI,WAAW;AACf,MAAI,aAAyB,MAAM;AAAA,EAAC;AAEpC,QAAM,UAAU,MAAM;AACpB,QAAI,SAAU;AACd,eAAW;AACX;AACA,eAAW;AACX,YAAQ,QAAQ;AAAA,EAClB;AAKA,MAAI,WAAW,GAAG;AAChB,wBAAoB,EAAE,YAAY,OAAO;AAAA,EAC3C;AAEA,eAAa,OAAO,MAAM;AACxB,mBAAe;AACf,UAAM,oBAAoB,EAAE;AAE5B,QAAI;AACJ,QAAI;AACF,oBAAc,OAAO;AAAA,IACvB,SAAS,OAAO;AACd,cAAQ,QAAQ;AAChB,YAAM,QAAQ,QAAQ,KAAK;AAC3B;AAAA,IACF;AAIA,QAAI,gBAAgB,QAAW;AAC7B,WAAK,QAAQ;AACb,cAAQ,QAAQ;AAChB,YAAM,QAAQ;AACd;AAAA,IACF;AAEA,UAAM,aAAa,IAAI,gBAAgB;AACvC,YAAQ,QAAQ;AAChB,UAAM,QAAQ;AACd,UAAM,gBAAgB;AACtB,UAAM,UAAU,MACd,YACG,WAAW,OAAO,WAClB,sBAAsB;AAO3B,UAAM,UAAU,gBAAgB;AAIhC,SAAK,QAAQ,QAAQ,EAAE,KAAK,YAAY;AACtC,UAAI,QAAQ,GAAG;AAAE,gBAAQ;AAAG;AAAA,MAAQ;AAEpC,UAAI;AACF,cAAM,QAAQ,MAAM,OAAO,eAAe,WAAW,MAAM;AAC3D,YAAI,QAAQ,GAAG;AAAE,kBAAQ;AAAG;AAAA,QAAQ;AACpC,aAAK,QAAQ;AACb,gBAAQ,QAAQ;AAChB,gBAAQ;AAAA,MACV,SAAS,OAAO;AACd,YAAI,QAAQ,GAAG;AAAE,kBAAQ;AAAG;AAAA,QAAQ;AACpC,cAAM,QAAQ,QAAQ,KAAK;AAC3B,gBAAQ,QAAQ;AAChB,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,UAAI,sBAAsB,WAAY;AACtC,iBAAW,MAAM;AACjB,cAAQ;AAAA,IACV;AAAA,EACF,CAAC;AAED,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AACR,UAAI,CAAC,SAAU,gBAAe;AAAA,IAChC;AAAA,IACA;AAAA,EACF;AACA,SAAO,WAAW,QAAQ,OAAO;AACnC;AAEA,SAAS,QAAQ,OAAuB;AACtC,MAAI,iBAAiB,MAAO,QAAO;AAEnC,MAAI;AACF,WAAO,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,EAChC,QAAQ;AACN,WAAO,IAAI,MAAM,eAAe;AAAA,EAClC;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,43 @@
1
+ import { tick } from "./signal.js";
2
+ const pending = /* @__PURE__ */ new Set();
3
+ function __enrollPending() {
4
+ let resolve;
5
+ const entry = new Promise((res) => {
6
+ resolve = res;
7
+ });
8
+ pending.add(entry);
9
+ let done = false;
10
+ return () => {
11
+ if (done) return;
12
+ done = true;
13
+ pending.delete(entry);
14
+ resolve();
15
+ };
16
+ }
17
+ function __resetPending() {
18
+ pending.clear();
19
+ }
20
+ function __pendingCount() {
21
+ return pending.size;
22
+ }
23
+ import { diag, isDev } from "./diagnostics.js";
24
+ import { registerResetHook } from "./injector.js";
25
+ registerResetHook(__resetPending);
26
+ async function settle() {
27
+ for (let i = 0; i < 50; i++) {
28
+ await tick();
29
+ if (pending.size === 0) return;
30
+ await Promise.allSettled([...pending]);
31
+ }
32
+ if (process.env.NODE_ENV !== "production" && isDev()) diag(
33
+ "N603",
34
+ "settle() did not reach quiescence within 50 iterations \u2014 something keeps scheduling new async work (a self-invalidating query or a polling loop?). settle() is a test/verify primitive; under live background refetch it is unbounded by design. Returning anyway."
35
+ );
36
+ }
37
+ export {
38
+ __enrollPending,
39
+ __pendingCount,
40
+ __resetPending,
41
+ settle
42
+ };
43
+ //# sourceMappingURL=settle.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/settle.ts"],
4
+ "sourcesContent": ["/**\n * settle.ts \u2014 Awaitable async quiescence (ADR 0030.2 T2).\n *\n * `await settle()` resolves when every framework-started logical async\n * request has TERMINATED and the reactive graph is quiescent. Query runs\n * (query.ts) and resource loads (resource.ts) enroll here when they start\n * and deregister at *termination* \u2014 commit, terminal error after retries,\n * or abort \u2014 never at raw promise settlement. An abort-ignoring fetcher\n * whose run was superseded therefore cannot wedge `settle()`: the run's\n * registry entry closed at the abort even though the underlying promise\n * never resolves.\n *\n * ```ts\n * mount(app);\n * await settle();\n * expect(screen.textContent).toContain('loaded'); // no waitFor polling\n * ```\n *\n * Contract and bounds:\n * - This is a **test/verify primitive** (and the SSG pre-snapshot barrier).\n * Awaiting it in a live app with continuous background refetch/polling is\n * unbounded *by design* \u2014 each commit may legally schedule the next\n * request. A scope argument waits for a second consumer (ADR 0030.2 \u00A78).\n * - Each iteration awaits the pending registry, then `tick()` (signal.ts's\n * bounded microtask-drain contract). Timer-scheduled work is NOT awaited\n * beyond what a pending logical request itself spans.\n * - Iteration-capped at 50 like `tick()`; hitting the cap emits diagnostic\n * N603 and resolves \u2014 never a silent hang, never an unbounded loop.\n *\n * MERGE ITEM: `__resetPending()` should be folded into `resetInjector()`\n * (injector.ts is owned by another worktree); until then test teardown\n * calls it directly.\n */\n\nimport { tick } from './signal.js';\n\n// \u2500\u2500 Pending logical-request registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * One entry per in-flight logical request. The promise is a *termination*\n * promise resolved by the enroller's `done()` callback \u2014 deliberately not\n * the fetcher/loader promise itself (see module doc).\n */\nconst pending = new Set<Promise<void>>();\n\n/**\n * Enroll a logical async request. Returns its `done()` terminator: call it\n * at commit, terminal error, or abort. Idempotent \u2014 a superseded run whose\n * terminator was already invoked by the superseding path may safely call\n * it again from its own settlement branch.\n *\n * @internal \u2014 called by query.ts runs and resource.ts loader generations.\n */\nexport function __enrollPending(): () => void {\n let resolve!: () => void;\n const entry = new Promise<void>((res) => { resolve = res; });\n pending.add(entry);\n let done = false;\n return () => {\n if (done) return;\n done = true;\n pending.delete(entry);\n resolve();\n };\n}\n\n/**\n * Reset the registry. Test-teardown escape hatch: entries orphaned by a\n * test that never terminated its fetchers would otherwise leak into the\n * next test's `settle()`.\n *\n * @internal\n */\nexport function __resetPending(): void {\n pending.clear();\n}\n\n/** Current number of enrolled (unterminated) logical requests. @internal */\nexport function __pendingCount(): number {\n return pending.size;\n}\n\n// \u2500\u2500 Diagnostics + test isolation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Reporting goes through the core diagnostics leaf. The pending registry\n// self-registers with resetInjector() so test teardown clears it without\n// an upside-down import (settle \u2192 injector, never the reverse).\nimport { diag, isDev } from './diagnostics.js';\nimport { registerResetHook } from './injector.js';\n\nregisterResetHook(__resetPending);\n\n// \u2500\u2500 settle() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Await async quiescence: loop `tick()` \u2192 `Promise.allSettled(pending)`\n * until a full `tick()` leaves the pending registry empty.\n *\n * The `tick()` *first* ordering matters: observer effects created since\n * the last flush are what *start* runs (and enroll them) \u2014 the registry\n * can only be trusted empty after the reactive graph has flushed.\n */\nexport async function settle(): Promise<void> {\n for (let i = 0; i < 50; i++) {\n await tick();\n if (pending.size === 0) return;\n await Promise.allSettled([...pending]);\n }\n if (process.env.NODE_ENV !== 'production' && isDev()) diag(\n 'N603',\n 'settle() did not reach quiescence within 50 iterations \u2014 something '\n + 'keeps scheduling new async work (a self-invalidating query or a '\n + 'polling loop?). settle() is a test/verify primitive; under live '\n + 'background refetch it is unbounded by design. Returning anyway.',\n );\n}\n"],
5
+ "mappings": "AAkCA,SAAS,YAAY;AASrB,MAAM,UAAU,oBAAI,IAAmB;AAUhC,SAAS,kBAA8B;AAC5C,MAAI;AACJ,QAAM,QAAQ,IAAI,QAAc,CAAC,QAAQ;AAAE,cAAU;AAAA,EAAK,CAAC;AAC3D,UAAQ,IAAI,KAAK;AACjB,MAAI,OAAO;AACX,SAAO,MAAM;AACX,QAAI,KAAM;AACV,WAAO;AACP,YAAQ,OAAO,KAAK;AACpB,YAAQ;AAAA,EACV;AACF;AASO,SAAS,iBAAuB;AACrC,UAAQ,MAAM;AAChB;AAGO,SAAS,iBAAyB;AACvC,SAAO,QAAQ;AACjB;AAMA,SAAS,MAAM,aAAa;AAC5B,SAAS,yBAAyB;AAElC,kBAAkB,cAAc;AAYhC,eAAsB,SAAwB;AAC5C,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,KAAK;AACX,QAAI,QAAQ,SAAS,EAAG;AACxB,UAAM,QAAQ,WAAW,CAAC,GAAG,OAAO,CAAC;AAAA,EACvC;AACA,MAAI,QAAQ,IAAI,aAAa,gBAAgB,MAAM,EAAG;AAAA,IACpD;AAAA,IACA;AAAA,EAIF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,403 @@
1
+ let contextHook = null;
2
+ function setContextHook(hook) {
3
+ contextHook = hook;
4
+ }
5
+ const SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("backlog.signal");
6
+ let activeObserver = null;
7
+ let runningEffect = null;
8
+ let globalEpoch = 0;
9
+ let nextEffectOrder = 0;
10
+ import { diag as emitDiag, isDev } from "./diagnostics.js";
11
+ import { disposable } from "./disposable.js";
12
+ function diag(code, message, detail) {
13
+ if (!isDev()) return;
14
+ emitDiag(code, message, detail);
15
+ }
16
+ const pendingEffects = /* @__PURE__ */ new Set();
17
+ let flushScheduled = false;
18
+ function scheduleFlush() {
19
+ if (!flushScheduled) {
20
+ flushScheduled = true;
21
+ queueMicrotask(flushPendingEffects);
22
+ }
23
+ }
24
+ function flushPendingEffects() {
25
+ flushScheduled = false;
26
+ const effects = [...pendingEffects].sort((a, b) => a.order - b.order);
27
+ pendingEffects.clear();
28
+ for (const effect2 of effects) {
29
+ if (!effect2.disposed) {
30
+ runEffect(effect2);
31
+ }
32
+ }
33
+ }
34
+ var NodeState = /* @__PURE__ */ ((NodeState2) => {
35
+ NodeState2[NodeState2["Clean"] = 0] = "Clean";
36
+ NodeState2[NodeState2["MaybeDirty"] = 1] = "MaybeDirty";
37
+ NodeState2[NodeState2["Dirty"] = 2] = "Dirty";
38
+ return NodeState2;
39
+ })(NodeState || {});
40
+ const NO_COMPUTED_ERROR = /* @__PURE__ */ Symbol("no-computed-error");
41
+ class SignalImpl {
42
+ [SIGNAL_BRAND] = true;
43
+ /** @internal */
44
+ _node;
45
+ constructor(initialValue) {
46
+ this._node = {
47
+ value: initialValue,
48
+ lastChanged: globalEpoch,
49
+ observers: /* @__PURE__ */ new Set()
50
+ };
51
+ }
52
+ get value() {
53
+ if (activeObserver) {
54
+ activeObserver.sources.add(this._node);
55
+ this._node.observers.add(activeObserver);
56
+ }
57
+ return this._node.value;
58
+ }
59
+ set value(newValue) {
60
+ if (Object.is(this._node.value, newValue)) return;
61
+ if (process.env.NODE_ENV !== "production" && isDev() && runningEffect !== null && runningEffect.sources.has(this._node)) {
62
+ diag(
63
+ "N302",
64
+ "Effect wrote to a signal it also reads \u2014 the write re-schedules the effect. If this does not converge to an equal value it is an infinite loop. Derive the value with computed() instead.",
65
+ runningEffect.createdAt ? { effect: runningEffect.createdAt } : void 0
66
+ );
67
+ }
68
+ this._node.value = newValue;
69
+ this._node.lastChanged = ++globalEpoch;
70
+ notifyObservers(this._node.observers, 2 /* Dirty */);
71
+ }
72
+ peek() {
73
+ return this._node.value;
74
+ }
75
+ subscribe(fn) {
76
+ return effect(() => {
77
+ const value = this.value;
78
+ untrack(() => fn(value));
79
+ });
80
+ }
81
+ }
82
+ function notifyObservers(observers, flavor) {
83
+ for (const observer of observers) {
84
+ observer.notify(flavor);
85
+ }
86
+ }
87
+ class ComputedImpl {
88
+ [SIGNAL_BRAND] = true;
89
+ /** @internal */
90
+ _node;
91
+ compute;
92
+ state = 2 /* Dirty */;
93
+ // Start dirty so first read computes
94
+ sources = /* @__PURE__ */ new Set();
95
+ computing = false;
96
+ error = NO_COMPUTED_ERROR;
97
+ /**
98
+ * globalEpoch snapshot at the end of the last update()/validation pass.
99
+ * A MaybeDirty computed recomputes only if some source's lastChanged
100
+ * advanced past this — otherwise it revalidates to Clean for free.
101
+ */
102
+ lastValidated = 0;
103
+ constructor(fn) {
104
+ this.compute = fn;
105
+ this._node = {
106
+ value: void 0,
107
+ lastChanged: 0,
108
+ observers: /* @__PURE__ */ new Set(),
109
+ pull: () => {
110
+ this.peek();
111
+ }
112
+ };
113
+ }
114
+ get value() {
115
+ if (this.computing) {
116
+ throw new Error("Circular dependency detected in computed()");
117
+ }
118
+ if (activeObserver) {
119
+ activeObserver.sources.add(this._node);
120
+ this._node.observers.add(activeObserver);
121
+ }
122
+ if (this.state !== 0 /* Clean */) {
123
+ this.update();
124
+ }
125
+ if (this.error !== NO_COMPUTED_ERROR) {
126
+ throw this.error;
127
+ }
128
+ return this._node.value;
129
+ }
130
+ peek() {
131
+ if (this.computing) {
132
+ throw new Error("Circular dependency detected in computed()");
133
+ }
134
+ if (this.state !== 0 /* Clean */) {
135
+ this.update();
136
+ }
137
+ if (this.error !== NO_COMPUTED_ERROR) {
138
+ throw this.error;
139
+ }
140
+ return this._node.value;
141
+ }
142
+ /**
143
+ * MaybeDirty validation: pull each source (computeds recompute lazily) and
144
+ * report whether any source's change-epoch advanced past our last
145
+ * validation. A pulled source may throw — its error-state transition
146
+ * already bumped its change-epoch, so the epoch comparison decides.
147
+ */
148
+ sourcesChanged() {
149
+ for (const source of this.sources) {
150
+ if ("pull" in source) {
151
+ try {
152
+ source.pull();
153
+ } catch (_) {
154
+ }
155
+ }
156
+ if (source.lastChanged > this.lastValidated) return true;
157
+ }
158
+ return false;
159
+ }
160
+ update() {
161
+ if (this.state === 1 /* MaybeDirty */ && !this.sourcesChanged()) {
162
+ this.state = 0 /* Clean */;
163
+ this.lastValidated = globalEpoch;
164
+ return;
165
+ }
166
+ for (const source of this.sources) {
167
+ source.observers.delete(this);
168
+ }
169
+ this.sources.clear();
170
+ const prevObserver = activeObserver;
171
+ activeObserver = this;
172
+ this.computing = true;
173
+ const wasError = this.error !== NO_COMPUTED_ERROR;
174
+ try {
175
+ const newValue = this.compute();
176
+ this.error = NO_COMPUTED_ERROR;
177
+ if (!Object.is(this._node.value, newValue)) {
178
+ this._node.value = newValue;
179
+ this._node.lastChanged = ++globalEpoch;
180
+ } else if (wasError) {
181
+ this._node.lastChanged = ++globalEpoch;
182
+ }
183
+ } catch (error) {
184
+ this.error = error;
185
+ if (!wasError) {
186
+ this._node.lastChanged = ++globalEpoch;
187
+ }
188
+ throw error;
189
+ } finally {
190
+ this.computing = false;
191
+ activeObserver = prevObserver;
192
+ this.state = 0 /* Clean */;
193
+ this.lastValidated = globalEpoch;
194
+ }
195
+ }
196
+ /** @internal — called by the ReactiveNode interface when a source changes */
197
+ notify(flavor) {
198
+ if (flavor > this.state) {
199
+ const wasClean = this.state === 0 /* Clean */;
200
+ this.state = flavor;
201
+ if (wasClean) {
202
+ notifyObservers(this._node.observers, 1 /* MaybeDirty */);
203
+ }
204
+ }
205
+ }
206
+ subscribe(fn) {
207
+ return effect(() => {
208
+ const value = this.value;
209
+ untrack(() => fn(value));
210
+ });
211
+ }
212
+ // ReactiveNode interface — used by dependency tracking
213
+ get sources_() {
214
+ return this.sources;
215
+ }
216
+ }
217
+ Object.defineProperty(ComputedImpl.prototype, "sources", {
218
+ enumerable: false
219
+ });
220
+ const MAX_EFFECT_RERUNS = 100;
221
+ function createEffectNode(fn) {
222
+ return {
223
+ state: 2 /* Dirty */,
224
+ fn,
225
+ cleanup: null,
226
+ disposed: false,
227
+ sources: /* @__PURE__ */ new Set(),
228
+ disposers: [],
229
+ order: nextEffectOrder++,
230
+ lastRunEpoch: 0,
231
+ selfReschedules: 0,
232
+ notify(flavor) {
233
+ if (this.disposed) return;
234
+ if (flavor > this.state) this.state = flavor;
235
+ pendingEffects.add(this);
236
+ scheduleFlush();
237
+ }
238
+ };
239
+ }
240
+ function effectSourcesChanged(node) {
241
+ for (const source of node.sources) {
242
+ if ("pull" in source) {
243
+ try {
244
+ source.pull();
245
+ } catch (_) {
246
+ }
247
+ }
248
+ if (source.lastChanged > node.lastRunEpoch) return true;
249
+ }
250
+ return false;
251
+ }
252
+ function isThenable(value) {
253
+ return value !== null && typeof value === "object" && typeof value.then === "function";
254
+ }
255
+ function runEffect(node) {
256
+ if (node.disposed) return;
257
+ if (node.state === 1 /* MaybeDirty */ && !effectSourcesChanged(node)) {
258
+ node.state = 0 /* Clean */;
259
+ return;
260
+ }
261
+ if (node.cleanup) {
262
+ try {
263
+ node.cleanup();
264
+ } catch (_) {
265
+ }
266
+ node.cleanup = null;
267
+ }
268
+ for (const source of node.sources) {
269
+ source.observers.delete(node);
270
+ }
271
+ node.sources.clear();
272
+ node.state = 0 /* Clean */;
273
+ const prevObserver = activeObserver;
274
+ const prevRunning = process.env.NODE_ENV !== "production" ? runningEffect : null;
275
+ activeObserver = node;
276
+ if (process.env.NODE_ENV !== "production") runningEffect = node;
277
+ try {
278
+ const result = node.fn();
279
+ if (typeof result === "function") {
280
+ node.cleanup = result;
281
+ } else if (isThenable(result)) {
282
+ if (process.env.NODE_ENV !== "production" && isDev() && !node.asyncWarned) {
283
+ node.asyncWarned = true;
284
+ diag(
285
+ "N310",
286
+ "effect() callback returned a Promise \u2014 effects must be synchronous. Async state belongs in resource(). The returned Promise is not a cleanup function; its rejection is contained by the effect error path.",
287
+ node.createdAt ? { effect: node.createdAt } : void 0
288
+ );
289
+ }
290
+ result.then(void 0, (err) => {
291
+ console.error("Effect error:", err);
292
+ });
293
+ }
294
+ } catch (err) {
295
+ console.error("Effect error:", err);
296
+ } finally {
297
+ activeObserver = prevObserver;
298
+ if (process.env.NODE_ENV !== "production") runningEffect = prevRunning;
299
+ node.lastRunEpoch = globalEpoch;
300
+ }
301
+ if (pendingEffects.has(node)) {
302
+ if (++node.selfReschedules > MAX_EFFECT_RERUNS) {
303
+ if (process.env.NODE_ENV !== "production" && isDev()) diag(
304
+ "N301",
305
+ `Effect exceeded maximum re-run limit (${MAX_EFFECT_RERUNS}). This usually means the effect writes to a signal it reads. The effect has been disposed to prevent a UI freeze.`,
306
+ node.createdAt ? { effect: node.createdAt } : void 0
307
+ );
308
+ node.disposed = true;
309
+ for (const source of node.sources) {
310
+ source.observers.delete(node);
311
+ }
312
+ node.sources.clear();
313
+ pendingEffects.delete(node);
314
+ }
315
+ } else {
316
+ node.selfReschedules = 0;
317
+ }
318
+ }
319
+ function signal(initialValue) {
320
+ return new SignalImpl(initialValue);
321
+ }
322
+ function computed(fn) {
323
+ return new ComputedImpl(fn);
324
+ }
325
+ function effect(fn, ..._rejectAsync) {
326
+ const node = createEffectNode(fn);
327
+ if (process.env.NODE_ENV !== "production" && isDev()) {
328
+ node.createdAt = new Error("effect() created here").stack;
329
+ }
330
+ runEffect(node);
331
+ const dispose = () => {
332
+ node.disposed = true;
333
+ if (node.cleanup) {
334
+ try {
335
+ node.cleanup();
336
+ } catch (_) {
337
+ }
338
+ node.cleanup = null;
339
+ }
340
+ for (const source of node.sources) {
341
+ source.observers.delete(node);
342
+ }
343
+ node.sources.clear();
344
+ pendingEffects.delete(node);
345
+ };
346
+ if (contextHook) {
347
+ const addDisposer = contextHook();
348
+ if (addDisposer) {
349
+ addDisposer(dispose);
350
+ }
351
+ }
352
+ return disposable(dispose, dispose);
353
+ }
354
+ function isSignal(value) {
355
+ return value !== null && typeof value === "object" && SIGNAL_BRAND in value;
356
+ }
357
+ function untrack(fn) {
358
+ const prev = activeObserver;
359
+ activeObserver = null;
360
+ try {
361
+ return fn();
362
+ } finally {
363
+ activeObserver = prev;
364
+ }
365
+ }
366
+ function flush() {
367
+ let guard = 0;
368
+ while (pendingEffects.size > 0) {
369
+ flushPendingEffects();
370
+ if (++guard > 1e5) {
371
+ if (process.env.NODE_ENV !== "production" && isDev()) diag(
372
+ "N303",
373
+ "flush() cascade cap reached (100000 passes) \u2014 effects keep scheduling more effects (a cross-effect write cycle?). Breaking out to avoid a freeze; pending work remains unflushed."
374
+ );
375
+ break;
376
+ }
377
+ }
378
+ }
379
+ async function tick() {
380
+ flush();
381
+ for (let i = 0; i < 50; i++) {
382
+ await new Promise((resolve) => setTimeout(resolve, 0));
383
+ if (pendingEffects.size === 0) return;
384
+ flush();
385
+ }
386
+ if (process.env.NODE_ENV !== "production" && isDev()) diag(
387
+ "N303",
388
+ "tick() iteration cap (50) reached with effects still pending \u2014 something keeps scheduling reactive work every task (a self-perpetuating scheduler?). Returning with work still queued."
389
+ );
390
+ }
391
+ export {
392
+ SIGNAL_BRAND,
393
+ computed,
394
+ effect,
395
+ flush,
396
+ flush as flushEffects,
397
+ isSignal,
398
+ setContextHook,
399
+ signal,
400
+ tick,
401
+ untrack
402
+ };
403
+ //# sourceMappingURL=signal.js.map