@abide/abide 0.123.3 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +31 -0
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +17 -0
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +71 -1715
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +44 -17
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/formatAbide.ts +5 -6
- package/src/compiler/handlersUnder.ts +16 -35
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readsIn.ts +180 -81
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +26 -20
- package/src/compiler/splitImports.ts +2 -2
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +6 -70
- package/src/server/answerRpc.ts +48 -260
- package/src/server/answerSocket.ts +41 -463
- package/src/server/appKit.ts +1 -1
- package/src/server/argsContract.ts +1 -1
- package/src/server/createApp.ts +8 -2
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/preload.ts +18 -0
- package/src/server/principal.ts +1 -1
- package/src/server/remote.ts +1 -1
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runDesktop.ts +19 -447
- package/src/server/seedBuffer.ts +11 -11
- package/src/server/servedApp.ts +1 -1
- package/src/server/shell.ts +1 -1
- package/src/server/surfaceOf.ts +1 -1
- package/src/server/versionOf.ts +1 -1
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/reactive/Keyed.ts +23 -15
- package/src/shared/reactive/Reactive.ts +34 -1001
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -24
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +8 -5
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/Router.ts +53 -69
- package/src/ui/outboxQueue.ts +2 -8
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/socketWire.ts +3 -3
- package/src/ui/wire.ts +9 -10
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
|
@@ -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
|
+
}
|
package/src/server/preload.ts
CHANGED
|
@@ -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
|
package/src/server/principal.ts
CHANGED
|
@@ -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
|
|
package/src/server/remote.ts
CHANGED
|
@@ -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
|
/**
|
package/src/server/render.ts
CHANGED
|
@@ -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
|
|
14
|
-
import { currentRequestScope
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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()
|