@abide/abide 0.123.2 → 0.124.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.
Files changed (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -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,32 +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
- heldGenerationOf,
51
47
  type Identity,
52
- isStreaming,
53
- loadOf,
54
48
  moveItem,
55
- patchInto,
56
- produceInto,
57
49
  type Reactive,
58
50
  type ReactiveOptions,
59
- refuseInto,
60
51
  rowItem,
61
52
  type Store,
62
- servedCellOf,
63
- standIn,
64
- streamingOrSettled,
65
53
  type Transformer,
66
- writtenLocally,
67
54
  } from './Reactive.ts'
68
55
  export { STREAMING } from './STREAMING.ts'
69
56
  export {
@@ -550,7 +537,8 @@ export function refresh<Chosen extends Selection>(
550
537
  (selection as { refresh(pattern: unknown): void }).refresh(pattern)
551
538
  else {
552
539
  refreshOver(sweep)
553
- RPC_SERVER.swept?.('refresh', sweep)
540
+ for (const followers of RPC_SERVER.following)
541
+ followers.sweep('refresh', sweep)
554
542
  }
555
543
  }
556
544
  /** Marks the selection stale: drops the cache behind it, never the value. */
@@ -565,7 +553,8 @@ export function invalidate<Chosen extends Selection>(
565
553
  )
566
554
  else {
567
555
  invalidateOver(sweep)
568
- RPC_SERVER.swept?.('invalidate', sweep)
556
+ for (const followers of RPC_SERVER.following)
557
+ followers.sweep('invalidate', sweep)
569
558
  }
570
559
  }
571
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,79 @@
1
+ // 18.11 — WHAT A PATCH WROTE, KEPT AS DATA for a value the server follows over the mux, by the cell
2
+ // it was written to. Only a followed value has a log, so every other patch records nothing but its
3
+ // marks (32.33). And 7.10 — what an invalidate of that value sends on: the core calls the log's
4
+ // followers for every cell it invalidates, however the trigger was spelled, so every other
5
+ // invalidate is one miss.
6
+ //
7
+ // HELD WHILE ANYONE IS FED THE VALUE, and gone with the last: a log nobody drains grows with every
8
+ // patch.
9
+
10
+ /**
11
+ * One JSON Patch operation (RFC 6902), its `path` and `from` JSON Pointers (RFC 6901).
12
+ *
13
+ * A patch abide records is `add`, `remove` and `replace` alone; `move`, `copy` and `test` are
14
+ * applied where a client sends them.
15
+ */
16
+ export type PatchOp =
17
+ | { op: 'add'; path: string; value: unknown }
18
+ | { op: 'remove'; path: string }
19
+ | { op: 'replace'; path: string; value: unknown }
20
+ | { op: 'move'; from: string; path: string }
21
+ | { op: 'copy'; from: string; path: string }
22
+ | { op: 'test'; path: string; value: unknown }
23
+
24
+ /**
25
+ * The writes patches made, as `recordWrites` keeps them. `lost` where one could not be kept as data,
26
+ * and then the change is sent as the value (18.13). `followers` stays empty on a log one patch keeps
27
+ * for itself rather than one the server holds.
28
+ */
29
+ export class PatchLog {
30
+ ops: PatchOp[] = []
31
+ lost = false
32
+ readonly followers = new Set<() => void>()
33
+ }
34
+
35
+ const LOGS = new WeakMap<object, PatchLog>()
36
+
37
+ /** The log held on `cell`, or `undefined` where nothing follows it. */
38
+ export function logOf(cell: object): PatchLog | undefined {
39
+ return LOGS.get(cell)
40
+ }
41
+
42
+ /**
43
+ * Holds a log on `cell`, with `invalidated` called on its every invalidate; returns what releases
44
+ * both.
45
+ */
46
+ export function holdLog(cell: object, invalidated: () => void): () => void {
47
+ let log = LOGS.get(cell)
48
+ if (log === undefined) {
49
+ log = new PatchLog()
50
+ LOGS.set(cell, log)
51
+ }
52
+ const held = log
53
+ const followers = held.followers
54
+ followers.add(invalidated)
55
+ // A release run twice, after another follower opened a fresh log, leaves that log standing.
56
+ return () => {
57
+ followers.delete(invalidated)
58
+ if (followers.size === 0 && LOGS.get(cell) === held) LOGS.delete(cell)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The writes kept on `cell` since the last drain, or `null` where there are none or one was lost —
64
+ * and the log starts over either way.
65
+ */
66
+ export function drainLog(cell: object): PatchOp[] | null {
67
+ const log = LOGS.get(cell)
68
+ if (log === undefined) return null
69
+ const ops = log.lost || log.ops.length === 0 ? null : log.ops
70
+ log.ops = []
71
+ log.lost = false
72
+ return ops
73
+ }
74
+
75
+ /** Calls every follower of `cell`'s log: an invalidate reached it. 7.6. */
76
+ export function sendInvalidate(cell: object): void {
77
+ const log = LOGS.get(cell)
78
+ if (log !== undefined) for (const sendOn of log.followers) sendOn()
79
+ }
@@ -0,0 +1,34 @@
1
+ // 32.33 — WHICH OBJECTS AN `s.patch` WROTE TO, AND WHEN. The epoch moves once per patch and each
2
+ // object written holds the epoch it was last written at, so a row that took its item at epoch `e` is
3
+ // stale exactly where its item was written since `e`. No imports, because `reconcile` and the
4
+ // socket wire read it on the client and must not drag the core into the runtime chunk with it.
5
+
6
+ let epoch = 0
7
+ const WRITTEN = new WeakMap<object, number>()
8
+
9
+ /** The epoch the last patch wrote at, 0 before any. */
10
+ export function patchEpoch(): number {
11
+ return epoch
12
+ }
13
+
14
+ /** Opens a patch: moves the epoch, and returns the one its writes are marked at. */
15
+ export function openPatch(): number {
16
+ epoch += 1
17
+ return epoch
18
+ }
19
+
20
+ /** Marks `written` as written by the patch at `at`. */
21
+ export function markWritten(written: object, at: number): void {
22
+ WRITTEN.set(written, at)
23
+ }
24
+
25
+ /** The epoch a patch last wrote to `value` at, 0 where none has or it is not an object. */
26
+ export function writtenAt(value: unknown): number {
27
+ if (value === null || typeof value !== 'object') return 0
28
+ return WRITTEN.get(value) ?? 0
29
+ }
30
+
31
+ /** Whether a patch wrote to `value` after epoch `since`. */
32
+ export function writtenSince(value: unknown, since: number): boolean {
33
+ return writtenAt(value) > since
34
+ }
@@ -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
+ }
@@ -22,8 +22,8 @@
22
22
  // holds it directly: `list.push(row); row.name = x`. So a placed object is SENT AS IT STANDS WHEN THE
23
23
  // CALLBACK RETURNS — every op's value is copied then rather than at its write — and a write made
24
24
  // under one is not kept, since that copy already carries it.
25
- import type { PatchOp } from './PATCH_SINKS.ts'
26
- import { PATCHES } from './PATCHES.ts'
25
+ import type { PatchLog, PatchOp } from './patchLog.ts'
26
+ import { markWritten, openPatch } from './patchMarks.ts'
27
27
  import { RESHAPING } from './RESHAPING.ts'
28
28
 
29
29
  // The reshaping methods JSON Patch has no op for: the array they leave is sent whole instead.
@@ -39,7 +39,7 @@ const REORDERING: ReadonlySet<string> = new Set([
39
39
  const TARGET = Symbol('target')
40
40
 
41
41
  /**
42
- * Runs `mutate` over a proxy of `root`, marking every object it writes to in `PATCHES`.
42
+ * Runs `mutate` over a proxy of `root`, marking every object it writes to (`patchMarks.ts`).
43
43
  *
44
44
  * Where a `sink` is handed in, each write is also kept there as a `PatchOp`, or the sink is marked
45
45
  * `lost` where it cannot be. A `root` that is not an array or a plain object is handed over as is.
@@ -47,7 +47,7 @@ const TARGET = Symbol('target')
47
47
  export function recordWrites<Value>(
48
48
  root: Value,
49
49
  mutate: (value: Value) => void,
50
- sink?: { ops: PatchOp[]; lost: boolean },
50
+ sink?: PatchLog,
51
51
  ): void {
52
52
  if (!isWrappable(root)) {
53
53
  // A root no proxy can stand in for is written as a whole: what the callback reads out of it
@@ -56,14 +56,12 @@ export function recordWrites<Value>(
56
56
  mutate(root)
57
57
  return
58
58
  }
59
- PATCHES.epoch += 1
60
- PATCHES.written.set(root, PATCHES.epoch)
61
- mutate(heldView(root, PATCHES.epoch) as Value)
59
+ const epoch = openPatch()
60
+ markWritten(root, epoch)
61
+ mutate(heldView(root, epoch) as Value)
62
62
  return
63
63
  }
64
- PATCHES.epoch += 1
65
- const epoch = PATCHES.epoch
66
- const written = PATCHES.written
64
+ const epoch = openPatch()
67
65
  // EVERY OBJECT THE PATCH REACHED, by what it is: the one proxy it is handed out as, so
68
66
  // `list[0] === list[0]` inside the callback, and its parent. ONE ENTRY rather than a map for each,
69
67
  // because a row read is the loop this runs per row and each map is a lookup and an insert there.
@@ -83,7 +81,7 @@ export function recordWrites<Value>(
83
81
  const entry = reached.get(held) as Reached
84
82
  if (entry.marked) return
85
83
  entry.marked = true
86
- written.set(held, epoch)
84
+ markWritten(held, epoch)
87
85
  const above = entry.parent
88
86
  if (above instanceof Parents) for (const one of above.all) mark(one)
89
87
  else mark(above)
@@ -428,7 +426,7 @@ class Reached {
428
426
  proxy: object | null
429
427
  parent: object | Parents | null
430
428
  /**
431
- * Written in this patch, directly or below — `PATCHES.written` holds this patch's epoch for it.
429
+ * Written in this patch, directly or below — its mark is this patch's epoch.
432
430
  */
433
431
  marked: boolean
434
432
  /** What one no proxy can stand in for is handed out as: `heldView` of it. */
@@ -556,8 +554,7 @@ function* markedEach(
556
554
  }
557
555
 
558
556
  function markedOut<Value>(value: Value, epoch: number): Value {
559
- if (typeof value === 'object' && value !== null)
560
- PATCHES.written.set(value, epoch)
557
+ if (typeof value === 'object' && value !== null) markWritten(value, epoch)
561
558
  return value
562
559
  }
563
560
 
@@ -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
+ }
@@ -64,14 +64,15 @@ export function share<Key extends string, Value>(
64
64
  * Opens a share scope around an emitted component's body, and returns the previous one.
65
65
  *
66
66
  * The PREVIOUS scope comes back rather than the new one: restoring is the whole of the close
67
- * (`leaveShare`), and a component that threw still has to restore it.
67
+ * (`leaveShare`), and a component that threw still has to restore it. `level` is handed in by a
68
+ * routed level opening its own scope (39.1); every other scope carries its parent's down.
68
69
  */
69
- export function enterShare(): ShareScope | null {
70
+ export function enterShare(level?: unknown): ShareScope | null {
70
71
  const previous = CURRENT
71
72
  CURRENT = {
72
73
  parent: previous,
73
74
  values: new Map(),
74
- level: previous === null ? undefined : previous.level,
75
+ level: level ?? previous?.level,
75
76
  }
76
77
  return previous
77
78
  }
@@ -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