@abide/abide 0.123.1 → 0.123.3

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 (98) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/abide.ts +36 -65
  3. package/docs/REGISTRY.md +2 -2
  4. package/docs/RULEBOOK.md +2 -2
  5. package/docs/abide.md +2 -2
  6. package/package.json +1 -1
  7. package/src/compiler/Diagnostics.ts +198 -0
  8. package/src/compiler/KeptChecker.ts +97 -0
  9. package/src/compiler/Lowering.ts +1 -1
  10. package/src/compiler/PositionedRefusal.ts +1 -1
  11. package/src/compiler/TypeSession.ts +62 -85
  12. package/src/compiler/appLayout.ts +28 -20
  13. package/src/compiler/argsDefaultsIn.ts +1 -1
  14. package/src/compiler/checkApp.ts +64 -68
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/compileApp.ts +101 -70
  17. package/src/compiler/declaredExports.ts +2 -4
  18. package/src/compiler/emitClient.ts +1 -1
  19. package/src/compiler/formatAbide.ts +2 -2
  20. package/src/compiler/handlersUnder.ts +17 -29
  21. package/src/compiler/lineOf.ts +0 -19
  22. package/src/compiler/matchersOf.ts +3 -3
  23. package/src/compiler/mountsOf.ts +3 -3
  24. package/src/compiler/readSources.ts +16 -18
  25. package/src/compiler/readsIn.ts +1 -1
  26. package/src/compiler/serveLanguage.ts +23 -57
  27. package/src/compiler/sharedModuleWrites.ts +1 -1
  28. package/src/compiler/templateWarnings.ts +1 -2
  29. package/src/compiler/trackingWarnings.ts +1 -1
  30. package/src/server/CLIENT_BUILD.ts +4 -32
  31. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  32. package/src/server/answerRequest.ts +11 -300
  33. package/src/server/answerRpc.ts +12 -54
  34. package/src/server/answerSocket.ts +8 -35
  35. package/src/server/appBuild.ts +377 -0
  36. package/src/server/appPlugins.ts +15 -17
  37. package/src/server/appTree.ts +6 -5
  38. package/src/server/argsContract.ts +209 -0
  39. package/src/server/bridgeRelay.ts +153 -65
  40. package/src/server/claudeCode.ts +30 -127
  41. package/src/server/config.ts +6 -7
  42. package/src/server/createApp.ts +30 -64
  43. package/src/server/devWorker.ts +11 -15
  44. package/src/server/handlerFileMount.ts +40 -0
  45. package/src/server/index.ts +2 -2
  46. package/src/server/packageDirectory.ts +14 -0
  47. package/src/server/preload.ts +3 -2
  48. package/src/server/principal.ts +4 -3
  49. package/src/server/remote.ts +1 -1
  50. package/src/server/runCompiled.ts +1 -1
  51. package/src/server/runDesktop.ts +1 -1
  52. package/src/server/seedBuffer.ts +1 -1
  53. package/src/server/serveDev.ts +24 -58
  54. package/src/server/servedApp.ts +390 -0
  55. package/src/server/signInChanged.ts +20 -0
  56. package/src/server/surfaceOf.ts +2 -1
  57. package/src/server/versionOf.ts +6 -15
  58. package/src/shared/applyPatch.ts +1 -1
  59. package/src/shared/muxFrames.ts +1 -1
  60. package/src/shared/reactive/Keyed.ts +1 -1
  61. package/src/shared/reactive/Reactive.ts +21 -8
  62. package/src/shared/reactive/argsContract.ts +131 -4
  63. package/src/shared/reactive/index.ts +1 -0
  64. package/src/shared/reactive/patchLog.ts +79 -0
  65. package/src/shared/reactive/patchMarks.ts +34 -0
  66. package/src/shared/reactive/recordWrites.ts +11 -14
  67. package/src/shared/reactive/share.ts +4 -3
  68. package/src/shared/rpc.ts +12 -19
  69. package/src/shared/validateJson.ts +14 -3
  70. package/src/ui/ArmRegion.ts +10 -4
  71. package/src/ui/Level.ts +11 -20
  72. package/src/ui/LevelChain.ts +272 -0
  73. package/src/ui/Router.ts +54 -245
  74. package/src/ui/Scope.ts +17 -3
  75. package/src/ui/TryRegion.ts +22 -23
  76. package/src/ui/bindSpread.ts +2 -4
  77. package/src/ui/listenForRoutes.ts +3 -10
  78. package/src/ui/outboxQueue.ts +21 -3
  79. package/src/ui/reconcile.ts +4 -6
  80. package/src/ui/routing.ts +2 -1
  81. package/src/ui/rows.ts +2 -2
  82. package/src/ui/setAttribute.ts +12 -3
  83. package/src/ui/socketWire.ts +26 -11
  84. package/src/compiler/Finding.ts +0 -2
  85. package/src/compiler/Refusals.ts +0 -65
  86. package/src/compiler/formatDiagnosis.ts +0 -20
  87. package/src/server/appDirectories.ts +0 -9
  88. package/src/server/argsFor.ts +0 -39
  89. package/src/server/buildDirectory.ts +0 -10
  90. package/src/server/coerceArgs.ts +0 -109
  91. package/src/server/derivedArgs.ts +0 -95
  92. package/src/server/runBuild.ts +0 -24
  93. package/src/server/serveBuild.ts +0 -144
  94. package/src/server/testBuild.ts +0 -220
  95. package/src/shared/reactive/FOLLOWED.ts +0 -8
  96. package/src/shared/reactive/PATCHES.ts +0 -11
  97. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  98. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -22,13 +22,18 @@ import { CELL } from './CELL.ts'
22
22
  import { CAUGHT_UP, Cursor, hasCursorBehind, keepPlaces } from './Cursor.ts'
23
23
  import { ENTRY_LIVES } from './ENTRY_LIVES.ts'
24
24
  import { armTimer, type Expiry, expiryFor } from './Expiry.ts'
25
- import { FOLLOWED } from './FOLLOWED.ts'
26
25
  import { isReactive } from './isReactive.ts'
27
26
  import { isThenable } from './isThenable.ts'
28
27
  import { MUTATION } from './MUTATION.ts'
29
28
  import { NO_INITIAL } from './NO_INITIAL.ts'
30
29
  import { NotLanded } from './NotLanded.ts'
31
- import { PATCH_SINKS, type PatchOp } from './PATCH_SINKS.ts'
30
+ import {
31
+ holdLog,
32
+ logOf,
33
+ PatchLog,
34
+ type PatchOp,
35
+ sendInvalidate,
36
+ } from './patchLog.ts'
32
37
  import { REACTIVE_BRAND } from './REACTIVE_BRAND.ts'
33
38
  import { recordWrites } from './recordWrites.ts'
34
39
  import { currentScope, runInScope, type Scope } from './scoping.ts'
@@ -3522,7 +3527,7 @@ function patchCell(cell: Cell, mutate: (value: unknown) => void): void {
3522
3527
  // served: patching the served one overwrote the held production with it, and the write
3523
3528
  // the window held was never seen.
3524
3529
  const head = cap?.holding && cell.body === null ? cap.held : cell.value
3525
- recordWrites(head, mutate, PATCH_SINKS.get(cell))
3530
+ recordWrites(head, mutate, logOf(cell))
3526
3531
  // 5.7: REPLACES THE HEAD PRODUCTION RATHER THAN APPENDING ONE, so the ordinal does not
3527
3532
  // move: the follow's version tells a patch from a production by exactly that, and sends a
3528
3533
  // patch as its writes (18.11). A cursor reads the change off the object's patch epoch
@@ -3542,10 +3547,8 @@ function patchCell(cell: Cell, mutate: (value: unknown) => void): void {
3542
3547
  const before = stream.chunks.length
3543
3548
  // A CURSOR BEHIND needs where the patch wrote, which its ops say: the follow's own sink, or one
3544
3549
  // of this patch's alone where nothing follows the value.
3545
- const followed = PATCH_SINKS.get(cell)
3546
3550
  const sink =
3547
- followed ??
3548
- (hasCursorBehind(cell) ? { ops: [], lost: false } : undefined)
3551
+ logOf(cell) ?? (hasCursorBehind(cell) ? new PatchLog() : undefined)
3549
3552
  const from = sink === undefined ? 0 : sink.ops.length
3550
3553
  recordWrites(stream.chunks, mutate, sink)
3551
3554
  // 5.7 AS THE VALUE BRANCH KEEPS IT: a patch mints no ordinal, so an accumulation it grew or
@@ -3681,8 +3684,7 @@ function reloadOwed(cell: Cell): void {
3681
3684
  */
3682
3685
  function invalidateOne(cell: Cell): void {
3683
3686
  // 7.6 — sent on to the browsers following it whether or not there is a producer to mark.
3684
- const followed = FOLLOWED.get(cell)
3685
- if (followed !== undefined) for (const sendOn of followed) sendOn()
3687
+ sendInvalidate(cell)
3686
3688
  if (!reloads(cell)) return
3687
3689
  // Not directly servable, so the next read goes a frame in and settles the debt.
3688
3690
  cell.direct = false
@@ -4493,6 +4495,17 @@ export function servedCellOf(value: unknown): { readonly ordinal: number } {
4493
4495
  return adoptedCellOf(value)
4494
4496
  }
4495
4497
 
4498
+ /**
4499
+ * Holds a patch log on the cell `value` serves from, with `invalidated` called on its every
4500
+ * invalidate; returns what releases both. 18.11, 7.10.
4501
+ */
4502
+ export function followServed(
4503
+ value: unknown,
4504
+ invalidated: () => void,
4505
+ ): () => void {
4506
+ return holdLog(adoptedCellOf(value), invalidated)
4507
+ }
4508
+
4496
4509
  /**
4497
4510
  * When the production `value` serves stops being servable, as a wall-clock time.
4498
4511
  *
@@ -1,7 +1,9 @@
1
1
  // WHAT A KEYED VALUE'S ARGS ARE HELD TO, past the face an app is handed: the `args` schema it was
2
- // declared with (19.7) and the defaults its body's parameter writes (11.29). A `Keyed` is where the
3
- // contract starts; a server's forwarder and an `Rpc` are faces built over one, and each carries it
4
- // through `contractOf` so a transport reads it off whichever face it was handed.
2
+ // declared with (19.7), the defaults its body's parameter writes (11.29), and — on a server's `Rpc`
3
+ // — the schema its annotation derives (19.9). A `Keyed` is where the contract starts; a server's
4
+ // forwarder and an `Rpc` are faces built over one, and each carries it through `contractOf` so a
5
+ // transport reads it off whichever face it was handed. What a server adds before a call off the wire
6
+ // reaches here — its text coerced, its path's parameters laid in — is `#server/argsContract.ts`.
5
7
  //
6
8
  // DEFAULTS BEFORE THE KEY, IN ONE PLACE. 11.27: a call that left a member to the parameter and one
7
9
  // that wrote it are one entry, so every reach, call and lookup takes the key through `normalized`
@@ -10,9 +12,11 @@
10
12
  //
11
13
  // Imports no part of the core, so `scoped.ts` can carry the contract without reaching `Keyed.ts`.
12
14
 
15
+ import { DERIVED_ARGS } from '../DERIVED_ARGS.ts'
16
+ import type { Issues } from '../Issues.ts'
13
17
  import { putOwn } from '../putOwn.ts'
18
+ import { validationError } from '../refuse.ts'
14
19
  import type { Schema } from '../Schema.ts'
15
- import { canonicalKey } from './canonicalKey.ts'
16
20
  import type { Defaults } from './parameterDefaults.ts'
17
21
 
18
22
  export const ARGS_GATE: unique symbol = Symbol.for('abide.argsGate')
@@ -62,6 +66,35 @@ export function normalized<Args>(
62
66
  return defaults === null ? args : withDefaults(args, defaults)
63
67
  }
64
68
 
69
+ /**
70
+ * 19.9 — the issues an `Rpc`'s args leave against the schema its annotation derives, `null` where
71
+ * they hold: `undefined` until the module its handler is loaded through stamps it (`derivedArgs`),
72
+ * and so in a browser always. A function rather than the schema, so this seam never carries the
73
+ * validator (19.13).
74
+ */
75
+ export type HeldToAnnotation = {
76
+ [DERIVED_ARGS]: ((args: unknown) => Issues<unknown> | null) | undefined
77
+ }
78
+
79
+ /**
80
+ * The args a call of `face` is made with: `normalized`, then held to the schema its annotation
81
+ * derives. Throws the `ValidationError` where they do not hold.
82
+ *
83
+ * 15.8, 19.9 — ONE STEP FOR EVERY WAY A HANDLER IS CALLED, in process and over the wire alike: a
84
+ * type an in-process caller wrote holds nothing at run time (`as never`, a value off JSON), so it is
85
+ * refused as the wire refuses it rather than handed to the body. 11.27 — the defaults first, so the
86
+ * check reads what the handler will.
87
+ */
88
+ export function acceptedArgs(
89
+ face: Pick<Gated<unknown>, typeof PARAMETER_DEFAULTS> & HeldToAnnotation,
90
+ given: unknown,
91
+ ): unknown {
92
+ const args = normalized(face, given)
93
+ const issues = face[DERIVED_ARGS]?.(args) ?? null
94
+ if (issues !== null) throw validationError(issues)
95
+ return args
96
+ }
97
+
65
98
  /** The entry key a call with `args` is filed under: `normalized`, then 11.24's canonical form. */
66
99
  export function keyOf(
67
100
  face: Pick<Gated<unknown>, typeof PARAMETER_DEFAULTS>,
@@ -95,3 +128,97 @@ function withDefaults<Args>(args: Args, defaults: Defaults): Args {
95
128
  }
96
129
  return (filled ?? given) as Args
97
130
  }
131
+
132
+ /**
133
+ * The entry key for an `Args` value: its canonical wire form, as a string.
134
+ *
135
+ * RULEBOOK 11.24: members sorted by name, `undefined` members dropped, `Date` written ISO.
136
+ *
137
+ * 11.26: the key is taken BEFORE any gate runs, because a browser holds no copy of the gate — a
138
+ * key taken after normalizing is computed one way on a server and another in a browser, and every
139
+ * handler with a normalizing schema would miss its seed buffer and re-issue the call on hydration.
140
+ *
141
+ * 11.64: WHAT THE WIRE FORM CANNOT WRITE IS REFUSED AT THE REACH. `JSON.stringify` does not fail
142
+ * on any of it — a `Set`, a `Map` and a class instance each have no own enumerable keys and
143
+ * canonicalize to `{}`, and a non-finite number is written `null` — so two different reaches
144
+ * produced one key and 1.8 joined the second to the first's standing work. The value was right
145
+ * for the first caller and wrong for every other one, with nothing to see.
146
+ */
147
+ export function canonicalKey(args: unknown): string {
148
+ if (args === undefined) return ''
149
+ return JSON.stringify(canonical(args, []))
150
+ }
151
+
152
+ // THE PATH IS A STACK OF STEPS, joined only when a refusal names it. Built as a string per member
153
+ // it was a concatenation for every element and every nested key on every reach, for a message the
154
+ // success path never reads.
155
+ type Steps = (string | number)[]
156
+
157
+ function canonical(value: unknown, path: Steps): unknown {
158
+ if (value === null) return value
159
+ const kind = typeof value
160
+ if (kind === 'number') {
161
+ if (!Number.isFinite(value)) throw outside(String(value), path)
162
+ return value
163
+ }
164
+ if (kind !== 'object') {
165
+ if (kind === 'boolean' || kind === 'string') return value
166
+ // `undefined` reaches here from an ARRAY element alone — a member is dropped above.
167
+ throw outside(kind === 'undefined' ? 'undefined' : `a ${kind}`, path)
168
+ }
169
+ if (value instanceof Date) return value.toISOString()
170
+ if (Array.isArray(value)) {
171
+ const out: unknown[] = []
172
+ for (let index = 0; index < value.length; index += 1) {
173
+ path.push(index)
174
+ out.push(canonical(value[index], path))
175
+ path.pop()
176
+ }
177
+ return out
178
+ }
179
+ const prototype = Object.getPrototypeOf(value)
180
+ if (prototype !== Object.prototype && prototype !== null)
181
+ throw outside(named(value as object), path)
182
+ const source = value as Record<string, unknown>
183
+ const keys = Object.keys(source).sort()
184
+ // AN ORDINARY OBJECT, AND `putOwn` FOR THE ONE KEY THAT NEEDS IT: `__proto__` is a SETTER on
185
+ // `Object.prototype`, so `out[key] = …` for that key creates no own property and
186
+ // `JSON.stringify` never sees it — `{id: 1, __proto__: {role: 'admin'}}` and `{id: 1}` then
187
+ // canonicalize to one string, and 1.8 joins the second reach to the first's standing work.
188
+ // `Object.create(null)` closed that too, and V8 builds it in DICTIONARY mode: the shipped key
189
+ // took 49-56% of the time without it there, and a standing keyed reach 53%, where JSC reads
190
+ // level to 110% (`abide/bench/sharedCases.ts`, two runs per engine).
191
+ const out: Record<string, unknown> = {}
192
+ for (let index = 0; index < keys.length; index += 1) {
193
+ const key = keys[index] as string
194
+ const member = source[key]
195
+ if (member === undefined) continue
196
+ path.push(key)
197
+ putOwn(out, key, canonical(member, path))
198
+ path.pop()
199
+ }
200
+ return out
201
+ }
202
+
203
+ function named(value: object): string {
204
+ const from = value.constructor as { name?: unknown } | undefined
205
+ return typeof from?.name === 'string' && from.name !== ''
206
+ ? `a ${from.name}`
207
+ : 'a class instance'
208
+ }
209
+
210
+ function outside(what: string, steps: Steps): Error {
211
+ let path = ''
212
+ for (const step of steps)
213
+ path +=
214
+ typeof step === 'number'
215
+ ? `[${step}]`
216
+ : path === ''
217
+ ? step
218
+ : `.${step}`
219
+ return new Error(
220
+ `An Args key cannot carry ${what} at ${
221
+ path === '' ? 'the arguments themselves' : `"${path}"`
222
+ }: the canonical wire form writes it as another entry's key (RULEBOOK 11.64).`,
223
+ )
224
+ }
@@ -47,6 +47,7 @@ export {
47
47
  deadlineOf,
48
48
  failedTransiently,
49
49
  flushSync,
50
+ followServed,
50
51
  heldGenerationOf,
51
52
  type Identity,
52
53
  isStreaming,
@@ -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
+ }
@@ -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
 
@@ -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
  }
package/src/shared/rpc.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // handler's own.
15
15
 
16
16
  import { DERIVED_ARGS } from './DERIVED_ARGS.ts'
17
- import { type Failed, isFailedNamed } from './Failed.ts'
17
+ import { type Failed, isFailed, isFailedNamed } from './Failed.ts'
18
18
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
19
19
  import { HandlerRoute } from './HandlerRoute.ts'
20
20
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
@@ -23,11 +23,12 @@ import { compose, type Rung } from './Middleware.ts'
23
23
  import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
24
24
  import { principalRotated } from './principalRotated.ts'
25
25
  import {
26
+ acceptedArgs,
26
27
  contractOf,
27
28
  type Gated,
28
29
  gateOf,
30
+ type HeldToAnnotation,
29
31
  keyOf,
30
- normalized,
31
32
  } from './reactive/argsContract.ts'
32
33
  import { callLoad } from './reactive/callLoad.ts'
33
34
  import {
@@ -52,7 +53,7 @@ import { MUTATION } from './reactive/MUTATION.ts'
52
53
  import { dropAnswered } from './reactive/Reactive.ts'
53
54
  import { builtHere, isForwarder } from './reactive/scoped.ts'
54
55
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
55
- import { checkArgs, validationError } from './refuse.ts'
56
+ import { checkArgs } from './refuse.ts'
56
57
  import { RPC_SERVER } from './RPC_SERVER.ts'
57
58
  import { rpcRequest } from './rpcRequest.ts'
58
59
  import type { Schema } from './Schema.ts'
@@ -162,16 +163,6 @@ type Derived<Member, Whole = Member> = Member extends
162
163
  : never
163
164
  : never
164
165
 
165
- /**
166
- * 19.9 — the issues an `Rpc`'s args leave against the schema its annotation derives, `null` where
167
- * they hold: `undefined` until the module its handler is loaded through stamps it (`derivedArgs`),
168
- * and so in a browser always. A function rather than the schema, so this seam never carries the
169
- * validator (19.13).
170
- */
171
- export type HeldToAnnotation = {
172
- [DERIVED_ARGS]: ((args: unknown) => Issues<unknown> | null) | undefined
173
- }
174
-
175
166
  /** Which generated surfaces carry a handler (20.1). */
176
167
  export type Clients = {
177
168
  ui?: boolean
@@ -453,12 +444,14 @@ export function declare(method: Method): Declare {
453
444
  return route
454
445
  }
455
446
  const call = (given?: unknown, options?: CallOptions): unknown => {
456
- const args = normalized(contract, given)
457
- // 15.8, 19.9 — AND HELD TO ITS ANNOTATION, where the build derived a schema off it: a
458
- // type an in-process caller wrote holds nothing at run time (`as never`, a value off
459
- // JSON), so the call is refused as the wire refuses it rather than handed to the body.
460
- const issues = rpc[DERIVED_ARGS]?.(args) ?? null
461
- if (issues !== null) return reactive(() => validationError(issues))
447
+ // 15.8, 19.9 — the args every way of calling the handler is held to.
448
+ let args: unknown
449
+ try {
450
+ args = acceptedArgs(rpc, given)
451
+ } catch (refused) {
452
+ if (!isFailed(refused)) throw refused
453
+ return reactive(() => refused)
454
+ }
462
455
  const route =
463
456
  rpc.address === undefined ? undefined : routeAt(rpc.address)
464
457
  if (route?.segments !== undefined) {
@@ -486,6 +486,17 @@ function checkArray(
486
486
  check(items, value[index], join(path, String(index)), found)
487
487
  }
488
488
 
489
+ /**
490
+ * Whether `value` carries `key` as JSON would: an own member, and not one left `undefined`.
491
+ *
492
+ * 19.25 — `undefined` IS NOT A JSON VALUE, so a member holding it is the member the wire leaves out:
493
+ * JSON drops it and a URL omits it (30.28). Read as present, `note?: string` refused a call in process
494
+ * passing `note: undefined` at 422 that the same call over the wire was answered for.
495
+ */
496
+ function holds(value: Record<string, unknown>, key: string): boolean {
497
+ return value[key] !== undefined && Object.hasOwn(value, key)
498
+ }
499
+
489
500
  function checkObject(
490
501
  node: Node,
491
502
  value: Record<string, unknown>,
@@ -504,7 +515,7 @@ function checkObject(
504
515
  // `required: ['toString']` was satisfied by `{}`, and — the one that matters —
505
516
  // `'__proto__' in declared` answered true, so `additionalProperties: false` accepted
506
517
  // `{"__proto__": …}` as a declared member and handed the object on unchecked.
507
- if (!Object.hasOwn(value, key))
518
+ if (!holds(value, key))
508
519
  noteOr(found, join(path, key), 'is required')
509
520
  }
510
521
  }
@@ -515,7 +526,7 @@ function checkObject(
515
526
  if (!Object.hasOwn(declared, key)) continue
516
527
  // An absent member is `required`'s business, not its schema's — checking it
517
528
  // here would report every optional member as the wrong type.
518
- if (!Object.hasOwn(value, key)) continue
529
+ if (!holds(value, key)) continue
519
530
  check(
520
531
  declared[key] as JsonValue,
521
532
  value[key],
@@ -527,7 +538,7 @@ function checkObject(
527
538
  const extra = node.additionalProperties
528
539
  if (extra === undefined) return
529
540
  for (const key in value) {
530
- if (!Object.hasOwn(value, key)) continue
541
+ if (!holds(value, key)) continue
531
542
  if (declared !== null && Object.hasOwn(declared, key)) continue
532
543
  if (extra === false) {
533
544
  noteOr(found, join(path, key), 'is not a known member')
@@ -88,10 +88,7 @@ export class ArmRegion {
88
88
 
89
89
  swap(next: unknown): void {
90
90
  const region = this.region
91
- const filling =
92
- this.at !== undefined ||
93
- isEmptyRegion(region) ||
94
- this.paintedOther(next)
91
+ const filling = this.fills(next)
95
92
  const arm = this.armOf(next)
96
93
  const scope = this.scope
97
94
  if (filling) {
@@ -102,6 +99,15 @@ export class ArmRegion {
102
99
  this.at = next
103
100
  }
104
101
 
102
+ /** Whether swapping in `next` fills the region rather than hydrating what the server painted. */
103
+ fills(next: unknown): boolean {
104
+ return (
105
+ this.at !== undefined ||
106
+ isEmptyRegion(this.region) ||
107
+ this.paintedOther(next)
108
+ )
109
+ }
110
+
105
111
  armOf(next: unknown): Arm {
106
112
  return (this.arms[next as number] as ArmMakers[number])(undefined)
107
113
  }
package/src/ui/Level.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  currentShare,
3
+ enterShare,
3
4
  leaveShare,
4
- resumeShare,
5
5
  type ShareScope,
6
6
  } from '#shared/reactive/share.ts'
7
7
  import { untracked } from '#shared/reactive/tracking.ts'
@@ -14,18 +14,14 @@ import type { Scope } from './Scope.ts'
14
14
  /**
15
15
  * ONE LEVEL OF A ROUTED CHAIN — a layout or the page — and the head it contributes (39.1, 39.14).
16
16
  *
17
- * 41.11 — the router holds these rather than letting each level own the one below it, which is
18
- * what makes a shared layout something it can KEEP: a scope stopped from above takes its children
19
- * with it, and a layout that survives a navigation must not take the page it was holding a moment
20
- * ago. The module standing here is the router's `chain` at the same index.
17
+ * 41.11 — a `LevelChain` holds these rather than letting each level own the one below it.
21
18
  *
22
19
  * ON EVERY PAGE'S STATIC CLOSURE, because `contributeHead` is on the runtime barrel — so it knows
23
- * nothing of the router, and the router knows it only through `levels`.
20
+ * the chain it stands in only as what reconciles the head, and nothing of the router.
24
21
  */
25
22
  export class Level {
26
- /** The chain this level stands in, and where: a live head yields to every level below it. */
27
- levels: Level[]
28
- at: number
23
+ /** The chain this level stands in: a live head yields to every level below it. */
24
+ chain: { reconcile(): void }
29
25
  scope: Scope | undefined = undefined
30
26
  /**
31
27
  * Where this level's `<slot/>` is, learned when the level below it mounted there, and
@@ -64,9 +60,8 @@ export class Level {
64
60
  */
65
61
  contributed = new Set<HeadContribution>()
66
62
 
67
- constructor(levels: Level[], at: number) {
68
- this.levels = levels
69
- this.at = at
63
+ constructor(chain: { reconcile(): void }) {
64
+ this.chain = chain
70
65
  }
71
66
 
72
67
  /** Left by the level's mount (`emitClient`), painted or not. */
@@ -85,7 +80,7 @@ export class Level {
85
80
 
86
81
  /** Reconciles the document head from the chain this level stands in (`reconcileHead`). */
87
82
  reconcile(): void {
88
- Level.reconcile(this.levels)
83
+ this.chain.reconcile()
89
84
  }
90
85
 
91
86
  /**
@@ -94,11 +89,11 @@ export class Level {
94
89
  * a head read here is followed by its own watch, not by whatever wrote.
95
90
  */
96
91
  static reconcile(levels: Level[]): void {
97
- reconcileHead(untracked(Level.chain, levels, undefined))
92
+ reconcileHead(untracked(Level.contributors, levels, undefined))
98
93
  }
99
94
 
100
95
  /** Every contributor in `levels` and the head it reads now, outermost first. */
101
- static chain(
96
+ static contributors(
102
97
  levels: Level[],
103
98
  _?: undefined,
104
99
  ): [who: object, text: string][] {
@@ -146,11 +141,7 @@ export class Level {
146
141
  mount<Result>(run: () => Result): Result {
147
142
  // 39.1 — THE LEVEL RIDES THE SHARE SCOPE, so a component finds it as it finds a shared key,
148
143
  // whether it mounts now or in an arm or a row after (`contributeHead`).
149
- const back = resumeShare({
150
- parent: currentShare(),
151
- values: new Map(),
152
- level: this,
153
- })
144
+ const back = enterShare(this)
154
145
  this.unapplied = false
155
146
  let unapplied: boolean
156
147
  let result: Result