@abide/abide 0.123.3 → 0.124.1

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.
Files changed (91) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +38 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +29 -16
  5. package/docs/abide.md +999 -459
  6. package/package.json +1 -1
  7. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  8. package/src/compiler/LoweredFile.ts +609 -0
  9. package/src/compiler/Lowering.ts +46 -4
  10. package/src/compiler/SHARED_NAMES.ts +2 -0
  11. package/src/compiler/ShadowHost.ts +670 -0
  12. package/src/compiler/ShadowMap.ts +1 -1
  13. package/src/compiler/TypeSession.ts +71 -1715
  14. package/src/compiler/appLints.ts +109 -0
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/classifyRead.ts +16 -1
  17. package/src/compiler/compileApp.ts +2 -2
  18. package/src/compiler/declaredExports.ts +44 -17
  19. package/src/compiler/diagnoseSources.ts +232 -0
  20. package/src/compiler/formatAbide.ts +5 -6
  21. package/src/compiler/handlersUnder.ts +16 -35
  22. package/src/compiler/projectShadow.ts +1 -0
  23. package/src/compiler/reactiveReads.ts +235 -0
  24. package/src/compiler/reactiveTypes.ts +13 -0
  25. package/src/compiler/readsIn.ts +180 -81
  26. package/src/compiler/routesDeclaration.ts +46 -0
  27. package/src/compiler/scaffold.ts +7 -2
  28. package/src/compiler/serveLanguage.ts +26 -20
  29. package/src/compiler/splitImports.ts +2 -2
  30. package/src/server/BRIDGE_VERSION.ts +1 -1
  31. package/src/server/DesktopSession.ts +213 -0
  32. package/src/server/Followers.ts +399 -0
  33. package/src/server/MODULE_QUERIES.ts +6 -2
  34. package/src/server/PageRender.ts +201 -0
  35. package/src/server/abideModules.ts +7 -7
  36. package/src/server/answerFailure.ts +3 -2
  37. package/src/server/answerRequest.ts +6 -70
  38. package/src/server/answerRpc.ts +48 -260
  39. package/src/server/answerSocket.ts +41 -463
  40. package/src/server/appKit.ts +1 -1
  41. package/src/server/argsContract.ts +1 -1
  42. package/src/server/createApp.ts +8 -2
  43. package/src/server/csp.ts +1 -1
  44. package/src/server/deferredBuilds.ts +3 -16
  45. package/src/server/faceOf.ts +1 -1
  46. package/src/server/generatedSurfaces.ts +2 -1
  47. package/src/server/handlerFileMount.ts +1 -1
  48. package/src/server/index.ts +1 -1
  49. package/src/server/installServer.ts +5 -9
  50. package/src/server/invokeHandler.ts +230 -0
  51. package/src/server/mountHandlers.ts +184 -0
  52. package/src/server/openDesktopWindow.ts +308 -0
  53. package/src/server/preload.ts +18 -0
  54. package/src/server/principal.ts +1 -1
  55. package/src/server/remote.ts +1 -1
  56. package/src/server/render.ts +31 -59
  57. package/src/server/requestScope.ts +5 -36
  58. package/src/server/runDesktop.ts +19 -447
  59. package/src/server/seedBuffer.ts +11 -11
  60. package/src/server/servedApp.ts +1 -1
  61. package/src/server/shell.ts +1 -1
  62. package/src/server/surfaceOf.ts +1 -1
  63. package/src/server/versionOf.ts +1 -1
  64. package/src/shared/RPC_SERVER.ts +27 -31
  65. package/src/shared/Schema.ts +1 -2
  66. package/src/shared/index.ts +1 -1
  67. package/src/shared/{note.ts → issueReport.ts} +22 -0
  68. package/src/shared/loadCall.ts +66 -0
  69. package/src/shared/reactive/Keyed.ts +23 -15
  70. package/src/shared/reactive/Reactive.ts +46 -1002
  71. package/src/shared/reactive/byOf.ts +2 -5
  72. package/src/shared/reactive/callLoad.ts +8 -14
  73. package/src/shared/reactive/cellShape.ts +734 -0
  74. package/src/shared/reactive/index.ts +12 -24
  75. package/src/shared/reactive/pairReactive.ts +5 -3
  76. package/src/shared/reactive/producer.ts +121 -0
  77. package/src/shared/reactive/scoped.ts +3 -2
  78. package/src/shared/reactive/selections.ts +209 -0
  79. package/src/shared/reactive/watch.ts +3 -4
  80. package/src/shared/rpc.ts +8 -5
  81. package/src/shared/validateJson.ts +1 -2
  82. package/src/shared/writePath.ts +29 -0
  83. package/src/ui/Router.ts +53 -69
  84. package/src/ui/outboxQueue.ts +2 -8
  85. package/src/ui/outcomeOf.ts +120 -0
  86. package/src/ui/socketWire.ts +3 -3
  87. package/src/ui/wire.ts +9 -10
  88. package/src/shared/LOAD_OFFLINE.ts +0 -15
  89. package/src/shared/LOAD_SIGNAL.ts +0 -10
  90. package/src/shared/issuesOf.ts +0 -18
  91. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
@@ -1,8 +1,8 @@
1
1
  // THE ONE FACTORY, and the barrel it is the address of. `state`, `memo` and `channel`
2
- // collapsed into overloads on one name — a channel being a value declaring `history` — and the
3
- // refusal behind that split survives rather than being reversed: what it refused was ONE
4
- // SIGNATURE WITH AN OPTIONAL PARAMETER, not one name, and overloads handing back two result
5
- // types is the shape its reason forces.
2
+ // collapsed into overloads on one name — a channel being a value over an array its `patch`
3
+ // appends to — and the refusal behind that split survives rather than being reversed: what it
4
+ // refused was ONE SIGNATURE WITH AN OPTIONAL PARAMETER, not one name, and overloads handing
5
+ // back two result types is the shape its reason forces.
6
6
  //
7
7
  // THE ORDER IS FORCED, and getting it wrong breaks the design silently: `() => C` is
8
8
  // assignable to `(args: A) => C` but not the reverse, so keyed-first swallows the
@@ -38,33 +38,19 @@ import { share } from './share.ts'
38
38
  export { isReactive } from './isReactive.ts'
39
39
  export { isKeyed, type Keyed } from './Keyed.ts'
40
40
  export { NotLanded } from './NotLanded.ts'
41
- // The server reaches the core through here rather than `./Reactive.ts`: `Reactive.ts` →
42
- // `log.ts` → this barrel → `scoped.ts` → `Reactive.ts` is a cycle, and a server module loaded
43
- // first that imports the core directly meets `MEMBER_KEYS` before it is initialized.
41
+ // The rest of the framework reaches the core through here rather than `./Reactive.ts`: `Reactive.ts` →
42
+ // `log.ts` → this barrel → `scoped.ts` → `Reactive.ts` is a cycle, and a module loaded first that
43
+ // imports the core directly meets `MEMBER_KEYS` before it is initialized. The verbs a producer
44
+ // outside the core spends a value through are `producer.ts`'s, which comes through here first.
44
45
  export {
45
- abandonLoad,
46
- chunksOfStream,
47
- deadlineOf,
48
- failedTransiently,
49
46
  flushSync,
50
- followServed,
51
- heldGenerationOf,
52
47
  type Identity,
53
- isStreaming,
54
- loadOf,
55
48
  moveItem,
56
- patchInto,
57
- produceInto,
58
49
  type Reactive,
59
50
  type ReactiveOptions,
60
- refuseInto,
61
51
  rowItem,
62
52
  type Store,
63
- servedCellOf,
64
- standIn,
65
- streamingOrSettled,
66
53
  type Transformer,
67
- writtenLocally,
68
54
  } from './Reactive.ts'
69
55
  export { STREAMING } from './STREAMING.ts'
70
56
  export {
@@ -551,7 +537,8 @@ export function refresh<Chosen extends Selection>(
551
537
  (selection as { refresh(pattern: unknown): void }).refresh(pattern)
552
538
  else {
553
539
  refreshOver(sweep)
554
- RPC_SERVER.swept?.('refresh', sweep)
540
+ for (const followers of RPC_SERVER.following)
541
+ followers.sweep('refresh', sweep)
555
542
  }
556
543
  }
557
544
  /** Marks the selection stale: drops the cache behind it, never the value. */
@@ -566,7 +553,8 @@ export function invalidate<Chosen extends Selection>(
566
553
  )
567
554
  else {
568
555
  invalidateOver(sweep)
569
- RPC_SERVER.swept?.('invalidate', sweep)
556
+ for (const followers of RPC_SERVER.following)
557
+ followers.sweep('invalidate', sweep)
570
558
  }
571
559
  }
572
560
 
@@ -9,6 +9,7 @@
9
9
  // callable it adopted.
10
10
 
11
11
  import { CELL } from './CELL.ts'
12
+ import type { Callable } from './cellShape.ts'
12
13
  import { reactive } from './index.ts'
13
14
  import { brandedPrototype, type Reactive } from './Reactive.ts'
14
15
 
@@ -32,9 +33,10 @@ const PAIR_PROTOTYPE = brandedPrototype({
32
33
  */
33
34
  export function pairReactive(pair: Pair): Reactive<unknown> {
34
35
  const over = reactive(() => pair.get())
35
- const callable = (over as unknown as { [CELL]: { callable: unknown } })[
36
- CELL
37
- ].callable as { [PAIR]: Pair }
36
+ const callable = (over as unknown as Callable)[CELL]
37
+ .callable as unknown as {
38
+ [PAIR]: Pair
39
+ }
38
40
  Object.setPrototypeOf(callable, PAIR_PROTOTYPE)
39
41
  callable[PAIR] = pair
40
42
  return over as Reactive<unknown>
@@ -0,0 +1,121 @@
1
+ // THE PRODUCER PORT: every verb a producer OUTSIDE the core spends a value through — the wire and
2
+ // the outbox writing what the server produced into a copy (`produceInto`, `patchInto`,
3
+ // `refuseInto`), a call ending or timing the load it reached (`loadOf`, `abandonLoad`), the server
4
+ // reading what a value serves to stamp, cache and follow it (`servedCellOf`, `deadlineOf`,
5
+ // `followServed`). None of them is a member an app can reach; this module is where the framework's
6
+ // other seams reach them, rather than the core itself or the app-facing barrel.
7
+ //
8
+ // What reads only the cell is here; what has to move the graph — a production, a refusal, a
9
+ // superseded load — is the core's own, and this module hands it on.
10
+ //
11
+ // THROUGH THE BARREL FIRST, for the cycle `index.ts` names: a module reaching this port ahead of
12
+ // the barrel would enter `Reactive.ts` first, and `scoped.ts` would meet `MEMBER_KEYS` before it is
13
+ // initialized. `isReactive` is taken from the barrel, ahead of `Keyed.ts` and `Reactive.ts`, for
14
+ // that reason — `cellShape.ts` has no edge into the cycle, so its place does not matter.
15
+
16
+ import { CELL } from './CELL.ts'
17
+ import { type Callable, type Cell, cellOf } from './cellShape.ts'
18
+ import { isReactive } from './index.ts'
19
+ import { entryDeadline } from './Keyed.ts'
20
+ import { holdLog } from './patchLog.ts'
21
+ import { ownStream, type Reactive } from './Reactive.ts'
22
+
23
+ export {
24
+ abandonLoad,
25
+ chunksOfStream,
26
+ dropAnswered,
27
+ failedTransiently,
28
+ patchInto,
29
+ produceInto,
30
+ refuseInto,
31
+ standIn,
32
+ streamingOrSettled,
33
+ } from './Reactive.ts'
34
+
35
+ /** Whether `value` holds what was written here, without subscribing — for a caller that is not a reader. */
36
+ export function writtenLocally(value: Reactive<unknown>): boolean {
37
+ return cellOf(value).local
38
+ }
39
+
40
+ /**
41
+ * The cell `value` serves from, through every adoption: its identity and its ordinal, and nothing
42
+ * else of it the server can reach.
43
+ *
44
+ * 11.12: an adopting value serves the adopted value's production, and an adoption of an adoption
45
+ * goes on down — so a follow's version (18.7) and its feed name the cell that actually produced.
46
+ */
47
+ export function servedCellOf(value: unknown): { readonly ordinal: number } {
48
+ return adoptedCellOf(value)
49
+ }
50
+
51
+ /**
52
+ * Holds a patch log on the cell `value` serves from, with `invalidated` called on its every
53
+ * invalidate; returns what releases both. 18.11, 7.10.
54
+ */
55
+ export function followServed(
56
+ value: unknown,
57
+ invalidated: () => void,
58
+ ): () => void {
59
+ return holdLog(adoptedCellOf(value), invalidated)
60
+ }
61
+
62
+ /**
63
+ * When the production `value` serves stops being servable, as a wall-clock time.
64
+ *
65
+ * 21.8 — WHEN THE PRODUCTION A VALUE IS SERVING STOPS BEING SERVABLE, as a wall-clock time: its
66
+ * own `ttl` from when it was minted (6.5), or its entry's from when the entry last produced
67
+ * (11.45), through whatever it adopts. Infinity where neither bounds it.
68
+ */
69
+ export function deadlineOf(value: unknown): number {
70
+ const cell = adoptedCellOf(value)
71
+ if (cell.expiry !== null) return cell.expiry.at + cell.expiry.ms
72
+ return entryDeadline(cell)
73
+ }
74
+
75
+ /**
76
+ * The generation of the load `value` has in flight, or `undefined`.
77
+ *
78
+ * 16.48 — WHICH LOAD A VALUE HAS IN FLIGHT, as the generation that started it, or `undefined` where
79
+ * none is. Read off the cell rather than through `busy`, which is a probe and would subscribe the
80
+ * caller that asked (3.3).
81
+ *
82
+ * A WINDOW HOLDING A PRODUCTION IS BUSY AND IS NO LOAD (5.15): nothing is in flight for a call to end
83
+ * or time, and the window's close is its own bound. Told apart by the hold, which only a value with
84
+ * neither body nor stream makes — so a write's load taken in under a hold is not one a call times.
85
+ */
86
+ export function loadOf(value: unknown): number | undefined {
87
+ const cell = cellOf(value)
88
+ if (!cell.loading) return undefined
89
+ if (cell.cap?.holding && cell.stream === null) return undefined
90
+ return cell.generation
91
+ }
92
+
93
+ /**
94
+ * The generation `value`'s past stands at, or `undefined` while a load is in flight.
95
+ *
96
+ * What 21.15's `Cached` compares across a call, read through adoption as `servedCellOf` is.
97
+ */
98
+ export function heldGenerationOf(value: unknown): number | undefined {
99
+ const cell = adoptedCellOf(value)
100
+ return cell.loading ? undefined : cell.generation
101
+ }
102
+
103
+ /**
104
+ * Whether `value` is a streaming producer's `Reactive`.
105
+ *
106
+ * A streaming producer's own `Reactive` — a body declared as an async generator, or one whose run
107
+ * handed one back (9.6).
108
+ */
109
+ export function isStreaming(value: unknown): boolean {
110
+ if (!isReactive(value)) return false
111
+ const stream = (value as Partial<Callable>)[CELL]?.stream
112
+ return stream !== null && stream !== undefined
113
+ }
114
+
115
+ function adoptedCellOf(value: unknown): Cell {
116
+ let cell = cellOf(value)
117
+ // A stream's own `adoption` is a write it follows (9.11), and its past is its own.
118
+ while (cell.adoption !== null && ownStream(cell) === null)
119
+ cell = cell.adoption.source
120
+ return cell
121
+ }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { contractOf, type Gated } from './argsContract.ts'
16
16
  import { CELL } from './CELL.ts'
17
+ import type { Callable, Cell } from './cellShape.ts'
17
18
  import { KEYED_BRAND } from './KEYED_BRAND.ts'
18
19
  import { brandedPrototype, MEMBER_KEYS, signalMoved } from './Reactive.ts'
19
20
  import { currentScope, runInScope, SCOPING } from './scoping.ts'
@@ -101,8 +102,8 @@ for (const key of MEMBER_KEYS)
101
102
  }
102
103
  const FORWARDING_PROTOTYPE = brandedPrototype(forwarders)
103
104
  Object.defineProperty(FORWARDING_PROTOTYPE, CELL, {
104
- get(this: Forwarder): unknown {
105
- return instanceOf(this[FILED])[CELL]
105
+ get(this: Forwarder): Cell {
106
+ return (instanceOf(this[FILED]) as unknown as Callable)[CELL]
106
107
  },
107
108
  })
108
109
 
@@ -0,0 +1,209 @@
1
+ // THE SELECTION INDEX (13.1, 13.3, 13.6): which values carry which tag, which loads a bare sweep
2
+ // reaches, and the one probe signal per selection a guard subscribes to. Over the cell's `tags`,
3
+ // `ref`, `filed`, `scope` and `global` and nothing else of it — what a selection DOES to the values
4
+ // it reaches (`busyOver`, `refreshOver`, …) is the graph's, and stays in `Reactive.ts`.
5
+
6
+ import { type Cell, cellOf } from './cellShape.ts'
7
+ import { currentScope } from './scoping.ts'
8
+ import {
9
+ newTracked,
10
+ type Subscriber,
11
+ type Tracked,
12
+ TRACKING,
13
+ trackProbe,
14
+ } from './tracking.ts'
15
+
16
+ /**
17
+ * Each tag to the values carrying it, held weakly.
18
+ *
19
+ * THE INDEX HOLDS VALUES WEAKLY. A tag is a way to find a value, never a reason to keep one, and a
20
+ * strong index would hold every tagged value a page ever made for the life of the process. A
21
+ * collected value is pruned from its sets by the next sweep that finds it gone.
22
+ */
23
+ const TAGGED = new Map<string, Set<WeakRef<Cell>>>()
24
+
25
+ /**
26
+ * Every value whose producer is a LOAD, held weakly — what a bare selection sweeps.
27
+ *
28
+ * 13.6: A BARE SWEEP REACHES PRODUCERS, and of those it files the ones that LOAD: a value holding
29
+ * a `store`, filed where it is built, and a body the first time it starts a load — a pending
30
+ * answer or a stream (`beginLoad`). A synchronous body is a derivation: reloading it recomputes
31
+ * what its sources already say, and whatever load it reads is filed on its own. And filing is
32
+ * what every filed value pays — a weak handle measured ~230 ns a value under V8 against ~20 under
33
+ * JSC — so a value with nothing to load never pays it: a derivation per cell per row stays at
34
+ * what it cost before, and a load pays it beside the promise it already allocates.
35
+ *
36
+ * Held weakly for `TAGGED`'s reason. AN ARRAY, COMPACTED, RATHER THAN A `Set`: a `Set` of weak
37
+ * handles measured ~120 ns a filing under JSC against ~10 for a push. What a `Set` bought was an
38
+ * O(1) delete on eviction; here an evicted value drops its `ref` instead, and a handle whose value
39
+ * no longer points back at it is dead, the same as one whose value was collected. Because a bare
40
+ * sweep may never run, the filing that finds the array past `compactProducersAt` compacts it, and
41
+ * the bar doubles past what survives, so the walk is amortized over the filings between two.
42
+ */
43
+ let filedProducers: WeakRef<Cell>[] = []
44
+ let compactProducersAt = 1024
45
+
46
+ /**
47
+ * The probe signal a guard over a selection subscribes to.
48
+ *
49
+ * 13.3 AND 13.9: ONE SIGNAL PER SELECTION, keyed by its canonical tags. A guard over a selection
50
+ * subscribes to this, never to each entry — the probe edge `trackProbe` makes, from a node that
51
+ * is nothing but that edge.
52
+ */
53
+ const SIGNALS = new Map<string, Tracked>()
54
+ const SIGNALS_BY_TAG = new Map<string, Tracked[]>()
55
+ /**
56
+ * The bare selection's one signal. Not a key in `SIGNALS`: `{ tags: [] }` canonicalizes to the
57
+ * empty set and matches nothing, where the bare selection matches every filed load in reach.
58
+ */
59
+ export const EVERY_PRODUCER = newTracked()
60
+
61
+ function refOf(cell: Cell): WeakRef<Cell> {
62
+ let ref = cell.ref
63
+ if (ref === null) {
64
+ ref = new WeakRef(cell)
65
+ cell.ref = ref
66
+ }
67
+ return ref
68
+ }
69
+
70
+ export function tagCell(cell: Cell): void {
71
+ const ref = refOf(cell)
72
+ for (const tag of cell.tags as string[]) {
73
+ let carrying = TAGGED.get(tag)
74
+ if (carrying === undefined) {
75
+ carrying = new Set()
76
+ TAGGED.set(tag, carrying)
77
+ }
78
+ carrying.add(ref)
79
+ }
80
+ }
81
+
82
+ export function fileProducer(cell: Cell): void {
83
+ cell.filed = true
84
+ filedProducers.push(refOf(cell))
85
+ if (filedProducers.length > compactProducersAt) {
86
+ filedProducers = liveProducers()
87
+ compactProducersAt = Math.max(1024, filedProducers.length * 2)
88
+ }
89
+ }
90
+
91
+ /** The handles whose value is still standing and still filed — neither collected nor evicted. */
92
+ function liveProducers(): WeakRef<Cell>[] {
93
+ const live: WeakRef<Cell>[] = []
94
+ for (let index = 0; index < filedProducers.length; index += 1) {
95
+ const ref = filedProducers[index] as WeakRef<Cell>
96
+ if (ref.deref()?.ref === ref) live.push(ref)
97
+ }
98
+ return live
99
+ }
100
+
101
+ /**
102
+ * Removes an evicted value from every selection index.
103
+ *
104
+ * What an eviction calls: an entry out of its `Keyed` is out of every selection with it, rather
105
+ * than reachable by a sweep until a collection happens to run.
106
+ */
107
+ export function leaveSelections(value: unknown): void {
108
+ const cell = cellOf(value)
109
+ const ref = cell.ref
110
+ if (ref === null) return
111
+ // What takes it out of `filedProducers`: a handle its value no longer points back at is dead.
112
+ cell.ref = null
113
+ if (cell.tags === null) return
114
+ for (const tag of cell.tags) {
115
+ const carrying = TAGGED.get(tag)
116
+ if (carrying === undefined) continue
117
+ carrying.delete(ref)
118
+ if (carrying.size === 0) TAGGED.delete(tag)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Takes every value filed so far out of the bare selection, each filed again by its next load.
124
+ *
125
+ * A TEST'S SEAM, with no caller in the framework — `bareSelection.test.ts` calls it before its
126
+ * first case: a page's process ends with the page, and a test process runs every file before this
127
+ * one in it. What those files left held — a module's `global`
128
+ * value, a follow the page keeps for its life — is in reach of a bare sweep (13.13), and a bare
129
+ * `refresh()` reloads it and reads `busy()` true while it asks.
130
+ */
131
+ export function unfileProducers(): void {
132
+ for (let index = 0; index < filedProducers.length; index += 1) {
133
+ const cell = (filedProducers[index] as WeakRef<Cell>).deref()
134
+ if (cell !== undefined) cell.filed = false
135
+ }
136
+ filedProducers = []
137
+ }
138
+
139
+ /**
140
+ * The values a selection reaches in the current scope, `global` values included: those carrying
141
+ * any of `tags`, or every filed load where `tags` is `null` — the bare selection.
142
+ *
143
+ * 13.8 — A SELECTION IS SCOPE-BOUNDED, and a tag with it, so a selection inside one request never
144
+ * reaches the values another request filed. 13.13 — a `global` value is in every scope, so every
145
+ * selection reaches it, and 13.10 is the bare case of that. In a browser every cell's scope is
146
+ * `undefined`, and so is this.
147
+ */
148
+ export function inReach(tags: string[] | null): Cell[] {
149
+ const scope = currentScope()
150
+ if (tags === null) {
151
+ filedProducers = liveProducers()
152
+ const found: Cell[] = []
153
+ for (let index = 0; index < filedProducers.length; index += 1) {
154
+ const cell = (
155
+ filedProducers[index] as WeakRef<Cell>
156
+ ).deref() as Cell
157
+ if (cell.scope === scope || cell.global) found.push(cell)
158
+ }
159
+ return found
160
+ }
161
+ const found = new Set<Cell>()
162
+ for (const tag of tags) {
163
+ const refs = TAGGED.get(tag)
164
+ if (refs === undefined) continue
165
+ for (const ref of refs) {
166
+ const cell = ref.deref()
167
+ if (cell === undefined) refs.delete(ref)
168
+ else if (cell.scope === scope || cell.global) found.add(cell)
169
+ }
170
+ }
171
+ return [...found]
172
+ }
173
+
174
+ export function trackSignal(tags: string[] | null): void {
175
+ const reader = TRACKING.reader
176
+ if (reader === null) return
177
+ if (tags === null) {
178
+ trackProbe(EVERY_PRODUCER, reader)
179
+ return
180
+ }
181
+ const key = JSON.stringify([...new Set(tags)].sort())
182
+ let signal = SIGNALS.get(key)
183
+ if (signal === undefined) {
184
+ signal = newTracked()
185
+ SIGNALS.set(key, signal)
186
+ for (const tag of tags) {
187
+ const signals = SIGNALS_BY_TAG.get(tag)
188
+ if (signals === undefined) SIGNALS_BY_TAG.set(tag, [signal])
189
+ else signals.push(signal)
190
+ }
191
+ }
192
+ trackProbe(signal, reader)
193
+ }
194
+
195
+ /**
196
+ * Hands `mark` the probe readers of every selection signal over any of `tags` (13.3).
197
+ *
198
+ * `mark` is the core's `markAsked`, passed rather than imported so this index stays off the graph.
199
+ */
200
+ export function markSignals(
201
+ tags: string[],
202
+ mark: (asked: Subscriber[]) => void,
203
+ ): void {
204
+ for (const tag of tags) {
205
+ const signals = SIGNALS_BY_TAG.get(tag)
206
+ if (signals === undefined) continue
207
+ for (const signal of signals) mark(signal.probeReaders)
208
+ }
209
+ }
@@ -15,6 +15,7 @@ import { namedLogger } from '../log.ts'
15
15
  import { UNEXPECTED } from '../UNEXPECTED.ts'
16
16
  import { WORK } from '../WORK.ts'
17
17
  import { CELL } from './CELL.ts'
18
+ import type { Callable } from './cellShape.ts'
18
19
  import { NotLanded } from './NotLanded.ts'
19
20
  import type { Reactive } from './Reactive.ts'
20
21
  import { currentScope, runInScope } from './scoping.ts'
@@ -161,12 +162,10 @@ export function watch(first: unknown, second?: Effect): Disposer {
161
162
  // An app hands over the VALUES; the reader list lives on each one's record. Copied
162
163
  // rather than aliased, because the reader keeps this array for its whole life and the
163
164
  // app's own array is the app's to mutate.
164
- const given = (Array.isArray(first) ? first : [first]) as {
165
- [CELL]: Tracked
166
- }[]
165
+ const given = (Array.isArray(first) ? first : [first]) as Callable[]
167
166
  const sources: Tracked[] = []
168
167
  for (let index = 0; index < given.length; index += 1)
169
- sources.push((given[index] as { [CELL]: Tracked })[CELL])
168
+ sources.push((given[index] as Callable)[CELL])
170
169
  return owned(watchOver(sources, null, second))
171
170
  }
172
171
 
package/src/shared/rpc.ts CHANGED
@@ -35,7 +35,6 @@ import {
35
35
  type AdoptedFailures,
36
36
  type AdoptedProduced,
37
37
  type ChunkOf,
38
- chunksOfStream,
39
38
  handlerValue,
40
39
  isKeyed,
41
40
  type Kept,
@@ -46,11 +45,14 @@ import {
46
45
  type Refusals,
47
46
  type Shaping,
48
47
  STREAMING,
49
- streamingOrSettled,
50
48
  type Streams,
51
49
  } from './reactive/index.ts'
52
50
  import { MUTATION } from './reactive/MUTATION.ts'
53
- import { dropAnswered } from './reactive/Reactive.ts'
51
+ import {
52
+ chunksOfStream,
53
+ dropAnswered,
54
+ streamingOrSettled,
55
+ } from './reactive/producer.ts'
54
56
  import { builtHere, isForwarder } from './reactive/scoped.ts'
55
57
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
56
58
  import { checkArgs } from './refuse.ts'
@@ -532,14 +534,15 @@ export function declare(method: Method): Declare {
532
534
  }) satisfies Gated<unknown>
533
535
  const members = rpc as unknown as Record<string, unknown>
534
536
  // 7.10, 7.18 — A TRIGGER ON A SERVER REACHES EVERY BROWSER HOLDING WHAT IT NAMES, through the
535
- // mux (18.3). `null` in a browser, where the trigger is the copy's own.
537
+ // mux (18.3). None follow in a browser, where the trigger is the copy's own.
536
538
  for (const name of SELECTION_MEMBERS) {
537
539
  if (name === 'busy') continue
538
540
  members[name] =
539
541
  name === 'invalidate' || name === 'refresh'
540
542
  ? (pattern?: unknown): unknown => {
541
543
  const done = faceHere()[name](pattern)
542
- RPC_SERVER.broadcast?.(rpc, name, pattern)
544
+ for (const followers of RPC_SERVER.following)
545
+ followers.broadcast(rpc, name, pattern)
543
546
  return done
544
547
  }
545
548
  : (pattern?: unknown): unknown => faceHere()[name](pattern)
@@ -14,10 +14,9 @@
14
14
  // generated document carries them.
15
15
 
16
16
  import { isRecord } from './isRecord.ts'
17
+ import { issuesOf, note } from './issueReport.ts'
17
18
  import type { Issues } from './Issues.ts'
18
- import { issuesOf } from './issuesOf.ts'
19
19
  import type { JsonValue } from './JsonValue.ts'
20
- import { note } from './note.ts'
21
20
 
22
21
  type Node = { [key: string]: JsonValue }
23
22
 
@@ -93,3 +93,32 @@ function copyOf(value: unknown, step: string | number): Level {
93
93
  return (typeof step === 'number' ? [] : {}) as unknown as Level
94
94
  return { ...(value as object) }
95
95
  }
96
+
97
+ /**
98
+ * What `mutate` hands back, called on a copy of the array at `path` that `target` then holds.
99
+ *
100
+ * 31.9 — A MUTATOR IS A WRITE THROUGH ITS RECEIVER'S PATH. `tags.push(t)` run on the held array
101
+ * grows it in place, so the identity a reader wakes on never moves (31.8's failure, spelled as a
102
+ * call). The compiler lifts a call here only where the receiver's TYPE is an array, so a class's
103
+ * own `push` stays a call on the class. A level that is not plain data on the way (31.14), or a
104
+ * receiver that is not an array when the call runs, takes the call in place, waking nobody.
105
+ */
106
+ export function mutatePath<Result>(
107
+ target: { (): unknown; set(value: unknown): unknown },
108
+ path: readonly (string | number)[],
109
+ mutate: (receiver: never) => Result,
110
+ ): Result {
111
+ const value = target()
112
+ let receiver = value
113
+ let plain = true
114
+ for (const step of path) {
115
+ plain &&= isPlain(receiver)
116
+ receiver = (receiver as Level | null | undefined)?.[step]
117
+ }
118
+ if (!plain) return mutate(receiver as never)
119
+ if (!Array.isArray(receiver)) return mutate(receiver as never)
120
+ const copy = receiver.slice()
121
+ const result = mutate(copy as never)
122
+ target.set(writePath(value, path, copy))
123
+ return result
124
+ }