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