@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
@@ -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: AppKit's events are drained
5
- // on a timer rather than by `[NSApp run]`, which would never hand the thread back to bun, and the app
6
- // is served on loopback for the window alone. A menu held open or a window being resized runs
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 { FFIType } from 'bun:ffi'
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 App, type AppOrigin, bootEntered, enterApp } from './createApp.ts'
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 { installMenu, menuBar, type NativeItem } from './nativeMenu.ts'
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 './serveBuild.ts'
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 stateFile = `${directory}/desktop.json`
103
- const remembered: Remembered = {
104
- last: null,
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: current?.url ?? base },
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 = failure
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
- async function disconnect(): Promise<void> {
211
- const leaving = current
212
- current = undefined
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
  }
@@ -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/canonicalKey.ts'
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/index.ts'
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(): PageRender['seed'] {
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): PageRender['seed'] {
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)