@abide/abide 0.123.3 → 0.124.1

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 (91) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +38 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +29 -16
  5. package/docs/abide.md +999 -459
  6. package/package.json +1 -1
  7. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  8. package/src/compiler/LoweredFile.ts +609 -0
  9. package/src/compiler/Lowering.ts +46 -4
  10. package/src/compiler/SHARED_NAMES.ts +2 -0
  11. package/src/compiler/ShadowHost.ts +670 -0
  12. package/src/compiler/ShadowMap.ts +1 -1
  13. package/src/compiler/TypeSession.ts +71 -1715
  14. package/src/compiler/appLints.ts +109 -0
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/classifyRead.ts +16 -1
  17. package/src/compiler/compileApp.ts +2 -2
  18. package/src/compiler/declaredExports.ts +44 -17
  19. package/src/compiler/diagnoseSources.ts +232 -0
  20. package/src/compiler/formatAbide.ts +5 -6
  21. package/src/compiler/handlersUnder.ts +16 -35
  22. package/src/compiler/projectShadow.ts +1 -0
  23. package/src/compiler/reactiveReads.ts +235 -0
  24. package/src/compiler/reactiveTypes.ts +13 -0
  25. package/src/compiler/readsIn.ts +180 -81
  26. package/src/compiler/routesDeclaration.ts +46 -0
  27. package/src/compiler/scaffold.ts +7 -2
  28. package/src/compiler/serveLanguage.ts +26 -20
  29. package/src/compiler/splitImports.ts +2 -2
  30. package/src/server/BRIDGE_VERSION.ts +1 -1
  31. package/src/server/DesktopSession.ts +213 -0
  32. package/src/server/Followers.ts +399 -0
  33. package/src/server/MODULE_QUERIES.ts +6 -2
  34. package/src/server/PageRender.ts +201 -0
  35. package/src/server/abideModules.ts +7 -7
  36. package/src/server/answerFailure.ts +3 -2
  37. package/src/server/answerRequest.ts +6 -70
  38. package/src/server/answerRpc.ts +48 -260
  39. package/src/server/answerSocket.ts +41 -463
  40. package/src/server/appKit.ts +1 -1
  41. package/src/server/argsContract.ts +1 -1
  42. package/src/server/createApp.ts +8 -2
  43. package/src/server/csp.ts +1 -1
  44. package/src/server/deferredBuilds.ts +3 -16
  45. package/src/server/faceOf.ts +1 -1
  46. package/src/server/generatedSurfaces.ts +2 -1
  47. package/src/server/handlerFileMount.ts +1 -1
  48. package/src/server/index.ts +1 -1
  49. package/src/server/installServer.ts +5 -9
  50. package/src/server/invokeHandler.ts +230 -0
  51. package/src/server/mountHandlers.ts +184 -0
  52. package/src/server/openDesktopWindow.ts +308 -0
  53. package/src/server/preload.ts +18 -0
  54. package/src/server/principal.ts +1 -1
  55. package/src/server/remote.ts +1 -1
  56. package/src/server/render.ts +31 -59
  57. package/src/server/requestScope.ts +5 -36
  58. package/src/server/runDesktop.ts +19 -447
  59. package/src/server/seedBuffer.ts +11 -11
  60. package/src/server/servedApp.ts +1 -1
  61. package/src/server/shell.ts +1 -1
  62. package/src/server/surfaceOf.ts +1 -1
  63. package/src/server/versionOf.ts +1 -1
  64. package/src/shared/RPC_SERVER.ts +27 -31
  65. package/src/shared/Schema.ts +1 -2
  66. package/src/shared/index.ts +1 -1
  67. package/src/shared/{note.ts → issueReport.ts} +22 -0
  68. package/src/shared/loadCall.ts +66 -0
  69. package/src/shared/reactive/Keyed.ts +23 -15
  70. package/src/shared/reactive/Reactive.ts +46 -1002
  71. package/src/shared/reactive/byOf.ts +2 -5
  72. package/src/shared/reactive/callLoad.ts +8 -14
  73. package/src/shared/reactive/cellShape.ts +734 -0
  74. package/src/shared/reactive/index.ts +12 -24
  75. package/src/shared/reactive/pairReactive.ts +5 -3
  76. package/src/shared/reactive/producer.ts +121 -0
  77. package/src/shared/reactive/scoped.ts +3 -2
  78. package/src/shared/reactive/selections.ts +209 -0
  79. package/src/shared/reactive/watch.ts +3 -4
  80. package/src/shared/rpc.ts +8 -5
  81. package/src/shared/validateJson.ts +1 -2
  82. package/src/shared/writePath.ts +29 -0
  83. package/src/ui/Router.ts +53 -69
  84. package/src/ui/outboxQueue.ts +2 -8
  85. package/src/ui/outcomeOf.ts +120 -0
  86. package/src/ui/socketWire.ts +3 -3
  87. package/src/ui/wire.ts +9 -10
  88. package/src/shared/LOAD_OFFLINE.ts +0 -15
  89. package/src/shared/LOAD_SIGNAL.ts +0 -10
  90. package/src/shared/issuesOf.ts +0 -18
  91. 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, ServerFrame } from '#shared/muxFrames.ts'
19
- import { followServed } from '#shared/reactive/index.ts'
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 { closedTo, crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
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
- 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
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(handlers: Map<string, Mounted>, app: RequestScope['app']) {
269
- 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
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 handler =
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, sent as JSON.
410
- const args = argsFor(followed.handler, frame.args, undefined)
411
- // 18.8 — A GUARDED `GET`'s RUNGS RUN ONCE, as the subscription opens, in this
412
- // connection's request: one they admit is fed the value, and one they refuse is sent its
413
- // triggers alone.
414
- // 18.26 — AND THE MIDDLEWARE ITS FILE'S ROUTE FILED, over the request a `GET` of it
415
- // would be: one a rung answers in the handler's place is not fed, as an http call it
416
- // answered would never reach the handler.
417
- const routed = followed.rungs.length > 0
418
- let admitted = !followed.handler.guarded && !routed
419
- if (!admitted) {
420
- subscriptions.get(id)?.()
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
- mounted: Mounted,
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
- 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
+ ),
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
@@ -1,4 +1,4 @@
1
- // THE OBJECTIVE-C RUNTIME, over `bun:ffi` — what the desktop launcher (`runDesktop`) drives AppKit
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 './answerRpc.ts'
26
+ import type { Mounted } from './mountHandlers.ts'
27
27
 
28
28
  export type Document = {
29
29
  type?: string | string[]
@@ -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(booted.handlers, answering)
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, isStreaming, loadOf } from '#shared/reactive/index.ts'
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 }