@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.
- package/dist/advanced.cjs +10 -8
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +4 -3
- package/dist/advanced.d.ts +4 -3
- package/dist/advanced.js +10 -8
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-TWELIZRY.js → chunk-5AA7HP4S.js} +5 -3
- package/dist/{chunk-TWELIZRY.js.map → chunk-5AA7HP4S.js.map} +1 -1
- package/dist/chunk-6SOPF5LZ.cjs +2363 -0
- package/dist/chunk-6SOPF5LZ.cjs.map +1 -0
- package/dist/{chunk-SO6X7G5S.js → chunk-BQG7VEBY.js} +501 -1880
- package/dist/chunk-BQG7VEBY.js.map +1 -0
- package/dist/chunk-FKDMDAUR.js +2363 -0
- package/dist/chunk-FKDMDAUR.js.map +1 -0
- package/dist/{chunk-L4DIV3RC.cjs → chunk-GHUV2FLD.cjs} +9 -7
- package/dist/chunk-GHUV2FLD.cjs.map +1 -0
- package/dist/{chunk-XLIZJMMJ.js → chunk-KKKYW54Z.js} +8 -6
- package/dist/{chunk-XLIZJMMJ.js.map → chunk-KKKYW54Z.js.map} +1 -1
- package/dist/{chunk-M2TSXZ4C.cjs → chunk-KYLNC4CD.cjs} +18 -16
- package/dist/chunk-KYLNC4CD.cjs.map +1 -0
- package/dist/chunk-TKWN42TA.cjs +2259 -0
- package/dist/chunk-TKWN42TA.cjs.map +1 -0
- package/dist/{context-B25xyQrJ.d.cts → context-CTBE00S_.d.cts} +1 -1
- package/dist/{context-CGdP7_Jb.d.ts → context-lkLhbkFJ.d.ts} +1 -1
- package/dist/{effect-D6kaLM2-.d.cts → effect-BpSNEJJz.d.cts} +7 -67
- package/dist/{effect-D6kaLM2-.d.ts → effect-BpSNEJJz.d.ts} +7 -67
- package/dist/index.cjs +40 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.dev.js +92 -4
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +19 -17
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +189 -202
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +13 -23
- package/dist/internal.d.ts +13 -23
- package/dist/internal.js +195 -208
- package/dist/internal.js.map +1 -1
- package/dist/loader.cjs +280 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.d.cts +57 -0
- package/dist/loader.d.ts +57 -0
- package/dist/loader.js +280 -0
- package/dist/loader.js.map +1 -0
- package/dist/{props-BIfromL0.d.cts → props-XTHYD19o.d.cts} +13 -2
- package/dist/{props-BEgIVMRx.d.ts → props-x-HbI-jX.d.ts} +13 -2
- package/dist/resume-BrAkmSTY.d.cts +79 -0
- package/dist/resume-Dx8_l72o.d.ts +79 -0
- package/dist/{scope-CzNkn587.d.ts → scope-CdbGmsFf.d.ts} +1 -1
- package/dist/{scope-Cx_3CjIZ.d.cts → scope-DfcP9I-A.d.cts} +1 -1
- package/dist/signal-C4ISF17w.d.cts +66 -0
- package/dist/signal-C4ISF17w.d.ts +66 -0
- package/package.json +8 -3
- package/src/binding.ts +254 -5
- package/src/dom.ts +103 -5
- package/src/hooks.ts +15 -2
- package/src/hydration.ts +75 -0
- package/src/internal.ts +34 -2
- package/src/list-helpers.ts +113 -12
- package/src/loader.ts +437 -0
- package/src/node-ops.ts +65 -0
- package/src/resume.ts +517 -0
- package/src/store.ts +8 -0
- package/dist/chunk-ID3WBWNO.cjs +0 -3638
- package/dist/chunk-ID3WBWNO.cjs.map +0 -1
- package/dist/chunk-L4DIV3RC.cjs.map +0 -1
- package/dist/chunk-M2TSXZ4C.cjs.map +0 -1
- 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) {
|