@fictjs/runtime 0.3.0 → 0.5.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 (70) hide show
  1. package/dist/advanced.cjs +10 -8
  2. package/dist/advanced.cjs.map +1 -1
  3. package/dist/advanced.d.cts +4 -3
  4. package/dist/advanced.d.ts +4 -3
  5. package/dist/advanced.js +10 -8
  6. package/dist/advanced.js.map +1 -1
  7. package/dist/{chunk-TWELIZRY.js → chunk-5AA7HP4S.js} +5 -3
  8. package/dist/{chunk-TWELIZRY.js.map → chunk-5AA7HP4S.js.map} +1 -1
  9. package/dist/chunk-6SOPF5LZ.cjs +2363 -0
  10. package/dist/chunk-6SOPF5LZ.cjs.map +1 -0
  11. package/dist/{chunk-SO6X7G5S.js → chunk-BQG7VEBY.js} +501 -1880
  12. package/dist/chunk-BQG7VEBY.js.map +1 -0
  13. package/dist/chunk-FKDMDAUR.js +2363 -0
  14. package/dist/chunk-FKDMDAUR.js.map +1 -0
  15. package/dist/{chunk-L4DIV3RC.cjs → chunk-GHUV2FLD.cjs} +9 -7
  16. package/dist/chunk-GHUV2FLD.cjs.map +1 -0
  17. package/dist/{chunk-XLIZJMMJ.js → chunk-KKKYW54Z.js} +8 -6
  18. package/dist/{chunk-XLIZJMMJ.js.map → chunk-KKKYW54Z.js.map} +1 -1
  19. package/dist/{chunk-M2TSXZ4C.cjs → chunk-KYLNC4CD.cjs} +18 -16
  20. package/dist/chunk-KYLNC4CD.cjs.map +1 -0
  21. package/dist/chunk-TKWN42TA.cjs +2259 -0
  22. package/dist/chunk-TKWN42TA.cjs.map +1 -0
  23. package/dist/{context-B25xyQrJ.d.cts → context-CTBE00S_.d.cts} +1 -1
  24. package/dist/{context-CGdP7_Jb.d.ts → context-lkLhbkFJ.d.ts} +1 -1
  25. package/dist/{effect-D6kaLM2-.d.cts → effect-BpSNEJJz.d.cts} +7 -67
  26. package/dist/{effect-D6kaLM2-.d.ts → effect-BpSNEJJz.d.ts} +7 -67
  27. package/dist/index.cjs +40 -38
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +5 -4
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.dev.js +92 -4
  32. package/dist/index.dev.js.map +1 -1
  33. package/dist/index.js +19 -17
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal.cjs +189 -202
  36. package/dist/internal.cjs.map +1 -1
  37. package/dist/internal.d.cts +13 -23
  38. package/dist/internal.d.ts +13 -23
  39. package/dist/internal.js +195 -208
  40. package/dist/internal.js.map +1 -1
  41. package/dist/loader.cjs +280 -0
  42. package/dist/loader.cjs.map +1 -0
  43. package/dist/loader.d.cts +57 -0
  44. package/dist/loader.d.ts +57 -0
  45. package/dist/loader.js +280 -0
  46. package/dist/loader.js.map +1 -0
  47. package/dist/{props-BIfromL0.d.cts → props-XTHYD19o.d.cts} +13 -2
  48. package/dist/{props-BEgIVMRx.d.ts → props-x-HbI-jX.d.ts} +13 -2
  49. package/dist/resume-BrAkmSTY.d.cts +79 -0
  50. package/dist/resume-Dx8_l72o.d.ts +79 -0
  51. package/dist/{scope-CzNkn587.d.ts → scope-CdbGmsFf.d.ts} +1 -1
  52. package/dist/{scope-Cx_3CjIZ.d.cts → scope-DfcP9I-A.d.cts} +1 -1
  53. package/dist/signal-C4ISF17w.d.cts +66 -0
  54. package/dist/signal-C4ISF17w.d.ts +66 -0
  55. package/package.json +8 -3
  56. package/src/binding.ts +254 -5
  57. package/src/dom.ts +103 -5
  58. package/src/hooks.ts +15 -2
  59. package/src/hydration.ts +75 -0
  60. package/src/internal.ts +34 -2
  61. package/src/list-helpers.ts +113 -12
  62. package/src/loader.ts +437 -0
  63. package/src/node-ops.ts +65 -0
  64. package/src/resume.ts +517 -0
  65. package/src/store.ts +8 -0
  66. package/dist/chunk-ID3WBWNO.cjs +0 -3638
  67. package/dist/chunk-ID3WBWNO.cjs.map +0 -1
  68. package/dist/chunk-L4DIV3RC.cjs.map +0 -1
  69. package/dist/chunk-M2TSXZ4C.cjs.map +0 -1
  70. package/dist/chunk-SO6X7G5S.js.map +0 -1
package/src/resume.ts ADDED
@@ -0,0 +1,517 @@
1
+ import type { HookContext } from './hooks'
2
+ import { createSignal, isSignal } from './signal'
3
+ import { createStore, isStoreProxy, unwrapStore } from './store'
4
+
5
+ // ============================================================================
6
+ // Serialization Types
7
+ // ============================================================================
8
+
9
+ /**
10
+ * Type markers for serialized values.
11
+ * These allow us to preserve type information through JSON serialization.
12
+ */
13
+ type SerializedMarker =
14
+ | { __t: 'd'; v: number } // Date (as timestamp)
15
+ | { __t: 'm'; v: [unknown, unknown][] } // Map (as entries array)
16
+ | { __t: 's'; v: unknown[] } // Set (as array)
17
+ | { __t: 'r'; v: { s: string; f: string } } // RegExp (source + flags)
18
+ | { __t: 'u' } // undefined
19
+ | { __t: 'n' } // NaN
20
+ | { __t: '+i' } // Infinity
21
+ | { __t: '-i' } // -Infinity
22
+ | { __t: 'b'; v: string } // BigInt (as string)
23
+ | { __t: 'ref'; v: string } // Circular reference (path)
24
+
25
+ export type SlotSnapshot =
26
+ | [index: number, type: 'sig', value: unknown]
27
+ | [index: number, type: 'store', value: unknown]
28
+ | [index: number, type: 'raw', value: unknown]
29
+
30
+ export interface ScopeSnapshot {
31
+ id: string
32
+ t?: string
33
+ slots: SlotSnapshot[]
34
+ props?: Record<string, unknown>
35
+ vars?: Record<string, number>
36
+ }
37
+
38
+ export interface SSRState {
39
+ scopes: Record<string, ScopeSnapshot>
40
+ }
41
+
42
+ export interface ScopeRecord {
43
+ id: string
44
+ ctx: HookContext
45
+ host: Element
46
+ type?: string
47
+ props?: Record<string, unknown>
48
+ }
49
+
50
+ let ssrEnabled = false
51
+ let resumableEnabled = false
52
+ let hydrating = false
53
+ let scopeCounter = 0
54
+ let scopeRegistry = new Map<string, ScopeRecord>()
55
+ let snapshotState: SSRState | null = null
56
+ const resumedScopes = new Map<
57
+ string,
58
+ { ctx: HookContext; host: Element; props?: Record<string, unknown> }
59
+ >()
60
+
61
+ export function __fictEnableSSR(): void {
62
+ ssrEnabled = true
63
+ scopeCounter = 0
64
+ scopeRegistry = new Map()
65
+ resumedScopes.clear()
66
+ snapshotState = null
67
+ }
68
+
69
+ export function __fictDisableSSR(): void {
70
+ ssrEnabled = false
71
+ }
72
+
73
+ export function __fictEnableResumable(): void {
74
+ resumableEnabled = true
75
+ }
76
+
77
+ export function __fictDisableResumable(): void {
78
+ resumableEnabled = false
79
+ resumedScopes.clear()
80
+ }
81
+
82
+ export function __fictIsResumable(): boolean {
83
+ return ssrEnabled || resumableEnabled
84
+ }
85
+
86
+ export function __fictIsSSR(): boolean {
87
+ return ssrEnabled
88
+ }
89
+
90
+ export function __fictEnterHydration(): void {
91
+ hydrating = true
92
+ }
93
+
94
+ export function __fictExitHydration(): void {
95
+ hydrating = false
96
+ }
97
+
98
+ export function __fictIsHydrating(): boolean {
99
+ return hydrating
100
+ }
101
+
102
+ export function __fictRegisterScope(
103
+ ctx: HookContext,
104
+ host: Element,
105
+ type?: string,
106
+ props?: Record<string, unknown>,
107
+ ): string {
108
+ if (!__fictIsResumable()) return ''
109
+
110
+ const id = `s${++scopeCounter}`
111
+ ctx.scopeId = id
112
+ if (type !== undefined) {
113
+ ctx.scopeType = type
114
+ }
115
+ host.setAttribute('data-fict-s', id)
116
+ if (type) {
117
+ host.setAttribute('data-fict-t', type)
118
+ }
119
+
120
+ const record: ScopeRecord = { id, ctx, host }
121
+ if (type !== undefined) {
122
+ record.type = type
123
+ }
124
+ if (props !== undefined) {
125
+ record.props = props
126
+ }
127
+ scopeRegistry.set(id, record)
128
+ return id
129
+ }
130
+
131
+ export function __fictGetScopeRegistry(): Map<string, ScopeRecord> {
132
+ return scopeRegistry
133
+ }
134
+
135
+ export function __fictSerializeSSRState(): SSRState {
136
+ const scopes: Record<string, ScopeSnapshot> = {}
137
+
138
+ for (const [id, record] of scopeRegistry.entries()) {
139
+ const snapshot: ScopeSnapshot = {
140
+ id,
141
+ slots: serializeSlots(record.ctx),
142
+ }
143
+ if (record.type !== undefined) {
144
+ snapshot.t = record.type
145
+ }
146
+ if (record.props !== undefined) {
147
+ snapshot.props = record.props
148
+ }
149
+ if (record.ctx.slotMap !== undefined) {
150
+ snapshot.vars = record.ctx.slotMap
151
+ }
152
+ scopes[id] = snapshot
153
+ }
154
+
155
+ return { scopes }
156
+ }
157
+
158
+ export function __fictSetSSRState(state: SSRState | null): void {
159
+ snapshotState = state
160
+ if (!state) {
161
+ resumedScopes.clear()
162
+ }
163
+ }
164
+
165
+ export function __fictGetSSRScope(id: string): ScopeSnapshot | undefined {
166
+ return snapshotState?.scopes[id]
167
+ }
168
+
169
+ export function __fictEnsureScope(
170
+ scopeId: string,
171
+ host: Element,
172
+ snapshot?: ScopeSnapshot,
173
+ ): HookContext {
174
+ const existing = resumedScopes.get(scopeId)
175
+ if (existing) return existing.ctx
176
+
177
+ const ctx = createContextFromSnapshot(snapshot)
178
+ ctx.scopeId = scopeId
179
+ if (snapshot?.t !== undefined) {
180
+ ctx.scopeType = snapshot.t
181
+ }
182
+ const entry: { ctx: HookContext; host: Element; props?: Record<string, unknown> } = { ctx, host }
183
+ if (snapshot?.props !== undefined) {
184
+ entry.props = snapshot.props
185
+ }
186
+ resumedScopes.set(scopeId, entry)
187
+ return ctx
188
+ }
189
+
190
+ export function __fictUseLexicalScope(scopeId: string, names: string[]): unknown[] {
191
+ const record = resumedScopes.get(scopeId)
192
+ if (!record) {
193
+ throw new Error(`[fict] Missing resumed scope for ${scopeId}`)
194
+ }
195
+ const ctx = record.ctx
196
+ const map = ctx.slotMap ?? {}
197
+ return names.map(name => ctx.slots[map[name] ?? -1])
198
+ }
199
+
200
+ export function __fictGetScopeProps(scopeId: string): Record<string, unknown> | undefined {
201
+ return resumedScopes.get(scopeId)?.props
202
+ }
203
+
204
+ export function __fictQrl(moduleId: string, exportName: string): string {
205
+ const manifest = (globalThis as Record<string, unknown>).__FICT_MANIFEST__ as
206
+ | Record<string, string>
207
+ | undefined
208
+
209
+ // Check manifest first (production builds)
210
+ if (manifest?.[moduleId]) {
211
+ return `${manifest[moduleId]}#${exportName}`
212
+ }
213
+
214
+ // Handle file:// URLs for Vite dev mode SSR
215
+ if (moduleId.startsWith('file://')) {
216
+ const filePath = moduleId.slice(7) // Remove 'file://'
217
+
218
+ // Check for configured SSR base path (project root)
219
+ const ssrBase = (globalThis as Record<string, unknown>).__FICT_SSR_BASE__ as string | undefined
220
+ if (ssrBase) {
221
+ // Strip base to get relative path (e.g., /src/App.tsx)
222
+ if (filePath.startsWith(ssrBase)) {
223
+ const relativePath = filePath.slice(ssrBase.length)
224
+ return `${relativePath}#${exportName}`
225
+ }
226
+ }
227
+
228
+ // Fallback: use Vite's /@fs/ convention for direct file system access
229
+ return `/@fs${filePath}#${exportName}`
230
+ }
231
+
232
+ return `${moduleId}#${exportName}`
233
+ }
234
+
235
+ // Registry for resume functions to prevent tree-shaking
236
+ const resumeFunctionRegistry = new Map<string, (...args: unknown[]) => unknown>()
237
+
238
+ /**
239
+ * Register a resume function to prevent it from being tree-shaken.
240
+ * This is called at module load time by compiled component code.
241
+ */
242
+ export function __fictRegisterResume(name: string, fn: (...args: unknown[]) => unknown): void {
243
+ resumeFunctionRegistry.set(name, fn)
244
+ }
245
+
246
+ /**
247
+ * Get a registered resume function by name.
248
+ * Used by the loader to find resume functions.
249
+ */
250
+ export function __fictGetResume(name: string): ((...args: unknown[]) => unknown) | undefined {
251
+ return resumeFunctionRegistry.get(name)
252
+ }
253
+
254
+ function serializeSlots(ctx: HookContext): SlotSnapshot[] {
255
+ const slots: SlotSnapshot[] = []
256
+ const values = ctx.slots ?? []
257
+ // Share the 'seen' map across all slots to handle cross-slot circular references
258
+ const seen = new Map<object, string>()
259
+
260
+ for (let i = 0; i < values.length; i++) {
261
+ const value = values[i]
262
+ // Note: we don't skip undefined anymore since we can serialize it
263
+ if (value === undefined) {
264
+ slots.push([i, 'raw', serializeValue(undefined, seen, `$[${i}]`)])
265
+ continue
266
+ }
267
+
268
+ if (isSignal(value)) {
269
+ try {
270
+ const raw = (value as () => unknown)()
271
+ slots.push([i, 'sig', serializeValue(raw, seen, `$[${i}]`)])
272
+ } catch {
273
+ // ignore signal read errors during SSR
274
+ }
275
+ continue
276
+ }
277
+
278
+ if (isStoreProxy(value)) {
279
+ const raw = unwrapStore(value)
280
+ slots.push([i, 'store', serializeValue(raw, seen, `$[${i}]`)])
281
+ continue
282
+ }
283
+
284
+ // Fallback: serialize raw slot value with complex type support
285
+ slots.push([i, 'raw', serializeValue(value, seen, `$[${i}]`)])
286
+ }
287
+
288
+ return slots
289
+ }
290
+
291
+ function createContextFromSnapshot(snapshot?: ScopeSnapshot): HookContext {
292
+ const ctx: HookContext = { slots: [], cursor: 0 }
293
+ if (!snapshot) return ctx
294
+
295
+ for (const slot of snapshot.slots) {
296
+ const [index, type, value] = slot
297
+ if (type === 'sig') {
298
+ ctx.slots[index] = createSignal(deserializeValue(value))
299
+ } else if (type === 'store') {
300
+ ctx.slots[index] = createStore(deserializeValue(value) as object)[0]
301
+ } else {
302
+ ctx.slots[index] = deserializeValue(value)
303
+ }
304
+ }
305
+ if (snapshot.vars) {
306
+ ctx.slotMap = { ...snapshot.vars }
307
+ }
308
+
309
+ return ctx
310
+ }
311
+
312
+ // ============================================================================
313
+ // Value Serialization - Complex Type Support
314
+ // ============================================================================
315
+
316
+ /**
317
+ * Check if value has a serialization marker
318
+ */
319
+ function isSerializedMarker(value: unknown): value is SerializedMarker {
320
+ return (
321
+ typeof value === 'object' &&
322
+ value !== null &&
323
+ '__t' in value &&
324
+ typeof (value as SerializedMarker).__t === 'string'
325
+ )
326
+ }
327
+
328
+ /**
329
+ * Serialize a value with support for complex types.
330
+ * Handles: Date, Map, Set, RegExp, undefined, NaN, Infinity, -Infinity, BigInt, circular references
331
+ */
332
+ export function serializeValue(
333
+ value: unknown,
334
+ seen = new Map<object, string>(),
335
+ path = '$',
336
+ ): unknown {
337
+ // Handle primitives that JSON can't represent correctly
338
+ if (value === undefined) {
339
+ return { __t: 'u' } as SerializedMarker
340
+ }
341
+
342
+ if (typeof value === 'number') {
343
+ if (Number.isNaN(value)) {
344
+ return { __t: 'n' } as SerializedMarker
345
+ }
346
+ if (value === Infinity) {
347
+ return { __t: '+i' } as SerializedMarker
348
+ }
349
+ if (value === -Infinity) {
350
+ return { __t: '-i' } as SerializedMarker
351
+ }
352
+ return value
353
+ }
354
+
355
+ if (typeof value === 'bigint') {
356
+ return { __t: 'b', v: value.toString() } as SerializedMarker
357
+ }
358
+
359
+ // Primitives that JSON handles correctly
360
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') {
361
+ return value
362
+ }
363
+
364
+ // Handle functions - can't serialize, skip
365
+ if (typeof value === 'function') {
366
+ return undefined
367
+ }
368
+
369
+ // Handle objects - check for circular references first
370
+ if (typeof value === 'object') {
371
+ // Check for circular reference
372
+ if (seen.has(value)) {
373
+ return { __t: 'ref', v: seen.get(value)! } as SerializedMarker
374
+ }
375
+
376
+ // Date
377
+ if (value instanceof Date) {
378
+ return { __t: 'd', v: value.getTime() } as SerializedMarker
379
+ }
380
+
381
+ // RegExp
382
+ if (value instanceof RegExp) {
383
+ return { __t: 'r', v: { s: value.source, f: value.flags } } as SerializedMarker
384
+ }
385
+
386
+ // Map
387
+ if (value instanceof Map) {
388
+ seen.set(value, path)
389
+ const entries: [unknown, unknown][] = []
390
+ let i = 0
391
+ for (const [k, v] of value) {
392
+ entries.push([
393
+ serializeValue(k, seen, `${path}.k${i}`),
394
+ serializeValue(v, seen, `${path}.v${i}`),
395
+ ])
396
+ i++
397
+ }
398
+ return { __t: 'm', v: entries } as SerializedMarker
399
+ }
400
+
401
+ // Set
402
+ if (value instanceof Set) {
403
+ seen.set(value, path)
404
+ const items: unknown[] = []
405
+ let i = 0
406
+ for (const item of value) {
407
+ items.push(serializeValue(item, seen, `${path}[${i}]`))
408
+ i++
409
+ }
410
+ return { __t: 's', v: items } as SerializedMarker
411
+ }
412
+
413
+ // Array
414
+ if (Array.isArray(value)) {
415
+ seen.set(value, path)
416
+ return value.map((item, i) => serializeValue(item, seen, `${path}[${i}]`))
417
+ }
418
+
419
+ // Plain object
420
+ seen.set(value, path)
421
+ const result: Record<string, unknown> = {}
422
+ for (const key of Object.keys(value)) {
423
+ const serialized = serializeValue(
424
+ (value as Record<string, unknown>)[key],
425
+ seen,
426
+ `${path}.${key}`,
427
+ )
428
+ if (serialized !== undefined) {
429
+ result[key] = serialized
430
+ }
431
+ }
432
+ return result
433
+ }
434
+
435
+ return value
436
+ }
437
+
438
+ /**
439
+ * Deserialize a value, restoring complex types from their serialized form.
440
+ */
441
+ export function deserializeValue(
442
+ value: unknown,
443
+ refs = new Map<string, unknown>(),
444
+ path = '$',
445
+ ): unknown {
446
+ // Handle null
447
+ if (value === null) {
448
+ return null
449
+ }
450
+
451
+ // Handle primitives
452
+ if (typeof value !== 'object') {
453
+ return value
454
+ }
455
+
456
+ // Check for serialization markers
457
+ if (isSerializedMarker(value)) {
458
+ switch (value.__t) {
459
+ case 'u':
460
+ return undefined
461
+ case 'n':
462
+ return NaN
463
+ case '+i':
464
+ return Infinity
465
+ case '-i':
466
+ return -Infinity
467
+ case 'b':
468
+ return BigInt(value.v)
469
+ case 'd':
470
+ return new Date(value.v)
471
+ case 'r':
472
+ return new RegExp(value.v.s, value.v.f)
473
+ case 'm': {
474
+ const map = new Map<unknown, unknown>()
475
+ refs.set(path, map)
476
+ for (let i = 0; i < value.v.length; i++) {
477
+ const entry = value.v[i]
478
+ if (!entry) continue
479
+ const [k, v] = entry
480
+ map.set(
481
+ deserializeValue(k, refs, `${path}.k${i}`),
482
+ deserializeValue(v, refs, `${path}.v${i}`),
483
+ )
484
+ }
485
+ return map
486
+ }
487
+ case 's': {
488
+ const set = new Set<unknown>()
489
+ refs.set(path, set)
490
+ for (let i = 0; i < value.v.length; i++) {
491
+ set.add(deserializeValue(value.v[i], refs, `${path}[${i}]`))
492
+ }
493
+ return set
494
+ }
495
+ case 'ref':
496
+ return refs.get(value.v)
497
+ }
498
+ }
499
+
500
+ // Handle arrays
501
+ if (Array.isArray(value)) {
502
+ const arr: unknown[] = []
503
+ refs.set(path, arr)
504
+ for (let i = 0; i < value.length; i++) {
505
+ arr.push(deserializeValue(value[i], refs, `${path}[${i}]`))
506
+ }
507
+ return arr
508
+ }
509
+
510
+ // Handle plain objects
511
+ const obj: Record<string, unknown> = {}
512
+ refs.set(path, obj)
513
+ for (const key of Object.keys(value)) {
514
+ obj[key] = deserializeValue((value as Record<string, unknown>)[key], refs, `${path}.${key}`)
515
+ }
516
+ return obj
517
+ }
package/src/store.ts CHANGED
@@ -131,6 +131,14 @@ function unwrap<T>(value: T): T {
131
131
  return value
132
132
  }
133
133
 
134
+ export function isStoreProxy(value: unknown): boolean {
135
+ return !!(value && typeof value === 'object' && Reflect.get(value as object, PROXY))
136
+ }
137
+
138
+ export function unwrapStore<T>(value: T): T {
139
+ return unwrap(value)
140
+ }
141
+
134
142
  function track(target: object, prop: string | symbol) {
135
143
  let signals = signalCache.get(target)
136
144
  if (!signals) {