@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.
Files changed (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. 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, ServerFrame } from '#shared/muxFrames.ts'
19
- import { FOLLOWED } from '#shared/reactive/FOLLOWED.ts'
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 { closedTo, crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
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
- readonly handlers: Map<string, Mounted>
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(handlers: Map<string, Mounted>, app: RequestScope['app']) {
271
- this.handlers = handlers
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 handler =
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, sent as JSON.
412
- const args = argsFor(followed, frame.args, undefined)
413
- // 18.8 — A GUARDED `GET`'s RUNGS RUN ONCE, as the subscription opens, in this
414
- // connection's request: one they admit is fed the value, and one they refuse is sent its
415
- // triggers alone.
416
- // 18.26 — AND THE MIDDLEWARE ITS FILE'S ROUTE FILED, over the request a `GET` of it
417
- // would be: one a rung answers in the handler's place is not fed, as an http call it
418
- // answered would never reach the handler.
419
- const routed = followed.rungs.length > 0
420
- let admitted = !followed.handler.guarded && !routed
421
- if (!admitted) {
422
- subscriptions.get(id)?.()
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
- mounted: Mounted,
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
- followWith(connection.peer, id, mounted, args, held, admitted),
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
- }