gesso-framework 0.1.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.
@@ -0,0 +1,2648 @@
1
+ import { BehaviorSubject, Observable, Subject, Subscription, combineLatest, debounceTime, distinctUntilChanged, map, of, skip, tap, throttleTime } from "rxjs";
2
+ //#region src/Input.ts
3
+ /**
4
+ * Reactive cell holding a component input.
5
+ *
6
+ * Inputs must be cells because render() runs exactly once. A component
7
+ * that read a plain input value during render would capture it for the
8
+ * life of the instance, so a parent supplying new props under a
9
+ * dynamic subtree would update the field while the rendered tree kept
10
+ * showing the original value.
11
+ *
12
+ * The cell is written by the component host only: it accepts whatever
13
+ * the parent passed, subscribing it first when the parent passed an
14
+ * Observable. `value` is deliberately read-only, so `this.label.value =
15
+ * x` inside a component is a compile error rather than a silent
16
+ * violation of the one-way data flow.
17
+ */
18
+ var InputCell = class extends BehaviorSubject {
19
+ /**
20
+ * What to call this cell in a warning: `Component.prop`, or
21
+ * `channel.key`. Set by whoever creates it; a cell without one is
22
+ * reported as "a cell".
23
+ */
24
+ label;
25
+ /** The component whose body read `.value`, for the stale-read warning below. */
26
+ snapshotBy = null;
27
+ warnedStale = false;
28
+ /** Everything emitted through `emit`, created on the first `events` read. */
29
+ emitted = null;
30
+ constructor(initialValue) {
31
+ super(initialValue);
32
+ }
33
+ /**
34
+ * Fires the output this cell stands for.
35
+ *
36
+ * An output is a cell whose value is what the parent gave to be
37
+ * called: a function, or a target made by `into(subject)`. `emit`
38
+ * calls it with the arguments, and also pushes the first argument
39
+ * through `events`, so the component can fire from three places
40
+ * without passing the cell around and a parent that wants a stream
41
+ * can have one. A parent that passed nothing is fine: the call
42
+ * simply reaches nobody.
43
+ */
44
+ emit(...args) {
45
+ const handler = super.getValue();
46
+ if (typeof handler === "function") handler(...args);
47
+ this.emitted?.next(args[0]);
48
+ }
49
+ /** What `emit` has fired, as a stream: the first argument of each call. */
50
+ get events() {
51
+ if (this.emitted === null) this.emitted = new Subject();
52
+ return this.emitted.asObservable();
53
+ }
54
+ get value() {
55
+ trackRead(this);
56
+ if (bodyOf !== null && tracking === null && this.snapshotBy === null) this.snapshotBy = bodyOf;
57
+ return super.getValue();
58
+ }
59
+ /**
60
+ * The one place the run-once model goes quietly wrong is a body that
61
+ * reads `props.x.value`, uses the value to build the tree, and never
62
+ * hears that it changed. Nothing crashes; the screen is simply stale.
63
+ * So a cell remembers being read while a body ran, and if it later
64
+ * changes with nobody subscribed to it, it says so once. A cell that
65
+ * something is following is fine: the follower carries the change.
66
+ */
67
+ next(value) {
68
+ if (this.snapshotBy !== null && !this.warnedStale && !this.observed && !Object.is(value, super.getValue())) {
69
+ this.warnedStale = true;
70
+ warnStaleRead(this.snapshotBy, this.label, super.getValue(), value);
71
+ }
72
+ super.next(value);
73
+ }
74
+ };
75
+ /** The component whose function body is running, while one is. */
76
+ let bodyOf = null;
77
+ /** The component whose body is running, for a cell that wants to remember being read there. */
78
+ function currentBody() {
79
+ return bodyOf;
80
+ }
81
+ /** The set a running `computed` is collecting its reads into, while one is. */
82
+ let tracking = null;
83
+ /** Records a `.value` read for whatever `computed` is running, if one is. */
84
+ function trackRead(cell) {
85
+ tracking?.add(cell);
86
+ }
87
+ /** Runs `run` with every `.value` read on the way recorded into `into`. */
88
+ function withTracking(into, run) {
89
+ const previous = tracking;
90
+ tracking = into;
91
+ try {
92
+ return run();
93
+ } finally {
94
+ tracking = previous;
95
+ }
96
+ }
97
+ /**
98
+ * Runs a component's body with its name on record, so a `.value` read
99
+ * inside it can be told apart from one in an event handler later, which
100
+ * is the ordinary way to read the current value and warns about nothing.
101
+ */
102
+ function withBodyOf(tag, run) {
103
+ const previous = bodyOf;
104
+ bodyOf = tag;
105
+ try {
106
+ return run();
107
+ } finally {
108
+ bodyOf = previous;
109
+ }
110
+ }
111
+ function warnStaleRead(tag, label, from, to) {
112
+ const what = label === void 0 ? "a cell" : `\`${label}\``;
113
+ const change = describeChange(from, to);
114
+ console.warn(`Component '${tag}' read ${what} with .value while its body ran, and nothing is following that cell. It has since changed ${change}, and whatever was built from the first value still shows it. A component body runs once: bind the cell instead (pass it, or pipe it, into the prop it feeds), or give the component a key so a new value builds a new one.`);
115
+ }
116
+ /**
117
+ * "from X to Y", or for two plain objects the first field that differs,
118
+ * because two objects that print alike for sixty characters say nothing
119
+ * about what actually moved.
120
+ */
121
+ function describeChange(from, to) {
122
+ if (isPlainObject$2(from) && isPlainObject$2(to)) {
123
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(from), ...Object.keys(to)])) {
124
+ const before = from[key];
125
+ const after = to[key];
126
+ if (!Object.is(before, after) && safeJson(before) !== safeJson(after)) return `at .${key}, from ${describe(before)} to ${describe(after)}`;
127
+ }
128
+ return "to an equal-looking object";
129
+ }
130
+ return `from ${describe(from)} to ${describe(to)}`;
131
+ }
132
+ function isPlainObject$2(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function safeJson(value) {
136
+ try {
137
+ return JSON.stringify(value) ?? String(value);
138
+ } catch {
139
+ return String(value);
140
+ }
141
+ }
142
+ function describe(value) {
143
+ let text;
144
+ try {
145
+ text = typeof value === "function" ? "a function" : JSON.stringify(value) ?? String(value);
146
+ } catch {
147
+ text = String(value);
148
+ }
149
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
150
+ }
151
+ function input(first, fallback) {
152
+ if (arguments.length < 2 || !(first instanceof InputCell)) return new InputCell(first);
153
+ const source = first;
154
+ const withFallback = (value) => value === void 0 ? fallback : value;
155
+ const derived = new InputCell(withFallback(source.value));
156
+ derived.label = source.label;
157
+ source.subscribe({
158
+ next: (value) => derived.next(withFallback(value)),
159
+ complete: () => derived.complete()
160
+ });
161
+ return derived;
162
+ }
163
+ /**
164
+ * An output a class component declares as a field:
165
+ *
166
+ * @Output() changed = output<[value: number]>();
167
+ *
168
+ * The host wires the parent's handler into it exactly as it wires an
169
+ * input, and `this.changed.emit(next)` fires it.
170
+ */
171
+ function output() {
172
+ return new InputCell(void 0);
173
+ }
174
+ const OUTPUT_TARGET = Symbol("gesso:output-target");
175
+ function into(target) {
176
+ return { [OUTPUT_TARGET]: target };
177
+ }
178
+ function isOutputTarget(value) {
179
+ return typeof value === "object" && value !== null && OUTPUT_TARGET in value;
180
+ }
181
+ /** The receiver an `into()` target wraps. */
182
+ function outputTargetOf(target) {
183
+ return target[OUTPUT_TARGET];
184
+ }
185
+ //#endregion
186
+ //#region src/InternalState.ts
187
+ /**
188
+ * A component's own state: originated here, and never crossing the
189
+ * barrier.
190
+ *
191
+ * The writable counterpart to `InputCell`. The two are the same
192
+ * `BehaviorSubject` and differ by one accessor — this one has a
193
+ * `.value` setter — and that difference is the whole semantics:
194
+ *
195
+ * internalState() never crosses I write it
196
+ * input() crosses inward someone else writes it
197
+ *
198
+ * Named on that axis deliberately. It used to be `state()`, which
199
+ * described *what* a thing was while `input()` described *where it
200
+ * came from*; two names on two axes made neither of them tell you
201
+ * anything about the other. `internalState(products)` reads as a
202
+ * mistake at the call site in a way `state(products)` never did.
203
+ *
204
+ * For values that originate on this thread and die with the component:
205
+ * a tooltip's open flag, a caret, a scroll offset, the active tab.
206
+ * Anything that survives a reload, or that another screen cares about,
207
+ * is application state and belongs on a channel. Anything derived from
208
+ * other cells is a `computed`.
209
+ *
210
+ * It is not only for components. The thread that owns a channel's data
211
+ * writes cells too, and wrote them as a `BehaviorSubject` mirrored
212
+ * into an `asObservable()` because this was reachable only through the
213
+ * framework's main entry. `gesso-framework/worker` is the same cell
214
+ * with none of the renderer behind it, so an application worker holds
215
+ * one cell rather than a subject and a copy of it, and reads it with
216
+ * `.value` in a `computed` rather than listing it in a
217
+ * `combineLatest`. "Internal" still means what it says there: written
218
+ * here, and crossing the barrier only as the plain data a view key
219
+ * publishes.
220
+ */
221
+ var InternalState = class extends BehaviorSubject {
222
+ /** What to call this cell in a warning or the inspector; optional. */
223
+ label;
224
+ constructor(initialValue) {
225
+ super(initialValue);
226
+ }
227
+ get value() {
228
+ trackRead(this);
229
+ return super.getValue();
230
+ }
231
+ set value(next) {
232
+ this.next(next);
233
+ }
234
+ };
235
+ /**
236
+ * Creates a reactive state cell.
237
+ *
238
+ * Usage inside a component:
239
+ *
240
+ * private readonly count = state(0);
241
+ *
242
+ * increment() {
243
+ * this.count.value++;
244
+ * }
245
+ */
246
+ function internalState(initialValue, label) {
247
+ const state = new InternalState(initialValue);
248
+ if (label !== void 0) state.label = label;
249
+ return state;
250
+ }
251
+ //#endregion
252
+ //#region src/channel/structuralEquals.ts
253
+ /**
254
+ * Deep comparison for store read models.
255
+ *
256
+ * Projections and selectors allocate a fresh value on every
257
+ * evaluation, so reference equality reports a change on every
258
+ * unrelated state emission. That invalidates bindings and dirties
259
+ * nodes across the whole tree for state the view never read.
260
+ *
261
+ * Scope is deliberately narrow: primitives, arrays, and plain objects
262
+ * — what a projection is allowed to return. Anything else (class
263
+ * instances, Date, Map, Set, functions) compares by reference, which
264
+ * is conservative: it reports a change, so the UI updates when it did
265
+ * not need to rather than failing to update when it did.
266
+ *
267
+ * The same comparison becomes the equality half of the patch differ
268
+ * in Phase E, so a store behaves identically local or remote.
269
+ */
270
+ const MAX_DEPTH$1 = 100;
271
+ function structurallyEqual(a, b) {
272
+ return compare(a, b, 0);
273
+ }
274
+ function compare(a, b, depth) {
275
+ if (Object.is(a, b)) return true;
276
+ if (depth > MAX_DEPTH$1) return false;
277
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
278
+ const aIsArray = Array.isArray(a);
279
+ if (aIsArray !== Array.isArray(b)) return false;
280
+ if (aIsArray) {
281
+ const left = a;
282
+ const right = b;
283
+ if (left.length !== right.length) return false;
284
+ for (let i = 0; i < left.length; i++) if (!compare(left[i], right[i], depth + 1)) return false;
285
+ return true;
286
+ }
287
+ if (!isPlainObject$1(a) || !isPlainObject$1(b)) return false;
288
+ const left = a;
289
+ const right = b;
290
+ const leftKeys = Object.keys(left);
291
+ if (leftKeys.length !== Object.keys(right).length) return false;
292
+ for (const key of leftKeys) {
293
+ if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
294
+ if (!compare(left[key], right[key], depth + 1)) return false;
295
+ }
296
+ return true;
297
+ }
298
+ /**
299
+ * Objects created from an object literal or a null prototype. Class
300
+ * instances are excluded so they keep reference semantics.
301
+ */
302
+ function isPlainObject$1(value) {
303
+ const prototype = Object.getPrototypeOf(value);
304
+ return prototype === Object.prototype || prototype === null;
305
+ }
306
+ //#endregion
307
+ //#region src/derive.ts
308
+ /**
309
+ * One value from several, kept equal to `project` of the latest of each
310
+ * source, and emitted only when it changes.
311
+ *
312
+ * This is `combineLatest(...).pipe(map(...), distinctUntilChanged())`,
313
+ * which is what nearly every derived binding in a component body wants
314
+ * and what nearly every one had to write out. Sources are cells or any
315
+ * Observables; the result is what a prop takes.
316
+ *
317
+ * const playing = derive([queue.view.playlistId, audio.state], (id, state) =>
318
+ * id === card.id && state.status === 'playing'
319
+ * );
320
+ *
321
+ * @deprecated Write it as a `computed`, which is the one derivation the
322
+ * documentation teaches. A cell is read with
323
+ * `.value` and anything else through the `read` the function is handed,
324
+ * so the sources are the reads themselves and there is no list beside
325
+ * the expression to keep in step with it:
326
+ *
327
+ * const playing = computed(read =>
328
+ * queue.view.playlistId.value === card.id && read(audio.state).status === 'playing'
329
+ * );
330
+ *
331
+ * It still works and nothing that uses it needs changing today.
332
+ */
333
+ function derive(sources, project, options = {}) {
334
+ const equal = equalityOf(options.equal ?? "reference");
335
+ return combineLatest(sources).pipe(map((values) => project(...values)), distinctUntilChanged(equal));
336
+ }
337
+ /** The comparison an `Equality` names. Shared with `computed`. */
338
+ function equalityOf(equal) {
339
+ if (equal === "reference") return Object.is;
340
+ if (equal === "structural") return (a, b) => structurallyEqual(a, b);
341
+ return equal;
342
+ }
343
+ //#endregion
344
+ //#region src/computed.ts
345
+ /**
346
+ * A cell whose value is a function of other cells.
347
+ *
348
+ * `computed(() => quantity.value * price.value)` reads like the value it
349
+ * is. The cells its function reads through `.value` are its sources,
350
+ * found by running the function and watching what it touches, so there
351
+ * is no list to keep in step with the expression; a read the function
352
+ * did not make this time is a source it no longer has.
353
+ *
354
+ * A stream that is not a cell is read through the `read` the function
355
+ * is handed: `computed(read => read(stream).status)` follows the stream
356
+ * as it follows a cell. That is what makes this the only derivation an
357
+ * application needs, whether or not the thing it derives from happens
358
+ * to have a current value of its own.
359
+ *
360
+ * It is a cell and nothing else. Bound to a prop it is an Observable
361
+ * like every other cell, so a component written with it and one written
362
+ * with `pipe` compose without translation. Read in a handler with
363
+ * `.value` it is the current result, computed on the spot if nothing is
364
+ * following it. RxJS is underneath and nothing here replaces it.
365
+ *
366
+ * Nothing runs until someone asks. With no subscriber the function runs
367
+ * only when `.value` is read; with one, the cell follows its sources and
368
+ * emits a result when it differs from the last by `equal`. When the last
369
+ * subscriber leaves it lets go of its sources, so a computed made in a
370
+ * component body dies with the component's bindings and needs no
371
+ * disposal of its own.
372
+ *
373
+ * Reading it with `.value` while a component body runs is the same
374
+ * snapshot an input read there is, and it warns the same way: once, if
375
+ * it later changes with nobody following.
376
+ */
377
+ var ComputedCell = class extends Observable {
378
+ compute;
379
+ label;
380
+ equal;
381
+ changes = new Subject();
382
+ sources = /* @__PURE__ */ new Set();
383
+ cached;
384
+ hasValue = false;
385
+ upstream = null;
386
+ subscribers = 0;
387
+ attaching = false;
388
+ snapshotBy = null;
389
+ warnedStale = false;
390
+ /** Follows the sources after a body read, only to notice the change the body will not. */
391
+ staleWatch = null;
392
+ constructor(compute, options = {}) {
393
+ super((subscriber) => {
394
+ this.subscribers++;
395
+ this.staleWatch?.unsubscribe();
396
+ this.staleWatch = null;
397
+ if (this.upstream === null) this.attach();
398
+ subscriber.next(this.cached);
399
+ const following = this.changes.subscribe(subscriber);
400
+ return () => {
401
+ following.unsubscribe();
402
+ this.subscribers--;
403
+ if (this.subscribers === 0) this.detach();
404
+ };
405
+ });
406
+ this.compute = compute;
407
+ this.equal = equalityOf(options.equal ?? "reference");
408
+ this.label = options.label;
409
+ }
410
+ /** The current result: kept by the sources while followed, computed now when not. */
411
+ get value() {
412
+ trackRead(this);
413
+ if (this.upstream === null) this.recompute();
414
+ const body = currentBody();
415
+ if (body !== null && this.snapshotBy === null) {
416
+ this.snapshotBy = body;
417
+ this.watchForStaleRead();
418
+ }
419
+ return this.cached;
420
+ }
421
+ /**
422
+ * A body read the value once; if a source now changes with nothing
423
+ * following this cell, the screen built from that read is stale and
424
+ * nobody would know. So the sources are watched for exactly that, and
425
+ * the watch ends with the warning or with a real subscriber arriving.
426
+ */
427
+ watchForStaleRead() {
428
+ this.staleWatch = new Subscription();
429
+ let settling = true;
430
+ for (const source of this.sources) this.staleWatch.add(source.subscribe(() => {
431
+ if (settling || this.observed) return;
432
+ if (this.recompute()) {
433
+ this.warnStale();
434
+ this.staleWatch?.unsubscribe();
435
+ this.staleWatch = null;
436
+ }
437
+ }));
438
+ settling = false;
439
+ }
440
+ warnStale() {
441
+ if (this.snapshotBy === null || this.warnedStale) return;
442
+ this.warnedStale = true;
443
+ console.warn(`Component '${this.snapshotBy}' read ${this.label === void 0 ? "a computed cell" : `\`${this.label}\``} with .value while its body ran, and nothing is following that cell. It has since changed, and whatever was built from the first value still shows it. A component body runs once: bind the cell instead.`);
444
+ }
445
+ /** Whether anything is following this cell; the stale-read warning's question. */
446
+ get observed() {
447
+ return this.subscribers > 0;
448
+ }
449
+ /** Runs the function, watching what it reads; true when the result changed. */
450
+ recompute() {
451
+ const touched = /* @__PURE__ */ new Set();
452
+ const next = withTracking(touched, () => this.compute(readSource));
453
+ this.sources = touched;
454
+ const changed = !this.hasValue || !this.equal(this.cached, next);
455
+ this.hasValue = true;
456
+ this.cached = next;
457
+ return changed;
458
+ }
459
+ /** Follows the current sources, re-running on any change to them. */
460
+ attach() {
461
+ this.attaching = true;
462
+ this.recompute();
463
+ this.upstream = new Subscription();
464
+ for (const source of this.sources) this.upstream.add(source.subscribe(() => this.onSourceChanged()));
465
+ this.attaching = false;
466
+ }
467
+ detach() {
468
+ this.upstream?.unsubscribe();
469
+ this.upstream = null;
470
+ }
471
+ onSourceChanged() {
472
+ if (this.attaching) return;
473
+ const before = new Set(this.sources);
474
+ const changed = this.recompute();
475
+ if (!sameSet(before, this.sources)) {
476
+ this.detach();
477
+ this.attaching = true;
478
+ this.upstream = new Subscription();
479
+ for (const source of this.sources) this.upstream.add(source.subscribe(() => this.onSourceChanged()));
480
+ this.attaching = false;
481
+ }
482
+ if (changed) this.changes.next(this.cached);
483
+ }
484
+ };
485
+ /**
486
+ * A cell computed from what its function reads. See `ComputedCell`.
487
+ *
488
+ * const total = computed(() => quantity.value * PRICE * RATES[currency.value]);
489
+ * <text text={computed(() => String(count.value))} />
490
+ *
491
+ * Cells are read with `.value`; anything else is read through the
492
+ * `read` the function is given, which follows it the same way:
493
+ *
494
+ * const late = computed(read => read(clock) > deadline);
495
+ */
496
+ function computed(compute, options = {}) {
497
+ return new ComputedCell(compute, options);
498
+ }
499
+ /**
500
+ * The cell standing for a stream, one per stream.
501
+ *
502
+ * Anything that already has a current value is its own cell, so a
503
+ * `read` of an input, an internal state or another computed costs a
504
+ * property access and nothing more. Everything else gets a `StreamCell`
505
+ * held against it here, so several computeds reading one stream share a
506
+ * single subscription to it rather than opening one each.
507
+ *
508
+ * Weak on purpose: the entry is reachable only while the stream is, so
509
+ * a stream made in a component body is collected with the component.
510
+ */
511
+ const streamCells = /* @__PURE__ */ new WeakMap();
512
+ /**
513
+ * Whether a source's `.value` announces itself to the running computed,
514
+ * decided once per source and remembered.
515
+ *
516
+ * Having a `value` is not enough. A framework cell records its reads
517
+ * through `trackRead`, which is what lets a computed learn what it
518
+ * depends on; a plain `BehaviorSubject` has a `value` too and records
519
+ * nothing, so a computed that trusted the property would read it once
520
+ * and never hear it change. That is exactly what happened to three
521
+ * data-layer specs that fed a raw subject where the application feeds
522
+ * a channel view. The probe reads `.value` once under a tracking set
523
+ * of its own and asks whether the source turned up in it.
524
+ */
525
+ const tracksReads = /* @__PURE__ */ new WeakMap();
526
+ function announcesItsReads(source) {
527
+ let known = tracksReads.get(source);
528
+ if (known === void 0) {
529
+ const seen = /* @__PURE__ */ new Set();
530
+ withTracking(seen, () => void source.value);
531
+ known = seen.has(source);
532
+ tracksReads.set(source, known);
533
+ }
534
+ return known;
535
+ }
536
+ function cellFor(source) {
537
+ if ("value" in source && announcesItsReads(source)) return source;
538
+ let cell = streamCells.get(source);
539
+ if (cell === void 0) {
540
+ cell = new StreamCell(source);
541
+ streamCells.set(source, cell);
542
+ }
543
+ return cell;
544
+ }
545
+ /** The `read` every computed's function is handed. */
546
+ const readSource = (source) => cellFor(source).value;
547
+ /**
548
+ * A plain stream, seen as a cell.
549
+ *
550
+ * It holds the last value it saw and hands it to whoever asks, which is
551
+ * the one thing a cell has and an Observable does not. While something
552
+ * follows it, it follows the stream; when the last follower leaves it
553
+ * lets go, so it costs nothing between uses and needs no disposal, on
554
+ * the same terms as `ComputedCell`.
555
+ *
556
+ * A `.value` read with nothing following takes one synchronous
557
+ * subscription and drops it again, which is how a `BehaviorSubject`
558
+ * behind an `asObservable()`, or a `combineLatest` over such subjects,
559
+ * answers with what it already holds. A stream that has nothing to say
560
+ * synchronously answers `undefined` until its first emission arrives,
561
+ * which is the honest answer: there is no value yet.
562
+ */
563
+ var StreamCell = class extends Observable {
564
+ stream;
565
+ last;
566
+ followers = 0;
567
+ upstream = null;
568
+ changes = new Subject();
569
+ constructor(stream) {
570
+ super((subscriber) => {
571
+ this.followers++;
572
+ if (this.upstream === null) this.attach();
573
+ subscriber.next(this.last);
574
+ const following = this.changes.subscribe(subscriber);
575
+ return () => {
576
+ following.unsubscribe();
577
+ this.followers--;
578
+ if (this.followers === 0) {
579
+ this.upstream?.unsubscribe();
580
+ this.upstream = null;
581
+ }
582
+ };
583
+ });
584
+ this.stream = stream;
585
+ }
586
+ get value() {
587
+ trackRead(this);
588
+ if (this.upstream === null) this.stream.subscribe((value) => {
589
+ this.last = value;
590
+ }).unsubscribe();
591
+ return this.last;
592
+ }
593
+ attach() {
594
+ this.upstream = this.stream.subscribe((value) => {
595
+ this.last = value;
596
+ this.changes.next(value);
597
+ });
598
+ }
599
+ };
600
+ function sameSet(a, b) {
601
+ if (a.size !== b.size) return false;
602
+ for (const item of a) if (!b.has(item)) return false;
603
+ return true;
604
+ }
605
+ //#endregion
606
+ //#region src/select.ts
607
+ function select(source, keyOrProject, options = {}) {
608
+ const project = typeof keyOrProject === "function" ? keyOrProject : (value) => value === null || value === void 0 ? void 0 : value[keyOrProject];
609
+ return computed((read) => project(read(source)), {
610
+ equal: options.equal ?? "structural",
611
+ ...options.label === void 0 ? {} : { label: options.label }
612
+ });
613
+ }
614
+ //#endregion
615
+ //#region src/resource.ts
616
+ /**
617
+ * A request, keyed, so a stale answer cannot win.
618
+ *
619
+ * The key says what to fetch, and every value the key source emits is
620
+ * a request. An answer is published only if its request is still the
621
+ * current one, which is the generation counter every screen that loads
622
+ * anything was writing by hand, and the reason opening a page, going
623
+ * back and opening another before the first answers does not end with
624
+ * the first answer on screen.
625
+ *
626
+ * private readonly ref = internalState<PageRef | null>(null);
627
+ * readonly page = resource(this.ref, ref => api.trackPage(ref));
628
+ *
629
+ * show(ref: PageRef | null): Promise<void> {
630
+ * this.ref.value = ref;
631
+ * return this.page.settled;
632
+ * }
633
+ *
634
+ * A `null` key is "nothing is being asked for": the status is `idle`,
635
+ * the value is `null`, and no fetch runs. That is what a screen showing
636
+ * nothing yet actually means, and it saves every caller a branch.
637
+ *
638
+ * Unlike `computed`, a resource is eager: it follows its key from the
639
+ * moment it is made, because a request is an effect and an effect that
640
+ * waits for a subscriber is a request that never happens. It takes one
641
+ * subscription to the key source for its whole life, however many
642
+ * requests run through it, and `dispose()` gives that back.
643
+ *
644
+ * It is a helper and not a data layer. Nothing in the framework
645
+ * requires one, a channel is reached exactly as it was, and an
646
+ * application that would rather write its own is writing against the
647
+ * same barrier this is written against.
648
+ */
649
+ var Resource = class {
650
+ fetch;
651
+ options;
652
+ /** The one cell everything else here is a projection of. */
653
+ cell;
654
+ /** Bumped per request; an answer from an older one is dropped. */
655
+ requests = 0;
656
+ asked = null;
657
+ settling = Promise.resolve();
658
+ following;
659
+ /** Status, value and error as one record: what a channel view key takes. */
660
+ state;
661
+ status;
662
+ /** What is loaded, or `null` while there is nothing to show. */
663
+ value;
664
+ /** Why the last request failed, as its message; `null` when it did not. */
665
+ error;
666
+ constructor(key, fetch, options = {}) {
667
+ this.fetch = fetch;
668
+ this.options = options;
669
+ this.cell = internalState({
670
+ status: "idle",
671
+ value: null,
672
+ error: null
673
+ }, options.label);
674
+ this.state = this.cell;
675
+ this.status = select(this.cell, "status", named(options.label, "status"));
676
+ this.value = select(this.cell, "value", {
677
+ ...named(options.label, "value"),
678
+ equal: "reference"
679
+ });
680
+ this.error = select(this.cell, "error", named(options.label, "error"));
681
+ this.following = key.subscribe((next) => this.request(next ?? null));
682
+ }
683
+ /** What is being asked for, for a caller that needs to guard on it. */
684
+ get requested() {
685
+ return this.asked;
686
+ }
687
+ /**
688
+ * The request in the air, as a promise that resolves when it settles.
689
+ *
690
+ * Already resolved when nothing is in flight, so a caller that sets
691
+ * the key and returns this reads as an ordinary async method. Each
692
+ * request keeps its own promise, so a dropped one still resolves for
693
+ * whoever is awaiting it; it simply changes nothing on the way.
694
+ */
695
+ get settled() {
696
+ return this.settling;
697
+ }
698
+ /**
699
+ * Asks again for the same key, keeping what is on screen.
700
+ *
701
+ * This is the retry button. It does not clear the value the way a
702
+ * new key does, because a person pressing retry is asking for the
703
+ * thing they can already see to be brought up to date, and blanking
704
+ * it first would be a worse answer than the stale one.
705
+ */
706
+ retry() {
707
+ if (this.asked === null) return this.settling;
708
+ const generation = ++this.requests;
709
+ const held = this.cell.value;
710
+ if (held.value === null && held.status !== "loading") this.write({
711
+ status: "loading",
712
+ value: null,
713
+ error: null
714
+ });
715
+ this.settling = this.run(this.asked, generation);
716
+ return this.settling;
717
+ }
718
+ /**
719
+ * Replaces what is loaded, for a change made here rather than
720
+ * fetched.
721
+ *
722
+ * A page of comments appended to the answer already held, an
723
+ * optimistic edit: the resource holds the value, so something has to
724
+ * be able to write it. It does not touch the request in flight, so a
725
+ * refresh that lands afterwards still wins, which is what it should
726
+ * do: it is the newer truth.
727
+ */
728
+ set(value) {
729
+ this.write({
730
+ status: "ready",
731
+ value,
732
+ error: null
733
+ });
734
+ }
735
+ /** Gives back the subscription to the key source. */
736
+ dispose() {
737
+ this.following.unsubscribe();
738
+ }
739
+ request(key) {
740
+ this.asked = key;
741
+ const generation = ++this.requests;
742
+ if (key === null) {
743
+ this.write({
744
+ status: "idle",
745
+ value: null,
746
+ error: null
747
+ });
748
+ this.settling = Promise.resolve();
749
+ return;
750
+ }
751
+ const held = this.options.peek?.(key) ?? null;
752
+ this.write({
753
+ status: held === null ? "loading" : "ready",
754
+ value: held,
755
+ error: null
756
+ });
757
+ this.settling = this.run(key, generation);
758
+ }
759
+ run(key, generation) {
760
+ return this.fetch(key).then((answer) => this.answered(generation, answer), (error) => this.refused(generation, error));
761
+ }
762
+ answered(generation, answer) {
763
+ if (generation !== this.requests) return;
764
+ if (answer === null) {
765
+ const held = this.cell.value.value;
766
+ this.write({
767
+ status: held === null ? "missing" : "ready",
768
+ value: held,
769
+ error: null
770
+ });
771
+ return;
772
+ }
773
+ this.write({
774
+ status: "ready",
775
+ value: answer,
776
+ error: null
777
+ });
778
+ }
779
+ refused(generation, error) {
780
+ if (generation !== this.requests) return;
781
+ const held = this.cell.value.value;
782
+ this.write({
783
+ status: held === null ? "failed" : "ready",
784
+ value: held,
785
+ error: messageOf(error)
786
+ });
787
+ }
788
+ write(next) {
789
+ this.cell.value = next;
790
+ }
791
+ };
792
+ /**
793
+ * A keyed request with a status, a value, an error and a retry.
794
+ *
795
+ * const page = resource(ref, key => api.page(key), { peek: key => store.get(key) });
796
+ * <Show when={computed(() => page.status.value === 'loading')}>{() => <Spinner />}</Show>
797
+ *
798
+ * See `Resource` for what each status means and when a stale value is
799
+ * kept. The key is an Observable so that setting a cell is what asks
800
+ * for something: `computed` and `internalState` both work, and so does
801
+ * a channel view key or a router match.
802
+ */
803
+ function resource(key, fetch, options = {}) {
804
+ return new Resource(key, fetch, options);
805
+ }
806
+ function named(base, part) {
807
+ return base === void 0 ? {} : { label: `${base}.${part}` };
808
+ }
809
+ function messageOf(error) {
810
+ return error instanceof Error ? error.message : String(error);
811
+ }
812
+ //#endregion
813
+ //#region src/mutate.ts
814
+ /**
815
+ * An optimistic change to a cell, with a rollback that does not fight
816
+ * the person.
817
+ *
818
+ * private readonly favourites = internalState<readonly string[]>([]);
819
+ * private readonly like = mutate(this.favourites, toggled, id => api.favourite(id));
820
+ *
821
+ * toggle(id: string): void {
822
+ * void this.like.run(id);
823
+ * }
824
+ *
825
+ * Three things happen and the order is the whole point. `apply` runs
826
+ * first and writes the cell, so the screen changes on the press rather
827
+ * than a round trip later. `commit` then does the real write. If it
828
+ * rejects, or resolves `false`, the cell goes back to what it held
829
+ * before.
830
+ *
831
+ * **The rollback is guarded.** It happens only while the cell still
832
+ * holds exactly what `apply` wrote. Without that, a slow rejection
833
+ * would fight a fast second press and the cell would end up saying the
834
+ * opposite of the last thing anyone did, which is the guard every
835
+ * optimistic screen writes by hand and half of them get wrong.
836
+ *
837
+ * The cell is named first, and not because it was first written
838
+ * `mutate(apply, commit)`: the cell is what makes the guard possible.
839
+ * A mutation handed only two functions can undo its own change but
840
+ * cannot tell whether undoing it is still the right thing to do.
841
+ *
842
+ * A helper and not a data layer: nothing in the framework requires
843
+ * one, and an application that would rather write the four lines out
844
+ * is writing the same four lines this does.
845
+ */
846
+ function mutate(cell, apply, commit, options = {}) {
847
+ const equal = equalityOf(options.equal ?? "structural");
848
+ const inFlight = internalState(0, options.label);
849
+ const revert = (before, applied) => {
850
+ if (!equal(cell.value, applied)) return;
851
+ cell.value = before;
852
+ };
853
+ return {
854
+ pending: inFlight,
855
+ async run(argument) {
856
+ const before = cell.value;
857
+ const applied = apply(before, argument);
858
+ cell.value = applied;
859
+ inFlight.value = inFlight.value + 1;
860
+ try {
861
+ if (await commit(argument, applied) === false) {
862
+ revert(before, applied);
863
+ return false;
864
+ }
865
+ return true;
866
+ } catch {
867
+ revert(before, applied);
868
+ return false;
869
+ } finally {
870
+ inFlight.value = Math.max(0, inFlight.value - 1);
871
+ }
872
+ }
873
+ };
874
+ }
875
+ //#endregion
876
+ //#region src/debounce.ts
877
+ /**
878
+ * A cell that lets its source through on a timer.
879
+ *
880
+ * The two operators below are the same machinery with a different
881
+ * gate, and both are cells rather than plain streams on purpose: a
882
+ * `computed` reads a cell with `.value` and follows it, so a debounced
883
+ * search term composes with everything else in the dialect instead of
884
+ * being the one value in a screen that has to be piped.
885
+ *
886
+ * It holds the last value the gate let through, which is what `.value`
887
+ * answers while something is following it. With nothing following
888
+ * there is no timer running to hold anything back, so `.value` reads
889
+ * the source directly, which is the honest answer rather than a value
890
+ * frozen at whatever moment the last follower left.
891
+ *
892
+ * A value equal to the one it already holds is not a change and is not
893
+ * emitted, the same rule `computed` follows. Without it the first pass
894
+ * of the gate after a subscription would repeat the value the
895
+ * subscriber had just been handed.
896
+ *
897
+ * One subscription upstream however many followers it has, given back
898
+ * when the last of them leaves, on the same terms as `ComputedCell`.
899
+ */
900
+ var TimedCell = class extends Observable {
901
+ stream;
902
+ gate;
903
+ current;
904
+ hasCurrent = false;
905
+ followers = 0;
906
+ seeding = false;
907
+ upstream = null;
908
+ changes = new Subject();
909
+ constructor(stream, gate) {
910
+ super((subscriber) => {
911
+ this.followers++;
912
+ if (this.upstream === null) this.attach();
913
+ subscriber.next(this.current);
914
+ const following = this.changes.subscribe(subscriber);
915
+ return () => {
916
+ following.unsubscribe();
917
+ this.followers--;
918
+ if (this.followers === 0) {
919
+ this.upstream?.unsubscribe();
920
+ this.upstream = null;
921
+ }
922
+ };
923
+ });
924
+ this.stream = stream;
925
+ this.gate = gate;
926
+ }
927
+ get value() {
928
+ trackRead(this);
929
+ if (this.upstream === null) this.stream.subscribe((value) => {
930
+ this.current = value;
931
+ this.hasCurrent = true;
932
+ }).unsubscribe();
933
+ return this.current;
934
+ }
935
+ attach() {
936
+ this.seeding = true;
937
+ this.upstream = this.stream.pipe(tap((value) => {
938
+ if (this.seeding && !this.hasCurrent) {
939
+ this.current = value;
940
+ this.hasCurrent = true;
941
+ }
942
+ }), this.gate).subscribe((value) => {
943
+ if (this.hasCurrent && Object.is(this.current, value)) return;
944
+ this.current = value;
945
+ this.hasCurrent = true;
946
+ this.changes.next(value);
947
+ });
948
+ this.seeding = false;
949
+ }
950
+ };
951
+ /**
952
+ * A cell that follows its source once it has stopped moving.
953
+ *
954
+ * const query = internalState('');
955
+ * const term = debounced(query, 200);
956
+ * const results = computed(() => index.search(term.value));
957
+ *
958
+ * Nothing is emitted while values keep arriving; `ms` after the last
959
+ * one, the last one is. This is what a search field wants and what
960
+ * every search field in the tree was reaching the router without: a
961
+ * keystroke is not a question, and a pause is.
962
+ *
963
+ * The value the source already held is there immediately, so a screen
964
+ * built from this draws on the first frame rather than `ms` later.
965
+ */
966
+ function debounced(source, ms) {
967
+ return new TimedCell(source, debounceTime(ms));
968
+ }
969
+ /**
970
+ * A cell that follows its source at most once every `ms`.
971
+ *
972
+ * const position = throttled(scrollOffset, 100);
973
+ *
974
+ * The first value goes straight through and the last of a burst
975
+ * follows at the end of the window, so a value that arrives while the
976
+ * window is open is late rather than lost. That pairing is what makes
977
+ * this usable for a position or a progress reading, where the
978
+ * beginning and the end of a movement both matter and the middle does
979
+ * not.
980
+ *
981
+ * Use this for something that is continuously true, and `debounced`
982
+ * for something a person has finished saying.
983
+ */
984
+ function throttled(source, ms) {
985
+ return new TimedCell(source, throttleTime(ms, void 0, {
986
+ leading: true,
987
+ trailing: true
988
+ }));
989
+ }
990
+ //#endregion
991
+ //#region src/channel/ChannelToken.ts
992
+ /**
993
+ * Declares a channel.
994
+ *
995
+ * The name identifies it across the thread boundary and must be
996
+ * stable; unlike a class name it survives minification, which is why
997
+ * it is written out rather than derived.
998
+ */
999
+ function channel(name, initial) {
1000
+ if (name.length === 0) throw new Error("A channel needs a name: it is how the two threads agree on which one this is.");
1001
+ return {
1002
+ name,
1003
+ initial
1004
+ };
1005
+ }
1006
+ /**
1007
+ * Declares a channel from one object.
1008
+ *
1009
+ * export const Catalog = defineChannel('catalog', {
1010
+ * view: {
1011
+ * products: [] as readonly ProductRow[],
1012
+ * status: 'loading' as ShelfStatus
1013
+ * },
1014
+ * commands: {} as {
1015
+ * addToCart(id: string, quantity: number): void;
1016
+ * move(from: number, to: number): void;
1017
+ * }
1018
+ * });
1019
+ *
1020
+ * export type CatalogView = ViewOf<typeof Catalog>;
1021
+ *
1022
+ * The same token `channel()` returns, declared once instead of three
1023
+ * times. `channel<View, Commands>(name, initial)` wrote the view as an
1024
+ * interface, then as an initial literal that had to agree with it, and
1025
+ * a key added to one and forgotten in the other was a type error in a
1026
+ * third file. Here the object is the type.
1027
+ *
1028
+ * A field whose initial value is narrower than the type it holds is
1029
+ * given the type it holds: `[]` is `never[]` and `'loading'` is
1030
+ * `string` unless it is said, which is what the `as` clauses above are
1031
+ * for. `ViewOf` and `CommandsOf` name the resulting types wherever the
1032
+ * application used to name its own interface.
1033
+ *
1034
+ * `channel()` is not deprecated and keeps working exactly as it did.
1035
+ * An application with interfaces it wants to keep, because they are
1036
+ * shared with something else or because the initial values are built
1037
+ * elsewhere, has nothing to migrate.
1038
+ */
1039
+ function defineChannel(name, spec) {
1040
+ return channel(name, spec.view);
1041
+ }
1042
+ /**
1043
+ * The keys a channel publishes.
1044
+ *
1045
+ * Structural in its parameter rather than generic over the token, so
1046
+ * it does not have to agree with any particular command type to read
1047
+ * what is only ever the initial value's shape.
1048
+ */
1049
+ function viewKeys(token) {
1050
+ return Object.keys(token.initial);
1051
+ }
1052
+ //#endregion
1053
+ //#region src/channel/ChannelProtocol.ts
1054
+ function isChannelClientMessage(value) {
1055
+ const type = value?.type;
1056
+ return type === "channel:sync" || type === "channel:command";
1057
+ }
1058
+ function isChannelHostMessage(value) {
1059
+ const type = value?.type;
1060
+ return type === "channel:patch" || type === "channel:error";
1061
+ }
1062
+ //#endregion
1063
+ //#region src/channel/StorePatch.ts
1064
+ /**
1065
+ * Describes how to turn `previous` into `current` for one projection.
1066
+ *
1067
+ * Returns an empty list when nothing changed, which is the common case
1068
+ * and the reason this exists: the point of a projection is that most
1069
+ * state changes do not alter it, and the ones that do usually alter a
1070
+ * small part.
1071
+ */
1072
+ function diffProjection(projection, previous, current) {
1073
+ const patches = [];
1074
+ diff(projection, [], previous, current, patches);
1075
+ return patches;
1076
+ }
1077
+ function diff(projection, path, previous, current, out) {
1078
+ if (structurallyEqual(previous, current)) return;
1079
+ if (Array.isArray(previous) && Array.isArray(current)) {
1080
+ diffArray(projection, path, previous, current, out);
1081
+ return;
1082
+ }
1083
+ if (isPlainObject(previous) && isPlainObject(current)) {
1084
+ for (const key of Object.keys(current)) if (Object.prototype.hasOwnProperty.call(previous, key)) diff(projection, [...path, key], previous[key], current[key], out);
1085
+ else out.push({
1086
+ op: "set",
1087
+ projection,
1088
+ path: [...path, key],
1089
+ value: current[key]
1090
+ });
1091
+ for (const key of Object.keys(previous)) if (!Object.prototype.hasOwnProperty.call(current, key)) out.push({
1092
+ op: "delete",
1093
+ projection,
1094
+ path: [...path, key]
1095
+ });
1096
+ return;
1097
+ }
1098
+ out.push({
1099
+ op: "set",
1100
+ projection,
1101
+ path,
1102
+ value: current
1103
+ });
1104
+ }
1105
+ /**
1106
+ * Diffs two arrays by trimming the common prefix and suffix.
1107
+ *
1108
+ * This is not a minimal edit script — a shuffle degrades to replacing
1109
+ * the middle wholesale. It is chosen because the operations lists
1110
+ * actually undergo (append, prepend, remove one, edit in place) all
1111
+ * reduce to a single small patch, and computing a true LCS on every
1112
+ * store change would cost more than it saves.
1113
+ */
1114
+ function diffArray(projection, path, previous, current, out) {
1115
+ let start = 0;
1116
+ while (start < previous.length && start < current.length && structurallyEqual(previous[start], current[start])) start++;
1117
+ let previousEnd = previous.length - 1;
1118
+ let currentEnd = current.length - 1;
1119
+ while (previousEnd >= start && currentEnd >= start && structurallyEqual(previous[previousEnd], current[currentEnd])) {
1120
+ previousEnd--;
1121
+ currentEnd--;
1122
+ }
1123
+ const previousCount = previousEnd - start + 1;
1124
+ const currentCount = currentEnd - start + 1;
1125
+ if (previousCount === 0 && currentCount === 0) return;
1126
+ if (previousCount === currentCount) {
1127
+ for (let offset = 0; offset < previousCount; offset++) {
1128
+ const index = start + offset;
1129
+ diff(projection, [...path, index], previous[index], current[index], out);
1130
+ }
1131
+ return;
1132
+ }
1133
+ out.push({
1134
+ op: "splice",
1135
+ projection,
1136
+ path,
1137
+ index: start,
1138
+ deleteCount: previousCount,
1139
+ items: current.slice(start, currentEnd + 1)
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Applies patches to a projection value, sharing structure with the
1144
+ * original everywhere the patch did not reach.
1145
+ *
1146
+ * Nothing is mutated: bindings hold onto emitted values, so a replica
1147
+ * that edited in place would change data a component already rendered.
1148
+ */
1149
+ function applyPatches(root, patches) {
1150
+ let next = root;
1151
+ for (const patch of patches) next = applyPatch(next, patch);
1152
+ return next;
1153
+ }
1154
+ function applyPatch(root, patch) {
1155
+ switch (patch.op) {
1156
+ case "set": return setIn(root, patch.path, 0, patch.value);
1157
+ case "delete":
1158
+ if (patch.path.length === 0) return;
1159
+ return deleteIn(root, patch.path, 0);
1160
+ case "splice": return updateIn(root, patch.path, 0, (node) => {
1161
+ const array = Array.isArray(node) ? node : [];
1162
+ return array.slice(0, patch.index).concat(patch.items, array.slice(patch.index + patch.deleteCount));
1163
+ });
1164
+ }
1165
+ }
1166
+ function setIn(node, path, index, value) {
1167
+ if (index === path.length) return value;
1168
+ const key = path[index];
1169
+ const copy = cloneContainer(node, key);
1170
+ setKey(copy, key, setIn(readKey(node, key), path, index + 1, value));
1171
+ return copy;
1172
+ }
1173
+ function deleteIn(node, path, index) {
1174
+ const key = path[index];
1175
+ const copy = cloneContainer(node, key);
1176
+ if (index === path.length - 1) {
1177
+ if (Array.isArray(copy)) copy.splice(Number(key), 1);
1178
+ else delete copy[String(key)];
1179
+ return copy;
1180
+ }
1181
+ setKey(copy, key, deleteIn(readKey(node, key), path, index + 1));
1182
+ return copy;
1183
+ }
1184
+ function updateIn(node, path, index, update) {
1185
+ if (index === path.length) return update(node);
1186
+ const key = path[index];
1187
+ const copy = cloneContainer(node, key);
1188
+ setKey(copy, key, updateIn(readKey(node, key), path, index + 1, update));
1189
+ return copy;
1190
+ }
1191
+ function cloneContainer(node, key) {
1192
+ if (Array.isArray(node)) return node.slice();
1193
+ if (isPlainObject(node)) return { ...node };
1194
+ return typeof key === "number" ? [] : {};
1195
+ }
1196
+ function readKey(node, key) {
1197
+ if (node === null || node === void 0) return;
1198
+ return node[key];
1199
+ }
1200
+ function setKey(container, key, value) {
1201
+ container[key] = value;
1202
+ }
1203
+ function isPlainObject(value) {
1204
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1205
+ const prototype = Object.getPrototypeOf(value);
1206
+ return prototype === Object.prototype || prototype === null;
1207
+ }
1208
+ //#endregion
1209
+ //#region src/channel/plainData.ts
1210
+ /**
1211
+ * Checks that a value can cross the barrier.
1212
+ *
1213
+ * `structurallyEqual` understands primitives, arrays and plain
1214
+ * objects, and falls back to reference equality for everything else —
1215
+ * which for a freshly built value reports "changed" every single time.
1216
+ * A view key holding a `Date`, a `Map` or a domain object therefore
1217
+ * re-emits on every unrelated update and dirties the subtree bound to
1218
+ * it, forever, while looking perfectly correct.
1219
+ *
1220
+ * That failure is invisible in a test and shows up as a vague slowness
1221
+ * much later, so `provide` checks each key's first emission and throws
1222
+ * naming the path. The view-model layer is where rich objects become
1223
+ * flat data; this is what makes that a rule rather than a convention.
1224
+ *
1225
+ * The check runs once per key, on the first value only. It is a
1226
+ * development guard against a design mistake, not a validator on the
1227
+ * hot path.
1228
+ */
1229
+ const MAX_DEPTH = 100;
1230
+ /**
1231
+ * Returns the path to the first value that cannot cross, or null when
1232
+ * the whole tree is plain data.
1233
+ */
1234
+ function findUnplainPath(value, path = []) {
1235
+ if (path.length > MAX_DEPTH) return format(path);
1236
+ if (value === null) return null;
1237
+ const type = typeof value;
1238
+ if (type === "string" || type === "number" || type === "boolean" || type === "undefined") return null;
1239
+ if (type === "function" || type === "symbol" || type === "bigint") return format(path);
1240
+ if (Array.isArray(value)) {
1241
+ for (let index = 0; index < value.length; index++) {
1242
+ const found = findUnplainPath(value[index], [...path, index]);
1243
+ if (found !== null) return found;
1244
+ }
1245
+ return null;
1246
+ }
1247
+ const prototype = Object.getPrototypeOf(value);
1248
+ if (prototype !== Object.prototype && prototype !== null) return format(path);
1249
+ for (const [key, member] of Object.entries(value)) {
1250
+ const found = findUnplainPath(member, [...path, key]);
1251
+ if (found !== null) return found;
1252
+ }
1253
+ return null;
1254
+ }
1255
+ /**
1256
+ * Throws when `value` cannot cross the barrier, naming the channel,
1257
+ * the key and the path within it.
1258
+ */
1259
+ function requirePlainData(channelName, key, value) {
1260
+ const path = findUnplainPath(value);
1261
+ if (path === null) return;
1262
+ const where = path === "" ? `'${key}'` : `'${key}'${path}`;
1263
+ throw new Error(`Channel '${channelName}' published ${where}, which is not plain data. Only primitives, arrays and plain objects cross the barrier: a Date, Map, Set, class instance or function compares by reference, so it would report a change on every update and rebuild the subtree bound to it. Flatten it in the view model.`);
1264
+ }
1265
+ function format(path) {
1266
+ return path.map((step) => typeof step === "number" ? `[${step}]` : `.${step}`).join("");
1267
+ }
1268
+ //#endregion
1269
+ //#region src/channel/provide.ts
1270
+ /**
1271
+ * Publishes a channel from the thread that owns its data.
1272
+ *
1273
+ * Each view key is subscribed, diffed against what the other side last
1274
+ * saw, and sent as patches. Whatever produced the observable — a bare
1275
+ * subject or a stack of layers — stays here; only plain data crosses.
1276
+ *
1277
+ * Keys are subscribed on the first sync request, so a channel nobody
1278
+ * is watching costs nothing.
1279
+ */
1280
+ function provide(token, source, port) {
1281
+ return new ProvidedChannel(token, source, port);
1282
+ }
1283
+ var ProvidedChannel = class {
1284
+ token;
1285
+ source;
1286
+ port;
1287
+ subscriptions = new Subscription();
1288
+ /**
1289
+ * What the other side is known to hold, seeded from the token's
1290
+ * initial value — which the replica also starts from, so an app
1291
+ * whose first emission equals the initial sends nothing at all.
1292
+ */
1293
+ previous = /* @__PURE__ */ new Map();
1294
+ checked = /* @__PURE__ */ new Set();
1295
+ synced = false;
1296
+ constructor(token, source, port) {
1297
+ this.token = token;
1298
+ this.source = source;
1299
+ this.port = port;
1300
+ for (const key of viewKeys(token)) this.previous.set(key, token.initial[key]);
1301
+ this.port.onmessage = (event) => this.receive(event.data);
1302
+ }
1303
+ receive(data) {
1304
+ if (!isChannelClientMessage(data)) return;
1305
+ try {
1306
+ if (data.type === "channel:sync") {
1307
+ this.sync();
1308
+ return;
1309
+ }
1310
+ this.runCommand(data.command, data.payload, data.rest);
1311
+ } catch (error) {
1312
+ this.post({
1313
+ type: "channel:error",
1314
+ message: error instanceof Error ? error.message : String(error),
1315
+ stack: error instanceof Error ? error.stack : void 0
1316
+ });
1317
+ }
1318
+ }
1319
+ runCommand(name, payload, rest) {
1320
+ const handler = this.source.commands?.[name];
1321
+ if (handler === void 0) {
1322
+ const names = Object.keys(this.source.commands ?? {}).sort().join(", ");
1323
+ throw new Error(`Channel '${this.token.name}' has no command '${name}'. Declared commands: ${names.length > 0 ? names : "(none)"}.`);
1324
+ }
1325
+ handler(payload, ...rest ?? []);
1326
+ }
1327
+ sync() {
1328
+ if (this.synced) {
1329
+ this.resend();
1330
+ return;
1331
+ }
1332
+ this.synced = true;
1333
+ for (const key of viewKeys(this.token)) {
1334
+ const observable = this.source.view[key];
1335
+ if (observable === void 0) {
1336
+ this.post({
1337
+ type: "channel:error",
1338
+ message: `Channel '${this.token.name}' declares view key '${key}' but nothing was provided for it.`
1339
+ });
1340
+ continue;
1341
+ }
1342
+ this.subscriptions.add(observable.subscribe({
1343
+ next: (value) => this.publish(key, value),
1344
+ error: (error) => this.post({
1345
+ type: "channel:error",
1346
+ message: `Channel '${this.token.name}' view key '${key}' errored: ${error instanceof Error ? error.message : String(error)}`,
1347
+ stack: error instanceof Error ? error.stack : void 0
1348
+ })
1349
+ }));
1350
+ }
1351
+ }
1352
+ publish(key, value) {
1353
+ if (!this.checked.has(key)) {
1354
+ this.checked.add(key);
1355
+ try {
1356
+ requirePlainData(this.token.name, key, value);
1357
+ } catch (error) {
1358
+ this.post({
1359
+ type: "channel:error",
1360
+ message: error instanceof Error ? error.message : String(error),
1361
+ stack: error instanceof Error ? error.stack : void 0
1362
+ });
1363
+ return;
1364
+ }
1365
+ }
1366
+ const patches = diffProjection(key, this.previous.get(key), value);
1367
+ this.previous.set(key, value);
1368
+ if (patches.length > 0) this.post({
1369
+ type: "channel:patch",
1370
+ patches
1371
+ });
1372
+ }
1373
+ /** Re-sends every key in full, for a client that reattached. */
1374
+ resend() {
1375
+ const patches = [];
1376
+ for (const [key, value] of this.previous) patches.push({
1377
+ op: "set",
1378
+ projection: key,
1379
+ path: [],
1380
+ value
1381
+ });
1382
+ if (patches.length > 0) this.post({
1383
+ type: "channel:patch",
1384
+ patches
1385
+ });
1386
+ }
1387
+ post(message) {
1388
+ this.port.postMessage(message);
1389
+ }
1390
+ dispose() {
1391
+ this.subscriptions.unsubscribe();
1392
+ this.port.onmessage = null;
1393
+ }
1394
+ };
1395
+ //#endregion
1396
+ //#region src/app/NodeReport.ts
1397
+ /**
1398
+ * A node as a path somebody can read: `App > TrackScreen > ActionRow >
1399
+ * Button "Like"`.
1400
+ *
1401
+ * Written for error messages rather than for the inspector, and the
1402
+ * difference decides the shape. A report is read beside the tree it
1403
+ * came from, so an id is a handle; an error is read in a console or an
1404
+ * overlay with no tree beside it, and `node-4821` there is a fact
1405
+ * about nothing. The owner chain is what a person recognises, because
1406
+ * it is the components they wrote.
1407
+ *
1408
+ * Owners arrive nearest-first, as `UiNodeReport.owners` computes them,
1409
+ * and read outermost-first, as a path does. The accessible name is the
1410
+ * leaf when there is one: two `Button`s in the same row are only told
1411
+ * apart by what they say.
1412
+ */
1413
+ function formatNodePath(owners, node) {
1414
+ const names = owners.map((owner) => owner.name).reverse();
1415
+ if (names.length === 0) return node.label === void 0 || node.label === "" ? `${node.type} ${node.id}` : `${node.type} "${node.label}"`;
1416
+ const path = names.join(" > ");
1417
+ return node.label === void 0 || node.label === "" ? path : `${path} "${node.label}"`;
1418
+ }
1419
+ /**
1420
+ * Describes the stream driving a property.
1421
+ *
1422
+ * `timeOrigin` turns the binding's own reading into an epoch stamp:
1423
+ * `performance.timeOrigin` on a thread that has one, and zero where
1424
+ * the reading is already an epoch.
1425
+ */
1426
+ function describeStream(binding, timeOrigin) {
1427
+ const label = binding.observable?.label;
1428
+ const labelled = typeof label === "string" && label !== "";
1429
+ const emittedAt = binding.emittedAt();
1430
+ return {
1431
+ source: labelled ? label : `observable #${binding.id}`,
1432
+ kind: labelled ? "cell" : "observable",
1433
+ value: printPropValue(binding.value()),
1434
+ emissions: binding.emissionCount(),
1435
+ emittedAt: emittedAt === null ? null : timeOrigin + emittedAt,
1436
+ connected: binding.connected()
1437
+ };
1438
+ }
1439
+ /** A stream as one line: what feeds the prop, and how long ago it said so. */
1440
+ function formatStream(stream, now = Date.now()) {
1441
+ const when = stream.emittedAt === null ? "no value yet" : `${formatAge(Math.max(0, now - stream.emittedAt))} ago`;
1442
+ const emissions = `${stream.emissions} emission${stream.emissions === 1 ? "" : "s"}`;
1443
+ return `${stream.source} · ${emissions} · ${when}${stream.connected ? "" : " · disconnected"}`;
1444
+ }
1445
+ /** An age in the largest unit that stays readable. */
1446
+ function formatAge(ms) {
1447
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
1448
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
1449
+ return `${Math.round(ms / 6e4)}m`;
1450
+ }
1451
+ /**
1452
+ * A value as one line a person can read.
1453
+ *
1454
+ * `JSON.stringify` alone is not enough: the two values a canvas UI
1455
+ * puts in a property that it cannot handle are a `Set` (a node's
1456
+ * visual states) and a function (an event handler), and it prints both
1457
+ * as `{}`, which in an inspector reads as a bug in the application
1458
+ * rather than one in the inspector.
1459
+ */
1460
+ function printPropValue(value) {
1461
+ if (value === void 0) return "undefined";
1462
+ if (value === null) return "null";
1463
+ if (typeof value === "function") return `ƒ ${value.name === "" ? "(anonymous)" : value.name}`;
1464
+ if (typeof value === "symbol") return value.toString();
1465
+ if (typeof value === "string") return value;
1466
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
1467
+ if (value instanceof Set) return `Set { ${[...value].map(printPropValue).join(", ")} }`;
1468
+ if (value instanceof Map) return `Map { ${[...value].map(([key, entry]) => `${printPropValue(key)}: ${printPropValue(entry)}`).join(", ")} }`;
1469
+ if (Array.isArray(value)) return `[${value.map(printPropValue).join(", ")}]`;
1470
+ try {
1471
+ return JSON.stringify(value) ?? String(value);
1472
+ } catch {
1473
+ return String(value);
1474
+ }
1475
+ }
1476
+ /** The report as text, for a console or a test failure. */
1477
+ function formatNodeReport(report) {
1478
+ const lines = [`${report.type} '${report.id}'`];
1479
+ if (report.owners.length > 0) lines.push(`rendered by ${report.owners.map((owner) => owner.name).join(" inside ")}`);
1480
+ if (report.modifiers.length > 0) lines.push(`modifiers: ${report.modifiers.join(", ")}`);
1481
+ if (report.listens.length > 0) lines.push(`listens: ${report.listens.join(", ")}`);
1482
+ if (report.beneath.length > 0) {
1483
+ lines.push("beneath, at the pointer:");
1484
+ for (const under of report.beneath) {
1485
+ const owner = under.owner === void 0 ? "" : ` (${under.owner})`;
1486
+ const listens = under.listens.length === 0 ? "" : `, listens: ${under.listens.join(", ")}`;
1487
+ lines.push(` ${under.type} ${under.id}${owner}${listens}`);
1488
+ }
1489
+ }
1490
+ if (report.props.length > 0) {
1491
+ lines.push("props:");
1492
+ for (const prop of report.props) {
1493
+ lines.push(` ${prop.name} = ${prop.value}${prop.source === void 0 ? "" : ` (${prop.source})`}`);
1494
+ if (prop.stream !== void 0) lines.push(` ${formatStream(prop.stream)}`);
1495
+ }
1496
+ }
1497
+ if (report.environment.length > 0) {
1498
+ lines.push("environment:");
1499
+ for (const entry of report.environment) lines.push(` ${entry.key} = ${entry.value}${entry.provided ? " (provided here)" : ""}`);
1500
+ }
1501
+ if (report.semantics !== void 0) {
1502
+ const parts = [
1503
+ report.semantics.role === void 0 ? void 0 : `role ${report.semantics.role}`,
1504
+ report.semantics.label === void 0 ? void 0 : `label ${report.semantics.label}`,
1505
+ report.semantics.value === void 0 ? void 0 : `value ${report.semantics.value}`,
1506
+ report.semantics.states === void 0 || report.semantics.states.length === 0 ? void 0 : `states ${report.semantics.states.join(", ")}`
1507
+ ].filter((part) => part !== void 0);
1508
+ if (parts.length > 0) lines.push(`semantics: ${parts.join(" · ")}`);
1509
+ }
1510
+ lines.push(report.explanation);
1511
+ return lines.join("\n");
1512
+ }
1513
+ //#endregion
1514
+ //#region src/worker/captureConsole.ts
1515
+ const LEVELS = [
1516
+ "log",
1517
+ "info",
1518
+ "warn",
1519
+ "error",
1520
+ "debug"
1521
+ ];
1522
+ /**
1523
+ * Copies every `console.*` call on `target` (the worker global's
1524
+ * console by default) to `sink`. Returns a function that restores the
1525
+ * original methods.
1526
+ *
1527
+ * Idempotent per target: a second capture on the same console replaces
1528
+ * the first's sink rather than nesting, so toggling a panel on twice
1529
+ * does not log twice.
1530
+ */
1531
+ function captureConsole(sink, target = console) {
1532
+ const installed = target[CAPTURED];
1533
+ if (installed !== void 0) {
1534
+ installed.sink = sink;
1535
+ return installed.restore;
1536
+ }
1537
+ const originals = /* @__PURE__ */ new Map();
1538
+ const state = {
1539
+ sink,
1540
+ restore: () => {
1541
+ for (const [level, original] of originals) target[level] = original;
1542
+ delete target[CAPTURED];
1543
+ }
1544
+ };
1545
+ for (const level of LEVELS) {
1546
+ const original = target[level];
1547
+ originals.set(level, original);
1548
+ target[level] = (...args) => {
1549
+ original.apply(target, args);
1550
+ try {
1551
+ state.sink({
1552
+ level,
1553
+ args: args.map(formatConsoleArg),
1554
+ at: Date.now()
1555
+ });
1556
+ } catch {}
1557
+ };
1558
+ }
1559
+ target[CAPTURED] = state;
1560
+ return state.restore;
1561
+ }
1562
+ const CAPTURED = Symbol.for("gesso:console-captured");
1563
+ /**
1564
+ * One console argument as the panel prints it.
1565
+ *
1566
+ * `printPropValue` already prints the values a canvas UI is likely to
1567
+ * log; an Error is the one thing it prints badly (`{}`), and the one
1568
+ * thing a developer most wants to read whole.
1569
+ */
1570
+ function formatConsoleArg(value) {
1571
+ if (value instanceof Error) return value.stack !== void 0 && value.stack !== "" ? value.stack : `${value.name}: ${value.message}`;
1572
+ return printPropValue(value);
1573
+ }
1574
+ function isConsoleForwardingMessage(value) {
1575
+ const message = value;
1576
+ return message?.type === "gesso:console" && typeof message.enabled === "boolean";
1577
+ }
1578
+ function isConsoleEntryMessage(value) {
1579
+ const message = value;
1580
+ return message?.type === "gesso:console" && typeof message.entry === "object" && message.entry !== null;
1581
+ }
1582
+ //#endregion
1583
+ //#region src/worker/WorkerPorts.ts
1584
+ /**
1585
+ * Named `MessagePort`s into a worker.
1586
+ *
1587
+ * A worker's global `onmessage` is a single channel, so a worker that
1588
+ * receives messages on it can host exactly one conversation. That is
1589
+ * why a store in a data worker used to mean a worker per store: the
1590
+ * client claimed the `Worker` object itself, and a second one had
1591
+ * nowhere to go.
1592
+ *
1593
+ * A handshake fixes it. The client opens a `MessageChannel`, keeps one
1594
+ * end and transfers the other with a name; the worker serves that name
1595
+ * and the two ends talk privately from then on. The global channel is
1596
+ * used once per conversation and carries nothing else.
1597
+ *
1598
+ * Nothing here knows what travels over a port. It is the transport the
1599
+ * store replication in `../store/worker` runs on today and the barrier
1600
+ * contract will run on next.
1601
+ */
1602
+ function isPortHandshake(value) {
1603
+ const message = value;
1604
+ return message?.type === "gesso:port" && typeof message.key === "string";
1605
+ }
1606
+ /**
1607
+ * Stands for "whichever worker the shell spawned for the application".
1608
+ *
1609
+ * A registration inside the render worker cannot name that worker: it
1610
+ * is created by the shell and its port only arrives with `init`, long
1611
+ * after `useChannel` and `useService` have run. This sentinel is what a
1612
+ * registration puts there instead, and the render worker swaps it for
1613
+ * the real handle once the port shows up.
1614
+ *
1615
+ * Opening a port on it before then is a bug rather than a race, so it
1616
+ * says so.
1617
+ */
1618
+ const APPLICATION_WORKER = {
1619
+ open() {
1620
+ throw new Error("APPLICATION_WORKER was used directly. It is a placeholder the render worker replaces with the shell's port; reaching it means no application-logic worker was supplied. Pass appLogicWorker to createApp.");
1621
+ },
1622
+ spawned: false,
1623
+ terminate() {}
1624
+ };
1625
+ /**
1626
+ * A handle over an endpoint someone else owns.
1627
+ *
1628
+ * The shell spawns the application worker and hands the render worker
1629
+ * one end of a channel to it; this is what the render worker opens
1630
+ * named ports over. `terminate` is a no-op — the lifetime belongs to
1631
+ * whoever created the endpoint, and a handle that could kill a worker
1632
+ * it did not spawn would be a surprising thing to hand out.
1633
+ */
1634
+ function portHandle(endpoint) {
1635
+ return {
1636
+ open(key) {
1637
+ const channel = new MessageChannel();
1638
+ endpoint.postMessage({
1639
+ type: "gesso:port",
1640
+ key
1641
+ }, [channel.port2]);
1642
+ return channel.port1;
1643
+ },
1644
+ get spawned() {
1645
+ return true;
1646
+ },
1647
+ terminate() {}
1648
+ };
1649
+ }
1650
+ function isHubMessage(value) {
1651
+ return value?.type === "gesso:hub";
1652
+ }
1653
+ const BASE_INSTALLED = Symbol.for("gesso:port-base-installed");
1654
+ const CONSOLE_RESTORE = Symbol.for("gesso:port-console-restore");
1655
+ /**
1656
+ * Starts or stops copying this worker's console to whoever posts to
1657
+ * it, as `ConsoleEntryMessage`s. A host with no `postMessage` (a test's
1658
+ * bare object) has nowhere to send them and forwards nothing.
1659
+ */
1660
+ function setConsoleForwarding(host, enabled) {
1661
+ host[CONSOLE_RESTORE]?.();
1662
+ delete host[CONSOLE_RESTORE];
1663
+ const post = host.postMessage;
1664
+ if (!enabled || typeof post !== "function") return;
1665
+ host[CONSOLE_RESTORE] = captureConsole((entry) => {
1666
+ const message = {
1667
+ type: "gesso:console",
1668
+ entry
1669
+ };
1670
+ post.call(host, message);
1671
+ });
1672
+ }
1673
+ /**
1674
+ * The handler every `servePorts` chain sits on top of.
1675
+ *
1676
+ * It owns the two things no individual server can: routing a hub port
1677
+ * through the whole chain, and answering a handshake that nobody
1678
+ * accepted. Both have to be innermost — the first because the chain is
1679
+ * only complete once every server has wrapped `onmessage`, the second
1680
+ * because "nobody accepted" is only known after every server has
1681
+ * declined.
1682
+ */
1683
+ function installBase(host) {
1684
+ if (host[BASE_INSTALLED] === true) return;
1685
+ host[BASE_INSTALLED] = true;
1686
+ const previous = host.onmessage;
1687
+ const registered = host[SERVED_NAMES] ?? [];
1688
+ host.onmessage = (event) => {
1689
+ if (isConsoleForwardingMessage(event.data)) {
1690
+ setConsoleForwarding(host, event.data.enabled);
1691
+ return;
1692
+ }
1693
+ if (isHubMessage(event.data)) {
1694
+ const port = event.ports?.[0];
1695
+ if (port === void 0) throw new Error("A hub message arrived with no port attached.");
1696
+ port.onmessage = host.onmessage;
1697
+ return;
1698
+ }
1699
+ if (!isPortHandshake(event.data)) {
1700
+ previous?.(event);
1701
+ return;
1702
+ }
1703
+ const port = event.ports?.[0];
1704
+ if (port === void 0) throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);
1705
+ const served = registered.flatMap((get) => [...get()]).sort();
1706
+ const error = {
1707
+ type: "port:error",
1708
+ message: `Nothing is served under '${event.data.key}'. This worker serves: ${served.length > 0 ? served.join(", ") : "(nothing)"}.`
1709
+ };
1710
+ port.postMessage(error);
1711
+ };
1712
+ }
1713
+ /**
1714
+ * Wraps a worker factory so the worker is created once and shared.
1715
+ *
1716
+ * A factory rather than a URL for the same reason the render worker
1717
+ * takes one: a bundler only emits a chunk for a worker it can see
1718
+ * constructed literally in the calling module.
1719
+ *
1720
+ * const data = workerHandle(
1721
+ * () => new Worker(new URL('./data.worker.ts', import.meta.url), { type: 'module' })
1722
+ * );
1723
+ */
1724
+ function workerHandle(factory) {
1725
+ let worker;
1726
+ return {
1727
+ open(key) {
1728
+ worker ??= factory();
1729
+ const channel = new MessageChannel();
1730
+ worker.postMessage({
1731
+ type: "gesso:port",
1732
+ key
1733
+ }, [channel.port2]);
1734
+ return channel.port1;
1735
+ },
1736
+ get spawned() {
1737
+ return worker !== void 0;
1738
+ },
1739
+ terminate() {
1740
+ worker?.terminate();
1741
+ worker = void 0;
1742
+ }
1743
+ };
1744
+ }
1745
+ function isPortErrorMessage(value) {
1746
+ const message = value;
1747
+ return message?.type === "port:error" && typeof message.message === "string";
1748
+ }
1749
+ /** Every name served on a host, across all `servePorts` calls on it. */
1750
+ const SERVED_NAMES = Symbol.for("gesso:served-port-names");
1751
+ /**
1752
+ * Serves named ports inside a worker.
1753
+ *
1754
+ * Call it synchronously at the top level of the worker module, before
1755
+ * any await, so no handshake is missed.
1756
+ *
1757
+ * `onPort` returns whether it took the port. Returning false passes
1758
+ * the handshake to whatever was serving before, which is what lets two
1759
+ * kinds of thing — stores and channels, during the migration — share
1760
+ * one worker: each answers for its own names and declines the rest.
1761
+ * When nobody accepts, the port is answered with an error naming
1762
+ * everything the worker does serve, because a handshake that silently
1763
+ * matched nothing leaves the client waiting forever with nothing said.
1764
+ *
1765
+ * `names` is only read to build that message.
1766
+ *
1767
+ * Returns a function that stops serving.
1768
+ */
1769
+ function servePorts(onPort, names, host = self) {
1770
+ const withNames = host;
1771
+ const registered = withNames[SERVED_NAMES] ??= [];
1772
+ registered.push(names);
1773
+ installBase(host);
1774
+ const previous = host.onmessage;
1775
+ host.onmessage = (event) => {
1776
+ if (!isPortHandshake(event.data)) {
1777
+ previous?.(event);
1778
+ return;
1779
+ }
1780
+ const port = event.ports?.[0];
1781
+ if (port === void 0) throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);
1782
+ if (onPort(event.data.key, port)) return;
1783
+ previous?.(event);
1784
+ };
1785
+ return () => {
1786
+ host.onmessage = previous;
1787
+ const index = registered.indexOf(names);
1788
+ if (index >= 0) registered.splice(index, 1);
1789
+ };
1790
+ }
1791
+ //#endregion
1792
+ //#region src/channel/serveChannels.ts
1793
+ /**
1794
+ * One served channel, with its source checked against its token.
1795
+ *
1796
+ * `ServedChannel` is erased on purpose, so one list can hold channels
1797
+ * of every shape; the cost is that a view key the token declares and
1798
+ * the source forgets is found at startup, by the error `provide`
1799
+ * reports, rather than by the compiler. This is the typed seam: the
1800
+ * source must hold an Observable for every key of the token's view and
1801
+ * a handler for every command, and each handler takes the arguments
1802
+ * the token declares, so none of them needs an annotation.
1803
+ *
1804
+ * serveChannels([
1805
+ * serve(Catalog, { view: catalog, commands: { add: name => catalog.add(name) } })
1806
+ * ]);
1807
+ *
1808
+ * The view may be any object with the right observables on it, which
1809
+ * is often the domain object itself when its properties are named
1810
+ * after the keys. Only the declared keys are read from it.
1811
+ */
1812
+ function serve(token, source) {
1813
+ return {
1814
+ token,
1815
+ source
1816
+ };
1817
+ }
1818
+ /**
1819
+ * Publishes channels from an application worker.
1820
+ *
1821
+ * Call it synchronously at the top level of the worker module, before
1822
+ * any await, so no handshake is missed:
1823
+ *
1824
+ * const catalog = new CatalogViewModel(new CatalogDomain(new OpfsStore()));
1825
+ * serveChannels([
1826
+ * serve(Catalog, { view: { products: catalog.products$ }, commands: { … } })
1827
+ * ]);
1828
+ *
1829
+ * Everything above this call is the application's own — plain classes,
1830
+ * plain observables, no framework import. This function is the entire
1831
+ * seam between it and the view.
1832
+ *
1833
+ * Returns a function that stops serving and disposes what it provided.
1834
+ */
1835
+ function serveChannels(channels, host) {
1836
+ const byName = /* @__PURE__ */ new Map();
1837
+ for (const served of channels) byName.set(served.token.name, served);
1838
+ const provided = [];
1839
+ const stop = servePorts((key, port) => {
1840
+ const served = byName.get(key);
1841
+ if (served === void 0) return false;
1842
+ provided.push(provide(served.token, served.source, port));
1843
+ return true;
1844
+ }, () => [...byName.keys()], host);
1845
+ return () => {
1846
+ stop();
1847
+ for (const channel of provided) channel.dispose();
1848
+ provided.length = 0;
1849
+ };
1850
+ }
1851
+ //#endregion
1852
+ //#region src/channel/pick.ts
1853
+ /**
1854
+ * One key of a view model, as its own Observable, emitting only when
1855
+ * that key's value changes.
1856
+ *
1857
+ * `provide` and `serveChannels` want one Observable per view key, so
1858
+ * the differ can patch each key on its own, while a view model is most
1859
+ * naturally one Observable of one object. This is the seam between the
1860
+ * two, and every application worker was about to write it.
1861
+ */
1862
+ function pick(source, key) {
1863
+ return source.pipe(map((value) => value[key]), distinctUntilChanged());
1864
+ }
1865
+ /**
1866
+ * Every key of a view model as its own Observable: the `view` a channel
1867
+ * source wants, from the one Observable a view model has.
1868
+ *
1869
+ * serveChannels([{ token: Queue, source: { view: pickKeys(queue.view, QUEUE_KEYS), commands } }])
1870
+ */
1871
+ function pickKeys(source, keys) {
1872
+ const out = {};
1873
+ for (const key of keys) out[key] = pick(source, key);
1874
+ return out;
1875
+ }
1876
+ //#endregion
1877
+ //#region src/storage/StorageAdapter.ts
1878
+ /**
1879
+ * Which of the four outcomes a thrown platform error is.
1880
+ *
1881
+ * The names are the ones the storage APIs actually throw.
1882
+ * `QuotaExceededError` is the DOM's word for full, and every browser
1883
+ * uses it for OPFS, IndexedDB and `localStorage` alike. `SecurityError`
1884
+ * and `NotAllowedError` are what a blocked origin gets. A missing API
1885
+ * (no `navigator.storage`, no `indexedDB`) is a `TypeError` here and
1886
+ * is `denied` for the same reason: nothing the application does will
1887
+ * produce a store.
1888
+ */
1889
+ function classifyStorageError(error) {
1890
+ const name = error instanceof Error ? error.name : "";
1891
+ if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return "full";
1892
+ if (name === "SecurityError" || name === "NotAllowedError" || name === "TypeError") return "denied";
1893
+ return "failed";
1894
+ }
1895
+ /** A thrown value as the message a screen could show. */
1896
+ function storageErrorMessage(error) {
1897
+ return error instanceof Error ? error.message : String(error);
1898
+ }
1899
+ /** A read that could not answer, as one record. */
1900
+ function storageReadFailure(error) {
1901
+ return {
1902
+ outcome: classifyStorageError(error),
1903
+ value: null,
1904
+ error: storageErrorMessage(error)
1905
+ };
1906
+ }
1907
+ /** A read that answered, whether or not it found anything. */
1908
+ function storageReadValue(value) {
1909
+ return {
1910
+ outcome: "ok",
1911
+ value,
1912
+ error: null
1913
+ };
1914
+ }
1915
+ /**
1916
+ * The same contract in memory, for specs and for a platform with no
1917
+ * store at all.
1918
+ *
1919
+ * Not a fallback anything installs on its own. An application that
1920
+ * would rather run with an unremembered session than fail says so by
1921
+ * passing one of these; one that would rather tell the person its
1922
+ * settings will not be kept reads the `denied` outcome and says so.
1923
+ * Choosing between those two on an application's behalf is exactly the
1924
+ * kind of decision the thread model keeps out of the framework.
1925
+ */
1926
+ var MemoryStorage = class {
1927
+ records = /* @__PURE__ */ new Map();
1928
+ /** Written by a spec that wants to see what a full store does. */
1929
+ full = false;
1930
+ read(key) {
1931
+ return Promise.resolve(storageReadValue(this.records.get(key) ?? null));
1932
+ }
1933
+ write(key, value) {
1934
+ if (this.full) return Promise.resolve("full");
1935
+ this.records.set(key, value);
1936
+ return Promise.resolve("ok");
1937
+ }
1938
+ remove(key) {
1939
+ this.records.delete(key);
1940
+ return Promise.resolve("ok");
1941
+ }
1942
+ keys() {
1943
+ return Promise.resolve([...this.records.keys()]);
1944
+ }
1945
+ };
1946
+ //#endregion
1947
+ //#region src/undo/UndoStack.ts
1948
+ const NOTHING = {
1949
+ undo: null,
1950
+ redo: null
1951
+ };
1952
+ /**
1953
+ * The application's undo, as a stack of named transactions.
1954
+ *
1955
+ * A component registers what it did and how to unmake it, and a menu,
1956
+ * a button or a keyboard shortcut drives the stack. Nothing here
1957
+ * listens to anything or knows what an application's state is: the two
1958
+ * functions in a transaction are the whole of the coupling, which is
1959
+ * why this can sit on either thread and why it does not become a data
1960
+ * layer.
1961
+ *
1962
+ * const undo = ctx.inject(UndoStack);
1963
+ *
1964
+ * queue.send.remove(at);
1965
+ * undo.push({
1966
+ * label: `Remove ${track.title}`,
1967
+ * undo: () => queue.send.addToQueue(track.id),
1968
+ * redo: () => queue.send.remove(at)
1969
+ * });
1970
+ *
1971
+ * ## How this relates to the undo inside a text field
1972
+ *
1973
+ * `EditableTextModel` has had its own undo since text became editable,
1974
+ * and the two are deliberately separate. A field's undo is a stack of
1975
+ * **snapshots of one string**, private to the field, and it has to be:
1976
+ * the model is the only thing that knows where the caret was, which
1977
+ * run of typing coalesces with which, and what an IME composition is
1978
+ * doing. This one is a stack of **inverse operations** over whatever
1979
+ * an application's state happens to be, and it cannot see inside a
1980
+ * field at all.
1981
+ *
1982
+ * They meet at one key press, and the rule there is that the focused
1983
+ * field wins: `registerUndoShortcuts` skips Mod+Z while something is
1984
+ * being typed into, so undo in a field undoes typing and undo
1985
+ * everywhere else undoes the application's last change. Merging the
1986
+ * two stacks would mean a keystroke and a queue reorder sharing one
1987
+ * history, which is not what either of them means.
1988
+ *
1989
+ * ## Reentrancy
1990
+ *
1991
+ * A push while an undo or a redo is running is dropped. Without it an
1992
+ * application whose edit path records itself would record the undo as
1993
+ * a new edit and the stack would never empty. Write the inverse
1994
+ * functions to call the state directly rather than through the same
1995
+ * path that records, and the guard never fires.
1996
+ */
1997
+ var UndoStack = class {
1998
+ entries = [];
1999
+ undone = [];
2000
+ limit;
2001
+ /** True while `undo()` or `redo()` is running one of the functions. */
2002
+ running = false;
2003
+ /** Set by `endRun`, so the next push starts a new entry whatever its key. */
2004
+ sealed = false;
2005
+ labels;
2006
+ /** What undoing would undo, or null when there is nothing to undo. */
2007
+ undoLabel;
2008
+ /** What redoing would redo, or null when there is nothing to redo. */
2009
+ redoLabel;
2010
+ /** Whether there is anything to undo, for a menu item's `disabled`. */
2011
+ canUndo;
2012
+ canRedo;
2013
+ constructor(options = {}) {
2014
+ this.limit = Math.max(1, options.limit ?? 100);
2015
+ this.labels = internalState(NOTHING, options.label);
2016
+ this.undoLabel = select(this.labels, "undo");
2017
+ this.redoLabel = select(this.labels, "redo");
2018
+ this.canUndo = computed(() => this.undoLabel.value !== null);
2019
+ this.canRedo = computed(() => this.redoLabel.value !== null);
2020
+ }
2021
+ /** How many entries are held, for a spec or a budget. */
2022
+ get size() {
2023
+ return this.entries.length;
2024
+ }
2025
+ /** How many redos are waiting, for a spec or a budget. */
2026
+ get redoSize() {
2027
+ return this.undone.length;
2028
+ }
2029
+ /**
2030
+ * Records a change that has already been made.
2031
+ *
2032
+ * Pushing is what discards the redo branch: making a change after
2033
+ * undoing two is the person choosing the other future, and keeping
2034
+ * the abandoned one would mean redoing into a state that no longer
2035
+ * follows from what is on screen.
2036
+ */
2037
+ push(transaction) {
2038
+ if (this.running) return;
2039
+ this.undone.length = 0;
2040
+ const previous = this.entries[this.entries.length - 1];
2041
+ const sealed = this.sealed;
2042
+ this.sealed = false;
2043
+ if (!sealed && previous !== void 0 && transaction.coalesce !== void 0 && previous.coalesce === transaction.coalesce) {
2044
+ this.entries[this.entries.length - 1] = {
2045
+ label: transaction.label,
2046
+ undo: previous.undo,
2047
+ redo: transaction.redo,
2048
+ coalesce: transaction.coalesce
2049
+ };
2050
+ this.publish();
2051
+ return;
2052
+ }
2053
+ this.entries.push(transaction);
2054
+ while (this.entries.length > this.limit) this.entries.shift();
2055
+ this.publish();
2056
+ }
2057
+ /**
2058
+ * Groups everything pushed inside `body` into one entry.
2059
+ *
2060
+ * For a change an application makes as several calls and a person
2061
+ * made as one press: undoing runs the group's undos in reverse, and
2062
+ * redoing runs its redos in order. Unlike `coalesce`, a group keeps
2063
+ * every step, because a group is written down as a group rather than
2064
+ * discovered from a run of similar pushes.
2065
+ */
2066
+ transact(label, body) {
2067
+ if (this.running) return body();
2068
+ const outer = this.entries.length;
2069
+ const result = body();
2070
+ const collected = this.entries.splice(outer);
2071
+ if (collected.length > 0) {
2072
+ this.entries.push({
2073
+ label,
2074
+ undo: () => {
2075
+ for (let at = collected.length - 1; at >= 0; at--) collected[at].undo();
2076
+ },
2077
+ redo: () => {
2078
+ for (const step of collected) step.redo();
2079
+ }
2080
+ });
2081
+ this.sealed = true;
2082
+ this.publish();
2083
+ }
2084
+ return result;
2085
+ }
2086
+ /**
2087
+ * Ends the current run, so the next push starts its own entry.
2088
+ *
2089
+ * The twin of `EditableTextModel.endTypingRun`. A drag calls it when
2090
+ * the pointer comes up: without it, dragging a row, letting go, and
2091
+ * dragging the same row again would coalesce into one entry, and one
2092
+ * undo would put the row back where it was two gestures ago.
2093
+ */
2094
+ endRun() {
2095
+ this.sealed = true;
2096
+ }
2097
+ /** Undoes the last change. False when there was nothing to undo. */
2098
+ undo() {
2099
+ const entry = this.entries.pop();
2100
+ if (entry === void 0) return false;
2101
+ this.run(entry.undo);
2102
+ this.undone.push(entry);
2103
+ this.sealed = true;
2104
+ this.publish();
2105
+ return true;
2106
+ }
2107
+ /** Redoes the last undone change. False when there was nothing to redo. */
2108
+ redo() {
2109
+ const entry = this.undone.pop();
2110
+ if (entry === void 0) return false;
2111
+ this.run(entry.redo);
2112
+ this.entries.push(entry);
2113
+ this.sealed = true;
2114
+ this.publish();
2115
+ return true;
2116
+ }
2117
+ /**
2118
+ * Forgets everything, in both directions.
2119
+ *
2120
+ * What an application calls when the thing the entries refer to is
2121
+ * gone: a queue emptied, a document closed, a signed-out account's
2122
+ * library replaced. `EditableTextModel.setText` does the same for
2123
+ * the same reason, and the reason is that an inverse function whose
2124
+ * subject no longer exists is not an undo, it is a surprise.
2125
+ */
2126
+ clear() {
2127
+ this.entries.length = 0;
2128
+ this.undone.length = 0;
2129
+ this.sealed = false;
2130
+ this.publish();
2131
+ }
2132
+ run(action) {
2133
+ this.running = true;
2134
+ try {
2135
+ action();
2136
+ } finally {
2137
+ this.running = false;
2138
+ }
2139
+ }
2140
+ publish() {
2141
+ const next = {
2142
+ undo: this.entries[this.entries.length - 1]?.label ?? null,
2143
+ redo: this.undone[this.undone.length - 1]?.label ?? null
2144
+ };
2145
+ if (next.undo !== this.labels.value.undo || next.redo !== this.labels.value.redo) this.labels.value = next;
2146
+ }
2147
+ };
2148
+ //#endregion
2149
+ //#region src/undo/undoable.ts
2150
+ /**
2151
+ * Puts a `mutate` on an undo stack, by way of its own inverse.
2152
+ *
2153
+ * private readonly liked = internalState<readonly string[]>([]);
2154
+ * private readonly like = mutate(this.liked, toggled, id => api.favourite(id));
2155
+ * readonly toggleLike = undoable(undo, this.like, id => id, { label: () => 'Like' });
2156
+ *
2157
+ * The whole of it is that undoing an optimistic change is another
2158
+ * optimistic change. Nothing here writes the cell behind the
2159
+ * mutation's back, which is the thing that would go wrong if an undo
2160
+ * stack held values rather than operations: it would restore a value
2161
+ * the server has not been told about, and the next rollback would
2162
+ * fight it.
2163
+ *
2164
+ * `invert` answers the argument that undoes this one. For a toggle
2165
+ * that is the same argument again, which is why the example above
2166
+ * looks like it does nothing.
2167
+ *
2168
+ * **A refused write is not on the stack.** `mutate` already puts the
2169
+ * cell back when a commit rejects or resolves `false`, so the change
2170
+ * did not happen and there is nothing to undo; pushing it would give a
2171
+ * person an undo that undoes something they never saw.
2172
+ *
2173
+ * The undo and the redo call `mutation.run` rather than this wrapper,
2174
+ * so running them records nothing and the stack's reentrancy guard
2175
+ * never has to fire.
2176
+ */
2177
+ function undoable(stack, mutation, invert, options) {
2178
+ const { label, coalesce } = options;
2179
+ return async (argument) => {
2180
+ if (!await mutation.run(argument)) return false;
2181
+ stack.push({
2182
+ label: typeof label === "function" ? label(argument) : label,
2183
+ undo: () => void mutation.run(invert(argument)),
2184
+ redo: () => void mutation.run(argument),
2185
+ ...coalesce === void 0 ? {} : { coalesce: typeof coalesce === "function" ? coalesce(argument) : coalesce }
2186
+ });
2187
+ return true;
2188
+ };
2189
+ }
2190
+ //#endregion
2191
+ //#region src/storage/OpfsStorage.ts
2192
+ /**
2193
+ * A store in the origin's private file system.
2194
+ *
2195
+ * The right default for an application's own state. It is reachable
2196
+ * from a worker, which `localStorage` is not, so the thread that owns
2197
+ * the state is the thread that writes it and nothing has to cross the
2198
+ * barrier to be remembered. It is asynchronous throughout, so nothing
2199
+ * it does blocks a frame. And it is per-origin and invisible to the
2200
+ * person, which is the right place for a queue or a draft and the
2201
+ * wrong place for anything they should be able to find and delete.
2202
+ *
2203
+ * One file per key, named by the key with the characters a file system
2204
+ * would refuse escaped, so a key is recoverable from a listing and a
2205
+ * key containing a slash cannot reach out of the folder.
2206
+ *
2207
+ * What happens on each failure:
2208
+ *
2209
+ * - **The platform has no OPFS**, or the browser refuses it (a private
2210
+ * window, a blocked origin): every method answers `denied` and the
2211
+ * error message says which. The store is not usable this session and
2212
+ * `persisted` stops writing to it after the first denial.
2213
+ * - **The quota is spent**: the write answers `full`. Nothing is
2214
+ * rolled back, because the value the application holds is the real
2215
+ * one and only the remembering failed.
2216
+ * - **There is no such record**: the read answers `ok` with `null`.
2217
+ * Not having been written yet is the ordinary first run, not a
2218
+ * failure.
2219
+ * - **Anything else**: `failed`, with the platform's message. A file
2220
+ * whose *contents* are not what this version writes is a different
2221
+ * thing and is `persisted`'s to judge, which it does by discarding
2222
+ * it, the same call `Tokens.ts` makes.
2223
+ */
2224
+ var OpfsStorage = class {
2225
+ folder;
2226
+ rootOf;
2227
+ /** The folder, once opened. Reused, because opening it is a round trip. */
2228
+ opening = null;
2229
+ constructor(options = {}) {
2230
+ this.folder = options.directory ?? "gesso";
2231
+ this.rootOf = options.root ?? defaultRoot;
2232
+ }
2233
+ async read(key) {
2234
+ try {
2235
+ return storageReadValue(await (await (await (await this.open()).getFileHandle(fileFor(key))).getFile()).text());
2236
+ } catch (error) {
2237
+ if (error instanceof Error && error.name === "NotFoundError") return storageReadValue(null);
2238
+ return storageReadFailure(error);
2239
+ }
2240
+ }
2241
+ async write(key, value) {
2242
+ try {
2243
+ const writable = await (await (await this.open()).getFileHandle(fileFor(key), { create: true })).createWritable();
2244
+ await writable.write(value);
2245
+ await writable.close();
2246
+ return "ok";
2247
+ } catch (error) {
2248
+ return classifyStorageError(error);
2249
+ }
2250
+ }
2251
+ async remove(key) {
2252
+ try {
2253
+ await (await this.open()).removeEntry(fileFor(key));
2254
+ return "ok";
2255
+ } catch (error) {
2256
+ if (error instanceof Error && error.name === "NotFoundError") return "ok";
2257
+ return classifyStorageError(error);
2258
+ }
2259
+ }
2260
+ async keys() {
2261
+ try {
2262
+ const found = [];
2263
+ for await (const name of (await this.open()).keys()) found.push(keyFor(name));
2264
+ return found;
2265
+ } catch {
2266
+ return [];
2267
+ }
2268
+ }
2269
+ /**
2270
+ * The folder, opened once and kept.
2271
+ *
2272
+ * A failed open is *not* kept. A rejected promise left in `opening`
2273
+ * would answer every later call with the same rejection, so one
2274
+ * refusal at start-up would be a store that never worked again even
2275
+ * after the person granted storage or made room. Kept when it
2276
+ * succeeds, dropped when it does not, which is one line and the
2277
+ * difference between a cache and a poison.
2278
+ */
2279
+ open() {
2280
+ if (this.opening === null) {
2281
+ const opening = this.rootOf().then((root) => root.getDirectoryHandle(this.folder, { create: true }));
2282
+ this.opening = opening;
2283
+ opening.catch(() => {
2284
+ if (this.opening === opening) this.opening = null;
2285
+ });
2286
+ }
2287
+ return this.opening;
2288
+ }
2289
+ };
2290
+ function defaultRoot() {
2291
+ const storage = globalThis.navigator?.storage;
2292
+ if (storage?.getDirectory === void 0) return Promise.reject(/* @__PURE__ */ new TypeError("This environment has no Origin Private File System."));
2293
+ return storage.getDirectory();
2294
+ }
2295
+ /**
2296
+ * A key as a file name.
2297
+ *
2298
+ * `encodeURIComponent` and not a hash, so a listing of the folder in
2299
+ * devtools reads as the keys the application wrote. It escapes the
2300
+ * slash and the dot, which is what stops a key reaching a directory it
2301
+ * was not given.
2302
+ */
2303
+ function fileFor(key) {
2304
+ return `${encodeURIComponent(key)}.json`;
2305
+ }
2306
+ function keyFor(name) {
2307
+ return decodeURIComponent(name.replace(/\.json$/, ""));
2308
+ }
2309
+ //#endregion
2310
+ //#region src/storage/IndexedDbStorage.ts
2311
+ /**
2312
+ * A store in IndexedDB.
2313
+ *
2314
+ * Beside OPFS rather than instead of it, because the two fail in
2315
+ * different places and an application picks by which failure it
2316
+ * minds. IndexedDB is reachable from every thread, survives longer
2317
+ * under a browser's own eviction, and is what a Safari that has
2318
+ * disabled OPFS still has; OPFS is faster for one large record and
2319
+ * simpler to inspect. Neither is a default the framework picks: an
2320
+ * application names the one it wants.
2321
+ *
2322
+ * One object store of strings keyed by string, which is the shape
2323
+ * `StorageAdapter` describes and no more. Indexes, versions past the
2324
+ * first, and cursors over ranges are what an application builds when
2325
+ * it has outgrown a key-value store, and at that point it is writing
2326
+ * against IndexedDB rather than against this.
2327
+ *
2328
+ * What happens on each failure:
2329
+ *
2330
+ * - **No `indexedDB`, or an origin that may not open one** (a private
2331
+ * window in some browsers, a blocked third-party context): every
2332
+ * method answers `denied`, and the open is retried next time rather
2333
+ * than cached, because a `denied` can be lifted by a site setting
2334
+ * mid-session.
2335
+ * - **The quota is spent**: the write answers `full`. IndexedDB
2336
+ * reports this on the transaction rather than on the request, which
2337
+ * is why the write waits for `oncomplete` and not for
2338
+ * `onsuccess`: a put that succeeded into a transaction that then
2339
+ * aborted has not been written, and answering `ok` for it would be
2340
+ * the adapter assuming success.
2341
+ * - **A version change from another tab**: the connection is closed
2342
+ * and dropped, so the next call opens a fresh one. Answering
2343
+ * `failed` and holding a dead connection would make every later
2344
+ * call fail too.
2345
+ * - **Anything else**: `failed`, with the platform's message.
2346
+ */
2347
+ var IndexedDbStorage = class {
2348
+ database;
2349
+ store;
2350
+ factory;
2351
+ connecting = null;
2352
+ constructor(options = {}) {
2353
+ this.database = options.database ?? "gesso";
2354
+ this.store = options.store ?? "records";
2355
+ this.factory = options.factory ?? globalThis.indexedDB;
2356
+ }
2357
+ async read(key) {
2358
+ try {
2359
+ const value = await this.transact("readonly", (store) => store.get(key));
2360
+ return storageReadValue(typeof value === "string" ? value : null);
2361
+ } catch (error) {
2362
+ return storageReadFailure(error);
2363
+ }
2364
+ }
2365
+ async write(key, value) {
2366
+ return this.outcomeOf(() => this.transact("readwrite", (store) => store.put(value, key)));
2367
+ }
2368
+ async remove(key) {
2369
+ return this.outcomeOf(() => this.transact("readwrite", (store) => store.delete(key)));
2370
+ }
2371
+ async keys() {
2372
+ try {
2373
+ const found = await this.transact("readonly", (store) => store.getAllKeys());
2374
+ return Array.isArray(found) ? found.filter((key) => typeof key === "string") : [];
2375
+ } catch {
2376
+ return [];
2377
+ }
2378
+ }
2379
+ /** Lets go of the connection, for a spec or an application shutting down. */
2380
+ close() {
2381
+ const connecting = this.connecting;
2382
+ this.connecting = null;
2383
+ connecting?.then((database) => database.close(), () => void 0);
2384
+ }
2385
+ async outcomeOf(work) {
2386
+ try {
2387
+ await work();
2388
+ return "ok";
2389
+ } catch (error) {
2390
+ const outcome = classifyStorageError(error);
2391
+ if (outcome === "denied") this.connecting = null;
2392
+ return outcome;
2393
+ }
2394
+ }
2395
+ /**
2396
+ * Runs one request inside one transaction and answers its result.
2397
+ *
2398
+ * A write resolves on the transaction completing rather than on the
2399
+ * request succeeding, because those are two different claims: the
2400
+ * second says the put was accepted, and only the first says it
2401
+ * reached the disk.
2402
+ */
2403
+ async transact(mode, run) {
2404
+ const database = await this.connect();
2405
+ return new Promise((resolve, reject) => {
2406
+ let answer;
2407
+ const transaction = database.transaction(this.store, mode);
2408
+ const request = run(transaction.objectStore(this.store));
2409
+ request.onsuccess = () => {
2410
+ answer = request.result;
2411
+ };
2412
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("The request failed."));
2413
+ transaction.oncomplete = () => resolve(answer);
2414
+ transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("The transaction was aborted."));
2415
+ });
2416
+ }
2417
+ connect() {
2418
+ this.connecting ??= this.open();
2419
+ return this.connecting;
2420
+ }
2421
+ open() {
2422
+ const factory = this.factory;
2423
+ if (factory === void 0) return Promise.reject(/* @__PURE__ */ new TypeError("This environment has no IndexedDB."));
2424
+ return new Promise((resolve, reject) => {
2425
+ let request;
2426
+ try {
2427
+ request = factory.open(this.database, 1);
2428
+ } catch (error) {
2429
+ reject(error);
2430
+ return;
2431
+ }
2432
+ request.onupgradeneeded = () => {
2433
+ if (!request.result.objectStoreNames.contains(this.store)) request.result.createObjectStore(this.store);
2434
+ };
2435
+ request.onsuccess = () => {
2436
+ request.result.onversionchange = () => this.close();
2437
+ resolve(request.result);
2438
+ };
2439
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("The database could not be opened."));
2440
+ request.onblocked = () => reject(/* @__PURE__ */ new Error("The database is open in another tab at a different version."));
2441
+ });
2442
+ }
2443
+ };
2444
+ //#endregion
2445
+ //#region src/storage/persisted.ts
2446
+ const MESSAGES = {
2447
+ denied: "This browser will not let the application store anything.",
2448
+ full: "There is no room left to store this.",
2449
+ failed: "The store could not be written to."
2450
+ };
2451
+ /**
2452
+ * A value that survives the application being closed.
2453
+ *
2454
+ * Hydration is a `resource`, which is not a detail: reading from a
2455
+ * disk is a keyed request that can be slow, can answer "there is
2456
+ * nothing", and can fail, which is the same set of outcomes a request
2457
+ * over the network has. So the statuses here are `ResourceStatus`
2458
+ * itself rather than a fourth enum saying the same five things in
2459
+ * different words.
2460
+ *
2461
+ * readonly draft = persisted(new OpfsStorage(), 'draft', { initial: '' });
2462
+ *
2463
+ * // on a screen
2464
+ * <TextInput value={draft.value} onChange={text => draft.set(text)} />
2465
+ *
2466
+ * ## What a screen sees before hydration finishes
2467
+ *
2468
+ * The default, and `status` reading `loading`. Nothing waits, nothing
2469
+ * is null, and no screen has a shape it only has for the first eighty
2470
+ * milliseconds. When the read lands the value changes like any other
2471
+ * cell change, and a screen that wants to say "restoring" reads
2472
+ * `status`.
2473
+ *
2474
+ * The one race that needs a rule is a person changing the value before
2475
+ * the disk has answered, which is not rare: a queue is a press away
2476
+ * and OPFS is a round trip away. **What they did wins.** A hydration
2477
+ * answer is applied only if nothing has been `set` since, on the same
2478
+ * reasoning as `mutate`'s guarded rollback: an answer that was
2479
+ * overtaken is stale, and putting it on screen would undo something
2480
+ * the person just did.
2481
+ *
2482
+ * ## What happens when storing fails
2483
+ *
2484
+ * - `denied`: nothing is written this session and nothing is tried
2485
+ * again, because the answer will not change. `status` is `failed`
2486
+ * and `saveError` says so once rather than on every keystroke.
2487
+ * - `full`: the write failed and the value in memory is kept. Nothing
2488
+ * is rolled back: the change is real and only the remembering of it
2489
+ * failed. The next change is still attempted, because a quota can be
2490
+ * given back.
2491
+ * - `failed`: the same as `full`, and for the same reason.
2492
+ * - A record that parses but is not the shape `revive` accepts is
2493
+ * treated as `missing`: the application starts from its default
2494
+ * rather than showing an error about a file the person cannot see.
2495
+ *
2496
+ * ## It is a helper
2497
+ *
2498
+ * Nothing in the framework holds one, and nothing is reachable only
2499
+ * through it. A channel is served from plain Observables as it always
2500
+ * was, and an application that would rather read and write a store
2501
+ * itself is writing against the same `StorageAdapter` this is written
2502
+ * against. The thread model declined to own an application's data
2503
+ * architecture, and remembering a value is not the exception to that.
2504
+ */
2505
+ var PersistedState = class {
2506
+ adapter;
2507
+ key;
2508
+ held;
2509
+ saves;
2510
+ failure;
2511
+ record;
2512
+ writing;
2513
+ /** True once the application has written a value of its own. */
2514
+ touched = false;
2515
+ /** Set when the store said `denied`, which is permanent for the session. */
2516
+ refused = false;
2517
+ /** The text last known to be on disk, so hydration does not write itself back. */
2518
+ stored = null;
2519
+ /** What `forget` goes back to. */
2520
+ initial;
2521
+ /** Where the first read got to; the same five words `resource` uses. */
2522
+ status;
2523
+ /** Why the read did not answer, as a message; null when it did. */
2524
+ error;
2525
+ /** What is remembered: the default until the read lands. */
2526
+ value;
2527
+ /** Writes in the air, for a saving indicator. A count, as `mutate.pending` is. */
2528
+ saving;
2529
+ /** Why the last write did not happen, as a message; null when it did. */
2530
+ saveError;
2531
+ /** Resolves when the first read has settled, whatever it found. */
2532
+ hydrated;
2533
+ constructor(adapter, key, options) {
2534
+ this.adapter = adapter;
2535
+ this.key = key;
2536
+ const label = options.label;
2537
+ this.initial = options.initial;
2538
+ this.held = internalState(options.initial, label);
2539
+ this.saves = internalState(0, label === void 0 ? void 0 : `${label}.saving`);
2540
+ this.failure = internalState(null, label === void 0 ? void 0 : `${label}.saveError`);
2541
+ this.value = this.held;
2542
+ this.saving = this.saves;
2543
+ this.saveError = this.failure;
2544
+ this.record = resource(of(key), () => this.load(options.revive), label === void 0 ? {} : { label: `${label}.hydration` });
2545
+ this.status = this.record.status;
2546
+ this.error = this.record.error;
2547
+ this.hydrated = this.record.settled.then(() => this.apply());
2548
+ this.writing = debounced(this.held, options.settle ?? 250).pipe(skip(1)).subscribe((value) => void this.flush(value));
2549
+ }
2550
+ /** What is remembered right now, for code that is not subscribing. */
2551
+ get current() {
2552
+ return this.held.value;
2553
+ }
2554
+ /** Remembers a new value. The write follows once the changes stop. */
2555
+ set(value) {
2556
+ this.touched = true;
2557
+ this.held.value = value;
2558
+ }
2559
+ /**
2560
+ * Writes what is held now, without waiting for the gate.
2561
+ *
2562
+ * For the moment an application knows it is about to lose the thread:
2563
+ * a `visibilitychange`, a route away from an editor, a sign-out.
2564
+ */
2565
+ save() {
2566
+ return this.flush(this.held.value);
2567
+ }
2568
+ /**
2569
+ * Forgets the record and goes back to the default.
2570
+ *
2571
+ * Both halves, because a stored value removed while the cell still
2572
+ * holds it would be written straight back by the next change.
2573
+ */
2574
+ async forget() {
2575
+ this.stored = null;
2576
+ this.touched = true;
2577
+ this.held.value = this.initial;
2578
+ await this.adapter.remove(this.key);
2579
+ }
2580
+ /** Gives back the write subscription and the resource's. */
2581
+ dispose() {
2582
+ this.writing.unsubscribe();
2583
+ this.record.dispose();
2584
+ }
2585
+ async load(revive) {
2586
+ const read = await this.adapter.read(this.key);
2587
+ if (read.outcome !== "ok") {
2588
+ if (read.outcome === "denied") this.refused = true;
2589
+ throw new Error(read.error ?? MESSAGES[read.outcome] ?? "The store could not be read.");
2590
+ }
2591
+ if (read.value === null) return null;
2592
+ this.stored = read.value;
2593
+ let parsed;
2594
+ try {
2595
+ parsed = JSON.parse(read.value);
2596
+ } catch {
2597
+ return null;
2598
+ }
2599
+ return revive === void 0 ? parsed : revive(parsed);
2600
+ }
2601
+ apply() {
2602
+ const found = this.record.value.value;
2603
+ if (found === null || this.touched) return;
2604
+ this.held.value = found;
2605
+ }
2606
+ async flush(value) {
2607
+ if (this.refused) return;
2608
+ let text;
2609
+ try {
2610
+ text = JSON.stringify(value);
2611
+ } catch (error) {
2612
+ this.failure.value = error instanceof Error ? error.message : String(error);
2613
+ return;
2614
+ }
2615
+ if (text === this.stored) return;
2616
+ this.saves.value = this.saves.value + 1;
2617
+ try {
2618
+ const outcome = await this.adapter.write(this.key, text);
2619
+ if (outcome === "ok") {
2620
+ this.stored = text;
2621
+ this.failure.value = null;
2622
+ return;
2623
+ }
2624
+ if (outcome === "denied") this.refused = true;
2625
+ this.failure.value = MESSAGES[outcome] ?? "The store could not be written to.";
2626
+ } finally {
2627
+ this.saves.value = Math.max(0, this.saves.value - 1);
2628
+ }
2629
+ }
2630
+ };
2631
+ /**
2632
+ * A value read from a store on start and written back as it changes.
2633
+ *
2634
+ * const settings = persisted(new IndexedDbStorage(), 'settings', {
2635
+ * initial: DEFAULTS,
2636
+ * revive: raw => (isSettings(raw) ? raw : null)
2637
+ * });
2638
+ *
2639
+ * See `PersistedState` for what a screen sees before the read lands
2640
+ * and what each kind of storage failure does.
2641
+ */
2642
+ function persisted(adapter, key, options) {
2643
+ return new PersistedState(adapter, key, options);
2644
+ }
2645
+ //#endregion
2646
+ export { InternalState as $, printPropValue as A, channel as B, captureConsole as C, formatNodePath as D, formatAge as E, applyPatch as F, mutate as G, viewKeys as H, applyPatches as I, select as J, Resource as K, diffProjection as L, provide as M, findUnplainPath as N, formatNodeReport as O, requirePlainData as P, structurallyEqual as Q, isChannelClientMessage as R, workerHandle as S, describeStream as T, debounced as U, defineChannel as V, throttled as W, computed as X, ComputedCell as Y, derive as Z, isHubMessage as _, undoable as a, output as at, portHandle as b, classifyStorageError as c, storageReadValue as d, internalState as et, pick as f, APPLICATION_WORKER as g, serveChannels as h, OpfsStorage as i, isOutputTarget as it, ProvidedChannel as j, formatStream as k, storageErrorMessage as l, serve as m, persisted as n, input as nt, UndoStack as o, outputTargetOf as ot, pickKeys as p, resource as q, IndexedDbStorage as r, into as rt, MemoryStorage as s, withBodyOf as st, PersistedState as t, InputCell as tt, storageReadFailure as u, isPortErrorMessage as v, isConsoleEntryMessage as w, servePorts as x, isPortHandshake as y, isChannelHostMessage as z };
2647
+
2648
+ //# sourceMappingURL=persisted-Ddb51avc.js.map