@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
@@ -0,0 +1,308 @@
1
+ // 38.70 — THE WINDOW `abide bundle`'S EXECUTABLE OPENS, over AppKit through `bun:ffi`: a WKWebView, the
2
+ // page's menu bar and an event drain. What it shows is the `DesktopSession`'s to decide.
3
+ //
4
+ // AppKit's events are drained on a timer rather than by `[NSApp run]`, which would never hand the
5
+ // thread back to bun. A menu held open or a window being resized runs AppKit's own loop inside one
6
+ // drain, and the app answers nothing until it ends.
7
+
8
+ import { FFIType } from 'bun:ffi'
9
+
10
+ import {
11
+ callBlock,
12
+ created,
13
+ definedClass,
14
+ type Id,
15
+ nsBoolean,
16
+ nsString,
17
+ objcClass,
18
+ selector,
19
+ send,
20
+ sendTyped,
21
+ stringOf,
22
+ } from './appKit.ts'
23
+ import type { DesktopSession } from './DesktopSession.ts'
24
+ import { reportUnexpected } from './hooks.ts'
25
+ import type { Launcher } from './launcherOptions.ts'
26
+ import { installMenu, menuBar, type NativeItem } from './nativeMenu.ts'
27
+
28
+ const DRAIN_MILLISECONDS = 8
29
+
30
+ /**
31
+ * Opens the app's window on a blank page and answers how to point it at an address.
32
+ *
33
+ * `base` is the launcher's own page, which the Connect… and Disconnect menu items lead back to.
34
+ * Quitting stops the session's app before the process ends (38.76).
35
+ */
36
+ export function openDesktopWindow(
37
+ appName: string,
38
+ options: Launcher,
39
+ base: string,
40
+ session: DesktopSession,
41
+ ): (address: string) => void {
42
+ const application = send(objcClass('NSApplication'), 'sharedApplication')
43
+ // Regular: a Dock icon and a menu bar, which a process outside a `.app` does not get by default.
44
+ sendTyped(
45
+ application,
46
+ 'setActivationPolicy:',
47
+ [FFIType.i64],
48
+ FFIType.void,
49
+ 0,
50
+ )
51
+ let quitting = false
52
+ const quit = (): void => {
53
+ if (quitting) return
54
+ quitting = true
55
+ session.disconnect().finally(() => process.exit(0))
56
+ }
57
+ send(
58
+ application,
59
+ 'setDelegate:',
60
+ send(
61
+ definedClass('AbideApplicationDelegate', {
62
+ 'applicationShouldTerminate:': {
63
+ types: 'Q@:@',
64
+ args: [FFIType.ptr],
65
+ returns: FFIType.u64,
66
+ // NSTerminateCancel: 38.76 — the app is stopped first, and the process ends after.
67
+ run: () => {
68
+ quit()
69
+ return 0
70
+ },
71
+ },
72
+ 'applicationShouldTerminateAfterLastWindowClosed:': {
73
+ types: 'c@:@',
74
+ args: [FFIType.ptr],
75
+ returns: FFIType.bool,
76
+ run: () => true,
77
+ },
78
+ }),
79
+ 'new',
80
+ ),
81
+ )
82
+ for (const signal of ['SIGTERM', 'SIGINT'] as const)
83
+ process.on(signal, quit)
84
+ // A desktop app outlives a fault in it: it is reported, and the window stays.
85
+ process.on('unhandledRejection', reportUnexpected)
86
+ process.on('uncaughtException', reportUnexpected)
87
+
88
+ const window = created('NSWindow')
89
+ // Titled, closable, miniaturizable, resizable.
90
+ sendTyped(window, 'setStyleMask:', [FFIType.u64], FFIType.void, 15)
91
+ sendTyped(
92
+ window,
93
+ 'setReleasedWhenClosed:',
94
+ [FFIType.bool],
95
+ FFIType.void,
96
+ false,
97
+ )
98
+ sendTyped(
99
+ window,
100
+ 'setContentSize:',
101
+ [FFIType.f64, FFIType.f64],
102
+ FFIType.void,
103
+ options.width,
104
+ options.height,
105
+ )
106
+ sendTyped(
107
+ window,
108
+ 'setContentMinSize:',
109
+ [FFIType.f64, FFIType.f64],
110
+ FFIType.void,
111
+ options.minWidth,
112
+ options.minHeight,
113
+ )
114
+ send(window, 'center')
115
+ // Where the window was and how large, as the last launch left it.
116
+ sendTyped(
117
+ window,
118
+ 'setFrameAutosaveName:',
119
+ [FFIType.ptr],
120
+ FFIType.bool,
121
+ nsString(`${appName} window`),
122
+ )
123
+ send(window, 'setTitle:', nsString(appName))
124
+
125
+ const view = created('WKWebView')
126
+ send(
127
+ send(send(view, 'configuration'), 'preferences'),
128
+ 'setValue:forKey:',
129
+ nsBoolean(true),
130
+ nsString('developerExtrasEnabled'),
131
+ )
132
+ if (
133
+ sendTyped(
134
+ view,
135
+ 'respondsToSelector:',
136
+ [FFIType.ptr],
137
+ FFIType.bool,
138
+ selector('setInspectable:'),
139
+ )
140
+ )
141
+ sendTyped(view, 'setInspectable:', [FFIType.bool], FFIType.void, true)
142
+ send(view, 'setUIDelegate:', send(uiDelegateClass(), 'new'))
143
+ send(window, 'setContentView:', view)
144
+ send(
145
+ view,
146
+ 'loadHTMLString:baseURL:',
147
+ nsString(
148
+ '<!doctype html><style>:root{color-scheme:light dark;background:#f5f5f7}@media (prefers-color-scheme:dark){:root{background:#1c1c1e}}</style>',
149
+ ),
150
+ null,
151
+ )
152
+ // 38.88 — THE MENU BAR IS THE PAGE'S: the defaults until the page the window loads tells it
153
+ // what its route exports, and again after every navigation it makes.
154
+ const showMenu = (told: NativeItem[]): void =>
155
+ installMenu(menuBar(appName, options.connect, told), (item) => {
156
+ if (item.role === 'connect')
157
+ return navigate(`${base}?focus=connect`)
158
+ // 38.74.
159
+ if (item.role === 'disconnect')
160
+ return void session.forget().then(() => navigate(base))
161
+ if (item.role === 'reload') return void send(view, 'reload')
162
+ // 38.90, 38.91 — the page that numbered the item does what it means.
163
+ if (item.id !== undefined)
164
+ send(
165
+ view,
166
+ 'evaluateJavaScript:completionHandler:',
167
+ nsString(
168
+ `dispatchEvent(new CustomEvent('abide:menu', { detail: ${item.id} }))`,
169
+ ),
170
+ null,
171
+ )
172
+ })
173
+ send(
174
+ send(send(view, 'configuration'), 'userContentController'),
175
+ 'addScriptMessageHandler:name:',
176
+ send(
177
+ definedClass('AbideMenuBridge', {
178
+ 'userContentController:didReceiveScriptMessage:': {
179
+ types: 'v@:@@',
180
+ args: [FFIType.ptr, FFIType.ptr],
181
+ returns: FFIType.void,
182
+ run: (_controller: Id, message: Id) => {
183
+ try {
184
+ showMenu(
185
+ JSON.parse(
186
+ stringOf(send(message, 'body')),
187
+ ) as NativeItem[],
188
+ )
189
+ } catch (thrown) {
190
+ reportUnexpected(thrown)
191
+ }
192
+ },
193
+ },
194
+ }),
195
+ 'new',
196
+ ),
197
+ nsString('abideMenu'),
198
+ )
199
+ const navigate = (address: string): void => {
200
+ // 38.92.
201
+ showMenu([])
202
+ send(
203
+ view,
204
+ 'loadRequest:',
205
+ send(
206
+ objcClass('NSURLRequest'),
207
+ 'requestWithURL:',
208
+ send(objcClass('NSURL'), 'URLWithString:', nsString(address)),
209
+ ),
210
+ )
211
+ }
212
+ showMenu([])
213
+
214
+ send(window, 'makeKeyAndOrderFront:', null)
215
+ sendTyped(
216
+ application,
217
+ 'activateIgnoringOtherApps:',
218
+ [FFIType.bool],
219
+ FFIType.void,
220
+ true,
221
+ )
222
+ send(application, 'finishLaunching')
223
+
224
+ const distantPast = send(objcClass('NSDate'), 'distantPast')
225
+ const defaultMode = nsString('kCFRunLoopDefaultMode')
226
+ let shownTitle = appName
227
+ let drains = 0
228
+ setInterval(() => {
229
+ for (;;) {
230
+ const event = sendTyped(
231
+ application,
232
+ 'nextEventMatchingMask:untilDate:inMode:dequeue:',
233
+ [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.bool],
234
+ FFIType.ptr,
235
+ 0xffffffffffffffffn,
236
+ distantPast,
237
+ defaultMode,
238
+ true,
239
+ ) as Id
240
+ if (event === null) break
241
+ send(application, 'sendEvent:', event)
242
+ }
243
+ // The window is titled by the page it shows.
244
+ drains += 1
245
+ if (drains % 32 !== 0) return
246
+ const title = stringOf(send(view, 'title')) || appName
247
+ if (title === shownTitle) return
248
+ shownTitle = title
249
+ send(window, 'setTitle:', nsString(title))
250
+ }, DRAIN_MILLISECONDS)
251
+ return navigate
252
+ }
253
+
254
+ // What a page asks of its window: a link out opens in the browser, and `alert` and `confirm` are
255
+ // sheets the page waits on.
256
+ let uiDelegate: Id = null
257
+ function uiDelegateClass(): Id {
258
+ uiDelegate ??= definedClass('AbideUIDelegate', {
259
+ 'webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:':
260
+ {
261
+ types: '@@:@@@@',
262
+ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
263
+ returns: FFIType.ptr,
264
+ run: (_view: Id, _configuration: Id, action: Id) => {
265
+ const url = send(send(action, 'request'), 'URL')
266
+ if (url !== null)
267
+ send(
268
+ send(objcClass('NSWorkspace'), 'sharedWorkspace'),
269
+ 'openURL:',
270
+ url,
271
+ )
272
+ return null
273
+ },
274
+ },
275
+ 'webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:':
276
+ {
277
+ types: 'v@:@@@@?',
278
+ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
279
+ returns: FFIType.void,
280
+ run: (_view: Id, message: Id, _frame: Id, done: Id) => {
281
+ alert(message, false)
282
+ callBlock(done, [])
283
+ },
284
+ },
285
+ 'webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:':
286
+ {
287
+ types: 'v@:@@@@?',
288
+ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
289
+ returns: FFIType.void,
290
+ run: (_view: Id, message: Id, _frame: Id, done: Id) => {
291
+ callBlock(done, [FFIType.bool], alert(message, true))
292
+ },
293
+ },
294
+ })
295
+ return uiDelegate
296
+ }
297
+
298
+ // An `NSAlert` with the page's message; answers whether it was accepted.
299
+ function alert(message: Id, cancellable: boolean): boolean {
300
+ const panel = created('NSAlert')
301
+ send(panel, 'setMessageText:', message)
302
+ send(panel, 'addButtonWithTitle:', nsString('OK'))
303
+ if (cancellable) send(panel, 'addButtonWithTitle:', nsString('Cancel'))
304
+ // NSAlertFirstButtonReturn.
305
+ return (
306
+ Number(sendTyped(panel, 'runModal', [], FFIType.i64) as bigint) === 1000
307
+ )
308
+ }
@@ -5,6 +5,10 @@
5
5
  // one place that runs first. The build is `createApp`'s own under `bun test` (42.16), so a test that
6
6
  // boots the app afterwards reuses it rather than building twice.
7
7
 
8
+ import { beforeEach } from 'bun:test'
9
+
10
+ import { invalidate } from '#shared/reactive/index.ts'
11
+
8
12
  import { processBuild, servedBuild, spawnedBuild } from './appBuild.ts'
9
13
  import { addDerived } from './config.ts'
10
14
  import { appRoot } from './createApp.ts'
@@ -20,6 +24,20 @@ const root = appRoot(undefined)
20
24
  if (root !== undefined && !(await isWorkspaceRoot(root)))
21
25
  await servedBuild(root, await processBuild(root, true), spawnedBuild)
22
26
 
27
+ // 38.133 — EACH TEST FILE STARTS COLD: a handler's answer another file settled is otherwise served
28
+ // at once to the next file in the process, so a file's tests pass or fail by the order `bun test`
29
+ // happened to run the files in. Bun has no per-file hook a preload can register, so a test's own
30
+ // `beforeEach` notices the file change through `Bun.main`. Outside the test runner — a bench or a
31
+ // script importing `mount`'s module — there is no runner to register with, and nothing to reset.
32
+ let fileUnderTest: string | undefined
33
+ try {
34
+ beforeEach(() => {
35
+ if (Bun.main === fileUnderTest) return
36
+ if (fileUnderTest !== undefined) invalidate()
37
+ fileUnderTest = Bun.main
38
+ })
39
+ } catch {}
40
+
23
41
  async function isWorkspaceRoot(directory: string): Promise<boolean> {
24
42
  const manifest = Bun.file(`${directory}/package.json`)
25
43
  if (!(await manifest.exists())) return false
@@ -18,10 +18,10 @@ import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
18
18
  import { refuse } from '#shared/refuse.ts'
19
19
  import { declare } from '#shared/rpc.ts'
20
20
 
21
- import { mountedOwn } from './answerRpc.ts'
22
21
  import { callerOf, callerOrMint, mintCaller } from './callerOf.ts'
23
22
  import { config, positiveSetting } from './config.ts'
24
23
  import { HOOKS, inOrder } from './hooks.ts'
24
+ import { mountedOwn } from './mountHandlers.ts'
25
25
  import { type RequestScope, type Sealed, within } from './requestScope.ts'
26
26
  import { signInChanged } from './signInChanged.ts'
27
27
 
@@ -12,8 +12,8 @@ import { rankRoutes } from '#shared/routePattern.ts'
12
12
  import type { Method } from '#shared/rpc.ts'
13
13
  import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
14
14
 
15
- import type { Mount } from './answerRpc.ts'
16
15
  import { DEFAULT_PORT } from './DEFAULT_PORT.ts'
16
+ import type { Mount } from './mountHandlers.ts'
17
17
  import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
18
18
 
19
19
  /**
@@ -4,14 +4,14 @@
4
4
  /// <reference path="./componentModules.d.ts" />
5
5
 
6
6
  import { regionMarkup } from '#shared/anchorMarkup.ts'
7
- import { EARLY_CALLS } from '#shared/EARLY_CALLS.ts'
8
7
  import { headKeyOf } from '#shared/headKeyOf.ts'
9
8
  import { isThenable } from '#shared/reactive/isThenable.ts'
9
+ import { runInScope } from '#shared/reactive/scoping.ts'
10
10
  import type { ShareScope } from '#shared/reactive/share.ts'
11
11
  import { setUpLevels } from '#shared/setUpLevels.ts'
12
12
 
13
- import { buildDeferred, markPainted } from './deferredBuilds.ts'
14
- import { currentRequestScope, type PageCall } from './requestScope.ts'
13
+ import { buildDeferred } from './deferredBuilds.ts'
14
+ import { currentRequestScope } from './requestScope.ts'
15
15
 
16
16
  /**
17
17
  * A compiled component, opaque to an app.
@@ -265,23 +265,42 @@ function dropAtHeadTop(
265
265
  * is bound the same way, with the failure where a page has its params.
266
266
  */
267
267
  export function boundPage(
268
- page: {
269
- load: () => Promise<{ default: Component }>
270
- layouts: (() => Promise<{ default: Component }>)[]
271
- },
268
+ page: PageModules,
272
269
  props: Record<string, () => unknown>,
273
270
  ): () => Promise<unknown> {
274
271
  return async () => {
275
- const [loadedPage, loadedLayouts] = await Promise.all([
276
- page.load(),
277
- Promise.all(page.layouts.map((layout) => layout())),
278
- ])
272
+ const [loadedPage, loadedLayouts] = await runInScope(
273
+ undefined,
274
+ loadedModules,
275
+ page,
276
+ )
279
277
  const layouts: Component[] = []
280
278
  for (const module of loadedLayouts) layouts.push(module.default)
281
279
  return composePage(layouts, loadedPage.default, props)
282
280
  }
283
281
  }
284
282
 
283
+ type PageModules = {
284
+ load: () => Promise<{ default: Component }>
285
+ layouts: (() => Promise<{ default: Component }>)[]
286
+ }
287
+
288
+ /**
289
+ * A page's module and its layouts', imported OUTSIDE EVERY REQUEST — 11.67. A module is evaluated in
290
+ * the async context its `import()` was called from, so the first request to reach a page evaluated
291
+ * it, and every module it imports, inside that request's scope: a module's `reactive` saw a scope at
292
+ * construction, was built as that request's own rather than as a forwarder (`scoped.ts`), and every
293
+ * later request read what the first one left.
294
+ */
295
+ function loadedModules(
296
+ page: PageModules,
297
+ ): Promise<[{ default: Component }, { default: Component }[]]> {
298
+ return Promise.all([
299
+ page.load(),
300
+ Promise.all(page.layouts.map((layout) => layout())),
301
+ ])
302
+ }
303
+
285
304
  /**
286
305
  * The document's bytes: `component` rendered into abide's own document (35.6).
287
306
  *
@@ -326,10 +345,7 @@ export async function renderDocument(
326
345
  // 41.20 — THE PAGE'S CALLS LEAVE AHEAD OF ITS MARKUP. Setup has run, so every call the page
327
346
  // makes is known, and a script in the head starts each one as the request the browser's own
328
347
  // `Rpc` would make — while the rest of the document and the bundle are still on their way.
329
- const calls = currentRequestScope()?.page?.calls
330
- if (calls !== undefined) markPainted(calls)
331
- const early =
332
- calls !== undefined && calls.length > 0 ? earlyCallScript(calls) : ''
348
+ const early = currentRequestScope()?.page?.earlyCallScript() ?? ''
333
349
  // 39.4 — THE SHELL IS THE OUTERMOST CONTRIBUTOR, so its head is merged with the page's in the
334
350
  // one pass that merges the levels'.
335
351
  //
@@ -342,50 +358,6 @@ export async function renderDocument(
342
358
  )
343
359
  }
344
360
 
345
- /**
346
- * The head markup that starts each of the page's calls before the bundle arrives.
347
- *
348
- * 41.20 — THE SCRIPT ITSELF: each call started with `fetch`, and its answer left under the call's
349
- * key in `EARLY_CALLS` for `wire` to take (41.21). An answer's body is left UNREAD (16.32), so a
350
- * streaming handler's copy takes its chunks as they land rather than after the last one.
351
- * Inline because nothing else runs before the bundle has arrived, which is the wait it exists to
352
- * overlap.
353
- *
354
- * 41.25 — A ROW ENDING IN `1` IS A CALL THE MARKUP WAS BUILT FROM SETTLED, a refusal included, and
355
- * its entry is marked `painted` for hydration to wait on — its body read to its bytes, so the wait
356
- * covers it too. Marked as each pass of the markup is built (`markPainted`), not here. 41.26 — A CALL THAT STREAMS IS NEVER MARKED: the
357
- * browser reads its chunks as they land (16.32), so no wait for its answer makes it settled where
358
- * the page hydrates, and its region is 41.24's to put right.
359
- *
360
- * 29.9, 29.10 — THE CALLS ARE DATA AND THE RUNNER IS A CONSTANT. What a page called differs per
361
- * response, so it rides in a JSON block a browser never executes, and the one script that does run
362
- * is the same bytes in every response — which is what lets `csp` permit it by its hash rather than
363
- * by the nonce an app's own inline script spends. The JSON has `<` escaped, so no string in it can
364
- * close the element.
365
- */
366
- function earlyCallScript(calls: PageCall[]): string {
367
- const rows: (
368
- [string, string, RequestInit] | [string, string, RequestInit, 1]
369
- )[] = []
370
- for (const { request, painted } of calls)
371
- rows.push(
372
- painted === true
373
- ? [request.key, request.url, request.init, 1]
374
- : [request.key, request.url, request.init],
375
- )
376
- return `<script type="application/json">${JSON.stringify(rows).replace(/</g, '\\u003c')}</script><script>${EARLY_CALL_RUNNER}</script>`
377
- }
378
-
379
- // THE RUNNER RECORDS, IT NEVER CLASSIFIES: the `Response`, and a painted call's body read to its end
380
- // — the one decision is `painted`, which the server made. `answerOf` reads the record as it reads an
381
- // answer it fetched, so no second reading of a content type lives in a string no typecheck reaches.
382
- // The shape is `EarlyCall`'s. 16.80 — a page the worker answers has it answer the page's calls too,
383
- // which the browser alone can tell: the server cannot see whether a worker will replay this page.
384
- 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)`
385
-
386
- /** The early-call runner's hash, as `script-src` names it. 29.9. */
387
- export const EARLY_CALL_HASH = `'sha256-${new Bun.CryptoHasher('sha256').update(EARLY_CALL_RUNNER).digest('base64')}'`
388
-
389
361
  /** Where the body's content ends, which is where a page's markup goes. */
390
362
  function bodyAnchor(document: string): number {
391
363
  const found = document.lastIndexOf('</body>')
@@ -16,10 +16,11 @@ import type { Framed } from '#shared/framing.ts'
16
16
  import type { Params } from '#shared/matchRoute.ts'
17
17
  import type { Principal } from '#shared/principal.ts'
18
18
  import { type Scope, SCOPING } from '#shared/reactive/scoping.ts'
19
- import type { RpcRequest } from '#shared/rpcRequest.ts'
20
19
 
20
+ import type { Followers } from './Followers.ts'
21
21
  import type { McpSurface } from './generatedSurfaces.ts'
22
22
  import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
23
+ import type { PageRender } from './PageRender.ts'
23
24
 
24
25
  /** The three framings a stream is answered in. 17.6. */
25
26
  export type Framing = Framed | 'bytes'
@@ -81,7 +82,7 @@ export type RequestScope = Scope & {
81
82
  /** What a write through `response` is collected onto, for whoever answers to apply. 22.9. */
82
83
  response: { status: number; headers: Headers }
83
84
  /**
84
- * The page this scope is rendering, which its calls report into.
85
+ * The page this scope is rendering, which its calls report into (`PageRender.ts`).
85
86
  *
86
87
  * 41.13, 41.20 — `undefined` in every scope but a page's.
87
88
  */
@@ -103,19 +104,6 @@ export type RequestScope = Scope & {
103
104
  }
104
105
  }
105
106
 
106
- /**
107
- * A page's render, as its calls see it: the seed sink and the requests the browser will make.
108
- *
109
- * The seed buffer's sink for this caller (41.13), and the requests the browser will make for the
110
- * calls, for the early-call script (41.20). ON THE SCOPE because that is the only thing a call made
111
- * deep in a page's setup can find it through when requests interleave — `RPC_SERVER.reached` is
112
- * one slot for every request in flight.
113
- */
114
- export type PageRender = {
115
- seed: (rpc: object, args: unknown, reached: unknown) => boolean
116
- calls: PageCall[]
117
- }
118
-
119
107
  /** An app as the scopes it opens hold it: what answers a request in it, and its MCP surface. */
120
108
  export type AnsweringApp = {
121
109
  answer: (request: Request) => Promise<Response>
@@ -125,17 +113,8 @@ export type AnsweringApp = {
125
113
  * still being written, a seed entry and its timer. 42.11 — `App.stop` ends every one.
126
114
  */
127
115
  held: Set<() => void>
128
- }
129
-
130
- /**
131
- * One call a page's render made: the request the browser will make for it, what it reached, and
132
- * whether the markup was built from it settled — `undefined` until the pass that built from it
133
- * ends (41.25, `markPainted`).
134
- */
135
- export type PageCall = {
136
- request: RpcRequest
137
- reached: unknown
138
- painted: boolean | undefined
116
+ /** Every subscription to this app's handlers, over its mux or as an event stream. 18.3. */
117
+ followers: Followers
139
118
  }
140
119
 
141
120
  // `undefined` IS A STORE, not the absence of one: a value built outside every request runs its body
@@ -327,16 +306,6 @@ export const LISTENING: {
327
306
  LISTENING.server?.timeout(received, seconds),
328
307
  }
329
308
 
330
- /**
331
- * Sets what the calls a render makes report into, on the request's own scope.
332
- *
333
- * 41.13 — set before the render runs: a page's render passes one, and an error page's none. The
334
- * render is awaited where the request is answered, so it runs in that scope throughout (42.6).
335
- */
336
- export function beginRender(page: PageRender | undefined): void {
337
- ;(STORAGE.getStore() as RequestScope).page = page
338
- }
339
-
340
309
  /** The scope a request or `App.run` opened, or `undefined` outside every one. */
341
310
  export function currentRequestScope(): RequestScope | undefined {
342
311
  return STORAGE.getStore()