fusee-framework 1.8.0 → 1.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 +64 -3
- package/app/benchmark/vite.config.js +23 -0
- package/auto-imports.d.ts +4 -0
- package/framework/auto-import-preset.js +3 -1
- package/framework/core/async-context.js +27 -0
- package/framework/core/compiler/ast.js +167 -0
- package/framework/core/compiler/errors-list.js +47 -0
- package/framework/core/compiler/errors.js +107 -0
- package/framework/core/compiler/generator.js +363 -0
- package/framework/core/compiler/lexer.js +347 -0
- package/framework/core/compiler/main-compiler.js +115 -0
- package/framework/core/compiler/parser.js +414 -0
- package/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/framework/core/compiler/transformer.js +398 -0
- package/framework/core/component.js +34 -10
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/signal.js +17 -15
- package/framework/server/actions.js +173 -0
- package/framework/server/actions.server.js +88 -0
- package/framework/server/index.js +18 -0
- package/framework/types/actions.d.ts +50 -0
- package/framework/types/actions.server.d.ts +36 -0
- package/framework/types/compiler/ast.d.ts +166 -0
- package/framework/types/compiler/errors-codes.d.ts +23 -0
- package/framework/types/compiler/errors.d.ts +43 -0
- package/framework/types/compiler/generator.d.ts +8 -0
- package/framework/types/compiler/h.d.ts +46 -0
- package/framework/types/compiler/lexer.d.ts +27 -0
- package/framework/types/compiler/main-compiler.d.ts +60 -0
- package/framework/types/compiler/parser.d.ts +4 -0
- package/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/framework/types/compiler/transformer.d.ts +28 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +6 -0
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +2 -7
- package/ghid_go_fusee_integrat_go1_26.txt +276 -0
- package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
- package/package.json +44 -36
- package/vite.config.js +65 -53
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { effect, batch, signal } from '../core/signal.js'
|
|
2
|
+
import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
|
|
3
|
+
|
|
4
|
+
export { effect as _effect, batch as _batch }
|
|
5
|
+
|
|
6
|
+
function isGetter(v) {
|
|
7
|
+
return typeof v === 'function' && !v.isSignal
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
|
|
11
|
+
const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
|
|
12
|
+
|
|
13
|
+
function sanitize(name, value) {
|
|
14
|
+
if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
|
|
15
|
+
const trimmed = String(value).trim()
|
|
16
|
+
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
17
|
+
console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
|
|
18
|
+
return 'about:blank'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function h(tag, props = {}, children = [], isStatic = false) {
|
|
25
|
+
const el = document.createElement(tag)
|
|
26
|
+
const effects = []
|
|
27
|
+
|
|
28
|
+
if (!isStatic) {
|
|
29
|
+
_applyProps(el, props, effects)
|
|
30
|
+
} else {
|
|
31
|
+
_applyStaticProps(el, props)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const childNodes = _mountChildren(el, children, effects)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
node: el,
|
|
38
|
+
effects,
|
|
39
|
+
props,
|
|
40
|
+
children: childNodes,
|
|
41
|
+
_tag: tag,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hText(value) {
|
|
46
|
+
const node = document.createTextNode('')
|
|
47
|
+
const effects = []
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'function') {
|
|
50
|
+
const cleanup = effect(() => {
|
|
51
|
+
node.textContent = String(value() ?? '')
|
|
52
|
+
})
|
|
53
|
+
effects.push(cleanup)
|
|
54
|
+
} else {
|
|
55
|
+
node.textContent = String(value ?? '')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { node, effects }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hIf(branches) {
|
|
62
|
+
const anchor = document.createComment('f-if')
|
|
63
|
+
const effects = []
|
|
64
|
+
|
|
65
|
+
let currentNodes = []
|
|
66
|
+
let currentBranch = -1
|
|
67
|
+
|
|
68
|
+
const cleanup = effect(() => {
|
|
69
|
+
let matchIdx = -1
|
|
70
|
+
for (let i = 0; i < branches.length; i++) {
|
|
71
|
+
const [cond] = branches[i]
|
|
72
|
+
if (cond === null || cond()) { matchIdx = i; break }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (matchIdx === currentBranch) return
|
|
76
|
+
|
|
77
|
+
_unmountNodes(currentNodes)
|
|
78
|
+
currentNodes = []
|
|
79
|
+
currentBranch = matchIdx
|
|
80
|
+
|
|
81
|
+
if (matchIdx === -1) return
|
|
82
|
+
|
|
83
|
+
const [, getChildren] = branches[matchIdx]
|
|
84
|
+
const children = getChildren()
|
|
85
|
+
const parent = anchor.parentNode
|
|
86
|
+
|
|
87
|
+
if (parent) {
|
|
88
|
+
for (const fnode of children) {
|
|
89
|
+
parent.insertBefore(fnode.node, anchor)
|
|
90
|
+
currentNodes.push(fnode)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
effects.push(cleanup)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
node: anchor,
|
|
99
|
+
effects,
|
|
100
|
+
_ifBranchNodes: () => currentNodes,
|
|
101
|
+
_isIf: true,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function hFor(sourceGetter, renderItem, keyFn) {
|
|
106
|
+
const anchor = document.createComment('f-for')
|
|
107
|
+
const effects = []
|
|
108
|
+
|
|
109
|
+
let keyedMap = new Map()
|
|
110
|
+
const keyEvaluatorSignal = signal(null)
|
|
111
|
+
|
|
112
|
+
const cleanup = effect(() => {
|
|
113
|
+
const list = sourceGetter() ?? []
|
|
114
|
+
const parent = anchor.parentNode
|
|
115
|
+
|
|
116
|
+
if (!parent) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const newMap = new Map()
|
|
121
|
+
const newKeys = []
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < list.length; i++) {
|
|
124
|
+
const item = list[i]
|
|
125
|
+
|
|
126
|
+
keyEvaluatorSignal(item)
|
|
127
|
+
const key = keyFn(keyEvaluatorSignal, i)
|
|
128
|
+
newKeys.push(key)
|
|
129
|
+
|
|
130
|
+
const exists = keyedMap.get(key)
|
|
131
|
+
if (exists) {
|
|
132
|
+
if (exists.itemSignal() !== item) {
|
|
133
|
+
exists.itemSignal(item)
|
|
134
|
+
}
|
|
135
|
+
newMap.set(key, exists)
|
|
136
|
+
} else {
|
|
137
|
+
const itemSignal = signal(item)
|
|
138
|
+
const fnode = renderItem(itemSignal, i)
|
|
139
|
+
newMap.set(key, { fnode, key, itemSignal })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const [key, entry] of keyedMap) {
|
|
144
|
+
if (!newMap.has(key)) {
|
|
145
|
+
_unmountNodes([entry.fnode])
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (let i = newKeys.length - 1; i >= 0; i--) {
|
|
150
|
+
const key = newKeys[i]
|
|
151
|
+
const entry = newMap.get(key)
|
|
152
|
+
const ref = i === newKeys.length - 1 ? anchor : newMap.get(newKeys[i + 1])?.fnode?.node
|
|
153
|
+
|
|
154
|
+
if (ref && entry.fnode.node.nextSibling !== ref) {
|
|
155
|
+
parent.insertBefore(entry.fnode.node, ref ?? anchor)
|
|
156
|
+
} else if (!entry.fnode.node.parentNode) {
|
|
157
|
+
parent.insertBefore(entry.fnode.node, anchor)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
keyedMap = newMap
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
effects.push(cleanup)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
node: anchor,
|
|
168
|
+
effects,
|
|
169
|
+
_isFor: true,
|
|
170
|
+
_getNodes: () => [...keyedMap.values()].map(e => e.fnode),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function hSlot(slots, name, fallback = []) {
|
|
175
|
+
const anchor = document.createComment(`slot:${name}`)
|
|
176
|
+
const effects = []
|
|
177
|
+
const slotFn = slots?.[name]
|
|
178
|
+
const nodes = typeof slotFn === 'function' ? slotFn() : fallback
|
|
179
|
+
return {
|
|
180
|
+
node: anchor,
|
|
181
|
+
effects,
|
|
182
|
+
_slotNodes: nodes,
|
|
183
|
+
_isSlot: true,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createComponent(name, ComponentFn, rawProps = {}, rawSlots = {}) {
|
|
188
|
+
if (!ComponentFn) {
|
|
189
|
+
console.warn(`[fusée] Component "${name}" is not registered`)
|
|
190
|
+
const placeholder = document.createComment(`missing:${name}`)
|
|
191
|
+
return { node: placeholder, effects: [] }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const resolvedProps = _resolveComponentProps(rawProps)
|
|
195
|
+
const listeners = {}
|
|
196
|
+
const slots = {}
|
|
197
|
+
|
|
198
|
+
for (const [key, value] of Object.entries(rawProps)) {
|
|
199
|
+
if (key.startsWith('on:')) {
|
|
200
|
+
listeners[key.slice(3)] = value
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const [slotName, slotFn] of Object.entries(rawSlots)) {
|
|
205
|
+
slots[slotName] = slotFn
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const container = document.createElement('div')
|
|
209
|
+
container.setAttribute('data-fusee-component', name)
|
|
210
|
+
|
|
211
|
+
const api = ComponentFn(resolvedProps, { listeners, slots })
|
|
212
|
+
api.render(container)
|
|
213
|
+
const root = container.firstElementChild ?? container
|
|
214
|
+
|
|
215
|
+
const effects = []
|
|
216
|
+
effects.push(() => api.unmount())
|
|
217
|
+
|
|
218
|
+
return { node: root, effects, _component: api }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function _applyProps(el, props, effects) {
|
|
222
|
+
for (const [key, value] of Object.entries(props)) {
|
|
223
|
+
if (key.startsWith('@')) {
|
|
224
|
+
const eventName = key.slice(1)
|
|
225
|
+
const { handler, modifiers = [] } = value
|
|
226
|
+
const handlerState = { timeoutId: null, throttleTimeoutId: null, lastRun: 0 }
|
|
227
|
+
const wrappedHandler = createEventHandler(handler, modifiers, null, null, handlerState)
|
|
228
|
+
|
|
229
|
+
let cleanup
|
|
230
|
+
if (isDelegatedEvent(eventName)) {
|
|
231
|
+
cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, { modifiers, handlerState })
|
|
232
|
+
} else {
|
|
233
|
+
el.addEventListener(eventName, wrappedHandler)
|
|
234
|
+
cleanup = () => el.removeEventListener(eventName, wrappedHandler)
|
|
235
|
+
}
|
|
236
|
+
effects.push(cleanup)
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (key === 'f-show') {
|
|
241
|
+
const cleanup = effect(() => {
|
|
242
|
+
el.style.display = value() ? '' : 'none'
|
|
243
|
+
})
|
|
244
|
+
effects.push(cleanup)
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (key === 'f-html') {
|
|
249
|
+
const cleanup = effect(() => {
|
|
250
|
+
el.innerHTML = String(value() ?? '')
|
|
251
|
+
})
|
|
252
|
+
effects.push(cleanup)
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (key === 'f-model') {
|
|
257
|
+
const sig = value()
|
|
258
|
+
const cleanup = effect(() => {
|
|
259
|
+
const v = typeof sig === 'function' ? sig() : sig
|
|
260
|
+
if (el.value !== String(v ?? '')) el.value = String(v ?? '')
|
|
261
|
+
})
|
|
262
|
+
effects.push(cleanup)
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (key === 'f-ref') {
|
|
267
|
+
el.dispatchEvent(new CustomEvent('fusee:ref', {
|
|
268
|
+
detail: { name: value },
|
|
269
|
+
bubbles: true,
|
|
270
|
+
}))
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (key === 'f-once') continue
|
|
275
|
+
|
|
276
|
+
if (typeof value !== 'function') {
|
|
277
|
+
_applyAttr(el, key, value)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const attrName = key
|
|
282
|
+
const getter = value
|
|
283
|
+
const cleanup = effect(() => {
|
|
284
|
+
_applyAttr(el, attrName, getter())
|
|
285
|
+
})
|
|
286
|
+
effects.push(cleanup)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function _applyStaticProps(el, props) {
|
|
291
|
+
for (const [key, value] of Object.entries(props)) {
|
|
292
|
+
if (key.startsWith('@') || key.startsWith('f-'))
|
|
293
|
+
continue
|
|
294
|
+
if (typeof value !== 'function')
|
|
295
|
+
_applyAttr(el, key, value)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function _applyAttr(el, name, value) {
|
|
300
|
+
if (name === 'class') {
|
|
301
|
+
const cls = _resolveClass(value)
|
|
302
|
+
if (cls) el.className = cls
|
|
303
|
+
else el.removeAttribute('class')
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (name === 'style') {
|
|
308
|
+
if (value !== null && typeof value === 'object') {
|
|
309
|
+
for (const k in value) el.style[k] = value[k]
|
|
310
|
+
} else {
|
|
311
|
+
el.style.cssText = String(value ?? '')
|
|
312
|
+
}
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (name in el && typeof el[name] !== 'undefined' && name !== 'type') {
|
|
317
|
+
try {
|
|
318
|
+
el[name] = value
|
|
319
|
+
return
|
|
320
|
+
} catch (_) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (typeof value === 'boolean') {
|
|
326
|
+
value ? el.setAttribute(name, '') : el.removeAttribute(name)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (value == null) {
|
|
331
|
+
el.removeAttribute(name)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
el.setAttribute(name, sanitize(name, String(value)))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function _resolveClass(value) {
|
|
339
|
+
if (typeof value === 'string')
|
|
340
|
+
return value
|
|
341
|
+
if (Array.isArray(value))
|
|
342
|
+
return value.filter(Boolean).join(' ')
|
|
343
|
+
if (value && typeof value === 'object')
|
|
344
|
+
return Object.entries(value).filter(([, v]) => !!v).map(([k]) => k).join(' ')
|
|
345
|
+
return String(value ?? '')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function _mountChildren(parent, children, parentEffects) {
|
|
349
|
+
const mounted = []
|
|
350
|
+
|
|
351
|
+
for (const fnode of children) {
|
|
352
|
+
if (!fnode || !fnode.node) continue
|
|
353
|
+
|
|
354
|
+
parent.appendChild(fnode.node)
|
|
355
|
+
|
|
356
|
+
if (fnode._isSlot && fnode._slotNodes) {
|
|
357
|
+
for (const sn of fnode._slotNodes) {
|
|
358
|
+
if (sn?.node) parent.insertBefore(sn.node, null)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (fnode.effects) parentEffects.push(...fnode.effects)
|
|
363
|
+
mounted.push(fnode)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return mounted
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function _unmountNodes(fnodes) {
|
|
370
|
+
for (const fnode of fnodes) {
|
|
371
|
+
if (!fnode) continue
|
|
372
|
+
|
|
373
|
+
if (fnode.effects) {
|
|
374
|
+
for (let i = fnode.effects.length - 1; i >= 0; i--) {
|
|
375
|
+
const cleanup = fnode.effects[i]
|
|
376
|
+
if (typeof cleanup === 'function') cleanup()
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (fnode.node?.parentNode) {
|
|
381
|
+
fnode.node.parentNode.removeChild(fnode.node)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (fnode._ifBranchNodes) {
|
|
385
|
+
_unmountNodes(fnode._ifBranchNodes())
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (fnode._getNodes) {
|
|
389
|
+
_unmountNodes(fnode._getNodes())
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _resolveComponentProps(rawProps) {
|
|
395
|
+
const resolved = {}
|
|
396
|
+
|
|
397
|
+
for (const [key, value] of Object.entries(rawProps)) {
|
|
398
|
+
if (key.startsWith('on:')) continue
|
|
399
|
+
|
|
400
|
+
if (isGetter(value)) {
|
|
401
|
+
Object.defineProperty(resolved, key, {
|
|
402
|
+
get: value,
|
|
403
|
+
enumerable: true,
|
|
404
|
+
configurable: true,
|
|
405
|
+
})
|
|
406
|
+
} else {
|
|
407
|
+
resolved[key] = value
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return resolved
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function mount(renderFn, ctx, components, container) {
|
|
415
|
+
const fnodes = renderFn(ctx, components)
|
|
416
|
+
const effects = []
|
|
417
|
+
|
|
418
|
+
for (const fnode of fnodes) {
|
|
419
|
+
if (!fnode?.node) continue
|
|
420
|
+
container.appendChild(fnode.node)
|
|
421
|
+
if (fnode.effects) effects.push(...fnode.effects)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
unmount() {
|
|
426
|
+
_unmountNodes(fnodes)
|
|
427
|
+
container.innerHTML = ''
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const _hydrationRegistry = new Map()
|
|
2
|
+
const _registeredCaches = []
|
|
3
|
+
|
|
4
|
+
export function _registerResourceCache(resourceKey, cache) {
|
|
5
|
+
_registeredCaches.push({ resourceKey, cache })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function extractHydrationData() {
|
|
9
|
+
const snapshot = {}
|
|
10
|
+
|
|
11
|
+
for (const { resourceKey, cache } of _registeredCaches) {
|
|
12
|
+
const entries = {}
|
|
13
|
+
for (const [cacheKey, entry] of cache) {
|
|
14
|
+
entries[cacheKey] = { data: entry.data, updatedAt: entry.updatedAt }
|
|
15
|
+
}
|
|
16
|
+
if (Object.keys(entries).length > 0) {
|
|
17
|
+
snapshot[resourceKey] = entries
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return snapshot
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dehydrate(snapshot = extractHydrationData()) {
|
|
25
|
+
try {
|
|
26
|
+
const json = JSON.stringify(snapshot)
|
|
27
|
+
return json.replace(/<\/script>/gi, '<\\/script>')
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[fusée] dehydrate() failed to serialize hydration data:', err)
|
|
30
|
+
return '{}'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadHydration(snapshot) {
|
|
35
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
36
|
+
console.warn('[fusée] loadHydration() received invalid snapshot')
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [resourceKey, entries] of Object.entries(snapshot)) {
|
|
41
|
+
if (!_hydrationRegistry.has(resourceKey)) {
|
|
42
|
+
_hydrationRegistry.set(resourceKey, new Map())
|
|
43
|
+
}
|
|
44
|
+
const map = _hydrationRegistry.get(resourceKey)
|
|
45
|
+
for (const [cacheKey, entry] of Object.entries(entries)) {
|
|
46
|
+
map.set(cacheKey, { data: entry.data, updatedAt: entry.updatedAt })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hydrateFromWindow() {
|
|
52
|
+
if (typeof window === 'undefined') return
|
|
53
|
+
const raw = window.__FUSEE_HYDRATION__
|
|
54
|
+
if (raw) loadHydration(raw)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getHydratedEntries(resourceKey) {
|
|
58
|
+
return _hydrationRegistry.get(resourceKey) ?? null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isHydrationFresh(resourceKey, cacheKey, staleTime = 0) {
|
|
62
|
+
const entries = _hydrationRegistry.get(resourceKey)
|
|
63
|
+
if (!entries) return false
|
|
64
|
+
const entry = entries.get(cacheKey)
|
|
65
|
+
if (!entry) return false
|
|
66
|
+
return (Date.now() - entry.updatedAt) <= staleTime
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function clearHydration() {
|
|
70
|
+
_hydrationRegistry.clear()
|
|
71
|
+
_registeredCaches.length = 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getHydrationSnapshot() {
|
|
75
|
+
const out = {}
|
|
76
|
+
for (const [resourceKey, map] of _hydrationRegistry) {
|
|
77
|
+
out[resourceKey] = {}
|
|
78
|
+
for (const [cacheKey, entry] of map) {
|
|
79
|
+
out[resourceKey][cacheKey] = { ...entry }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
package/framework/core/signal.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { getInFlightStore } from './async-context.js'
|
|
2
|
+
import { _registerResourceCache, getHydratedEntries } from './hydration.js'
|
|
3
|
+
|
|
1
4
|
let currentEffect = null
|
|
2
5
|
let currentOwner = null
|
|
3
6
|
let onEffectCreated = null
|
|
@@ -431,9 +434,6 @@ function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
|
|
|
431
434
|
return !equals(newValue, oldValue)
|
|
432
435
|
}
|
|
433
436
|
|
|
434
|
-
const globalInFlightByKey = new Map()
|
|
435
|
-
const globalInFlightByFetcher = new WeakMap()
|
|
436
|
-
|
|
437
437
|
export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
|
|
438
438
|
let source = null
|
|
439
439
|
let actualFetcher = null
|
|
@@ -462,9 +462,13 @@ export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
|
|
|
462
462
|
|
|
463
463
|
let currentPromiseId = 0
|
|
464
464
|
let currentPromise = null
|
|
465
|
-
const cache = new Map()
|
|
466
465
|
const staleTime = typeof options.staleTime === 'number' ? options.staleTime : 0
|
|
467
466
|
|
|
467
|
+
const resourceKey = options.key !== undefined ? String(options.key) : (actualFetcher.name || 'anonymous')
|
|
468
|
+
const hydratedEntries = getHydratedEntries(resourceKey)
|
|
469
|
+
const cache = hydratedEntries ? new Map(hydratedEntries) : new Map()
|
|
470
|
+
_registerResourceCache(resourceKey, cache)
|
|
471
|
+
|
|
468
472
|
function serializeKey(input) {
|
|
469
473
|
if (input === undefined) return 'undefined'
|
|
470
474
|
if (input === null) return 'null'
|
|
@@ -509,32 +513,30 @@ export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
|
|
|
509
513
|
|
|
510
514
|
let promise
|
|
511
515
|
|
|
512
|
-
|
|
513
|
-
const dedupeKey = options.key ? `key_${options.key}_${key}` : `fn_${actualFetcher.name}_${key}`;
|
|
514
|
-
promise = globalInFlightByKey.get(dedupeKey)
|
|
516
|
+
const { byKey, byFetcher } = getInFlightStore()
|
|
515
517
|
|
|
518
|
+
if (options.key !== undefined) {
|
|
519
|
+
const dedupeKey = `key_${options.key}_${key}`
|
|
520
|
+
promise = byKey.get(dedupeKey)
|
|
516
521
|
if (!promise) {
|
|
517
522
|
promise = makeScheduledPromise(() => actualFetcher(input))
|
|
518
|
-
|
|
519
|
-
|
|
523
|
+
byKey.set(dedupeKey, promise)
|
|
520
524
|
promise.finally(() => {
|
|
521
|
-
if (
|
|
522
|
-
|
|
525
|
+
if (byKey.get(dedupeKey) === promise) {
|
|
526
|
+
byKey.delete(dedupeKey)
|
|
523
527
|
}
|
|
524
528
|
}).catch(() => {})
|
|
525
529
|
}
|
|
526
530
|
} else {
|
|
527
|
-
let inFlightMap =
|
|
531
|
+
let inFlightMap = byFetcher.get(actualFetcher)
|
|
528
532
|
if (!inFlightMap) {
|
|
529
533
|
inFlightMap = new Map()
|
|
530
|
-
|
|
534
|
+
byFetcher.set(actualFetcher, inFlightMap)
|
|
531
535
|
}
|
|
532
|
-
|
|
533
536
|
promise = inFlightMap.get(key)
|
|
534
537
|
if (!promise) {
|
|
535
538
|
promise = makeScheduledPromise(() => actualFetcher(input))
|
|
536
539
|
inFlightMap.set(key, promise)
|
|
537
|
-
|
|
538
540
|
promise.finally(() => {
|
|
539
541
|
if (inFlightMap.get(key) === promise) {
|
|
540
542
|
inFlightMap.delete(key)
|