@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
@@ -0,0 +1,399 @@
1
+ // 18.3 — WHO FOLLOWS WHAT, for one app: every subscription to a handler, by handler, whichever lane
2
+ // it came over — a socket on the mux, or an event stream (18.20) — and the one feed per value served
3
+ // that its followers share (18.6). The mux and the http lane subscribe; a trigger, a sweep and an
4
+ // answer reach them.
5
+ //
6
+ // A TRIGGER REACHES EVERY APP'S FOLLOWERS in the process: `rpc.invalidate({ id })` inside one request
7
+ // reaches every browser holding that key, whichever connection it came over, and two apps in one
8
+ // process share their `global` entries (42.12). So an app's followers are filed on `RPC_SERVER` for
9
+ // as long as anyone follows, which is what the in-process trigger walks.
10
+
11
+ import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
12
+ import type { ServerFrame } from '#shared/muxFrames.ts'
13
+ import { matchesPattern } from '#shared/reactive/matchesPattern.ts'
14
+ import { followServed } from '#shared/reactive/producer.ts'
15
+ import { unowned } from '#shared/reactive/watch.ts'
16
+ import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
17
+
18
+ import type { FollowedHandler } from './mountHandlers.ts'
19
+ import { heldByApp } from './requestScope.ts'
20
+ import { EVENT_STREAM_OPENER, streamEvent } from './streamEvent.ts'
21
+ import { patchesSince, versionOf } from './versionOf.ts'
22
+
23
+ const ENCODER = new TextEncoder()
24
+
25
+ /**
26
+ * One `ServerFrame`, for every follower it is sent to.
27
+ *
28
+ * 18.6 — ITS BODY IS SERIALIZED ONCE PER VERSION, and each encoding of it once however many
29
+ * followers it reaches: a socket is sent the JSON with its follower's id spliced in, and an event
30
+ * stream the event, which names no follower and is the same text for every stream.
31
+ */
32
+ export class Frame {
33
+ readonly op: ServerFrame['op']
34
+ readonly base: number | undefined
35
+ readonly version: number | undefined
36
+ // The value, the patch's ops or the refusal, as JSON.
37
+ readonly body: string | undefined
38
+ // What follows the id in the JSON, and the event's bytes, each written on first use.
39
+ private tail: string | undefined = undefined
40
+ private event: Uint8Array | undefined = undefined
41
+
42
+ constructor(
43
+ op: ServerFrame['op'],
44
+ base: number | undefined,
45
+ version: number | undefined,
46
+ body: string | undefined,
47
+ ) {
48
+ this.op = op
49
+ this.base = base
50
+ this.version = version
51
+ this.body = body
52
+ }
53
+
54
+ /** As `ServerFrame` JSON for `id`: `base` and `version` where it has them, then the body under its op's member. */
55
+ json(id: number): string {
56
+ if (this.tail === undefined) {
57
+ let tail = ''
58
+ if (this.base !== undefined) tail += `,"base":${this.base}`
59
+ if (this.version !== undefined) tail += `,"version":${this.version}`
60
+ if (this.body !== undefined)
61
+ tail += `,"${this.op === 'patch' ? 'ops' : this.op}":${this.body}`
62
+ this.tail = `${tail}}`
63
+ }
64
+ return `{"op":"${this.op}","id":${id}${this.tail}`
65
+ }
66
+
67
+ /** As the SSE event 18.20 makes it, encoded: the op its name, the version its id. */
68
+ asEvent(): Uint8Array {
69
+ this.event ??= ENCODER.encode(
70
+ streamEvent(
71
+ this.op,
72
+ this.version,
73
+ this.body ?? (this.op === 'value' ? 'null' : ''),
74
+ ),
75
+ )
76
+ return this.event
77
+ }
78
+ }
79
+
80
+ /** A trigger's frame (7.10, 7.18), the same for every follower and every trigger. */
81
+ const TRIGGERS = {
82
+ invalidate: new Frame('invalidate', undefined, undefined, undefined),
83
+ refresh: new Frame('refresh', undefined, undefined, undefined),
84
+ }
85
+
86
+ /** What a frame is written to, under the follower's id: a socket, or an event stream. */
87
+ export type Peer = {
88
+ send(id: number, frame: Frame): void
89
+ close(code: number, reason: string): unknown
90
+ }
91
+
92
+ type Follower = {
93
+ peer: Peer
94
+ id: number
95
+ args: unknown
96
+ // The entry key its args are filed under (`GLOBAL_GETS`), taken once as it subscribes.
97
+ key: string
98
+ // The `global` entry its productions are pushed from, which is what a refresh would otherwise
99
+ // re-fetch — or `undefined`, and it is sent triggers instead.
100
+ serving: unknown
101
+ stopFeed: (() => void) | undefined
102
+ // 18.6 — the version it was last sent, or named on subscribing.
103
+ sent: number | undefined
104
+ // 18.8 — whether the handler's rungs let it through when it opened: a guarded `GET` feeds only
105
+ // a follower they admitted, and sends the rest its triggers alone.
106
+ admitted: boolean
107
+ }
108
+
109
+ type HandlerFollowers = {
110
+ // 13.15 — the literal `tags` the build copied onto the browser's copy, for a sweep to match:
111
+ // the handler's, so one for all of its followers.
112
+ tags: string[] | undefined
113
+ // What a trigger walks, matching each follower's args against its pattern.
114
+ all: Set<Follower>
115
+ // BY ENTRY KEY, so an entry answered walks the followers of its own args alone (18.8). Filed
116
+ // apart from `all` rather than instead of it: a walk of every follower through a set per key
117
+ // measured twice a flat set's.
118
+ byKey: Map<string, Set<Follower>>
119
+ }
120
+
121
+ // 18.6 — ONE WATCH PER VALUE SERVED, however many follow it, and each frame's body serialized once
122
+ // per version: every follower standing where the others stand is sent the same bytes, with its own
123
+ // id spliced in front.
124
+ type Feed = {
125
+ fed: Set<Follower>
126
+ landed: boolean
127
+ stop: () => void
128
+ }
129
+
130
+ /** Every follower one app holds, and the feeds they share. */
131
+ export class Followers {
132
+ private readonly handlers = new Map<object, HandlerFollowers>()
133
+ private readonly feeds = new WeakMap<object, Feed>()
134
+ // How many follow, which files these followers where a trigger reaches them while any do.
135
+ private following = 0
136
+
137
+ /**
138
+ * Follows `followed` for `id` on `peer`: its triggers always, its value where `global` and
139
+ * admitted; returns what stops it.
140
+ *
141
+ * 18.3, 18.8. STATE, NOT A LOG: nothing is replayed. 18.6 — the value is sent where the version
142
+ * the subscription named (`held`) is not the one standing, and after that once per change, a
143
+ * burst in one turn going out as the value it left.
144
+ */
145
+ follow(
146
+ peer: Peer,
147
+ id: number,
148
+ followed: FollowedHandler,
149
+ args: unknown,
150
+ held: number | undefined,
151
+ admitted: boolean,
152
+ ): () => void {
153
+ const rpc = followed.handler
154
+ const key = GLOBAL_GETS.get(rpc)?.key(args) ?? ''
155
+ const follower: Follower = {
156
+ peer,
157
+ id,
158
+ args,
159
+ key,
160
+ serving: undefined,
161
+ stopFeed: undefined,
162
+ sent: held,
163
+ admitted,
164
+ }
165
+ this.attach(rpc, follower)
166
+ let handlerFollowers = this.handlers.get(rpc)
167
+ if (handlerFollowers === undefined) {
168
+ handlerFollowers = {
169
+ tags: followed.copy?.tags,
170
+ all: new Set(),
171
+ byKey: new Map(),
172
+ }
173
+ this.handlers.set(rpc, handlerFollowers)
174
+ }
175
+ const byKey = handlerFollowers.byKey
176
+ let followers = byKey.get(key)
177
+ if (followers === undefined) {
178
+ followers = new Set()
179
+ byKey.set(key, followers)
180
+ }
181
+ followers.add(follower)
182
+ const all = handlerFollowers.all
183
+ all.add(follower)
184
+ if (this.following++ === 0) RPC_SERVER.following.add(this)
185
+ return () => {
186
+ follower.stopFeed?.()
187
+ if (all.delete(follower) && --this.following === 0)
188
+ RPC_SERVER.following.delete(this)
189
+ // A key nobody follows leaves, or the index grows with every key ever followed.
190
+ if (followers.delete(follower) && followers.size === 0)
191
+ byKey.delete(key)
192
+ }
193
+ }
194
+
195
+ /**
196
+ * `followed` followed as an event stream: the same follow a browser's mux makes, written as SSE.
197
+ *
198
+ * 18.20 — EACH MUX FRAME IS ONE EVENT, field for field: its `op` the event's name, its version
199
+ * the event's `id`, and what it carries the `data` — the value for `value`, the JSON Patch for
200
+ * `patch`, nothing for a trigger. So an `EventSource` reconnecting hands the version back as
201
+ * `Last-Event-ID`, which is `held` here, and is sent nothing until the value moves past it.
202
+ */
203
+ events(
204
+ followed: FollowedHandler,
205
+ args: unknown,
206
+ held: number | undefined,
207
+ ): ReadableStream<Uint8Array> {
208
+ let stop = (): void => {}
209
+ let unhold = (): void => {}
210
+ let ended = false
211
+ return new ReadableStream({
212
+ start: (controller) => {
213
+ controller.enqueue(EVENT_STREAM_OPENER)
214
+ // Ended by the app's stop as any body it holds open is, or that stop waits on it.
215
+ unhold = heldByApp(() => {
216
+ if (ended) return
217
+ ended = true
218
+ stop()
219
+ controller.close()
220
+ })
221
+ stop = this.follow(
222
+ {
223
+ // 18.6 — the event written and encoded once for every stream, from the body
224
+ // serialized once for every follower: nothing is parsed back.
225
+ send(_id, frame) {
226
+ controller.enqueue(frame.asEvent())
227
+ },
228
+ close() {
229
+ if (ended) return
230
+ ended = true
231
+ unhold()
232
+ controller.close()
233
+ },
234
+ },
235
+ 0,
236
+ followed,
237
+ args,
238
+ held,
239
+ // The request this stream answers passed the rungs already, or it would not be here.
240
+ true,
241
+ )
242
+ },
243
+ cancel() {
244
+ ended = true
245
+ unhold()
246
+ stop()
247
+ },
248
+ })
249
+ }
250
+
251
+ /** A trigger on `rpc`, narrowed by the pattern it was handed as the handler's own entries are. 7.10, 7.18, 13.12. */
252
+ broadcast(
253
+ rpc: object,
254
+ member: 'invalidate' | 'refresh',
255
+ pattern: unknown,
256
+ ): void {
257
+ const handlerFollowers = this.handlers.get(rpc)
258
+ if (handlerFollowers === undefined) return
259
+ for (const follower of handlerFollowers.all) {
260
+ if (!matchesPattern(follower.args, pattern)) continue
261
+ // A fed copy is refreshed by the production the reload mints (7.18), and invalidated
262
+ // through the cell it is fed from (`followServed`), whichever trigger reached it — where
263
+ // the entry it is fed from is still the one standing.
264
+ if (this.attach(rpc, follower) !== undefined) continue
265
+ follower.peer.send(follower.id, TRIGGERS[member])
266
+ }
267
+ }
268
+
269
+ /**
270
+ * 13.15 — A SWEEP REACHES EVERY COPY A BROWSER FOLLOWS that carries one of its `tags`, or every
271
+ * copy where it is bare, as that copy's own trigger. A fed copy is skipped for `broadcast`'s
272
+ * reason: the sweep reloaded the entry it is fed from here, and the production that mints is
273
+ * its refresh.
274
+ */
275
+ sweep(member: 'invalidate' | 'refresh', tags: string[] | null): void {
276
+ for (const [rpc, handlerFollowers] of this.handlers) {
277
+ if (tags !== null && !sharesTag(handlerFollowers.tags, tags))
278
+ continue
279
+ for (const follower of handlerFollowers.all) {
280
+ if (this.attach(rpc, follower) !== undefined) continue
281
+ follower.peer.send(follower.id, TRIGGERS[member])
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * 18.8 — AN ENTRY ANSWERED THAT NOBODY IS FED FROM: one made after a follower subscribed for its
288
+ * args, or rebuilt after the one it was fed from was evicted. An entry already fed is the common
289
+ * answer, and asks nothing more; one that is not reaches the followers filed under its key.
290
+ */
291
+ served(rpc: object, serving: unknown, key: string): void {
292
+ if (this.feeds.has(serving as object)) return
293
+ const followers = this.handlers.get(rpc)?.byKey.get(key)
294
+ if (followers === undefined) return
295
+ for (const follower of followers) this.attach(rpc, follower)
296
+ }
297
+
298
+ /**
299
+ * Feeds the follower from the entry standing for its args; returns what it is fed from now.
300
+ *
301
+ * Only where that entry is not the one it is fed from already.
302
+ */
303
+ private attach(rpc: object, follower: Follower): unknown {
304
+ // 18.8, 18.26 — what its own rungs or its route's refused is sent its triggers alone.
305
+ const serving = !follower.admitted
306
+ ? undefined
307
+ : GLOBAL_GETS.get(rpc)?.at(follower.key)
308
+ if (serving === follower.serving) return serving
309
+ follower.stopFeed?.()
310
+ // A version is one entry's count, so what it was sent of another says nothing about this one.
311
+ if (follower.serving !== undefined) follower.sent = undefined
312
+ follower.serving = serving
313
+ follower.stopFeed =
314
+ serving === undefined ? undefined : this.feed(serving, follower)
315
+ return serving
316
+ }
317
+
318
+ private feed(serving: unknown, fed: Follower): () => void {
319
+ const shared =
320
+ this.feeds.get(serving as object) ?? this.startFeed(serving)
321
+ shared.fed.add(fed)
322
+ if (shared.landed) sendAll(serving, [fed])
323
+ return () => {
324
+ shared.fed.delete(fed)
325
+ if (shared.fed.size === 0) shared.stop()
326
+ }
327
+ }
328
+
329
+ private startFeed(serving: unknown): Feed {
330
+ const fed = new Set<Follower>()
331
+ // 18.11 — A VALUE FED TO A FOLLOWER KEEPS ITS PATCHES AS DATA while anyone is fed it, and
332
+ // 7.10 — an invalidate of the value, however it was spelled, reaches every copy it feeds.
333
+ const release = followServed(serving, () => {
334
+ for (const one of fed) one.peer.send(one.id, TRIGGERS.invalidate)
335
+ })
336
+ const feeds = this.feeds
337
+ const shared: Feed = {
338
+ fed,
339
+ landed: false,
340
+ stop: () => {
341
+ stopWatch()
342
+ release()
343
+ feeds.delete(serving as object)
344
+ },
345
+ }
346
+ feeds.set(serving as object, shared)
347
+ // UNOWNED: the feed is stopped when its last follower leaves, and the request it may have
348
+ // been started in is answered long before that (12.5).
349
+ const stopWatch = unowned(() =>
350
+ (serving as { watch(effect: () => void): () => void }).watch(() => {
351
+ shared.landed = true
352
+ sendAll(serving, fed)
353
+ }),
354
+ )
355
+ return shared
356
+ }
357
+ }
358
+
359
+ function sharesTag(held: string[] | undefined, swept: string[]): boolean {
360
+ if (held === undefined) return false
361
+ for (const tag of swept) if (held.includes(tag)) return true
362
+ return false
363
+ }
364
+
365
+ function sendAll(serving: unknown, fed: Iterable<Follower>): void {
366
+ const version = versionOf(serving)
367
+ let valueFrame: Frame | undefined
368
+ // ONE FRAME PER VERSION A FOLLOWER STANDS AT, since followers a burst left at different places
369
+ // along the chain are each sent the writes since their own.
370
+ let patchFrames: Map<number, Frame> | undefined
371
+ for (const one of fed) {
372
+ if (one.sent === version) continue
373
+ // 18.11 — the writes, where the follower stands on the chain they were written along.
374
+ const since = one.sent
375
+ let frame = since === undefined ? undefined : patchFrames?.get(since)
376
+ if (frame === undefined && since !== undefined) {
377
+ const ops = patchesSince(serving, since)
378
+ if (ops !== undefined) {
379
+ frame = new Frame('patch', since, version, JSON.stringify(ops))
380
+ patchFrames ??= new Map()
381
+ patchFrames.set(since, frame)
382
+ }
383
+ }
384
+ if (frame === undefined) {
385
+ if (valueFrame === undefined) {
386
+ const held = (serving as { peek(): unknown }).peek()
387
+ valueFrame = new Frame(
388
+ 'value',
389
+ undefined,
390
+ version,
391
+ held === undefined ? undefined : JSON.stringify(held),
392
+ )
393
+ }
394
+ frame = valueFrame
395
+ }
396
+ one.peer.send(one.id, frame)
397
+ one.sent = version
398
+ }
399
+ }
@@ -8,8 +8,12 @@ export const COMPONENT_QUERY = '?component'
8
8
  /** The file's scoped stylesheet, which its browser module imports. 34.4. */
9
9
  export const STYLE_QUERY = '?style'
10
10
 
11
- /** The file's text. 34.21. */
12
- export const TEXT_QUERY = '?text'
11
+ /**
12
+ * The file's text, after its own path. 34.21. A SUFFIX WITH AN EXTENSION rather than a query: bun's
13
+ * runtime asks no plugin's `onResolve` of a specifier whose last segment has no extension, and
14
+ * `./Dockerfile?text` has none — bun loaded the file as JavaScript. The bundler asks either way.
15
+ */
16
+ export const TEXT_SUFFIX = '.abide-text'
13
17
 
14
18
  /**
15
19
  * The file's browser module, asked for outside a browser bundle — a component's hydration tested
@@ -0,0 +1,201 @@
1
+ // 41.13, 41.20 — ONE PAGE'S RENDER, as the calls it makes see it. A page's render calls its handlers
2
+ // in process; each call is recorded once (1.8) — left in the seed buffer for the browser's own call
3
+ // to be answered from, and written into the document's head as the request the browser will make
4
+ // for it, so it starts before the bundle has arrived. The router begins one where it renders a page,
5
+ // a call made in it records into it through `RPC_SERVER.reached`, the document takes its early-call
6
+ // script, and the browser's call takes the seed back.
7
+ //
8
+ // ON THE SCOPE because that is the only thing a call made deep in a page's setup can find it through
9
+ // when requests interleave — `RPC_SERVER.reached` is one slot for every request in flight.
10
+
11
+ import { EARLY_CALLS } from '#shared/EARLY_CALLS.ts'
12
+ import { HandlerRoute } from '#shared/HandlerRoute.ts'
13
+ import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
14
+ import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
15
+ import type { Rpc } from '#shared/rpc.ts'
16
+ import { type RpcRequest, rpcRequest } from '#shared/rpcRequest.ts'
17
+
18
+ import { callerOf, callerOrMint } from './callerOf.ts'
19
+ import { markPainted } from './deferredBuilds.ts'
20
+ import { currentRequestScope, type RequestScope } from './requestScope.ts'
21
+ import {
22
+ callsOnly,
23
+ type Outcome,
24
+ seedFor,
25
+ type SeedSink,
26
+ takeSeed,
27
+ } from './seedBuffer.ts'
28
+
29
+ /**
30
+ * One call a page's render made: the request the browser will make for it, what it reached, and
31
+ * whether the markup was built from it settled — `undefined` until the pass that built from it
32
+ * ends (41.25, `markPainted`).
33
+ */
34
+ export type PageCall = {
35
+ request: RpcRequest
36
+ reached: unknown
37
+ painted: boolean | undefined
38
+ }
39
+
40
+ // SYNCHRONOUS SETUP IS WHAT MAKES ONE COUNTER ENOUGH. 34.3 keeps a setup from awaiting, so no other
41
+ // request's render runs between a component's two reads, however many renders are in flight.
42
+ let callsInRenders = 0
43
+
44
+ /**
45
+ * How many calls renders have made, read on either side of a component's setup.
46
+ *
47
+ * 41.28 — what tells a child whose setup made a call, which is built after the turn as a page is.
48
+ */
49
+ export function callsInRender(): number {
50
+ return callsInRenders
51
+ }
52
+
53
+ // 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
54
+ const SEEDS_NOTHING: SeedSink = () => false
55
+
56
+ // Each handler's route, compiled once rather than per render that calls it — as its `Rpc` keeps its
57
+ // own; compiled again only where it was mounted at another address since.
58
+ const ROUTE_OF = new WeakMap<object, HandlerRoute>()
59
+
60
+ /** A page's render: the seed sink its calls report into, and the calls the browser will make. */
61
+ export class PageRender {
62
+ readonly calls: PageCall[] = []
63
+ private readonly seed: SeedSink
64
+
65
+ constructor(seed: SeedSink) {
66
+ this.seed = seed
67
+ }
68
+
69
+ /**
70
+ * Records a call the render made: into the seed buffer (41.13), and — where the handler is
71
+ * mounted — as the request the browser will make for it, for the early-call script (41.20). Once
72
+ * per render, as the browser's `Rpc` makes it once, and the seed sink is what says which is the
73
+ * once.
74
+ */
75
+ record(rpc: object, args: unknown, reached: unknown): void {
76
+ callsInRenders += 1
77
+ if (!this.seed(rpc, args, reached)) return
78
+ // 16.10 — the address the browser's own call is made at, stamped under the mount by
79
+ // `mountHandlers` — and `undefined` on a handler nothing mounted.
80
+ const { address, method } = rpc as Rpc<unknown, unknown>
81
+ if (address === undefined) return
82
+ let route = ROUTE_OF.get(rpc)
83
+ if (route?.address !== address) {
84
+ route = new HandlerRoute(address)
85
+ ROUTE_OF.set(rpc, route)
86
+ }
87
+ this.calls.push({
88
+ request: rpcRequest(
89
+ method,
90
+ route,
91
+ args as Record<string, unknown> | undefined,
92
+ ),
93
+ reached,
94
+ painted: undefined,
95
+ })
96
+ }
97
+
98
+ /**
99
+ * The head markup that starts each of the page's calls before the bundle arrives, or `''` where
100
+ * it made none — every call marked first as the markup just built read it (41.25).
101
+ *
102
+ * 41.20 — THE SCRIPT ITSELF: each call started with `fetch`, and its answer left under the call's
103
+ * key in `EARLY_CALLS` for `wire` to take (41.21). An answer's body is left UNREAD (16.32), so a
104
+ * streaming handler's copy takes its chunks as they land rather than after the last one.
105
+ * Inline because nothing else runs before the bundle has arrived, which is the wait it exists
106
+ * to overlap.
107
+ *
108
+ * 41.25 — A ROW ENDING IN `1` IS A CALL THE MARKUP WAS BUILT FROM SETTLED, a refusal included,
109
+ * and its entry is marked `painted` for hydration to wait on — its body read to its bytes, so
110
+ * the wait covers it too. 41.26 — A CALL THAT STREAMS IS NEVER MARKED: the browser reads its
111
+ * chunks as they land (16.32), so no wait for its answer makes it settled where the page
112
+ * hydrates, and its region is 41.24's to put right.
113
+ *
114
+ * 29.9, 29.10 — THE CALLS ARE DATA AND THE RUNNER IS A CONSTANT. What a page called differs per
115
+ * response, so it rides in a JSON block a browser never executes, and the one script that does
116
+ * run is the same bytes in every response — which is what lets `csp` permit it by its hash
117
+ * rather than by the nonce an app's own inline script spends. The JSON has `<` escaped, so no
118
+ * string in it can close the element.
119
+ */
120
+ earlyCallScript(): string {
121
+ const calls = this.calls
122
+ markPainted(calls)
123
+ if (calls.length === 0) return ''
124
+ const rows: (
125
+ [string, string, RequestInit] | [string, string, RequestInit, 1]
126
+ )[] = []
127
+ for (const { request, painted } of calls)
128
+ rows.push(
129
+ painted === true
130
+ ? [request.key, request.url, request.init, 1]
131
+ : [request.key, request.url, request.init],
132
+ )
133
+ return `<script type="application/json">${JSON.stringify(rows).replace(/</g, '\\u003c')}</script><script>${EARLY_CALL_RUNNER}</script>`
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Begins the render of the page `request` asks for, in its scope, and returns it.
139
+ *
140
+ * 26.5 — the caller the seed buffer is keyed by is minted where the browser carries none: the same
141
+ * one `principal.caller` reads for the rest of the request. 41.13 — every call the render makes is
142
+ * left for the browser's own call to be answered from. 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which
143
+ * nothing hydrates: its entries would answer the next call this caller makes with what the render
144
+ * saw, a sign-in since included — save the one the worker fetched for the navigation it answers,
145
+ * which is the page that hydrates (45.23). 41.30 — SAVE ONE THE WORKER KEEPS, whose early calls are
146
+ * what a later open of it starts.
147
+ */
148
+ export function beginPageRender(request: Request): PageRender {
149
+ const caller = callerOrMint()
150
+ const keep = request.headers.get(KEEP_HEADER)
151
+ const hydrated =
152
+ keep === KEEP_OPENED ||
153
+ request.headers.get('sec-fetch-dest') !== 'empty'
154
+ const page = new PageRender(
155
+ hydrated
156
+ ? seedFor(caller)
157
+ : keep !== null
158
+ ? callsOnly()
159
+ : SEEDS_NOTHING,
160
+ )
161
+ ;(currentRequestScope() as RequestScope).page = page
162
+ return page
163
+ }
164
+
165
+ /** Renders on in this scope as no page's: an error page's render records nothing. 41.13. */
166
+ export function leavePageRender(): void {
167
+ ;(currentRequestScope() as RequestScope).page = undefined
168
+ }
169
+
170
+ /** Records a call made in the page this scope is rendering, where it renders one. */
171
+ export function recordInRender(
172
+ rpc: object,
173
+ args: unknown,
174
+ reached: unknown,
175
+ ): void {
176
+ currentRequestScope()?.page?.record(rpc, args, reached)
177
+ }
178
+
179
+ /**
180
+ * Takes what this request's caller's render reached for `rpc` and `args`, or `undefined`.
181
+ *
182
+ * 41.7 — the call a hydrating page makes for what its render already reached is answered from
183
+ * that, and the handler is not run again (41.13).
184
+ */
185
+ export function takeSeeded(
186
+ rpc: object,
187
+ args: unknown,
188
+ ): Promise<Outcome> | undefined {
189
+ const caller = callerOf()
190
+ return caller === undefined ? undefined : takeSeed(caller, rpc, args)
191
+ }
192
+
193
+ // THE RUNNER RECORDS, IT NEVER CLASSIFIES: the `Response`, and a painted call's body read to its end
194
+ // — the one decision is `painted`, which the server made. `answerOf` reads the record as it reads an
195
+ // answer it fetched, so no second reading of a content type lives in a string no typecheck reaches.
196
+ // The shape is `EarlyCall`'s. 16.80 — a page the worker answers has it answer the page's calls too,
197
+ // which the browser alone can tell: the server cannot see whether a worker will replay this page.
198
+ export const EARLY_CALL_RUNNER = `(function(s,c,w){for(var i=0;i<c.length;i++)(function(k,u,n,h){if(w)n.cache='force-cache';var e=s[k]={painted:h,response:void 0,bytes:void 0};e.promise=fetch(u,n).then(function(r){return h?r.arrayBuffer().then(function(b){e.bytes=b;e.response=r}):e.response=r}).catch(function(){})})(c[i][0],c[i][1],c[i][2],c[i][3])})(self.${EARLY_CALLS}||(self.${EARLY_CALLS}={}),JSON.parse(document.currentScript.previousElementSibling.textContent),(self.navigator.serviceWorker||{}).controller)`
199
+
200
+ /** The early-call runner's hash, as `script-src` names it. 29.9. */
201
+ export const EARLY_CALL_HASH = `'sha256-${new Bun.CryptoHasher('sha256').update(EARLY_CALL_RUNNER).digest('base64')}'`
@@ -0,0 +1,2 @@
1
+ /** How long a pairing whose stream dropped waits to be resumed. 44.26. */
2
+ export const RESUME_MILLISECONDS = 60_000
@@ -6,9 +6,9 @@
6
6
  //
7
7
  // WHICH FACE AN IMPORT GETS IS SPELLED ON ITS SPECIFIER (`MODULE_QUERIES`), because bun hands a plugin
8
8
  // no import attributes: a compiled module asks for `?component`, a browser module for its own
9
- // `?style`, a text import for `?text`. A `.abide` named bare is a module abide did not compile
10
- // asking — on a server that is the bound face 34.16 states, and in a browser the component module
11
- // itself.
9
+ // `?style`, a text import for `.abide-text` (`TEXT_SUFFIX`). A `.abide` named bare is a module abide
10
+ // did not compile asking — on a server that is the bound face 34.16 states, and in a browser the
11
+ // component module itself.
12
12
  //
13
13
  // THE TABLES — the route, error and handler tables, and in a browser the router's table and the
14
14
  // entry — are the build's own modules, served from `__abide` below the root, where no app writes.
@@ -22,7 +22,6 @@ import { basename, dirname, resolve } from 'node:path' // bun exposes no path he
22
22
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
23
23
 
24
24
  import type { ServerTable } from './answerRequest.ts'
25
- import type { MountRow } from './answerRpc.ts'
26
25
  import {
27
26
  type Asked,
28
27
  BOUND_NAMESPACE,
@@ -44,9 +43,10 @@ import {
44
43
  HANDLER_SUFFIX,
45
44
  SERVER_QUERY,
46
45
  STYLE_QUERY,
47
- TEXT_QUERY,
46
+ TEXT_SUFFIX,
48
47
  } from './MODULE_QUERIES.ts'
49
48
  import { MOUNTABLE } from './MOUNTABLE.ts'
49
+ import type { MountRow } from './mountHandlers.ts'
50
50
  import { PRIVATE_EXPORT } from './PRIVATE_EXPORT.ts'
51
51
  import { realOrAsGiven } from './realOrAsGiven.ts'
52
52
  import { SERVER_TABLE } from './TABLES.ts'
@@ -293,8 +293,8 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
293
293
  new RegExp(`\\.abide${RegExp.escape(query)}$`)
294
294
  asking(
295
295
  'text',
296
- TEXT_QUERY,
297
- new RegExp(`${RegExp.escape(TEXT_QUERY)}$`),
296
+ TEXT_SUFFIX,
297
+ new RegExp(`${RegExp.escape(TEXT_SUFFIX)}$`),
298
298
  )
299
299
  asking('style', STYLE_QUERY, abideWith(STYLE_QUERY))
300
300
  asking('component', COMPONENT_QUERY, abideWith(COMPONENT_QUERY))
@@ -10,9 +10,10 @@ import { coversRoute, parseRoutePattern } from '#shared/routePattern.ts'
10
10
 
11
11
  import type { AppSources } from './answerRequest.ts'
12
12
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
13
+ import { leavePageRender } from './PageRender.ts'
13
14
  import { prefers } from './prefers.ts'
14
15
  import { boundPage, renderDocument } from './render.ts'
15
- import { beginRender, currentRequestScope } from './requestScope.ts'
16
+ import { currentRequestScope } from './requestScope.ts'
16
17
 
17
18
  /** What a page's failure is painted from: the error pages, and the document they go into. */
18
19
  export type ErrorPages = Pick<AppSources, 'errors' | 'errorDocument'>
@@ -88,7 +89,7 @@ export async function answerFailure(
88
89
  for (const error of errorsFor(app.errors, pathname)) {
89
90
  try {
90
91
  const bound = boundPage(error, { failure: () => failure })
91
- beginRender(undefined)
92
+ leavePageRender()
92
93
  const document = await renderDocument(bound, app.errorDocument)
93
94
  return new Response(document, { status, headers })
94
95
  } catch (thrown) {