@tschk/moonshine-svelte 0.0.0-stage → 0.4.4
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 +473 -2
- package/package.json +57 -4
- package/src/await.ts +123 -0
- package/src/binding.ts +115 -0
- package/src/class.ts +21 -0
- package/src/const-derived.ts +47 -0
- package/src/derived.ts +276 -0
- package/src/directives.ts +129 -0
- package/src/each.ts +306 -0
- package/src/effect.ts +243 -0
- package/src/framework.ts +1 -0
- package/src/history.ts +104 -0
- package/src/index.ts +55 -0
- package/src/mount-types.ts +46 -0
- package/src/mount.ts +100 -0
- package/src/pre.ts +671 -0
- package/src/private-class-state.ts +54 -0
- package/src/prop.ts +64 -0
- package/src/raw-snippet-dom.ts +214 -0
- package/src/raw-snippet-ssr.ts +75 -0
- package/src/raw-snippet.ts +36 -0
- package/src/raw-ssr-lifecycle.ts +8 -0
- package/src/raw-state.ts +19 -0
- package/src/runes.ts +11 -0
- package/src/server-mount.ts +31 -0
- package/src/server-prop.ts +29 -0
- package/src/server-store.ts +113 -0
- package/src/spread-props.ts +153 -0
- package/src/ssr.ts +50 -0
- package/src/state.ts +492 -0
- package/src/store-binding.ts +125 -0
- package/src/store-internal.ts +34 -0
- package/src/store-write.ts +38 -0
- package/src/store.ts +208 -0
- package/src/svelte.ts +152 -0
- package/src/tick-queue.ts +31 -0
package/src/each.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional keyed each. Reconciliation visit/move ordering adapted from
|
|
3
|
+
* Svelte's MIT-licensed each.js (copyright 2016–2025 Svelte Contributors).
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
8
|
+
* of the Software, and to permit persons to whom the Software is furnished to
|
|
9
|
+
* do so, subject to the following conditions:
|
|
10
|
+
* The above copyright notice and this permission notice shall be included in
|
|
11
|
+
* all copies or substantial portions of the Software.
|
|
12
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
16
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
18
|
+
* SOFTWARE.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
createInsertionRange,
|
|
22
|
+
insertionRangeEnd,
|
|
23
|
+
prepareInsertionRange,
|
|
24
|
+
moveInsertionRange,
|
|
25
|
+
type DOMChild,
|
|
26
|
+
} from "@tschk/moonshine-dom/dom";
|
|
27
|
+
import {
|
|
28
|
+
createRoot,
|
|
29
|
+
createComponentScope,
|
|
30
|
+
getOwner,
|
|
31
|
+
getRenderScheduler,
|
|
32
|
+
createSignal,
|
|
33
|
+
createRenderMemo,
|
|
34
|
+
onCleanup,
|
|
35
|
+
type Owner,
|
|
36
|
+
type Accessor,
|
|
37
|
+
type Setter,
|
|
38
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
39
|
+
type Entry<T> = {
|
|
40
|
+
key: unknown;
|
|
41
|
+
node: Node;
|
|
42
|
+
stop: () => void;
|
|
43
|
+
scope: Owner;
|
|
44
|
+
setItem: Setter<T>;
|
|
45
|
+
setIndex: Setter<number>;
|
|
46
|
+
prev: Entry<T> | null;
|
|
47
|
+
next: Entry<T> | null;
|
|
48
|
+
fresh: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Source compiler-owned entry; no SSR dispatch or ordinary native policy. */
|
|
51
|
+
export function keyedEach<T>(
|
|
52
|
+
read: () => ArrayLike<T> | Iterable<T> | null | undefined,
|
|
53
|
+
key: (item: T, index: number) => unknown,
|
|
54
|
+
map: (item: Accessor<T>, index: Accessor<number>) => DOMChild,
|
|
55
|
+
fallback?: () => DOMChild,
|
|
56
|
+
retain?: (node: Node) => boolean,
|
|
57
|
+
): Node {
|
|
58
|
+
const parent = getOwner();
|
|
59
|
+
if (!parent || parent.disposed) throw new Error("Each requires a live owner");
|
|
60
|
+
const group = createComponentScope(() => getOwner()!);
|
|
61
|
+
const scheduler = getRenderScheduler();
|
|
62
|
+
const entries = new Map<unknown, Entry<T>>();
|
|
63
|
+
let first: Entry<T> | null = null,
|
|
64
|
+
last: Entry<T> | null = null,
|
|
65
|
+
initial = true,
|
|
66
|
+
closed = false;
|
|
67
|
+
let empty: Entry<T> | undefined;
|
|
68
|
+
let outer!: Node;
|
|
69
|
+
function unlink(row: Entry<T>) {
|
|
70
|
+
if (row.prev) row.prev.next = row.next;
|
|
71
|
+
else first = row.next;
|
|
72
|
+
if (row.next) row.next.prev = row.prev;
|
|
73
|
+
else last = row.prev;
|
|
74
|
+
}
|
|
75
|
+
function link(row: Entry<T>, prev: Entry<T> | null, next: Entry<T> | null) {
|
|
76
|
+
row.prev = prev;
|
|
77
|
+
row.next = next;
|
|
78
|
+
if (prev) prev.next = row;
|
|
79
|
+
else first = row;
|
|
80
|
+
if (next) next.prev = row;
|
|
81
|
+
else last = row;
|
|
82
|
+
}
|
|
83
|
+
function destroy(rows: Iterable<Entry<T>>) {
|
|
84
|
+
let failed = false,
|
|
85
|
+
failure: unknown;
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
unlink(row);
|
|
88
|
+
entries.delete(row.key);
|
|
89
|
+
try {
|
|
90
|
+
row.stop();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!failed) failure = error;
|
|
93
|
+
failed = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (failed) throw failure;
|
|
97
|
+
}
|
|
98
|
+
function create(
|
|
99
|
+
value: T,
|
|
100
|
+
index: number,
|
|
101
|
+
id: unknown,
|
|
102
|
+
renderRow = map,
|
|
103
|
+
): Entry<T> {
|
|
104
|
+
let stopRow: (() => void) | undefined;
|
|
105
|
+
try {
|
|
106
|
+
return createRoot((dispose) => {
|
|
107
|
+
stopRow = dispose;
|
|
108
|
+
const scope = getOwner()!;
|
|
109
|
+
const [item, setItem] = createSignal(value);
|
|
110
|
+
const [position, setIndex] = createSignal(index);
|
|
111
|
+
const build = () => {
|
|
112
|
+
const node = createInsertionRange(
|
|
113
|
+
renderRow(item, position),
|
|
114
|
+
undefined,
|
|
115
|
+
retain,
|
|
116
|
+
);
|
|
117
|
+
prepareInsertionRange(node);
|
|
118
|
+
return node;
|
|
119
|
+
};
|
|
120
|
+
const activeScheduler = getRenderScheduler();
|
|
121
|
+
const node = activeScheduler
|
|
122
|
+
? activeScheduler.branch(build, position)
|
|
123
|
+
: build();
|
|
124
|
+
if (closed || group.disposed) {
|
|
125
|
+
dispose();
|
|
126
|
+
throw new Error("Each disposed during row setup");
|
|
127
|
+
}
|
|
128
|
+
scope.parent = group;
|
|
129
|
+
group.children.add(scope);
|
|
130
|
+
const row: Entry<T> = {
|
|
131
|
+
key: id,
|
|
132
|
+
node,
|
|
133
|
+
stop: dispose,
|
|
134
|
+
scope,
|
|
135
|
+
setItem,
|
|
136
|
+
setIndex,
|
|
137
|
+
prev: null,
|
|
138
|
+
next: null,
|
|
139
|
+
fresh: true,
|
|
140
|
+
};
|
|
141
|
+
link(row, last, null);
|
|
142
|
+
return row;
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
try {
|
|
146
|
+
stopRow?.();
|
|
147
|
+
} catch {}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function stop() {
|
|
152
|
+
if (closed) return;
|
|
153
|
+
closed = true;
|
|
154
|
+
const rows = [...entries.values()];
|
|
155
|
+
if (empty) rows.push(empty);
|
|
156
|
+
empty = undefined;
|
|
157
|
+
destroy(rows);
|
|
158
|
+
}
|
|
159
|
+
onCleanup(stop);
|
|
160
|
+
const reconcile = () => {
|
|
161
|
+
if (closed) return null;
|
|
162
|
+
const input = read();
|
|
163
|
+
const items = Array.isArray(input) ? input : Array.from(input ?? []);
|
|
164
|
+
const length = items.length;
|
|
165
|
+
const keys = new Set<unknown>();
|
|
166
|
+
const preparedKeys: unknown[] = [];
|
|
167
|
+
const created: Entry<T>[] = [];
|
|
168
|
+
try {
|
|
169
|
+
for (let index = 0; index < length; index++) {
|
|
170
|
+
const value = items[index]!,
|
|
171
|
+
id = key(value, index);
|
|
172
|
+
if (keys.has(id)) throw new Error("Duplicate each key");
|
|
173
|
+
keys.add(id);
|
|
174
|
+
preparedKeys.push(id);
|
|
175
|
+
const row = entries.get(id);
|
|
176
|
+
if (row) {
|
|
177
|
+
row.setItem(() => value);
|
|
178
|
+
row.setIndex(index);
|
|
179
|
+
} else {
|
|
180
|
+
const createdRow = create(value, index, id);
|
|
181
|
+
created.push(createdRow);
|
|
182
|
+
entries.set(id, createdRow);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (length && empty) {
|
|
186
|
+
const old = empty;
|
|
187
|
+
empty = undefined;
|
|
188
|
+
unlink(old);
|
|
189
|
+
old.stop();
|
|
190
|
+
}
|
|
191
|
+
if (!initial && (length > 0 || !empty)) {
|
|
192
|
+
let current = first,
|
|
193
|
+
prev: Entry<T> | null = null,
|
|
194
|
+
seen: Set<Entry<T>> | undefined;
|
|
195
|
+
let matched: Entry<T>[] = [],
|
|
196
|
+
stashed: Entry<T>[] = [];
|
|
197
|
+
for (let index = 0; index < length; index++) {
|
|
198
|
+
const id = key(items[index]!, index);
|
|
199
|
+
const prepared = preparedKeys[index];
|
|
200
|
+
if (id !== prepared && !(id !== id && prepared !== prepared))
|
|
201
|
+
throw new Error("Each key changed during reconciliation");
|
|
202
|
+
const row = entries.get(id);
|
|
203
|
+
if (!row) throw new Error("Each key changed during reconciliation");
|
|
204
|
+
if (row.fresh) {
|
|
205
|
+
row.fresh = false;
|
|
206
|
+
if (row === current) {
|
|
207
|
+
moveInsertionRange(row.node, insertionRangeEnd(outer));
|
|
208
|
+
} else {
|
|
209
|
+
const next = prev ? prev.next : current;
|
|
210
|
+
unlink(row);
|
|
211
|
+
link(row, prev, next);
|
|
212
|
+
moveInsertionRange(
|
|
213
|
+
row.node,
|
|
214
|
+
next?.node ?? insertionRangeEnd(outer),
|
|
215
|
+
);
|
|
216
|
+
prev = row;
|
|
217
|
+
matched = [];
|
|
218
|
+
stashed = [];
|
|
219
|
+
current = prev.next;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (row !== current) {
|
|
224
|
+
if (seen?.has(row)) {
|
|
225
|
+
if (matched.length < stashed.length) {
|
|
226
|
+
const start = stashed[0]!,
|
|
227
|
+
a = matched[0]!,
|
|
228
|
+
b = matched[matched.length - 1]!;
|
|
229
|
+
prev = start.prev;
|
|
230
|
+
for (const part of matched)
|
|
231
|
+
moveInsertionRange(part.node, start.node);
|
|
232
|
+
for (const part of stashed) seen.delete(part);
|
|
233
|
+
const beforeA = a.prev,
|
|
234
|
+
afterB = b.next;
|
|
235
|
+
if (beforeA) beforeA.next = afterB;
|
|
236
|
+
else first = afterB;
|
|
237
|
+
if (afterB) afterB.prev = beforeA;
|
|
238
|
+
else last = beforeA;
|
|
239
|
+
if (prev) prev.next = a;
|
|
240
|
+
else first = a;
|
|
241
|
+
a.prev = prev;
|
|
242
|
+
b.next = start;
|
|
243
|
+
start.prev = b;
|
|
244
|
+
current = start;
|
|
245
|
+
prev = b;
|
|
246
|
+
index--;
|
|
247
|
+
matched = [];
|
|
248
|
+
stashed = [];
|
|
249
|
+
} else {
|
|
250
|
+
seen.delete(row);
|
|
251
|
+
moveInsertionRange(
|
|
252
|
+
row.node,
|
|
253
|
+
current?.node ?? insertionRangeEnd(outer),
|
|
254
|
+
);
|
|
255
|
+
unlink(row);
|
|
256
|
+
const next = prev === null ? first : prev.next;
|
|
257
|
+
link(row, prev, next);
|
|
258
|
+
prev = row;
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
matched = [];
|
|
263
|
+
stashed = [];
|
|
264
|
+
while (current && current !== row) {
|
|
265
|
+
(seen ??= new Set()).add(current);
|
|
266
|
+
stashed.push(current);
|
|
267
|
+
current = current.next;
|
|
268
|
+
}
|
|
269
|
+
if (!current) continue;
|
|
270
|
+
}
|
|
271
|
+
if (!row.fresh) matched.push(row);
|
|
272
|
+
prev = row;
|
|
273
|
+
current = row.next;
|
|
274
|
+
}
|
|
275
|
+
const removed = [...(seen ?? [])];
|
|
276
|
+
while (current) {
|
|
277
|
+
removed.push(current);
|
|
278
|
+
current = current.next;
|
|
279
|
+
}
|
|
280
|
+
destroy(removed);
|
|
281
|
+
}
|
|
282
|
+
initial = false;
|
|
283
|
+
for (const row of entries.values()) row.fresh = false;
|
|
284
|
+
if (!length && fallback && !empty)
|
|
285
|
+
empty = create(undefined as T, 0, Symbol("fallback"), () => fallback());
|
|
286
|
+
const tokens: Node[] = [];
|
|
287
|
+
for (let row = first; row; row = row.next) tokens.push(row.node);
|
|
288
|
+
// Preserve current source order for child-first owner teardown.
|
|
289
|
+
group.children.clear();
|
|
290
|
+
for (let row = first; row; row = row.next) group.children.add(row.scope);
|
|
291
|
+
scheduler?.reorder();
|
|
292
|
+
return tokens;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
try {
|
|
295
|
+
destroy(created.filter((row) => entries.get(row.key) === row));
|
|
296
|
+
} catch {}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
outer = createInsertionRange(
|
|
301
|
+
reconcile,
|
|
302
|
+
scheduler ? createRenderMemo : undefined,
|
|
303
|
+
retain,
|
|
304
|
+
);
|
|
305
|
+
return outer;
|
|
306
|
+
}
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { isSSRRendering } from "@tschk/moonshine-dom/ssr-owner";
|
|
2
|
+
/** Post-DOM native rune effects; opt-in pre/render phases live on /pre. */
|
|
3
|
+
import {
|
|
4
|
+
collectDeps,
|
|
5
|
+
subscribeInvalidation,
|
|
6
|
+
type Trackable,
|
|
7
|
+
} from "@tschk/moonshine/tracking";
|
|
8
|
+
import {
|
|
9
|
+
createRoot,
|
|
10
|
+
getRenderScheduler,
|
|
11
|
+
getOwner,
|
|
12
|
+
onCleanup,
|
|
13
|
+
runWithOwner,
|
|
14
|
+
untrack,
|
|
15
|
+
} from "@tschk/moonshine-dom/reactive";
|
|
16
|
+
import { peekComponentContext } from "@tschk/moonshine-dom/component";
|
|
17
|
+
import { flushComponentReferences } from "@tschk/moonshine-dom/component-instance";
|
|
18
|
+
|
|
19
|
+
import { holdHistory, releaseHistory, withHistoryCleanup } from "./history";
|
|
20
|
+
import { queueTickWork, removeTickWork } from "./tick-queue";
|
|
21
|
+
|
|
22
|
+
const pending = new Set<() => void>();
|
|
23
|
+
let scheduled = false,
|
|
24
|
+
epoch = 0;
|
|
25
|
+
function flush(): void {
|
|
26
|
+
removeTickWork(flush);
|
|
27
|
+
let failed = false,
|
|
28
|
+
failure: unknown;
|
|
29
|
+
const runs = new Map<() => void, number>();
|
|
30
|
+
try {
|
|
31
|
+
while (pending.size) {
|
|
32
|
+
try {
|
|
33
|
+
flushComponentReferences();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!failed) failure = error;
|
|
36
|
+
failed = true;
|
|
37
|
+
}
|
|
38
|
+
if (!pending.size) break;
|
|
39
|
+
const next = pending.values().next().value!;
|
|
40
|
+
pending.delete(next);
|
|
41
|
+
const count = (runs.get(next) ?? 0) + 1;
|
|
42
|
+
runs.set(next, count);
|
|
43
|
+
if (count > 1000) {
|
|
44
|
+
if (!failed)
|
|
45
|
+
failure = new Error("Native rune effect update limit exceeded");
|
|
46
|
+
failed = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
next();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!failed) failure = error;
|
|
53
|
+
failed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!failed) failure = error;
|
|
58
|
+
failed = true;
|
|
59
|
+
} finally {
|
|
60
|
+
scheduled = false;
|
|
61
|
+
releaseHistory();
|
|
62
|
+
}
|
|
63
|
+
if (failed) throw failure;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancel(): void {
|
|
67
|
+
pending.clear();
|
|
68
|
+
if (scheduled) flush();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Untracked, manually disposed effect scope; deliberately independent of its caller. */
|
|
72
|
+
export function createRuneRoot(fn: () => void | (() => void)): () => void {
|
|
73
|
+
if (isSSRRendering() || peekComponentContext()?.mode === "ssr")
|
|
74
|
+
return () => {};
|
|
75
|
+
return createRoot((stop) => {
|
|
76
|
+
const scope = getOwner();
|
|
77
|
+
const cleanup = fn();
|
|
78
|
+
if (typeof cleanup === "function") {
|
|
79
|
+
if (getOwner()?.disposed) withHistoryCleanup(() => untrack(cleanup));
|
|
80
|
+
else onCleanup(cleanup);
|
|
81
|
+
}
|
|
82
|
+
return () => runWithOwner(scope, () => withHistoryCleanup(stop));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Initial and invalidated callbacks run in a deduplicated microtask after DOM updates. */
|
|
87
|
+
export function createRuneEffect(fn: () => void | (() => void)): void {
|
|
88
|
+
const owner = getOwner();
|
|
89
|
+
if (!owner || owner.disposed)
|
|
90
|
+
throw new Error("Rune effects require a live native owner");
|
|
91
|
+
if (isSSRRendering() || peekComponentContext()?.mode === "ssr") return;
|
|
92
|
+
const scheduler = getRenderScheduler();
|
|
93
|
+
const effectPending = scheduler ? new Set<() => void>() : pending;
|
|
94
|
+
const scheduledRun = scheduler
|
|
95
|
+
? () => {
|
|
96
|
+
effectPending.delete(run);
|
|
97
|
+
run();
|
|
98
|
+
}
|
|
99
|
+
: run;
|
|
100
|
+
let disposed = false,
|
|
101
|
+
stopRun: (() => void) | undefined;
|
|
102
|
+
let subscriptions = new Map<Trackable, () => void>();
|
|
103
|
+
let lastVersions: Map<Trackable, number> | undefined;
|
|
104
|
+
let forced = false;
|
|
105
|
+
function enqueue(): void {
|
|
106
|
+
if (disposed || owner!.disposed) return;
|
|
107
|
+
effectPending.add(run);
|
|
108
|
+
if (scheduler) {
|
|
109
|
+
scheduler.post(scheduledRun);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!scheduled) {
|
|
113
|
+
scheduled = true;
|
|
114
|
+
holdHistory();
|
|
115
|
+
queueTickWork(flush, cancel);
|
|
116
|
+
const token = ++epoch;
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
if (scheduled && token === epoch) flush();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function run(): void {
|
|
123
|
+
if (disposed || owner!.disposed) return;
|
|
124
|
+
if (lastVersions && !forced) {
|
|
125
|
+
let changed = false;
|
|
126
|
+
runWithOwner(owner, () =>
|
|
127
|
+
untrack(() => {
|
|
128
|
+
for (const [dep, version] of lastVersions!) {
|
|
129
|
+
dep.validate?.();
|
|
130
|
+
if (dep.version !== version) changed = true;
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
if (!changed) return;
|
|
135
|
+
}
|
|
136
|
+
forced = false;
|
|
137
|
+
const previous = stopRun;
|
|
138
|
+
stopRun = undefined;
|
|
139
|
+
if (previous) untrack(previous);
|
|
140
|
+
const cleanupInvalidated = effectPending.has(run);
|
|
141
|
+
let failed = false,
|
|
142
|
+
failure: unknown;
|
|
143
|
+
const versions = new Map<Trackable, number>();
|
|
144
|
+
let dependencies: Trackable[] = [];
|
|
145
|
+
runWithOwner(owner, () =>
|
|
146
|
+
createRoot((stop) => {
|
|
147
|
+
const scope = getOwner();
|
|
148
|
+
stopRun = () => runWithOwner(scope, () => withHistoryCleanup(stop));
|
|
149
|
+
dependencies = collectDeps(
|
|
150
|
+
() => {
|
|
151
|
+
try {
|
|
152
|
+
const cleanup = fn();
|
|
153
|
+
if (typeof cleanup === "function") {
|
|
154
|
+
if (getOwner()?.disposed)
|
|
155
|
+
withHistoryCleanup(() => untrack(cleanup));
|
|
156
|
+
else onCleanup(cleanup);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
failed = true;
|
|
160
|
+
failure = error;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
(dependency) => versions.set(dependency, dependency.version),
|
|
164
|
+
);
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
if (disposed || owner!.disposed) {
|
|
168
|
+
const cleanup = stopRun as (() => void) | undefined;
|
|
169
|
+
stopRun = undefined;
|
|
170
|
+
untrack(() => cleanup?.());
|
|
171
|
+
if (failed) throw failure;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const next = new Map<Trackable, () => void>();
|
|
175
|
+
try {
|
|
176
|
+
untrack(() => {
|
|
177
|
+
for (const dependency of dependencies)
|
|
178
|
+
next.set(
|
|
179
|
+
dependency,
|
|
180
|
+
subscriptions.get(dependency) ??
|
|
181
|
+
subscribeInvalidation(dependency, {
|
|
182
|
+
invalidate() {},
|
|
183
|
+
notify: enqueue,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
for (const [dependency, unsubscribe] of subscriptions)
|
|
187
|
+
if (!next.has(dependency)) unsubscribe();
|
|
188
|
+
});
|
|
189
|
+
subscriptions = next;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
for (const [dependency, unsubscribe] of next)
|
|
192
|
+
if (!subscriptions.has(dependency)) unsubscribe();
|
|
193
|
+
if (!failed) failure = error;
|
|
194
|
+
failed = true;
|
|
195
|
+
}
|
|
196
|
+
// Cleanup writes retain their queued rerun even if setup drops that dependency.
|
|
197
|
+
// Setup-only old branch invalidations are discarded; current self-writes rerun.
|
|
198
|
+
effectPending.delete(run);
|
|
199
|
+
lastVersions = versions;
|
|
200
|
+
if (cleanupInvalidated) forced = true;
|
|
201
|
+
if (
|
|
202
|
+
cleanupInvalidated ||
|
|
203
|
+
dependencies.some(
|
|
204
|
+
(dependency) => dependency.version !== versions.get(dependency),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
enqueue();
|
|
208
|
+
if (failed) {
|
|
209
|
+
const cleanup = stopRun as (() => void) | undefined;
|
|
210
|
+
stopRun = undefined;
|
|
211
|
+
try {
|
|
212
|
+
cleanup?.();
|
|
213
|
+
} catch {
|
|
214
|
+
/* Preserve callback/subscription failure. */
|
|
215
|
+
}
|
|
216
|
+
throw failure;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
onCleanup(() => {
|
|
220
|
+
disposed = true;
|
|
221
|
+
effectPending.delete(run);
|
|
222
|
+
let failed = false,
|
|
223
|
+
failure: unknown;
|
|
224
|
+
for (const cleanup of [
|
|
225
|
+
...subscriptions.values(),
|
|
226
|
+
...(stopRun ? [stopRun] : []),
|
|
227
|
+
]) {
|
|
228
|
+
try {
|
|
229
|
+
untrack(cleanup);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!failed) failure = error;
|
|
232
|
+
failed = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
subscriptions.clear();
|
|
236
|
+
stopRun = undefined;
|
|
237
|
+
if (failed) throw failure;
|
|
238
|
+
});
|
|
239
|
+
enqueue();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Query synchronous tracking only; ownership alone does not imply tracking. */
|
|
243
|
+
export { isTracking as runeTracking } from "@tschk/moonshine/tracking";
|
package/src/framework.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { crepusRenderer } from "@tschk/moonshine-dom/framework-lazy";
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Svelte teardown reads use the first values preceding the current flush. */
|
|
2
|
+
import { getOwner, type Owner } from "@tschk/moonshine-dom/reactive";
|
|
3
|
+
export type HistoryRead = { target: object; key: PropertyKey };
|
|
4
|
+
let values = new Map<object, Map<PropertyKey, unknown>>();
|
|
5
|
+
let historical = new Map<object, unknown>();
|
|
6
|
+
let scheduledEpoch: number | undefined;
|
|
7
|
+
let epoch = 0,
|
|
8
|
+
holds = 0,
|
|
9
|
+
mutationDepth = 0;
|
|
10
|
+
let cleanupOwner: Owner | null | undefined;
|
|
11
|
+
const collectors: HistoryRead[][] = [];
|
|
12
|
+
function clear(): void {
|
|
13
|
+
values.clear();
|
|
14
|
+
historical.clear();
|
|
15
|
+
epoch++;
|
|
16
|
+
}
|
|
17
|
+
function schedule(): void {
|
|
18
|
+
if (holds || (!values.size && !historical.size) || scheduledEpoch === epoch)
|
|
19
|
+
return;
|
|
20
|
+
const expected = epoch;
|
|
21
|
+
scheduledEpoch = expected;
|
|
22
|
+
queueMicrotask(() => {
|
|
23
|
+
if (scheduledEpoch === expected) scheduledEpoch = undefined;
|
|
24
|
+
if (expected === epoch && !holds && !mutationDepth) clear();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function holdHistory(): void {
|
|
28
|
+
holds++;
|
|
29
|
+
}
|
|
30
|
+
export function releaseHistory(): void {
|
|
31
|
+
if (!holds) throw new Error("Unbalanced history release");
|
|
32
|
+
if (--holds === 0 && !mutationDepth) clear();
|
|
33
|
+
}
|
|
34
|
+
export function withHistoryMutation<T>(fn: () => T): T {
|
|
35
|
+
mutationDepth++;
|
|
36
|
+
try {
|
|
37
|
+
return fn();
|
|
38
|
+
} finally {
|
|
39
|
+
if (--mutationDepth === 0) schedule();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function isHistoryReading(): boolean {
|
|
43
|
+
return cleanupOwner !== undefined && cleanupOwner === getOwner();
|
|
44
|
+
}
|
|
45
|
+
export function withHistoryCleanup<T>(fn: () => T): T {
|
|
46
|
+
const previous = cleanupOwner;
|
|
47
|
+
cleanupOwner = getOwner();
|
|
48
|
+
try {
|
|
49
|
+
return fn();
|
|
50
|
+
} finally {
|
|
51
|
+
cleanupOwner = previous;
|
|
52
|
+
if (cleanupOwner === undefined && !mutationDepth) schedule();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function recordHistory(
|
|
56
|
+
target: object,
|
|
57
|
+
key: PropertyKey,
|
|
58
|
+
before: unknown,
|
|
59
|
+
after: unknown,
|
|
60
|
+
): void {
|
|
61
|
+
let entries = values.get(target);
|
|
62
|
+
if (!entries) values.set(target, (entries = new Map()));
|
|
63
|
+
if (isHistoryReading()) entries.set(key, after);
|
|
64
|
+
else if (!entries.has(key)) entries.set(key, before);
|
|
65
|
+
}
|
|
66
|
+
export function trackHistoryRead(target: object, key: PropertyKey): void {
|
|
67
|
+
for (const collector of collectors)
|
|
68
|
+
if (!collector.some((read) => read.target === target && read.key === key))
|
|
69
|
+
collector.push({ target, key });
|
|
70
|
+
}
|
|
71
|
+
export function readHistory<T>(
|
|
72
|
+
target: object,
|
|
73
|
+
key: PropertyKey,
|
|
74
|
+
live: () => T,
|
|
75
|
+
): T {
|
|
76
|
+
trackHistoryRead(target, key);
|
|
77
|
+
const entries = values.get(target);
|
|
78
|
+
return isHistoryReading() && entries?.has(key)
|
|
79
|
+
? (entries.get(key) as T)
|
|
80
|
+
: live();
|
|
81
|
+
}
|
|
82
|
+
export function collectHistoryReads<T>(fn: () => T): {
|
|
83
|
+
value: T;
|
|
84
|
+
reads: HistoryRead[];
|
|
85
|
+
} {
|
|
86
|
+
const reads: HistoryRead[] = [];
|
|
87
|
+
collectors.push(reads);
|
|
88
|
+
try {
|
|
89
|
+
return { value: fn(), reads };
|
|
90
|
+
} finally {
|
|
91
|
+
collectors.pop();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function hasHistory(reads: readonly HistoryRead[]): boolean {
|
|
95
|
+
return reads.some(
|
|
96
|
+
({ target, key }) => values.get(target)?.has(key) || historical.has(target),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
export function historicalValue<T>(token: object, compute: () => T): T {
|
|
100
|
+
if (historical.has(token)) return historical.get(token) as T;
|
|
101
|
+
const value = compute();
|
|
102
|
+
historical.set(token, value);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Independent store bridge; component-language parsing belongs in Crepus. */
|
|
2
|
+
import {
|
|
3
|
+
createSignal as msCreateSignal,
|
|
4
|
+
type Signal as MsSignal,
|
|
5
|
+
} from "@tschk/moonshine";
|
|
6
|
+
import { writable, type Writable } from "./store";
|
|
7
|
+
|
|
8
|
+
export type SvelteWritable<T> = Writable<T>;
|
|
9
|
+
|
|
10
|
+
/** Store subscribers share one kernel subscription while they are active. */
|
|
11
|
+
export function fromMoonshineSignal<T>(signal: MsSignal<T>): SvelteWritable<T> {
|
|
12
|
+
let mirrored = signal.peek();
|
|
13
|
+
const store = writable(mirrored, (set) => {
|
|
14
|
+
mirrored = signal.peek();
|
|
15
|
+
set(mirrored);
|
|
16
|
+
return signal.subscribe(() => {
|
|
17
|
+
const current = signal.peek();
|
|
18
|
+
if (!Object.is(mirrored, current)) {
|
|
19
|
+
mirrored = current;
|
|
20
|
+
set(current);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
function set(next: T): void {
|
|
25
|
+
const previous = signal.peek();
|
|
26
|
+
signal.set(() => next);
|
|
27
|
+
const current = signal.peek();
|
|
28
|
+
// Preserve store object notifications without duplicating deferred kernel writes.
|
|
29
|
+
if (Object.is(previous, current) || !Object.is(mirrored, current)) {
|
|
30
|
+
mirrored = current;
|
|
31
|
+
store.set(current);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
subscribe: store.subscribe,
|
|
36
|
+
set,
|
|
37
|
+
update: (fn) => set(fn(signal.peek())),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createBridgedWritable<T>(initial: T): SvelteWritable<T> {
|
|
42
|
+
return fromMoonshineSignal(msCreateSignal(initial));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { msCreateSignal as createMoonshineSignal };
|
|
46
|
+
export type { MsSignal as MoonshineSignal };
|
|
47
|
+
export {
|
|
48
|
+
createResource,
|
|
49
|
+
createSignal as createMsSignal,
|
|
50
|
+
createMemo,
|
|
51
|
+
batch,
|
|
52
|
+
untrack,
|
|
53
|
+
} from "@tschk/moonshine";
|
|
54
|
+
export type { Resource, ResourceStatus } from "@tschk/moonshine";
|
|
55
|
+
export { crepusRenderer } from "./framework";
|