@abide/abide 0.100.0 → 0.102.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 +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
//
|
|
6
6
|
// ONLY ARRAYS AND PLAIN OBJECTS ARE WRAPPED. A `Map`, a `Set` or a class instance keeps internal
|
|
7
7
|
// slots a proxy cannot stand in for — `Map.prototype.set` refuses a proxy as its receiver — so one
|
|
8
|
-
// reached is marked written the moment it is read, path and all
|
|
9
|
-
//
|
|
8
|
+
// reached is marked written the moment it is read, path and all, and each object a `Map` or a `Set`
|
|
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
11
|
//
|
|
11
12
|
// 18.11 — AND WHERE A SINK IS HANDED IN, each write is kept as data too, as JSON Patch (RFC 6902) at
|
|
12
13
|
// the JSON Pointer it was reached by: a member set is `add`, an index `replace` (or `add` one past
|
|
@@ -49,7 +50,15 @@ export function recordWrites<Value>(
|
|
|
49
50
|
sink?: { ops: PatchOp[]; lost: boolean },
|
|
50
51
|
): void {
|
|
51
52
|
if (!isWrappable(root)) {
|
|
52
|
-
|
|
53
|
+
// A root no proxy can stand in for is written as a whole: what the callback reads out of it
|
|
54
|
+
// reaches the rows that show it with no proxy to say so.
|
|
55
|
+
if (root === null || typeof root !== 'object') {
|
|
56
|
+
mutate(root)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
PATCHES.epoch += 1
|
|
60
|
+
PATCHES.written.set(root, PATCHES.epoch)
|
|
61
|
+
mutate(heldView(root, PATCHES.epoch) as Value)
|
|
53
62
|
return
|
|
54
63
|
}
|
|
55
64
|
PATCHES.epoch += 1
|
|
@@ -217,9 +226,10 @@ export function recordWrites<Value>(
|
|
|
217
226
|
}
|
|
218
227
|
if (entry.proxy !== null) return entry.proxy
|
|
219
228
|
mark(value)
|
|
229
|
+
entry.view ??= heldView(value, epoch)
|
|
220
230
|
// Reached, and outside what a proxy can watch: whatever it does next is not kept.
|
|
221
231
|
if (sink !== undefined) sink.lost = true
|
|
222
|
-
return
|
|
232
|
+
return entry.view
|
|
223
233
|
},
|
|
224
234
|
set(held, property, value) {
|
|
225
235
|
mark(held)
|
|
@@ -363,7 +373,7 @@ function pointerOf(path: (string | number)[]): string {
|
|
|
363
373
|
/**
|
|
364
374
|
* The JSON Patch an array method makes, read off the array BEFORE it runs.
|
|
365
375
|
*
|
|
366
|
-
* A `splice`'s start and count are
|
|
376
|
+
* A `splice`'s start and count are normalized the way the method does, so the `remove`s and `add`s
|
|
367
377
|
* name the indices it touches.
|
|
368
378
|
*/
|
|
369
379
|
function opsOfCall(
|
|
@@ -421,10 +431,13 @@ class Reached {
|
|
|
421
431
|
* Written in this patch, directly or below — `PATCHES.written` holds this patch's epoch for it.
|
|
422
432
|
*/
|
|
423
433
|
marked: boolean
|
|
434
|
+
/** What one no proxy can stand in for is handed out as: `heldView` of it. */
|
|
435
|
+
view: object | undefined
|
|
424
436
|
constructor(proxy: object | null, parent: object | Parents | null) {
|
|
425
437
|
this.proxy = proxy
|
|
426
438
|
this.parent = parent
|
|
427
439
|
this.marked = false
|
|
440
|
+
this.view = undefined
|
|
428
441
|
}
|
|
429
442
|
}
|
|
430
443
|
|
|
@@ -463,6 +476,91 @@ class Parents {
|
|
|
463
476
|
}
|
|
464
477
|
}
|
|
465
478
|
|
|
479
|
+
/**
|
|
480
|
+
* `container` as the callback is handed it: a `Map` or a `Set` that marks written each object key,
|
|
481
|
+
* value or member it hands out, and anything else as it is.
|
|
482
|
+
*
|
|
483
|
+
* A row over `[...map.values()]` reads one of them, and a write the callback makes to it through
|
|
484
|
+
* `map.get(id)` reaches it with nothing on the way to record it — so each one taken out is taken as
|
|
485
|
+
* written. AS IT LEAVES rather than all of them up front, which would make a patch of one entry cost
|
|
486
|
+
* the whole container. One level down: a row reads its item.
|
|
487
|
+
*/
|
|
488
|
+
function heldView(container: object, epoch: number): object {
|
|
489
|
+
if (!(container instanceof Map) && !(container instanceof Set))
|
|
490
|
+
return container
|
|
491
|
+
return new Proxy(container, new HeldHandler(epoch))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// `Map`'s and `Set`'s own methods run on the container itself, which is the receiver their internal
|
|
495
|
+
// slots answer to; the ones that hand an object out mark it first.
|
|
496
|
+
class HeldHandler {
|
|
497
|
+
epoch: number
|
|
498
|
+
constructor(epoch: number) {
|
|
499
|
+
this.epoch = epoch
|
|
500
|
+
}
|
|
501
|
+
get(
|
|
502
|
+
held: Map<unknown, unknown> | Set<unknown>,
|
|
503
|
+
property: string | symbol,
|
|
504
|
+
): unknown {
|
|
505
|
+
if (property === TARGET) return held
|
|
506
|
+
const value = Reflect.get(held, property, held)
|
|
507
|
+
if (typeof value !== 'function') return value
|
|
508
|
+
const epoch = this.epoch
|
|
509
|
+
if (property === 'get')
|
|
510
|
+
return (key: unknown) =>
|
|
511
|
+
markedOut((held as Map<unknown, unknown>).get(key), epoch)
|
|
512
|
+
if (property === 'forEach')
|
|
513
|
+
return (
|
|
514
|
+
each: (value: unknown, key: unknown, set: unknown) => void,
|
|
515
|
+
self?: unknown,
|
|
516
|
+
) => {
|
|
517
|
+
for (const [key, member] of held.entries())
|
|
518
|
+
each.call(
|
|
519
|
+
self,
|
|
520
|
+
markedOut(member, epoch),
|
|
521
|
+
markedOut(key, epoch),
|
|
522
|
+
held,
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
if (
|
|
526
|
+
property === 'values' ||
|
|
527
|
+
property === 'keys' ||
|
|
528
|
+
property === 'entries' ||
|
|
529
|
+
property === Symbol.iterator
|
|
530
|
+
)
|
|
531
|
+
// A Map's own iterator is its entries; a Set's is its values.
|
|
532
|
+
return () =>
|
|
533
|
+
markedEach(
|
|
534
|
+
(value as () => Iterator<unknown>).call(held),
|
|
535
|
+
property === 'entries' ||
|
|
536
|
+
(property === Symbol.iterator && held instanceof Map),
|
|
537
|
+
epoch,
|
|
538
|
+
)
|
|
539
|
+
return (value as (...args: unknown[]) => unknown).bind(held)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function* markedEach(
|
|
544
|
+
each: Iterator<unknown>,
|
|
545
|
+
pairs: boolean,
|
|
546
|
+
epoch: number,
|
|
547
|
+
): Generator<unknown> {
|
|
548
|
+
for (let next = each.next(); next.done !== true; next = each.next()) {
|
|
549
|
+
if (pairs) {
|
|
550
|
+
const [key, value] = next.value as [unknown, unknown]
|
|
551
|
+
markedOut(key, epoch)
|
|
552
|
+
markedOut(value, epoch)
|
|
553
|
+
} else markedOut(next.value, epoch)
|
|
554
|
+
yield next.value
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function markedOut<Value>(value: Value, epoch: number): Value {
|
|
559
|
+
if (typeof value === 'object' && value !== null)
|
|
560
|
+
PATCHES.written.set(value, epoch)
|
|
561
|
+
return value
|
|
562
|
+
}
|
|
563
|
+
|
|
466
564
|
/** Whether a proxy can stand in for `value`: an array or a plain object. */
|
|
467
565
|
function isWrappable(value: unknown): value is object {
|
|
468
566
|
if (value === null || typeof value !== 'object') return false
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The by-value comparator an app passes as `identity`: deep equality over arrays and plain objects.
|
|
3
3
|
*
|
|
4
4
|
* Not the default: the default is the reference, and this is what a value whose producer
|
|
5
|
-
* RE-
|
|
5
|
+
* RE-MATERIALIZES declares — `reactive(() => getInvoice({ id }), { identity: structural })`. A
|
|
6
6
|
* fresh-but-equal payload waking every reader is why it exists at all; what it stops being is the
|
|
7
7
|
* price every other value pays.
|
|
8
8
|
*
|
|
@@ -87,6 +87,11 @@ export type Subscriber = {
|
|
|
87
87
|
* the identity gate that exists to stop exactly that cannot see a production nobody minted.
|
|
88
88
|
*/
|
|
89
89
|
probeSources: Tracked[]
|
|
90
|
+
/**
|
|
91
|
+
* How far into `probeSources` this run has re-asked — `cursor` for the probe edge, and see
|
|
92
|
+
* `trackProbe` for why it is not the same protocol.
|
|
93
|
+
*/
|
|
94
|
+
probeCursor: number
|
|
90
95
|
/**
|
|
91
96
|
* How far into `sources` this run has matched.
|
|
92
97
|
*
|
|
@@ -111,8 +116,22 @@ export type Subscriber = {
|
|
|
111
116
|
|
|
112
117
|
export type Tracked = {
|
|
113
118
|
readers: Subscriber[]
|
|
119
|
+
/** How many of `readers` are disposed and not yet swept out — see `dropFrom`. */
|
|
120
|
+
deadReaders: number
|
|
114
121
|
/** Who asked about this value's probes. Apart from `readers` — see `probeSources`. */
|
|
115
122
|
probeReaders: Subscriber[]
|
|
123
|
+
/** `deadReaders` for `probeReaders`. */
|
|
124
|
+
deadProbeReaders: number
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A node that is nothing but its edges — a probe signal with no value behind it. */
|
|
128
|
+
export function newTracked(): Tracked {
|
|
129
|
+
return {
|
|
130
|
+
readers: [],
|
|
131
|
+
deadReaders: 0,
|
|
132
|
+
probeReaders: [],
|
|
133
|
+
deadProbeReaders: 0,
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
/** The reader whose run is on the stack, or `null` where reads subscribe to nothing. */
|
|
@@ -144,6 +163,7 @@ export function newSubscriber(
|
|
|
144
163
|
return {
|
|
145
164
|
sources,
|
|
146
165
|
probeSources: [],
|
|
166
|
+
probeCursor: 0,
|
|
147
167
|
cursor: 0,
|
|
148
168
|
seen: null,
|
|
149
169
|
seenThrough: 0,
|
|
@@ -167,8 +187,9 @@ export function newSubscriber(
|
|
|
167
187
|
* of the loop beside this one is what CLAUDE.md's sibling-seam rule refuses.
|
|
168
188
|
*/
|
|
169
189
|
export function dropReader(source: Tracked, reader: Subscriber): void {
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
const readers = source.readers
|
|
191
|
+
source.deadReaders = dropFrom(readers, reader, source.deadReaders)
|
|
192
|
+
if (readers.length === 0) unread(source)
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
/**
|
|
@@ -218,40 +239,102 @@ export function untracked<First, Second, Result>(
|
|
|
218
239
|
* rather than on a value.
|
|
219
240
|
*/
|
|
220
241
|
export function dropProbeReader(source: Tracked, reader: Subscriber): void {
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
const readers = source.probeReaders
|
|
243
|
+
source.deadProbeReaders = dropFrom(readers, reader, source.deadProbeReaders)
|
|
244
|
+
if (readers.length === 0) unread(source)
|
|
223
245
|
}
|
|
224
246
|
|
|
225
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Takes `reader` off `readers`, of which `dead` are disposed, and returns how many are now.
|
|
249
|
+
*
|
|
250
|
+
* A DISPOSED READER IS LEFT WHERE IT IS AND COUNTED, rather than found. Finding it is a scan of
|
|
251
|
+
* every reader of the value, and a teardown drops them all — n rows reading one page value, torn
|
|
252
|
+
* down, paid n²/2 compares, and a keyed replace is the worst order there is: the new rows are
|
|
253
|
+
* pushed behind the old ones the scan walks past. 10,000 rows went 16-39 ms to 0.2-0.4. Every
|
|
254
|
+
* walk over a reader list skips `!alive`, so a dead slot costs a walk one test until the
|
|
255
|
+
* sweep below takes it out.
|
|
256
|
+
*
|
|
257
|
+
* SWEPT ONCE HALF THE LIST IS DEAD, which keeps a walk to at most twice the live readers and the
|
|
258
|
+
* sweep amortized O(1) a drop — and keeps the invariant every `length` test leans on: a list with
|
|
259
|
+
* entries has a LIVE one, since a list that is all dead is at least half dead. HALF, NOT MORE THAN
|
|
260
|
+
* HALF: a keyed replace ends on exactly half, n old rows dead behind n new ones, and a sweep that
|
|
261
|
+
* waited for one more drop kept the old rows — and through each one's `notify`, its scope and its
|
|
262
|
+
* nodes — reachable until some unrelated reader left. An over-count — a
|
|
263
|
+
* reader dropped twice — only sweeps early, and the sweep recounts from zero.
|
|
264
|
+
*
|
|
265
|
+
* A LIVE reader still scans: that is a run's own truncation or an orphan's release, a reader
|
|
266
|
+
* that may subscribe again, so it cannot be left to read as dead.
|
|
267
|
+
*/
|
|
268
|
+
function dropFrom(
|
|
269
|
+
readers: Subscriber[],
|
|
270
|
+
reader: Subscriber,
|
|
271
|
+
dead: number,
|
|
272
|
+
): number {
|
|
273
|
+
if (!reader.alive) dead += 1
|
|
274
|
+
else purge(readers, reader)
|
|
275
|
+
if (dead * 2 < readers.length) return dead
|
|
276
|
+
let kept = 0
|
|
226
277
|
for (let index = 0; index < readers.length; index += 1) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
const survivor = readers[index] as Subscriber
|
|
279
|
+
if (survivor.alive) {
|
|
280
|
+
readers[kept] = survivor
|
|
281
|
+
kept += 1
|
|
282
|
+
}
|
|
231
283
|
}
|
|
284
|
+
readers.length = kept
|
|
285
|
+
return 0
|
|
232
286
|
}
|
|
233
287
|
|
|
234
288
|
/**
|
|
235
289
|
* Subscribes `reader` to `source`'s probes, once per run.
|
|
236
290
|
*
|
|
237
|
-
* A PROBE EDGE IS
|
|
238
|
-
*
|
|
239
|
-
*
|
|
291
|
+
* A PROBE EDGE IS CURSORED, and kept in the order this run asked. `probeSources[0..probeCursor)`
|
|
292
|
+
* is what this run has asked so far; a source asked again is moved to the cursor rather than
|
|
293
|
+
* dropped and re-pushed, so a run that asks what its last one did touches no source's list at
|
|
294
|
+
* all. It used to be dropped at the start of every run and re-made — which put a scan of the
|
|
295
|
+
* SOURCE'S probe readers behind every rerun, and n guards over one shared `ready()` woken by one
|
|
296
|
+
* flip paid n²: 10,000 went 39 ms to 0.4. A guard that stops being taken still stops waking:
|
|
297
|
+
* what the run did not re-ask is past the cursor at its end, and `truncateProbes` drops it.
|
|
298
|
+
*
|
|
299
|
+
* NOT `track`'s protocol, because the list is a SET in no particular order where `sources` is
|
|
300
|
+
* positional: a re-ask out of order is a swap, never a divergence, and a body asks one to three.
|
|
240
301
|
*/
|
|
241
302
|
export function trackProbe(source: Tracked, reader: Subscriber): void {
|
|
242
303
|
const asked = reader.probeSources
|
|
243
|
-
|
|
304
|
+
const at = reader.probeCursor
|
|
305
|
+
if (asked[at] === source) {
|
|
306
|
+
reader.probeCursor = at + 1
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
for (let index = 0; index < at; index += 1)
|
|
244
310
|
if (asked[index] === source) return
|
|
245
|
-
asked.
|
|
246
|
-
|
|
311
|
+
let found = asked.length
|
|
312
|
+
for (let index = at + 1; index < asked.length; index += 1)
|
|
313
|
+
if (asked[index] === source) {
|
|
314
|
+
found = index
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
if (found === asked.length) {
|
|
318
|
+
asked.push(source)
|
|
319
|
+
source.probeReaders.push(reader)
|
|
320
|
+
}
|
|
321
|
+
asked[found] = asked[at] as Tracked
|
|
322
|
+
asked[at] = source
|
|
323
|
+
reader.probeCursor = at + 1
|
|
247
324
|
}
|
|
248
325
|
|
|
249
|
-
/** Drops
|
|
250
|
-
export function
|
|
326
|
+
/** Drops the probe subscriptions `reader`'s run did not re-ask — the tail past its cursor. */
|
|
327
|
+
export function truncateProbes(reader: Subscriber): void {
|
|
251
328
|
const asked = reader.probeSources
|
|
252
|
-
for (let index =
|
|
329
|
+
for (let index = reader.probeCursor; index < asked.length; index += 1)
|
|
253
330
|
dropProbeReader(asked[index] as Tracked, reader)
|
|
254
|
-
asked.length =
|
|
331
|
+
asked.length = reader.probeCursor
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Drops every probe subscription `reader` holds. */
|
|
335
|
+
export function detachProbes(reader: Subscriber): void {
|
|
336
|
+
reader.probeCursor = 0
|
|
337
|
+
truncateProbes(reader)
|
|
255
338
|
}
|
|
256
339
|
|
|
257
340
|
// How far `kept * dropped` must outgrow `kept + dropped` before `truncate` builds a `Set` over the
|
|
@@ -278,8 +361,9 @@ const SET_OVER_SCAN = 20
|
|
|
278
361
|
* `SET_OVER_SCAN`): at 1000/999 the scan is one probe per dropped slot and building the Set
|
|
279
362
|
* doubled the flip, so the two are priced against each other instead.
|
|
280
363
|
*
|
|
281
|
-
* A duplicate inside the DROPPED tail needs no test — `
|
|
282
|
-
* second call finds none
|
|
364
|
+
* A duplicate inside the DROPPED tail needs no test — `dropFrom` takes one occurrence and the
|
|
365
|
+
* second call finds none, or for a disposed reader counts one dead too many, which only sweeps
|
|
366
|
+
* early.
|
|
283
367
|
*
|
|
284
368
|
* Exported because a tracked run's end is exactly this and nothing more: the TEST that there is a
|
|
285
369
|
* tail is the caller's — two field loads, on the rare arm.
|
|
@@ -416,3 +500,53 @@ export function detach(reader: Subscriber): void {
|
|
|
416
500
|
truncate(reader)
|
|
417
501
|
if (reader.probeSources.length !== 0) detachProbes(reader)
|
|
418
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Lets go of every subscription `reader` holds as a DISPOSAL — counted dead where it stands
|
|
506
|
+
* (`dropFrom`) rather than scanned for — while keeping its lists, so `revive` can find what it left.
|
|
507
|
+
*
|
|
508
|
+
* FOR A RELEASED ORPHAN (12.14): n rows each deriving off one page value, torn down, scanned the
|
|
509
|
+
* page value's readers once a row. The orphan may be read again, which is what `revive` is for —
|
|
510
|
+
* paid by the read that comes back, which is rare, rather than by the release, which is per row.
|
|
511
|
+
*/
|
|
512
|
+
export function tombstone(reader: Subscriber): void {
|
|
513
|
+
reader.alive = false
|
|
514
|
+
const sources = reader.sources
|
|
515
|
+
for (let index = 0; index < sources.length; index += 1)
|
|
516
|
+
dropReader(sources[index] as Tracked, reader)
|
|
517
|
+
reader.cursor = 0
|
|
518
|
+
const asked = reader.probeSources
|
|
519
|
+
for (let index = 0; index < asked.length; index += 1)
|
|
520
|
+
dropProbeReader(asked[index] as Tracked, reader)
|
|
521
|
+
reader.probeCursor = 0
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Takes a `tombstone`d reader's dead entries out of whatever list has not swept them yet, and
|
|
526
|
+
* makes it live with nothing subscribed — the state a fresh reader starts its run in.
|
|
527
|
+
*/
|
|
528
|
+
export function revive(reader: Subscriber): void {
|
|
529
|
+
reader.alive = true
|
|
530
|
+
const sources = reader.sources
|
|
531
|
+
for (let index = 0; index < sources.length; index += 1) {
|
|
532
|
+
const source = sources[index] as Tracked
|
|
533
|
+
if (purge(source.readers, reader)) source.deadReaders -= 1
|
|
534
|
+
}
|
|
535
|
+
sources.length = 0
|
|
536
|
+
const asked = reader.probeSources
|
|
537
|
+
for (let index = 0; index < asked.length; index += 1) {
|
|
538
|
+
const source = asked[index] as Tracked
|
|
539
|
+
if (purge(source.probeReaders, reader)) source.deadProbeReaders -= 1
|
|
540
|
+
}
|
|
541
|
+
asked.length = 0
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function purge(readers: Subscriber[], reader: Subscriber): boolean {
|
|
545
|
+
for (let index = 0; index < readers.length; index += 1) {
|
|
546
|
+
if (readers[index] !== reader) continue
|
|
547
|
+
readers[index] = readers[readers.length - 1] as Subscriber
|
|
548
|
+
readers.pop()
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
return false
|
|
552
|
+
}
|
|
@@ -20,12 +20,12 @@ import type { Reactive } from './Reactive.ts'
|
|
|
20
20
|
import { currentScope, runInScope } from './scoping.ts'
|
|
21
21
|
import {
|
|
22
22
|
detach,
|
|
23
|
-
detachProbes,
|
|
24
23
|
newSubscriber,
|
|
25
24
|
type Subscriber,
|
|
26
25
|
TRACKING,
|
|
27
26
|
type Tracked,
|
|
28
27
|
truncate,
|
|
28
|
+
truncateProbes,
|
|
29
29
|
untracked,
|
|
30
30
|
} from './tracking.ts'
|
|
31
31
|
|
|
@@ -203,7 +203,7 @@ export function watchOver(
|
|
|
203
203
|
// UNTRACKED, AND THE CLEAR IS WHAT MAKES IT SO. The sources are given rather
|
|
204
204
|
// than discovered, but the effect still RUNS wherever it was reached from — a
|
|
205
205
|
// `s.watch` opened inside a `watch` had its reads attributed to the OUTER
|
|
206
|
-
// reader, which then
|
|
206
|
+
// reader, which then reran on a value it never read. A mirror is the same
|
|
207
207
|
// shape one turn up: it runs inside the mark walk, and 4.7 has the `transform`
|
|
208
208
|
// it calls there running untracked.
|
|
209
209
|
const previous = TRACKING.reader
|
|
@@ -219,7 +219,7 @@ export function watchOver(
|
|
|
219
219
|
// subscribed to, so its landing is what runs this again.
|
|
220
220
|
if (thrown instanceof NotLanded) return
|
|
221
221
|
// 12.8: one that throws once usually throws every run, and a watch
|
|
222
|
-
//
|
|
222
|
+
// rerunning into the same throw is a loop with a log line per iteration.
|
|
223
223
|
reader.alive = false
|
|
224
224
|
detach(reader)
|
|
225
225
|
effectThrew(thrown)
|
|
@@ -242,13 +242,13 @@ export function watchOver(
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* The bare form: runs `effect` as a tracked context,
|
|
245
|
+
* The bare form: runs `effect` as a tracked context, rerunning when anything it read changes.
|
|
246
246
|
*
|
|
247
247
|
* UNOWNED — the caller holds the stop. `watch` is this handed to its owners (`owned`); a template
|
|
248
248
|
* binding's `Scope` and a `{#try}`'s retry hold their own, and call this direct.
|
|
249
249
|
*
|
|
250
250
|
* A SECOND BODY BESIDE `watchOver` ON PURPOSE, and it reads as the duplication it is: the two
|
|
251
|
-
* differ by the cursor
|
|
251
|
+
* differ by the cursor resets and the tail trims, and the rest is the same lines.
|
|
252
252
|
* ONE BODY WITH A `tracked` FLAG WAS BUILT AND MEASURED, under V8 against this pair, interleaved
|
|
253
253
|
* four runs a side: `write a value that changed` — one bound watch — went 15.6 ns to 21.4 in every
|
|
254
254
|
* run, and `mount 50 readers` +12% (+7% under JSC). Three flag tests cannot cost 6 ns. The likely
|
|
@@ -276,7 +276,7 @@ export function watchTracked(effect: Effect): Disposer {
|
|
|
276
276
|
const previous = TRACKING.reader
|
|
277
277
|
TRACKING.reader = reader
|
|
278
278
|
reader.cursor = 0
|
|
279
|
-
|
|
279
|
+
reader.probeCursor = 0
|
|
280
280
|
reader.running = true
|
|
281
281
|
try {
|
|
282
282
|
WORK.wakes += 1
|
|
@@ -285,6 +285,8 @@ export function watchTracked(effect: Effect): Disposer {
|
|
|
285
285
|
reader.running = false
|
|
286
286
|
TRACKING.reader = previous
|
|
287
287
|
if (reader.cursor < reader.sources.length) truncate(reader)
|
|
288
|
+
if (reader.probeCursor < reader.probeSources.length)
|
|
289
|
+
truncateProbes(reader)
|
|
288
290
|
// The abort reaches here too: an effect that read a value which has
|
|
289
291
|
// not landed is not an effect that failed, it is one whose turn has
|
|
290
292
|
// not come. The read subscribed before it threw, so the value landing
|
|
@@ -299,6 +301,8 @@ export function watchTracked(effect: Effect): Disposer {
|
|
|
299
301
|
reader.running = false
|
|
300
302
|
TRACKING.reader = previous
|
|
301
303
|
if (reader.cursor < reader.sources.length) truncate(reader)
|
|
304
|
+
if (reader.probeCursor < reader.probeSources.length)
|
|
305
|
+
truncateProbes(reader)
|
|
302
306
|
}
|
|
303
307
|
reader.notify()
|
|
304
308
|
return (): void => {
|
package/src/shared/refuse.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { JsonValue } from './JsonValue.ts'
|
|
|
15
15
|
import { namedLogger } from './log.ts'
|
|
16
16
|
import { type Found, runSchema, type Schema } from './Schema.ts'
|
|
17
17
|
|
|
18
|
-
/** The `abide:refuse` logger (28.
|
|
18
|
+
/** The `abide:refuse` logger (28.8) — 15.13 and 15.15's namespace. */
|
|
19
19
|
const REFUSE_LOG = namedLogger('abide:refuse')
|
|
20
20
|
|
|
21
21
|
/**
|