@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/src/pre.ts ADDED
@@ -0,0 +1,671 @@
1
+ import { cleanupOrderKey } from "@tschk/moonshine-dom/reactive";
2
+ /** Opt-in owned pre/render/post phases; source lowering belongs to Crepus. */
3
+ import {
4
+ collectDeps,
5
+ subscribeInvalidation,
6
+ type Trackable,
7
+ } from "@tschk/moonshine/tracking";
8
+ import {
9
+ createRoot,
10
+ createComponentScope,
11
+ createSignal,
12
+ getOwner,
13
+ getRenderScheduler,
14
+ onCleanup,
15
+ onBeforeDispose,
16
+ queuePostCommit,
17
+ runWithOwner,
18
+ untrack,
19
+ type Owner,
20
+ } from "@tschk/moonshine-dom/reactive";
21
+ import {
22
+ renderSchedulerKey,
23
+ type RenderScheduler,
24
+ } from "@tschk/moonshine-dom/render-scheduler";
25
+ import {
26
+ peekComponentContext,
27
+ type ComponentContext,
28
+ } from "@tschk/moonshine-dom/component";
29
+ import { isSSRRendering } from "@tschk/moonshine-dom/ssr-owner";
30
+ import { flushComponentReferences } from "@tschk/moonshine-dom/component-instance";
31
+ import { holdHistory, releaseHistory, withHistoryCleanup } from "./history";
32
+ import { queueTickWork, removeTickWork } from "./tick-queue";
33
+ const runeConsumerKey = /* @__PURE__ */ Symbol("moonshine.svelte.consumer");
34
+ const runeNodeKey = /* @__PURE__ */ Symbol("moonshine.svelte.node");
35
+ type Node = {
36
+ source?: true;
37
+ componentCap?: Node;
38
+ referenceTail?: { owner: Owner; callbacks: Array<() => void> };
39
+ consumer?: ComponentContext;
40
+ owner: Owner;
41
+ parent?: Node;
42
+ order: number;
43
+ next: number;
44
+ orderRead?: () => number;
45
+ disposed: boolean;
46
+ publication?: true;
47
+ phase?: "pre" | "render" | "structural" | "post";
48
+ };
49
+ type Job = Node & { run: () => void };
50
+ function node(owner: Owner, parent?: Node, phase?: Node["phase"]): Node {
51
+ return {
52
+ owner,
53
+ parent,
54
+ phase,
55
+ order: parent ? parent.next++ : 0,
56
+ next: 0,
57
+ disposed: false,
58
+ };
59
+ }
60
+ const rootOrder = new Map<Node, number>();
61
+ const postNodes = new WeakMap<() => void, Node>();
62
+ function queueRoot(value: Node): void {
63
+ const root = rootOf(value);
64
+ if (!rootOrder.has(root)) rootOrder.set(root, rootOrder.size);
65
+ }
66
+ function isAncestor(parent: Node, child: Node): boolean {
67
+ for (let value: Node | undefined = child; value; value = value.parent)
68
+ if (value === parent) return true;
69
+ return false;
70
+ }
71
+ function comparePosts(leftFn: () => void, rightFn: () => void): number {
72
+ if (leftFn === rightFn) return 0;
73
+ const left = postNodes.get(leftFn)!,
74
+ right = postNodes.get(rightFn)!;
75
+ const a = left.parent!,
76
+ b = right.parent!;
77
+ return a === b
78
+ ? left.order - right.order
79
+ : isAncestor(a, b)
80
+ ? 1
81
+ : isAncestor(b, a)
82
+ ? -1
83
+ : compare(left, right);
84
+ }
85
+ let orderedPosts: Array<() => void> = [],
86
+ postCursor = 0,
87
+ postsDirty = true;
88
+ function firstPost(): () => void {
89
+ if (postsDirty) {
90
+ // oxlint-disable-next-line unicorn/no-array-sort -- sort a fresh snapshot without allocating a second array.
91
+ orderedPosts = [...posts].sort(comparePosts);
92
+ postCursor = 0;
93
+ postsDirty = false;
94
+ }
95
+ while (postCursor < orderedPosts.length) {
96
+ const next = orderedPosts[postCursor++]!;
97
+ if (posts.has(next)) return next;
98
+ }
99
+ throw new Error("Rune scheduler lost pending post work");
100
+ }
101
+ function rootOf(value: Node): Node {
102
+ while (value.parent) value = value.parent;
103
+ return value;
104
+ }
105
+
106
+ const pres = new Set<Job>(),
107
+ renders = new Set<Job>(),
108
+ posts = new Set<() => void>();
109
+ let scheduled = false,
110
+ flushing = false,
111
+ epoch = 0,
112
+ initializing = 0;
113
+ let current: Node | undefined;
114
+ function schedule() {
115
+ if (scheduled) return;
116
+ scheduled = true;
117
+ holdHistory();
118
+ const token = ++epoch;
119
+ queueTickWork(flushRuneWork, cancel);
120
+ queueMicrotask(() => {
121
+ if (token === epoch && scheduled) flush();
122
+ });
123
+ }
124
+ /** Internal await completion commit; this is not Svelte's general flushSync API. */
125
+ export function flushRuneWork(): void {
126
+ if (flushing || !scheduled || initializing || peekComponentContext()?.active)
127
+ return;
128
+ ++epoch;
129
+ flush();
130
+ }
131
+ function alive(job: Job): boolean {
132
+ for (let next: Node | undefined = job; next; next = next.parent)
133
+ if (next.disposed || next.owner.disposed) return false;
134
+ return true;
135
+ }
136
+ function compare(a: Node, b: Node): number {
137
+ if (a === b) return 0;
138
+ const left: Node[] = [],
139
+ right: Node[] = [];
140
+ for (let n: Node | undefined = a; n; n = n.parent) left.unshift(n);
141
+ for (let n: Node | undefined = b; n; n = n.parent) right.unshift(n);
142
+ if (left[0] !== right[0])
143
+ return rootOrder.get(left[0]!)! - rootOrder.get(right[0]!)!;
144
+ let i = 0;
145
+ while (i < left.length && i < right.length && left[i] === right[i]) i++;
146
+ if (i === left.length) return -1;
147
+ if (i === right.length) return 1;
148
+ const l = left[i]!,
149
+ r = right[i]!;
150
+ return (
151
+ (l.phase === "pre" ? 0 : 1) - (r.phase === "pre" ? 0 : 1) ||
152
+ untrack(() => (l.orderRead?.() ?? l.order) - (r.orderRead?.() ?? r.order))
153
+ );
154
+ }
155
+ let ordered: Job[] = [],
156
+ cursor = 0,
157
+ jobsDirty = true;
158
+ function first(): Job {
159
+ if (jobsDirty) {
160
+ // oxlint-disable-next-line unicorn/no-array-sort -- sort a fresh snapshot without allocating a second array.
161
+ ordered = [...pres, ...renders].sort(compare);
162
+ cursor = 0;
163
+ jobsDirty = false;
164
+ }
165
+ while (cursor < ordered.length) {
166
+ const next = ordered[cursor++]!;
167
+ if (pres.has(next) || renders.has(next)) return next;
168
+ }
169
+ throw new Error("Rune scheduler lost pending render work");
170
+ }
171
+ const completedPres = new Set<Job>();
172
+ const nextPres = new Set<Job>();
173
+ const nextRenders = new Set<Job>();
174
+ let preDepth = 0;
175
+ let inPost = false;
176
+ let inPublication = false;
177
+ let currentStructural: Job | undefined;
178
+ function flush() {
179
+ removeTickWork(flushRuneWork);
180
+ flushing = true;
181
+ let failure: unknown,
182
+ failed = false;
183
+ const counts = new Map<unknown, number>();
184
+ function attempt(key: unknown, fn: () => void) {
185
+ const count = (counts.get(key) ?? 0) + 1;
186
+ counts.set(key, count);
187
+ if (count > 1000) {
188
+ if (!failed) failure = Error("Rune scheduler update limit exceeded");
189
+ failed = true;
190
+ return;
191
+ }
192
+ try {
193
+ fn();
194
+ } catch (e) {
195
+ if (!failed) failure = e;
196
+ failed = true;
197
+ }
198
+ }
199
+ try {
200
+ while (
201
+ pres.size ||
202
+ renders.size ||
203
+ posts.size ||
204
+ nextPres.size ||
205
+ nextRenders.size
206
+ ) {
207
+ if (!pres.size && !renders.size && !posts.size) {
208
+ completedPres.clear();
209
+ for (const job of nextPres) pres.add(job);
210
+ for (const job of nextRenders) renders.add(job);
211
+ nextPres.clear();
212
+ nextRenders.clear();
213
+ jobsDirty = true;
214
+ }
215
+ if (pres.size || renders.size) {
216
+ const job = first();
217
+ pres.delete(job);
218
+ renders.delete(job);
219
+ if (alive(job)) attempt(job, job.run);
220
+ continue;
221
+ }
222
+ try {
223
+ flushComponentReferences();
224
+ } catch (error) {
225
+ if (!failed) failure = error;
226
+ failed = true;
227
+ }
228
+ if (pres.size || renders.size) continue;
229
+ const post = posts.size ? firstPost() : undefined;
230
+ if (post) {
231
+ posts.delete(post);
232
+ inPost = true;
233
+ inPublication = !!postNodes.get(post)?.publication;
234
+ try {
235
+ attempt(post, post);
236
+ } finally {
237
+ inPost = false;
238
+ inPublication = false;
239
+ }
240
+ }
241
+ }
242
+ } finally {
243
+ scheduled = false;
244
+ rootOrder.clear();
245
+ completedPres.clear();
246
+ nextPres.clear();
247
+ nextRenders.clear();
248
+ ordered = [];
249
+ orderedPosts = [];
250
+ jobsDirty = true;
251
+ postsDirty = true;
252
+ flushing = false;
253
+ releaseHistory();
254
+ }
255
+ if (failed) throw failure;
256
+ }
257
+ function cancel(): void {
258
+ pres.clear();
259
+ renders.clear();
260
+ posts.clear();
261
+ nextPres.clear();
262
+ nextRenders.clear();
263
+ flushRuneWork();
264
+ }
265
+ function carrier(parent: Node | undefined): RenderScheduler & {
266
+ readonly [runeConsumerKey]: ComponentContext | undefined;
267
+ readonly [runeNodeKey]: Node | undefined;
268
+ } {
269
+ return {
270
+ get [runeNodeKey]() {
271
+ return parent;
272
+ },
273
+ get [runeConsumerKey]() {
274
+ for (let value = parent; value; value = value.parent)
275
+ if (value.consumer) return value.consumer;
276
+ return undefined;
277
+ },
278
+ reorder() {
279
+ jobsDirty = true;
280
+ postsDirty = true;
281
+ },
282
+ pre(fn) {
283
+ ownedJob(fn, "pre", parent);
284
+ },
285
+ effect: (fn, initial) => {
286
+ let previous = initial;
287
+ ownedJob(
288
+ () => {
289
+ previous = fn(previous);
290
+ },
291
+ "render",
292
+ parent,
293
+ );
294
+ },
295
+ memo: ((fn: any, initial: any, options: any) => {
296
+ const [value, setValue] = createSignal(initial, options);
297
+ ownedJob(() => setValue(() => fn(untrack(value))), "structural", parent);
298
+ return value;
299
+ }) as RenderScheduler["memo"],
300
+ post(fn, referencePublication) {
301
+ let token = postNodes.get(fn);
302
+ if (!token) {
303
+ token = node(parent!.owner, parent, "post");
304
+ postNodes.set(fn, token);
305
+ }
306
+ token.publication = referencePublication;
307
+ queueRoot(token);
308
+ if (!posts.has(fn)) postsDirty = true;
309
+ posts.add(fn);
310
+ if (!flushing) schedule();
311
+ },
312
+ component(fn) {
313
+ return setupGroup(fn, parent);
314
+ },
315
+ branch(fn, order) {
316
+ return setupGroup(fn, parent, order);
317
+ },
318
+ };
319
+ }
320
+ function setupGroup<T>(
321
+ fn: () => T,
322
+ parent: Node | undefined,
323
+ order?: () => number,
324
+ ): T {
325
+ const owner = getOwner()!;
326
+ const group = node(owner, parent);
327
+ group.orderRead = order;
328
+ onCleanup(() => {
329
+ group.disposed = true;
330
+ });
331
+ owner.context = new Map(owner.context);
332
+ owner.context.set(renderSchedulerKey, carrier(group));
333
+ const previous = current;
334
+ current = group;
335
+ try {
336
+ return fn();
337
+ } finally {
338
+ current = previous;
339
+ }
340
+ }
341
+ function ownedJob(
342
+ fn: () => unknown,
343
+ phase: "pre" | "render" | "structural",
344
+ parent: Node | undefined,
345
+ ): void {
346
+ const owner = getOwner();
347
+ if (!owner || owner.disposed)
348
+ throw Error("Scheduled work requires a live owner");
349
+ const queue = phase === "pre" ? pres : renders;
350
+ let stopRun: (() => void) | undefined;
351
+ let deps = new Map<Trackable, () => void>();
352
+ let lastVersions: Map<Trackable, number> | undefined;
353
+ let cleanupRunning = false,
354
+ forced = false;
355
+ const job: Job = { ...node(owner, parent, phase), run: runWithPhase };
356
+ function enqueue() {
357
+ if (!alive(job)) return;
358
+ if (cleanupRunning) forced = true;
359
+ if (nextPres.has(job) || nextRenders.has(job)) return;
360
+ // Reference publication starts a later pass; ordinary post cleanup still renders immediately.
361
+ if (
362
+ flushing &&
363
+ ((inPublication && phase !== "structural") ||
364
+ (phase === "pre" && !preDepth && !inPost && completedPres.has(job)) ||
365
+ (phase === "structural" &&
366
+ currentStructural &&
367
+ compare(job, currentStructural) < 0))
368
+ ) {
369
+ queue.delete(job);
370
+ jobsDirty = true;
371
+ (phase === "pre" ? nextPres : nextRenders).add(job);
372
+ queueRoot(job);
373
+ schedule();
374
+ return;
375
+ }
376
+ if (!queue.has(job)) jobsDirty = true;
377
+ queue.add(job);
378
+ queueRoot(job);
379
+ schedule();
380
+ }
381
+ function runWithPhase() {
382
+ if (!alive(job)) return;
383
+ const previousStructural = currentStructural;
384
+ if (phase === "structural") currentStructural = job;
385
+ if (phase === "pre") {
386
+ if (flushing) completedPres.add(job);
387
+ preDepth++;
388
+ }
389
+ try {
390
+ run();
391
+ } finally {
392
+ if (phase === "pre") preDepth--;
393
+ currentStructural = previousStructural;
394
+ }
395
+ }
396
+ function run() {
397
+ if (!alive(job)) return;
398
+ if (lastVersions && !forced) {
399
+ let changed = false,
400
+ completed = false;
401
+ runWithOwner(owner, () =>
402
+ createRoot((stop) => {
403
+ try {
404
+ createComponentScope(() => {
405
+ untrack(() => {
406
+ for (const [dep, version] of lastVersions!) {
407
+ dep.validate?.();
408
+ if (dep.version !== version) changed = true;
409
+ }
410
+ });
411
+ completed = true;
412
+ });
413
+ } finally {
414
+ stop();
415
+ }
416
+ }),
417
+ );
418
+ if (!completed) {
419
+ shutdown();
420
+ return;
421
+ }
422
+ if (!changed) return;
423
+ }
424
+ forced = false;
425
+ const old = stopRun;
426
+ stopRun = undefined;
427
+ cleanupRunning = true;
428
+ try {
429
+ if (old) untrack(old);
430
+ } finally {
431
+ cleanupRunning = false;
432
+ }
433
+ if (!alive(job)) return;
434
+ let failed = false,
435
+ failure: unknown;
436
+ const versions = new Map<Trackable, number>();
437
+ let found: Trackable[] = [];
438
+ runWithOwner(owner, () =>
439
+ createRoot((stop) => {
440
+ const scope = getOwner()!;
441
+ stopRun = () => runWithOwner(scope, () => withHistoryCleanup(stop));
442
+ scope.context = new Map(scope.context);
443
+ scope.context.set(renderSchedulerKey, carrier(job));
444
+ const previous = current;
445
+ current = job;
446
+ try {
447
+ createComponentScope(() => {
448
+ found = collectDeps(
449
+ () => {
450
+ try {
451
+ const cleanup = fn();
452
+ if (phase === "pre" && typeof cleanup === "function")
453
+ onCleanup(cleanup as () => void);
454
+ } catch (e) {
455
+ failed = true;
456
+ failure = e;
457
+ }
458
+ },
459
+ (dep) => versions.set(dep, dep.version),
460
+ );
461
+ if (failed) throw failure;
462
+ });
463
+ } finally {
464
+ current = previous;
465
+ }
466
+ // Render descendants tear down before their component; pre jobs retain
467
+ // subscription-first shutdown, including its first-error precedence.
468
+ if (phase !== "pre" && !failed && alive(job)) {
469
+ scope.parent = job.owner;
470
+ job.owner.children.add(scope);
471
+ }
472
+ }),
473
+ );
474
+ // A returned failed setup was routed by the native component scope.
475
+ // Never subscribe or revive that failed branch while its boundary commits.
476
+ if (failed || !alive(job)) {
477
+ shutdown();
478
+ return;
479
+ }
480
+ const next = new Map<Trackable, () => void>();
481
+ try {
482
+ untrack(() => {
483
+ for (const dep of found)
484
+ next.set(
485
+ dep,
486
+ deps.get(dep) ??
487
+ subscribeInvalidation(dep, { invalidate() {}, notify: enqueue }),
488
+ );
489
+ });
490
+ } catch (error) {
491
+ try {
492
+ shutdown([...next.values()]);
493
+ } catch {
494
+ /* Preserve subscription failure. */
495
+ }
496
+ throw error;
497
+ }
498
+ if (!alive(job)) {
499
+ shutdown([...next.values()]);
500
+ return;
501
+ }
502
+ const previous = deps;
503
+ deps = next;
504
+ let unsubscribeFailed = false,
505
+ unsubscribeFailure: unknown;
506
+ for (const [dep, stop] of previous)
507
+ if (!next.has(dep)) {
508
+ try {
509
+ untrack(stop);
510
+ } catch (error) {
511
+ if (!unsubscribeFailed) unsubscribeFailure = error;
512
+ unsubscribeFailed = true;
513
+ }
514
+ }
515
+ if (unsubscribeFailed) {
516
+ try {
517
+ shutdown();
518
+ } catch {
519
+ /* Preserve unsubscribe failure. */
520
+ }
521
+ throw unsubscribeFailure;
522
+ }
523
+ lastVersions = versions;
524
+ if (found.some((dep) => dep.version !== versions.get(dep))) enqueue();
525
+ }
526
+ function shutdown(extra: Iterable<() => void> = []) {
527
+ job.disposed = true;
528
+ queue.delete(job);
529
+ nextPres.delete(job);
530
+ nextRenders.delete(job);
531
+ const callbacks = new Set([
532
+ ...deps.values(),
533
+ ...extra,
534
+ ...(stopRun ? [stopRun] : []),
535
+ ]);
536
+ deps.clear();
537
+ stopRun = undefined;
538
+ let failed = false,
539
+ failure: unknown;
540
+ for (const stop of callbacks) {
541
+ try {
542
+ untrack(stop);
543
+ } catch (error) {
544
+ if (!failed) failure = error;
545
+ failed = true;
546
+ }
547
+ }
548
+ if (failed) throw failure;
549
+ }
550
+ onCleanup(shutdown);
551
+ runWithPhase();
552
+ }
553
+ /** Internal const evaluation provenance; synthetic scheduler groups inherit it. */
554
+ export function getRuneConsumer(): ComponentContext | undefined {
555
+ return (
556
+ getRenderScheduler() as
557
+ | (RenderScheduler & { readonly [runeConsumerKey]?: ComponentContext })
558
+ | undefined
559
+ )?.[runeConsumerKey];
560
+ }
561
+ function runeNode(): Node | undefined {
562
+ return (
563
+ getRenderScheduler() as
564
+ (RenderScheduler & { readonly [runeNodeKey]?: Node }) | undefined
565
+ )?.[runeNodeKey];
566
+ }
567
+ function sourceNode(value: Node | undefined): Node | undefined {
568
+ for (; value; value = value.parent) if (value.source) return value;
569
+ }
570
+ /** Compiler-only: called inside an actual source control callback, not synthetic groups. */
571
+ export function markRuneSourceEffect(): void {
572
+ const value = runeNode();
573
+ if (
574
+ !value ||
575
+ value.owner !== getOwner() ||
576
+ value.owner.disposed ||
577
+ value.owner.disposing
578
+ )
579
+ throw Error("Source effects require a live current Rune group");
580
+ value.source = true;
581
+ }
582
+ /** Internal intrinsic-reference cleanup policy; captures provenance before its synthetic branch. */
583
+ export function captureRuneReferenceCleanup(): (clear: () => void) => void {
584
+ const value = runeNode();
585
+ const source = sourceNode(value);
586
+ let cap: Node | undefined;
587
+ for (let n = value; n; n = n.parent)
588
+ if (n.componentCap) {
589
+ cap = n.componentCap;
590
+ break;
591
+ }
592
+ if (!source || !cap || !isAncestor(cap, source))
593
+ throw Error("Element references require valid Rune source provenance");
594
+ const path: Node[] = [];
595
+ for (let n: Node | undefined = source; n; n = n.parent) {
596
+ if (n.source) path.push(n);
597
+ if (n === cap) break;
598
+ }
599
+ for (const n of path) {
600
+ if (n.referenceTail) continue;
601
+ const owner =
602
+ n.componentCap === n
603
+ ? (n.consumer?.owner ?? n.owner.parent ?? n.owner)
604
+ : n.owner;
605
+ if (owner.disposed || owner.disposing)
606
+ throw Error("Reference tails require live source owners");
607
+ const callbacks: Array<() => void> = [];
608
+ n.referenceTail = { owner, callbacks };
609
+ runWithOwner(owner, () =>
610
+ onBeforeDispose(() =>
611
+ onCleanup(() => {
612
+ let failed = false,
613
+ failure: unknown;
614
+ while (callbacks.length) {
615
+ try {
616
+ untrack(callbacks.pop()!);
617
+ } catch (error) {
618
+ if (!failed) failure = error;
619
+ failed = true;
620
+ }
621
+ }
622
+ if (failed) throw failure;
623
+ }),
624
+ ),
625
+ );
626
+ }
627
+ return (clear) => {
628
+ let selected: Node | undefined;
629
+ for (const n of path) if (n.referenceTail!.owner.disposing) selected = n;
630
+ if (selected) selected.referenceTail!.callbacks.push(clear);
631
+ else queuePostCommit(clear);
632
+ };
633
+ }
634
+ export function withRuneScheduler<T>(fn: () => T): T {
635
+ if (isSSRRendering() || peekComponentContext()?.mode === "ssr") return fn();
636
+ const parent = getOwner();
637
+ if (!parent || parent.disposed)
638
+ throw new Error("Rune scheduling requires a live native owner");
639
+ const consumer = peekComponentContext();
640
+ const inherited = runeNode();
641
+ const inheritedSource = sourceNode(inherited);
642
+ return createComponentScope(() => {
643
+ const owner = getOwner()!;
644
+ const group = node(owner, inherited ?? current);
645
+ if (!inheritedSource) group.source = true;
646
+ group.componentCap = inheritedSource ?? group;
647
+ if (consumer?.active) group.consumer = consumer;
648
+ onCleanup(() => {
649
+ group.disposed = true;
650
+ group.consumer = undefined;
651
+ });
652
+ owner.context = new Map(owner.context);
653
+ owner.context.delete(cleanupOrderKey);
654
+ owner.context.set(renderSchedulerKey, carrier(group));
655
+ const previous = current;
656
+ current = group;
657
+ initializing++;
658
+ try {
659
+ return fn();
660
+ } finally {
661
+ initializing--;
662
+ current = previous;
663
+ }
664
+ });
665
+ }
666
+ export function createRunePreEffect(fn: () => void | (() => void)): void {
667
+ if (isSSRRendering() || peekComponentContext()?.mode === "ssr") return;
668
+ const scheduler = getRenderScheduler();
669
+ if (!scheduler) throw Error("Pre effects require withRuneScheduler");
670
+ scheduler.pre(fn);
671
+ }
@@ -0,0 +1,54 @@
1
+ /** Optional private-field cells; value is historical, peek is live and untracked. */
2
+ import { createSignal, untrack } from "@tschk/moonshine-dom/reactive";
3
+ import { readHistory, recordHistory, withHistoryMutation } from "./history";
4
+ import { graph } from "./state";
5
+
6
+ export function createPrivateState<T>(initial: T): {
7
+ value: T;
8
+ assign(next: T): T;
9
+ peek(): T;
10
+ } {
11
+ const wrap = graph();
12
+ const [read, write] = createSignal(wrap(initial));
13
+ function assign(next: T): T {
14
+ return withHistoryMutation(() => {
15
+ const previous = untrack(read),
16
+ resolved = wrap(next);
17
+ if (previous !== resolved)
18
+ recordHistory(read, "value", previous, resolved);
19
+ write(() => resolved);
20
+ return resolved;
21
+ });
22
+ }
23
+ return {
24
+ get value() {
25
+ return readHistory(read, "value", read);
26
+ },
27
+ set value(next: T) {
28
+ assign(next);
29
+ },
30
+ assign,
31
+ peek() {
32
+ return untrack(read);
33
+ },
34
+ };
35
+ }
36
+
37
+ export function createPrivateRawState<T>(initial: T): { value: T; peek(): T } {
38
+ const [read, write] = createSignal(initial);
39
+ return {
40
+ get value() {
41
+ return readHistory(read, "value", read);
42
+ },
43
+ set value(next: T) {
44
+ withHistoryMutation(() => {
45
+ const previous = untrack(read);
46
+ if (previous !== next) recordHistory(read, "value", previous, next);
47
+ write(() => next);
48
+ });
49
+ },
50
+ peek() {
51
+ return untrack(read);
52
+ },
53
+ };
54
+ }