@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.
- package/CHANGELOG.md +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- 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
|
|
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
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
reader.state
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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% (`
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 (`
|
|
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
|
|
package/src/ui/ArmRegion.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
169
|
-
list.setUp
|
|
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
|
|
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
|
}
|