@fictjs/runtime 0.7.0 → 0.9.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 +46 -0
- package/dist/advanced.cjs +9 -9
- package/dist/advanced.d.cts +4 -4
- package/dist/advanced.d.ts +4 -4
- package/dist/advanced.js +4 -4
- package/dist/{effect-DAzpH7Mm.d.cts → binding-BWchH3Kp.d.cts} +33 -24
- package/dist/{effect-DAzpH7Mm.d.ts → binding-BWchH3Kp.d.ts} +33 -24
- package/dist/{chunk-7YQK3XKY.js → chunk-DXG3TARY.js} +520 -518
- package/dist/chunk-DXG3TARY.js.map +1 -0
- package/dist/{chunk-TLDT76RV.js → chunk-FVX77557.js} +3 -3
- package/dist/{chunk-WRU3IZOA.js → chunk-JVYH76ZX.js} +3 -3
- package/dist/chunk-LBE6DC3V.cjs +768 -0
- package/dist/chunk-LBE6DC3V.cjs.map +1 -0
- package/dist/chunk-N6ODUM2Y.js +768 -0
- package/dist/chunk-N6ODUM2Y.js.map +1 -0
- package/dist/{chunk-PRF4QG73.cjs → chunk-OAM7HABA.cjs} +423 -246
- package/dist/chunk-OAM7HABA.cjs.map +1 -0
- package/dist/{chunk-CEV6TO5U.cjs → chunk-PD6IQY2Y.cjs} +8 -8
- package/dist/{chunk-CEV6TO5U.cjs.map → chunk-PD6IQY2Y.cjs.map} +1 -1
- package/dist/{chunk-HHDHQGJY.cjs → chunk-PG4QX2I2.cjs} +17 -17
- package/dist/{chunk-HHDHQGJY.cjs.map → chunk-PG4QX2I2.cjs.map} +1 -1
- package/dist/{chunk-4LCHQ7U4.js → chunk-T2LNV5Q5.js} +271 -94
- package/dist/chunk-T2LNV5Q5.js.map +1 -0
- package/dist/{chunk-FSCBL7RI.cjs → chunk-UBFDB6OL.cjs} +521 -519
- package/dist/chunk-UBFDB6OL.cjs.map +1 -0
- package/dist/{context-C4vBQbb4.d.ts → devtools-5AipK9CX.d.cts} +35 -35
- package/dist/{context-BFbHf9nC.d.cts → devtools-BDp76luf.d.ts} +35 -35
- package/dist/index.cjs +42 -42
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.dev.js +3 -3
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/internal-list.cjs +12 -0
- package/dist/internal-list.cjs.map +1 -0
- package/dist/internal-list.d.cts +2 -0
- package/dist/internal-list.d.ts +2 -0
- package/dist/internal-list.js +12 -0
- package/dist/internal-list.js.map +1 -0
- package/dist/internal.cjs +6 -746
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +6 -74
- package/dist/internal.d.ts +6 -74
- package/dist/internal.js +12 -752
- package/dist/internal.js.map +1 -1
- package/dist/list-DL5DOFcO.d.ts +71 -0
- package/dist/list-hP7hQ9Vk.d.cts +71 -0
- package/dist/loader.cjs +94 -15
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.d.cts +16 -2
- package/dist/loader.d.ts +16 -2
- package/dist/loader.js +87 -8
- package/dist/loader.js.map +1 -1
- package/dist/{props-84UJeWO8.d.cts → props-BpZz0AOq.d.cts} +2 -2
- package/dist/{props-BRhFK50f.d.ts → props-CjLH0JE-.d.ts} +2 -2
- package/dist/{resume-i-A3EFox.d.cts → resume-BJ4oHLi_.d.cts} +3 -1
- package/dist/{resume-CqeQ3v_q.d.ts → resume-CuyJWXP_.d.ts} +3 -1
- package/dist/{scope-DlCBL1Ft.d.cts → scope-BJCtq8hJ.d.cts} +1 -1
- package/dist/{scope-D3DpsfoG.d.ts → scope-jPt5DHRT.d.ts} +1 -1
- package/package.json +8 -1
- package/src/binding.ts +113 -36
- package/src/cycle-guard.ts +3 -3
- package/src/internal/list.ts +7 -0
- package/src/internal.ts +1 -0
- package/src/list-helpers.ts +1 -1
- package/src/loader.ts +119 -9
- package/src/resume.ts +6 -3
- package/src/signal.ts +8 -1
- package/dist/chunk-4LCHQ7U4.js.map +0 -1
- package/dist/chunk-7YQK3XKY.js.map +0 -1
- package/dist/chunk-FSCBL7RI.cjs.map +0 -1
- package/dist/chunk-PRF4QG73.cjs.map +0 -1
- /package/dist/{chunk-TLDT76RV.js.map → chunk-FVX77557.js.map} +0 -0
- /package/dist/{chunk-WRU3IZOA.js.map → chunk-JVYH76ZX.js.map} +0 -0
package/dist/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { _ as __fictUseLexicalScope } from './resume-CuyJWXP_.js';
|
|
2
2
|
import './signal-C4ISF17w.js';
|
|
3
3
|
|
|
4
4
|
interface PrefetchStrategy {
|
|
@@ -29,6 +29,11 @@ interface ResumableLoaderOptions {
|
|
|
29
29
|
document?: Document;
|
|
30
30
|
snapshotScriptId?: string;
|
|
31
31
|
events?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Receives structured snapshot/resume issues detected by the loader.
|
|
34
|
+
* Useful for telemetry and fail-safe fallback orchestration.
|
|
35
|
+
*/
|
|
36
|
+
onSnapshotIssue?: (issue: SnapshotIssue) => void;
|
|
32
37
|
/**
|
|
33
38
|
* Prefetch strategy configuration.
|
|
34
39
|
* Set to false to disable all prefetching.
|
|
@@ -36,6 +41,15 @@ interface ResumableLoaderOptions {
|
|
|
36
41
|
*/
|
|
37
42
|
prefetch?: PrefetchStrategy | false;
|
|
38
43
|
}
|
|
44
|
+
type SnapshotIssueCode = 'snapshot_parse_error' | 'snapshot_invalid_shape' | 'snapshot_unsupported_version' | 'scope_snapshot_missing';
|
|
45
|
+
interface SnapshotIssue {
|
|
46
|
+
code: SnapshotIssueCode;
|
|
47
|
+
message: string;
|
|
48
|
+
source: string;
|
|
49
|
+
expectedVersion: number;
|
|
50
|
+
actualVersion?: number;
|
|
51
|
+
scopeId?: string;
|
|
52
|
+
}
|
|
39
53
|
/**
|
|
40
54
|
* Reset the hydrated scopes set. Useful for testing.
|
|
41
55
|
*/
|
|
@@ -54,4 +68,4 @@ declare function waitForPendingHandlers(): Promise<void>;
|
|
|
54
68
|
declare function cleanupEventListeners(): void;
|
|
55
69
|
declare function installResumableLoader(options?: ResumableLoaderOptions): void;
|
|
56
70
|
|
|
57
|
-
export { type PrefetchStrategy, type ResumableLoaderOptions, cleanupEventListeners, installResumableLoader, resetHydratedScopes, resetPrefetchedUrls, waitForPendingHandlers };
|
|
71
|
+
export { type PrefetchStrategy, type ResumableLoaderOptions, type SnapshotIssue, type SnapshotIssueCode, cleanupEventListeners, installResumableLoader, resetHydratedScopes, resetPrefetchedUrls, waitForPendingHandlers };
|
package/dist/loader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DelegatedEvents,
|
|
3
|
+
FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
|
|
3
4
|
__fictEnableResumable,
|
|
4
5
|
__fictEnsureScope,
|
|
5
6
|
__fictGetResume,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
__fictMergeSSRState,
|
|
8
9
|
__fictSetSSRState,
|
|
9
10
|
__fictUseLexicalScope
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DXG3TARY.js";
|
|
11
12
|
|
|
12
13
|
// src/loader.ts
|
|
13
14
|
function resolveModuleUrl(url) {
|
|
@@ -26,6 +27,8 @@ var prefetchCleanup = null;
|
|
|
26
27
|
var eventListenerCleanup = null;
|
|
27
28
|
var snapshotObserver = null;
|
|
28
29
|
var processedSnapshots = /* @__PURE__ */ new Set();
|
|
30
|
+
var snapshotIssueHandler = null;
|
|
31
|
+
var emittedIssueKeys = /* @__PURE__ */ new Set();
|
|
29
32
|
function resetHydratedScopes() {
|
|
30
33
|
hydratedScopes.clear();
|
|
31
34
|
}
|
|
@@ -46,9 +49,12 @@ function cleanupEventListeners() {
|
|
|
46
49
|
function installResumableLoader(options = {}) {
|
|
47
50
|
const doc = options.document ?? window.document;
|
|
48
51
|
const scriptId = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
|
|
52
|
+
snapshotIssueHandler = options.onSnapshotIssue ?? null;
|
|
49
53
|
hydratedScopes.clear();
|
|
50
54
|
prefetchedUrls.clear();
|
|
51
55
|
processedSnapshots.clear();
|
|
56
|
+
emittedIssueKeys.clear();
|
|
57
|
+
__fictSetSSRState(null);
|
|
52
58
|
if (eventListenerCleanup) {
|
|
53
59
|
eventListenerCleanup();
|
|
54
60
|
eventListenerCleanup = null;
|
|
@@ -63,10 +69,9 @@ function installResumableLoader(options = {}) {
|
|
|
63
69
|
}
|
|
64
70
|
const snapshotEl = doc.getElementById(scriptId);
|
|
65
71
|
if (snapshotEl?.textContent) {
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
const state = parseSnapshotText(snapshotEl.textContent, `#${scriptId}`);
|
|
73
|
+
if (state) {
|
|
68
74
|
__fictSetSSRState(state);
|
|
69
|
-
} catch {
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
const snapshotScripts = doc.querySelectorAll(
|
|
@@ -121,10 +126,76 @@ function parseSnapshotScript(script) {
|
|
|
121
126
|
processedSnapshots.add(script);
|
|
122
127
|
const text = script.textContent;
|
|
123
128
|
if (!text) return;
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
const source = script.id ? `#${script.id}` : "<script[data-fict-snapshot]>";
|
|
130
|
+
const state = parseSnapshotText(text, source);
|
|
131
|
+
if (state) {
|
|
126
132
|
__fictMergeSSRState(state);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function parseSnapshotText(text, source) {
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(text);
|
|
127
139
|
} catch {
|
|
140
|
+
emitSnapshotIssue({
|
|
141
|
+
code: "snapshot_parse_error",
|
|
142
|
+
message: "[fict/loader] Failed to parse SSR snapshot JSON.",
|
|
143
|
+
source,
|
|
144
|
+
expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return normalizeSnapshotState(parsed, source);
|
|
149
|
+
}
|
|
150
|
+
function normalizeSnapshotState(value, source) {
|
|
151
|
+
if (!isRecord(value)) {
|
|
152
|
+
emitSnapshotIssue({
|
|
153
|
+
code: "snapshot_invalid_shape",
|
|
154
|
+
message: "[fict/loader] Snapshot payload must be an object.",
|
|
155
|
+
source,
|
|
156
|
+
expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION
|
|
157
|
+
});
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const rawVersion = value.v;
|
|
161
|
+
const version = rawVersion === void 0 ? FICT_SSR_SNAPSHOT_SCHEMA_VERSION : rawVersion;
|
|
162
|
+
if (!Number.isInteger(version) || version !== FICT_SSR_SNAPSHOT_SCHEMA_VERSION) {
|
|
163
|
+
const versionIssue = {
|
|
164
|
+
code: "snapshot_unsupported_version",
|
|
165
|
+
message: `[fict/loader] Snapshot schema version ${String(version)} is not supported by this runtime.`,
|
|
166
|
+
source,
|
|
167
|
+
expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION
|
|
168
|
+
};
|
|
169
|
+
if (typeof version === "number") {
|
|
170
|
+
versionIssue.actualVersion = version;
|
|
171
|
+
}
|
|
172
|
+
emitSnapshotIssue({
|
|
173
|
+
...versionIssue
|
|
174
|
+
});
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const scopes = value.scopes;
|
|
178
|
+
if (!isRecord(scopes)) {
|
|
179
|
+
emitSnapshotIssue({
|
|
180
|
+
code: "snapshot_invalid_shape",
|
|
181
|
+
message: "[fict/loader] Snapshot payload is missing a valid `scopes` object.",
|
|
182
|
+
source,
|
|
183
|
+
expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION
|
|
184
|
+
});
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return { v: FICT_SSR_SNAPSHOT_SCHEMA_VERSION, scopes };
|
|
188
|
+
}
|
|
189
|
+
function isRecord(value) {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
191
|
+
}
|
|
192
|
+
function emitSnapshotIssue(issue) {
|
|
193
|
+
const key = `${issue.code}|${issue.source}|${issue.scopeId ?? ""}|${issue.actualVersion ?? ""}|${issue.expectedVersion}`;
|
|
194
|
+
if (emittedIssueKeys.has(key)) return;
|
|
195
|
+
emittedIssueKeys.add(key);
|
|
196
|
+
snapshotIssueHandler?.(issue);
|
|
197
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
198
|
+
console.warn(issue.message);
|
|
128
199
|
}
|
|
129
200
|
}
|
|
130
201
|
function setupPrefetch(doc, strategy) {
|
|
@@ -262,9 +333,17 @@ async function handleResumableEventAsync(event) {
|
|
|
262
333
|
const scopeId = host.getAttribute("data-fict-s");
|
|
263
334
|
if (!scopeId) continue;
|
|
264
335
|
const snapshot = __fictGetSSRScope(scopeId);
|
|
265
|
-
if (snapshot) {
|
|
266
|
-
|
|
336
|
+
if (!snapshot) {
|
|
337
|
+
emitSnapshotIssue({
|
|
338
|
+
code: "scope_snapshot_missing",
|
|
339
|
+
message: `[fict/loader] Missing scope snapshot for ${scopeId}; skipping resumable handler execution.`,
|
|
340
|
+
source: "event",
|
|
341
|
+
expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
|
|
342
|
+
scopeId
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
267
345
|
}
|
|
346
|
+
__fictEnsureScope(scopeId, host, snapshot);
|
|
268
347
|
const { url, exportName } = parseQrl(qrl);
|
|
269
348
|
if (event.cancelable && (event.type === "click" || event.type === "submit")) {
|
|
270
349
|
const tag = node.tagName.toLowerCase();
|
package/dist/loader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loader.ts"],"sourcesContent":["import { DelegatedEvents } from './constants'\nimport {\n __fictEnableResumable,\n __fictEnsureScope,\n __fictGetResume,\n __fictGetSSRScope,\n __fictMergeSSRState,\n __fictSetSSRState,\n __fictUseLexicalScope,\n} from './resume'\n\n// ============================================================================\n// Module Resolution\n// ============================================================================\n\n/**\n * Resolve a module URL through the manifest if available.\n * In production, virtual module URLs (virtual:fict-handler:...) are mapped\n * to their built chunk URLs through the manifest.\n */\nfunction resolveModuleUrl(url: string): string {\n const manifest = (globalThis as Record<string, unknown>).__FICT_MANIFEST__ as\n | Record<string, string>\n | undefined\n\n if (manifest) {\n // Check if the URL (without #fragment) is in the manifest\n const resolved = manifest[url]\n if (resolved) {\n return resolved\n }\n }\n\n return url\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface PrefetchStrategy {\n /**\n * Enable visibility-based prefetch using IntersectionObserver.\n * Prefetches modules when interactive elements come into view.\n * @default true\n */\n visibility?: boolean\n /**\n * Root margin for IntersectionObserver (e.g., '200px' to prefetch earlier).\n * @default '200px'\n */\n visibilityMargin?: string\n /**\n * Enable hover-based prefetch using pointerover events.\n * Prefetches modules when user hovers over interactive elements.\n * @default true\n */\n hover?: boolean\n /**\n * Delay in ms before prefetching on hover (debounce rapid movements).\n * @default 50\n */\n hoverDelay?: number\n}\n\nexport interface ResumableLoaderOptions {\n document?: Document\n snapshotScriptId?: string\n events?: string[]\n /**\n * Prefetch strategy configuration.\n * Set to false to disable all prefetching.\n * @default { visibility: true, hover: true }\n */\n prefetch?: PrefetchStrategy | false\n}\n\n// ============================================================================\n// State\n// ============================================================================\n\nconst hydratedScopes = new Set<string>()\nconst prefetchedUrls = new Set<string>()\nlet prefetchCleanup: (() => void) | null = null\nlet eventListenerCleanup: (() => void) | null = null\nlet snapshotObserver: MutationObserver | null = null\nconst processedSnapshots = new Set<HTMLScriptElement>()\n\n/**\n * Reset the hydrated scopes set. Useful for testing.\n */\nexport function resetHydratedScopes(): void {\n hydratedScopes.clear()\n}\n\n/**\n * Reset the prefetched URLs set. Useful for testing.\n */\nexport function resetPrefetchedUrls(): void {\n prefetchedUrls.clear()\n}\n\n/**\n * Set of pending handler promises. Used for testing to wait for all handlers to complete.\n */\nconst pendingHandlers = new Set<Promise<void>>()\n\n/**\n * Wait for all pending event handlers to complete. Useful for testing.\n */\nexport async function waitForPendingHandlers(): Promise<void> {\n if (pendingHandlers.size === 0) return\n await Promise.allSettled([...pendingHandlers])\n}\n\n/**\n * Clean up all registered event listeners. Useful for testing.\n */\nexport function cleanupEventListeners(): void {\n if (eventListenerCleanup) {\n eventListenerCleanup()\n eventListenerCleanup = null\n }\n}\n\n// ============================================================================\n// Main Entry Point\n// ============================================================================\n\nexport function installResumableLoader(options: ResumableLoaderOptions = {}): void {\n const doc = options.document ?? window.document\n const scriptId = options.snapshotScriptId ?? '__FICT_SNAPSHOT__'\n\n // Reset hydrated scopes for fresh loader installation\n hydratedScopes.clear()\n prefetchedUrls.clear()\n processedSnapshots.clear()\n\n // Clean up previous event listeners\n if (eventListenerCleanup) {\n eventListenerCleanup()\n eventListenerCleanup = null\n }\n\n // Clean up previous prefetch handlers\n if (prefetchCleanup) {\n prefetchCleanup()\n prefetchCleanup = null\n }\n\n if (snapshotObserver) {\n snapshotObserver.disconnect()\n snapshotObserver = null\n }\n\n const snapshotEl = doc.getElementById(scriptId)\n if (snapshotEl?.textContent) {\n try {\n const state = JSON.parse(snapshotEl.textContent)\n __fictSetSSRState(state)\n } catch {\n // Ignore parse errors\n }\n }\n\n const snapshotScripts = doc.querySelectorAll(\n 'script[type=\"application/json\"][data-fict-snapshot]',\n )\n for (const script of Array.from(snapshotScripts)) {\n parseSnapshotScript(script as HTMLScriptElement)\n }\n\n if (typeof MutationObserver !== 'undefined') {\n snapshotObserver = new MutationObserver(mutations => {\n for (const mutation of mutations) {\n for (const node of Array.from(mutation.addedNodes)) {\n if (!(node instanceof Element)) continue\n if (node.tagName === 'SCRIPT') {\n const script = node as HTMLScriptElement\n if (isSnapshotScript(script)) {\n parseSnapshotScript(script)\n }\n }\n const nested = node.querySelectorAll?.(\n 'script[type=\"application/json\"][data-fict-snapshot]',\n )\n if (nested && nested.length) {\n for (const script of Array.from(nested)) {\n parseSnapshotScript(script as HTMLScriptElement)\n }\n }\n }\n }\n })\n snapshotObserver.observe(doc.documentElement ?? doc, { childList: true, subtree: true })\n }\n\n __fictEnableResumable()\n\n const events = options.events ?? Array.from(DelegatedEvents)\n for (const eventName of events) {\n doc.addEventListener(eventName, handleResumableEvent, true)\n }\n\n // Store cleanup function for event listeners\n eventListenerCleanup = () => {\n for (const eventName of events) {\n doc.removeEventListener(eventName, handleResumableEvent, true)\n }\n }\n\n // Setup prefetch if enabled\n if (options.prefetch !== false) {\n prefetchCleanup = setupPrefetch(doc, options.prefetch ?? {})\n }\n}\n\nfunction isSnapshotScript(script: HTMLScriptElement): boolean {\n return script.type === 'application/json' && script.hasAttribute('data-fict-snapshot')\n}\n\nfunction parseSnapshotScript(script: HTMLScriptElement): void {\n if (processedSnapshots.has(script)) return\n processedSnapshots.add(script)\n const text = script.textContent\n if (!text) return\n try {\n const state = JSON.parse(text)\n __fictMergeSSRState(state)\n } catch {\n // Ignore parse errors\n }\n}\n\n// ============================================================================\n// Prefetch Implementation\n// ============================================================================\n\nfunction setupPrefetch(doc: Document, strategy: PrefetchStrategy): () => void {\n const cleanupFns: (() => void)[] = []\n\n // Visibility-based prefetch\n if (strategy.visibility !== false) {\n const cleanup = setupVisibilityPrefetch(doc, strategy.visibilityMargin ?? '200px')\n cleanupFns.push(cleanup)\n }\n\n // Hover-based prefetch\n if (strategy.hover !== false) {\n const cleanup = setupHoverPrefetch(doc, strategy.hoverDelay ?? 50)\n cleanupFns.push(cleanup)\n }\n\n return () => {\n for (const cleanup of cleanupFns) {\n cleanup()\n }\n }\n}\n\nfunction setupVisibilityPrefetch(doc: Document, rootMargin: string): () => void {\n // Check if IntersectionObserver is available\n if (typeof IntersectionObserver === 'undefined') {\n return () => {}\n }\n\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const el = entry.target as Element\n prefetchElementQrls(el)\n // Stop observing after prefetch\n observer.unobserve(el)\n }\n }\n },\n { rootMargin },\n )\n\n // Observe all elements with on:* attributes\n const interactiveElements = doc.querySelectorAll(\n '[on\\\\:click], [on\\\\:input], [on\\\\:change], [on\\\\:submit], [on\\\\:keydown], [on\\\\:keyup]',\n )\n interactiveElements.forEach(el => observer.observe(el))\n\n // Also observe elements with data-fict-h (resumable components)\n const resumableHosts = doc.querySelectorAll('[data-fict-h]')\n resumableHosts.forEach(el => observer.observe(el))\n\n return () => {\n observer.disconnect()\n }\n}\n\nfunction setupHoverPrefetch(doc: Document, delay: number): () => void {\n let hoverTimeout: ReturnType<typeof setTimeout> | null = null\n let lastHoveredElement: Element | null = null\n\n const handlePointerOver = (event: Event) => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n // Find the closest element with interactive attributes\n const interactiveEl =\n target.closest('[on\\\\:click]') ||\n target.closest('[on\\\\:input]') ||\n target.closest('[on\\\\:change]') ||\n target.closest('[on\\\\:submit]') ||\n target.closest('[data-fict-h]')\n\n if (!interactiveEl || interactiveEl === lastHoveredElement) return\n\n lastHoveredElement = interactiveEl\n\n // Clear previous timeout\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n }\n\n // Debounce prefetch\n hoverTimeout = setTimeout(() => {\n prefetchElementQrls(interactiveEl)\n }, delay)\n }\n\n const handlePointerOut = () => {\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n hoverTimeout = null\n }\n lastHoveredElement = null\n }\n\n doc.addEventListener('pointerover', handlePointerOver, { passive: true })\n doc.addEventListener('pointerout', handlePointerOut, { passive: true })\n\n return () => {\n doc.removeEventListener('pointerover', handlePointerOver)\n doc.removeEventListener('pointerout', handlePointerOut)\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n }\n }\n}\n\nfunction prefetchElementQrls(el: Element): void {\n // Prefetch event handler QRLs\n const eventAttrs = ['on:click', 'on:input', 'on:change', 'on:submit', 'on:keydown', 'on:keyup']\n for (const attr of eventAttrs) {\n const qrl = el.getAttribute(attr)\n if (qrl) {\n prefetchQrl(qrl)\n }\n }\n\n // Prefetch resume handler QRL\n const resumeQrl = el.getAttribute('data-fict-h')\n if (resumeQrl) {\n prefetchQrl(resumeQrl)\n }\n\n // Also check children for nested QRLs\n const children = el.querySelectorAll(\n '[on\\\\:click], [on\\\\:input], [on\\\\:change], [on\\\\:submit], [data-fict-h]',\n )\n children.forEach(child => {\n for (const attr of eventAttrs) {\n const qrl = child.getAttribute(attr)\n if (qrl) {\n prefetchQrl(qrl)\n }\n }\n const childResumeQrl = child.getAttribute('data-fict-h')\n if (childResumeQrl) {\n prefetchQrl(childResumeQrl)\n }\n })\n}\n\nfunction prefetchQrl(qrl: string): void {\n const { url } = parseQrl(qrl)\n if (!url || prefetchedUrls.has(url)) return\n\n prefetchedUrls.add(url)\n\n // Resolve through manifest for production builds\n const resolvedUrl = resolveModuleUrl(url)\n\n // Use modulepreload link for best browser support\n if (typeof document !== 'undefined') {\n const link = document.createElement('link')\n link.rel = 'modulepreload'\n link.href = resolvedUrl\n link.crossOrigin = 'anonymous'\n document.head.appendChild(link)\n }\n}\n\n// ============================================================================\n\n/**\n * Wrapper that tracks the async handler promise for testing.\n */\nfunction handleResumableEvent(event: Event): void {\n const promise = handleResumableEventAsync(event)\n pendingHandlers.add(promise)\n promise.finally(() => {\n pendingHandlers.delete(promise)\n })\n}\n\nasync function handleResumableEventAsync(event: Event): Promise<void> {\n const path =\n typeof event.composedPath === 'function' ? event.composedPath() : buildEventPath(event)\n\n for (const node of path) {\n if (!(node instanceof Element)) continue\n const qrl = node.getAttribute(`on:${event.type}`)\n if (!qrl) continue\n\n const host = node.closest('[data-fict-s]') as Element | null\n if (!host) continue\n const scopeId = host.getAttribute('data-fict-s')\n if (!scopeId) continue\n\n const snapshot = __fictGetSSRScope(scopeId)\n if (snapshot) {\n __fictEnsureScope(scopeId, host, snapshot)\n }\n\n const { url, exportName } = parseQrl(qrl)\n\n // Pre-emptively prevent default on navigations/forms while we await modules\n if (event.cancelable && (event.type === 'click' || event.type === 'submit')) {\n const tag = node.tagName.toLowerCase()\n if (tag === 'a' || tag === 'form') {\n event.preventDefault()\n }\n }\n\n // Resume FIRST to set up reactive bindings BEFORE the handler runs\n if (!hydratedScopes.has(scopeId)) {\n const resumeQrl = host.getAttribute('data-fict-h')\n if (resumeQrl) {\n const { url: resumeUrl, exportName: resumeExport } = parseQrl(resumeQrl)\n const resolvedResumeUrl = resolveModuleUrl(resumeUrl)\n // Load the module to ensure resume functions are registered\n await import(/* @vite-ignore */ resolvedResumeUrl)\n // Get resume function from registry (not module exports)\n const resumeFn = __fictGetResume(resumeExport)\n if (typeof resumeFn === 'function') {\n await (resumeFn as (scopeId: string, host: Element) => unknown)(scopeId, host)\n hydratedScopes.add(scopeId)\n }\n }\n }\n\n // THEN run the handler - now signal updates will trigger DOM updates\n const resolvedUrl = resolveModuleUrl(url)\n const mod = await import(/* @vite-ignore */ resolvedUrl)\n const handler = (mod as Record<string, unknown>)[exportName]\n if (typeof handler === 'function') {\n await (handler as (scopeId: string, ev: Event, el: Element) => unknown)(scopeId, event, node)\n }\n\n return\n }\n}\n\nfunction parseQrl(qrl: string): { url: string; exportName: string } {\n const [ref] = qrl.split('[')\n if (!ref) {\n return { url: '', exportName: 'default' }\n }\n const hashIndex = ref.lastIndexOf('#')\n if (hashIndex === -1) {\n return { url: ref, exportName: 'default' }\n }\n return { url: ref.slice(0, hashIndex), exportName: ref.slice(hashIndex + 1) }\n}\n\nfunction buildEventPath(event: Event): EventTarget[] {\n const path: EventTarget[] = []\n let node: EventTarget | null = event.target\n while (node) {\n path.push(node)\n node = (node as Node).parentNode\n }\n path.push(window)\n return path\n}\n\n// Re-export for handler authors (optional)\nexport { __fictUseLexicalScope } from './resume'\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/loader.ts"],"sourcesContent":["import { DelegatedEvents } from './constants'\nimport {\n FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n type SSRState,\n __fictEnableResumable,\n __fictEnsureScope,\n __fictGetResume,\n __fictGetSSRScope,\n __fictMergeSSRState,\n __fictSetSSRState,\n __fictUseLexicalScope,\n} from './resume'\n\n// ============================================================================\n// Module Resolution\n// ============================================================================\n\n/**\n * Resolve a module URL through the manifest if available.\n * In production, virtual module URLs (virtual:fict-handler:...) are mapped\n * to their built chunk URLs through the manifest.\n */\nfunction resolveModuleUrl(url: string): string {\n const manifest = (globalThis as Record<string, unknown>).__FICT_MANIFEST__ as\n | Record<string, string>\n | undefined\n\n if (manifest) {\n // Check if the URL (without #fragment) is in the manifest\n const resolved = manifest[url]\n if (resolved) {\n return resolved\n }\n }\n\n return url\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface PrefetchStrategy {\n /**\n * Enable visibility-based prefetch using IntersectionObserver.\n * Prefetches modules when interactive elements come into view.\n * @default true\n */\n visibility?: boolean\n /**\n * Root margin for IntersectionObserver (e.g., '200px' to prefetch earlier).\n * @default '200px'\n */\n visibilityMargin?: string\n /**\n * Enable hover-based prefetch using pointerover events.\n * Prefetches modules when user hovers over interactive elements.\n * @default true\n */\n hover?: boolean\n /**\n * Delay in ms before prefetching on hover (debounce rapid movements).\n * @default 50\n */\n hoverDelay?: number\n}\n\nexport interface ResumableLoaderOptions {\n document?: Document\n snapshotScriptId?: string\n events?: string[]\n /**\n * Receives structured snapshot/resume issues detected by the loader.\n * Useful for telemetry and fail-safe fallback orchestration.\n */\n onSnapshotIssue?: (issue: SnapshotIssue) => void\n /**\n * Prefetch strategy configuration.\n * Set to false to disable all prefetching.\n * @default { visibility: true, hover: true }\n */\n prefetch?: PrefetchStrategy | false\n}\n\nexport type SnapshotIssueCode =\n | 'snapshot_parse_error'\n | 'snapshot_invalid_shape'\n | 'snapshot_unsupported_version'\n | 'scope_snapshot_missing'\n\nexport interface SnapshotIssue {\n code: SnapshotIssueCode\n message: string\n source: string\n expectedVersion: number\n actualVersion?: number\n scopeId?: string\n}\n\n// ============================================================================\n// State\n// ============================================================================\n\nconst hydratedScopes = new Set<string>()\nconst prefetchedUrls = new Set<string>()\nlet prefetchCleanup: (() => void) | null = null\nlet eventListenerCleanup: (() => void) | null = null\nlet snapshotObserver: MutationObserver | null = null\nconst processedSnapshots = new Set<HTMLScriptElement>()\nlet snapshotIssueHandler: ((issue: SnapshotIssue) => void) | null = null\nconst emittedIssueKeys = new Set<string>()\n\n/**\n * Reset the hydrated scopes set. Useful for testing.\n */\nexport function resetHydratedScopes(): void {\n hydratedScopes.clear()\n}\n\n/**\n * Reset the prefetched URLs set. Useful for testing.\n */\nexport function resetPrefetchedUrls(): void {\n prefetchedUrls.clear()\n}\n\n/**\n * Set of pending handler promises. Used for testing to wait for all handlers to complete.\n */\nconst pendingHandlers = new Set<Promise<void>>()\n\n/**\n * Wait for all pending event handlers to complete. Useful for testing.\n */\nexport async function waitForPendingHandlers(): Promise<void> {\n if (pendingHandlers.size === 0) return\n await Promise.allSettled([...pendingHandlers])\n}\n\n/**\n * Clean up all registered event listeners. Useful for testing.\n */\nexport function cleanupEventListeners(): void {\n if (eventListenerCleanup) {\n eventListenerCleanup()\n eventListenerCleanup = null\n }\n}\n\n// ============================================================================\n// Main Entry Point\n// ============================================================================\n\nexport function installResumableLoader(options: ResumableLoaderOptions = {}): void {\n const doc = options.document ?? window.document\n const scriptId = options.snapshotScriptId ?? '__FICT_SNAPSHOT__'\n snapshotIssueHandler = options.onSnapshotIssue ?? null\n\n // Reset hydrated scopes for fresh loader installation\n hydratedScopes.clear()\n prefetchedUrls.clear()\n processedSnapshots.clear()\n emittedIssueKeys.clear()\n __fictSetSSRState(null)\n\n // Clean up previous event listeners\n if (eventListenerCleanup) {\n eventListenerCleanup()\n eventListenerCleanup = null\n }\n\n // Clean up previous prefetch handlers\n if (prefetchCleanup) {\n prefetchCleanup()\n prefetchCleanup = null\n }\n\n if (snapshotObserver) {\n snapshotObserver.disconnect()\n snapshotObserver = null\n }\n\n const snapshotEl = doc.getElementById(scriptId)\n if (snapshotEl?.textContent) {\n const state = parseSnapshotText(snapshotEl.textContent, `#${scriptId}`)\n if (state) {\n __fictSetSSRState(state)\n }\n }\n\n const snapshotScripts = doc.querySelectorAll(\n 'script[type=\"application/json\"][data-fict-snapshot]',\n )\n for (const script of Array.from(snapshotScripts)) {\n parseSnapshotScript(script as HTMLScriptElement)\n }\n\n if (typeof MutationObserver !== 'undefined') {\n snapshotObserver = new MutationObserver(mutations => {\n for (const mutation of mutations) {\n for (const node of Array.from(mutation.addedNodes)) {\n if (!(node instanceof Element)) continue\n if (node.tagName === 'SCRIPT') {\n const script = node as HTMLScriptElement\n if (isSnapshotScript(script)) {\n parseSnapshotScript(script)\n }\n }\n const nested = node.querySelectorAll?.(\n 'script[type=\"application/json\"][data-fict-snapshot]',\n )\n if (nested && nested.length) {\n for (const script of Array.from(nested)) {\n parseSnapshotScript(script as HTMLScriptElement)\n }\n }\n }\n }\n })\n snapshotObserver.observe(doc.documentElement ?? doc, { childList: true, subtree: true })\n }\n\n __fictEnableResumable()\n\n const events = options.events ?? Array.from(DelegatedEvents)\n for (const eventName of events) {\n doc.addEventListener(eventName, handleResumableEvent, true)\n }\n\n // Store cleanup function for event listeners\n eventListenerCleanup = () => {\n for (const eventName of events) {\n doc.removeEventListener(eventName, handleResumableEvent, true)\n }\n }\n\n // Setup prefetch if enabled\n if (options.prefetch !== false) {\n prefetchCleanup = setupPrefetch(doc, options.prefetch ?? {})\n }\n}\n\nfunction isSnapshotScript(script: HTMLScriptElement): boolean {\n return script.type === 'application/json' && script.hasAttribute('data-fict-snapshot')\n}\n\nfunction parseSnapshotScript(script: HTMLScriptElement): void {\n if (processedSnapshots.has(script)) return\n processedSnapshots.add(script)\n const text = script.textContent\n if (!text) return\n const source = script.id ? `#${script.id}` : '<script[data-fict-snapshot]>'\n const state = parseSnapshotText(text, source)\n if (state) {\n __fictMergeSSRState(state)\n }\n}\n\nfunction parseSnapshotText(text: string, source: string): SSRState | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n emitSnapshotIssue({\n code: 'snapshot_parse_error',\n message: '[fict/loader] Failed to parse SSR snapshot JSON.',\n source,\n expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n })\n return null\n }\n\n return normalizeSnapshotState(parsed, source)\n}\n\nfunction normalizeSnapshotState(value: unknown, source: string): SSRState | null {\n if (!isRecord(value)) {\n emitSnapshotIssue({\n code: 'snapshot_invalid_shape',\n message: '[fict/loader] Snapshot payload must be an object.',\n source,\n expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n })\n return null\n }\n\n const rawVersion = value.v\n const version = rawVersion === undefined ? FICT_SSR_SNAPSHOT_SCHEMA_VERSION : rawVersion\n if (!Number.isInteger(version) || version !== FICT_SSR_SNAPSHOT_SCHEMA_VERSION) {\n const versionIssue: SnapshotIssue = {\n code: 'snapshot_unsupported_version',\n message: `[fict/loader] Snapshot schema version ${String(version)} is not supported by this runtime.`,\n source,\n expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n }\n if (typeof version === 'number') {\n versionIssue.actualVersion = version\n }\n emitSnapshotIssue({\n ...versionIssue,\n })\n return null\n }\n\n const scopes = value.scopes\n if (!isRecord(scopes)) {\n emitSnapshotIssue({\n code: 'snapshot_invalid_shape',\n message: '[fict/loader] Snapshot payload is missing a valid `scopes` object.',\n source,\n expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n })\n return null\n }\n\n return { v: FICT_SSR_SNAPSHOT_SCHEMA_VERSION, scopes: scopes as SSRState['scopes'] }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction emitSnapshotIssue(issue: SnapshotIssue): void {\n const key =\n `${issue.code}|${issue.source}|${issue.scopeId ?? ''}|` +\n `${issue.actualVersion ?? ''}|${issue.expectedVersion}`\n if (emittedIssueKeys.has(key)) return\n emittedIssueKeys.add(key)\n\n snapshotIssueHandler?.(issue)\n\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(issue.message)\n }\n}\n\n// ============================================================================\n// Prefetch Implementation\n// ============================================================================\n\nfunction setupPrefetch(doc: Document, strategy: PrefetchStrategy): () => void {\n const cleanupFns: (() => void)[] = []\n\n // Visibility-based prefetch\n if (strategy.visibility !== false) {\n const cleanup = setupVisibilityPrefetch(doc, strategy.visibilityMargin ?? '200px')\n cleanupFns.push(cleanup)\n }\n\n // Hover-based prefetch\n if (strategy.hover !== false) {\n const cleanup = setupHoverPrefetch(doc, strategy.hoverDelay ?? 50)\n cleanupFns.push(cleanup)\n }\n\n return () => {\n for (const cleanup of cleanupFns) {\n cleanup()\n }\n }\n}\n\nfunction setupVisibilityPrefetch(doc: Document, rootMargin: string): () => void {\n // Check if IntersectionObserver is available\n if (typeof IntersectionObserver === 'undefined') {\n return () => {}\n }\n\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const el = entry.target as Element\n prefetchElementQrls(el)\n // Stop observing after prefetch\n observer.unobserve(el)\n }\n }\n },\n { rootMargin },\n )\n\n // Observe all elements with on:* attributes\n const interactiveElements = doc.querySelectorAll(\n '[on\\\\:click], [on\\\\:input], [on\\\\:change], [on\\\\:submit], [on\\\\:keydown], [on\\\\:keyup]',\n )\n interactiveElements.forEach(el => observer.observe(el))\n\n // Also observe elements with data-fict-h (resumable components)\n const resumableHosts = doc.querySelectorAll('[data-fict-h]')\n resumableHosts.forEach(el => observer.observe(el))\n\n return () => {\n observer.disconnect()\n }\n}\n\nfunction setupHoverPrefetch(doc: Document, delay: number): () => void {\n let hoverTimeout: ReturnType<typeof setTimeout> | null = null\n let lastHoveredElement: Element | null = null\n\n const handlePointerOver = (event: Event) => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n // Find the closest element with interactive attributes\n const interactiveEl =\n target.closest('[on\\\\:click]') ||\n target.closest('[on\\\\:input]') ||\n target.closest('[on\\\\:change]') ||\n target.closest('[on\\\\:submit]') ||\n target.closest('[data-fict-h]')\n\n if (!interactiveEl || interactiveEl === lastHoveredElement) return\n\n lastHoveredElement = interactiveEl\n\n // Clear previous timeout\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n }\n\n // Debounce prefetch\n hoverTimeout = setTimeout(() => {\n prefetchElementQrls(interactiveEl)\n }, delay)\n }\n\n const handlePointerOut = () => {\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n hoverTimeout = null\n }\n lastHoveredElement = null\n }\n\n doc.addEventListener('pointerover', handlePointerOver, { passive: true })\n doc.addEventListener('pointerout', handlePointerOut, { passive: true })\n\n return () => {\n doc.removeEventListener('pointerover', handlePointerOver)\n doc.removeEventListener('pointerout', handlePointerOut)\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n }\n }\n}\n\nfunction prefetchElementQrls(el: Element): void {\n // Prefetch event handler QRLs\n const eventAttrs = ['on:click', 'on:input', 'on:change', 'on:submit', 'on:keydown', 'on:keyup']\n for (const attr of eventAttrs) {\n const qrl = el.getAttribute(attr)\n if (qrl) {\n prefetchQrl(qrl)\n }\n }\n\n // Prefetch resume handler QRL\n const resumeQrl = el.getAttribute('data-fict-h')\n if (resumeQrl) {\n prefetchQrl(resumeQrl)\n }\n\n // Also check children for nested QRLs\n const children = el.querySelectorAll(\n '[on\\\\:click], [on\\\\:input], [on\\\\:change], [on\\\\:submit], [data-fict-h]',\n )\n children.forEach(child => {\n for (const attr of eventAttrs) {\n const qrl = child.getAttribute(attr)\n if (qrl) {\n prefetchQrl(qrl)\n }\n }\n const childResumeQrl = child.getAttribute('data-fict-h')\n if (childResumeQrl) {\n prefetchQrl(childResumeQrl)\n }\n })\n}\n\nfunction prefetchQrl(qrl: string): void {\n const { url } = parseQrl(qrl)\n if (!url || prefetchedUrls.has(url)) return\n\n prefetchedUrls.add(url)\n\n // Resolve through manifest for production builds\n const resolvedUrl = resolveModuleUrl(url)\n\n // Use modulepreload link for best browser support\n if (typeof document !== 'undefined') {\n const link = document.createElement('link')\n link.rel = 'modulepreload'\n link.href = resolvedUrl\n link.crossOrigin = 'anonymous'\n document.head.appendChild(link)\n }\n}\n\n// ============================================================================\n\n/**\n * Wrapper that tracks the async handler promise for testing.\n */\nfunction handleResumableEvent(event: Event): void {\n const promise = handleResumableEventAsync(event)\n pendingHandlers.add(promise)\n promise.finally(() => {\n pendingHandlers.delete(promise)\n })\n}\n\nasync function handleResumableEventAsync(event: Event): Promise<void> {\n const path =\n typeof event.composedPath === 'function' ? event.composedPath() : buildEventPath(event)\n\n for (const node of path) {\n if (!(node instanceof Element)) continue\n const qrl = node.getAttribute(`on:${event.type}`)\n if (!qrl) continue\n\n const host = node.closest('[data-fict-s]') as Element | null\n if (!host) continue\n const scopeId = host.getAttribute('data-fict-s')\n if (!scopeId) continue\n\n const snapshot = __fictGetSSRScope(scopeId)\n if (!snapshot) {\n emitSnapshotIssue({\n code: 'scope_snapshot_missing',\n message: `[fict/loader] Missing scope snapshot for ${scopeId}; skipping resumable handler execution.`,\n source: 'event',\n expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,\n scopeId,\n })\n return\n }\n __fictEnsureScope(scopeId, host, snapshot)\n\n const { url, exportName } = parseQrl(qrl)\n\n // Pre-emptively prevent default on navigations/forms while we await modules\n if (event.cancelable && (event.type === 'click' || event.type === 'submit')) {\n const tag = node.tagName.toLowerCase()\n if (tag === 'a' || tag === 'form') {\n event.preventDefault()\n }\n }\n\n // Resume FIRST to set up reactive bindings BEFORE the handler runs\n if (!hydratedScopes.has(scopeId)) {\n const resumeQrl = host.getAttribute('data-fict-h')\n if (resumeQrl) {\n const { url: resumeUrl, exportName: resumeExport } = parseQrl(resumeQrl)\n const resolvedResumeUrl = resolveModuleUrl(resumeUrl)\n // Load the module to ensure resume functions are registered\n await import(/* @vite-ignore */ resolvedResumeUrl)\n // Get resume function from registry (not module exports)\n const resumeFn = __fictGetResume(resumeExport)\n if (typeof resumeFn === 'function') {\n await (resumeFn as (scopeId: string, host: Element) => unknown)(scopeId, host)\n hydratedScopes.add(scopeId)\n }\n }\n }\n\n // THEN run the handler - now signal updates will trigger DOM updates\n const resolvedUrl = resolveModuleUrl(url)\n const mod = await import(/* @vite-ignore */ resolvedUrl)\n const handler = (mod as Record<string, unknown>)[exportName]\n if (typeof handler === 'function') {\n await (handler as (scopeId: string, ev: Event, el: Element) => unknown)(scopeId, event, node)\n }\n\n return\n }\n}\n\nfunction parseQrl(qrl: string): { url: string; exportName: string } {\n const [ref] = qrl.split('[')\n if (!ref) {\n return { url: '', exportName: 'default' }\n }\n const hashIndex = ref.lastIndexOf('#')\n if (hashIndex === -1) {\n return { url: ref, exportName: 'default' }\n }\n return { url: ref.slice(0, hashIndex), exportName: ref.slice(hashIndex + 1) }\n}\n\nfunction buildEventPath(event: Event): EventTarget[] {\n const path: EventTarget[] = []\n let node: EventTarget | null = event.target\n while (node) {\n path.push(node)\n node = (node as Node).parentNode\n }\n path.push(window)\n return path\n}\n\n// Re-export for handler authors (optional)\nexport { __fictUseLexicalScope } from './resume'\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as MemoOptions } from './signal-C4ISF17w.cjs';
|
|
2
|
-
import { F as FictNode,
|
|
2
|
+
import { F as FictNode, R as DOMElement } from './binding-BWchH3Kp.cjs';
|
|
3
3
|
|
|
4
4
|
type Memo<T> = () => T;
|
|
5
5
|
declare function createMemo<T>(fn: () => T, options?: MemoOptions<T>): Memo<T>;
|
|
@@ -768,4 +768,4 @@ declare function keyed<T, K extends string | number | symbol>(target: T | PropGe
|
|
|
768
768
|
*/
|
|
769
769
|
declare function prop<T>(getter: () => T, options?: PropOptions): PropGetter<T>;
|
|
770
770
|
|
|
771
|
-
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _,
|
|
771
|
+
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _, __fictPropsRest as a, createPropsProxy as b, createMemo as c, createElement as d, hydrateComponent as h, keyed as k, mergeProps as m, prop as p, render as r, template as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as MemoOptions } from './signal-C4ISF17w.js';
|
|
2
|
-
import { F as FictNode,
|
|
2
|
+
import { F as FictNode, R as DOMElement } from './binding-BWchH3Kp.js';
|
|
3
3
|
|
|
4
4
|
type Memo<T> = () => T;
|
|
5
5
|
declare function createMemo<T>(fn: () => T, options?: MemoOptions<T>): Memo<T>;
|
|
@@ -768,4 +768,4 @@ declare function keyed<T, K extends string | number | symbol>(target: T | PropGe
|
|
|
768
768
|
*/
|
|
769
769
|
declare function prop<T>(getter: () => T, options?: PropOptions): PropGetter<T>;
|
|
770
770
|
|
|
771
|
-
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _,
|
|
771
|
+
export { Fragment as F, JSX as J, type Memo as M, __fictProp as _, __fictPropsRest as a, createPropsProxy as b, createMemo as c, createElement as d, hydrateComponent as h, keyed as k, mergeProps as m, prop as p, render as r, template as t };
|
|
@@ -28,7 +28,9 @@ interface ScopeSnapshot {
|
|
|
28
28
|
props?: Record<string, unknown>;
|
|
29
29
|
vars?: Record<string, number>;
|
|
30
30
|
}
|
|
31
|
+
declare const FICT_SSR_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
31
32
|
interface SSRState {
|
|
33
|
+
v: number;
|
|
32
34
|
scopes: Record<string, ScopeSnapshot>;
|
|
33
35
|
}
|
|
34
36
|
interface ScopeRecord {
|
|
@@ -80,4 +82,4 @@ declare function serializeValue(value: unknown, seen?: Map<object, string>, path
|
|
|
80
82
|
*/
|
|
81
83
|
declare function deserializeValue(value: unknown, refs?: Map<string, unknown>, path?: string): unknown;
|
|
82
84
|
|
|
83
|
-
export {
|
|
85
|
+
export { __fictEnsureScope as A, __fictGetScopeProps as B, __fictQrl as C, __fictRegisterResume as D, __fictGetResume as E, FICT_SSR_SNAPSHOT_SCHEMA_VERSION as F, serializeValue as G, deserializeValue as H, __fictUseLexicalScope as _, __fictUseContext as a, __fictPushContext as b, __fictPopContext as c, __fictUseSignal as d, __fictUseMemo as e, __fictUseEffect as f, __fictRender as g, __fictResetContext as h, __fictPrepareContext as i, __fictEnableSSR as j, __fictDisableSSR as k, __fictIsSSR as l, __fictEnableResumable as m, __fictDisableResumable as n, __fictIsResumable as o, __fictEnterHydration as p, __fictExitHydration as q, __fictIsHydrating as r, __fictRegisterScope as s, __fictGetScopeRegistry as t, __fictGetScopesForBoundary as u, __fictSerializeSSRState as v, __fictSerializeSSRStateForScopes as w, __fictSetSSRState as x, __fictMergeSSRState as y, __fictGetSSRScope as z };
|
|
@@ -28,7 +28,9 @@ interface ScopeSnapshot {
|
|
|
28
28
|
props?: Record<string, unknown>;
|
|
29
29
|
vars?: Record<string, number>;
|
|
30
30
|
}
|
|
31
|
+
declare const FICT_SSR_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
31
32
|
interface SSRState {
|
|
33
|
+
v: number;
|
|
32
34
|
scopes: Record<string, ScopeSnapshot>;
|
|
33
35
|
}
|
|
34
36
|
interface ScopeRecord {
|
|
@@ -80,4 +82,4 @@ declare function serializeValue(value: unknown, seen?: Map<object, string>, path
|
|
|
80
82
|
*/
|
|
81
83
|
declare function deserializeValue(value: unknown, refs?: Map<string, unknown>, path?: string): unknown;
|
|
82
84
|
|
|
83
|
-
export {
|
|
85
|
+
export { __fictEnsureScope as A, __fictGetScopeProps as B, __fictQrl as C, __fictRegisterResume as D, __fictGetResume as E, FICT_SSR_SNAPSHOT_SCHEMA_VERSION as F, serializeValue as G, deserializeValue as H, __fictUseLexicalScope as _, __fictUseContext as a, __fictPushContext as b, __fictPopContext as c, __fictUseSignal as d, __fictUseMemo as e, __fictUseEffect as f, __fictRender as g, __fictResetContext as h, __fictPrepareContext as i, __fictEnableSSR as j, __fictDisableSSR as k, __fictIsSSR as l, __fictEnableResumable as m, __fictDisableResumable as n, __fictIsResumable as o, __fictEnterHydration as p, __fictExitHydration as q, __fictIsHydrating as r, __fictRegisterScope as s, __fictGetScopeRegistry as t, __fictGetScopesForBoundary as u, __fictSerializeSSRState as v, __fictSerializeSSRStateForScopes as w, __fictSetSSRState as x, __fictMergeSSRState as y, __fictGetSSRScope as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fictjs/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Fict reactive runtime",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/internal.js",
|
|
34
34
|
"require": "./dist/internal.cjs"
|
|
35
35
|
},
|
|
36
|
+
"./internal/list": {
|
|
37
|
+
"types": "./dist/internal-list.d.ts",
|
|
38
|
+
"import": "./dist/internal-list.js",
|
|
39
|
+
"require": "./dist/internal-list.cjs"
|
|
40
|
+
},
|
|
36
41
|
"./advanced": {
|
|
37
42
|
"types": "./dist/advanced.d.ts",
|
|
38
43
|
"import": "./dist/advanced.js",
|
|
@@ -68,6 +73,8 @@
|
|
|
68
73
|
"build": "tsup",
|
|
69
74
|
"dev": "tsup --watch",
|
|
70
75
|
"test": "vitest run",
|
|
76
|
+
"test:stress": "FICT_RUNTIME_STRESS=1 vitest run test/runtime-stability.stress.test.ts",
|
|
77
|
+
"test:stress:long": "FICT_RUNTIME_STRESS=1 FICT_RUNTIME_SOAK_ITERS=12000 FICT_RUNTIME_CHURN_CYCLES=500 FICT_RUNTIME_LEAK_ROUNDS=120 FICT_RUNTIME_LEAK_EFFECTS_PER_ROUND=200 FICT_RUNTIME_BACKPRESSURE_UPDATES=16000 FICT_RUNTIME_MAX_DRAIN_LATENCY_MS=2500 vitest run test/runtime-stability.stress.test.ts",
|
|
71
78
|
"test:watch": "vitest",
|
|
72
79
|
"test:coverage": "vitest run --coverage",
|
|
73
80
|
"bench": "vitest bench --run",
|
package/src/binding.ts
CHANGED
|
@@ -112,6 +112,16 @@ export interface BindingHandle {
|
|
|
112
112
|
dispose: Cleanup
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export interface ConditionalBindingOptions {
|
|
116
|
+
/**
|
|
117
|
+
* When true, track signal reads inside active branch render callbacks and
|
|
118
|
+
* re-run the branch callback on updates even if the top-level condition stays
|
|
119
|
+
* the same. This preserves reactivity for control-flow callbacks that cannot
|
|
120
|
+
* be lowered into fine-grained bindings.
|
|
121
|
+
*/
|
|
122
|
+
trackBranchReads?: boolean
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
/** Managed child node with its dispose function */
|
|
116
126
|
// ============================================================================
|
|
117
127
|
// Utility Functions
|
|
@@ -1743,7 +1753,9 @@ export function createConditional(
|
|
|
1743
1753
|
renderFalse?: () => FictNode,
|
|
1744
1754
|
startOverride?: Comment,
|
|
1745
1755
|
endOverride?: Comment,
|
|
1756
|
+
options?: ConditionalBindingOptions,
|
|
1746
1757
|
): BindingHandle {
|
|
1758
|
+
const trackBranchReads = options?.trackBranchReads === true
|
|
1747
1759
|
const useProvided = !!(startOverride && endOverride)
|
|
1748
1760
|
const startMarker = useProvided ? startOverride! : document.createComment('fict:cond:start')
|
|
1749
1761
|
const endMarker = useProvided ? endOverride! : document.createComment('fict:cond:end')
|
|
@@ -1805,7 +1817,7 @@ export function createConditional(
|
|
|
1805
1817
|
endMarker,
|
|
1806
1818
|
parent.ownerDocument ?? document,
|
|
1807
1819
|
() => {
|
|
1808
|
-
const output = untrack(render)
|
|
1820
|
+
const output = trackBranchReads ? render() : untrack(render)
|
|
1809
1821
|
if (output == null || output === false) {
|
|
1810
1822
|
return
|
|
1811
1823
|
}
|
|
@@ -1837,10 +1849,102 @@ export function createConditional(
|
|
|
1837
1849
|
return
|
|
1838
1850
|
}
|
|
1839
1851
|
|
|
1840
|
-
if (
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1852
|
+
if (!trackBranchReads) {
|
|
1853
|
+
if (lastCondition === cond && currentNodes.length > 0) {
|
|
1854
|
+
return
|
|
1855
|
+
}
|
|
1856
|
+
if (lastCondition === cond && lastCondition === false && renderFalse === undefined) {
|
|
1857
|
+
return
|
|
1858
|
+
}
|
|
1859
|
+
} else if (lastCondition === cond) {
|
|
1860
|
+
const render = cond ? renderTrue : renderFalse
|
|
1861
|
+
if (!render) {
|
|
1862
|
+
return
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
let patched = false
|
|
1866
|
+
const scratchRoot = createRootContext(hostRoot)
|
|
1867
|
+
const prevScratch = pushRoot(scratchRoot)
|
|
1868
|
+
let handledPatchError = false
|
|
1869
|
+
let scratchOutput: FictNode = null
|
|
1870
|
+
try {
|
|
1871
|
+
const output = render()
|
|
1872
|
+
scratchOutput = output
|
|
1873
|
+
if (output != null && output !== false) {
|
|
1874
|
+
if (currentNodes.length === 1) {
|
|
1875
|
+
patched = patchNode(currentNodes[0] ?? null, output)
|
|
1876
|
+
}
|
|
1877
|
+
if (!patched && Array.isArray(output)) {
|
|
1878
|
+
patched = _patchFragmentChildren(currentNodes, output)
|
|
1879
|
+
}
|
|
1880
|
+
if (!patched && _isFragmentVNode(output)) {
|
|
1881
|
+
patched = _patchFragmentChildren(currentNodes, output.props?.children)
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
if (handleSuspend(err as any, scratchRoot)) {
|
|
1886
|
+
handledPatchError = true
|
|
1887
|
+
return
|
|
1888
|
+
}
|
|
1889
|
+
if (handleError(err, { source: 'renderChild' }, scratchRoot)) {
|
|
1890
|
+
handledPatchError = true
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
throw err
|
|
1894
|
+
} finally {
|
|
1895
|
+
popRoot(prevScratch)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (handledPatchError) {
|
|
1899
|
+
destroyRoot(scratchRoot)
|
|
1900
|
+
return
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (patched) {
|
|
1904
|
+
destroyRoot(scratchRoot)
|
|
1905
|
+
return
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
lastCondition = cond
|
|
1909
|
+
|
|
1910
|
+
if (currentRoot) {
|
|
1911
|
+
destroyRoot(currentRoot)
|
|
1912
|
+
currentRoot = null
|
|
1913
|
+
}
|
|
1914
|
+
removeNodes(currentNodes)
|
|
1915
|
+
currentNodes = []
|
|
1916
|
+
|
|
1917
|
+
const prev = pushRoot(scratchRoot)
|
|
1918
|
+
let handledError = false
|
|
1919
|
+
try {
|
|
1920
|
+
if (scratchOutput == null || scratchOutput === false) {
|
|
1921
|
+
return
|
|
1922
|
+
}
|
|
1923
|
+
const el = createElementFn(scratchOutput)
|
|
1924
|
+
const nodes = toNodeArray(el)
|
|
1925
|
+
insertNodesBefore(parent, nodes, endMarker)
|
|
1926
|
+
currentNodes = nodes
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
if (handleSuspend(err as any, scratchRoot)) {
|
|
1929
|
+
handledError = true
|
|
1930
|
+
destroyRoot(scratchRoot)
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
if (handleError(err, { source: 'renderChild' }, scratchRoot)) {
|
|
1934
|
+
handledError = true
|
|
1935
|
+
destroyRoot(scratchRoot)
|
|
1936
|
+
return
|
|
1937
|
+
}
|
|
1938
|
+
throw err
|
|
1939
|
+
} finally {
|
|
1940
|
+
popRoot(prev)
|
|
1941
|
+
if (!handledError) {
|
|
1942
|
+
flushOnMount(scratchRoot)
|
|
1943
|
+
currentRoot = scratchRoot
|
|
1944
|
+
} else {
|
|
1945
|
+
currentRoot = null
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1844
1948
|
return
|
|
1845
1949
|
}
|
|
1846
1950
|
lastCondition = cond
|
|
@@ -1865,7 +1969,7 @@ export function createConditional(
|
|
|
1865
1969
|
// tracked by createConditional's effect. This ensures that signals used
|
|
1866
1970
|
// inside the render function (e.g., nested if conditions) don't cause
|
|
1867
1971
|
// createConditional to re-run, which would purge child effect deps.
|
|
1868
|
-
const output = untrack(render)
|
|
1972
|
+
const output = trackBranchReads ? render() : untrack(render)
|
|
1869
1973
|
if (output == null || output === false) {
|
|
1870
1974
|
return
|
|
1871
1975
|
}
|
|
@@ -2050,23 +2154,6 @@ export function createPortal(
|
|
|
2050
2154
|
}
|
|
2051
2155
|
|
|
2052
2156
|
function patchElement(el: Element, output: FictNode): boolean {
|
|
2053
|
-
if (
|
|
2054
|
-
output === null ||
|
|
2055
|
-
output === undefined ||
|
|
2056
|
-
output === false ||
|
|
2057
|
-
typeof output === 'string' ||
|
|
2058
|
-
typeof output === 'number'
|
|
2059
|
-
) {
|
|
2060
|
-
el.textContent =
|
|
2061
|
-
output === null || output === undefined || output === false ? '' : String(output)
|
|
2062
|
-
return true
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (output instanceof Text) {
|
|
2066
|
-
el.textContent = output.data
|
|
2067
|
-
return true
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
2157
|
if (output && typeof output === 'object' && !(output instanceof Node)) {
|
|
2071
2158
|
const vnode = output as { type?: unknown; props?: Record<string, unknown> }
|
|
2072
2159
|
if (typeof vnode.type === 'string' && vnode.type.toLowerCase() === el.tagName.toLowerCase()) {
|
|
@@ -2131,19 +2218,6 @@ function patchElement(el: Element, output: FictNode): boolean {
|
|
|
2131
2218
|
}
|
|
2132
2219
|
}
|
|
2133
2220
|
|
|
2134
|
-
if (output instanceof Node) {
|
|
2135
|
-
if (output.nodeType === Node.ELEMENT_NODE) {
|
|
2136
|
-
const nextEl = output as Element
|
|
2137
|
-
if (nextEl.tagName.toLowerCase() === el.tagName.toLowerCase()) {
|
|
2138
|
-
el.textContent = nextEl.textContent
|
|
2139
|
-
return true
|
|
2140
|
-
}
|
|
2141
|
-
} else if (output.nodeType === Node.TEXT_NODE) {
|
|
2142
|
-
el.textContent = (output as Text).data
|
|
2143
|
-
return true
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
2221
|
return false
|
|
2148
2222
|
}
|
|
2149
2223
|
|
|
@@ -2207,6 +2281,9 @@ function normalizeChildren(
|
|
|
2207
2281
|
if (children === null || children === false) {
|
|
2208
2282
|
return result
|
|
2209
2283
|
}
|
|
2284
|
+
if (_isFragmentVNode(children)) {
|
|
2285
|
+
return normalizeChildren(children.props?.children, result)
|
|
2286
|
+
}
|
|
2210
2287
|
result.push(children)
|
|
2211
2288
|
return result
|
|
2212
2289
|
}
|
package/src/cycle-guard.ts
CHANGED
|
@@ -6,7 +6,7 @@ const isDev =
|
|
|
6
6
|
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
7
7
|
|
|
8
8
|
export interface CycleProtectionOptions {
|
|
9
|
-
/** Enable cycle protection guards (enabled by default in
|
|
9
|
+
/** Enable cycle protection guards (enabled by default in dev mode) */
|
|
10
10
|
enabled?: boolean
|
|
11
11
|
maxFlushCyclesPerMicrotask?: number
|
|
12
12
|
maxEffectRunsPerFlush?: number
|
|
@@ -35,8 +35,8 @@ let enterRootGuard: (root: object) => boolean = () => true
|
|
|
35
35
|
let exitRootGuard: (root: object) => void = () => {}
|
|
36
36
|
|
|
37
37
|
const defaultOptions = {
|
|
38
|
-
//
|
|
39
|
-
enabled:
|
|
38
|
+
// DX-first in development, performance-first in production.
|
|
39
|
+
enabled: isDev,
|
|
40
40
|
maxFlushCyclesPerMicrotask: 10_000,
|
|
41
41
|
maxEffectRunsPerFlush: 20_000,
|
|
42
42
|
windowSize: 5,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight internal list helpers for compiler-generated keyed list paths.
|
|
3
|
+
*
|
|
4
|
+
* This subpath avoids pulling the broad `@fictjs/runtime/internal` barrel when
|
|
5
|
+
* code only needs list primitives.
|
|
6
|
+
*/
|
|
7
|
+
export { createKeyedList, toNodeArray, type KeyedListBinding } from '../list-helpers'
|