@sigil-dev/runtime 0.7.5 → 0.7.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/easing.ts +66 -0
- package/index.test.ts +587 -40
- package/index.ts +486 -317
- package/package.json +1 -1
- package/reactivity/map.ts +98 -0
- package/reactivity/set.ts +74 -0
package/index.ts
CHANGED
|
@@ -1,220 +1,385 @@
|
|
|
1
1
|
export { Boundary } from "./boundary";
|
|
2
|
+
export {
|
|
3
|
+
easeInCubic,
|
|
4
|
+
easeInOutCubic,
|
|
5
|
+
easeInOutQuad,
|
|
6
|
+
easeInOutQuart,
|
|
7
|
+
easeInOutQuint,
|
|
8
|
+
easeInOutSine,
|
|
9
|
+
easeInQuad,
|
|
10
|
+
easeInQuart,
|
|
11
|
+
easeInQuint,
|
|
12
|
+
easeInSine,
|
|
13
|
+
easeOutCubic,
|
|
14
|
+
easeOutQuad,
|
|
15
|
+
easeOutQuart,
|
|
16
|
+
easeOutQuint,
|
|
17
|
+
easeOutSine,
|
|
18
|
+
linear,
|
|
19
|
+
} from "./easing.ts";
|
|
20
|
+
export { ReactiveMap } from "./reactivity/map.ts";
|
|
21
|
+
export { ReactiveSet } from "./reactivity/set.ts";
|
|
2
22
|
|
|
3
23
|
export const SIGIL_SAFE = Symbol.for("sigil.safe");
|
|
4
24
|
export class SafeHtml {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
[SIGIL_SAFE] = true;
|
|
26
|
+
constructor(public v: string) { }
|
|
27
|
+
toString() {
|
|
28
|
+
return this.v;
|
|
29
|
+
}
|
|
10
30
|
}
|
|
11
31
|
|
|
12
|
-
|
|
13
32
|
let currentEffect: (() => void) | null = null;
|
|
14
33
|
let currentDeps: Set<Set<() => void>> | null = null;
|
|
15
34
|
|
|
16
35
|
let batchDepth = 0;
|
|
36
|
+
let scheduledFlush = false;
|
|
17
37
|
const pendingEffects = new Set<() => void>();
|
|
38
|
+
const pendingPreEffects = new Set<() => void>();
|
|
39
|
+
|
|
40
|
+
function flush() {
|
|
41
|
+
scheduledFlush = false;
|
|
42
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
43
|
+
pendingPreEffects.forEach((e) => e());
|
|
44
|
+
pendingPreEffects.clear();
|
|
45
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
46
|
+
pendingEffects.forEach((e) => e());
|
|
47
|
+
pendingEffects.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scheduleEffect(fn: () => void) {
|
|
51
|
+
const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
|
|
52
|
+
target.add(fn);
|
|
53
|
+
if (batchDepth === 0 && !scheduledFlush) {
|
|
54
|
+
scheduledFlush = true;
|
|
55
|
+
queueMicrotask(flush);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
18
58
|
|
|
19
59
|
const targetDepsMap = new WeakMap<
|
|
20
|
-
|
|
21
|
-
|
|
60
|
+
object,
|
|
61
|
+
Map<string | symbol, Set<() => void>>
|
|
22
62
|
>();
|
|
23
63
|
|
|
24
64
|
function getDeps(target: object, key: string | symbol): Set<() => void> {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
let deps = targetDepsMap.get(target);
|
|
66
|
+
if (!deps) {
|
|
67
|
+
deps = new Map();
|
|
68
|
+
targetDepsMap.set(target, deps);
|
|
69
|
+
}
|
|
70
|
+
let set = deps.get(key);
|
|
71
|
+
if (!set) {
|
|
72
|
+
set = new Set();
|
|
73
|
+
deps.set(key, set);
|
|
74
|
+
}
|
|
75
|
+
return set;
|
|
36
76
|
}
|
|
37
77
|
|
|
38
78
|
function track(target: object, key: string | symbol) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
if (currentEffect && currentDeps) {
|
|
80
|
+
const deps = getDeps(target, key);
|
|
81
|
+
deps.add(currentEffect);
|
|
82
|
+
currentDeps.add(deps); // Set, deduplicates automatically
|
|
83
|
+
}
|
|
44
84
|
}
|
|
45
85
|
|
|
46
86
|
function trigger(target: object, key: string | symbol) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
87
|
+
const deps = targetDepsMap.get(target);
|
|
88
|
+
if (!deps) return;
|
|
89
|
+
const set = deps.get(key);
|
|
90
|
+
if (!set) return;
|
|
91
|
+
for (const fn of [...set]) {
|
|
92
|
+
scheduleEffect(fn);
|
|
93
|
+
}
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
const proxyCache = new WeakMap<object, object>();
|
|
97
|
+
const proxyToTarget = new WeakMap<object, object>();
|
|
58
98
|
const reactiveTargets = new WeakSet<object>();
|
|
99
|
+
const ARRAY_MUTATORS = new Set([
|
|
100
|
+
"push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill"
|
|
101
|
+
]);
|
|
59
102
|
|
|
60
103
|
function createReactiveProxy<T extends object>(target: T): T {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
104
|
+
const existing = proxyCache.get(target);
|
|
105
|
+
if (existing) return existing as T;
|
|
106
|
+
|
|
107
|
+
const proxy = new Proxy(target, {
|
|
108
|
+
get(target, key, receiver) {
|
|
109
|
+
if (Array.isArray(target) && typeof key === "string" && ARRAY_MUTATORS.has(key)) {
|
|
110
|
+
return (...args: any[]) => {
|
|
111
|
+
const result = (target as any)[key](...args);
|
|
112
|
+
trigger(target, "length");
|
|
113
|
+
// trigger all numeric keys that changed — for splice/sort
|
|
114
|
+
// triggering length is sufficient for most reactive patterns
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (key === "set") {
|
|
119
|
+
return (newValue: T) => {
|
|
120
|
+
const oldKeys = new Set(Object.keys(target));
|
|
121
|
+
const newKeys = new Set(Object.keys(newValue));
|
|
122
|
+
for (const k of oldKeys) {
|
|
123
|
+
if (!newKeys.has(k)) {
|
|
124
|
+
delete (target as any)[k];
|
|
125
|
+
trigger(target, k);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const k of newKeys) {
|
|
129
|
+
(target as any)[k] = (newValue as any)[k];
|
|
130
|
+
trigger(target, k);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(target)) {
|
|
133
|
+
trigger(target, "length");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (key === "peek") {
|
|
139
|
+
return () => target;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof key === "symbol") {
|
|
143
|
+
return Reflect.get(target, key, receiver);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
track(target, key);
|
|
147
|
+
const value = target[key as keyof T];
|
|
148
|
+
|
|
149
|
+
if (typeof value === "object" && value !== null) {
|
|
150
|
+
return createReactiveProxy(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return value;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
set(target, key, newValue, receiver) {
|
|
157
|
+
const oldValue = target[key as keyof T];
|
|
158
|
+
const result = Reflect.set(target, key, newValue, receiver);
|
|
159
|
+
if (oldValue !== newValue) {
|
|
160
|
+
trigger(target, key as string | symbol);
|
|
161
|
+
if (
|
|
162
|
+
Array.isArray(target) &&
|
|
163
|
+
typeof key === "string" &&
|
|
164
|
+
!Number.isNaN(Number(key))
|
|
165
|
+
) {
|
|
166
|
+
trigger(target, "length");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
deleteProperty(target, key) {
|
|
173
|
+
const result = Reflect.deleteProperty(target, key);
|
|
174
|
+
trigger(target, key as string | symbol);
|
|
175
|
+
return result;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
proxyCache.set(target, proxy);
|
|
180
|
+
proxyToTarget.set(proxy, target);
|
|
181
|
+
reactiveTargets.add(target);
|
|
182
|
+
return proxy as T;
|
|
130
183
|
}
|
|
131
184
|
|
|
132
185
|
// ─── createSignal (branches on type) ───
|
|
133
186
|
|
|
134
187
|
export type Signal<T> = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
(): T;
|
|
189
|
+
set: (next: T) => void;
|
|
190
|
+
peek: () => T;
|
|
138
191
|
};
|
|
139
192
|
|
|
140
193
|
export function batch(fn: () => void) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
194
|
+
batchDepth++;
|
|
195
|
+
fn();
|
|
196
|
+
batchDepth--;
|
|
197
|
+
if (batchDepth === 0) {
|
|
198
|
+
scheduledFlush = false;
|
|
199
|
+
flush();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function tick(): Promise<void> {
|
|
204
|
+
return new Promise((resolve) => queueMicrotask(resolve));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createPrimitiveSignal<T>(initial: T): Signal<T> {
|
|
208
|
+
const subs = new Set<() => void>();
|
|
209
|
+
let value = initial;
|
|
210
|
+
|
|
211
|
+
const getter = () => {
|
|
212
|
+
if (currentEffect && currentDeps) {
|
|
213
|
+
subs.add(currentEffect);
|
|
214
|
+
currentDeps.add(subs);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
getter.set = (next: T) => {
|
|
220
|
+
value = next;
|
|
221
|
+
for (const fn of [...subs]) {
|
|
222
|
+
scheduleEffect(fn);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
getter.peek = () => value;
|
|
227
|
+
|
|
228
|
+
return getter;
|
|
149
229
|
}
|
|
150
230
|
|
|
151
231
|
export function createSignal<T>(initial: T): Signal<T> {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
232
|
+
if (typeof initial === "object" && initial !== null) {
|
|
233
|
+
const proxy = createReactiveProxy(initial);
|
|
234
|
+
const getter = () => {
|
|
235
|
+
// array signals: auto-track length so push/pop/splice re-run effects
|
|
236
|
+
if (Array.isArray(initial)) {
|
|
237
|
+
track(initial, "length");
|
|
238
|
+
}
|
|
239
|
+
return proxy;
|
|
240
|
+
};
|
|
241
|
+
getter.set = (next: T) => (proxy as any).set(next);
|
|
242
|
+
getter.peek = () => initial;
|
|
243
|
+
return getter;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return createPrimitiveSignal(initial);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// A1: $state.raw — create signal without Proxy wrapping for objects/arrays
|
|
250
|
+
export function createRawSignal<T>(initial: T): Signal<T> {
|
|
251
|
+
return createPrimitiveSignal(initial);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// A2: $state.snapshot — deep un-proxy a signal value
|
|
255
|
+
function deepUnproxy<T>(value: T): T {
|
|
256
|
+
if (typeof value !== "object" || value === null) return value;
|
|
257
|
+
const raw = proxyToTarget.get(value as object);
|
|
258
|
+
if (!raw) return value; // not a proxy, return as-is
|
|
259
|
+
if (Array.isArray(raw)) {
|
|
260
|
+
return raw.map(deepUnproxy) as T;
|
|
261
|
+
}
|
|
262
|
+
const result: Record<string, unknown> = {};
|
|
263
|
+
for (const key of Object.keys(raw)) {
|
|
264
|
+
result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
|
|
265
|
+
}
|
|
266
|
+
return result as T;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function snapshot<T>(value: T): T {
|
|
270
|
+
return deepUnproxy(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// A3: $effect.tracking() — check if inside reactive context
|
|
274
|
+
export function tracking(): boolean {
|
|
275
|
+
return currentEffect !== null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// A4: $inspect — dev-only effect logging
|
|
279
|
+
export function createInspectEffect(
|
|
280
|
+
getValues: () => unknown[],
|
|
281
|
+
cb?: (vals: unknown[]) => void,
|
|
282
|
+
): void {
|
|
283
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
284
|
+
return;
|
|
285
|
+
createEffect(() => {
|
|
286
|
+
const vals = getValues();
|
|
287
|
+
if (cb) cb(vals);
|
|
288
|
+
else console.log(...vals);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A5: $effect.pre — effect that flushes before DOM update
|
|
293
|
+
export function createEffectPre(fn: () => void | (() => void)): () => void {
|
|
294
|
+
let cleanup: (() => void) | void;
|
|
295
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
296
|
+
|
|
297
|
+
const effect = () => {
|
|
298
|
+
if (cleanup) cleanup();
|
|
299
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
300
|
+
ownDeps.clear();
|
|
301
|
+
|
|
302
|
+
const prevEffect = currentEffect;
|
|
303
|
+
const prevDeps = currentDeps;
|
|
304
|
+
currentEffect = effect;
|
|
305
|
+
currentDeps = ownDeps;
|
|
306
|
+
try {
|
|
307
|
+
cleanup = fn();
|
|
308
|
+
} finally {
|
|
309
|
+
currentEffect = prevEffect;
|
|
310
|
+
currentDeps = prevDeps;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
(effect as any).__sigil_pre = true;
|
|
315
|
+
effect();
|
|
316
|
+
|
|
317
|
+
const dispose = () => {
|
|
318
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
319
|
+
ownDeps.clear();
|
|
320
|
+
if (cleanup) cleanup();
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
324
|
+
dispose,
|
|
325
|
+
);
|
|
326
|
+
return dispose;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// A9: Profiler effect timing (dev)
|
|
330
|
+
export function createProfilerEffect(
|
|
331
|
+
name: string,
|
|
332
|
+
fn: () => void | (() => void),
|
|
333
|
+
): () => void {
|
|
334
|
+
return createEffect(() => {
|
|
335
|
+
if (
|
|
336
|
+
typeof process !== "undefined" &&
|
|
337
|
+
process.env.NODE_ENV === "production"
|
|
338
|
+
) {
|
|
339
|
+
return fn();
|
|
340
|
+
}
|
|
341
|
+
const start = performance.now();
|
|
342
|
+
const cleanup = fn();
|
|
343
|
+
const end = performance.now();
|
|
344
|
+
console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
|
|
345
|
+
return cleanup;
|
|
346
|
+
});
|
|
182
347
|
}
|
|
183
348
|
|
|
184
349
|
// biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
|
|
185
350
|
export function createEffect(fn: () => void | (() => void)): () => void {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
351
|
+
let cleanup: (() => void) | void;
|
|
352
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
353
|
+
|
|
354
|
+
const effect = () => {
|
|
355
|
+
if (cleanup) cleanup();
|
|
356
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
357
|
+
ownDeps.clear();
|
|
358
|
+
|
|
359
|
+
const prevEffect = currentEffect;
|
|
360
|
+
const prevDeps = currentDeps;
|
|
361
|
+
currentEffect = effect;
|
|
362
|
+
currentDeps = ownDeps;
|
|
363
|
+
try {
|
|
364
|
+
cleanup = fn();
|
|
365
|
+
} finally {
|
|
366
|
+
currentEffect = prevEffect;
|
|
367
|
+
currentDeps = prevDeps;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
effect();
|
|
372
|
+
|
|
373
|
+
const dispose = () => {
|
|
374
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
375
|
+
ownDeps.clear();
|
|
376
|
+
if (cleanup) cleanup();
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
380
|
+
dispose,
|
|
381
|
+
);
|
|
382
|
+
return dispose;
|
|
218
383
|
}
|
|
219
384
|
|
|
220
385
|
/**
|
|
@@ -223,24 +388,24 @@ export function createEffect(fn: () => void | (() => void)): () => void {
|
|
|
223
388
|
* Supports nesting: inner scopes don't leak to outer ones.
|
|
224
389
|
*/
|
|
225
390
|
export function withEffectScope(fn: () => void): () => void {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
391
|
+
const prev = (globalThis as any).__sigilEffectScope;
|
|
392
|
+
const disposers: (() => void)[] = [];
|
|
393
|
+
(globalThis as any).__sigilEffectScope = disposers;
|
|
394
|
+
try {
|
|
395
|
+
fn();
|
|
396
|
+
} finally {
|
|
397
|
+
(globalThis as any).__sigilEffectScope = prev;
|
|
398
|
+
}
|
|
399
|
+
return () => {
|
|
400
|
+
for (const d of disposers) d();
|
|
401
|
+
disposers.length = 0;
|
|
402
|
+
};
|
|
238
403
|
}
|
|
239
404
|
|
|
240
405
|
export function createMemo<T>(fn: () => T): Signal<T> {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
406
|
+
const inner = createSignal<T>(undefined as T);
|
|
407
|
+
createEffect(() => inner.set(fn()));
|
|
408
|
+
return inner;
|
|
244
409
|
}
|
|
245
410
|
|
|
246
411
|
/**
|
|
@@ -249,23 +414,23 @@ export function createMemo<T>(fn: () => T): Signal<T> {
|
|
|
249
414
|
* When falling back to createElement, appends to parent if provided (SPA mode).
|
|
250
415
|
*/
|
|
251
416
|
export function claim(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
417
|
+
nodes: ChildNode[],
|
|
418
|
+
tag: string,
|
|
419
|
+
parent?: Element,
|
|
255
420
|
): Element {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
421
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
422
|
+
const node = nodes[i];
|
|
423
|
+
if (
|
|
424
|
+
node instanceof Element &&
|
|
425
|
+
node.tagName.toLowerCase() === tag.toLowerCase()
|
|
426
|
+
) {
|
|
427
|
+
nodes.splice(i, 1);
|
|
428
|
+
return node;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const el = document.createElement(tag);
|
|
432
|
+
if (parent) parent.appendChild(el);
|
|
433
|
+
return el;
|
|
269
434
|
}
|
|
270
435
|
|
|
271
436
|
/**
|
|
@@ -274,16 +439,16 @@ export function claim(
|
|
|
274
439
|
* When falling back to createTextNode, appends to parent if provided (SPA mode).
|
|
275
440
|
*/
|
|
276
441
|
export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
442
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
443
|
+
const node = nodes[i];
|
|
444
|
+
if (node instanceof Text) {
|
|
445
|
+
nodes.splice(i, 1);
|
|
446
|
+
return node;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const text = document.createTextNode("");
|
|
450
|
+
if (parent) parent.appendChild(text);
|
|
451
|
+
return text;
|
|
287
452
|
}
|
|
288
453
|
|
|
289
454
|
/**
|
|
@@ -292,20 +457,20 @@ export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
|
292
457
|
* When falling back to createComment, appends to parent if provided (SPA mode).
|
|
293
458
|
*/
|
|
294
459
|
export function claimComment(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
460
|
+
nodes: ChildNode[],
|
|
461
|
+
text: string = "",
|
|
462
|
+
parent?: Element,
|
|
298
463
|
): Comment {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
464
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
465
|
+
const node = nodes[i];
|
|
466
|
+
if (node instanceof Comment && node.data === text) {
|
|
467
|
+
nodes.splice(i, 1);
|
|
468
|
+
return node;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const comment = document.createComment(text);
|
|
472
|
+
if (parent) parent.appendChild(comment);
|
|
473
|
+
return comment;
|
|
309
474
|
}
|
|
310
475
|
|
|
311
476
|
/**
|
|
@@ -316,33 +481,33 @@ export function claimComment(
|
|
|
316
481
|
* or null to discard.
|
|
317
482
|
*/
|
|
318
483
|
export function reconcile<T>(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
484
|
+
anchor: Partial<Comment>,
|
|
485
|
+
keyMap: Map<unknown, Node>,
|
|
486
|
+
newItems: T[],
|
|
487
|
+
getKey: (item: T) => unknown,
|
|
488
|
+
createNode: (item: T) => Node,
|
|
489
|
+
// is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
|
|
325
490
|
): void {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
491
|
+
const newKeys = new Set(newItems.map(getKey));
|
|
492
|
+
|
|
493
|
+
// Remove nodes no longer in list (or claim them for hydration)
|
|
494
|
+
for (const [key, node] of keyMap) {
|
|
495
|
+
if (!newKeys.has(key)) {
|
|
496
|
+
node.parentNode?.removeChild(node);
|
|
497
|
+
keyMap.delete(key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Insert/move in order — anchor.before() handles both cases
|
|
502
|
+
for (const item of newItems) {
|
|
503
|
+
const key = getKey(item);
|
|
504
|
+
let node = keyMap.get(key);
|
|
505
|
+
if (!node) {
|
|
506
|
+
node = createNode(item);
|
|
507
|
+
keyMap.set(key, node);
|
|
508
|
+
}
|
|
509
|
+
anchor.before?.(node);
|
|
510
|
+
}
|
|
346
511
|
}
|
|
347
512
|
|
|
348
513
|
/**
|
|
@@ -353,59 +518,63 @@ export function reconcile<T>(
|
|
|
353
518
|
* builds the keyMap, removes from DOM, then reconcile re-inserts in order.
|
|
354
519
|
*/
|
|
355
520
|
export function hydrateKeyedList<T>(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
521
|
+
nodesVar: ChildNode[],
|
|
522
|
+
signalItems: T[],
|
|
523
|
+
getKey: (item: T) => unknown,
|
|
524
|
+
createNode: (item: T) => Node,
|
|
525
|
+
parent?: Element,
|
|
361
526
|
): { anchor: Comment; keyMap: Map<unknown, Node> } {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
527
|
+
// 1. Claim the <!--g--> opening delimiter as anchor
|
|
528
|
+
const anchor = claimComment(nodesVar, "g", parent);
|
|
529
|
+
|
|
530
|
+
// 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
|
|
531
|
+
const keyMap = new Map<unknown, Node>();
|
|
532
|
+
const anchorParent = anchor.parentNode;
|
|
533
|
+
if (anchorParent && signalItems.length > 0) {
|
|
534
|
+
const claimed: Element[] = [];
|
|
535
|
+
let next = anchor.nextSibling;
|
|
536
|
+
while (next) {
|
|
537
|
+
const current = next;
|
|
538
|
+
next = next.nextSibling;
|
|
539
|
+
// Stop at closing <!--/g--> delimiter and consume it
|
|
540
|
+
if (current instanceof Comment && current.data === "/g") {
|
|
541
|
+
current.remove();
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
if (current instanceof Element) {
|
|
545
|
+
current.remove();
|
|
546
|
+
claimed.push(current);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Map claimed elements to keys by index
|
|
550
|
+
for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
|
|
551
|
+
//@ts-expect-error i is always in bounds
|
|
552
|
+
const key = getKey(signalItems[i]);
|
|
553
|
+
//@ts-expect-error i is always in bounds
|
|
554
|
+
keyMap.set(key, claimed[i]);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 3. Reconcile — keyMap is pre-populated with claimed nodes
|
|
559
|
+
reconcile(anchor, keyMap, signalItems, getKey, createNode);
|
|
560
|
+
return { anchor, keyMap };
|
|
396
561
|
}
|
|
397
562
|
|
|
398
563
|
export function insert(parent: Element, value: unknown): void {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
564
|
+
if (value === false || value === null || value === undefined) return;
|
|
565
|
+
if (typeof value === "function") {
|
|
566
|
+
insert(parent, value());
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (Array.isArray(value)) {
|
|
570
|
+
for (const item of value) insert(parent, item);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (value instanceof Node) {
|
|
574
|
+
parent.appendChild(value);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
parent.append(String(value));
|
|
409
578
|
}
|
|
410
579
|
|
|
411
580
|
/**
|
|
@@ -416,16 +585,16 @@ export function insert(parent: Element, value: unknown): void {
|
|
|
416
585
|
export type Directive = (node: Element, params: unknown) => void | (() => void);
|
|
417
586
|
|
|
418
587
|
export function applyDirective(
|
|
419
|
-
|
|
420
|
-
|
|
588
|
+
node: Element,
|
|
589
|
+
directive: Directive | [Directive, unknown],
|
|
421
590
|
): void {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
591
|
+
const [fn, params] = Array.isArray(directive)
|
|
592
|
+
? directive
|
|
593
|
+
: [directive, undefined];
|
|
594
|
+
const cleanup = fn(node, params);
|
|
595
|
+
if (typeof cleanup === "function") {
|
|
596
|
+
createEffect(() => () => cleanup());
|
|
597
|
+
}
|
|
429
598
|
}
|
|
430
599
|
|
|
431
600
|
export type ContextKey<T> = symbol & { __type: T };
|
|
@@ -434,23 +603,23 @@ const clientContextStore = new Map<symbol, unknown>();
|
|
|
434
603
|
|
|
435
604
|
let storeProvider: (() => Map<symbol, unknown>) | null = null;
|
|
436
605
|
export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
|
|
437
|
-
|
|
606
|
+
storeProvider = fn;
|
|
438
607
|
}
|
|
439
608
|
|
|
440
609
|
function getStore(): Map<symbol, unknown> {
|
|
441
|
-
|
|
610
|
+
return storeProvider?.() ?? clientContextStore;
|
|
442
611
|
}
|
|
443
612
|
|
|
444
613
|
export function createContext<T>(): ContextKey<T> {
|
|
445
|
-
|
|
614
|
+
return Symbol() as ContextKey<T>;
|
|
446
615
|
}
|
|
447
616
|
|
|
448
617
|
export function setContext<T>(key: ContextKey<T>, value: T): void {
|
|
449
|
-
|
|
618
|
+
getStore().set(key as unknown as symbol, value);
|
|
450
619
|
}
|
|
451
620
|
|
|
452
621
|
export function getContext<T>(key: ContextKey<T>): T {
|
|
453
|
-
|
|
622
|
+
return getStore().get(key as unknown as symbol) as T;
|
|
454
623
|
}
|
|
455
624
|
|
|
456
625
|
const _hydrationStack: ChildNode[][] = [];
|