@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.
Files changed (74) hide show
  1. package/README.md +46 -0
  2. package/dist/advanced.cjs +9 -9
  3. package/dist/advanced.d.cts +4 -4
  4. package/dist/advanced.d.ts +4 -4
  5. package/dist/advanced.js +4 -4
  6. package/dist/{effect-DAzpH7Mm.d.cts → binding-BWchH3Kp.d.cts} +33 -24
  7. package/dist/{effect-DAzpH7Mm.d.ts → binding-BWchH3Kp.d.ts} +33 -24
  8. package/dist/{chunk-7YQK3XKY.js → chunk-DXG3TARY.js} +520 -518
  9. package/dist/chunk-DXG3TARY.js.map +1 -0
  10. package/dist/{chunk-TLDT76RV.js → chunk-FVX77557.js} +3 -3
  11. package/dist/{chunk-WRU3IZOA.js → chunk-JVYH76ZX.js} +3 -3
  12. package/dist/chunk-LBE6DC3V.cjs +768 -0
  13. package/dist/chunk-LBE6DC3V.cjs.map +1 -0
  14. package/dist/chunk-N6ODUM2Y.js +768 -0
  15. package/dist/chunk-N6ODUM2Y.js.map +1 -0
  16. package/dist/{chunk-PRF4QG73.cjs → chunk-OAM7HABA.cjs} +423 -246
  17. package/dist/chunk-OAM7HABA.cjs.map +1 -0
  18. package/dist/{chunk-CEV6TO5U.cjs → chunk-PD6IQY2Y.cjs} +8 -8
  19. package/dist/{chunk-CEV6TO5U.cjs.map → chunk-PD6IQY2Y.cjs.map} +1 -1
  20. package/dist/{chunk-HHDHQGJY.cjs → chunk-PG4QX2I2.cjs} +17 -17
  21. package/dist/{chunk-HHDHQGJY.cjs.map → chunk-PG4QX2I2.cjs.map} +1 -1
  22. package/dist/{chunk-4LCHQ7U4.js → chunk-T2LNV5Q5.js} +271 -94
  23. package/dist/chunk-T2LNV5Q5.js.map +1 -0
  24. package/dist/{chunk-FSCBL7RI.cjs → chunk-UBFDB6OL.cjs} +521 -519
  25. package/dist/chunk-UBFDB6OL.cjs.map +1 -0
  26. package/dist/{context-C4vBQbb4.d.ts → devtools-5AipK9CX.d.cts} +35 -35
  27. package/dist/{context-BFbHf9nC.d.cts → devtools-BDp76luf.d.ts} +35 -35
  28. package/dist/index.cjs +42 -42
  29. package/dist/index.d.cts +4 -4
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.dev.js +3 -3
  32. package/dist/index.dev.js.map +1 -1
  33. package/dist/index.js +3 -3
  34. package/dist/internal-list.cjs +12 -0
  35. package/dist/internal-list.cjs.map +1 -0
  36. package/dist/internal-list.d.cts +2 -0
  37. package/dist/internal-list.d.ts +2 -0
  38. package/dist/internal-list.js +12 -0
  39. package/dist/internal-list.js.map +1 -0
  40. package/dist/internal.cjs +6 -746
  41. package/dist/internal.cjs.map +1 -1
  42. package/dist/internal.d.cts +6 -74
  43. package/dist/internal.d.ts +6 -74
  44. package/dist/internal.js +12 -752
  45. package/dist/internal.js.map +1 -1
  46. package/dist/list-DL5DOFcO.d.ts +71 -0
  47. package/dist/list-hP7hQ9Vk.d.cts +71 -0
  48. package/dist/loader.cjs +94 -15
  49. package/dist/loader.cjs.map +1 -1
  50. package/dist/loader.d.cts +16 -2
  51. package/dist/loader.d.ts +16 -2
  52. package/dist/loader.js +87 -8
  53. package/dist/loader.js.map +1 -1
  54. package/dist/{props-84UJeWO8.d.cts → props-BpZz0AOq.d.cts} +2 -2
  55. package/dist/{props-BRhFK50f.d.ts → props-CjLH0JE-.d.ts} +2 -2
  56. package/dist/{resume-i-A3EFox.d.cts → resume-BJ4oHLi_.d.cts} +3 -1
  57. package/dist/{resume-CqeQ3v_q.d.ts → resume-CuyJWXP_.d.ts} +3 -1
  58. package/dist/{scope-DlCBL1Ft.d.cts → scope-BJCtq8hJ.d.cts} +1 -1
  59. package/dist/{scope-D3DpsfoG.d.ts → scope-jPt5DHRT.d.ts} +1 -1
  60. package/package.json +8 -1
  61. package/src/binding.ts +113 -36
  62. package/src/cycle-guard.ts +3 -3
  63. package/src/internal/list.ts +7 -0
  64. package/src/internal.ts +1 -0
  65. package/src/list-helpers.ts +1 -1
  66. package/src/loader.ts +119 -9
  67. package/src/resume.ts +6 -3
  68. package/src/signal.ts +8 -1
  69. package/dist/chunk-4LCHQ7U4.js.map +0 -1
  70. package/dist/chunk-7YQK3XKY.js.map +0 -1
  71. package/dist/chunk-FSCBL7RI.cjs.map +0 -1
  72. package/dist/chunk-PRF4QG73.cjs.map +0 -1
  73. /package/dist/{chunk-TLDT76RV.js.map → chunk-FVX77557.js.map} +0 -0
  74. /package/dist/{chunk-WRU3IZOA.js.map → chunk-JVYH76ZX.js.map} +0 -0
package/src/internal.ts CHANGED
@@ -62,6 +62,7 @@ export {
62
62
  __fictQrl,
63
63
  __fictRegisterResume,
64
64
  __fictGetResume,
65
+ FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
65
66
  serializeValue,
66
67
  deserializeValue,
67
68
  } from './resume'
@@ -30,7 +30,7 @@ export { insertNodesBefore, removeNodes, toNodeArray }
30
30
  const isDev =
31
31
  typeof __DEV__ !== 'undefined'
32
32
  ? __DEV__
33
- : typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
33
+ : typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
34
34
 
35
35
  const isShadowRoot = (node: Node): node is ShadowRoot =>
36
36
  typeof ShadowRoot !== 'undefined' && node instanceof ShadowRoot
package/src/loader.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { DelegatedEvents } from './constants'
2
2
  import {
3
+ FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
4
+ type SSRState,
3
5
  __fictEnableResumable,
4
6
  __fictEnsureScope,
5
7
  __fictGetResume,
@@ -67,6 +69,11 @@ export interface ResumableLoaderOptions {
67
69
  document?: Document
68
70
  snapshotScriptId?: string
69
71
  events?: string[]
72
+ /**
73
+ * Receives structured snapshot/resume issues detected by the loader.
74
+ * Useful for telemetry and fail-safe fallback orchestration.
75
+ */
76
+ onSnapshotIssue?: (issue: SnapshotIssue) => void
70
77
  /**
71
78
  * Prefetch strategy configuration.
72
79
  * Set to false to disable all prefetching.
@@ -75,6 +82,21 @@ export interface ResumableLoaderOptions {
75
82
  prefetch?: PrefetchStrategy | false
76
83
  }
77
84
 
85
+ export type SnapshotIssueCode =
86
+ | 'snapshot_parse_error'
87
+ | 'snapshot_invalid_shape'
88
+ | 'snapshot_unsupported_version'
89
+ | 'scope_snapshot_missing'
90
+
91
+ export interface SnapshotIssue {
92
+ code: SnapshotIssueCode
93
+ message: string
94
+ source: string
95
+ expectedVersion: number
96
+ actualVersion?: number
97
+ scopeId?: string
98
+ }
99
+
78
100
  // ============================================================================
79
101
  // State
80
102
  // ============================================================================
@@ -85,6 +107,8 @@ let prefetchCleanup: (() => void) | null = null
85
107
  let eventListenerCleanup: (() => void) | null = null
86
108
  let snapshotObserver: MutationObserver | null = null
87
109
  const processedSnapshots = new Set<HTMLScriptElement>()
110
+ let snapshotIssueHandler: ((issue: SnapshotIssue) => void) | null = null
111
+ const emittedIssueKeys = new Set<string>()
88
112
 
89
113
  /**
90
114
  * Reset the hydrated scopes set. Useful for testing.
@@ -130,11 +154,14 @@ export function cleanupEventListeners(): void {
130
154
  export function installResumableLoader(options: ResumableLoaderOptions = {}): void {
131
155
  const doc = options.document ?? window.document
132
156
  const scriptId = options.snapshotScriptId ?? '__FICT_SNAPSHOT__'
157
+ snapshotIssueHandler = options.onSnapshotIssue ?? null
133
158
 
134
159
  // Reset hydrated scopes for fresh loader installation
135
160
  hydratedScopes.clear()
136
161
  prefetchedUrls.clear()
137
162
  processedSnapshots.clear()
163
+ emittedIssueKeys.clear()
164
+ __fictSetSSRState(null)
138
165
 
139
166
  // Clean up previous event listeners
140
167
  if (eventListenerCleanup) {
@@ -155,11 +182,9 @@ export function installResumableLoader(options: ResumableLoaderOptions = {}): vo
155
182
 
156
183
  const snapshotEl = doc.getElementById(scriptId)
157
184
  if (snapshotEl?.textContent) {
158
- try {
159
- const state = JSON.parse(snapshotEl.textContent)
185
+ const state = parseSnapshotText(snapshotEl.textContent, `#${scriptId}`)
186
+ if (state) {
160
187
  __fictSetSSRState(state)
161
- } catch {
162
- // Ignore parse errors
163
188
  }
164
189
  }
165
190
 
@@ -224,11 +249,88 @@ function parseSnapshotScript(script: HTMLScriptElement): void {
224
249
  processedSnapshots.add(script)
225
250
  const text = script.textContent
226
251
  if (!text) return
227
- try {
228
- const state = JSON.parse(text)
252
+ const source = script.id ? `#${script.id}` : '<script[data-fict-snapshot]>'
253
+ const state = parseSnapshotText(text, source)
254
+ if (state) {
229
255
  __fictMergeSSRState(state)
256
+ }
257
+ }
258
+
259
+ function parseSnapshotText(text: string, source: string): SSRState | null {
260
+ let parsed: unknown
261
+ try {
262
+ parsed = JSON.parse(text)
230
263
  } catch {
231
- // Ignore parse errors
264
+ emitSnapshotIssue({
265
+ code: 'snapshot_parse_error',
266
+ message: '[fict/loader] Failed to parse SSR snapshot JSON.',
267
+ source,
268
+ expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
269
+ })
270
+ return null
271
+ }
272
+
273
+ return normalizeSnapshotState(parsed, source)
274
+ }
275
+
276
+ function normalizeSnapshotState(value: unknown, source: string): SSRState | null {
277
+ if (!isRecord(value)) {
278
+ emitSnapshotIssue({
279
+ code: 'snapshot_invalid_shape',
280
+ message: '[fict/loader] Snapshot payload must be an object.',
281
+ source,
282
+ expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
283
+ })
284
+ return null
285
+ }
286
+
287
+ const rawVersion = value.v
288
+ const version = rawVersion === undefined ? FICT_SSR_SNAPSHOT_SCHEMA_VERSION : rawVersion
289
+ if (!Number.isInteger(version) || version !== FICT_SSR_SNAPSHOT_SCHEMA_VERSION) {
290
+ const versionIssue: SnapshotIssue = {
291
+ code: 'snapshot_unsupported_version',
292
+ message: `[fict/loader] Snapshot schema version ${String(version)} is not supported by this runtime.`,
293
+ source,
294
+ expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
295
+ }
296
+ if (typeof version === 'number') {
297
+ versionIssue.actualVersion = version
298
+ }
299
+ emitSnapshotIssue({
300
+ ...versionIssue,
301
+ })
302
+ return null
303
+ }
304
+
305
+ const scopes = value.scopes
306
+ if (!isRecord(scopes)) {
307
+ emitSnapshotIssue({
308
+ code: 'snapshot_invalid_shape',
309
+ message: '[fict/loader] Snapshot payload is missing a valid `scopes` object.',
310
+ source,
311
+ expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
312
+ })
313
+ return null
314
+ }
315
+
316
+ return { v: FICT_SSR_SNAPSHOT_SCHEMA_VERSION, scopes: scopes as SSRState['scopes'] }
317
+ }
318
+
319
+ function isRecord(value: unknown): value is Record<string, unknown> {
320
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
321
+ }
322
+
323
+ function emitSnapshotIssue(issue: SnapshotIssue): void {
324
+ const key =
325
+ `${issue.code}|${issue.source}|${issue.scopeId ?? ''}|` +
326
+ `${issue.actualVersion ?? ''}|${issue.expectedVersion}`
327
+ if (emittedIssueKeys.has(key)) return
328
+ emittedIssueKeys.add(key)
329
+
330
+ snapshotIssueHandler?.(issue)
331
+
332
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
333
+ console.warn(issue.message)
232
334
  }
233
335
  }
234
336
 
@@ -425,9 +527,17 @@ async function handleResumableEventAsync(event: Event): Promise<void> {
425
527
  if (!scopeId) continue
426
528
 
427
529
  const snapshot = __fictGetSSRScope(scopeId)
428
- if (snapshot) {
429
- __fictEnsureScope(scopeId, host, snapshot)
530
+ if (!snapshot) {
531
+ emitSnapshotIssue({
532
+ code: 'scope_snapshot_missing',
533
+ message: `[fict/loader] Missing scope snapshot for ${scopeId}; skipping resumable handler execution.`,
534
+ source: 'event',
535
+ expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
536
+ scopeId,
537
+ })
538
+ return
430
539
  }
540
+ __fictEnsureScope(scopeId, host, snapshot)
431
541
 
432
542
  const { url, exportName } = parseQrl(qrl)
433
543
 
package/src/resume.ts CHANGED
@@ -36,7 +36,10 @@ export interface ScopeSnapshot {
36
36
  vars?: Record<string, number>
37
37
  }
38
38
 
39
+ export const FICT_SSR_SNAPSHOT_SCHEMA_VERSION = 1
40
+
39
41
  export interface SSRState {
42
+ v: number
40
43
  scopes: Record<string, ScopeSnapshot>
41
44
  }
42
45
 
@@ -173,7 +176,7 @@ export function __fictSerializeSSRState(): SSRState {
173
176
  scopes[id] = snapshot
174
177
  }
175
178
 
176
- return { scopes }
179
+ return { v: FICT_SSR_SNAPSHOT_SCHEMA_VERSION, scopes }
177
180
  }
178
181
 
179
182
  export function __fictSerializeSSRStateForScopes(scopeIds: Iterable<string>): SSRState {
@@ -198,7 +201,7 @@ export function __fictSerializeSSRStateForScopes(scopeIds: Iterable<string>): SS
198
201
  scopes[id] = snapshot
199
202
  }
200
203
 
201
- return { scopes }
204
+ return { v: FICT_SSR_SNAPSHOT_SCHEMA_VERSION, scopes }
202
205
  }
203
206
 
204
207
  export function __fictSetSSRState(state: SSRState | null): void {
@@ -211,7 +214,7 @@ export function __fictSetSSRState(state: SSRState | null): void {
211
214
  export function __fictMergeSSRState(state: SSRState | null): void {
212
215
  if (!state) return
213
216
  if (!snapshotState) {
214
- snapshotState = { scopes: { ...state.scopes } }
217
+ snapshotState = { v: state.v, scopes: { ...state.scopes } }
215
218
  return
216
219
  }
217
220
  Object.assign(snapshotState.scopes, state.scopes)
package/src/signal.ts CHANGED
@@ -1595,7 +1595,14 @@ let effectRunDevtools: (node: EffectNode) => void = () => {}
1595
1595
  let trackDependencyDevtools: (dep: ReactiveNode, sub: ReactiveNode) => void = () => {}
1596
1596
  let untrackDependencyDevtools: (dep: ReactiveNode, sub: ReactiveNode) => void = () => {}
1597
1597
 
1598
- if (isDev) {
1598
+ // Keep this as a direct conditional expression (instead of `if (isDev)`) so
1599
+ // bundlers can eliminate the entire devtools setup block when `__DEV__` is
1600
+ // defined as `false` in production builds.
1601
+ if (
1602
+ typeof __DEV__ !== 'undefined'
1603
+ ? __DEV__
1604
+ : typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
1605
+ ) {
1599
1606
  // Unified ID counter for all reactive nodes (signal/computed/effect)
1600
1607
  // to prevent ID collisions when storing in single devtools maps
1601
1608
  let nextDevtoolsId = 0