@sigil-dev/runtime 0.9.6 → 0.9.7
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 +140 -140
- package/index.test.ts +44 -0
- package/index.ts +901 -850
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,850 +1,901 @@
|
|
|
1
|
-
export { __asyncBoundary } from "./async-boundary.ts";
|
|
2
|
-
export { Boundary } from "./boundary";
|
|
3
|
-
export {
|
|
4
|
-
easeInCubic,
|
|
5
|
-
easeInOutCubic,
|
|
6
|
-
easeInOutQuad,
|
|
7
|
-
easeInOutQuart,
|
|
8
|
-
easeInOutQuint,
|
|
9
|
-
easeInOutSine,
|
|
10
|
-
easeInQuad,
|
|
11
|
-
easeInQuart,
|
|
12
|
-
easeInQuint,
|
|
13
|
-
easeInSine,
|
|
14
|
-
easeOutCubic,
|
|
15
|
-
easeOutQuad,
|
|
16
|
-
easeOutQuart,
|
|
17
|
-
easeOutQuint,
|
|
18
|
-
easeOutSine,
|
|
19
|
-
linear,
|
|
20
|
-
} from "./easing.ts";
|
|
21
|
-
export { ReactiveMap } from "./reactivity/map.ts";
|
|
22
|
-
export { ReactiveSet } from "./reactivity/set.ts";
|
|
23
|
-
export { mountTransition, removeAnimated } from "./transition.ts";
|
|
24
|
-
|
|
25
|
-
export const SIGIL_SAFE = Symbol.for("sigil.safe");
|
|
26
|
-
export class SafeHtml {
|
|
27
|
-
[SIGIL_SAFE] = true;
|
|
28
|
-
constructor(public v: string) {}
|
|
29
|
-
toString() {
|
|
30
|
-
return this.v;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let currentEffect: (() => void) | null = null;
|
|
35
|
-
let currentDeps: Set<Set<() => void>> | null = null;
|
|
36
|
-
|
|
37
|
-
let batchDepth = 0;
|
|
38
|
-
let scheduledFlush = false;
|
|
39
|
-
const pendingEffects = new Set<() => void>();
|
|
40
|
-
const pendingPreEffects = new Set<() => void>();
|
|
41
|
-
|
|
42
|
-
function flush() {
|
|
43
|
-
scheduledFlush = false;
|
|
44
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
45
|
-
pendingPreEffects.forEach((e) => e());
|
|
46
|
-
pendingPreEffects.clear();
|
|
47
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
48
|
-
pendingEffects.forEach((e) => e());
|
|
49
|
-
pendingEffects.clear();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function scheduleEffect(fn: () => void) {
|
|
53
|
-
const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
|
|
54
|
-
target.add(fn);
|
|
55
|
-
if (batchDepth === 0 && !scheduledFlush) {
|
|
56
|
-
scheduledFlush = true;
|
|
57
|
-
queueMicrotask(flush);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const targetDepsMap = new WeakMap<
|
|
62
|
-
object,
|
|
63
|
-
Map<string | symbol, Set<() => void>>
|
|
64
|
-
>();
|
|
65
|
-
|
|
66
|
-
function getDeps(target: object, key: string | symbol): Set<() => void> {
|
|
67
|
-
let deps = targetDepsMap.get(target);
|
|
68
|
-
if (!deps) {
|
|
69
|
-
deps = new Map();
|
|
70
|
-
targetDepsMap.set(target, deps);
|
|
71
|
-
}
|
|
72
|
-
let set = deps.get(key);
|
|
73
|
-
if (!set) {
|
|
74
|
-
set = new Set();
|
|
75
|
-
deps.set(key, set);
|
|
76
|
-
}
|
|
77
|
-
return set;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function track(target: object, key: string | symbol) {
|
|
81
|
-
if (currentEffect && currentDeps) {
|
|
82
|
-
const deps = getDeps(target, key);
|
|
83
|
-
deps.add(currentEffect);
|
|
84
|
-
currentDeps.add(deps); // Set, deduplicates automatically
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function trigger(target: object, key: string | symbol) {
|
|
89
|
-
const deps = targetDepsMap.get(target);
|
|
90
|
-
if (!deps) return;
|
|
91
|
-
const set = deps.get(key);
|
|
92
|
-
if (!set) return;
|
|
93
|
-
for (const fn of [...set]) {
|
|
94
|
-
scheduleEffect(fn);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const proxyCache = new WeakMap<object, object>();
|
|
99
|
-
const proxyToTarget = new WeakMap<object, object>();
|
|
100
|
-
const reactiveTargets = new WeakSet<object>();
|
|
101
|
-
const ARRAY_MUTATORS = new Set([
|
|
102
|
-
"push",
|
|
103
|
-
"pop",
|
|
104
|
-
"shift",
|
|
105
|
-
"unshift",
|
|
106
|
-
"splice",
|
|
107
|
-
"sort",
|
|
108
|
-
"reverse",
|
|
109
|
-
"fill",
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
function createReactiveProxy<T extends object>(target: T): T {
|
|
113
|
-
const existing = proxyCache.get(target);
|
|
114
|
-
if (existing) return existing as T;
|
|
115
|
-
|
|
116
|
-
const proxy = new Proxy(target, {
|
|
117
|
-
get(target, key, receiver) {
|
|
118
|
-
if (
|
|
119
|
-
Array.isArray(target) &&
|
|
120
|
-
typeof key === "string" &&
|
|
121
|
-
ARRAY_MUTATORS.has(key)
|
|
122
|
-
) {
|
|
123
|
-
return (...args: any[]) => {
|
|
124
|
-
const result = (target as any)[key](...args);
|
|
125
|
-
trigger(target, "length");
|
|
126
|
-
// trigger all numeric keys that changed — for splice/sort
|
|
127
|
-
// triggering length is sufficient for most reactive patterns
|
|
128
|
-
return result;
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (key === "set") {
|
|
132
|
-
return (newValue: T) => {
|
|
133
|
-
const oldKeys = new Set(Object.keys(target));
|
|
134
|
-
const newKeys = new Set(Object.keys(newValue));
|
|
135
|
-
for (const k of oldKeys) {
|
|
136
|
-
if (!newKeys.has(k)) {
|
|
137
|
-
delete (target as any)[k];
|
|
138
|
-
trigger(target, k);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
for (const k of newKeys) {
|
|
142
|
-
(target as any)[k] = (newValue as any)[k];
|
|
143
|
-
trigger(target, k);
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(target)) {
|
|
146
|
-
// delete leaves holes — length must shrink to the new value's,
|
|
147
|
-
// or iteration (spread, reconcile) sees phantom trailing slots
|
|
148
|
-
target.length = (newValue as unknown[]).length;
|
|
149
|
-
trigger(target, "length");
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (key === "peek") {
|
|
155
|
-
return () => target;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (typeof key === "symbol") {
|
|
159
|
-
return Reflect.get(target, key, receiver);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
track(target, key);
|
|
163
|
-
const value = target[key as keyof T];
|
|
164
|
-
|
|
165
|
-
if (typeof value === "object" && value !== null) {
|
|
166
|
-
return createReactiveProxy(value);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return value;
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
set(target, key, newValue, receiver) {
|
|
173
|
-
const oldValue = target[key as keyof T];
|
|
174
|
-
const result = Reflect.set(target, key, newValue, receiver);
|
|
175
|
-
if (oldValue !== newValue) {
|
|
176
|
-
trigger(target, key as string | symbol);
|
|
177
|
-
if (
|
|
178
|
-
Array.isArray(target) &&
|
|
179
|
-
typeof key === "string" &&
|
|
180
|
-
!Number.isNaN(Number(key))
|
|
181
|
-
) {
|
|
182
|
-
trigger(target, "length");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
deleteProperty(target, key) {
|
|
189
|
-
const result = Reflect.deleteProperty(target, key);
|
|
190
|
-
trigger(target, key as string | symbol);
|
|
191
|
-
return result;
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
proxyCache.set(target, proxy);
|
|
196
|
-
proxyToTarget.set(proxy, target);
|
|
197
|
-
reactiveTargets.add(target);
|
|
198
|
-
return proxy as T;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── createSignal (branches on type) ───
|
|
202
|
-
|
|
203
|
-
export type Signal<T> = {
|
|
204
|
-
(): T;
|
|
205
|
-
set: (next: T) => void;
|
|
206
|
-
peek: () => T;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export function batch(fn: () => void) {
|
|
210
|
-
batchDepth++;
|
|
211
|
-
fn();
|
|
212
|
-
batchDepth--;
|
|
213
|
-
if (batchDepth === 0) {
|
|
214
|
-
scheduledFlush = false;
|
|
215
|
-
flush();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function tick(): Promise<void> {
|
|
220
|
-
return new Promise((resolve) => queueMicrotask(resolve));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function createPrimitiveSignal<T>(initial: T): Signal<T> {
|
|
224
|
-
const subs = new Set<() => void>();
|
|
225
|
-
let value = initial;
|
|
226
|
-
|
|
227
|
-
const getter = () => {
|
|
228
|
-
if (currentEffect && currentDeps) {
|
|
229
|
-
subs.add(currentEffect);
|
|
230
|
-
currentDeps.add(subs);
|
|
231
|
-
}
|
|
232
|
-
return value;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
getter.set = (next: T) => {
|
|
236
|
-
value = next;
|
|
237
|
-
for (const fn of [...subs]) {
|
|
238
|
-
scheduleEffect(fn);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
getter.peek = () => value;
|
|
243
|
-
|
|
244
|
-
return getter;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function createSignal<T>(initial: T): Signal<T> {
|
|
248
|
-
if (typeof initial === "object" && initial !== null) {
|
|
249
|
-
const proxy = createReactiveProxy(initial);
|
|
250
|
-
const getter = () => {
|
|
251
|
-
// array signals: auto-track length so push/pop/splice re-run effects
|
|
252
|
-
if (Array.isArray(initial)) {
|
|
253
|
-
track(initial, "length");
|
|
254
|
-
}
|
|
255
|
-
return proxy;
|
|
256
|
-
};
|
|
257
|
-
getter.set = (next: T) => (proxy as any).set(next);
|
|
258
|
-
getter.peek = () => initial;
|
|
259
|
-
return getter;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return createPrimitiveSignal(initial);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// A1: $state.raw — create signal without Proxy wrapping for objects/arrays
|
|
266
|
-
export function createRawSignal<T>(initial: T): Signal<T> {
|
|
267
|
-
return createPrimitiveSignal(initial);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// A2: $state.snapshot — deep un-proxy a signal value
|
|
271
|
-
function deepUnproxy<T>(value: T): T {
|
|
272
|
-
if (typeof value !== "object" || value === null) return value;
|
|
273
|
-
const raw = proxyToTarget.get(value as object);
|
|
274
|
-
if (!raw) return value; // not a proxy, return as-is
|
|
275
|
-
if (Array.isArray(raw)) {
|
|
276
|
-
return raw.map(deepUnproxy) as T;
|
|
277
|
-
}
|
|
278
|
-
const result: Record<string, unknown> = {};
|
|
279
|
-
for (const key of Object.keys(raw)) {
|
|
280
|
-
result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
|
|
281
|
-
}
|
|
282
|
-
return result as T;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function snapshot<T>(value: T): T {
|
|
286
|
-
return deepUnproxy(value);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// A3: $effect.tracking() — check if inside reactive context
|
|
290
|
-
export function tracking(): boolean {
|
|
291
|
-
return currentEffect !== null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Run fn() outside of any reactive context: signal reads inside fn() do not
|
|
296
|
-
* register dependencies on the enclosing effect.
|
|
297
|
-
*/
|
|
298
|
-
export function untrack<T>(fn: () => T): T {
|
|
299
|
-
const prevEffect = currentEffect;
|
|
300
|
-
const prevDeps = currentDeps;
|
|
301
|
-
currentEffect = null;
|
|
302
|
-
currentDeps = null;
|
|
303
|
-
try {
|
|
304
|
-
return fn();
|
|
305
|
-
} finally {
|
|
306
|
-
currentEffect = prevEffect;
|
|
307
|
-
currentDeps = prevDeps;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// A4: $inspect — dev-only effect logging
|
|
312
|
-
export function createInspectEffect(
|
|
313
|
-
getValues: () => unknown[],
|
|
314
|
-
cb?: (vals: unknown[]) => void,
|
|
315
|
-
): void {
|
|
316
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
317
|
-
return;
|
|
318
|
-
createEffect(() => {
|
|
319
|
-
const vals = getValues();
|
|
320
|
-
if (cb) cb(vals);
|
|
321
|
-
else console.log(...vals);
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// A5: $effect.pre — effect that flushes before DOM update
|
|
326
|
-
export function createEffectPre(
|
|
327
|
-
fn: () => undefined | (() => void),
|
|
328
|
-
): () => void {
|
|
329
|
-
let cleanup: (() => void) | undefined;
|
|
330
|
-
const ownDeps = new Set<Set<() => void>>();
|
|
331
|
-
|
|
332
|
-
const effect = () => {
|
|
333
|
-
if (cleanup) cleanup();
|
|
334
|
-
for (const depSet of ownDeps) depSet.delete(effect);
|
|
335
|
-
ownDeps.clear();
|
|
336
|
-
|
|
337
|
-
const prevEffect = currentEffect;
|
|
338
|
-
const prevDeps = currentDeps;
|
|
339
|
-
currentEffect = effect;
|
|
340
|
-
currentDeps = ownDeps;
|
|
341
|
-
try {
|
|
342
|
-
cleanup = fn();
|
|
343
|
-
} finally {
|
|
344
|
-
currentEffect = prevEffect;
|
|
345
|
-
currentDeps = prevDeps;
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
(effect as any).__sigil_pre = true;
|
|
350
|
-
effect();
|
|
351
|
-
|
|
352
|
-
const dispose = () => {
|
|
353
|
-
for (const depSet of ownDeps) depSet.delete(effect);
|
|
354
|
-
ownDeps.clear();
|
|
355
|
-
if (cleanup) cleanup();
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
359
|
-
dispose,
|
|
360
|
-
);
|
|
361
|
-
return dispose;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// A9: Profiler effect timing (dev)
|
|
365
|
-
export function createProfilerEffect(
|
|
366
|
-
_name: string,
|
|
367
|
-
fn: () => undefined | (() => void),
|
|
368
|
-
): () => void {
|
|
369
|
-
return createEffect(() => {
|
|
370
|
-
if (
|
|
371
|
-
typeof process !== "undefined" &&
|
|
372
|
-
process.env.NODE_ENV === "production"
|
|
373
|
-
) {
|
|
374
|
-
return fn();
|
|
375
|
-
}
|
|
376
|
-
const _start = performance.now();
|
|
377
|
-
const cleanup = fn();
|
|
378
|
-
const _end = performance.now();
|
|
379
|
-
// console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
|
|
380
|
-
return cleanup;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Deferred effect mode for hydration: effects created while the flag is set
|
|
385
|
-
// are queued instead of run immediately, then flushed after DOM insertion.
|
|
386
|
-
const deferredEffectQueue: (() => void)[] = [];
|
|
387
|
-
|
|
388
|
-
export function flushDeferredEffects(): void {
|
|
389
|
-
const pending = deferredEffectQueue.splice(0);
|
|
390
|
-
for (const run of pending) run();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
|
|
394
|
-
export function createEffect(fn: () => void | (() => void)): () => void {
|
|
395
|
-
let cleanup: (() => void) | undefined;
|
|
396
|
-
const ownDeps = new Set<Set<() => void>>();
|
|
397
|
-
|
|
398
|
-
const effect = () => {
|
|
399
|
-
if (cleanup) cleanup();
|
|
400
|
-
for (const depSet of ownDeps) depSet.delete(effect);
|
|
401
|
-
ownDeps.clear();
|
|
402
|
-
|
|
403
|
-
const prevEffect = currentEffect;
|
|
404
|
-
const prevDeps = currentDeps;
|
|
405
|
-
currentEffect = effect;
|
|
406
|
-
currentDeps = ownDeps;
|
|
407
|
-
try {
|
|
408
|
-
//@ts-expect-error kill yourself
|
|
409
|
-
cleanup = fn();
|
|
410
|
-
} finally {
|
|
411
|
-
currentEffect = prevEffect;
|
|
412
|
-
currentDeps = prevDeps;
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
// In deferred mode (hydration), queue the effect instead of running immediately.
|
|
417
|
-
// This ensures the DOM is inserted before effect callbacks query it.
|
|
418
|
-
if ((globalThis as any).__sigilDeferEffects) {
|
|
419
|
-
deferredEffectQueue.push(() => effect());
|
|
420
|
-
} else {
|
|
421
|
-
effect();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const dispose = () => {
|
|
425
|
-
for (const depSet of ownDeps) depSet.delete(effect);
|
|
426
|
-
ownDeps.clear();
|
|
427
|
-
if (cleanup) cleanup();
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
431
|
-
dispose,
|
|
432
|
-
);
|
|
433
|
-
return dispose;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Run fn() and return a dispose function that tears down every effect
|
|
438
|
-
* created during that call (including effects inside child createMemo calls).
|
|
439
|
-
* Supports nesting: inner scopes don't leak to outer ones.
|
|
440
|
-
*/
|
|
441
|
-
export function withEffectScope(fn: () => void): () => void {
|
|
442
|
-
const prev = (globalThis as any).__sigilEffectScope;
|
|
443
|
-
const disposers: (() => void)[] = [];
|
|
444
|
-
(globalThis as any).__sigilEffectScope = disposers;
|
|
445
|
-
try {
|
|
446
|
-
fn();
|
|
447
|
-
} finally {
|
|
448
|
-
(globalThis as any).__sigilEffectScope = prev;
|
|
449
|
-
}
|
|
450
|
-
return () => {
|
|
451
|
-
for (const d of disposers) d();
|
|
452
|
-
disposers.length = 0;
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
type EffectScopeGlobal = { __sigilEffectScope?: (() => void)[] };
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Run fn() with a detached effect scope. Effects created inside are invisible
|
|
460
|
-
* to enclosing scopes (the scope array is replaced, not merged) and live until
|
|
461
|
-
* dispose() is called — for app bootstrap, routers, and long-lived listeners.
|
|
462
|
-
*/
|
|
463
|
-
export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
464
|
-
const g = globalThis as EffectScopeGlobal;
|
|
465
|
-
const prev = g.__sigilEffectScope;
|
|
466
|
-
const disposers: (() => void)[] = [];
|
|
467
|
-
g.__sigilEffectScope = disposers;
|
|
468
|
-
const dispose = () => {
|
|
469
|
-
for (const d of disposers.splice(0)) d();
|
|
470
|
-
};
|
|
471
|
-
try {
|
|
472
|
-
return fn(dispose);
|
|
473
|
-
} finally {
|
|
474
|
-
g.__sigilEffectScope = prev;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
export interface Resource<T> {
|
|
479
|
-
data: () => T | undefined;
|
|
480
|
-
loading: () => boolean;
|
|
481
|
-
error: () => Error | undefined;
|
|
482
|
-
refetch: () => void;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Async data source with reactive re-fetching. Re-runs when `source()` changes
|
|
487
|
-
* or refetch() is called; stale responses are discarded by request id.
|
|
488
|
-
*/
|
|
489
|
-
export function createResource<T>(
|
|
490
|
-
fetcher: (source: unknown, info: { refetch: () => void }) => Promise<T>,
|
|
491
|
-
source?: () => unknown,
|
|
492
|
-
): Resource<T> {
|
|
493
|
-
const data = createSignal<T | undefined>(undefined);
|
|
494
|
-
const loading = createSignal(true);
|
|
495
|
-
const error = createSignal<Error | undefined>(undefined);
|
|
496
|
-
const version = createSignal(0);
|
|
497
|
-
let requestCount = 0;
|
|
498
|
-
let versionCounter = 0;
|
|
499
|
-
|
|
500
|
-
const refetch = () => {
|
|
501
|
-
versionCounter++;
|
|
502
|
-
version.set(versionCounter);
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
createEffect(() => {
|
|
506
|
-
const src = source?.();
|
|
507
|
-
version(); // track refetch bumps
|
|
508
|
-
const id = ++requestCount;
|
|
509
|
-
loading.set(true);
|
|
510
|
-
error.set(undefined);
|
|
511
|
-
fetcher(src, { refetch }).then(
|
|
512
|
-
(result) => {
|
|
513
|
-
if (id !== requestCount) return; // stale response
|
|
514
|
-
data.set(result);
|
|
515
|
-
loading.set(false);
|
|
516
|
-
},
|
|
517
|
-
(err: unknown) => {
|
|
518
|
-
if (id !== requestCount) return;
|
|
519
|
-
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
520
|
-
loading.set(false);
|
|
521
|
-
},
|
|
522
|
-
);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
return { data, loading, error, refetch };
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
export function createMemo<T>(fn: () => T): Signal<T> {
|
|
529
|
-
const inner = createSignal<T>(undefined as T);
|
|
530
|
-
createEffect(() => inner.set(fn()));
|
|
531
|
-
return inner;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Reconcile a list of keyed DOM nodes.
|
|
536
|
-
* anchor.before(node) handles both insert and move natively.
|
|
537
|
-
* @param claimNode - Optional hydration callback. Called with (element, key) for
|
|
538
|
-
* server-rendered elements removed from the DOM. Returns the element to use,
|
|
539
|
-
* or null to discard.
|
|
540
|
-
*/
|
|
541
|
-
const lastItemFor = new WeakMap<object, unknown>();
|
|
542
|
-
|
|
543
|
-
export function reconcile<T>(
|
|
544
|
-
anchor: Partial<Comment>,
|
|
545
|
-
keyMap: Map<unknown, Node>,
|
|
546
|
-
newItems: T[],
|
|
547
|
-
getKey: (item: T, index: number) => unknown,
|
|
548
|
-
createNode: (item: T) => Node,
|
|
549
|
-
// is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
|
|
550
|
-
): void {
|
|
551
|
-
const newKeys = new Set(newItems.map((item, i) => getKey(item, i)));
|
|
552
|
-
|
|
553
|
-
// Remove nodes no longer in list (or claim them for hydration)
|
|
554
|
-
for (const [key, node] of keyMap) {
|
|
555
|
-
if (!newKeys.has(key)) {
|
|
556
|
-
node.parentNode?.removeChild(node);
|
|
557
|
-
keyMap.delete(key);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Insert/move in order — anchor.before() handles both cases.
|
|
562
|
-
// A surviving node whose item instance changed is rebuilt: compiled row
|
|
563
|
-
// bodies bake item reads at creation time, so immutable-style updates
|
|
564
|
-
// (new object, same key) must swap the node to show fresh content.
|
|
565
|
-
for (let i = 0; i < newItems.length; i++) {
|
|
566
|
-
const item = newItems[i]!;
|
|
567
|
-
const key = getKey(item, i);
|
|
568
|
-
let node = keyMap.get(key);
|
|
569
|
-
if (
|
|
570
|
-
node &&
|
|
571
|
-
lastItemFor.has(node) &&
|
|
572
|
-
!Object.is(lastItemFor.get(node), item)
|
|
573
|
-
) {
|
|
574
|
-
node.parentNode?.removeChild(node);
|
|
575
|
-
keyMap.delete(key);
|
|
576
|
-
node = undefined;
|
|
577
|
-
}
|
|
578
|
-
if (!node) {
|
|
579
|
-
node = createNode(item);
|
|
580
|
-
lastItemFor.set(node, item);
|
|
581
|
-
keyMap.set(key, node);
|
|
582
|
-
}
|
|
583
|
-
anchor.before?.(node);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Claim an existing element from a hydration pool by tag name, or create a new one.
|
|
589
|
-
* Removes the claimed node from the pool array.
|
|
590
|
-
* When falling back to createElement, appends to parent if provided (SPA mode).
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
for
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
*
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1
|
+
export { __asyncBoundary } from "./async-boundary.ts";
|
|
2
|
+
export { Boundary } from "./boundary";
|
|
3
|
+
export {
|
|
4
|
+
easeInCubic,
|
|
5
|
+
easeInOutCubic,
|
|
6
|
+
easeInOutQuad,
|
|
7
|
+
easeInOutQuart,
|
|
8
|
+
easeInOutQuint,
|
|
9
|
+
easeInOutSine,
|
|
10
|
+
easeInQuad,
|
|
11
|
+
easeInQuart,
|
|
12
|
+
easeInQuint,
|
|
13
|
+
easeInSine,
|
|
14
|
+
easeOutCubic,
|
|
15
|
+
easeOutQuad,
|
|
16
|
+
easeOutQuart,
|
|
17
|
+
easeOutQuint,
|
|
18
|
+
easeOutSine,
|
|
19
|
+
linear,
|
|
20
|
+
} from "./easing.ts";
|
|
21
|
+
export { ReactiveMap } from "./reactivity/map.ts";
|
|
22
|
+
export { ReactiveSet } from "./reactivity/set.ts";
|
|
23
|
+
export { mountTransition, removeAnimated } from "./transition.ts";
|
|
24
|
+
|
|
25
|
+
export const SIGIL_SAFE = Symbol.for("sigil.safe");
|
|
26
|
+
export class SafeHtml {
|
|
27
|
+
[SIGIL_SAFE] = true;
|
|
28
|
+
constructor(public v: string) {}
|
|
29
|
+
toString() {
|
|
30
|
+
return this.v;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let currentEffect: (() => void) | null = null;
|
|
35
|
+
let currentDeps: Set<Set<() => void>> | null = null;
|
|
36
|
+
|
|
37
|
+
let batchDepth = 0;
|
|
38
|
+
let scheduledFlush = false;
|
|
39
|
+
const pendingEffects = new Set<() => void>();
|
|
40
|
+
const pendingPreEffects = new Set<() => void>();
|
|
41
|
+
|
|
42
|
+
function flush() {
|
|
43
|
+
scheduledFlush = false;
|
|
44
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
45
|
+
pendingPreEffects.forEach((e) => e());
|
|
46
|
+
pendingPreEffects.clear();
|
|
47
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
48
|
+
pendingEffects.forEach((e) => e());
|
|
49
|
+
pendingEffects.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scheduleEffect(fn: () => void) {
|
|
53
|
+
const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
|
|
54
|
+
target.add(fn);
|
|
55
|
+
if (batchDepth === 0 && !scheduledFlush) {
|
|
56
|
+
scheduledFlush = true;
|
|
57
|
+
queueMicrotask(flush);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const targetDepsMap = new WeakMap<
|
|
62
|
+
object,
|
|
63
|
+
Map<string | symbol, Set<() => void>>
|
|
64
|
+
>();
|
|
65
|
+
|
|
66
|
+
function getDeps(target: object, key: string | symbol): Set<() => void> {
|
|
67
|
+
let deps = targetDepsMap.get(target);
|
|
68
|
+
if (!deps) {
|
|
69
|
+
deps = new Map();
|
|
70
|
+
targetDepsMap.set(target, deps);
|
|
71
|
+
}
|
|
72
|
+
let set = deps.get(key);
|
|
73
|
+
if (!set) {
|
|
74
|
+
set = new Set();
|
|
75
|
+
deps.set(key, set);
|
|
76
|
+
}
|
|
77
|
+
return set;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function track(target: object, key: string | symbol) {
|
|
81
|
+
if (currentEffect && currentDeps) {
|
|
82
|
+
const deps = getDeps(target, key);
|
|
83
|
+
deps.add(currentEffect);
|
|
84
|
+
currentDeps.add(deps); // Set, deduplicates automatically
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function trigger(target: object, key: string | symbol) {
|
|
89
|
+
const deps = targetDepsMap.get(target);
|
|
90
|
+
if (!deps) return;
|
|
91
|
+
const set = deps.get(key);
|
|
92
|
+
if (!set) return;
|
|
93
|
+
for (const fn of [...set]) {
|
|
94
|
+
scheduleEffect(fn);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const proxyCache = new WeakMap<object, object>();
|
|
99
|
+
const proxyToTarget = new WeakMap<object, object>();
|
|
100
|
+
const reactiveTargets = new WeakSet<object>();
|
|
101
|
+
const ARRAY_MUTATORS = new Set([
|
|
102
|
+
"push",
|
|
103
|
+
"pop",
|
|
104
|
+
"shift",
|
|
105
|
+
"unshift",
|
|
106
|
+
"splice",
|
|
107
|
+
"sort",
|
|
108
|
+
"reverse",
|
|
109
|
+
"fill",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
function createReactiveProxy<T extends object>(target: T): T {
|
|
113
|
+
const existing = proxyCache.get(target);
|
|
114
|
+
if (existing) return existing as T;
|
|
115
|
+
|
|
116
|
+
const proxy = new Proxy(target, {
|
|
117
|
+
get(target, key, receiver) {
|
|
118
|
+
if (
|
|
119
|
+
Array.isArray(target) &&
|
|
120
|
+
typeof key === "string" &&
|
|
121
|
+
ARRAY_MUTATORS.has(key)
|
|
122
|
+
) {
|
|
123
|
+
return (...args: any[]) => {
|
|
124
|
+
const result = (target as any)[key](...args);
|
|
125
|
+
trigger(target, "length");
|
|
126
|
+
// trigger all numeric keys that changed — for splice/sort
|
|
127
|
+
// triggering length is sufficient for most reactive patterns
|
|
128
|
+
return result;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (key === "set") {
|
|
132
|
+
return (newValue: T) => {
|
|
133
|
+
const oldKeys = new Set(Object.keys(target));
|
|
134
|
+
const newKeys = new Set(Object.keys(newValue));
|
|
135
|
+
for (const k of oldKeys) {
|
|
136
|
+
if (!newKeys.has(k)) {
|
|
137
|
+
delete (target as any)[k];
|
|
138
|
+
trigger(target, k);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const k of newKeys) {
|
|
142
|
+
(target as any)[k] = (newValue as any)[k];
|
|
143
|
+
trigger(target, k);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(target)) {
|
|
146
|
+
// delete leaves holes — length must shrink to the new value's,
|
|
147
|
+
// or iteration (spread, reconcile) sees phantom trailing slots
|
|
148
|
+
target.length = (newValue as unknown[]).length;
|
|
149
|
+
trigger(target, "length");
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (key === "peek") {
|
|
155
|
+
return () => target;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof key === "symbol") {
|
|
159
|
+
return Reflect.get(target, key, receiver);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
track(target, key);
|
|
163
|
+
const value = target[key as keyof T];
|
|
164
|
+
|
|
165
|
+
if (typeof value === "object" && value !== null) {
|
|
166
|
+
return createReactiveProxy(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return value;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
set(target, key, newValue, receiver) {
|
|
173
|
+
const oldValue = target[key as keyof T];
|
|
174
|
+
const result = Reflect.set(target, key, newValue, receiver);
|
|
175
|
+
if (oldValue !== newValue) {
|
|
176
|
+
trigger(target, key as string | symbol);
|
|
177
|
+
if (
|
|
178
|
+
Array.isArray(target) &&
|
|
179
|
+
typeof key === "string" &&
|
|
180
|
+
!Number.isNaN(Number(key))
|
|
181
|
+
) {
|
|
182
|
+
trigger(target, "length");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
deleteProperty(target, key) {
|
|
189
|
+
const result = Reflect.deleteProperty(target, key);
|
|
190
|
+
trigger(target, key as string | symbol);
|
|
191
|
+
return result;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
proxyCache.set(target, proxy);
|
|
196
|
+
proxyToTarget.set(proxy, target);
|
|
197
|
+
reactiveTargets.add(target);
|
|
198
|
+
return proxy as T;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── createSignal (branches on type) ───
|
|
202
|
+
|
|
203
|
+
export type Signal<T> = {
|
|
204
|
+
(): T;
|
|
205
|
+
set: (next: T) => void;
|
|
206
|
+
peek: () => T;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export function batch(fn: () => void) {
|
|
210
|
+
batchDepth++;
|
|
211
|
+
fn();
|
|
212
|
+
batchDepth--;
|
|
213
|
+
if (batchDepth === 0) {
|
|
214
|
+
scheduledFlush = false;
|
|
215
|
+
flush();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function tick(): Promise<void> {
|
|
220
|
+
return new Promise((resolve) => queueMicrotask(resolve));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createPrimitiveSignal<T>(initial: T): Signal<T> {
|
|
224
|
+
const subs = new Set<() => void>();
|
|
225
|
+
let value = initial;
|
|
226
|
+
|
|
227
|
+
const getter = () => {
|
|
228
|
+
if (currentEffect && currentDeps) {
|
|
229
|
+
subs.add(currentEffect);
|
|
230
|
+
currentDeps.add(subs);
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
getter.set = (next: T) => {
|
|
236
|
+
value = next;
|
|
237
|
+
for (const fn of [...subs]) {
|
|
238
|
+
scheduleEffect(fn);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
getter.peek = () => value;
|
|
243
|
+
|
|
244
|
+
return getter;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createSignal<T>(initial: T): Signal<T> {
|
|
248
|
+
if (typeof initial === "object" && initial !== null) {
|
|
249
|
+
const proxy = createReactiveProxy(initial);
|
|
250
|
+
const getter = () => {
|
|
251
|
+
// array signals: auto-track length so push/pop/splice re-run effects
|
|
252
|
+
if (Array.isArray(initial)) {
|
|
253
|
+
track(initial, "length");
|
|
254
|
+
}
|
|
255
|
+
return proxy;
|
|
256
|
+
};
|
|
257
|
+
getter.set = (next: T) => (proxy as any).set(next);
|
|
258
|
+
getter.peek = () => initial;
|
|
259
|
+
return getter;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return createPrimitiveSignal(initial);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A1: $state.raw — create signal without Proxy wrapping for objects/arrays
|
|
266
|
+
export function createRawSignal<T>(initial: T): Signal<T> {
|
|
267
|
+
return createPrimitiveSignal(initial);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// A2: $state.snapshot — deep un-proxy a signal value
|
|
271
|
+
function deepUnproxy<T>(value: T): T {
|
|
272
|
+
if (typeof value !== "object" || value === null) return value;
|
|
273
|
+
const raw = proxyToTarget.get(value as object);
|
|
274
|
+
if (!raw) return value; // not a proxy, return as-is
|
|
275
|
+
if (Array.isArray(raw)) {
|
|
276
|
+
return raw.map(deepUnproxy) as T;
|
|
277
|
+
}
|
|
278
|
+
const result: Record<string, unknown> = {};
|
|
279
|
+
for (const key of Object.keys(raw)) {
|
|
280
|
+
result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
|
|
281
|
+
}
|
|
282
|
+
return result as T;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function snapshot<T>(value: T): T {
|
|
286
|
+
return deepUnproxy(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// A3: $effect.tracking() — check if inside reactive context
|
|
290
|
+
export function tracking(): boolean {
|
|
291
|
+
return currentEffect !== null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Run fn() outside of any reactive context: signal reads inside fn() do not
|
|
296
|
+
* register dependencies on the enclosing effect.
|
|
297
|
+
*/
|
|
298
|
+
export function untrack<T>(fn: () => T): T {
|
|
299
|
+
const prevEffect = currentEffect;
|
|
300
|
+
const prevDeps = currentDeps;
|
|
301
|
+
currentEffect = null;
|
|
302
|
+
currentDeps = null;
|
|
303
|
+
try {
|
|
304
|
+
return fn();
|
|
305
|
+
} finally {
|
|
306
|
+
currentEffect = prevEffect;
|
|
307
|
+
currentDeps = prevDeps;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A4: $inspect — dev-only effect logging
|
|
312
|
+
export function createInspectEffect(
|
|
313
|
+
getValues: () => unknown[],
|
|
314
|
+
cb?: (vals: unknown[]) => void,
|
|
315
|
+
): void {
|
|
316
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
317
|
+
return;
|
|
318
|
+
createEffect(() => {
|
|
319
|
+
const vals = getValues();
|
|
320
|
+
if (cb) cb(vals);
|
|
321
|
+
else console.log(...vals);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// A5: $effect.pre — effect that flushes before DOM update
|
|
326
|
+
export function createEffectPre(
|
|
327
|
+
fn: () => undefined | (() => void),
|
|
328
|
+
): () => void {
|
|
329
|
+
let cleanup: (() => void) | undefined;
|
|
330
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
331
|
+
|
|
332
|
+
const effect = () => {
|
|
333
|
+
if (cleanup) cleanup();
|
|
334
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
335
|
+
ownDeps.clear();
|
|
336
|
+
|
|
337
|
+
const prevEffect = currentEffect;
|
|
338
|
+
const prevDeps = currentDeps;
|
|
339
|
+
currentEffect = effect;
|
|
340
|
+
currentDeps = ownDeps;
|
|
341
|
+
try {
|
|
342
|
+
cleanup = fn();
|
|
343
|
+
} finally {
|
|
344
|
+
currentEffect = prevEffect;
|
|
345
|
+
currentDeps = prevDeps;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
(effect as any).__sigil_pre = true;
|
|
350
|
+
effect();
|
|
351
|
+
|
|
352
|
+
const dispose = () => {
|
|
353
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
354
|
+
ownDeps.clear();
|
|
355
|
+
if (cleanup) cleanup();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
359
|
+
dispose,
|
|
360
|
+
);
|
|
361
|
+
return dispose;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// A9: Profiler effect timing (dev)
|
|
365
|
+
export function createProfilerEffect(
|
|
366
|
+
_name: string,
|
|
367
|
+
fn: () => undefined | (() => void),
|
|
368
|
+
): () => void {
|
|
369
|
+
return createEffect(() => {
|
|
370
|
+
if (
|
|
371
|
+
typeof process !== "undefined" &&
|
|
372
|
+
process.env.NODE_ENV === "production"
|
|
373
|
+
) {
|
|
374
|
+
return fn();
|
|
375
|
+
}
|
|
376
|
+
const _start = performance.now();
|
|
377
|
+
const cleanup = fn();
|
|
378
|
+
const _end = performance.now();
|
|
379
|
+
// console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
|
|
380
|
+
return cleanup;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Deferred effect mode for hydration: effects created while the flag is set
|
|
385
|
+
// are queued instead of run immediately, then flushed after DOM insertion.
|
|
386
|
+
const deferredEffectQueue: (() => void)[] = [];
|
|
387
|
+
|
|
388
|
+
export function flushDeferredEffects(): void {
|
|
389
|
+
const pending = deferredEffectQueue.splice(0);
|
|
390
|
+
for (const run of pending) run();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
|
|
394
|
+
export function createEffect(fn: () => void | (() => void)): () => void {
|
|
395
|
+
let cleanup: (() => void) | undefined;
|
|
396
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
397
|
+
|
|
398
|
+
const effect = () => {
|
|
399
|
+
if (cleanup) cleanup();
|
|
400
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
401
|
+
ownDeps.clear();
|
|
402
|
+
|
|
403
|
+
const prevEffect = currentEffect;
|
|
404
|
+
const prevDeps = currentDeps;
|
|
405
|
+
currentEffect = effect;
|
|
406
|
+
currentDeps = ownDeps;
|
|
407
|
+
try {
|
|
408
|
+
//@ts-expect-error kill yourself
|
|
409
|
+
cleanup = fn();
|
|
410
|
+
} finally {
|
|
411
|
+
currentEffect = prevEffect;
|
|
412
|
+
currentDeps = prevDeps;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// In deferred mode (hydration), queue the effect instead of running immediately.
|
|
417
|
+
// This ensures the DOM is inserted before effect callbacks query it.
|
|
418
|
+
if ((globalThis as any).__sigilDeferEffects) {
|
|
419
|
+
deferredEffectQueue.push(() => effect());
|
|
420
|
+
} else {
|
|
421
|
+
effect();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const dispose = () => {
|
|
425
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
426
|
+
ownDeps.clear();
|
|
427
|
+
if (cleanup) cleanup();
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
431
|
+
dispose,
|
|
432
|
+
);
|
|
433
|
+
return dispose;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Run fn() and return a dispose function that tears down every effect
|
|
438
|
+
* created during that call (including effects inside child createMemo calls).
|
|
439
|
+
* Supports nesting: inner scopes don't leak to outer ones.
|
|
440
|
+
*/
|
|
441
|
+
export function withEffectScope(fn: () => void): () => void {
|
|
442
|
+
const prev = (globalThis as any).__sigilEffectScope;
|
|
443
|
+
const disposers: (() => void)[] = [];
|
|
444
|
+
(globalThis as any).__sigilEffectScope = disposers;
|
|
445
|
+
try {
|
|
446
|
+
fn();
|
|
447
|
+
} finally {
|
|
448
|
+
(globalThis as any).__sigilEffectScope = prev;
|
|
449
|
+
}
|
|
450
|
+
return () => {
|
|
451
|
+
for (const d of disposers) d();
|
|
452
|
+
disposers.length = 0;
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
type EffectScopeGlobal = { __sigilEffectScope?: (() => void)[] };
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Run fn() with a detached effect scope. Effects created inside are invisible
|
|
460
|
+
* to enclosing scopes (the scope array is replaced, not merged) and live until
|
|
461
|
+
* dispose() is called — for app bootstrap, routers, and long-lived listeners.
|
|
462
|
+
*/
|
|
463
|
+
export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
464
|
+
const g = globalThis as EffectScopeGlobal;
|
|
465
|
+
const prev = g.__sigilEffectScope;
|
|
466
|
+
const disposers: (() => void)[] = [];
|
|
467
|
+
g.__sigilEffectScope = disposers;
|
|
468
|
+
const dispose = () => {
|
|
469
|
+
for (const d of disposers.splice(0)) d();
|
|
470
|
+
};
|
|
471
|
+
try {
|
|
472
|
+
return fn(dispose);
|
|
473
|
+
} finally {
|
|
474
|
+
g.__sigilEffectScope = prev;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export interface Resource<T> {
|
|
479
|
+
data: () => T | undefined;
|
|
480
|
+
loading: () => boolean;
|
|
481
|
+
error: () => Error | undefined;
|
|
482
|
+
refetch: () => void;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Async data source with reactive re-fetching. Re-runs when `source()` changes
|
|
487
|
+
* or refetch() is called; stale responses are discarded by request id.
|
|
488
|
+
*/
|
|
489
|
+
export function createResource<T>(
|
|
490
|
+
fetcher: (source: unknown, info: { refetch: () => void }) => Promise<T>,
|
|
491
|
+
source?: () => unknown,
|
|
492
|
+
): Resource<T> {
|
|
493
|
+
const data = createSignal<T | undefined>(undefined);
|
|
494
|
+
const loading = createSignal(true);
|
|
495
|
+
const error = createSignal<Error | undefined>(undefined);
|
|
496
|
+
const version = createSignal(0);
|
|
497
|
+
let requestCount = 0;
|
|
498
|
+
let versionCounter = 0;
|
|
499
|
+
|
|
500
|
+
const refetch = () => {
|
|
501
|
+
versionCounter++;
|
|
502
|
+
version.set(versionCounter);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
createEffect(() => {
|
|
506
|
+
const src = source?.();
|
|
507
|
+
version(); // track refetch bumps
|
|
508
|
+
const id = ++requestCount;
|
|
509
|
+
loading.set(true);
|
|
510
|
+
error.set(undefined);
|
|
511
|
+
fetcher(src, { refetch }).then(
|
|
512
|
+
(result) => {
|
|
513
|
+
if (id !== requestCount) return; // stale response
|
|
514
|
+
data.set(result);
|
|
515
|
+
loading.set(false);
|
|
516
|
+
},
|
|
517
|
+
(err: unknown) => {
|
|
518
|
+
if (id !== requestCount) return;
|
|
519
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
520
|
+
loading.set(false);
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return { data, loading, error, refetch };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function createMemo<T>(fn: () => T): Signal<T> {
|
|
529
|
+
const inner = createSignal<T>(undefined as T);
|
|
530
|
+
createEffect(() => inner.set(fn()));
|
|
531
|
+
return inner;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Reconcile a list of keyed DOM nodes.
|
|
536
|
+
* anchor.before(node) handles both insert and move natively.
|
|
537
|
+
* @param claimNode - Optional hydration callback. Called with (element, key) for
|
|
538
|
+
* server-rendered elements removed from the DOM. Returns the element to use,
|
|
539
|
+
* or null to discard.
|
|
540
|
+
*/
|
|
541
|
+
const lastItemFor = new WeakMap<object, unknown>();
|
|
542
|
+
|
|
543
|
+
export function reconcile<T>(
|
|
544
|
+
anchor: Partial<Comment>,
|
|
545
|
+
keyMap: Map<unknown, Node>,
|
|
546
|
+
newItems: T[],
|
|
547
|
+
getKey: (item: T, index: number) => unknown,
|
|
548
|
+
createNode: (item: T) => Node,
|
|
549
|
+
// is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
|
|
550
|
+
): void {
|
|
551
|
+
const newKeys = new Set(newItems.map((item, i) => getKey(item, i)));
|
|
552
|
+
|
|
553
|
+
// Remove nodes no longer in list (or claim them for hydration)
|
|
554
|
+
for (const [key, node] of keyMap) {
|
|
555
|
+
if (!newKeys.has(key)) {
|
|
556
|
+
node.parentNode?.removeChild(node);
|
|
557
|
+
keyMap.delete(key);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Insert/move in order — anchor.before() handles both cases.
|
|
562
|
+
// A surviving node whose item instance changed is rebuilt: compiled row
|
|
563
|
+
// bodies bake item reads at creation time, so immutable-style updates
|
|
564
|
+
// (new object, same key) must swap the node to show fresh content.
|
|
565
|
+
for (let i = 0; i < newItems.length; i++) {
|
|
566
|
+
const item = newItems[i]!;
|
|
567
|
+
const key = getKey(item, i);
|
|
568
|
+
let node = keyMap.get(key);
|
|
569
|
+
if (
|
|
570
|
+
node &&
|
|
571
|
+
lastItemFor.has(node) &&
|
|
572
|
+
!Object.is(lastItemFor.get(node), item)
|
|
573
|
+
) {
|
|
574
|
+
node.parentNode?.removeChild(node);
|
|
575
|
+
keyMap.delete(key);
|
|
576
|
+
node = undefined;
|
|
577
|
+
}
|
|
578
|
+
if (!node) {
|
|
579
|
+
node = createNode(item);
|
|
580
|
+
lastItemFor.set(node, item);
|
|
581
|
+
keyMap.set(key, node);
|
|
582
|
+
}
|
|
583
|
+
anchor.before?.(node);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Claim an existing element from a hydration pool by tag name, or create a new one.
|
|
589
|
+
* Removes the claimed node from the pool array.
|
|
590
|
+
* When falling back to createElement, appends to parent if provided (SPA mode).
|
|
591
|
+
* SVG elements (and children of SVG parents) are created with createElementNS —
|
|
592
|
+
* plain createElement puts them in the HTML namespace where they render as
|
|
593
|
+
* invisible unknown elements (killed CSR-rendered inline SVG, e.g. topbar icons).
|
|
594
|
+
*/
|
|
595
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
596
|
+
|
|
597
|
+
export function claim(
|
|
598
|
+
nodes: ChildNode[],
|
|
599
|
+
tag: string,
|
|
600
|
+
parent?: Element,
|
|
601
|
+
): Element {
|
|
602
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
603
|
+
const node = nodes[i];
|
|
604
|
+
if (
|
|
605
|
+
node instanceof Element &&
|
|
606
|
+
node.tagName.toLowerCase() === tag.toLowerCase()
|
|
607
|
+
) {
|
|
608
|
+
// SSR injects a _csrf hidden input as the first child of every <form>
|
|
609
|
+
// (rendering/index.ts). The compiled hydration code doesn't know about
|
|
610
|
+
// it, so it must never be claimed — claiming it shifts the whole chain
|
|
611
|
+
// by one and duplicates the last form child (e.g. two display_name
|
|
612
|
+
// inputs). Skip it; it stays in place as a live DOM node.
|
|
613
|
+
if (
|
|
614
|
+
tag.toLowerCase() === "input" &&
|
|
615
|
+
node.hasAttribute("data-grimoire-csrf")
|
|
616
|
+
) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
nodes.splice(i, 1);
|
|
620
|
+
return node;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const el =
|
|
624
|
+
tag.toLowerCase() === "svg" || parent?.namespaceURI === SVG_NS
|
|
625
|
+
? document.createElementNS(SVG_NS, tag)
|
|
626
|
+
: document.createElement(tag);
|
|
627
|
+
if (parent) parent.appendChild(el);
|
|
628
|
+
return el;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Write a JSX prop to an element. On HTML elements this is the classic blind
|
|
633
|
+
* `el[name] = value` (preserves IDL semantics for value/checked/etc). On
|
|
634
|
+
* SVG-namespace elements it goes through setAttribute instead — IDL property
|
|
635
|
+
* assignment throws on real SVG elements (`svg.width = 15` hits a
|
|
636
|
+
* getter-only property and aborts the whole render).
|
|
637
|
+
*/
|
|
638
|
+
const SVG_ATTR_FIX: Record<string, string> = {
|
|
639
|
+
className: "class",
|
|
640
|
+
htmlFor: "for",
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// Never attribute-routed, even inside SVG.
|
|
644
|
+
const SVG_PROP_ONLY = new Set(["textContent", "innerHTML", "outerHTML"]);
|
|
645
|
+
|
|
646
|
+
export function setProp(el: Element, name: string, value: unknown): void {
|
|
647
|
+
if (el.namespaceURI === SVG_NS && !SVG_PROP_ONLY.has(name)) {
|
|
648
|
+
const attr = SVG_ATTR_FIX[name] ?? name;
|
|
649
|
+
if (value == null || value === false) el.removeAttribute(attr);
|
|
650
|
+
else el.setAttribute(attr, value === true ? "" : String(value));
|
|
651
|
+
} else {
|
|
652
|
+
(el as unknown as Record<string, unknown>)[name] = value;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Claim an existing Text node from a hydration pool, or create a new empty one.
|
|
658
|
+
* Removes the claimed node from the pool array.
|
|
659
|
+
* When falling back to createTextNode, appends to parent if provided (SPA mode).
|
|
660
|
+
*/
|
|
661
|
+
export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
662
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
663
|
+
const node = nodes[i];
|
|
664
|
+
if (node instanceof Text) {
|
|
665
|
+
nodes.splice(i, 1);
|
|
666
|
+
return node;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const text = document.createTextNode("");
|
|
670
|
+
if (parent) parent.appendChild(text);
|
|
671
|
+
return text;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Claim an existing Comment node from a hydration pool, or create a new one.
|
|
676
|
+
* Removes the claimed node from the pool array.
|
|
677
|
+
* When falling back to createComment, appends to parent if provided (SPA mode).
|
|
678
|
+
*/
|
|
679
|
+
export function claimComment(
|
|
680
|
+
nodes: ChildNode[],
|
|
681
|
+
text: string = "",
|
|
682
|
+
parent?: Element,
|
|
683
|
+
): Comment {
|
|
684
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
685
|
+
const node = nodes[i];
|
|
686
|
+
if (node instanceof Comment && node.data === text) {
|
|
687
|
+
nodes.splice(i, 1);
|
|
688
|
+
return node;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const comment = document.createComment(text);
|
|
692
|
+
if (parent) parent.appendChild(comment);
|
|
693
|
+
return comment;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Hydrate a keyed list: claim the comment anchor, walk its sibling elements
|
|
698
|
+
* to build a keyMap, then reconcile with signal data.
|
|
699
|
+
*
|
|
700
|
+
* SSR renders items as flat siblings. Hydration claims them by position,
|
|
701
|
+
* builds the keyMap, removes from DOM, then reconcile re-inserts in order.
|
|
702
|
+
*/
|
|
703
|
+
export function hydrateKeyedList<T>(
|
|
704
|
+
nodesVar: ChildNode[],
|
|
705
|
+
signalItems: T[],
|
|
706
|
+
getKey: (item: T, index: number) => unknown,
|
|
707
|
+
createNode: (item: T) => Node,
|
|
708
|
+
parent?: Element,
|
|
709
|
+
): { anchor: Comment; keyMap: Map<unknown, Node> } {
|
|
710
|
+
// 1. Claim the <!--g--> opening delimiter as anchor
|
|
711
|
+
const anchor = claimComment(nodesVar, "g", parent);
|
|
712
|
+
|
|
713
|
+
// 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
|
|
714
|
+
const keyMap = new Map<unknown, Node>();
|
|
715
|
+
const anchorParent = anchor.parentNode;
|
|
716
|
+
if (anchorParent && signalItems.length > 0) {
|
|
717
|
+
const claimed: Element[] = [];
|
|
718
|
+
let next = anchor.nextSibling;
|
|
719
|
+
while (next) {
|
|
720
|
+
const current = next;
|
|
721
|
+
next = next.nextSibling;
|
|
722
|
+
// Stop at closing <!--/g--> delimiter and consume it
|
|
723
|
+
if (current instanceof Comment && current.data === "/g") {
|
|
724
|
+
current.remove();
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
if (current instanceof Element) {
|
|
728
|
+
current.remove();
|
|
729
|
+
claimed.push(current);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Map claimed elements to keys by index
|
|
733
|
+
for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
|
|
734
|
+
//@ts-expect-error i is always in bounds
|
|
735
|
+
const key = getKey(signalItems[i], i);
|
|
736
|
+
//@ts-expect-error i is always in bounds
|
|
737
|
+
keyMap.set(key, claimed[i]);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 3. Reconcile — keyMap is pre-populated with claimed nodes
|
|
742
|
+
reconcile(anchor, keyMap, signalItems, getKey, createNode);
|
|
743
|
+
return { anchor, keyMap };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function insert(
|
|
747
|
+
parent: Element | DocumentFragment,
|
|
748
|
+
value: unknown,
|
|
749
|
+
): void {
|
|
750
|
+
if (value === false || value === null || value === undefined) return;
|
|
751
|
+
if (typeof value === "function") {
|
|
752
|
+
insert(parent, value());
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (Array.isArray(value)) {
|
|
756
|
+
for (const item of value) insert(parent, item);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (value instanceof Node) {
|
|
760
|
+
parent.appendChild(value);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
parent.append(String(value));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Hydrate a `<!--g-->…<!--/g-->` slot.
|
|
768
|
+
*
|
|
769
|
+
* Consumes the delimiter pair and the SSR content between them from `nodes`,
|
|
770
|
+
* then runs `value` against that content as its hydration pool. Component
|
|
771
|
+
* children are compiled to thunks precisely so this call — not the caller's
|
|
772
|
+
* pool — decides what they claim: `<Wrap><Item/></Wrap>` puts `Item`'s SSR
|
|
773
|
+
* markup inside `Wrap`'s root, which the caller cannot see.
|
|
774
|
+
*
|
|
775
|
+
* SSR left no slot (SPA navigation renders into empty elements): fall back to
|
|
776
|
+
* a fresh `insert`.
|
|
777
|
+
*/
|
|
778
|
+
export function hydrateSlot(
|
|
779
|
+
nodes: ChildNode[],
|
|
780
|
+
parent: Element | DocumentFragment,
|
|
781
|
+
value: unknown,
|
|
782
|
+
): void {
|
|
783
|
+
let open = -1;
|
|
784
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
785
|
+
const node = nodes[i];
|
|
786
|
+
if (node instanceof Comment && node.data === "g") {
|
|
787
|
+
open = i;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (open === -1) {
|
|
792
|
+
insert(parent, value);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
let close = open + 1;
|
|
796
|
+
while (close < nodes.length) {
|
|
797
|
+
const node = nodes[close];
|
|
798
|
+
if (node instanceof Comment && node.data === "/g") break;
|
|
799
|
+
close++;
|
|
800
|
+
}
|
|
801
|
+
const inner = nodes.slice(open + 1, close);
|
|
802
|
+
// Drop the whole run so later siblings can't claim slot content.
|
|
803
|
+
nodes.splice(open, close - open + 1);
|
|
804
|
+
pushHydrationNodes(inner);
|
|
805
|
+
try {
|
|
806
|
+
attachHydrated(parent, value);
|
|
807
|
+
} finally {
|
|
808
|
+
popHydrationNodes();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Realize a slot value that already has SSR markup in the DOM: run thunks,
|
|
814
|
+
* skip primitives (their text is already rendered), and append only nodes
|
|
815
|
+
* that hydration created fresh instead of claiming.
|
|
816
|
+
*/
|
|
817
|
+
function attachHydrated(
|
|
818
|
+
parent: Element | DocumentFragment,
|
|
819
|
+
value: unknown,
|
|
820
|
+
): void {
|
|
821
|
+
if (typeof value === "function") {
|
|
822
|
+
attachHydrated(parent, (value as () => unknown)());
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (Array.isArray(value)) {
|
|
826
|
+
for (const item of value) attachHydrated(parent, item);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (value instanceof Node) {
|
|
830
|
+
// During hydration the parent's SSR-rendered children are already in
|
|
831
|
+
// the DOM. A freshly-created JSX Node for the same slot is a duplicate
|
|
832
|
+
// — appending it doubles the content (e.g. a `<span>` rendered both
|
|
833
|
+
// server-side and as a JSX value prop). Keep the SSR node, drop the clone.
|
|
834
|
+
// A node that already sits in the DOM was claimed from SSR markup:
|
|
835
|
+
// leave it where it is.
|
|
836
|
+
if (parent.childNodes.length > 0 || value.parentNode) return;
|
|
837
|
+
parent.appendChild(value);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Apply a use: directive to an element.
|
|
843
|
+
* The directive function receives (element, params) and may return a cleanup function.
|
|
844
|
+
* Cleanup runs when the enclosing $effect re-runs or the component is destroyed.
|
|
845
|
+
*/
|
|
846
|
+
export type Directive = (
|
|
847
|
+
node: Element,
|
|
848
|
+
params: unknown,
|
|
849
|
+
) => undefined | (() => void);
|
|
850
|
+
|
|
851
|
+
export function applyDirective(
|
|
852
|
+
node: Element,
|
|
853
|
+
directive: Directive | [Directive, unknown],
|
|
854
|
+
): void {
|
|
855
|
+
const [fn, params] = Array.isArray(directive)
|
|
856
|
+
? directive
|
|
857
|
+
: [directive, undefined];
|
|
858
|
+
const cleanup = fn(node, params);
|
|
859
|
+
if (typeof cleanup === "function") {
|
|
860
|
+
createEffect(() => () => cleanup());
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export type ContextKey<T> = symbol & { __type: T };
|
|
865
|
+
|
|
866
|
+
const clientContextStore = new Map<symbol, unknown>();
|
|
867
|
+
|
|
868
|
+
let storeProvider: (() => Map<symbol, unknown>) | null = null;
|
|
869
|
+
export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
|
|
870
|
+
storeProvider = fn;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function getStore(): Map<symbol, unknown> {
|
|
874
|
+
return storeProvider?.() ?? clientContextStore;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function createContext<T>(): ContextKey<T> {
|
|
878
|
+
return Symbol() as ContextKey<T>;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function setContext<T>(key: ContextKey<T>, value: T): void {
|
|
882
|
+
getStore().set(key as unknown as symbol, value);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function getContext<T>(key: ContextKey<T>): T {
|
|
886
|
+
return getStore().get(key as unknown as symbol) as T;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const _hydrationStack: ChildNode[][] = [];
|
|
890
|
+
|
|
891
|
+
export function pushHydrationNodes(nodes: ChildNode[]): void {
|
|
892
|
+
_hydrationStack.push(nodes);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export function popHydrationNodes(): void {
|
|
896
|
+
_hydrationStack.pop();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function getHydrationNodes(): ChildNode[] {
|
|
900
|
+
return _hydrationStack[_hydrationStack.length - 1] ?? [];
|
|
901
|
+
}
|