@abide/abide 0.123.2 → 0.124.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/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- 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/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- 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 +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- 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 +106 -313
- 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 +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- 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 +28 -13
- package/src/ui/wire.ts +9 -10
- 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/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- 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
|
@@ -11,34 +11,25 @@
|
|
|
11
11
|
// subscription its rungs admitted when it opened (18.8) — they run once, in the connection's request.
|
|
12
12
|
|
|
13
13
|
import { problemOf } from '#shared/Failed.ts'
|
|
14
|
-
import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
|
|
15
14
|
import { namedLogger } from '#shared/log.ts'
|
|
16
|
-
import { compose } from '#shared/Middleware.ts'
|
|
17
15
|
import { unmounted } from '#shared/mount.ts'
|
|
18
|
-
import type { BrowserFrame
|
|
19
|
-
import {
|
|
20
|
-
import { servedCellOf } from '#shared/reactive/index.ts'
|
|
21
|
-
import { matchesPattern } from '#shared/reactive/matchesPattern.ts'
|
|
22
|
-
import { PATCH_SINKS } from '#shared/reactive/PATCH_SINKS.ts'
|
|
23
|
-
import { unowned } from '#shared/reactive/watch.ts'
|
|
16
|
+
import type { BrowserFrame } from '#shared/muxFrames.ts'
|
|
17
|
+
import { isThenable } from '#shared/reactive/isThenable.ts'
|
|
24
18
|
import { refuse } from '#shared/refuse.ts'
|
|
25
|
-
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
26
19
|
|
|
27
|
-
import { handlerKey, type Mounted } from './answerRpc.ts'
|
|
28
|
-
import { argsFor } from './argsFor.ts'
|
|
29
20
|
import { positiveSetting } from './config.ts'
|
|
30
|
-
import {
|
|
21
|
+
import { crossingOrigin } from './crossingOrigin.ts'
|
|
22
|
+
import { type Followers, Frame, type Peer } from './Followers.ts'
|
|
31
23
|
import { failureOf } from './hooks.ts'
|
|
24
|
+
import { admitsFollower, followedArgs } from './invokeHandler.ts'
|
|
25
|
+
import type { FollowedHandler } from './mountHandlers.ts'
|
|
32
26
|
import { sendOrClose } from './muxUpgrade.ts'
|
|
33
27
|
import {
|
|
34
|
-
heldByApp,
|
|
35
28
|
inOpenScope,
|
|
36
29
|
releaseScope,
|
|
37
30
|
type RequestScope,
|
|
38
31
|
scopeFor,
|
|
39
32
|
} from './requestScope.ts'
|
|
40
|
-
import { EVENT_STREAM_OPENER, streamEvent } from './streamEvent.ts'
|
|
41
|
-
import { patchesSince, versionOf } from './versionOf.ts'
|
|
42
33
|
|
|
43
34
|
/**
|
|
44
35
|
* The far end of one connection to the mux, which its frames are written to as text.
|
|
@@ -51,78 +42,9 @@ export type Socket = {
|
|
|
51
42
|
close(code: number, reason: string): unknown
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
const ENCODER = new TextEncoder()
|
|
55
|
-
|
|
56
45
|
// 18.31 — a page follows each value it reads once, so a thousand is a page no one could read.
|
|
57
46
|
const MAX_SUBSCRIPTIONS = 1024
|
|
58
47
|
|
|
59
|
-
/**
|
|
60
|
-
* One `ServerFrame`, for every follower it is sent to.
|
|
61
|
-
*
|
|
62
|
-
* 18.6 — ITS BODY IS SERIALIZED ONCE PER VERSION, and each encoding of it once however many
|
|
63
|
-
* followers it reaches: a socket is sent the JSON with its follower's id spliced in, and an event
|
|
64
|
-
* stream the event, which names no follower and is the same text for every stream.
|
|
65
|
-
*/
|
|
66
|
-
class Frame {
|
|
67
|
-
readonly op: ServerFrame['op']
|
|
68
|
-
readonly base: number | undefined
|
|
69
|
-
readonly version: number | undefined
|
|
70
|
-
// The value, the patch's ops or the refusal, as JSON.
|
|
71
|
-
readonly body: string | undefined
|
|
72
|
-
// What follows the id in the JSON, and the event's bytes, each written on first use.
|
|
73
|
-
private tail: string | undefined = undefined
|
|
74
|
-
private event: Uint8Array | undefined = undefined
|
|
75
|
-
|
|
76
|
-
constructor(
|
|
77
|
-
op: ServerFrame['op'],
|
|
78
|
-
base: number | undefined,
|
|
79
|
-
version: number | undefined,
|
|
80
|
-
body: string | undefined,
|
|
81
|
-
) {
|
|
82
|
-
this.op = op
|
|
83
|
-
this.base = base
|
|
84
|
-
this.version = version
|
|
85
|
-
this.body = body
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** As `ServerFrame` JSON for `id`: `base` and `version` where it has them, then the body under its op's member. */
|
|
89
|
-
json(id: number): string {
|
|
90
|
-
if (this.tail === undefined) {
|
|
91
|
-
let tail = ''
|
|
92
|
-
if (this.base !== undefined) tail += `,"base":${this.base}`
|
|
93
|
-
if (this.version !== undefined) tail += `,"version":${this.version}`
|
|
94
|
-
if (this.body !== undefined)
|
|
95
|
-
tail += `,"${this.op === 'patch' ? 'ops' : this.op}":${this.body}`
|
|
96
|
-
this.tail = `${tail}}`
|
|
97
|
-
}
|
|
98
|
-
return `{"op":"${this.op}","id":${id}${this.tail}`
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** As the SSE event 18.20 makes it, encoded: the op its name, the version its id. */
|
|
102
|
-
asEvent(): Uint8Array {
|
|
103
|
-
this.event ??= ENCODER.encode(
|
|
104
|
-
streamEvent(
|
|
105
|
-
this.op,
|
|
106
|
-
this.version,
|
|
107
|
-
this.body ?? (this.op === 'value' ? 'null' : ''),
|
|
108
|
-
),
|
|
109
|
-
)
|
|
110
|
-
return this.event
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** A trigger's frame (7.10, 7.18), the same for every follower and every trigger. */
|
|
115
|
-
const TRIGGERS = {
|
|
116
|
-
invalidate: new Frame('invalidate', undefined, undefined, undefined),
|
|
117
|
-
refresh: new Frame('refresh', undefined, undefined, undefined),
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** What a frame is written to, under the follower's id: a socket, or an event stream. */
|
|
121
|
-
type Peer = {
|
|
122
|
-
send(id: number, frame: Frame): void
|
|
123
|
-
close(code: number, reason: string): unknown
|
|
124
|
-
}
|
|
125
|
-
|
|
126
48
|
/** A socket's peer: each frame as the `ServerFrame` JSON a browser parses. */
|
|
127
49
|
class SocketPeer implements Peer {
|
|
128
50
|
readonly socket: Socket
|
|
@@ -139,114 +61,6 @@ class SocketPeer implements Peer {
|
|
|
139
61
|
|
|
140
62
|
const SOCKET_LOG = namedLogger('abide:socket')
|
|
141
63
|
|
|
142
|
-
// 18.3 — EVERY SUBSCRIPTION TO A HANDLER, by handler, for its triggers to reach (7.10, 7.18).
|
|
143
|
-
// PROCESS-WIDE because a trigger is: `rpc.invalidate({ id })` inside one request reaches every browser
|
|
144
|
-
// holding that key, whichever connection it came over.
|
|
145
|
-
type Follower = {
|
|
146
|
-
peer: Peer
|
|
147
|
-
id: number
|
|
148
|
-
args: unknown
|
|
149
|
-
// The entry key its args are filed under (`GLOBAL_GETS`), taken once as it subscribes.
|
|
150
|
-
key: string
|
|
151
|
-
// The `global` entry its productions are pushed from, which is what a refresh would otherwise
|
|
152
|
-
// re-fetch — or `undefined`, and it is sent triggers instead.
|
|
153
|
-
serving: unknown
|
|
154
|
-
stopFeed: (() => void) | undefined
|
|
155
|
-
// 18.6 — the version it was last sent, or named on subscribing.
|
|
156
|
-
sent: number | undefined
|
|
157
|
-
// 18.8 — whether the handler's rungs let it through when it opened: a guarded `GET` feeds only
|
|
158
|
-
// a follower they admitted, and sends the rest its triggers alone.
|
|
159
|
-
admitted: boolean
|
|
160
|
-
}
|
|
161
|
-
type Followed = {
|
|
162
|
-
// 13.15 — the literal `tags` the build copied onto the browser's copy, for a sweep to match:
|
|
163
|
-
// the handler's, so one for all of its followers.
|
|
164
|
-
tags: string[] | undefined
|
|
165
|
-
// What a trigger walks, matching each follower's args against its pattern.
|
|
166
|
-
all: Set<Follower>
|
|
167
|
-
// BY ENTRY KEY, so an entry answered walks the followers of its own args alone (18.8). Filed
|
|
168
|
-
// apart from `all` rather than instead of it: a walk of every follower through a set per key
|
|
169
|
-
// measured twice a flat set's.
|
|
170
|
-
byKey: Map<string, Set<Follower>>
|
|
171
|
-
}
|
|
172
|
-
const FOLLOWERS = new Map<object, Followed>()
|
|
173
|
-
|
|
174
|
-
// 13.12 — narrowed by the pattern the trigger was handed, as the handler's own entries are.
|
|
175
|
-
export function broadcast(
|
|
176
|
-
rpc: object,
|
|
177
|
-
member: 'invalidate' | 'refresh',
|
|
178
|
-
pattern: unknown,
|
|
179
|
-
): void {
|
|
180
|
-
const followed = FOLLOWERS.get(rpc)
|
|
181
|
-
if (followed === undefined) return
|
|
182
|
-
for (const follower of followed.all) {
|
|
183
|
-
if (!matchesPattern(follower.args, pattern)) continue
|
|
184
|
-
// A fed copy is refreshed by the production the reload mints (7.18), and invalidated through
|
|
185
|
-
// the cell it is fed from (`FOLLOWED`), whichever trigger reached it — where the entry it is
|
|
186
|
-
// fed from is still the one standing.
|
|
187
|
-
if (attach(rpc as Mounted['handler'], follower) !== undefined) continue
|
|
188
|
-
follower.peer.send(follower.id, TRIGGERS[member])
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 13.15 — A SWEEP REACHES EVERY COPY A BROWSER FOLLOWS that carries one of its `tags`, or every copy
|
|
193
|
-
// where it is bare, as that copy's own trigger. A fed copy is skipped for `broadcast`'s reason: the
|
|
194
|
-
// sweep reloaded the entry it is fed from here, and the production that mints is its refresh.
|
|
195
|
-
export function swept(
|
|
196
|
-
member: 'invalidate' | 'refresh',
|
|
197
|
-
tags: string[] | null,
|
|
198
|
-
): void {
|
|
199
|
-
for (const [rpc, followed] of FOLLOWERS) {
|
|
200
|
-
if (tags !== null && !sharesTag(followed.tags, tags)) continue
|
|
201
|
-
for (const follower of followed.all) {
|
|
202
|
-
if (attach(rpc as Mounted['handler'], follower) !== undefined)
|
|
203
|
-
continue
|
|
204
|
-
follower.peer.send(follower.id, TRIGGERS[member])
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function sharesTag(held: string[] | undefined, swept: string[]): boolean {
|
|
210
|
-
if (held === undefined) return false
|
|
211
|
-
for (const tag of swept) if (held.includes(tag)) return true
|
|
212
|
-
return false
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 18.8 — AN ENTRY ANSWERED THAT NOBODY IS FED FROM: one made after a follower subscribed for its
|
|
216
|
-
// args, or rebuilt after the one it was fed from was evicted. An entry already fed is the common
|
|
217
|
-
// answer, and asks nothing more; one that is not reaches the followers filed under its key.
|
|
218
|
-
export function feedFromServed(
|
|
219
|
-
rpc: object,
|
|
220
|
-
serving: unknown,
|
|
221
|
-
key: string,
|
|
222
|
-
): void {
|
|
223
|
-
if (FEEDS.has(serving as object)) return
|
|
224
|
-
const followers = FOLLOWERS.get(rpc)?.byKey.get(key)
|
|
225
|
-
if (followers === undefined) return
|
|
226
|
-
for (const follower of followers)
|
|
227
|
-
attach(rpc as Mounted['handler'], follower)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Feeds the follower from the entry standing for its args; returns what it is fed from now.
|
|
232
|
-
*
|
|
233
|
-
* Only where that entry is not the one it is fed from already.
|
|
234
|
-
*/
|
|
235
|
-
function attach(rpc: Mounted['handler'], follower: Follower): unknown {
|
|
236
|
-
// 18.8, 18.26 — what its own rungs or its route's refused is sent its triggers alone.
|
|
237
|
-
const serving = !follower.admitted
|
|
238
|
-
? undefined
|
|
239
|
-
: GLOBAL_GETS.get(rpc)?.at(follower.key)
|
|
240
|
-
if (serving === follower.serving) return serving
|
|
241
|
-
follower.stopFeed?.()
|
|
242
|
-
// A version is one entry's count, so what it was sent of another says nothing about this one.
|
|
243
|
-
if (follower.serving !== undefined) follower.sent = undefined
|
|
244
|
-
follower.serving = serving
|
|
245
|
-
follower.stopFeed =
|
|
246
|
-
serving === undefined ? undefined : feed(serving, follower)
|
|
247
|
-
return serving
|
|
248
|
-
}
|
|
249
|
-
|
|
250
64
|
/** A refusal's frame: the failure `error` is, as Problem Details (15.21). */
|
|
251
65
|
function refusal(error: unknown): Frame {
|
|
252
66
|
return new Frame(
|
|
@@ -262,13 +76,21 @@ function refusal(error: unknown): Frame {
|
|
|
262
76
|
* and what closes them all when the app stops (42.11).
|
|
263
77
|
*/
|
|
264
78
|
export class Mux {
|
|
265
|
-
|
|
79
|
+
/** The `GET` a browser follows at an address below the mount, or `undefined` (`followedIn`). */
|
|
80
|
+
readonly followedAt: (address: string) => FollowedHandler | undefined
|
|
81
|
+
/** Who follows what in the app this mux carries for. 18.3. */
|
|
82
|
+
readonly followers: Followers
|
|
266
83
|
// 42.12 — the app a frame's scope answers `rpc.raw` and the MCP surface through.
|
|
267
84
|
readonly app: RequestScope['app']
|
|
268
85
|
readonly connections = new Set<MuxConnection>()
|
|
269
86
|
|
|
270
|
-
constructor(
|
|
271
|
-
|
|
87
|
+
constructor(
|
|
88
|
+
followedAt: (address: string) => FollowedHandler | undefined,
|
|
89
|
+
followers: Followers,
|
|
90
|
+
app: RequestScope['app'],
|
|
91
|
+
) {
|
|
92
|
+
this.followedAt = followedAt
|
|
93
|
+
this.followers = followers
|
|
272
94
|
this.app = app
|
|
273
95
|
}
|
|
274
96
|
|
|
@@ -377,11 +199,8 @@ export class MuxConnection {
|
|
|
377
199
|
return
|
|
378
200
|
}
|
|
379
201
|
}
|
|
380
|
-
const
|
|
381
|
-
frame.op === 'sub'
|
|
382
|
-
? this.mux.handlers.get(handlerKey('GET', address))
|
|
383
|
-
: undefined
|
|
384
|
-
const followed = handler?.ui ? handler : undefined
|
|
202
|
+
const followed =
|
|
203
|
+
frame.op === 'sub' ? this.mux.followedAt(address) : undefined
|
|
385
204
|
if (followed === undefined) {
|
|
386
205
|
this.peer.send(
|
|
387
206
|
id,
|
|
@@ -394,45 +213,23 @@ export class MuxConnection {
|
|
|
394
213
|
)
|
|
395
214
|
return
|
|
396
215
|
}
|
|
397
|
-
// 18.2 — ANOTHER ORIGIN IS CLOSED OUT unless the handler declared it: the same gate, and the
|
|
398
|
-
// same own origin, as its http answer's (16.13, 16.24, 36.2).
|
|
399
|
-
if (closedTo(followed.handler.crossOrigin, this.crossing)) {
|
|
400
|
-
this.peer.send(
|
|
401
|
-
id,
|
|
402
|
-
refusal(crossingRefusal(this.crossing, 'subscription')),
|
|
403
|
-
)
|
|
404
|
-
return
|
|
405
|
-
}
|
|
406
216
|
// A SUBSCRIPTION IS HELD FROM THE FRAME ON, not from when its rungs let it through: an
|
|
407
217
|
// `unsub` or the connection's close that lands while a rung is pending stops this
|
|
408
218
|
// placeholder, and the rungs resolving to find another in its place install nothing.
|
|
409
219
|
const pending = (): void => {}
|
|
410
220
|
try {
|
|
411
|
-
// 18.3 — the args a `GET` over http is answered with
|
|
412
|
-
const args =
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
subscriptions.set(id, pending)
|
|
424
|
-
// No route rungs, no turn: a guarded `GET`'s rungs start in this frame's own run.
|
|
425
|
-
admitted =
|
|
426
|
-
!routed || (await routeAdmits(this.scope, followed, args))
|
|
427
|
-
if (admitted && followed.handler.guarded)
|
|
428
|
-
admitted = await Promise.resolve(
|
|
429
|
-
followed.handler(args as never) as PromiseLike<unknown>,
|
|
430
|
-
).then(
|
|
431
|
-
() => true,
|
|
432
|
-
() => false,
|
|
433
|
-
)
|
|
434
|
-
if (subscriptions.get(id) !== pending) return
|
|
435
|
-
}
|
|
221
|
+
// 18.2, 18.3 — the origin gate and the args a `GET` over http is answered with.
|
|
222
|
+
const args = followedArgs(followed, frame.args, this.crossing)
|
|
223
|
+
subscriptions.get(id)?.()
|
|
224
|
+
subscriptions.set(id, pending)
|
|
225
|
+
// 18.8, 18.26 — run once, in this connection's request.
|
|
226
|
+
const admits = admitsFollower(
|
|
227
|
+
followed,
|
|
228
|
+
args,
|
|
229
|
+
this.scope.request as Request,
|
|
230
|
+
)
|
|
231
|
+
const admitted = isThenable(admits) ? await admits : admits
|
|
232
|
+
if (subscriptions.get(id) !== pending) return
|
|
436
233
|
follow(this, id, followed, args, frame.version, admitted)
|
|
437
234
|
} catch (thrown) {
|
|
438
235
|
if (subscriptions.get(id) === pending) subscriptions.delete(id)
|
|
@@ -441,50 +238,11 @@ export class MuxConnection {
|
|
|
441
238
|
}
|
|
442
239
|
}
|
|
443
240
|
|
|
444
|
-
/**
|
|
445
|
-
* Whether the rungs over `mounted`'s route let a `GET` of it with `args` through, asked over the
|
|
446
|
-
* upgrade's own request at the handler's address. 18.26.
|
|
447
|
-
*/
|
|
448
|
-
async function routeAdmits(
|
|
449
|
-
scope: RequestScope,
|
|
450
|
-
mounted: Mounted,
|
|
451
|
-
args: unknown,
|
|
452
|
-
): Promise<boolean> {
|
|
453
|
-
const upgrade = scope.request as Request
|
|
454
|
-
const asked = rpcRequest(
|
|
455
|
-
'GET',
|
|
456
|
-
mounted.address,
|
|
457
|
-
args as Record<string, unknown> | undefined,
|
|
458
|
-
)
|
|
459
|
-
const request = new Request(new URL(asked.url, upgrade.url), {
|
|
460
|
-
headers: upgrade.headers,
|
|
461
|
-
signal: upgrade.signal,
|
|
462
|
-
})
|
|
463
|
-
// REACHED BY A FLAG, not by the answer's identity: a rung that wraps or re-heads what `next`
|
|
464
|
-
// answered still let the call through, and a shared answer would carry every rung's headers.
|
|
465
|
-
let reached = false
|
|
466
|
-
try {
|
|
467
|
-
const answered = await compose(mounted.rungs, async () => {
|
|
468
|
-
reached = true
|
|
469
|
-
return new Response(null, { status: 204 })
|
|
470
|
-
})({ request })
|
|
471
|
-
return reached && answered.ok
|
|
472
|
-
} catch {
|
|
473
|
-
return false
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Follows a handler for `id`: its triggers always, its value where `global` and unguarded.
|
|
479
|
-
*
|
|
480
|
-
* 18.3, 18.8. STATE, NOT A LOG: nothing is replayed. 18.6 — the value is sent where the version
|
|
481
|
-
* the subscription named is not the one standing, and after that once per change, a burst in one
|
|
482
|
-
* turn going out as the value it left.
|
|
483
|
-
*/
|
|
241
|
+
/** Follows `followed` for `id` on the connection, in place of whatever `id` followed. 18.3, 18.32. */
|
|
484
242
|
function follow(
|
|
485
243
|
connection: MuxConnection,
|
|
486
244
|
id: number,
|
|
487
|
-
|
|
245
|
+
followed: FollowedHandler,
|
|
488
246
|
args: unknown,
|
|
489
247
|
held: number | undefined,
|
|
490
248
|
admitted: boolean,
|
|
@@ -492,225 +250,18 @@ function follow(
|
|
|
492
250
|
connection.subscriptions.get(id)?.()
|
|
493
251
|
connection.subscriptions.set(
|
|
494
252
|
id,
|
|
495
|
-
|
|
253
|
+
connection.mux.followers.follow(
|
|
254
|
+
connection.peer,
|
|
255
|
+
id,
|
|
256
|
+
followed,
|
|
257
|
+
args,
|
|
258
|
+
held,
|
|
259
|
+
admitted,
|
|
260
|
+
),
|
|
496
261
|
)
|
|
497
262
|
}
|
|
498
263
|
|
|
499
|
-
/** Follows a handler for `id` on `peer`, however the peer writes; returns what stops it. */
|
|
500
|
-
function followWith(
|
|
501
|
-
peer: Peer,
|
|
502
|
-
id: number,
|
|
503
|
-
mounted: Mounted,
|
|
504
|
-
args: unknown,
|
|
505
|
-
held: number | undefined,
|
|
506
|
-
admitted: boolean,
|
|
507
|
-
): () => void {
|
|
508
|
-
const rpc = mounted.handler
|
|
509
|
-
const key = GLOBAL_GETS.get(rpc)?.key(args) ?? ''
|
|
510
|
-
const follower: Follower = {
|
|
511
|
-
peer,
|
|
512
|
-
id,
|
|
513
|
-
args,
|
|
514
|
-
key,
|
|
515
|
-
serving: undefined,
|
|
516
|
-
stopFeed: undefined,
|
|
517
|
-
sent: held,
|
|
518
|
-
admitted,
|
|
519
|
-
}
|
|
520
|
-
attach(rpc, follower)
|
|
521
|
-
let followed = FOLLOWERS.get(rpc)
|
|
522
|
-
if (followed === undefined) {
|
|
523
|
-
followed = {
|
|
524
|
-
tags: mounted.copy?.tags,
|
|
525
|
-
all: new Set(),
|
|
526
|
-
byKey: new Map(),
|
|
527
|
-
}
|
|
528
|
-
FOLLOWERS.set(rpc, followed)
|
|
529
|
-
}
|
|
530
|
-
const byKey = followed.byKey
|
|
531
|
-
let followers = byKey.get(key)
|
|
532
|
-
if (followers === undefined) {
|
|
533
|
-
followers = new Set()
|
|
534
|
-
byKey.set(key, followers)
|
|
535
|
-
}
|
|
536
|
-
followers.add(follower)
|
|
537
|
-
const all = followed.all
|
|
538
|
-
all.add(follower)
|
|
539
|
-
return () => {
|
|
540
|
-
follower.stopFeed?.()
|
|
541
|
-
all.delete(follower)
|
|
542
|
-
// A key nobody follows leaves, or the index grows with every key ever followed.
|
|
543
|
-
if (followers.delete(follower) && followers.size === 0)
|
|
544
|
-
byKey.delete(key)
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* A handler followed as an event stream: the same follow a browser's mux makes, written as SSE.
|
|
550
|
-
*
|
|
551
|
-
* 18.20 — EACH MUX FRAME IS ONE EVENT, field for field: its `op` the event's name, its version the
|
|
552
|
-
* event's `id`, and what it carries the `data` — the value for `value`, the JSON Patch for `patch`,
|
|
553
|
-
* nothing for a trigger. So an `EventSource` reconnecting hands the version back as `Last-Event-ID`,
|
|
554
|
-
* which is `held` here, and is sent nothing until the value moves past it.
|
|
555
|
-
*/
|
|
556
|
-
export function followAsEvents(
|
|
557
|
-
mounted: Mounted,
|
|
558
|
-
args: unknown,
|
|
559
|
-
held: number | undefined,
|
|
560
|
-
): ReadableStream<Uint8Array> {
|
|
561
|
-
let stop = (): void => {}
|
|
562
|
-
let unhold = (): void => {}
|
|
563
|
-
let ended = false
|
|
564
|
-
return new ReadableStream({
|
|
565
|
-
start(controller) {
|
|
566
|
-
controller.enqueue(EVENT_STREAM_OPENER)
|
|
567
|
-
// Ended by the app's stop as any body it holds open is, or that stop waits on it.
|
|
568
|
-
unhold = heldByApp(() => {
|
|
569
|
-
if (ended) return
|
|
570
|
-
ended = true
|
|
571
|
-
stop()
|
|
572
|
-
controller.close()
|
|
573
|
-
})
|
|
574
|
-
stop = followWith(
|
|
575
|
-
{
|
|
576
|
-
// 18.6 — the event written and encoded once for every stream, from the body
|
|
577
|
-
// serialized once for every follower: nothing is parsed back.
|
|
578
|
-
send(_id, frame) {
|
|
579
|
-
controller.enqueue(frame.asEvent())
|
|
580
|
-
},
|
|
581
|
-
close() {
|
|
582
|
-
if (ended) return
|
|
583
|
-
ended = true
|
|
584
|
-
unhold()
|
|
585
|
-
controller.close()
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
0,
|
|
589
|
-
mounted,
|
|
590
|
-
args,
|
|
591
|
-
held,
|
|
592
|
-
// The request this stream answers passed the rungs already, or it would not be here.
|
|
593
|
-
true,
|
|
594
|
-
)
|
|
595
|
-
},
|
|
596
|
-
cancel() {
|
|
597
|
-
ended = true
|
|
598
|
-
unhold()
|
|
599
|
-
stop()
|
|
600
|
-
},
|
|
601
|
-
})
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// 18.6 — ONE WATCH PER VALUE SERVED, however many follow it, and each frame's body serialized once
|
|
605
|
-
// per version: every follower standing where the others stand is sent the same bytes, with its own
|
|
606
|
-
// id spliced in front.
|
|
607
|
-
type Feed = {
|
|
608
|
-
fed: Set<Follower>
|
|
609
|
-
landed: boolean
|
|
610
|
-
stop: () => void
|
|
611
|
-
}
|
|
612
|
-
const FEEDS = new WeakMap<object, Feed>()
|
|
613
|
-
|
|
614
|
-
function feed(serving: unknown, fed: Follower): () => void {
|
|
615
|
-
const shared = FEEDS.get(serving as object) ?? startFeed(serving)
|
|
616
|
-
shared.fed.add(fed)
|
|
617
|
-
if (shared.landed) sendAll(serving, [fed])
|
|
618
|
-
return () => {
|
|
619
|
-
shared.fed.delete(fed)
|
|
620
|
-
if (shared.fed.size === 0) shared.stop()
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function startFeed(serving: unknown): Feed {
|
|
625
|
-
const past = servedCellOf(serving)
|
|
626
|
-
const release = sinkFor(past)
|
|
627
|
-
const fed = new Set<Follower>()
|
|
628
|
-
// 7.10 — an invalidate of the value, however it was spelled, reaches every copy it feeds.
|
|
629
|
-
const invalidated = (): void => {
|
|
630
|
-
for (const one of fed) one.peer.send(one.id, TRIGGERS.invalidate)
|
|
631
|
-
}
|
|
632
|
-
let followed = FOLLOWED.get(past)
|
|
633
|
-
if (followed === undefined) {
|
|
634
|
-
followed = new Set()
|
|
635
|
-
FOLLOWED.set(past, followed)
|
|
636
|
-
}
|
|
637
|
-
followed.add(invalidated)
|
|
638
|
-
const shared: Feed = {
|
|
639
|
-
fed,
|
|
640
|
-
landed: false,
|
|
641
|
-
stop: () => {
|
|
642
|
-
stopWatch()
|
|
643
|
-
release()
|
|
644
|
-
FEEDS.delete(serving as object)
|
|
645
|
-
followed.delete(invalidated)
|
|
646
|
-
if (followed.size === 0) FOLLOWED.delete(past)
|
|
647
|
-
},
|
|
648
|
-
}
|
|
649
|
-
FEEDS.set(serving as object, shared)
|
|
650
|
-
// UNOWNED: the feed is stopped when its last follower leaves, and the request it may have been
|
|
651
|
-
// started in is answered long before that (12.5).
|
|
652
|
-
const stopWatch = unowned(() =>
|
|
653
|
-
(serving as { watch(effect: () => void): () => void }).watch(() => {
|
|
654
|
-
shared.landed = true
|
|
655
|
-
sendAll(serving, fed)
|
|
656
|
-
}),
|
|
657
|
-
)
|
|
658
|
-
return shared
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function sendAll(serving: unknown, fed: Iterable<Follower>): void {
|
|
662
|
-
const version = versionOf(serving)
|
|
663
|
-
let valueFrame: Frame | undefined
|
|
664
|
-
// ONE FRAME PER VERSION A FOLLOWER STANDS AT, since followers a burst left at different places
|
|
665
|
-
// along the chain are each sent the writes since their own.
|
|
666
|
-
let patchFrames: Map<number, Frame> | undefined
|
|
667
|
-
for (const one of fed) {
|
|
668
|
-
if (one.sent === version) continue
|
|
669
|
-
// 18.11 — the writes, where the follower stands on the chain they were written along.
|
|
670
|
-
const since = one.sent
|
|
671
|
-
let frame = since === undefined ? undefined : patchFrames?.get(since)
|
|
672
|
-
if (frame === undefined && since !== undefined) {
|
|
673
|
-
const ops = patchesSince(serving, since)
|
|
674
|
-
if (ops !== undefined) {
|
|
675
|
-
frame = new Frame('patch', since, version, JSON.stringify(ops))
|
|
676
|
-
patchFrames ??= new Map()
|
|
677
|
-
patchFrames.set(since, frame)
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (frame === undefined) {
|
|
681
|
-
if (valueFrame === undefined) {
|
|
682
|
-
const held = (serving as { peek(): unknown }).peek()
|
|
683
|
-
valueFrame = new Frame(
|
|
684
|
-
'value',
|
|
685
|
-
undefined,
|
|
686
|
-
version,
|
|
687
|
-
held === undefined ? undefined : JSON.stringify(held),
|
|
688
|
-
)
|
|
689
|
-
}
|
|
690
|
-
frame = valueFrame
|
|
691
|
-
}
|
|
692
|
-
one.peer.send(one.id, frame)
|
|
693
|
-
one.sent = version
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
264
|
function addressIn(named: string): string {
|
|
698
265
|
const path = URL.parse(named)?.pathname ?? named
|
|
699
266
|
return unmounted(path) ?? path
|
|
700
267
|
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Holds a patch sink on `cell`; returns what releases it.
|
|
704
|
-
*
|
|
705
|
-
* 18.11 — A VALUE FED TO A FOLLOWER KEEPS ITS PATCHES AS DATA while anyone is fed it, and stops
|
|
706
|
-
* when the last one goes: a sink nobody drains grows with every patch.
|
|
707
|
-
*/
|
|
708
|
-
function sinkFor(cell: object): () => void {
|
|
709
|
-
const sink = PATCH_SINKS.get(cell) ?? { ops: [], lost: false, fed: 0 }
|
|
710
|
-
PATCH_SINKS.set(cell, sink)
|
|
711
|
-
sink.fed += 1
|
|
712
|
-
return () => {
|
|
713
|
-
sink.fed -= 1
|
|
714
|
-
if (sink.fed === 0) PATCH_SINKS.delete(cell)
|
|
715
|
-
}
|
|
716
|
-
}
|