@sigil-dev/runtime 0.5.0 → 0.6.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/index.test.ts +59 -35
- package/index.ts +318 -311
- package/jsr.json +1 -1
- package/package.json +1 -1
package/index.test.ts
CHANGED
|
@@ -753,32 +753,32 @@ describe("hydration: full element tree claim", () => {
|
|
|
753
753
|
});
|
|
754
754
|
|
|
755
755
|
describe("Scary reactivity", () => {
|
|
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
|
-
})
|
|
756
|
+
test("effect unsubscribes from stale dependencies", () => {
|
|
757
|
+
const condition = createSignal(true);
|
|
758
|
+
const a = createSignal(1);
|
|
759
|
+
const b = createSignal(1);
|
|
760
|
+
let runs = 0;
|
|
761
|
+
|
|
762
|
+
createEffect(() => {
|
|
763
|
+
runs++;
|
|
764
|
+
if (condition()) {
|
|
765
|
+
a(); // read a only when condition is true
|
|
766
|
+
} else {
|
|
767
|
+
b(); // read b only when condition is false
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
expect(runs).toBe(1);
|
|
772
|
+
|
|
773
|
+
// flip condition, effect should now track b, not a
|
|
774
|
+
condition.set(false);
|
|
775
|
+
expect(runs).toBe(2);
|
|
776
|
+
|
|
777
|
+
// updating a should NOT trigger the effect anymore
|
|
778
|
+
a.set(99);
|
|
779
|
+
expect(runs).toBe(2); // ← this will fail with current implementation
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
782
|
|
|
783
783
|
describe("withEffectScope", () => {
|
|
784
784
|
test("collects effects created during fn()", () => {
|
|
@@ -820,8 +820,14 @@ describe("withEffectScope", () => {
|
|
|
820
820
|
let runsA = 0;
|
|
821
821
|
let runsB = 0;
|
|
822
822
|
const dispose = withEffectScope(() => {
|
|
823
|
-
createEffect(() => {
|
|
824
|
-
|
|
823
|
+
createEffect(() => {
|
|
824
|
+
a();
|
|
825
|
+
runsA++;
|
|
826
|
+
});
|
|
827
|
+
createEffect(() => {
|
|
828
|
+
b();
|
|
829
|
+
runsB++;
|
|
830
|
+
});
|
|
825
831
|
});
|
|
826
832
|
a.set(1);
|
|
827
833
|
b.set(1);
|
|
@@ -841,10 +847,16 @@ describe("withEffectScope", () => {
|
|
|
841
847
|
let outsideRuns = 0;
|
|
842
848
|
let insideRuns = 0;
|
|
843
849
|
|
|
844
|
-
createEffect(() => {
|
|
850
|
+
createEffect(() => {
|
|
851
|
+
count();
|
|
852
|
+
outsideRuns++;
|
|
853
|
+
});
|
|
845
854
|
|
|
846
855
|
const dispose = withEffectScope(() => {
|
|
847
|
-
createEffect(() => {
|
|
856
|
+
createEffect(() => {
|
|
857
|
+
count();
|
|
858
|
+
insideRuns++;
|
|
859
|
+
});
|
|
848
860
|
});
|
|
849
861
|
|
|
850
862
|
count.set(1);
|
|
@@ -855,7 +867,7 @@ describe("withEffectScope", () => {
|
|
|
855
867
|
|
|
856
868
|
count.set(2);
|
|
857
869
|
expect(outsideRuns).toBe(3); // still running
|
|
858
|
-
expect(insideRuns).toBe(2);
|
|
870
|
+
expect(insideRuns).toBe(2); // stopped
|
|
859
871
|
});
|
|
860
872
|
|
|
861
873
|
test("nested scopes: inner dispose doesn't affect outer", () => {
|
|
@@ -864,10 +876,16 @@ describe("withEffectScope", () => {
|
|
|
864
876
|
let innerRuns = 0;
|
|
865
877
|
|
|
866
878
|
const disposeOuter = withEffectScope(() => {
|
|
867
|
-
createEffect(() => {
|
|
879
|
+
createEffect(() => {
|
|
880
|
+
count();
|
|
881
|
+
outerRuns++;
|
|
882
|
+
});
|
|
868
883
|
// simulates a child component scope inside a page scope
|
|
869
884
|
const disposeInner = withEffectScope(() => {
|
|
870
|
-
createEffect(() => {
|
|
885
|
+
createEffect(() => {
|
|
886
|
+
count();
|
|
887
|
+
innerRuns++;
|
|
888
|
+
});
|
|
871
889
|
});
|
|
872
890
|
disposeInner();
|
|
873
891
|
});
|
|
@@ -886,7 +904,10 @@ describe("withEffectScope", () => {
|
|
|
886
904
|
let memoRuns = 0;
|
|
887
905
|
const dispose = withEffectScope(() => {
|
|
888
906
|
// createMemo internally calls createEffect
|
|
889
|
-
const doubled = createMemo(() => {
|
|
907
|
+
const doubled = createMemo(() => {
|
|
908
|
+
memoRuns++;
|
|
909
|
+
return count() * 2;
|
|
910
|
+
});
|
|
890
911
|
expect(doubled()).toBe(0);
|
|
891
912
|
});
|
|
892
913
|
expect(memoRuns).toBe(1);
|
|
@@ -902,7 +923,10 @@ describe("withEffectScope", () => {
|
|
|
902
923
|
const count = createSignal(0);
|
|
903
924
|
let runs = 0;
|
|
904
925
|
const dispose = withEffectScope(() => {
|
|
905
|
-
createEffect(() => {
|
|
926
|
+
createEffect(() => {
|
|
927
|
+
count();
|
|
928
|
+
runs++;
|
|
929
|
+
});
|
|
906
930
|
});
|
|
907
931
|
dispose();
|
|
908
932
|
dispose(); // second call must not throw or double-fire
|
package/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const SIGIL_SAFE = Symbol.for("sigil.safe");
|
|
2
2
|
export class SafeHtml {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
[SIGIL_SAFE] = true;
|
|
4
|
+
constructor(public v: string) {}
|
|
5
|
+
toString() {
|
|
6
|
+
return this.v;
|
|
7
|
+
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
let currentEffect: (() => void) | null = null;
|
|
@@ -14,202 +14,204 @@ let batchDepth = 0;
|
|
|
14
14
|
const pendingEffects = new Set<() => void>();
|
|
15
15
|
|
|
16
16
|
const targetDepsMap = new WeakMap<
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
object,
|
|
18
|
+
Map<string | symbol, Set<() => void>>
|
|
19
19
|
>();
|
|
20
20
|
|
|
21
21
|
function getDeps(target: object, key: string | symbol): Set<() => void> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
let deps = targetDepsMap.get(target);
|
|
23
|
+
if (!deps) {
|
|
24
|
+
deps = new Map();
|
|
25
|
+
targetDepsMap.set(target, deps);
|
|
26
|
+
}
|
|
27
|
+
let set = deps.get(key);
|
|
28
|
+
if (!set) {
|
|
29
|
+
set = new Set();
|
|
30
|
+
deps.set(key, set);
|
|
31
|
+
}
|
|
32
|
+
return set;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function track(target: object, key: string | symbol) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (currentEffect && currentDeps) {
|
|
37
|
+
const deps = getDeps(target, key);
|
|
38
|
+
deps.add(currentEffect);
|
|
39
|
+
currentDeps.add(deps); // Set, deduplicates automatically
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function trigger(target: object, key: string | symbol) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
const deps = targetDepsMap.get(target);
|
|
45
|
+
if (!deps) return;
|
|
46
|
+
const set = deps.get(key);
|
|
47
|
+
if (!set) return;
|
|
48
|
+
for (const fn of [...set]) {
|
|
49
|
+
if (batchDepth > 0) pendingEffects.add(fn);
|
|
50
|
+
else fn();
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const proxyCache = new WeakMap<object, object>();
|
|
55
55
|
const reactiveTargets = new WeakSet<object>();
|
|
56
56
|
|
|
57
57
|
function createReactiveProxy<T extends object>(target: T): T {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
58
|
+
const existing = proxyCache.get(target);
|
|
59
|
+
if (existing) return existing as T;
|
|
60
|
+
|
|
61
|
+
const proxy = new Proxy(target, {
|
|
62
|
+
get(target, key, receiver) {
|
|
63
|
+
if (key === "set") {
|
|
64
|
+
return (newValue: T) => {
|
|
65
|
+
const oldKeys = new Set(Object.keys(target));
|
|
66
|
+
const newKeys = new Set(Object.keys(newValue));
|
|
67
|
+
for (const k of oldKeys) {
|
|
68
|
+
if (!newKeys.has(k)) {
|
|
69
|
+
delete (target as any)[k];
|
|
70
|
+
trigger(target, k);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const k of newKeys) {
|
|
74
|
+
(target as any)[k] = (newValue as any)[k];
|
|
75
|
+
trigger(target, k);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(target)) {
|
|
78
|
+
trigger(target, "length");
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (key === "peek") {
|
|
84
|
+
return () => target;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof key === "symbol") {
|
|
88
|
+
return Reflect.get(target, key, receiver);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
track(target, key);
|
|
92
|
+
const value = target[key as keyof T];
|
|
93
|
+
|
|
94
|
+
if (typeof value === "object" && value !== null) {
|
|
95
|
+
return createReactiveProxy(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return value;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
set(target, key, newValue, receiver) {
|
|
102
|
+
const oldValue = target[key as keyof T];
|
|
103
|
+
const result = Reflect.set(target, key, newValue, receiver);
|
|
104
|
+
if (oldValue !== newValue) {
|
|
105
|
+
trigger(target, key as string | symbol);
|
|
106
|
+
if (
|
|
107
|
+
Array.isArray(target) &&
|
|
108
|
+
typeof key === "string" &&
|
|
109
|
+
!Number.isNaN(Number(key))
|
|
110
|
+
) {
|
|
111
|
+
trigger(target, "length");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
deleteProperty(target, key) {
|
|
118
|
+
const result = Reflect.deleteProperty(target, key);
|
|
119
|
+
trigger(target, key as string | symbol);
|
|
120
|
+
return result;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
proxyCache.set(target, proxy);
|
|
125
|
+
reactiveTargets.add(target);
|
|
126
|
+
return proxy as T;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// ─── createSignal (branches on type) ───
|
|
130
130
|
|
|
131
131
|
export type Signal<T> = {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
(): T;
|
|
133
|
+
set: (next: T) => void;
|
|
134
|
+
peek: () => T;
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
export function batch(fn: () => void) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
batchDepth++;
|
|
139
|
+
fn();
|
|
140
|
+
batchDepth--;
|
|
141
|
+
if (batchDepth === 0) {
|
|
142
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returing cleanup is intended
|
|
143
|
+
pendingEffects.forEach((e) => e());
|
|
144
|
+
pendingEffects.clear();
|
|
145
|
+
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
export function createSignal<T>(initial: T): Signal<T> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
149
|
+
if (typeof initial === "object" && initial !== null) {
|
|
150
|
+
const proxy = createReactiveProxy(initial);
|
|
151
|
+
const getter = () => proxy;
|
|
152
|
+
getter.set = (next: T) => (proxy as any).set(next);
|
|
153
|
+
getter.peek = () => initial;
|
|
154
|
+
return getter;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const subs = new Set<() => void>();
|
|
158
|
+
let value = initial;
|
|
159
|
+
|
|
160
|
+
const getter = () => {
|
|
161
|
+
if (currentEffect && currentDeps) {
|
|
162
|
+
subs.add(currentEffect);
|
|
163
|
+
currentDeps.add(subs);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
getter.set = (next: T) => {
|
|
169
|
+
value = next;
|
|
170
|
+
for (const fn of [...subs]) {
|
|
171
|
+
if (batchDepth > 0) pendingEffects.add(fn);
|
|
172
|
+
else fn();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
getter.peek = () => value;
|
|
177
|
+
|
|
178
|
+
return getter;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
|
|
182
182
|
export function createEffect(fn: () => void | (() => void)): () => void {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
183
|
+
let cleanup: (() => void) | void;
|
|
184
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
185
|
+
|
|
186
|
+
const effect = () => {
|
|
187
|
+
if (cleanup) cleanup();
|
|
188
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
189
|
+
ownDeps.clear();
|
|
190
|
+
|
|
191
|
+
const prevEffect = currentEffect;
|
|
192
|
+
const prevDeps = currentDeps;
|
|
193
|
+
currentEffect = effect;
|
|
194
|
+
currentDeps = ownDeps;
|
|
195
|
+
try {
|
|
196
|
+
cleanup = fn();
|
|
197
|
+
} finally {
|
|
198
|
+
currentEffect = prevEffect;
|
|
199
|
+
currentDeps = prevDeps;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
effect();
|
|
204
|
+
|
|
205
|
+
const dispose = () => {
|
|
206
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
207
|
+
ownDeps.clear();
|
|
208
|
+
if (cleanup) cleanup();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
212
|
+
dispose,
|
|
213
|
+
);
|
|
214
|
+
return dispose;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
/**
|
|
@@ -218,24 +220,24 @@ export function createEffect(fn: () => void | (() => void)): () => void {
|
|
|
218
220
|
* Supports nesting: inner scopes don't leak to outer ones.
|
|
219
221
|
*/
|
|
220
222
|
export function withEffectScope(fn: () => void): () => void {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
223
|
+
const prev = (globalThis as any).__sigilEffectScope;
|
|
224
|
+
const disposers: (() => void)[] = [];
|
|
225
|
+
(globalThis as any).__sigilEffectScope = disposers;
|
|
226
|
+
try {
|
|
227
|
+
fn();
|
|
228
|
+
} finally {
|
|
229
|
+
(globalThis as any).__sigilEffectScope = prev;
|
|
230
|
+
}
|
|
231
|
+
return () => {
|
|
232
|
+
for (const d of disposers) d();
|
|
233
|
+
disposers.length = 0;
|
|
234
|
+
};
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
export function createMemo<T>(fn: () => T): Signal<T> {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
const inner = createSignal<T>(undefined as T);
|
|
239
|
+
createEffect(() => inner.set(fn()));
|
|
240
|
+
return inner;
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
/**
|
|
@@ -244,23 +246,23 @@ export function createMemo<T>(fn: () => T): Signal<T> {
|
|
|
244
246
|
* When falling back to createElement, appends to parent if provided (SPA mode).
|
|
245
247
|
*/
|
|
246
248
|
export function claim(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
nodes: ChildNode[],
|
|
250
|
+
tag: string,
|
|
251
|
+
parent?: Element,
|
|
250
252
|
): Element {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
253
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
254
|
+
const node = nodes[i];
|
|
255
|
+
if (
|
|
256
|
+
node instanceof Element &&
|
|
257
|
+
node.tagName.toLowerCase() === tag.toLowerCase()
|
|
258
|
+
) {
|
|
259
|
+
nodes.splice(i, 1);
|
|
260
|
+
return node;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const el = document.createElement(tag);
|
|
264
|
+
if (parent) parent.appendChild(el);
|
|
265
|
+
return el;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
/**
|
|
@@ -269,16 +271,16 @@ export function claim(
|
|
|
269
271
|
* When falling back to createTextNode, appends to parent if provided (SPA mode).
|
|
270
272
|
*/
|
|
271
273
|
export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
274
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
275
|
+
const node = nodes[i];
|
|
276
|
+
if (node instanceof Text) {
|
|
277
|
+
nodes.splice(i, 1);
|
|
278
|
+
return node;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const text = document.createTextNode("");
|
|
282
|
+
if (parent) parent.appendChild(text);
|
|
283
|
+
return text;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
/**
|
|
@@ -287,20 +289,20 @@ export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
|
287
289
|
* When falling back to createComment, appends to parent if provided (SPA mode).
|
|
288
290
|
*/
|
|
289
291
|
export function claimComment(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
nodes: ChildNode[],
|
|
293
|
+
text: string = "",
|
|
294
|
+
parent?: Element,
|
|
293
295
|
): Comment {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
296
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
297
|
+
const node = nodes[i];
|
|
298
|
+
if (node instanceof Comment && node.data === text) {
|
|
299
|
+
nodes.splice(i, 1);
|
|
300
|
+
return node;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const comment = document.createComment(text);
|
|
304
|
+
if (parent) parent.appendChild(comment);
|
|
305
|
+
return comment;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
/**
|
|
@@ -311,33 +313,33 @@ export function claimComment(
|
|
|
311
313
|
* or null to discard.
|
|
312
314
|
*/
|
|
313
315
|
export function reconcile<T>(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
anchor: Partial<Comment>,
|
|
317
|
+
keyMap: Map<unknown, Node>,
|
|
318
|
+
newItems: T[],
|
|
319
|
+
getKey: (item: T) => unknown,
|
|
320
|
+
createNode: (item: T) => Node,
|
|
321
|
+
// is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
|
|
320
322
|
): void {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
323
|
+
const newKeys = new Set(newItems.map(getKey));
|
|
324
|
+
|
|
325
|
+
// Remove nodes no longer in list (or claim them for hydration)
|
|
326
|
+
for (const [key, node] of keyMap) {
|
|
327
|
+
if (!newKeys.has(key)) {
|
|
328
|
+
node.parentNode?.removeChild(node);
|
|
329
|
+
keyMap.delete(key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Insert/move in order — anchor.before() handles both cases
|
|
334
|
+
for (const item of newItems) {
|
|
335
|
+
const key = getKey(item);
|
|
336
|
+
let node = keyMap.get(key);
|
|
337
|
+
if (!node) {
|
|
338
|
+
node = createNode(item);
|
|
339
|
+
keyMap.set(key, node);
|
|
340
|
+
}
|
|
341
|
+
anchor.before?.(node);
|
|
342
|
+
}
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
/**
|
|
@@ -348,59 +350,59 @@ export function reconcile<T>(
|
|
|
348
350
|
* builds the keyMap, removes from DOM, then reconcile re-inserts in order.
|
|
349
351
|
*/
|
|
350
352
|
export function hydrateKeyedList<T>(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
nodesVar: ChildNode[],
|
|
354
|
+
signalItems: T[],
|
|
355
|
+
getKey: (item: T) => unknown,
|
|
356
|
+
createNode: (item: T) => Node,
|
|
357
|
+
parent?: Element,
|
|
356
358
|
): { anchor: Comment; keyMap: Map<unknown, Node> } {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
359
|
+
// 1. Claim the <!--g--> opening delimiter as anchor
|
|
360
|
+
const anchor = claimComment(nodesVar, "g", parent);
|
|
361
|
+
|
|
362
|
+
// 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
|
|
363
|
+
const keyMap = new Map<unknown, Node>();
|
|
364
|
+
const anchorParent = anchor.parentNode;
|
|
365
|
+
if (anchorParent && signalItems.length > 0) {
|
|
366
|
+
const claimed: Element[] = [];
|
|
367
|
+
let next = anchor.nextSibling;
|
|
368
|
+
while (next) {
|
|
369
|
+
const current = next;
|
|
370
|
+
next = next.nextSibling;
|
|
371
|
+
// Stop at closing <!--/g--> delimiter and consume it
|
|
372
|
+
if (current instanceof Comment && current.data === "/g") {
|
|
373
|
+
current.remove();
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (current instanceof Element) {
|
|
377
|
+
current.remove();
|
|
378
|
+
claimed.push(current);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Map claimed elements to keys by index
|
|
382
|
+
for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
|
|
383
|
+
//@ts-expect-error i is always in bounds
|
|
384
|
+
const key = getKey(signalItems[i]);
|
|
385
|
+
//@ts-expect-error i is always in bounds
|
|
386
|
+
keyMap.set(key, claimed[i]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 3. Reconcile — keyMap is pre-populated with claimed nodes
|
|
391
|
+
reconcile(anchor, keyMap, signalItems, getKey, createNode);
|
|
392
|
+
return { anchor, keyMap };
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
export function insert(parent: Element, value: unknown): void {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
396
|
+
if (value === false || value === null || value === undefined) return;
|
|
397
|
+
if (Array.isArray(value)) {
|
|
398
|
+
for (const item of value) insert(parent, item);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (value instanceof Node) {
|
|
402
|
+
parent.appendChild(value);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
parent.append(String(value));
|
|
404
406
|
}
|
|
405
407
|
|
|
406
408
|
/**
|
|
@@ -410,12 +412,17 @@ export function insert(parent: Element, value: unknown): void {
|
|
|
410
412
|
*/
|
|
411
413
|
export type Directive = (node: Element, params: unknown) => void | (() => void);
|
|
412
414
|
|
|
413
|
-
export function applyDirective(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
export function applyDirective(
|
|
416
|
+
node: Element,
|
|
417
|
+
directive: Directive | [Directive, unknown],
|
|
418
|
+
): void {
|
|
419
|
+
const [fn, params] = Array.isArray(directive)
|
|
420
|
+
? directive
|
|
421
|
+
: [directive, undefined];
|
|
422
|
+
const cleanup = fn(node, params);
|
|
423
|
+
if (typeof cleanup === "function") {
|
|
424
|
+
createEffect(() => () => cleanup());
|
|
425
|
+
}
|
|
419
426
|
}
|
|
420
427
|
|
|
421
428
|
export type ContextKey<T> = symbol & { __type: T };
|
|
@@ -424,21 +431,21 @@ const clientContextStore = new Map<symbol, unknown>();
|
|
|
424
431
|
|
|
425
432
|
let storeProvider: (() => Map<symbol, unknown>) | null = null;
|
|
426
433
|
export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
|
|
427
|
-
|
|
434
|
+
storeProvider = fn;
|
|
428
435
|
}
|
|
429
436
|
|
|
430
437
|
function getStore(): Map<symbol, unknown> {
|
|
431
|
-
|
|
438
|
+
return storeProvider?.() ?? clientContextStore;
|
|
432
439
|
}
|
|
433
440
|
|
|
434
441
|
export function createContext<T>(): ContextKey<T> {
|
|
435
|
-
|
|
442
|
+
return Symbol() as ContextKey<T>;
|
|
436
443
|
}
|
|
437
444
|
|
|
438
445
|
export function setContext<T>(key: ContextKey<T>, value: T): void {
|
|
439
|
-
|
|
446
|
+
getStore().set(key as unknown as symbol, value);
|
|
440
447
|
}
|
|
441
448
|
|
|
442
449
|
export function getContext<T>(key: ContextKey<T>): T {
|
|
443
|
-
|
|
450
|
+
return getStore().get(key as unknown as symbol) as T;
|
|
444
451
|
}
|
package/jsr.json
CHANGED