@abide/abide 0.105.0 → 0.106.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 (100) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +375 -87
  5. package/docs/abide.md +7243 -10256
  6. package/package.json +4 -1
  7. package/scaffold/AGENTS.md +2 -0
  8. package/scaffold/src/getGreeting.ts +6 -0
  9. package/scaffold/src/index.abide +3 -1
  10. package/scaffold/src/tests/getGreeting.test.ts +6 -0
  11. package/scaffold/src/tests/index.test.ts +18 -0
  12. package/src/compiler/DOCS_SITE.ts +2 -0
  13. package/src/compiler/Lowering.ts +234 -14
  14. package/src/compiler/PositionTable.ts +14 -7
  15. package/src/compiler/PositionedRefusal.ts +13 -0
  16. package/src/compiler/Refusals.ts +18 -11
  17. package/src/compiler/SHARED_NAMES.ts +2 -0
  18. package/src/compiler/ShadowMap.ts +37 -0
  19. package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
  20. package/src/compiler/TypeSession.ts +410 -31
  21. package/src/compiler/appLayout.ts +18 -2
  22. package/src/compiler/argsSchemaOf.ts +55 -9
  23. package/src/compiler/checkApp.ts +33 -0
  24. package/src/compiler/checkedShadow.ts +36 -22
  25. package/src/compiler/citedClauses.ts +14 -0
  26. package/src/compiler/classifyRead.ts +6 -0
  27. package/src/compiler/classifyTemplate.ts +2 -1
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +304 -103
  31. package/src/compiler/handlersUnder.ts +4 -0
  32. package/src/compiler/invocationProps.ts +1 -1
  33. package/src/compiler/isIgnored.ts +27 -0
  34. package/src/compiler/parseAbide.ts +176 -76
  35. package/src/compiler/projectShadow.ts +30 -6
  36. package/src/compiler/readSources.ts +1 -1
  37. package/src/compiler/readsIn.ts +100 -1
  38. package/src/compiler/renderMarkup.ts +36 -5
  39. package/src/compiler/scaffold.ts +19 -6
  40. package/src/compiler/serveLanguage.ts +156 -21
  41. package/src/compiler/templateContext.ts +150 -0
  42. package/src/compiler/tokenize.ts +6 -0
  43. package/src/server/BRIDGE_VERSION.ts +1 -1
  44. package/src/server/Inbox.ts +8 -0
  45. package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
  46. package/src/server/MOUNTABLE.ts +6 -0
  47. package/src/server/abideModules.ts +8 -1
  48. package/src/server/answerRequest.ts +3 -1
  49. package/src/server/answerRpc.ts +26 -12
  50. package/src/server/answerSocket.ts +26 -1
  51. package/src/server/argsFor.ts +4 -3
  52. package/src/server/assistant.ts +19 -13
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +7 -7
  55. package/src/server/claudeCode.ts +11 -11
  56. package/src/server/commands.ts +73 -9
  57. package/src/server/createApp.ts +41 -7
  58. package/src/server/devWorker.ts +3 -3
  59. package/src/server/generatedSurfaces.ts +37 -4
  60. package/src/server/muxUpgrade.ts +47 -0
  61. package/src/server/preload.ts +13 -2
  62. package/src/server/render.ts +4 -1
  63. package/src/server/runCompiled.ts +1 -0
  64. package/src/server/seedBuffer.ts +31 -1
  65. package/src/server/serveDev.ts +83 -72
  66. package/src/server/surfaceOf.ts +3 -0
  67. package/src/server/testMount.ts +53 -0
  68. package/src/shared/ADDRESSES.ts +1 -1
  69. package/src/shared/ERROR_META.ts +3 -0
  70. package/src/shared/Schema.ts +1 -1
  71. package/src/shared/WORK.ts +14 -12
  72. package/src/shared/applyPatch.ts +1 -1
  73. package/src/shared/boundValue.ts +81 -0
  74. package/src/shared/index.ts +7 -2
  75. package/src/shared/jsonSchema.ts +25 -2
  76. package/src/shared/log.ts +11 -1
  77. package/src/shared/reactive/Keyed.ts +30 -6
  78. package/src/shared/reactive/PATCHES.ts +1 -1
  79. package/src/shared/reactive/Reactive.ts +118 -60
  80. package/src/shared/reactive/canonicalKey.ts +1 -1
  81. package/src/shared/reactive/index.ts +13 -5
  82. package/src/shared/reactive/recordWrites.ts +1 -1
  83. package/src/shared/reactive/watch.ts +26 -4
  84. package/src/shared/rpc.ts +6 -1
  85. package/src/shared/url.ts +7 -3
  86. package/src/shared/validateJson.ts +1 -1
  87. package/src/ui/ArmRegion.ts +2 -2
  88. package/src/ui/List.ts +9 -9
  89. package/src/ui/RawRun.ts +1 -1
  90. package/src/ui/Router.ts +194 -36
  91. package/src/ui/Scope.ts +29 -0
  92. package/src/ui/adoptWalk.ts +3 -3
  93. package/src/ui/applyHead.ts +25 -3
  94. package/src/ui/bindProperty.ts +70 -22
  95. package/src/ui/index.ts +1 -1
  96. package/src/ui/leave.ts +5 -0
  97. package/src/ui/listenForRoutes.ts +14 -3
  98. package/src/ui/reconcile.ts +48 -28
  99. package/src/ui/regions.ts +36 -4
  100. package/src/ui/rows.ts +108 -28
@@ -1394,6 +1394,44 @@ export function signalMoved(signal: Tracked): void {
1394
1394
  schedule()
1395
1395
  }
1396
1396
 
1397
+ /**
1398
+ * A `{#for}` row's item as a cell, read by calling what this hands back (32.67).
1399
+ *
1400
+ * A `Cell` AND NOT A `reactive(value)`. The item is held exactly as the list handed it over — a
1401
+ * `Reactive` item is not adopted (11.61), a function is not run as a body and `undefined` is a
1402
+ * value rather than nothing landed (2.2) — and a `Cell` rather than a leaner record, so a derived
1403
+ * value that reads a row keeps one element type in its `sources` for every walk over them.
1404
+ *
1405
+ * NO BRAND: the accessor is the row's own, and nothing it is handed to should take it for a value.
1406
+ */
1407
+ export function rowItem(value: unknown): () => unknown {
1408
+ const cell = new Cell('row', null, undefined, undefined, false)
1409
+ serve(cell, value)
1410
+ // A ROW ITEM IS ALWAYS DIRECT — it reads nothing, so no mark ever reaches it, and it carries no
1411
+ // expiry, stream or store — so the read is the tracking and the field.
1412
+ const self = function readItem(): unknown {
1413
+ const reader = TRACKING.reader
1414
+ if (reader !== null) {
1415
+ const at = reader.cursor
1416
+ if (reader.sources[at] === cell) reader.cursor = at + 1
1417
+ else track(cell, reader)
1418
+ }
1419
+ return cell.value
1420
+ } as unknown as (() => unknown) & Callable
1421
+ self[CELL] = cell
1422
+ return self
1423
+ }
1424
+
1425
+ /**
1426
+ * Writes `value` into a row's item and wakes what reads it — the same object included, which is
1427
+ * what a patch that wrote inside it is (32.33).
1428
+ */
1429
+ export function moveItem(item: () => unknown, value: unknown): void {
1430
+ const cell = (item as unknown as Callable)[CELL]
1431
+ cell.value = value
1432
+ cascade(cell)
1433
+ }
1434
+
1397
1435
  /**
1398
1436
  * Marks a production's readers and schedules the effects it reached.
1399
1437
  *
@@ -1420,68 +1458,54 @@ function cascade(cell: Cell): void {
1420
1458
  }
1421
1459
 
1422
1460
  /**
1423
- * Runs the queued effects, abandoning the queue if one throws.
1461
+ * Runs the queued effects to the end of the queue, reporting a throw rather than stopping at it.
1462
+ *
1463
+ * THE DRAIN, BEHIND ITS OWN FRAME. A `try` in `cascade` is a `try` on every write, and the arm
1464
+ * that needs it is the one that actually has an effect to run — so the guard is up there and the
1465
+ * handler is down here. The `try` is ENTERED ONCE PER THROW, not once per effect: the inner loop is
1466
+ * the drain, and a throw re-enters it at the next slot.
1424
1467
  *
1425
- * THE DRAIN, BEHIND ITS OWN FRAME. A `try`/`finally` in `cascade` is a `try`/`finally`
1426
- * on every write, and the arm that needs it is the one that actually has an effect to
1427
- * run — so the guard is up there and the handler is down here.
1468
+ * 12.7 — THE DRAIN FINISHES. What reaches the catch is an effect that already stopped its own
1469
+ * watch (12.8) and was reported (12.6); unwinding the queue over it left every sibling the drain
1470
+ * had not reached unrun on the write that woke them. `reportError` is the throw surfacing as
1471
+ * uncaught — the window's `error` event and the console, with the thrown value as thrown — without
1472
+ * the unwind, and in the same turn, where a `queueMicrotask` re-throw would put it after effects
1473
+ * that ran later than it did.
1428
1474
  */
1429
1475
  function drainPending(): void {
1430
1476
  draining = true
1431
- try {
1432
- // Re-read `pendingCount` per turn: an effect that writes appends to the queue
1433
- // this loop is walking, and those belong to this drain rather than the next write.
1434
- while (pendingAt < pendingCount) {
1435
- const reader = PENDING[pendingAt] as Subscriber
1436
- // THE SLOT IS RELEASED, THE ARRAY IS NOT. Holding the capacity is what keeps a
1437
- // push a plain indexed store, and a slot left pointing at a drained subscriber
1438
- // holds its `notify` — and through it the effect body and the whole scope that
1439
- // body closed over — until another effect happens to land on that index. A
1440
- // disposed `watch` stayed reachable to the next high-water mark, which on a page
1441
- // is every node the torn-down component built. One store per effect ALREADY RUN,
1442
- // against a body call.
1443
- PENDING[pendingAt] = null
1444
- pendingAt += 1
1445
- // A DIRECT READER OF WHAT WAS WRITTEN is `DIRTY`, which is the overwhelming
1446
- // shape and asks nothing: there is no source to interrogate, because the
1447
- // mark says one of them already produced.
1448
- if (reader.state === DIRTY && reader.alive) {
1449
- reader.state = CLEAN
1450
- reader.notify()
1451
- continue
1477
+ // Re-read `pendingCount` per turn: an effect that writes appends to the queue this loop is
1478
+ // walking, and those belong to this drain rather than the next write.
1479
+ while (pendingAt < pendingCount) {
1480
+ try {
1481
+ while (pendingAt < pendingCount) {
1482
+ const reader = PENDING[pendingAt] as Subscriber
1483
+ // THE SLOT IS RELEASED, THE ARRAY IS NOT. Holding the capacity is what keeps a
1484
+ // push a plain indexed store, and a slot left pointing at a drained subscriber
1485
+ // holds its `notify` — and through it the effect body and the whole scope that
1486
+ // body closed over — until another effect happens to land on that index. A
1487
+ // disposed `watch` stayed reachable to the next high-water mark, which on a page
1488
+ // is every node the torn-down component built. One store per effect ALREADY RUN,
1489
+ // against a body call. Released BEFORE the run, so a throw resumes past it.
1490
+ PENDING[pendingAt] = null
1491
+ pendingAt += 1
1492
+ // A DIRECT READER OF WHAT WAS WRITTEN is `DIRTY`, which is the overwhelming
1493
+ // shape and asks nothing: there is no source to interrogate, because the
1494
+ // mark says one of them already produced.
1495
+ if (reader.state === DIRTY && reader.alive) {
1496
+ reader.state = CLEAN
1497
+ reader.notify()
1498
+ continue
1499
+ }
1500
+ resolveEffect(reader)
1452
1501
  }
1453
- resolveEffect(reader)
1454
- }
1455
- } finally {
1456
- // The queue is abandoned rather than carried into the next write: an effect that
1457
- // threw has already stopped its own watch (12.8), and a half-drained queue
1458
- // outliving the throw would run it against a later write's marks. The tail is
1459
- // released for the reason the loop above releases each slot; on the path that did
1460
- // not throw the cursor has already reached the count and this walks nothing.
1461
- //
1462
- // THE MARK GOES WITH THE QUEUE, and that is what abandoning has to mean. A reader
1463
- // left standing at `DIRTY` with nothing holding it takes `markReaders`' own
1464
- // `state >= wanted` short-circuit on every later write, so it is never queued
1465
- // again — one effect throwing left every sibling it had not reached permanently
1466
- // deaf, silently, while the value went on being right. Back to `CLEAN` is the
1467
- // state the next write's mark can reach.
1468
- //
1469
- // AN EFFECT ONLY, THOUGH. The queue is not effects alone: `markReaders` also pushes a
1470
- // derived CELL on 2.13's aborted-body path, and for one of those `state` is not a
1471
- // reminder to run — it IS the pull the next read takes (`state !== CLEAN` is what sends
1472
- // a read a frame in). Forced back to `CLEAN` it is not deaf, it is settled: the read
1473
- // path skips `resolve`, the aborted body is never rerun and the `ready()` guard that
1474
- // queued it answers false for the life of the page — undoing, on any throw anywhere in
1475
- // the drain, exactly what that push was added to repair.
1476
- for (let index = pendingAt; index < pendingCount; index += 1) {
1477
- const abandoned = PENDING[index] as Subscriber
1478
- PENDING[index] = null
1479
- if (abandoned.isEffect) abandoned.state = CLEAN
1502
+ } catch (thrown) {
1503
+ reportError(thrown)
1480
1504
  }
1481
- pendingAt = 0
1482
- pendingCount = 0
1483
- draining = false
1484
1505
  }
1506
+ pendingAt = 0
1507
+ pendingCount = 0
1508
+ draining = false
1485
1509
  }
1486
1510
 
1487
1511
  /**
@@ -3095,7 +3119,7 @@ function walkBusy(cell: Cell, pass: number): boolean {
3095
3119
  * never reads `s()` — so it subscribes to NOTHING, and the value landing wakes nobody. It
3096
3120
  * is stuck on the spinner for the life of the page, with the value sitting right there.
3097
3121
  *
3098
- * ON ITS OWN EDGE, which is 3.3's "that probe alone" and is bought by what the other
3122
+ * ON ITS OWN EDGE, which is 3.3's "and MUST NOT subscribe it to the value", bought by what the other
3099
3123
  * spelling costs the DOM. Subscribing a probe reader through the VALUE edge works and is
3100
3124
  * twenty lines lighter — and then a load STARTING wakes every reader of the value, which
3101
3125
  * on a fifty-row list is fifty `.data` writes with the same string in them. Measured with
@@ -3610,7 +3634,7 @@ function readStore(cell: Cell, persisted: Persisted): unknown {
3610
3634
  } catch (failed) {
3611
3635
  warnStore(
3612
3636
  cell,
3613
- 'could not be read, and is taken as a miss (8.15)',
3637
+ 'could not be read, and is taken as a miss. RULEBOOK 8.15',
3614
3638
  failed,
3615
3639
  )
3616
3640
  return undefined
@@ -3657,7 +3681,7 @@ function restoreFirst(
3657
3681
  if (cell.generation !== generation) return
3658
3682
  warnStore(
3659
3683
  cell,
3660
- 'could not be read, and is taken as a miss (8.15)',
3684
+ 'could not be read, and is taken as a miss. RULEBOOK 8.15',
3661
3685
  failed,
3662
3686
  )
3663
3687
  settleRestore(cell, persisted, undefined, initial)
@@ -3759,7 +3783,7 @@ function rereadStore(cell: Cell, persisted: Persisted): void {
3759
3783
  if (cell.generation !== generation) return
3760
3784
  warnStore(
3761
3785
  cell,
3762
- 'could not be read, and is taken as a miss (8.15)',
3786
+ 'could not be read, and is taken as a miss. RULEBOOK 8.15',
3763
3787
  failed,
3764
3788
  )
3765
3789
  settleReread(cell, persisted, undefined)
@@ -3820,7 +3844,11 @@ function writeStore(cell: Cell, persisted: Persisted, value: unknown): void {
3820
3844
  }
3821
3845
 
3822
3846
  function storeWriteFailed(cell: Cell, failed: unknown): void {
3823
- warnStore(cell, 'write failed, and the store stays attached (8.12)', failed)
3847
+ warnStore(
3848
+ cell,
3849
+ 'write failed, and the store stays attached. RULEBOOK 8.12',
3850
+ failed,
3851
+ )
3824
3852
  UNEXPECTED.report?.(failed)
3825
3853
  }
3826
3854
 
@@ -4269,6 +4297,36 @@ export function loadOf(value: unknown): number | undefined {
4269
4297
  return cell.generation
4270
4298
  }
4271
4299
 
4300
+ // The cells a mutation's call has been answered from, so the next call knows the answer is spent.
4301
+ const ANSWERED = new WeakSet<object>()
4302
+
4303
+ /**
4304
+ * Drops what an unkeyed mutation's last call was answered with, where it is past its deadline.
4305
+ *
4306
+ * 16.7 — A MUTATION CALLED AGAIN IS A SECOND WRITE. Its `ttl` of 0 puts what the last call answered
4307
+ * past its deadline as it lands, and 6.3's timer drops it — but a call in the same turn reaches it
4308
+ * before the timer does, so the call takes the timer's drop itself and its await reloads (6.6).
4309
+ *
4310
+ * ONLY AN ANSWER A CALL WAS GIVEN: reaching a value is what starts its first run, and a request's
4311
+ * own instance (11.32) starts there — that run is this call's, not a spent one. A load in flight is
4312
+ * joined (1.8), and a `ttl` the handler declared still holds.
4313
+ */
4314
+ export function dropAnswered(value: unknown): void {
4315
+ const cell = (value as Callable)[CELL]
4316
+ if (!ANSWERED.has(cell)) {
4317
+ ANSWERED.add(cell)
4318
+ return
4319
+ }
4320
+ const expiry = cell.expiry
4321
+ if (
4322
+ expiry !== null &&
4323
+ !cell.loading &&
4324
+ standingLive(cell, expiry) &&
4325
+ Date.now() - expiry.at >= expiry.ms
4326
+ )
4327
+ dropStanding(cell, expiry)
4328
+ }
4329
+
4272
4330
  /**
4273
4331
  * The generation `value`'s past stands at, or `undefined` while a load is in flight.
4274
4332
  *
@@ -58,7 +58,7 @@ function canonical(value: unknown, path: Steps): unknown {
58
58
  // canonicalize to one string, and 1.8 joins the second reach to the first's standing work.
59
59
  // `Object.create(null)` closed that too, and V8 builds it in DICTIONARY mode: the shipped key
60
60
  // took 49-56% of the time without it there, and a standing keyed reach 53%, where JSC reads
61
- // level to 110% (`harness/scripts/sharedCases.ts`, two runs per engine).
61
+ // level to 110% (`abide/bench/sharedCases.ts`, two runs per engine).
62
62
  const out: Record<string, unknown> = {}
63
63
  for (let index = 0; index < keys.length; index += 1) {
64
64
  const key = keys[index] as string
@@ -49,10 +49,12 @@ export {
49
49
  type Identity,
50
50
  isStreaming,
51
51
  loadOf,
52
- servedCellOf,
52
+ moveItem,
53
53
  type Reactive,
54
54
  type ReactiveOptions,
55
+ rowItem,
55
56
  type Store,
57
+ servedCellOf,
56
58
  streamingOrSettled,
57
59
  type Transformer,
58
60
  } from './Reactive.ts'
@@ -308,21 +310,27 @@ export function reactive<
308
310
  >,
309
311
  AdoptedProduced<ReturnType<Body>, Kept<Shaped>>
310
312
  >
311
- /** A computed value: `body` produces it, and runs again when a value it read changes. */
313
+ /**
314
+ * A computed value: `body` produces it, and runs again when a value it read changes.
315
+ *
316
+ * 19.24 — `Computed` IS READ OFF THE BODY ALONE, hence `NoInfer` in the options: the checker infers
317
+ * through `Awaited`, so a `Schema<{ id: number }>` beside an async body put `{ id: number }` in
318
+ * `Computed` and the body's promise no longer fit it.
319
+ */
312
320
  export function reactive<
313
321
  Computed,
314
322
  Shaped = Awaited<Computed>,
315
323
  Gate = Schema<Awaited<Computed>>,
316
324
  >(
317
325
  body: () => Computed,
318
- options?: Shaping<Awaited<Computed>, Shaped, never, Gate>,
326
+ options?: Shaping<Awaited<NoInfer<Computed>>, Shaped, never, Gate>,
319
327
  ): Reactive<
320
328
  Kept<Shaped>,
321
329
  Awaited<Computed>,
322
330
  AdoptedFailures<Computed> | Refusals<Shaped, Gate, Awaited<Computed>>,
323
331
  AdoptedProduced<Computed, Kept<Shaped>>
324
332
  >
325
- /** A keyed value: `body` declares a parameter, and each `Args` key reached is its own entry. */
333
+ /** A keyed value: `body` declares a parameter, and each `Args` key reached is its own entry. `NoInfer` for 19.24, as above. */
326
334
  export function reactive<
327
335
  Computed,
328
336
  Args,
@@ -331,7 +339,7 @@ export function reactive<
331
339
  ArgsGate = Schema<Args>,
332
340
  >(
333
341
  body: (args: Args) => Computed,
334
- options?: Shaping<Awaited<Computed>, Shaped, Args, Gate, ArgsGate>,
342
+ options?: Shaping<Awaited<NoInfer<Computed>>, Shaped, Args, Gate, ArgsGate>,
335
343
  ): Keyed<
336
344
  Kept<Shaped>,
337
345
  Args,
@@ -7,7 +7,7 @@
7
7
  // slots a proxy cannot stand in for — `Map.prototype.set` refuses a proxy as its receiver — so one
8
8
  // reached is marked written the moment it is read, path and all, and each object a `Map` or a `Set`
9
9
  // hands out is marked as it leaves (`heldView`). That over-reports, and an over-report costs a row
10
- // remade where an under-report shows a stale one.
10
+ // its bindings rerun where an under-report shows a stale one.
11
11
  //
12
12
  // 18.11 — AND WHERE A SINK IS HANDED IN, each write is kept as data too, as JSON Patch (RFC 6902) at
13
13
  // the JSON Pointer it was reached by: a member set is `add`, an index `replace` (or `add` one past
@@ -48,9 +48,11 @@ function reportThrow(thrown: unknown, message: string): void {
48
48
  /**
49
49
  * Reports an effect's throw, then re-throws it in a browser.
50
50
  *
51
- * 12.7 — A BROWSER HAS NO `onError`, so the throw goes on out to surface as an uncaught error. A
52
- * server's has been handed it, and re-thrown there it would fail the handler or the render whose
53
- * write woke this effect — a second report of one failure, against code that did not fail.
51
+ * 12.7 — A BROWSER HAS NO `onError`, so the throw goes on out to surface as an uncaught error: to
52
+ * the caller of `watch` on a first run, and on a rerun to the drain, which reports it and runs the
53
+ * rest of its queue (`drainPending`). A server's has been handed it, and re-thrown there it would
54
+ * fail the handler or the render whose write woke this effect — a second report of one failure,
55
+ * against code that did not fail.
54
56
  */
55
57
  function effectThrew(thrown: unknown): void {
56
58
  reportThrow(
@@ -305,10 +307,30 @@ export function watchTracked(effect: Effect): Disposer {
305
307
  truncateProbes(reader)
306
308
  }
307
309
  reader.notify()
308
- return (): void => {
310
+ return (signal?: typeof RERUN): void => {
309
311
  if (!reader.alive) return
312
+ if (signal === RERUN) {
313
+ reader.notify()
314
+ return
315
+ }
310
316
  reader.alive = false
311
317
  detach(reader)
312
318
  if (typeof dispose === 'function') runDisposer(dispose)
313
319
  }
314
320
  }
321
+
322
+ // What `rerunWatch` hands a tracked watch's stop: a symbol, so a stop called with any argument a
323
+ // caller happens to pass — an index from `forEach` — still stops.
324
+ const RERUN = Symbol('rerun')
325
+
326
+ /**
327
+ * Runs the watch `stop` was handed back for (`watchTracked`) again now, tracking what it reads this
328
+ * time; a stopped one stays stopped.
329
+ *
330
+ * THE WATCH'S OWN STOP, CALLED WITH A SIGNAL, rather than a handle beside it: what reruns a watch
331
+ * is its reader, which only the stop closes over, and a second handle would cost every watch an
332
+ * allocation for a rerun almost none of them are ever asked for.
333
+ */
334
+ export function rerunWatch(stop: Disposer): void {
335
+ ;(stop as (signal: typeof RERUN) => void)(RERUN)
336
+ }
package/src/shared/rpc.ts CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  streamingOrSettled,
47
47
  } from './reactive/index.ts'
48
48
  import { MUTATION } from './reactive/MUTATION.ts'
49
+ import { dropAnswered } from './reactive/Reactive.ts'
49
50
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
50
51
  import { isForwarder } from './reactive/scoped.ts'
51
52
  import { checkArgs } from './refuse.ts'
@@ -393,6 +394,10 @@ export function declare(method: Method): Declare {
393
394
  return reactive(() => refused)
394
395
  }
395
396
  }
397
+ // 16.7 — a mutation's call is a write each time, even where the last one settled this
398
+ // turn: a keyed value's reach evicts an outlived entry itself, and an unkeyed one's
399
+ // spent answer is dropped here.
400
+ if (cacheOptions !== undefined && !keyed) dropAnswered(value)
396
401
  const reached = callLoad(
397
402
  reachThrough,
398
403
  args,
@@ -494,7 +499,7 @@ export const DELETE: Declare = declare('DELETE')
494
499
  function addressOf(rpc: { address: string | undefined }): string {
495
500
  if (rpc.address === undefined)
496
501
  throw new Error(
497
- "abide: this handler has no address yet. A handler is given one where it is mounted: a handler file's default (16.2), or a `const` in a <script server> (16.70).",
502
+ "abide: this handler has no address yet. A handler is given one where it is mounted: a handler file's default (RULEBOOK 16.2), or a `const` in a <script server> (RULEBOOK 16.70).",
498
503
  )
499
504
  return rpc.address
500
505
  }
package/src/shared/url.ts CHANGED
@@ -35,9 +35,13 @@ export type RestNames<P extends string> =
35
35
  : never
36
36
  : never
37
37
 
38
- export type HasParams<P extends string> = [RequiredNames<P>] extends [never]
39
- ? false
40
- : true
38
+ // DISTRIBUTED over a union, so a value typed as any of several literals (`x as RouteLiteral`) takes
39
+ // its params optionally rather than demanding the union of every member's.
40
+ export type HasParams<P extends string> = P extends string
41
+ ? [RequiredNames<P>] extends [never]
42
+ ? false
43
+ : true
44
+ : never
41
45
 
42
46
  export type HasSegments<P extends string> = [
43
47
  RequiredNames<P> | OptionalNames<P> | RestNames<P>,
@@ -35,7 +35,7 @@ const VERIFIED = new WeakSet<object>()
35
35
  * WHERE AN ISSUE GOES, and `null` is a combinator's SCRATCH ATTEMPT: a branch whose issues are
36
36
  * only ever counted, never read. A record per branch per value was the whole of what `anyOf`,
37
37
  * `oneOf` and `not` allocated beyond the walk itself: 83-90% of the time without it in V8 and
38
- * 78-88% in JSC, and the walk is 86-99% of a gated write (`harness/scripts/sharedCases.ts`).
38
+ * 78-88% in JSC, and the walk is 86-99% of a gated write (`abide/bench/sharedCases.ts`).
39
39
  */
40
40
  type Report = Record<string, string[]> | null
41
41
 
@@ -116,7 +116,7 @@ export class ArmRegion {
116
116
  const painted = namedArm(this.region.open.data)
117
117
  if (painted === undefined || painted === next) return false
118
118
  HYDRATE_LOG.warning(
119
- `abide: the server painted arm ${painted} of this region and the client chose arm ${String(next)}, so the region is rebuilt (41.24).`,
119
+ `abide: the server painted arm ${painted} of this region and the client chose arm ${String(next)}, so the region is rebuilt. RULEBOOK 41.24.`,
120
120
  )
121
121
  return true
122
122
  }
@@ -162,7 +162,7 @@ export class ChosenRegion extends ArmRegion {
162
162
  override paintedOther(next: unknown): boolean {
163
163
  if (next != null) return false
164
164
  HYDRATE_LOG.warning(
165
- 'abide: the server painted a component in this region and the client chose none, so the region is emptied (41.24).',
165
+ 'abide: the server painted a component in this region and the client chose none, so the region is emptied. RULEBOOK 41.24.',
166
166
  )
167
167
  return true
168
168
  }
package/src/ui/List.ts CHANGED
@@ -6,9 +6,9 @@ import { isEmptyRegion, type Region, removeUntil } from './regions.ts'
6
6
  import {
7
7
  dropRow,
8
8
  makeRow,
9
+ paintedRow,
9
10
  type Row,
10
- type RowBuilders,
11
- rowOf,
11
+ type RowSetUp,
12
12
  rowsIn,
13
13
  } from './rows.ts'
14
14
 
@@ -58,13 +58,13 @@ export class List {
58
58
  * names; every other list hands back the one record it was built with, so a row allocates
59
59
  * nothing more.
60
60
  */
61
- setUp: (item: unknown, at: number) => RowBuilders
61
+ setUp: RowSetUp
62
62
 
63
63
  constructor(
64
64
  region: Region,
65
65
  anchored: boolean,
66
66
  key: (item: unknown, at: number) => unknown,
67
- setUp: (item: unknown, at: number) => RowBuilders,
67
+ setUp: RowSetUp,
68
68
  ) {
69
69
  this.region = region
70
70
  this.rows = []
@@ -110,13 +110,13 @@ function stopPump(list: List): void {
110
110
  * read there and only there, so the rows are hydrated against the same items the watch then
111
111
  * compares against. Read twice — once to hydrate and again on the watch's first run — a subject
112
112
  * that builds fresh items (`rows.map(…)`) matched no row by identity, and every row the server sent
113
- * was dropped and remade on load.
113
+ * was written again on load, every binding in it rerun (32.67).
114
114
  */
115
115
  export function listAt(
116
116
  region: Region,
117
117
  anchored: boolean,
118
118
  key: (item: unknown, at: number) => unknown,
119
- setUp: (item: unknown, at: number) => RowBuilders,
119
+ setUp: RowSetUp,
120
120
  ): List {
121
121
  return new List(region, anchored, key, setUp)
122
122
  }
@@ -155,7 +155,7 @@ function hydrate(list: List, items: unknown[]): boolean {
155
155
  if (!agreed) {
156
156
  warnMismatch(
157
157
  new Mismatch(
158
- `abide: the document holds ${found.length} rows where this {#for} computes ${items.length}, so the client and the server disagree about this region's shape (41.10).`,
158
+ `abide: the document holds ${found.length} rows where this {#for} computes ${items.length}, so the client and the server disagree about this region's shape. RULEBOOK 41.10.`,
159
159
  ),
160
160
  )
161
161
  if (found.length > items.length)
@@ -165,8 +165,8 @@ function hydrate(list: List, items: unknown[]): boolean {
165
165
  for (let at = 0; at < wired; at += 1) {
166
166
  const item = items[at]
167
167
  list.rows.push(
168
- rowOf(
169
- list.setUp(item, at),
168
+ paintedRow(
169
+ list.setUp,
170
170
  list.key(item, at),
171
171
  item,
172
172
  found[at] as ChildNode,
package/src/ui/RawRun.ts CHANGED
@@ -94,6 +94,6 @@ function clearRun(run: RawRun): void {
94
94
 
95
95
  function detached(): Mismatch {
96
96
  return new Mismatch(
97
- "abide: this text run is not in a document, so the client and the server disagree about this region's shape (41.10).",
97
+ "abide: this text run is not in a document, so the client and the server disagree about this region's shape. RULEBOOK 41.10.",
98
98
  )
99
99
  }