@nisli/core 0.56.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.
- package/README.md +38 -6
- package/dist/bundler/component.js +409 -0
- package/dist/bundler/component.js.map +7 -0
- package/dist/bundler/context.js +27 -0
- package/dist/bundler/context.js.map +7 -0
- package/dist/bundler/diagnostics.js +55 -0
- package/dist/bundler/diagnostics.js.map +7 -0
- package/dist/bundler/disposable.js +9 -0
- package/dist/bundler/disposable.js.map +7 -0
- package/dist/bundler/element-context.js +47 -0
- package/dist/bundler/element-context.js.map +7 -0
- package/dist/bundler/emitter.js +70 -0
- package/dist/bundler/emitter.js.map +7 -0
- package/dist/bundler/hmr/registry.js +72 -0
- package/dist/bundler/hmr/registry.js.map +7 -0
- package/dist/bundler/hmr/transform.js +30 -0
- package/dist/bundler/hmr/transform.js.map +7 -0
- package/dist/bundler/index.js +103 -0
- package/dist/bundler/index.js.map +7 -0
- package/dist/bundler/injector.js +75 -0
- package/dist/bundler/injector.js.map +7 -0
- package/dist/bundler/lifecycle.js +56 -0
- package/dist/bundler/lifecycle.js.map +7 -0
- package/dist/bundler/projection.js +92 -0
- package/dist/bundler/projection.js.map +7 -0
- package/dist/bundler/query.js +302 -0
- package/dist/bundler/query.js.map +7 -0
- package/dist/bundler/ref.js +16 -0
- package/dist/bundler/ref.js.map +7 -0
- package/dist/bundler/resource.js +99 -0
- package/dist/bundler/resource.js.map +7 -0
- package/dist/bundler/settle.js +43 -0
- package/dist/bundler/settle.js.map +7 -0
- package/dist/bundler/signal.js +403 -0
- package/dist/bundler/signal.js.map +7 -0
- package/dist/bundler/template-audit.js +162 -0
- package/dist/bundler/template-audit.js.map +7 -0
- package/dist/bundler/template.js +847 -0
- package/dist/bundler/template.js.map +7 -0
- package/dist/bundler/view-transition.js +20 -0
- package/dist/bundler/view-transition.js.map +7 -0
- package/dist/bundler/vite-hmr/plugin.js +32 -0
- package/dist/bundler/vite-hmr/plugin.js.map +7 -0
- package/dist/bundler/vite-hmr/runtime.js +8 -0
- package/dist/bundler/vite-hmr/runtime.js.map +7 -0
- package/dist/index.js +2607 -47
- package/dist/index.js.map +7 -1
- package/dist/index.production.js +2 -0
- package/dist/index.production.js.map +6 -0
- package/dist/{diagnostics.d.ts → types/diagnostics.d.ts} +8 -15
- package/dist/{template-audit.d.ts → types/template-audit.d.ts} +4 -5
- package/package.json +12 -9
- package/dist/component.js +0 -633
- package/dist/component.js.map +0 -1
- package/dist/context.js +0 -55
- package/dist/context.js.map +0 -1
- package/dist/diagnostics.js +0 -118
- package/dist/diagnostics.js.map +0 -1
- package/dist/disposable.js +0 -8
- package/dist/disposable.js.map +0 -1
- package/dist/element-context.js +0 -108
- package/dist/element-context.js.map +0 -1
- package/dist/emitter.js +0 -91
- package/dist/emitter.js.map +0 -1
- package/dist/hmr/registry.js +0 -136
- package/dist/hmr/registry.js.map +0 -1
- package/dist/hmr/transform.js +0 -52
- package/dist/hmr/transform.js.map +0 -1
- package/dist/injector.js +0 -118
- package/dist/injector.js.map +0 -1
- package/dist/lifecycle.js +0 -117
- package/dist/lifecycle.js.map +0 -1
- package/dist/projection.js +0 -179
- package/dist/projection.js.map +0 -1
- package/dist/query.js +0 -429
- package/dist/query.js.map +0 -1
- package/dist/ref.js +0 -43
- package/dist/ref.js.map +0 -1
- package/dist/resource.js +0 -129
- package/dist/resource.js.map +0 -1
- package/dist/settle.js +0 -106
- package/dist/settle.js.map +0 -1
- package/dist/signal.js +0 -645
- package/dist/signal.js.map +0 -1
- package/dist/template-audit.js +0 -246
- package/dist/template-audit.js.map +0 -1
- package/dist/template.js +0 -1244
- package/dist/template.js.map +0 -1
- package/dist/view-transition.js +0 -98
- package/dist/view-transition.js.map +0 -1
- package/dist/vite-hmr/plugin.js +0 -62
- package/dist/vite-hmr/plugin.js.map +0 -1
- package/dist/vite-hmr/runtime.js +0 -17
- package/dist/vite-hmr/runtime.js.map +0 -1
- /package/dist/{component.d.ts → types/component.d.ts} +0 -0
- /package/dist/{context.d.ts → types/context.d.ts} +0 -0
- /package/dist/{disposable.d.ts → types/disposable.d.ts} +0 -0
- /package/dist/{element-context.d.ts → types/element-context.d.ts} +0 -0
- /package/dist/{emitter.d.ts → types/emitter.d.ts} +0 -0
- /package/dist/{hmr → types/hmr}/registry.d.ts +0 -0
- /package/dist/{hmr → types/hmr}/transform.d.ts +0 -0
- /package/dist/{index.d.ts → types/index.d.ts} +0 -0
- /package/dist/{injector.d.ts → types/injector.d.ts} +0 -0
- /package/dist/{lifecycle.d.ts → types/lifecycle.d.ts} +0 -0
- /package/dist/{projection.d.ts → types/projection.d.ts} +0 -0
- /package/dist/{query.d.ts → types/query.d.ts} +0 -0
- /package/dist/{ref.d.ts → types/ref.d.ts} +0 -0
- /package/dist/{resource.d.ts → types/resource.d.ts} +0 -0
- /package/dist/{settle.d.ts → types/settle.d.ts} +0 -0
- /package/dist/{signal.d.ts → types/signal.d.ts} +0 -0
- /package/dist/{template.d.ts → types/template.d.ts} +0 -0
- /package/dist/{view-transition.d.ts → types/view-transition.d.ts} +0 -0
- /package/dist/{vite-hmr → types/vite-hmr}/plugin.d.ts +0 -0
- /package/dist/{vite-hmr → types/vite-hmr}/runtime.d.ts +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { hasContext, getCurrentComponent } from "./context.js";
|
|
2
|
+
const mountCallbacks = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function onMount(callback) {
|
|
4
|
+
if (!hasContext()) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"onMount() called outside setup(). It can only be called during component initialization."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
const comp = getCurrentComponent();
|
|
10
|
+
let callbacks = mountCallbacks.get(comp);
|
|
11
|
+
if (!callbacks) {
|
|
12
|
+
callbacks = [];
|
|
13
|
+
mountCallbacks.set(comp, callbacks);
|
|
14
|
+
}
|
|
15
|
+
callbacks.push(callback);
|
|
16
|
+
}
|
|
17
|
+
function onCleanup(callback) {
|
|
18
|
+
if (!hasContext()) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"onCleanup() called outside setup(). It can only be called during component initialization."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
getCurrentComponent().addDisposer(callback);
|
|
24
|
+
}
|
|
25
|
+
function useHostEvent(target, eventName, handler) {
|
|
26
|
+
if (!hasContext()) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"useHostEvent() called outside setup(). It can only be called during component initialization."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
target.addEventListener(eventName, handler);
|
|
32
|
+
getCurrentComponent().addDisposer(() => {
|
|
33
|
+
target.removeEventListener(eventName, handler);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function runMountCallbacks(comp) {
|
|
37
|
+
const callbacks = mountCallbacks.get(comp);
|
|
38
|
+
if (!callbacks) return;
|
|
39
|
+
try {
|
|
40
|
+
for (const cb of callbacks) {
|
|
41
|
+
const cleanup = cb();
|
|
42
|
+
if (typeof cleanup === "function") {
|
|
43
|
+
comp.addDisposer(cleanup);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
mountCallbacks.delete(comp);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
onCleanup,
|
|
52
|
+
onMount,
|
|
53
|
+
runMountCallbacks,
|
|
54
|
+
useHostEvent
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lifecycle.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * lifecycle.ts \u2014 Post-mount and cleanup lifecycle hooks\n *\n * `onMount()` registers a callback that runs after the component's\n * template is mounted to the DOM. If the callback returns a function,\n * that function runs on disconnect (cleanup).\n *\n * `onCleanup()` registers a standalone cleanup callback that runs\n * on disconnect \u2014 for cases where you need cleanup without mount logic.\n *\n * Both must be called synchronously during component setup.\n *\n * ```ts\n * component('my-el', (props, host) => {\n * onMount(() => {\n * host.querySelector('input')?.focus();\n * return () => console.log('unmounted');\n * });\n *\n * onCleanup(() => clearInterval(timerId));\n *\n * return html`<input />`;\n * });\n * ```\n */\n\nimport { hasContext, getCurrentComponent } from './context.js';\n\n// \u2500\u2500 Mount callback storage \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/** Mount callbacks are stored on the component host and called after template mount. */\nconst mountCallbacks = new WeakMap<object, (() => void | (() => void))[]>();\n\n/**\n * Register a callback to run after the component's template is mounted.\n * If the callback returns a function, it will be called on disconnect.\n *\n * Must be called synchronously during component setup.\n */\nexport function onMount(callback: () => void | (() => void)): void {\n if (!hasContext()) {\n throw new Error(\n 'onMount() called outside setup(). ' +\n 'It can only be called during component initialization.'\n );\n }\n const comp = getCurrentComponent();\n let callbacks = mountCallbacks.get(comp);\n if (!callbacks) {\n callbacks = [];\n mountCallbacks.set(comp, callbacks);\n }\n callbacks.push(callback);\n}\n\n/**\n * Register a cleanup callback to run on disconnect.\n * This is a convenience for cases where you need cleanup without mount logic.\n *\n * Must be called synchronously during component setup.\n */\nexport function onCleanup(callback: () => void): void {\n if (!hasContext()) {\n throw new Error(\n 'onCleanup() called outside setup(). ' +\n 'It can only be called during component initialization.'\n );\n }\n getCurrentComponent().addDisposer(callback);\n}\n\n/**\n * Listen for a DOM event on a target and auto-dispose the listener when the\n * component disconnects. Works with any EventTarget (host element, document,\n * window, etc.). Must be called during component setup.\n *\n * ```ts\n * useHostEvent(host, 'click', (e) => {\n * // Runs on each click\n * });\n * ```\n */\nexport function useHostEvent<E extends Event = Event>(\n target: EventTarget,\n eventName: string,\n handler: (event: E) => void,\n): void {\n if (!hasContext()) {\n throw new Error(\n 'useHostEvent() called outside setup(). ' +\n 'It can only be called during component initialization.'\n );\n }\n target.addEventListener(eventName, handler as EventListener);\n getCurrentComponent().addDisposer(() => {\n target.removeEventListener(eventName, handler as EventListener);\n });\n}\n\n/**\n * Run all registered mount callbacks for a component host.\n * Called by component.ts after the template is mounted to the DOM.\n *\n * A throw here PROPAGATES to component.ts's containment boundary \u2014 the SAME\n * boundary as a setup throw (ADR 0030.2 T6, code N402): remaining callbacks\n * are skipped, the partial scope (including cleanups already registered by\n * earlier callbacks) is disposed, the error fallback renders, and the host is\n * stamped `data-nisli-error`. The pre-T6 behavior (console-and-limp per\n * callback) left a half-mounted component alive with no machine-readable\n * trace and no teardown \u2014 the one containment semantics setup already had\n * (issue 0010) now covers mount too.\n * @internal\n */\nexport function runMountCallbacks(comp: object): void {\n const callbacks = mountCallbacks.get(comp);\n if (!callbacks) return;\n\n try {\n for (const cb of callbacks) {\n const cleanup = cb();\n if (typeof cleanup === 'function') {\n // Register cleanup as a disposer on the component\n (comp as any).addDisposer(cleanup);\n }\n }\n } finally {\n // Clear \u2014 mount callbacks run once (also when a throw contains the scope:\n // the boundary re-runs setup from scratch on the next mount, which\n // re-registers a fresh callback list).\n mountCallbacks.delete(comp);\n }\n}\n"],
|
|
5
|
+
"mappings": "AA0BA,SAAS,YAAY,2BAA2B;AAKhD,MAAM,iBAAiB,oBAAI,QAA+C;AAQnE,SAAS,QAAQ,UAA2C;AACjE,MAAI,CAAC,WAAW,GAAG;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,OAAO,oBAAoB;AACjC,MAAI,YAAY,eAAe,IAAI,IAAI;AACvC,MAAI,CAAC,WAAW;AACd,gBAAY,CAAC;AACb,mBAAe,IAAI,MAAM,SAAS;AAAA,EACpC;AACA,YAAU,KAAK,QAAQ;AACzB;AAQO,SAAS,UAAU,UAA4B;AACpD,MAAI,CAAC,WAAW,GAAG;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,sBAAoB,EAAE,YAAY,QAAQ;AAC5C;AAaO,SAAS,aACd,QACA,WACA,SACM;AACN,MAAI,CAAC,WAAW,GAAG;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO,iBAAiB,WAAW,OAAwB;AAC3D,sBAAoB,EAAE,YAAY,MAAM;AACtC,WAAO,oBAAoB,WAAW,OAAwB;AAAA,EAChE,CAAC;AACH;AAgBO,SAAS,kBAAkB,MAAoB;AACpD,QAAM,YAAY,eAAe,IAAI,IAAI;AACzC,MAAI,CAAC,UAAW;AAEhB,MAAI;AACF,eAAW,MAAM,WAAW;AAC1B,YAAM,UAAU,GAAG;AACnB,UAAI,OAAO,YAAY,YAAY;AAEjC,QAAC,KAAa,YAAY,OAAO;AAAA,MACnC;AAAA,IACF;AAAA,EACF,UAAE;AAIA,mBAAe,OAAO,IAAI;AAAA,EAC5B;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getCurrentComponent } from "./context.js";
|
|
2
|
+
import { onCleanup } from "./lifecycle.js";
|
|
3
|
+
import { signal, computed, flush } from "./signal.js";
|
|
4
|
+
function isMeaningful(nodes) {
|
|
5
|
+
return nodes.some(
|
|
6
|
+
(n) => n.nodeType === 1 || n.nodeType === 3 && (n.textContent ?? "").trim() !== ""
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
function nodesTemplate(nodes) {
|
|
10
|
+
return {
|
|
11
|
+
__templateResult: true,
|
|
12
|
+
mount(w) {
|
|
13
|
+
for (const node of nodes) w.appendChild(node);
|
|
14
|
+
},
|
|
15
|
+
dispose() {
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function textTemplate(text) {
|
|
20
|
+
return {
|
|
21
|
+
__templateResult: true,
|
|
22
|
+
mount(w) {
|
|
23
|
+
w.appendChild(document.createTextNode(text));
|
|
24
|
+
},
|
|
25
|
+
dispose() {
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function asSlot(value) {
|
|
30
|
+
if (value == null || value === "") return null;
|
|
31
|
+
if (typeof value === "object" && ("__templateResult" in value || "__type" in value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) return value;
|
|
35
|
+
return textTemplate(String(value));
|
|
36
|
+
}
|
|
37
|
+
function children(fallback) {
|
|
38
|
+
const host = getCurrentComponent().element;
|
|
39
|
+
const captured = host.__capturedChildren ?? [];
|
|
40
|
+
host.__capturedChildren = void 0;
|
|
41
|
+
for (const node of captured) {
|
|
42
|
+
if (node.parentNode === host) host.removeChild(node);
|
|
43
|
+
}
|
|
44
|
+
const childrenProp = host._propSignal ? host._propSignal("children") : void 0;
|
|
45
|
+
const projectedNodes = signal(captured);
|
|
46
|
+
let disposed = false;
|
|
47
|
+
onCleanup(() => {
|
|
48
|
+
disposed = true;
|
|
49
|
+
const nodes = projectedNodes.value;
|
|
50
|
+
host.__capturedChildren = nodes.length ? nodes : void 0;
|
|
51
|
+
});
|
|
52
|
+
host.__nisliProjectionSweep = () => {
|
|
53
|
+
const renderedRoots = new Set(Array.from(host.childNodes));
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
if (disposed) return;
|
|
56
|
+
const late = Array.from(host.childNodes).filter((n) => !renderedRoots.has(n));
|
|
57
|
+
if (late.length && isMeaningful(late)) {
|
|
58
|
+
for (const node of late) host.removeChild(node);
|
|
59
|
+
projectedNodes.value = [...projectedNodes.value, ...late];
|
|
60
|
+
flush();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
let fallbackSlot = null;
|
|
65
|
+
if (fallback != null) {
|
|
66
|
+
if (typeof fallback === "string") {
|
|
67
|
+
fallbackSlot = textTemplate(fallback);
|
|
68
|
+
} else {
|
|
69
|
+
const holder = document.createDocumentFragment();
|
|
70
|
+
fallback.mount(holder);
|
|
71
|
+
const fallbackNodes = Array.from(holder.childNodes);
|
|
72
|
+
onCleanup(() => {
|
|
73
|
+
try {
|
|
74
|
+
fallback.dispose();
|
|
75
|
+
} catch (_) {
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
fallbackSlot = nodesTemplate(fallbackNodes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return computed(() => {
|
|
82
|
+
const kids = childrenProp?.value;
|
|
83
|
+
if (kids != null && kids !== "") return asSlot(kids);
|
|
84
|
+
const nodes = projectedNodes.value;
|
|
85
|
+
if (isMeaningful(nodes)) return nodesTemplate(nodes);
|
|
86
|
+
return fallbackSlot;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
children
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/projection.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * projection.ts \u2014 First-class content projection (ADR 0025 item 1)\n *\n * `children(fallback?)` is a setup-context primitive that owns the light-DOM\n * projection machinery the registry currently hand-rolls\n * (captureChildren/projectChildren + per-component conditional-default swaps).\n *\n * ```ts\n * component('ui-button', (props, host) => {\n * transparentHost(host);\n * return html`<button class=\"${classes}\">${children()}</button>`;\n * });\n * ```\n *\n * Semantics:\n * - Returns a reactive slot value (a signal) that a template interpolates. The\n * slot resolves, in priority order: the factory `children` prop \u2192 the host's\n * captured light-DOM children \u2192 the fallback.\n * - Factory `children` route through the SAME slot: `children()` READS the\n * host's `children` prop signal (sugar over one path). Crucially it renders\n * them AT the slot position (not by relocating them to light DOM), so a\n * factory-composed child element is a DOM descendant of its parent when its\n * connectedCallback runs \u2014 `closest()`-based parent lookup keeps working.\n * - Plain-HTML light-DOM children are captured pre-setup by component.ts\n * (SEED \u2192 CAPTURE \u2192 CONTEXT) and projected here; late parser children are\n * swept in a microtask (the ADR 0023 streaming machinery, now in core).\n * - `children(fallback)` renders `fallback` when no MEANINGFUL children exist\n * (whitespace-only light DOM does not count) and REPLACES it reactively when\n * children arrive \u2014 including late parser children. This natively solves the\n * conditional-default class (message-scroller-button's default glyph).\n * - Single default slot only; named/multiple slots are a later design (v1 OUT).\n *\n * Zero-cost when unused: component.ts only snapshots childNodes when the host\n * has any, and only children() consumes them.\n */\n\nimport { getCurrentComponent } from './context.js';\nimport { onCleanup } from './lifecycle.js';\nimport { signal, computed, flush, type ReadonlySignal, type Signal } from './signal.js';\nimport type { TemplateResult } from './template.js';\n\n/** True if the nodes carry meaningful content (element or non-blank text). */\nfunction isMeaningful(nodes: Node[]): boolean {\n return nodes.some(\n (n) => n.nodeType === 1 || (n.nodeType === 3 && (n.textContent ?? '').trim() !== ''),\n );\n}\n\n/** A minimal TemplateResult that mounts pre-existing DOM nodes into the slot. */\nfunction nodesTemplate(nodes: Node[]): TemplateResult {\n return {\n __templateResult: true,\n mount(w: HTMLElement) {\n for (const node of nodes) w.appendChild(node);\n },\n dispose() { /* nodes are owned by the host tree; nothing to tear down */ },\n } as unknown as TemplateResult;\n}\n\n/** A re-mountable TemplateResult that renders a primitive as a fresh text node. */\nfunction textTemplate(text: string): TemplateResult {\n return {\n __templateResult: true,\n mount(w: HTMLElement) {\n w.appendChild(document.createTextNode(text));\n },\n dispose() { /* text node removed by the slot */ },\n } as unknown as TemplateResult;\n}\n\n/**\n * Normalize a slot value so the template ALWAYS sees a TemplateResult / factory\n * / array / null \u2014 never a bare primitive. Projection values may be detached\n * and reinserted as fallback/light-DOM ownership changes, so wrapping primitive\n * text keeps that projection value explicitly re-mountable.\n */\nfunction asSlot(value: unknown): unknown {\n if (value == null || value === '') return null;\n if (\n typeof value === 'object'\n && ('__templateResult' in (value as object) || '__type' in (value as object))\n ) {\n return value;\n }\n if (Array.isArray(value)) return value;\n return textTemplate(String(value));\n}\n\n/**\n * Project the host's children (factory `children` prop or light DOM) into this\n * slot, with an optional `fallback` rendered when empty. Must be called in\n * setup context. Interpolate the result in a template:\n * `html`<button>${children()}</button>``.\n */\nexport function children(fallback?: TemplateResult | string): ReadonlySignal<unknown> {\n const host = getCurrentComponent().element as HTMLElement & {\n __capturedChildren?: Node[];\n __nisliProjectionSweep?: () => void;\n _propSignal?(key: string): Signal<unknown>;\n };\n\n // Consume the pre-setup light-DOM capture; remove those nodes from the host\n // so the rendered template doesn't duplicate them.\n const captured = host.__capturedChildren ?? [];\n host.__capturedChildren = undefined;\n for (const node of captured) {\n if (node.parentNode === host) host.removeChild(node);\n }\n\n // The factory `children` prop, folded into the same slot (sugar over one path).\n const childrenProp = host._propSignal ? host._propSignal('children') : undefined;\n\n // Projected light-DOM nodes; grows when the late-parser sweep finds more.\n const projectedNodes = signal<Node[]>(captured);\n\n // True once this setup scope has been disposed (true disconnect, HMR\n // teardown, OR the containment boundary tearing down a failed mount). The\n // queued sweep below must then no-op: it would otherwise misread the\n // post-teardown host children (restored light DOM, or the error fallback)\n // as \"late\" and relocate them into a dead slot.\n let disposed = false;\n\n // A true disconnect/HMR teardown clears the component-owned rendered tree\n // before setup runs again. Hand the plain light-DOM nodes back to component.ts\n // so it can restore them as direct host children for the next capture.\n onCleanup(() => {\n disposed = true;\n const nodes = projectedNodes.value;\n host.__capturedChildren = nodes.length ? nodes : undefined;\n });\n\n // LATE-CHILDREN SWEEP \u2014 scheduled by component.ts's dedicated projection\n // phase (ADR 0030.2 \u00A73), which runs after the template is committed and\n // BEFORE any onMount callback. Pre-0030.2 this was registered via\n // children()'s own onMount, so the rendered-roots snapshot depended on\n // where children() sat relative to other onMount() registrations (the 0025\n // batch-3B ordering trap). Hoisting the SCHEDULING makes the rule\n // universal: the snapshot is exactly the rendered template roots, and\n // anything appearing after it \u2014 late parser/innerHTML children AND onMount\n // host-appends \u2014 classifies as projected content. Only scheduling moved;\n // the prop READ path (the computed below) is unchanged.\n host.__nisliProjectionSweep = () => {\n // The host's direct children at phase time are the rendered template\n // roots. Anything that appears later is late projected content.\n const renderedRoots = new Set<Node>(Array.from(host.childNodes));\n queueMicrotask(() => {\n if (disposed) return; // scope torn down before the sweep ran\n const late = Array.from(host.childNodes).filter((n) => !renderedRoots.has(n));\n if (late.length && isMeaningful(late)) {\n for (const node of late) host.removeChild(node);\n projectedNodes.value = [...projectedNodes.value, ...late];\n // Flush the slot effect NOW so the late children land in this same\n // microtask \u2014 the projectChildren() this replaces mutated the DOM\n // imperatively, so callers only await one microtask for the sweep.\n flush();\n }\n });\n };\n\n // Stable, RE-MOUNTABLE fallback slot: the same slot object is returned on\n // every empty render, and it can be mounted \u2192 unmounted \u2192 mounted again\n // (empty \u2192 filled \u2192 empty) without crashing. A string wraps to a fresh text\n // node each mount. A TemplateResult is mounted ONCE here into a detached\n // holder (its reactive bindings run in this component's context and dispose\n // with it); its rendered nodes are then exposed as a re-mountable node slot,\n // so the engine can detach and re-append them freely.\n let fallbackSlot: unknown = null;\n if (fallback != null) {\n if (typeof fallback === 'string') {\n fallbackSlot = textTemplate(fallback);\n } else {\n const holder = document.createDocumentFragment();\n fallback.mount(holder as unknown as HTMLElement);\n const fallbackNodes = Array.from(holder.childNodes);\n onCleanup(() => { try { fallback.dispose(); } catch (_) { /* ignore */ } });\n fallbackSlot = nodesTemplate(fallbackNodes);\n }\n }\n\n // Priority: factory children \u2192 captured/late light DOM \u2192 fallback. Reactive,\n // so late children (or a changing `children` prop) replace the fallback.\n return computed(() => {\n const kids = childrenProp?.value;\n if (kids != null && kids !== '') return asSlot(kids);\n const nodes = projectedNodes.value;\n if (isMeaningful(nodes)) return nodesTemplate(nodes);\n return fallbackSlot;\n });\n}\n"],
|
|
5
|
+
"mappings": "AAoCA,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAC1B,SAAS,QAAQ,UAAU,aAA+C;AAI1E,SAAS,aAAa,OAAwB;AAC5C,SAAO,MAAM;AAAA,IACX,CAAC,MAAM,EAAE,aAAa,KAAM,EAAE,aAAa,MAAM,EAAE,eAAe,IAAI,KAAK,MAAM;AAAA,EACnF;AACF;AAGA,SAAS,cAAc,OAA+B;AACpD,SAAO;AAAA,IACL,kBAAkB;AAAA,IAClB,MAAM,GAAgB;AACpB,iBAAW,QAAQ,MAAO,GAAE,YAAY,IAAI;AAAA,IAC9C;AAAA,IACA,UAAU;AAAA,IAA+D;AAAA,EAC3E;AACF;AAGA,SAAS,aAAa,MAA8B;AAClD,SAAO;AAAA,IACL,kBAAkB;AAAA,IAClB,MAAM,GAAgB;AACpB,QAAE,YAAY,SAAS,eAAe,IAAI,CAAC;AAAA,IAC7C;AAAA,IACA,UAAU;AAAA,IAAsC;AAAA,EAClD;AACF;AAQA,SAAS,OAAO,OAAyB;AACvC,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAC1C,MACE,OAAO,UAAU,aACb,sBAAuB,SAAoB,YAAa,QAC5D;AACA,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,SAAO,aAAa,OAAO,KAAK,CAAC;AACnC;AAQO,SAAS,SAAS,UAA6D;AACpF,QAAM,OAAO,oBAAoB,EAAE;AAQnC,QAAM,WAAW,KAAK,sBAAsB,CAAC;AAC7C,OAAK,qBAAqB;AAC1B,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,eAAe,KAAM,MAAK,YAAY,IAAI;AAAA,EACrD;AAGA,QAAM,eAAe,KAAK,cAAc,KAAK,YAAY,UAAU,IAAI;AAGvE,QAAM,iBAAiB,OAAe,QAAQ;AAO9C,MAAI,WAAW;AAKf,YAAU,MAAM;AACd,eAAW;AACX,UAAM,QAAQ,eAAe;AAC7B,SAAK,qBAAqB,MAAM,SAAS,QAAQ;AAAA,EACnD,CAAC;AAYD,OAAK,yBAAyB,MAAM;AAGlC,UAAM,gBAAgB,IAAI,IAAU,MAAM,KAAK,KAAK,UAAU,CAAC;AAC/D,mBAAe,MAAM;AACnB,UAAI,SAAU;AACd,YAAM,OAAO,MAAM,KAAK,KAAK,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC;AAC5E,UAAI,KAAK,UAAU,aAAa,IAAI,GAAG;AACrC,mBAAW,QAAQ,KAAM,MAAK,YAAY,IAAI;AAC9C,uBAAe,QAAQ,CAAC,GAAG,eAAe,OAAO,GAAG,IAAI;AAIxD,cAAM;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AASA,MAAI,eAAwB;AAC5B,MAAI,YAAY,MAAM;AACpB,QAAI,OAAO,aAAa,UAAU;AAChC,qBAAe,aAAa,QAAQ;AAAA,IACtC,OAAO;AACL,YAAM,SAAS,SAAS,uBAAuB;AAC/C,eAAS,MAAM,MAAgC;AAC/C,YAAM,gBAAgB,MAAM,KAAK,OAAO,UAAU;AAClD,gBAAU,MAAM;AAAE,YAAI;AAAE,mBAAS,QAAQ;AAAA,QAAG,SAAS,GAAG;AAAA,QAAe;AAAA,MAAE,CAAC;AAC1E,qBAAe,cAAc,aAAa;AAAA,IAC5C;AAAA,EACF;AAIA,SAAO,SAAS,MAAM;AACpB,UAAM,OAAO,cAAc;AAC3B,QAAI,QAAQ,QAAQ,SAAS,GAAI,QAAO,OAAO,IAAI;AACnD,UAAM,QAAQ,eAAe;AAC7B,QAAI,aAAa,KAAK,EAAG,QAAO,cAAc,KAAK;AACnD,WAAO;AAAA,EACT,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signal,
|
|
3
|
+
effect,
|
|
4
|
+
computed,
|
|
5
|
+
untrack
|
|
6
|
+
} from "./signal.js";
|
|
7
|
+
import { hasContext, getCurrentComponent } from "./context.js";
|
|
8
|
+
import { disposable } from "./disposable.js";
|
|
9
|
+
import { inject } from "./injector.js";
|
|
10
|
+
import { __enrollPending } from "./settle.js";
|
|
11
|
+
import { formatDiag } from "./diagnostics.js";
|
|
12
|
+
function keyError(detail) {
|
|
13
|
+
return new Error(formatDiag(
|
|
14
|
+
"N602",
|
|
15
|
+
`invalid query key: ${detail}. Keys must be flat readonly (string | number | boolean | null)[] arrays \u2014 spread object params into tuple elements; null is the optional sentinel.`
|
|
16
|
+
));
|
|
17
|
+
}
|
|
18
|
+
function validateKey(raw) {
|
|
19
|
+
if (!Array.isArray(raw)) {
|
|
20
|
+
throw keyError(`expected an array, got ${typeof raw}`);
|
|
21
|
+
}
|
|
22
|
+
for (let i = 0; i < raw.length; i++) {
|
|
23
|
+
const el = raw[i];
|
|
24
|
+
if (el === null) continue;
|
|
25
|
+
const t = typeof el;
|
|
26
|
+
if (t === "string" || t === "boolean") continue;
|
|
27
|
+
if (t === "number") {
|
|
28
|
+
if (Number.isFinite(el)) continue;
|
|
29
|
+
throw keyError(`non-finite number at index ${i} (would collide with null)`);
|
|
30
|
+
}
|
|
31
|
+
if (el === void 0) {
|
|
32
|
+
throw keyError(`undefined at index ${i}`);
|
|
33
|
+
}
|
|
34
|
+
throw keyError(`${t} at index ${i}`);
|
|
35
|
+
}
|
|
36
|
+
return raw.slice();
|
|
37
|
+
}
|
|
38
|
+
function serializeKey(key) {
|
|
39
|
+
return JSON.stringify(key);
|
|
40
|
+
}
|
|
41
|
+
function matchesPrefix(key, prefix) {
|
|
42
|
+
if (prefix.length > key.length) return false;
|
|
43
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
44
|
+
if (key[i] !== prefix[i]) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
let nextMountIndex = 0;
|
|
49
|
+
function peek(s) {
|
|
50
|
+
let v;
|
|
51
|
+
untrack(() => {
|
|
52
|
+
v = s.value;
|
|
53
|
+
});
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
function toError(cause) {
|
|
57
|
+
if (cause instanceof Error) return cause;
|
|
58
|
+
try {
|
|
59
|
+
return new Error(String(cause));
|
|
60
|
+
} catch {
|
|
61
|
+
return new Error("Unknown error");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class QueryClient {
|
|
65
|
+
/** @internal */
|
|
66
|
+
_records = /* @__PURE__ */ new Map();
|
|
67
|
+
/** @internal clear() bumps this; observer effects track it and re-resolve. */
|
|
68
|
+
_version = signal(0);
|
|
69
|
+
/** @internal live observer count (N601 mixed-client diagnostic). */
|
|
70
|
+
_observers = 0;
|
|
71
|
+
/**
|
|
72
|
+
* Mark records matching the key prefix stale and rerun those with ≥1
|
|
73
|
+
* enabled observer (using the owning observer's fetcher/options).
|
|
74
|
+
* Disabled observers revalidate on re-enable; a record with only
|
|
75
|
+
* disabled/no observers stays stale until one arrives. Returns the
|
|
76
|
+
* number of records invalidated.
|
|
77
|
+
*
|
|
78
|
+
* invalidate(['tasks']) matches ['tasks'], ['tasks', '1'], ['tasks', '2', 'x'].
|
|
79
|
+
*/
|
|
80
|
+
invalidate(keyPrefix) {
|
|
81
|
+
const prefix = validateKey(keyPrefix);
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const rec of this._records.values()) {
|
|
84
|
+
if (!matchesPrefix(rec.key, prefix)) continue;
|
|
85
|
+
count++;
|
|
86
|
+
rec.stale = true;
|
|
87
|
+
const owner = ownerOf(
|
|
88
|
+
rec,
|
|
89
|
+
/* requireEnabled */
|
|
90
|
+
true
|
|
91
|
+
);
|
|
92
|
+
if (owner) startRun(rec, owner.fetcher, owner.retry);
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Drop every record. In-flight runs are aborted (a settle() termination).
|
|
98
|
+
* Live observers re-resolve fresh records on the version bump — enabled
|
|
99
|
+
* ones revalidate immediately.
|
|
100
|
+
*/
|
|
101
|
+
clear() {
|
|
102
|
+
for (const rec of this._records.values()) {
|
|
103
|
+
rec.generation++;
|
|
104
|
+
rec.controller?.abort();
|
|
105
|
+
rec.terminate?.();
|
|
106
|
+
rec.controller = null;
|
|
107
|
+
rec.terminate = null;
|
|
108
|
+
rec.runPromise = null;
|
|
109
|
+
rec.running = false;
|
|
110
|
+
rec.observers.clear();
|
|
111
|
+
}
|
|
112
|
+
this._records.clear();
|
|
113
|
+
this._version.value++;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Imperatively warm a record. Joins an in-flight run; a fresh record
|
|
117
|
+
* (per the given staleTime) is a no-op. Otherwise starts a run with THE
|
|
118
|
+
* GIVEN fetcher — an explicit imperative is its own owner. The run
|
|
119
|
+
* commits even with zero observers. Resolves at run termination.
|
|
120
|
+
*/
|
|
121
|
+
prefetch(key, fetcher, staleTime = 0) {
|
|
122
|
+
const rec = resolveRecord(this, key);
|
|
123
|
+
if (rec.running) return rec.runPromise ?? Promise.resolve();
|
|
124
|
+
if (!rec.stale && peek(rec.status) === "success" && staleTime > 0 && Date.now() - peek(rec.fetchedAt) < staleTime) {
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
return startRun(rec, fetcher, 0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function resolveRecord(client, rawKey) {
|
|
131
|
+
const key = validateKey(rawKey);
|
|
132
|
+
const serialized = serializeKey(key);
|
|
133
|
+
let rec = client._records.get(serialized);
|
|
134
|
+
if (!rec) {
|
|
135
|
+
rec = {
|
|
136
|
+
key,
|
|
137
|
+
serialized,
|
|
138
|
+
data: signal(void 0),
|
|
139
|
+
error: signal(null),
|
|
140
|
+
status: signal("idle"),
|
|
141
|
+
fetchedAt: signal(0),
|
|
142
|
+
stale: false,
|
|
143
|
+
running: false,
|
|
144
|
+
generation: 0,
|
|
145
|
+
controller: null,
|
|
146
|
+
runPromise: null,
|
|
147
|
+
terminate: null,
|
|
148
|
+
observers: /* @__PURE__ */ new Set()
|
|
149
|
+
};
|
|
150
|
+
client._records.set(serialized, rec);
|
|
151
|
+
}
|
|
152
|
+
return rec;
|
|
153
|
+
}
|
|
154
|
+
function ownerOf(rec, requireEnabled = false) {
|
|
155
|
+
let best;
|
|
156
|
+
let bestDisabled;
|
|
157
|
+
for (const obs of rec.observers) {
|
|
158
|
+
if (obs.enabled) {
|
|
159
|
+
if (!best || obs.mountIndex > best.mountIndex) best = obs;
|
|
160
|
+
} else if (!requireEnabled) {
|
|
161
|
+
if (!bestDisabled || obs.mountIndex > bestDisabled.mountIndex) bestDisabled = obs;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return best ?? bestDisabled;
|
|
165
|
+
}
|
|
166
|
+
function needsRun(rec, observer) {
|
|
167
|
+
if (rec.running) return false;
|
|
168
|
+
if (rec.stale) return true;
|
|
169
|
+
const status = peek(rec.status);
|
|
170
|
+
if (status === "idle" || status === "error") return true;
|
|
171
|
+
if (observer.staleTime <= 0) return true;
|
|
172
|
+
return Date.now() - peek(rec.fetchedAt) >= observer.staleTime;
|
|
173
|
+
}
|
|
174
|
+
function startRun(rec, fetcher, retry) {
|
|
175
|
+
rec.controller?.abort();
|
|
176
|
+
rec.terminate?.();
|
|
177
|
+
const generation = ++rec.generation;
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const terminate = __enrollPending();
|
|
180
|
+
rec.controller = controller;
|
|
181
|
+
rec.terminate = terminate;
|
|
182
|
+
rec.running = true;
|
|
183
|
+
rec.stale = false;
|
|
184
|
+
rec.status.value = "loading";
|
|
185
|
+
rec.error.value = null;
|
|
186
|
+
const run = (async () => {
|
|
187
|
+
try {
|
|
188
|
+
let lastError = null;
|
|
189
|
+
for (let attempt = 0; attempt <= retry; attempt++) {
|
|
190
|
+
if (generation !== rec.generation) return;
|
|
191
|
+
try {
|
|
192
|
+
const result = await Promise.resolve().then(() => fetcher(controller.signal));
|
|
193
|
+
if (generation !== rec.generation) return;
|
|
194
|
+
rec.data.value = result;
|
|
195
|
+
rec.error.value = null;
|
|
196
|
+
rec.fetchedAt.value = Date.now();
|
|
197
|
+
rec.status.value = "success";
|
|
198
|
+
return;
|
|
199
|
+
} catch (cause) {
|
|
200
|
+
lastError = toError(cause);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (generation !== rec.generation) return;
|
|
204
|
+
rec.error.value = lastError;
|
|
205
|
+
rec.status.value = "error";
|
|
206
|
+
} finally {
|
|
207
|
+
if (generation === rec.generation) {
|
|
208
|
+
rec.running = false;
|
|
209
|
+
rec.controller = null;
|
|
210
|
+
rec.runPromise = null;
|
|
211
|
+
rec.terminate = null;
|
|
212
|
+
}
|
|
213
|
+
terminate();
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
rec.runPromise = run;
|
|
217
|
+
return run;
|
|
218
|
+
}
|
|
219
|
+
function query(keyFn, fetcher, options = {}) {
|
|
220
|
+
const { staleTime = 0, retry = 0, enabled, initialData } = options;
|
|
221
|
+
const client = inject(QueryClient);
|
|
222
|
+
untrack(() => {
|
|
223
|
+
validateKey(keyFn());
|
|
224
|
+
});
|
|
225
|
+
const observer = {
|
|
226
|
+
mountIndex: nextMountIndex++,
|
|
227
|
+
fetcher,
|
|
228
|
+
staleTime,
|
|
229
|
+
retry,
|
|
230
|
+
enabled: true,
|
|
231
|
+
record: null
|
|
232
|
+
};
|
|
233
|
+
client._observers++;
|
|
234
|
+
const recSig = signal(null);
|
|
235
|
+
const enabledSig = signal(true);
|
|
236
|
+
let disposed = false;
|
|
237
|
+
let prevEnabled = null;
|
|
238
|
+
let prevVersion = -1;
|
|
239
|
+
const stopEffect = effect(() => {
|
|
240
|
+
if (disposed) return;
|
|
241
|
+
const version = client._version.value;
|
|
242
|
+
const key = keyFn();
|
|
243
|
+
const on = enabled ? !!enabled() : true;
|
|
244
|
+
const rec = resolveRecord(client, key);
|
|
245
|
+
observer.enabled = on;
|
|
246
|
+
enabledSig.value = on;
|
|
247
|
+
const moved = rec !== observer.record;
|
|
248
|
+
if (moved) {
|
|
249
|
+
observer.record?.observers.delete(observer);
|
|
250
|
+
rec.observers.add(observer);
|
|
251
|
+
observer.record = rec;
|
|
252
|
+
if (initialData !== void 0 && peek(rec.status) === "idle" && peek(rec.data) === void 0) {
|
|
253
|
+
rec.data.value = initialData;
|
|
254
|
+
}
|
|
255
|
+
recSig.value = rec;
|
|
256
|
+
}
|
|
257
|
+
const reEnabled = prevEnabled === false && on;
|
|
258
|
+
const versionChanged = version !== prevVersion;
|
|
259
|
+
prevEnabled = on;
|
|
260
|
+
prevVersion = version;
|
|
261
|
+
if (on && (moved || reEnabled || versionChanged) && needsRun(rec, observer)) {
|
|
262
|
+
const owner = ownerOf(rec) ?? observer;
|
|
263
|
+
startRun(rec, owner.fetcher, owner.retry);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
const dispose = () => {
|
|
267
|
+
if (disposed) return;
|
|
268
|
+
disposed = true;
|
|
269
|
+
observer.record?.observers.delete(observer);
|
|
270
|
+
observer.record = null;
|
|
271
|
+
client._observers--;
|
|
272
|
+
stopEffect();
|
|
273
|
+
};
|
|
274
|
+
if (hasContext()) {
|
|
275
|
+
getCurrentComponent().addDisposer(dispose);
|
|
276
|
+
}
|
|
277
|
+
const refetch = () => {
|
|
278
|
+
if (disposed) return;
|
|
279
|
+
const rec = observer.record;
|
|
280
|
+
if (!rec || !observer.enabled) return;
|
|
281
|
+
if (rec.running) return;
|
|
282
|
+
const owner = ownerOf(rec) ?? observer;
|
|
283
|
+
startRun(rec, owner.fetcher, owner.retry);
|
|
284
|
+
};
|
|
285
|
+
const result = {
|
|
286
|
+
data: computed(() => {
|
|
287
|
+
const rec = recSig.value;
|
|
288
|
+
return rec ? rec.data.value : initialData;
|
|
289
|
+
}),
|
|
290
|
+
loading: computed(() => enabledSig.value && recSig.value?.status.value === "loading"),
|
|
291
|
+
error: computed(() => recSig.value?.error.value ?? null),
|
|
292
|
+
status: computed(() => recSig.value?.status.value ?? "idle"),
|
|
293
|
+
refetch,
|
|
294
|
+
dispose
|
|
295
|
+
};
|
|
296
|
+
return disposable(result, dispose);
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
QueryClient,
|
|
300
|
+
query
|
|
301
|
+
};
|
|
302
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/query.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * query.ts \u2014 Keyed logical-request store (ADR 0030.2 T1).\n *\n * The cache unit is a per-key RECORD living in `QueryClient`:\n * `{data, error, status, fetchedAt}` as signals, plus exactly one `run()`\n * owning the retry loop, a per-run `AbortController` handed to the fetcher,\n * and the cache commit. `query()` is a thin OBSERVER: an effect keeps it\n * registered on the record its current key points at, and its outputs are\n * `computed()` views over that record. Dedup unit = logical request, by\n * construction (issues 0005\u20130009 close as a class).\n *\n * ```ts\n * const tasks = query(\n * () => ['tasks', scopeId.value],\n * (signal) => api.getTasks(scopeId.value, { signal }),\n * );\n * tasks.data // ReadonlySignal<Task[] | undefined>\n * tasks.loading // ReadonlySignal<boolean>\n * tasks.error // ReadonlySignal<Error | null>\n * tasks.status // ReadonlySignal<'idle' | 'loading' | 'success' | 'error'>\n * tasks.refetch() // force a refetch (joins an in-flight run)\n * tasks.dispose() // standalone callers; component setup auto-disposes\n * ```\n *\n * Ownership rules (ADR 0030.2 \u00A78, binding):\n * - The most-recently-mounted enabled observer's fetcher/options own the\n * next run for a shared key; `staleTime` freshness is per-observer POLICY\n * (it decides whether *that* observer triggers a run), while the record\n * state is shared truth.\n * - `invalidate()` marks records stale and reruns only records with \u22651\n * enabled observer; disabled observers revalidate on re-enable.\n * - A zero-observer record's in-flight run completes and commits.\n * - Records are NEVER garbage-collected: the cache is unbounded for the\n * life of the client (restates today's unbounded-cache bound). Bound it\n * operationally with `clear()` or a fresh client per app lifetime.\n *\n * Key contract: keys are FLAT \u2014 `readonly (string|number|boolean|null)[]`.\n * Objects and `undefined` are rejected with coded error N602 (`null` is\n * the optional sentinel). Flat keys are order-sensitive: prefer\n * per-endpoint key-builder helpers \u2014 a reordered key is a silent cache\n * *miss*, benign relative to a collision, but a miss.\n *\n * SSG stance (documented, not built): under core-render, records fetch\n * into the build-lifetime client and must TERMINATE before snapshot \u2014\n * `await settle()` is the barrier. Otherwise queries must be statically\n * disabled for the build.\n */\n\nimport {\n signal,\n effect,\n computed,\n untrack,\n type Signal,\n type ReadonlySignal,\n} from './signal.js';\nimport { hasContext, getCurrentComponent } from './context.js';\nimport { disposable, type DisposableResource } from './disposable.js';\nimport { inject } from './injector.js';\nimport { __enrollPending } from './settle.js';\nimport { formatDiag } from './diagnostics.js';\n\n// Diagnostics go through the core leaf (diagnostics.ts). Codes N602\u2013N603\n// belong to the query/async cluster (N601 retired before release \u2014 see\n// the eliminated-class note below).\n\n/** Build the coded error for key-contract violations. Always thrown \u2014\n * key validity is a contract, not an advisory diagnostic. */\nfunction keyError(detail: string): Error {\n return new Error(formatDiag(\n 'N602',\n `invalid query key: ${detail}. Keys must be flat `\n + 'readonly (string | number | boolean | null)[] arrays \u2014 spread '\n + 'object params into tuple elements; null is the optional sentinel.',\n ));\n}\n\n// \u2500\u2500 Key contract \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/** A single query-key element. `null` is the optional sentinel. */\nexport type QueryKeyElement = string | number | boolean | null;\n\n/** The supported key contract: a flat, order-sensitive tuple. */\nexport type QueryKey = readonly QueryKeyElement[];\n\n/**\n * Validate a key against the flat contract and return a detached copy\n * (callers may mutate their array; records must not observe that).\n * Rejects objects, arrays, functions, undefined, and non-finite numbers\n * (NaN/Infinity JSON-serialize as null and would collide) with N602.\n */\nfunction validateKey(raw: unknown): QueryKey {\n if (!Array.isArray(raw)) {\n throw keyError(`expected an array, got ${typeof raw}`);\n }\n for (let i = 0; i < raw.length; i++) {\n const el: unknown = raw[i];\n if (el === null) continue;\n const t = typeof el;\n if (t === 'string' || t === 'boolean') continue;\n if (t === 'number') {\n if (Number.isFinite(el)) continue;\n throw keyError(`non-finite number at index ${i} (would collide with null)`);\n }\n if (el === undefined) {\n throw keyError(`undefined at index ${i}`);\n }\n throw keyError(`${t} at index ${i}`);\n }\n return raw.slice() as QueryKey;\n}\n\n/** THE one serializer: JSON.stringify of the validated flat array. */\nfunction serializeKey(key: QueryKey): string {\n return JSON.stringify(key);\n}\n\n/** Element-wise prefix match over validated keys (=== is sound: elements\n * are primitives and NaN is rejected by the contract). */\nfunction matchesPrefix(key: QueryKey, prefix: QueryKey): boolean {\n if (prefix.length > key.length) return false;\n for (let i = 0; i < prefix.length; i++) {\n if (key[i] !== prefix[i]) return false;\n }\n return true;\n}\n\n// \u2500\u2500 Types \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\u2500\u2500\u2500\n\nexport type QueryStatus = 'idle' | 'loading' | 'success' | 'error';\n\n/** Fetcher contract: receives the run's AbortSignal. Zero-arg fetchers\n * remain assignable. */\nexport type QueryFetcher<T> = (signal: AbortSignal) => Promise<T>;\n\nexport interface QueryResult<T> extends DisposableResource {\n /** The record's data, or `initialData`/undefined before first commit. */\n data: ReadonlySignal<T | undefined>;\n /** True while this observer is enabled and its record is fetching. */\n loading: ReadonlySignal<boolean>;\n /** The record's terminal error, or null. Cleared when a run starts. */\n error: ReadonlySignal<Error | null>;\n /** The record's lifecycle status. */\n status: ReadonlySignal<QueryStatus>;\n /** Force a refetch: bypasses staleTime freshness, but JOINS an in-flight\n * logical request rather than starting a second one (issue 0005). No-op\n * while disabled. */\n refetch(): void;\n /** Unregister this observer. Component setup calls this automatically at\n * teardown; standalone callers (tests, scripts) call it explicitly. The\n * record \u2014 and any in-flight run \u2014 survives: zero-observer runs complete\n * and commit (ADR 0030.2 \u00A78). */\n dispose(): void;\n}\n\nexport interface QueryOptions<T> {\n /** Cache freshness duration in ms for THIS observer. Default: 0 (a\n * registration/re-enable always revalidates). */\n staleTime?: number;\n /** Number of retries the run performs when this observer owns it. Default: 0 */\n retry?: number;\n /** Skip fetching while this returns false. Signal reads are tracked. */\n enabled?: () => boolean;\n /** Synchronous seed committed into an untouched (idle) record. */\n initialData?: T;\n}\n\n// \u2500\u2500 Record store \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\ninterface QueryRecord {\n readonly key: QueryKey;\n readonly serialized: string;\n readonly data: Signal<unknown>;\n readonly error: Signal<Error | null>;\n readonly status: Signal<QueryStatus>;\n /** Epoch ms of the last successful commit; 0 = never. */\n readonly fetchedAt: Signal<number>;\n /** Marked by invalidate(); cleared when a run starts. */\n stale: boolean;\n /** True while a run is in flight (status mirrors it reactively). */\n running: boolean;\n /** Increments per run start; a superseded run may never commit. */\n generation: number;\n controller: AbortController | null;\n /** The in-flight run (joinable by prefetch). */\n runPromise: Promise<void> | null;\n /** settle() terminator for the in-flight run. */\n terminate: (() => void) | null;\n readonly observers: Set<QueryObserver>;\n}\n\ninterface QueryObserver {\n readonly mountIndex: number;\n readonly fetcher: QueryFetcher<unknown>;\n readonly staleTime: number;\n readonly retry: number;\n enabled: boolean;\n record: QueryRecord | null;\n}\n\n/** Monotonic mount counter \u2014 \"most recently mounted\" is decided by\n * query() construction order, not re-registration order. */\nlet nextMountIndex = 0;\n\n/** Read a record signal without tracking it into the ambient observer \u2014\n * the observer effect must depend on keys/enabled/version ONLY, never on\n * record state (that is what keeps refetch loops unrepresentable). */\nfunction peek<T>(s: ReadonlySignal<T>): T {\n let v!: T;\n untrack(() => { v = s.value; });\n return v;\n}\n\nfunction toError(cause: unknown): Error {\n if (cause instanceof Error) return cause;\n try {\n return new Error(String(cause));\n } catch {\n return new Error('Unknown error');\n }\n}\n\n/**\n * Global keyed logical-request store. One per app: resolved via\n * `inject(QueryClient)` (auto-singleton; override with\n * `provide(QueryClient, ...)` BEFORE the first query() call).\n *\n * Records are never GC'd \u2014 see the module doc for the bound.\n */\nexport class QueryClient {\n /** @internal */ _records = new Map<string, QueryRecord>();\n /** @internal clear() bumps this; observer effects track it and re-resolve. */\n _version = signal(0);\n /** @internal live observer count (N601 mixed-client diagnostic). */\n _observers = 0;\n\n /**\n * Mark records matching the key prefix stale and rerun those with \u22651\n * enabled observer (using the owning observer's fetcher/options).\n * Disabled observers revalidate on re-enable; a record with only\n * disabled/no observers stays stale until one arrives. Returns the\n * number of records invalidated.\n *\n * invalidate(['tasks']) matches ['tasks'], ['tasks', '1'], ['tasks', '2', 'x'].\n */\n invalidate(keyPrefix: QueryKey): number {\n const prefix = validateKey(keyPrefix);\n let count = 0;\n for (const rec of this._records.values()) {\n if (!matchesPrefix(rec.key, prefix)) continue;\n count++;\n rec.stale = true;\n const owner = ownerOf(rec, /* requireEnabled */ true);\n if (owner) startRun(rec, owner.fetcher, owner.retry);\n }\n return count;\n }\n\n /**\n * Drop every record. In-flight runs are aborted (a settle() termination).\n * Live observers re-resolve fresh records on the version bump \u2014 enabled\n * ones revalidate immediately.\n */\n clear(): void {\n for (const rec of this._records.values()) {\n rec.generation++;\n rec.controller?.abort();\n rec.terminate?.();\n rec.controller = null;\n rec.terminate = null;\n rec.runPromise = null;\n rec.running = false;\n rec.observers.clear();\n }\n this._records.clear();\n this._version.value++;\n }\n\n /**\n * Imperatively warm a record. Joins an in-flight run; a fresh record\n * (per the given staleTime) is a no-op. Otherwise starts a run with THE\n * GIVEN fetcher \u2014 an explicit imperative is its own owner. The run\n * commits even with zero observers. Resolves at run termination.\n */\n prefetch<T>(key: QueryKey, fetcher: QueryFetcher<T>, staleTime = 0): Promise<void> {\n const rec = resolveRecord(this, key);\n if (rec.running) return rec.runPromise ?? Promise.resolve();\n if (!rec.stale && peek(rec.status) === 'success' && staleTime > 0\n && Date.now() - peek(rec.fetchedAt) < staleTime) {\n return Promise.resolve();\n }\n return startRun(rec, fetcher as QueryFetcher<unknown>, 0);\n }\n}\n\n/** Get-or-create the record for a key. Validation throws N602. */\nfunction resolveRecord(client: QueryClient, rawKey: unknown): QueryRecord {\n const key = validateKey(rawKey);\n const serialized = serializeKey(key);\n let rec = client._records.get(serialized);\n if (!rec) {\n rec = {\n key,\n serialized,\n data: signal<unknown>(undefined),\n error: signal<Error | null>(null),\n status: signal<QueryStatus>('idle'),\n fetchedAt: signal(0),\n stale: false,\n running: false,\n generation: 0,\n controller: null,\n runPromise: null,\n terminate: null,\n observers: new Set(),\n };\n client._records.set(serialized, rec); // never deleted \u2014 documented bound\n }\n return rec;\n}\n\n/** The observer whose fetcher/options own the next run: most recently\n * mounted, preferring enabled observers (a disabled observer asked not\n * to fetch \u2014 its fetcher should not fire). */\nfunction ownerOf(rec: QueryRecord, requireEnabled = false): QueryObserver | undefined {\n let best: QueryObserver | undefined;\n let bestDisabled: QueryObserver | undefined;\n for (const obs of rec.observers) {\n if (obs.enabled) {\n if (!best || obs.mountIndex > best.mountIndex) best = obs;\n } else if (!requireEnabled) {\n if (!bestDisabled || obs.mountIndex > bestDisabled.mountIndex) bestDisabled = obs;\n }\n }\n return best ?? bestDisabled;\n}\n\n/** Does this record need a run, per THIS observer's freshness policy?\n * Called only on real transitions (registration, re-enable, clear());\n * record reads are untracked, so commits never re-trigger observers. */\nfunction needsRun(rec: QueryRecord, observer: QueryObserver): boolean {\n if (rec.running) return false; // join the in-flight logical request\n if (rec.stale) return true;\n const status = peek(rec.status);\n if (status === 'idle' || status === 'error') return true;\n if (observer.staleTime <= 0) return true;\n return Date.now() - peek(rec.fetchedAt) >= observer.staleTime;\n}\n\n/**\n * THE run: one logical request owning the retry loop and the commit.\n * Supersedes (aborts) any in-flight run for the record. Enrolls in the\n * settle() registry on start and terminates it at commit, terminal error,\n * or abort \u2014 never at raw promise settlement.\n */\nfunction startRun(\n rec: QueryRecord,\n fetcher: QueryFetcher<unknown>,\n retry: number,\n): Promise<void> {\n // Supersede: abort IS the superseded run's termination.\n rec.controller?.abort();\n rec.terminate?.();\n\n const generation = ++rec.generation;\n const controller = new AbortController();\n const terminate = __enrollPending();\n rec.controller = controller;\n rec.terminate = terminate;\n rec.running = true;\n rec.stale = false;\n rec.status.value = 'loading';\n rec.error.value = null;\n\n const run = (async () => {\n try {\n let lastError: Error | null = null;\n for (let attempt = 0; attempt <= retry; attempt++) {\n if (generation !== rec.generation) return; // superseded\n try {\n // The microtask hop makes a synchronous fetcher throw a normal\n // rejection \u2014 it enters the same retry/terminal path (issue 0008).\n const result = await Promise.resolve().then(() => fetcher(controller.signal));\n if (generation !== rec.generation) return;\n rec.data.value = result;\n rec.error.value = null;\n rec.fetchedAt.value = Date.now();\n rec.status.value = 'success';\n return; // commit\n } catch (cause) {\n lastError = toError(cause);\n }\n }\n if (generation !== rec.generation) return;\n rec.error.value = lastError;\n rec.status.value = 'error'; // terminal error after retries\n } finally {\n if (generation === rec.generation) {\n rec.running = false;\n rec.controller = null;\n rec.runPromise = null;\n rec.terminate = null;\n }\n terminate(); // idempotent \u2014 superseded runs already terminated at abort\n }\n })();\n rec.runPromise = run;\n return run;\n}\n\n// \u2500\u2500 Mixed clients: an eliminated class (formerly N601) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// With inject() as the single resolution path, the only way two clients\n// could mix was provide(QueryClient, ...) landing after queries already\n// bound the auto-created singleton. The injector now throws N501 at that\n// exact provide() (DI freeze, ADR 0030.2 T6), so the split-brain state is\n// unrepresentable in dev and was always silent in prod \u2014 the symptom-side\n// warning this block used to hold (N601) died with its cause. Retired\n// before release; the code number is not reused.\n\n// \u2500\u2500 query() \u2014 the thin observer \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 * Observe the record the reactive key points at.\n *\n * @param keyFn - Returns the flat cache key. Signal reads are tracked; a\n * key change re-registers the observer on the new record.\n * @param fetcher - Fetches the data; receives the run's AbortSignal.\n * @param options - staleTime / retry / enabled / initialData.\n * @throws N602 synchronously when the initial key violates the contract.\n */\nexport function query<T>(\n keyFn: () => QueryKey,\n fetcher: QueryFetcher<T>,\n options: QueryOptions<T> = {},\n): QueryResult<T> {\n const { staleTime = 0, retry = 0, enabled, initialData } = options;\n\n const client = inject(QueryClient);\n\n // Surface key-contract violations at the call site: the observer effect\n // is error-contained (a later reactive key going invalid is logged with\n // the same coded message), so the construction-time key must be checked\n // synchronously. untrack: query() may run under an ambient observer\n // (component setup) and key signals must not widen its dependencies.\n untrack(() => { validateKey(keyFn()); });\n\n const observer: QueryObserver = {\n mountIndex: nextMountIndex++,\n fetcher: fetcher as QueryFetcher<unknown>,\n staleTime,\n retry,\n enabled: true,\n record: null,\n };\n client._observers++;\n\n const recSig = signal<QueryRecord | null>(null);\n const enabledSig = signal(true);\n let disposed = false;\n let prevEnabled: boolean | null = null;\n let prevVersion = -1;\n\n // The registration effect \u2014 depends on keyFn signals, enabled() signals,\n // and the client version. NEVER on record state (all record reads are\n // untracked), so commits cannot re-trigger it: run starts happen only on\n // the real transitions listed in needsRun()'s doc.\n const stopEffect = effect(() => {\n if (disposed) return;\n const version = client._version.value;\n const key = keyFn();\n const on = enabled ? !!enabled() : true;\n const rec = resolveRecord(client, key);\n\n observer.enabled = on;\n enabledSig.value = on;\n\n const moved = rec !== observer.record;\n if (moved) {\n observer.record?.observers.delete(observer);\n rec.observers.add(observer);\n observer.record = rec;\n if (initialData !== undefined && peek(rec.status) === 'idle'\n && peek(rec.data) === undefined) {\n rec.data.value = initialData; // seed; not a fetch \u2014 status stays idle\n }\n recSig.value = rec;\n }\n\n const reEnabled = prevEnabled === false && on;\n const versionChanged = version !== prevVersion;\n prevEnabled = on;\n prevVersion = version;\n\n if (on && (moved || reEnabled || versionChanged) && needsRun(rec, observer)) {\n const owner = ownerOf(rec) ?? observer;\n startRun(rec, owner.fetcher, owner.retry);\n }\n });\n\n const dispose = () => {\n if (disposed) return;\n disposed = true;\n observer.record?.observers.delete(observer);\n observer.record = null;\n client._observers--;\n stopEffect();\n // NOTE: no abort \u2014 a zero-observer in-flight run completes and commits.\n };\n\n if (hasContext()) {\n getCurrentComponent().addDisposer(dispose);\n }\n\n const refetch = () => {\n if (disposed) return;\n const rec = observer.record;\n if (!rec || !observer.enabled) return;\n if (rec.running) return; // join the in-flight logical request (dedup)\n const owner = ownerOf(rec) ?? observer;\n startRun(rec, owner.fetcher, owner.retry);\n };\n\n const result = {\n data: computed(() => {\n const rec = recSig.value;\n return (rec ? rec.data.value : initialData) as T | undefined;\n }),\n loading: computed(() => enabledSig.value && recSig.value?.status.value === 'loading'),\n error: computed(() => recSig.value?.error.value ?? null),\n status: computed<QueryStatus>(() => recSig.value?.status.value ?? 'idle'),\n refetch,\n dispose,\n };\n return disposable(result, dispose);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const REF_BRAND = /* @__PURE__ */ Symbol.for("backlog.ref");
|
|
2
|
+
function ref() {
|
|
3
|
+
return {
|
|
4
|
+
[REF_BRAND]: true,
|
|
5
|
+
current: null
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function isRef(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && REF_BRAND in value;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
REF_BRAND,
|
|
13
|
+
isRef,
|
|
14
|
+
ref
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=ref.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ref.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * ref.ts \u2014 Element reference primitive for imperative DOM access\n *\n * `ref()` creates a container that receives a DOM element reference\n * after the template is mounted. Use it for focus management, DOM\n * measurement, scroll management, and third-party library init.\n *\n * ```ts\n * const inputRef = ref<HTMLInputElement>();\n *\n * onMount(() => inputRef.current?.focus());\n *\n * return html`<input ref=\"${inputRef}\" @keydown.enter=${onSubmit} />`;\n * ```\n *\n * The template engine detects `Ref` objects in `ref=\"${...}\"`\n * attribute bindings and assigns the referenced element.\n */\n\n// \u2500\u2500 Brand for ref detection in templates \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\nexport const REF_BRAND = Symbol.for('backlog.ref');\n\n// \u2500\u2500 Types \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\u2500\u2500\u2500\n\nexport interface Ref<T extends Element = Element> {\n readonly [REF_BRAND]: true;\n /** The DOM element, or null before mount / after unmount */\n current: T | null;\n}\n\n// \u2500\u2500 Public API \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 * Create a ref container for imperative DOM access.\n *\n * After the template is mounted, `ref.current` holds the element.\n * After dispose, `ref.current` is set back to null.\n */\nexport function ref<T extends Element = Element>(): Ref<T> {\n return {\n [REF_BRAND]: true as const,\n current: null,\n };\n}\n\n/**\n * Check if a value is a Ref.\n * Used by the template engine to detect refs in expression slots.\n */\nexport function isRef(value: unknown): value is Ref {\n return (\n value !== null &&\n typeof value === 'object' &&\n REF_BRAND in (value as Record<symbol, unknown>)\n );\n}\n"],
|
|
5
|
+
"mappings": "AAqBO,MAAM,YAAY,uBAAO,IAAI,aAAa;AAkB1C,SAAS,MAA2C;AACzD,SAAO;AAAA,IACL,CAAC,SAAS,GAAG;AAAA,IACb,SAAS;AAAA,EACX;AACF;AAMO,SAAS,MAAM,OAA8B;AAClD,SACE,UAAU,QACV,OAAO,UAAU,YACjB,aAAc;AAElB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|