@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.
- package/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- 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/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- 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 +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- 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/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- 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/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- 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/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- 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 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +106 -313
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +28 -13
- package/src/ui/wire.ts +9 -10
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- 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
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
package/src/server/runDesktop.ts
CHANGED
|
@@ -1,56 +1,23 @@
|
|
|
1
1
|
// 38.70 — WHAT `abide bundle` WRITES, STARTED WITH NO COMMAND: a window on the app. Given a command
|
|
2
2
|
// it is the executable `abide compile` writes (`runCompiled`), so a terminal still reaches every one.
|
|
3
3
|
//
|
|
4
|
-
// ONE PROCESS. The app, the launcher's own page and the window share it
|
|
5
|
-
//
|
|
6
|
-
// is
|
|
7
|
-
// AppKit's own loop inside one drain, and the app answers nothing until it ends.
|
|
4
|
+
// ONE PROCESS. The app, the launcher's own page and the window share it, and the app is served on
|
|
5
|
+
// loopback for the window alone. What the window shows is the `DesktopSession`'s; the window itself
|
|
6
|
+
// is `openDesktopWindow`'s.
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
import { mkdir } from 'node:fs/promises' // bun exposes no directory creation of its own
|
|
11
|
-
|
|
12
|
-
import { isFailed } from '#shared/Failed.ts'
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
callBlock,
|
|
16
|
-
created,
|
|
17
|
-
definedClass,
|
|
18
|
-
type Id,
|
|
19
|
-
nsBoolean,
|
|
20
|
-
nsString,
|
|
21
|
-
objcClass,
|
|
22
|
-
selector,
|
|
23
|
-
send,
|
|
24
|
-
sendTyped,
|
|
25
|
-
stringOf,
|
|
26
|
-
} from './appKit.ts'
|
|
27
|
-
import { configuredMount, dataDirectory } from './config.ts'
|
|
8
|
+
import { dataDirectory } from './config.ts'
|
|
28
9
|
import { configFields } from './configFields.ts'
|
|
29
|
-
import { type
|
|
10
|
+
import { type AppOrigin, bootEntered, enterApp } from './createApp.ts'
|
|
11
|
+
import { DesktopSession } from './DesktopSession.ts'
|
|
30
12
|
import { executableName } from './executableName.ts'
|
|
31
13
|
import { HOOKS, inOrder, reportUnexpected } from './hooks.ts'
|
|
32
14
|
import { launcherOf } from './launcherOptions.ts'
|
|
33
15
|
import { type Launch, launcherPage } from './launcherPage.ts'
|
|
34
|
-
import {
|
|
16
|
+
import { openDesktopWindow } from './openDesktopWindow.ts'
|
|
35
17
|
import { randomToken } from './randomToken.ts'
|
|
36
18
|
import { type Component, renderDocument } from './render.ts'
|
|
37
|
-
import { LISTENING } from './requestScope.ts'
|
|
38
19
|
import { runCompiled } from './runCompiled.ts'
|
|
39
|
-
import { type BuildParts, builtSources } from './
|
|
40
|
-
|
|
41
|
-
/** What the launcher did last, and what its forms held, kept across launches. 38.73. */
|
|
42
|
-
type Remembered = {
|
|
43
|
-
last: { start: Record<string, string> } | { connect: string } | null
|
|
44
|
-
values: Record<string, string>
|
|
45
|
-
url: string
|
|
46
|
-
port: number
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type Failure = {
|
|
50
|
-
form: 'start' | 'connect'
|
|
51
|
-
message: string
|
|
52
|
-
issues: Record<string, string[]>
|
|
53
|
-
}
|
|
20
|
+
import { type BuildParts, builtSources } from './servedApp.ts'
|
|
54
21
|
|
|
55
22
|
/** What `abide bundle` read of the launcher's files, carried in the executable. */
|
|
56
23
|
export type Desktop = {
|
|
@@ -65,8 +32,6 @@ export type Desktop = {
|
|
|
65
32
|
}
|
|
66
33
|
|
|
67
34
|
const LOOPBACK = '127.0.0.1'
|
|
68
|
-
const DRAIN_MILLISECONDS = 8
|
|
69
|
-
const CONNECT_TIMEOUT_MILLISECONDS = 5000
|
|
70
35
|
|
|
71
36
|
export async function runDesktop(
|
|
72
37
|
origin: AppOrigin,
|
|
@@ -99,31 +64,16 @@ async function launch(
|
|
|
99
64
|
throw new Error(`abide: ${options}. RULEBOOK 38.81.`)
|
|
100
65
|
const appName = executableName(origin.package.name)
|
|
101
66
|
const directory = dataDirectory(origin.package.name ?? 'app')
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
values: {},
|
|
106
|
-
url: '',
|
|
107
|
-
port: 0,
|
|
108
|
-
...(await Bun.file(stateFile)
|
|
109
|
-
.json()
|
|
110
|
-
.catch(() => ({}))),
|
|
111
|
-
}
|
|
112
|
-
const remember = async (): Promise<void> => {
|
|
113
|
-
await mkdir(directory, { recursive: true })
|
|
114
|
-
await Bun.write(stateFile, JSON.stringify(remembered, null, 4))
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// What the window shows: the app served here, one elsewhere, or neither.
|
|
118
|
-
let current: { url: string; app: App | undefined } | undefined
|
|
119
|
-
let failure: Failure | undefined
|
|
67
|
+
const session = await DesktopSession.open(directory, () =>
|
|
68
|
+
bootEntered(load),
|
|
69
|
+
)
|
|
120
70
|
const token = randomToken(16, 'hex')
|
|
121
71
|
const derived = (): Record<string, string | undefined> => ({
|
|
122
72
|
APP_NAME: origin.package.name ?? 'app',
|
|
123
73
|
APP_VERSION: origin.package.version ?? '0.0.0',
|
|
124
74
|
APP_DATA_DIR: directory,
|
|
125
75
|
...process.env,
|
|
126
|
-
...remembered.values,
|
|
76
|
+
...session.remembered.values,
|
|
127
77
|
})
|
|
128
78
|
|
|
129
79
|
const launcher = Bun.serve({
|
|
@@ -147,7 +97,7 @@ async function launch(
|
|
|
147
97
|
values[field.name] = String(
|
|
148
98
|
form.getAll(field.name).at(-1) ?? '',
|
|
149
99
|
)
|
|
150
|
-
const started = await start(values)
|
|
100
|
+
const started = await session.start(values)
|
|
151
101
|
return started === undefined ? onwards() : page('start')
|
|
152
102
|
}
|
|
153
103
|
if (
|
|
@@ -155,7 +105,7 @@ async function launch(
|
|
|
155
105
|
request.method === 'POST' &&
|
|
156
106
|
action === 'connect'
|
|
157
107
|
) {
|
|
158
|
-
const connected = await connect(
|
|
108
|
+
const connected = await session.connect(
|
|
159
109
|
String((await request.formData()).get('url') ?? ''),
|
|
160
110
|
)
|
|
161
111
|
return connected === undefined ? onwards() : page('connect')
|
|
@@ -173,19 +123,18 @@ async function launch(
|
|
|
173
123
|
const onwards = (): Response =>
|
|
174
124
|
new Response(null, {
|
|
175
125
|
status: 303,
|
|
176
|
-
headers: { location:
|
|
126
|
+
headers: { location: session.shown ?? base },
|
|
177
127
|
})
|
|
178
128
|
const page = async (
|
|
179
129
|
focus: 'start' | 'connect' | undefined,
|
|
180
130
|
): Promise<Response> => {
|
|
181
|
-
const shown =
|
|
182
|
-
failure = undefined
|
|
131
|
+
const shown = session.takeFailure()
|
|
183
132
|
const props: Launch = {
|
|
184
133
|
name: appName,
|
|
185
134
|
start: options.start ? `${base}start` : undefined,
|
|
186
135
|
connect: options.connect ? `${base}connect` : undefined,
|
|
187
136
|
fields: configFields(derived()),
|
|
188
|
-
url: remembered.url,
|
|
137
|
+
url: session.remembered.url,
|
|
189
138
|
failure: shown,
|
|
190
139
|
focus:
|
|
191
140
|
shown?.form ??
|
|
@@ -207,384 +156,7 @@ async function launch(
|
|
|
207
156
|
return new Response(launcherPage(props), { headers })
|
|
208
157
|
}
|
|
209
158
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (leaving?.app !== undefined)
|
|
214
|
-
await leaving.app.stop().catch(reportUnexpected)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 38.72 — the form's values are the environment the app is booted under, so 27.8 coerces them
|
|
218
|
-
// and 27.7's schema refuses them as it would any other.
|
|
219
|
-
async function start(
|
|
220
|
-
values: Record<string, string>,
|
|
221
|
-
): Promise<Failure | undefined> {
|
|
222
|
-
await disconnect()
|
|
223
|
-
remembered.values = values
|
|
224
|
-
for (const [name, value] of Object.entries(values))
|
|
225
|
-
if (value === '') delete process.env[name]
|
|
226
|
-
else process.env[name] = value
|
|
227
|
-
let app: App
|
|
228
|
-
try {
|
|
229
|
-
app = await bootEntered(load)
|
|
230
|
-
} catch (thrown) {
|
|
231
|
-
failure = refused('start', thrown)
|
|
232
|
-
return failure
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
await app
|
|
236
|
-
// 38.75.
|
|
237
|
-
.listen({ hostname: LOOPBACK, port: remembered.port })
|
|
238
|
-
// The port the last launch served on, so the app's origin — and its storage with
|
|
239
|
-
// it — is the one it had; any free one where that is taken.
|
|
240
|
-
.catch(() => app.listen({ hostname: LOOPBACK, port: 0 }))
|
|
241
|
-
} catch (thrown) {
|
|
242
|
-
await app.stop().catch(reportUnexpected)
|
|
243
|
-
failure = refused('start', thrown)
|
|
244
|
-
return failure
|
|
245
|
-
}
|
|
246
|
-
const server = LISTENING.server as Bun.Server<unknown>
|
|
247
|
-
remembered.port = server.port ?? 0
|
|
248
|
-
remembered.last = { start: values }
|
|
249
|
-
current = { url: `${server.url.origin}${configuredMount()}/`, app }
|
|
250
|
-
await remember()
|
|
251
|
-
return undefined
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function connect(given: string): Promise<Failure | undefined> {
|
|
255
|
-
const text = given.trim()
|
|
256
|
-
remembered.url = text
|
|
257
|
-
const url = URL.parse(
|
|
258
|
-
/^[a-z][a-z0-9+.-]*:/i.test(text) ? text : `https://${text}`,
|
|
259
|
-
)
|
|
260
|
-
if (
|
|
261
|
-
url === null ||
|
|
262
|
-
(url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
263
|
-
) {
|
|
264
|
-
failure = {
|
|
265
|
-
form: 'connect',
|
|
266
|
-
message: `${text} is not an address.`,
|
|
267
|
-
issues: {},
|
|
268
|
-
}
|
|
269
|
-
return failure
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
await fetch(url, {
|
|
273
|
-
method: 'HEAD',
|
|
274
|
-
redirect: 'manual',
|
|
275
|
-
signal: AbortSignal.timeout(CONNECT_TIMEOUT_MILLISECONDS),
|
|
276
|
-
})
|
|
277
|
-
} catch {
|
|
278
|
-
failure = {
|
|
279
|
-
form: 'connect',
|
|
280
|
-
message: `Nothing answered at ${url.origin}.`,
|
|
281
|
-
issues: {},
|
|
282
|
-
}
|
|
283
|
-
return failure
|
|
284
|
-
}
|
|
285
|
-
await disconnect()
|
|
286
|
-
remembered.last = { connect: url.href }
|
|
287
|
-
current = { url: url.href, app: undefined }
|
|
288
|
-
await remember()
|
|
289
|
-
return undefined
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// THE WINDOW.
|
|
293
|
-
const application = send(objcClass('NSApplication'), 'sharedApplication')
|
|
294
|
-
// Regular: a Dock icon and a menu bar, which a process outside a `.app` does not get by default.
|
|
295
|
-
sendTyped(
|
|
296
|
-
application,
|
|
297
|
-
'setActivationPolicy:',
|
|
298
|
-
[FFIType.i64],
|
|
299
|
-
FFIType.void,
|
|
300
|
-
0,
|
|
301
|
-
)
|
|
302
|
-
let quitting = false
|
|
303
|
-
const quit = (): void => {
|
|
304
|
-
if (quitting) return
|
|
305
|
-
quitting = true
|
|
306
|
-
disconnect().finally(() => process.exit(0))
|
|
307
|
-
}
|
|
308
|
-
send(
|
|
309
|
-
application,
|
|
310
|
-
'setDelegate:',
|
|
311
|
-
send(
|
|
312
|
-
definedClass('AbideApplicationDelegate', {
|
|
313
|
-
'applicationShouldTerminate:': {
|
|
314
|
-
types: 'Q@:@',
|
|
315
|
-
args: [FFIType.ptr],
|
|
316
|
-
returns: FFIType.u64,
|
|
317
|
-
// NSTerminateCancel: 38.76 — the app is stopped first, and the process ends after.
|
|
318
|
-
run: () => {
|
|
319
|
-
quit()
|
|
320
|
-
return 0
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
'applicationShouldTerminateAfterLastWindowClosed:': {
|
|
324
|
-
types: 'c@:@',
|
|
325
|
-
args: [FFIType.ptr],
|
|
326
|
-
returns: FFIType.bool,
|
|
327
|
-
run: () => true,
|
|
328
|
-
},
|
|
329
|
-
}),
|
|
330
|
-
'new',
|
|
331
|
-
),
|
|
332
|
-
)
|
|
333
|
-
for (const signal of ['SIGTERM', 'SIGINT'] as const)
|
|
334
|
-
process.on(signal, quit)
|
|
335
|
-
// A desktop app outlives a fault in it: it is reported, and the window stays.
|
|
336
|
-
process.on('unhandledRejection', reportUnexpected)
|
|
337
|
-
process.on('uncaughtException', reportUnexpected)
|
|
338
|
-
|
|
339
|
-
const window = created('NSWindow')
|
|
340
|
-
// Titled, closable, miniaturizable, resizable.
|
|
341
|
-
sendTyped(window, 'setStyleMask:', [FFIType.u64], FFIType.void, 15)
|
|
342
|
-
sendTyped(
|
|
343
|
-
window,
|
|
344
|
-
'setReleasedWhenClosed:',
|
|
345
|
-
[FFIType.bool],
|
|
346
|
-
FFIType.void,
|
|
347
|
-
false,
|
|
348
|
-
)
|
|
349
|
-
sendTyped(
|
|
350
|
-
window,
|
|
351
|
-
'setContentSize:',
|
|
352
|
-
[FFIType.f64, FFIType.f64],
|
|
353
|
-
FFIType.void,
|
|
354
|
-
options.width,
|
|
355
|
-
options.height,
|
|
356
|
-
)
|
|
357
|
-
sendTyped(
|
|
358
|
-
window,
|
|
359
|
-
'setContentMinSize:',
|
|
360
|
-
[FFIType.f64, FFIType.f64],
|
|
361
|
-
FFIType.void,
|
|
362
|
-
options.minWidth,
|
|
363
|
-
options.minHeight,
|
|
364
|
-
)
|
|
365
|
-
send(window, 'center')
|
|
366
|
-
// Where the window was and how large, as the last launch left it.
|
|
367
|
-
sendTyped(
|
|
368
|
-
window,
|
|
369
|
-
'setFrameAutosaveName:',
|
|
370
|
-
[FFIType.ptr],
|
|
371
|
-
FFIType.bool,
|
|
372
|
-
nsString(`${appName} window`),
|
|
373
|
-
)
|
|
374
|
-
send(window, 'setTitle:', nsString(appName))
|
|
375
|
-
|
|
376
|
-
const view = created('WKWebView')
|
|
377
|
-
send(
|
|
378
|
-
send(send(view, 'configuration'), 'preferences'),
|
|
379
|
-
'setValue:forKey:',
|
|
380
|
-
nsBoolean(true),
|
|
381
|
-
nsString('developerExtrasEnabled'),
|
|
382
|
-
)
|
|
383
|
-
if (
|
|
384
|
-
sendTyped(
|
|
385
|
-
view,
|
|
386
|
-
'respondsToSelector:',
|
|
387
|
-
[FFIType.ptr],
|
|
388
|
-
FFIType.bool,
|
|
389
|
-
selector('setInspectable:'),
|
|
390
|
-
)
|
|
391
|
-
)
|
|
392
|
-
sendTyped(view, 'setInspectable:', [FFIType.bool], FFIType.void, true)
|
|
393
|
-
send(view, 'setUIDelegate:', send(uiDelegateClass(), 'new'))
|
|
394
|
-
send(window, 'setContentView:', view)
|
|
395
|
-
send(
|
|
396
|
-
view,
|
|
397
|
-
'loadHTMLString:baseURL:',
|
|
398
|
-
nsString(
|
|
399
|
-
'<!doctype html><style>:root{color-scheme:light dark;background:#f5f5f7}@media (prefers-color-scheme:dark){:root{background:#1c1c1e}}</style>',
|
|
400
|
-
),
|
|
401
|
-
null,
|
|
402
|
-
)
|
|
403
|
-
// 38.88 — THE MENU BAR IS THE PAGE'S: the defaults until the page the window loads tells it
|
|
404
|
-
// what its route exports, and again after every navigation it makes.
|
|
405
|
-
const showMenu = (told: NativeItem[]): void =>
|
|
406
|
-
installMenu(menuBar(appName, options.connect, told), (item) => {
|
|
407
|
-
if (item.role === 'connect')
|
|
408
|
-
return navigate(`${base}?focus=connect`)
|
|
409
|
-
// 38.74.
|
|
410
|
-
if (item.role === 'disconnect') {
|
|
411
|
-
remembered.last = null
|
|
412
|
-
void remember()
|
|
413
|
-
return void disconnect().then(() => navigate(base))
|
|
414
|
-
}
|
|
415
|
-
if (item.role === 'reload') return void send(view, 'reload')
|
|
416
|
-
// 38.90, 38.91 — the page that numbered the item does what it means.
|
|
417
|
-
if (item.id !== undefined)
|
|
418
|
-
send(
|
|
419
|
-
view,
|
|
420
|
-
'evaluateJavaScript:completionHandler:',
|
|
421
|
-
nsString(
|
|
422
|
-
`dispatchEvent(new CustomEvent('abide:menu', { detail: ${item.id} }))`,
|
|
423
|
-
),
|
|
424
|
-
null,
|
|
425
|
-
)
|
|
426
|
-
})
|
|
427
|
-
send(
|
|
428
|
-
send(send(view, 'configuration'), 'userContentController'),
|
|
429
|
-
'addScriptMessageHandler:name:',
|
|
430
|
-
send(
|
|
431
|
-
definedClass('AbideMenuBridge', {
|
|
432
|
-
'userContentController:didReceiveScriptMessage:': {
|
|
433
|
-
types: 'v@:@@',
|
|
434
|
-
args: [FFIType.ptr, FFIType.ptr],
|
|
435
|
-
returns: FFIType.void,
|
|
436
|
-
run: (_controller: Id, message: Id) => {
|
|
437
|
-
try {
|
|
438
|
-
showMenu(
|
|
439
|
-
JSON.parse(
|
|
440
|
-
stringOf(send(message, 'body')),
|
|
441
|
-
) as NativeItem[],
|
|
442
|
-
)
|
|
443
|
-
} catch (thrown) {
|
|
444
|
-
reportUnexpected(thrown)
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
}),
|
|
449
|
-
'new',
|
|
450
|
-
),
|
|
451
|
-
nsString('abideMenu'),
|
|
452
|
-
)
|
|
453
|
-
const navigate = (address: string): void => {
|
|
454
|
-
// 38.92.
|
|
455
|
-
showMenu([])
|
|
456
|
-
send(
|
|
457
|
-
view,
|
|
458
|
-
'loadRequest:',
|
|
459
|
-
send(
|
|
460
|
-
objcClass('NSURLRequest'),
|
|
461
|
-
'requestWithURL:',
|
|
462
|
-
send(objcClass('NSURL'), 'URLWithString:', nsString(address)),
|
|
463
|
-
),
|
|
464
|
-
)
|
|
465
|
-
}
|
|
466
|
-
showMenu([])
|
|
467
|
-
|
|
468
|
-
send(window, 'makeKeyAndOrderFront:', null)
|
|
469
|
-
sendTyped(
|
|
470
|
-
application,
|
|
471
|
-
'activateIgnoringOtherApps:',
|
|
472
|
-
[FFIType.bool],
|
|
473
|
-
FFIType.void,
|
|
474
|
-
true,
|
|
475
|
-
)
|
|
476
|
-
send(application, 'finishLaunching')
|
|
477
|
-
|
|
478
|
-
const distantPast = send(objcClass('NSDate'), 'distantPast')
|
|
479
|
-
const defaultMode = nsString('kCFRunLoopDefaultMode')
|
|
480
|
-
let shownTitle = appName
|
|
481
|
-
let drains = 0
|
|
482
|
-
setInterval(() => {
|
|
483
|
-
for (;;) {
|
|
484
|
-
const event = sendTyped(
|
|
485
|
-
application,
|
|
486
|
-
'nextEventMatchingMask:untilDate:inMode:dequeue:',
|
|
487
|
-
[FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.bool],
|
|
488
|
-
FFIType.ptr,
|
|
489
|
-
0xffffffffffffffffn,
|
|
490
|
-
distantPast,
|
|
491
|
-
defaultMode,
|
|
492
|
-
true,
|
|
493
|
-
) as Id
|
|
494
|
-
if (event === null) break
|
|
495
|
-
send(application, 'sendEvent:', event)
|
|
496
|
-
}
|
|
497
|
-
// The window is titled by the page it shows.
|
|
498
|
-
drains += 1
|
|
499
|
-
if (drains % 32 !== 0) return
|
|
500
|
-
const title = stringOf(send(view, 'title')) || appName
|
|
501
|
-
if (title === shownTitle) return
|
|
502
|
-
shownTitle = title
|
|
503
|
-
send(window, 'setTitle:', nsString(title))
|
|
504
|
-
}, DRAIN_MILLISECONDS)
|
|
505
|
-
|
|
506
|
-
// 38.73 — WHAT IT DID LAST, done again.
|
|
507
|
-
const last = remembered.last
|
|
508
|
-
if (options.start && last !== null && 'start' in last)
|
|
509
|
-
await start(last.start)
|
|
510
|
-
else if (options.connect && last !== null && 'connect' in last)
|
|
511
|
-
await connect(last.connect)
|
|
512
|
-
navigate(current?.url ?? base)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/** A refusal as the form it came back to: 27.7's issues by field, or the error's message. */
|
|
516
|
-
function refused(form: 'start' | 'connect', thrown: unknown): Failure {
|
|
517
|
-
if (
|
|
518
|
-
isFailed(thrown) &&
|
|
519
|
-
thrown.data !== null &&
|
|
520
|
-
typeof thrown.data === 'object'
|
|
521
|
-
)
|
|
522
|
-
return {
|
|
523
|
-
form,
|
|
524
|
-
message: 'The configuration was refused.',
|
|
525
|
-
issues: Array.isArray(thrown.data)
|
|
526
|
-
? { '': thrown.data as string[] }
|
|
527
|
-
: (thrown.data as Record<string, string[]>),
|
|
528
|
-
}
|
|
529
|
-
return {
|
|
530
|
-
form,
|
|
531
|
-
message: thrown instanceof Error ? thrown.message : String(thrown),
|
|
532
|
-
issues: {},
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// What a page asks of its window: a link out opens in the browser, and `alert` and `confirm` are
|
|
537
|
-
// sheets the page waits on.
|
|
538
|
-
let uiDelegate: Id = null
|
|
539
|
-
function uiDelegateClass(): Id {
|
|
540
|
-
uiDelegate ??= definedClass('AbideUIDelegate', {
|
|
541
|
-
'webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:':
|
|
542
|
-
{
|
|
543
|
-
types: '@@:@@@@',
|
|
544
|
-
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
|
|
545
|
-
returns: FFIType.ptr,
|
|
546
|
-
run: (_view: Id, _configuration: Id, action: Id) => {
|
|
547
|
-
const url = send(send(action, 'request'), 'URL')
|
|
548
|
-
if (url !== null)
|
|
549
|
-
send(
|
|
550
|
-
send(objcClass('NSWorkspace'), 'sharedWorkspace'),
|
|
551
|
-
'openURL:',
|
|
552
|
-
url,
|
|
553
|
-
)
|
|
554
|
-
return null
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
'webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:':
|
|
558
|
-
{
|
|
559
|
-
types: 'v@:@@@@?',
|
|
560
|
-
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
|
|
561
|
-
returns: FFIType.void,
|
|
562
|
-
run: (_view: Id, message: Id, _frame: Id, done: Id) => {
|
|
563
|
-
alert(message, false)
|
|
564
|
-
callBlock(done, [])
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
'webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:':
|
|
568
|
-
{
|
|
569
|
-
types: 'v@:@@@@?',
|
|
570
|
-
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr],
|
|
571
|
-
returns: FFIType.void,
|
|
572
|
-
run: (_view: Id, message: Id, _frame: Id, done: Id) => {
|
|
573
|
-
callBlock(done, [FFIType.bool], alert(message, true))
|
|
574
|
-
},
|
|
575
|
-
},
|
|
576
|
-
})
|
|
577
|
-
return uiDelegate
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// An `NSAlert` with the page's message; answers whether it was accepted.
|
|
581
|
-
function alert(message: Id, cancellable: boolean): boolean {
|
|
582
|
-
const panel = created('NSAlert')
|
|
583
|
-
send(panel, 'setMessageText:', message)
|
|
584
|
-
send(panel, 'addButtonWithTitle:', nsString('OK'))
|
|
585
|
-
if (cancellable) send(panel, 'addButtonWithTitle:', nsString('Cancel'))
|
|
586
|
-
// NSAlertFirstButtonReturn.
|
|
587
|
-
return (
|
|
588
|
-
Number(sendTyped(panel, 'runModal', [], FFIType.i64) as bigint) === 1000
|
|
589
|
-
)
|
|
159
|
+
const navigate = openDesktopWindow(appName, options, base, session)
|
|
160
|
+
await session.resume(options)
|
|
161
|
+
navigate(session.shown ?? base)
|
|
590
162
|
}
|
package/src/server/seedBuffer.ts
CHANGED
|
@@ -23,21 +23,15 @@
|
|
|
23
23
|
// refresh, and says nothing.
|
|
24
24
|
|
|
25
25
|
import { namedLogger } from '#shared/log.ts'
|
|
26
|
-
import { canonicalKey } from '#shared/reactive/
|
|
26
|
+
import { canonicalKey } from '#shared/reactive/argsContract.ts'
|
|
27
|
+
import { isReactive, STREAMING } from '#shared/reactive/index.ts'
|
|
27
28
|
import {
|
|
28
29
|
chunksOfStream,
|
|
29
|
-
isReactive,
|
|
30
|
-
STREAMING,
|
|
31
30
|
streamingOrSettled,
|
|
32
|
-
} from '#shared/reactive/
|
|
31
|
+
} from '#shared/reactive/producer.ts'
|
|
33
32
|
|
|
34
33
|
import { positiveSetting } from './config.ts'
|
|
35
|
-
import {
|
|
36
|
-
currentRequestScope,
|
|
37
|
-
heldByApp,
|
|
38
|
-
type PageRender,
|
|
39
|
-
releaseScope,
|
|
40
|
-
} from './requestScope.ts'
|
|
34
|
+
import { currentRequestScope, heldByApp, releaseScope } from './requestScope.ts'
|
|
41
35
|
|
|
42
36
|
const HYDRATE_LOG = namedLogger('abide:hydrate')
|
|
43
37
|
|
|
@@ -58,6 +52,12 @@ export type Outcome =
|
|
|
58
52
|
| { settled: true; value: unknown; duration: number | undefined }
|
|
59
53
|
| { settled: false; error: unknown; duration: number }
|
|
60
54
|
|
|
55
|
+
/**
|
|
56
|
+
* What a page's render reports each call into: `true` where the call was new to the render, for the
|
|
57
|
+
* early-call script to start (41.20).
|
|
58
|
+
*/
|
|
59
|
+
export type SeedSink = (rpc: object, args: unknown, reached: unknown) => boolean
|
|
60
|
+
|
|
61
61
|
type Entry = {
|
|
62
62
|
rpc: object
|
|
63
63
|
key: string
|
|
@@ -105,7 +105,7 @@ function reportsOnce(): (rpc: object, args: unknown) => string | undefined {
|
|
|
105
105
|
* 41.30 — the seed sink of a page the worker keeps: each call reported once, for the early-call
|
|
106
106
|
* script, and none of them left in the seed buffer (41.29) — the page is opened later, if at all.
|
|
107
107
|
*/
|
|
108
|
-
export function callsOnly():
|
|
108
|
+
export function callsOnly(): SeedSink {
|
|
109
109
|
const firstReport = reportsOnce()
|
|
110
110
|
return (rpc, args) => firstReport(rpc, args) !== undefined
|
|
111
111
|
}
|
|
@@ -117,7 +117,7 @@ export function callsOnly(): PageRender['seed'] {
|
|
|
117
117
|
* reported once: two components reaching one invoice are one call in the browser too, since the
|
|
118
118
|
* browser's `Rpc` joins a standing call (1.8).
|
|
119
119
|
*/
|
|
120
|
-
export function seedFor(caller: string):
|
|
120
|
+
export function seedFor(caller: string): SeedSink {
|
|
121
121
|
const firstReport = reportsOnce()
|
|
122
122
|
return (rpc, args, reached) => {
|
|
123
123
|
const key = firstReport(rpc, args)
|