@abide/abide 0.123.2 → 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.
- package/CHANGELOG.md +27 -0
- package/bin/abide.ts +36 -65
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +54 -245
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +21 -3
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +26 -11
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- 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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
+
}
|
|
@@ -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 './
|
|
26
|
-
import {
|
|
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
|
|
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?:
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
mutate(heldView(root,
|
|
59
|
+
const epoch = openPatch()
|
|
60
|
+
markWritten(root, epoch)
|
|
61
|
+
mutate(heldView(root, epoch) as Value)
|
|
62
62
|
return
|
|
63
63
|
}
|
|
64
|
-
|
|
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
|
-
|
|
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 —
|
|
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:
|
|
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
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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) {
|
package/src/ui/ArmRegion.ts
CHANGED
|
@@ -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 —
|
|
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
|
-
*
|
|
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
|
|
27
|
-
|
|
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(
|
|
68
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|