@fungi.computer/oyster 0.2.0 → 0.3.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 (287) hide show
  1. package/README.md +376 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +210 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/pack.d.ts +14 -3
  130. package/dist/pack.js +44 -19
  131. package/dist/page/app-access.d.ts +7 -0
  132. package/dist/page/app-access.js +75 -0
  133. package/dist/page/boot-host.js +57 -0
  134. package/dist/page/capabilities.d.ts +3 -3
  135. package/dist/page/capabilities.js +16 -12
  136. package/dist/page/computer-handles.d.ts +3 -3
  137. package/dist/page/computer-target.d.ts +3 -0
  138. package/dist/page/computer-target.js +9 -0
  139. package/dist/page/mount.d.ts +23 -3
  140. package/dist/page/mount.js +152 -16
  141. package/dist/page-assets.d.ts +22 -0
  142. package/dist/page-assets.js +28 -0
  143. package/dist/page-script.d.ts +3 -0
  144. package/dist/page-script.js +5 -4
  145. package/dist/release.d.ts +13 -6
  146. package/dist/release.js +10 -7
  147. package/dist/root-data.d.ts +16 -6
  148. package/dist/root-data.js +2 -2
  149. package/dist/source/index.d.ts +1 -1
  150. package/dist/source/team-source.d.ts +20 -8
  151. package/dist/source/team-source.js +39 -10
  152. package/dist/source/template-seed.d.ts +2 -2
  153. package/dist/standalone/agent-mail.d.ts +24 -0
  154. package/dist/standalone/agent-mail.js +62 -0
  155. package/dist/standalone/agent-store.d.ts +9 -0
  156. package/dist/standalone/agent-store.js +30 -0
  157. package/dist/standalone/agents.d.ts +68 -9
  158. package/dist/standalone/agents.js +428 -56
  159. package/dist/standalone/app-builds-binding.d.ts +20 -2
  160. package/dist/standalone/app-builds-binding.js +147 -23
  161. package/dist/standalone/app-set.d.ts +5 -2
  162. package/dist/standalone/app-set.js +174 -143
  163. package/dist/standalone/browser.d.ts +11 -0
  164. package/dist/standalone/browser.js +72 -0
  165. package/dist/standalone/builds.d.ts +13 -4
  166. package/dist/standalone/builds.js +28 -44
  167. package/dist/standalone/computer-egress.d.ts +2 -0
  168. package/dist/standalone/computer-egress.js +13 -4
  169. package/dist/standalone/computer-fleet.d.ts +43 -0
  170. package/dist/standalone/computer-fleet.js +200 -0
  171. package/dist/standalone/computer.d.ts +44 -9
  172. package/dist/standalone/computer.js +129 -22
  173. package/dist/standalone/install-apps-cli.js +43 -6
  174. package/dist/standalone/instance.d.ts +2 -2
  175. package/dist/standalone/instance.js +8 -11
  176. package/dist/standalone/machine-link.d.ts +354 -0
  177. package/dist/standalone/machine-link.js +368 -0
  178. package/dist/standalone/media.d.ts +2 -2
  179. package/dist/standalone/media.js +19 -16
  180. package/dist/standalone/preview.d.ts +2 -2
  181. package/dist/standalone/preview.js +37 -10
  182. package/dist/standalone/registry.d.ts +149 -9
  183. package/dist/standalone/registry.js +299 -8
  184. package/dist/standalone/worker.d.ts +19 -5
  185. package/dist/standalone/worker.js +90 -54
  186. package/dist/standalone/workflow-binding.d.ts +18 -22
  187. package/dist/standalone/workflow-binding.js +18 -18
  188. package/dist/standalone/workflow-control.d.ts +4 -0
  189. package/dist/standalone/workflow-control.js +40 -0
  190. package/dist/standalone/workflow-events.d.ts +2 -2
  191. package/dist/standalone/workflow-events.js +9 -10
  192. package/dist/standalone/workflow-git-capture.js +4 -3
  193. package/dist/standalone/workflow-host.d.ts +162 -161
  194. package/dist/standalone/workflow-host.js +101 -21
  195. package/dist/standalone/workflow-node-capture.js +11 -4
  196. package/dist/standalone/workflow-schedules.d.ts +5 -2
  197. package/dist/standalone/workflow-schedules.js +9 -10
  198. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  199. package/dist/standalone/workflow-trigger-host.js +2 -2
  200. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  201. package/dist/standalone/workflow-webhooks.js +39 -0
  202. package/dist/standalone/workflows.d.ts +140 -43
  203. package/dist/standalone/workflows.js +198 -39
  204. package/dist/storage-frame.d.ts +6 -6
  205. package/dist/testing/capabilities.js +14 -3
  206. package/dist/testing/installation.js +26 -2
  207. package/dist/testing/worker.js +1 -1
  208. package/dist/testing/workers-runtime.d.ts +8 -2
  209. package/dist/testing/workers-runtime.js +20 -2
  210. package/dist/webhook-abuse.d.ts +25 -0
  211. package/dist/webhook-abuse.js +54 -0
  212. package/dist/webhook-custody.d.ts +144 -0
  213. package/dist/webhook-custody.js +222 -0
  214. package/dist/webhook-ingress.d.ts +36 -0
  215. package/dist/webhook-ingress.js +146 -0
  216. package/dist/webhooks.d.ts +111 -0
  217. package/dist/webhooks.js +189 -0
  218. package/dist/workers-git.d.ts +12 -17
  219. package/dist/workers-git.js +60 -52
  220. package/dist/workers.d.ts +224 -13
  221. package/dist/workers.js +351 -51
  222. package/dist/workflow-agent-authority.d.ts +8 -0
  223. package/dist/workflow-agent-authority.js +42 -0
  224. package/dist/workflows.d.ts +66 -0
  225. package/dist/workflows.js +97 -0
  226. package/docs/api/README.md +63 -37
  227. package/docs/api/agents.md +17 -0
  228. package/docs/api/app-storage.md +1 -1
  229. package/docs/api/apps.md +1394 -543
  230. package/docs/api/browser/boot.md +132 -0
  231. package/docs/api/browser.md +1440 -0
  232. package/docs/api/build-contract.md +38 -25
  233. package/docs/api/build.md +263 -84
  234. package/docs/api/cloudflare.md +93 -7
  235. package/docs/api/git.md +3 -13
  236. package/docs/api/host.md +944 -118
  237. package/docs/api/index.md +320 -18
  238. package/docs/api/keys.md +24 -1
  239. package/docs/api/mcp.md +59 -12
  240. package/docs/api/modules.md +33 -40
  241. package/docs/api/pack.md +57 -8
  242. package/docs/api/page/mount.md +92 -4
  243. package/docs/api/workers-git.md +103 -24
  244. package/docs/api/workers.md +1581 -244
  245. package/docs/site/agent-quickstart.mdx +28 -14
  246. package/examples/standalone/worker.ts +3 -1
  247. package/examples/standalone/wrangler.jsonc +2 -0
  248. package/package.json +73 -71
  249. package/dist/installation-consent.d.ts +0 -81
  250. package/dist/installation-consent.js +0 -84
  251. package/dist/shell/app-directory.d.ts +0 -542
  252. package/dist/shell/app-directory.js +0 -196
  253. package/dist/shell/browser-actions.d.ts +0 -7
  254. package/dist/shell/browser-actions.js +0 -56
  255. package/dist/shell/command-host.d.ts +0 -112
  256. package/dist/shell/command-host.js +0 -502
  257. package/dist/shell/commands.d.ts +0 -25
  258. package/dist/shell/commands.js +0 -78
  259. package/dist/shell/index.d.ts +0 -8
  260. package/dist/shell/index.js +0 -8
  261. package/dist/shell/keys.d.ts +0 -41
  262. package/dist/shell/keys.js +0 -134
  263. package/dist/shell/shell-module.d.ts +0 -55
  264. package/dist/shell/shell-module.js +0 -86
  265. package/dist/shell/window-manager.d.ts +0 -128
  266. package/dist/shell/window-manager.js +0 -730
  267. package/dist/shell/workbench-contract.d.ts +0 -68
  268. package/dist/shell/workbench-directory.d.ts +0 -332
  269. package/dist/shell/workbench-directory.js +0 -35
  270. package/dist/shell/workbench-host-types.d.ts +0 -96
  271. package/dist/shell/workbench-host-types.js +0 -1
  272. package/dist/shell/workbench-host.d.ts +0 -34
  273. package/dist/shell/workbench-host.js +0 -352
  274. package/dist/shell/workbench-model.d.ts +0 -194
  275. package/dist/shell/workbench-model.js +0 -53
  276. package/dist/shell/workbench.d.ts +0 -4
  277. package/dist/shell/workbench.js +0 -2
  278. package/docs/api/shell/app-directory.md +0 -599
  279. package/docs/api/shell/command-host.md +0 -224
  280. package/docs/api/shell/commands.md +0 -197
  281. package/docs/api/shell/keys.md +0 -183
  282. package/docs/api/shell/shell-module.md +0 -204
  283. package/docs/api/shell/window-manager.md +0 -267
  284. package/docs/api/shell/workbench-directory.md +0 -119
  285. package/docs/api/shell/workbench.md +0 -205
  286. package/docs/api/shell.md +0 -1915
  287. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -1,730 +0,0 @@
1
- import * as Redacted from "effect/Redacted";
2
- import { AppError, navigationActivationAllowed, statusActionAllowed, } from "@fungi.computer/agar";
3
- /**
4
- * Shared window lifetime owner for browser and terminal shells.
5
- *
6
- * This module knows how a window is mounted and disposed. It deliberately does
7
- * not know how an App is selected, admitted, or transported; those decisions
8
- * are made by the owner issuing the launch descriptor.
9
- */
10
- /** Mount failures may outlive their window; keep their cause safe and observable. */
11
- function defaultFailure(windowId, generation, phase, cause) {
12
- console.error(`App window ${phase} failed`, { windowId, generation, phase }, Redacted.make(cause));
13
- }
14
- function sameStatusItems(current, next) {
15
- // Decoded status items are plain data, so their serialization is their value.
16
- return JSON.stringify(current) === JSON.stringify(next);
17
- }
18
- function sameNavigation(current, next) {
19
- if (current === null || next === null)
20
- return current === next;
21
- return (current.canGoBack === next.canGoBack &&
22
- current.title === next.title &&
23
- JSON.stringify([
24
- current.crumbs,
25
- current.primary,
26
- current.menu,
27
- current.status,
28
- ]) === JSON.stringify([next.crumbs, next.primary, next.menu, next.status]));
29
- }
30
- /** Whether any projected App window is still visible in the workspace. */
31
- export function hasVisibleWindows(projection) {
32
- return projection.windows.some((window) => window.status !== "minimized");
33
- }
34
- /**
35
- * Owns App-window generations while leaving selection and App authority to the
36
- * caller. A stale mount is always disposed when it eventually resolves.
37
- */
38
- export function createWindowManager(options) {
39
- const reportFailure = (windowId, generation, phase, cause) => {
40
- if (options.onFailure !== undefined)
41
- options.onFailure({ windowId, generation, phase, cause });
42
- else
43
- defaultFailure(windowId, generation, phase, cause);
44
- };
45
- const records = new Map();
46
- const hiddenForDesktop = new Set();
47
- const listeners = new Set();
48
- const operations = new Map();
49
- let managerDisposed = false;
50
- let disposalInFlight;
51
- const enqueueMutation = (windowId, task) => {
52
- const generation = records.get(windowId)?.generation ?? null;
53
- const previous = operations.get(windowId) ?? Promise.resolve();
54
- const next = previous.then(task);
55
- const settled = next.then(() => {
56
- if (operations.get(windowId) === settled)
57
- operations.delete(windowId);
58
- }, (cause) => {
59
- reportFailure(windowId, generation, "mutation", cause);
60
- if (operations.get(windowId) === settled)
61
- operations.delete(windowId);
62
- });
63
- operations.set(windowId, settled);
64
- return next;
65
- };
66
- const project = () => ({
67
- windows: Array.from(records.values(), (record) => {
68
- const projected = {
69
- windowId: record.launch.windowId,
70
- launch: record.launch,
71
- container: record.container,
72
- status: record.status,
73
- generation: record.generation,
74
- // A loading, hidden or failed App offers no Back.
75
- navigation: record.status === "ready" ? record.navigation : null,
76
- statusItems: record.statusItems,
77
- };
78
- const withError = record.error === undefined
79
- ? projected
80
- : { ...projected, error: record.error };
81
- return record.cleanupError === undefined
82
- ? withError
83
- : { ...withError, cleanupError: record.cleanupError };
84
- }),
85
- });
86
- let projection = project();
87
- const notify = () => {
88
- projection = project();
89
- for (const listener of Array.from(listeners))
90
- listener();
91
- };
92
- const read = () => projection;
93
- const isCurrent = (record) => records.get(record.launch.windowId) === record;
94
- const isActive = (record) => isCurrent(record) &&
95
- !record.controller.signal.aborted &&
96
- !record.retiring &&
97
- !record.disposed;
98
- const hasCloseRequest = (record) => record.closeRequested;
99
- const isVisibilityCurrent = (record, version) => isCurrent(record) &&
100
- !record.disposed &&
101
- record.visibilityVersion === version;
102
- const reconfigurationWasCancelled = (record, controller) => controller.signal.aborted || hasCloseRequest(record) || !isActive(record);
103
- const retire = (record) => {
104
- hiddenForDesktop.delete(record);
105
- if (!record.retiring) {
106
- record.retiring = true;
107
- record.controller.abort();
108
- }
109
- for (const resolve of record.attachmentWaiters)
110
- resolve(false);
111
- record.attachmentWaiters.clear();
112
- };
113
- /**
114
- * Dispose the host currently owned by a record. The disposal promise is
115
- * shared, so close, replacement, and setup failure cannot call a host's
116
- * disposer twice. A host operation is allowed to settle before disposal;
117
- * otherwise visibility and disposal would race on the same connection.
118
- */
119
- const disposeRecordHost = async (record) => {
120
- if (record.disposed)
121
- return;
122
- if (record.disposal) {
123
- await record.disposal;
124
- return;
125
- }
126
- for (const resolve of record.attachmentWaiters)
127
- resolve(false);
128
- record.attachmentWaiters.clear();
129
- const disposal = (async () => {
130
- // A retiring record may still be awaiting attachment or its async mount.
131
- // `mount` disposes any host that arrives after retirement; waiting here
132
- // makes close/dispose mean that cleanup has actually completed.
133
- if (!record.host && record.mountPromise)
134
- await record.mountPromise;
135
- const host = record.host;
136
- if (!host) {
137
- record.disposed = true;
138
- return;
139
- }
140
- const operation = record.hostOperation;
141
- if (operation) {
142
- try {
143
- await operation;
144
- }
145
- catch (error) {
146
- // The visibility owner records this failure as the primary error
147
- // when it handles the operation. This branch covers close winning
148
- // the race and therefore records it here as well.
149
- if (record.error === undefined) {
150
- record.status = "error";
151
- record.error = error;
152
- notify();
153
- }
154
- }
155
- }
156
- await host.dispose();
157
- if (record.host === host)
158
- record.host = undefined;
159
- record.disposed = true;
160
- })();
161
- record.disposal = disposal;
162
- try {
163
- await disposal;
164
- }
165
- catch (error) {
166
- record.disposal = undefined;
167
- record.cleanupError = error;
168
- notify();
169
- throw error;
170
- }
171
- };
172
- const disposeLateHost = async (record, host) => {
173
- try {
174
- await host.dispose();
175
- }
176
- catch (error) {
177
- record.cleanupError = error;
178
- if (isCurrent(record) && record.error === undefined) {
179
- record.status = "error";
180
- record.error = error;
181
- notify();
182
- }
183
- throw error;
184
- }
185
- };
186
- const setHostVisibility = async (record, host, visibility) => {
187
- const operation = Promise.resolve().then(() => host.setVisibility(visibility));
188
- record.hostOperation = operation;
189
- try {
190
- await operation;
191
- }
192
- finally {
193
- if (record.hostOperation === operation)
194
- record.hostOperation = undefined;
195
- }
196
- };
197
- const applyVisibility = async (record, version) => {
198
- if (!isVisibilityCurrent(record, version) || record.retiring)
199
- return;
200
- const host = record.host;
201
- if (!host)
202
- return;
203
- const requested = record.visibility;
204
- try {
205
- await setHostVisibility(record, host, requested);
206
- }
207
- catch (error) {
208
- if (!isVisibilityCurrent(record, version))
209
- return;
210
- record.status = "error";
211
- record.error = error;
212
- notify();
213
- try {
214
- await disposeRecordHost(record);
215
- }
216
- catch (cleanupError) {
217
- // Keep the visibility error in `error`; cleanupError makes the second
218
- // failure observable without changing the first failure's identity.
219
- record.cleanupError = cleanupError;
220
- notify();
221
- throw cleanupError;
222
- }
223
- }
224
- };
225
- const mount = async (record) => {
226
- performance.mark("fungi.app-open.window-mount-start");
227
- if (options.waitForAttachment && !options.isAttached?.(record.container)) {
228
- performance.mark("fungi.app-open.window-wait-for-attachment");
229
- const attached = await new Promise((resolve) => {
230
- record.attachmentWaiters.add(resolve);
231
- });
232
- if (!attached)
233
- return;
234
- performance.mark("fungi.app-open.window-attached");
235
- }
236
- let host;
237
- try {
238
- performance.mark("fungi.app-open.window-host-mount-start");
239
- host = await options.mount(record.container, record.launch, record.controller.signal, {
240
- onComputerChoice: () => Promise.resolve().then(() => {
241
- if (!isActive(record) ||
242
- record.status !== "ready" ||
243
- record.computerChoice === undefined)
244
- throw new AppError("Computer choice is unavailable for this window", { code: "CAPABILITY_DENIED", retryable: false });
245
- record.computerChoice();
246
- }),
247
- onNavigationState: (state) => {
248
- // A retired generation never speaks for the window again.
249
- if (!isActive(record) || sameNavigation(record.navigation, state))
250
- return;
251
- record.navigation = state;
252
- notify();
253
- },
254
- onStatusItems: (items) => {
255
- if (!isActive(record) || sameStatusItems(record.statusItems, items))
256
- return;
257
- record.statusItems = items;
258
- notify();
259
- },
260
- });
261
- performance.mark("fungi.app-open.window-host-mount-ready");
262
- }
263
- catch (error) {
264
- if (!isActive(record)) {
265
- if (error !== record.controller.signal.reason)
266
- reportFailure(record.launch.windowId, record.generation, "mount", error);
267
- return;
268
- }
269
- record.status = record.visibility === "hidden" ? "minimized" : "error";
270
- record.error = error;
271
- notify();
272
- return;
273
- }
274
- if (!isActive(record)) {
275
- await disposeLateHost(record, host);
276
- return;
277
- }
278
- record.host = host;
279
- if (record.visibility === "hidden") {
280
- await applyVisibility(record, record.visibilityVersion);
281
- }
282
- if (!isActive(record)) {
283
- await disposeRecordHost(record);
284
- return;
285
- }
286
- if (record.status !== "error") {
287
- record.status = record.visibility === "hidden" ? "minimized" : "ready";
288
- record.error = undefined;
289
- record.cleanupError = undefined;
290
- notify();
291
- }
292
- };
293
- const startMount = (record) => {
294
- const mounted = mount(record);
295
- record.mountPromise = mounted;
296
- // Callers such as React effect cleanup can intentionally ignore open's
297
- // promise. Keep that ordinary use from creating an unhandled rejection;
298
- // the original promise still reports failures to callers that await it.
299
- void mounted.catch((cause) => {
300
- reportFailure(record.launch.windowId, record.generation, "mount", cause);
301
- return undefined;
302
- });
303
- return mounted;
304
- };
305
- const createRecord = (launch, generation) => ({
306
- launch,
307
- container: options.createContainer(launch),
308
- generation,
309
- status: "loading",
310
- controller: new AbortController(),
311
- host: undefined,
312
- disposed: false,
313
- retiring: false,
314
- disposal: undefined,
315
- mountPromise: undefined,
316
- hostOperation: undefined,
317
- reconfigureController: undefined,
318
- closeRequested: false,
319
- error: undefined,
320
- cleanupError: undefined,
321
- visibility: "visible",
322
- attachmentWaiters: new Set(),
323
- visibilityVersion: 0,
324
- navigation: null,
325
- statusItems: [],
326
- computerChoice: undefined,
327
- });
328
- const open = (launch) => {
329
- const existingAtCall = records.get(launch.windowId);
330
- const created = !managerDisposed && !existingAtCall ? createRecord(launch, 1) : undefined;
331
- let mounted;
332
- if (created) {
333
- records.set(launch.windowId, created);
334
- notify();
335
- mounted = startMount(created);
336
- }
337
- const operation = enqueueMutation(launch.windowId, async () => {
338
- if (managerDisposed) {
339
- if (created && records.get(launch.windowId) === created) {
340
- retire(created);
341
- await disposeRecordHost(created);
342
- records.delete(launch.windowId);
343
- notify();
344
- }
345
- return;
346
- }
347
- const existing = records.get(launch.windowId);
348
- if (created) {
349
- if (existing !== created)
350
- return;
351
- return;
352
- }
353
- if (existing) {
354
- mounted = existing.mountPromise;
355
- if (existing.visibility === "hidden" && !existing.retiring) {
356
- const version = ++existing.visibilityVersion;
357
- existing.visibility = "visible";
358
- existing.status =
359
- existing.error !== undefined
360
- ? "error"
361
- : existing.host
362
- ? "ready"
363
- : "loading";
364
- notify();
365
- if (existing.error === undefined)
366
- await applyVisibility(existing, version);
367
- }
368
- else {
369
- notify();
370
- }
371
- return;
372
- }
373
- const record = createRecord(launch, 1);
374
- records.set(launch.windowId, record);
375
- notify();
376
- mounted = startMount(record);
377
- });
378
- return operation.then(async () => {
379
- if (mounted)
380
- await mounted;
381
- });
382
- };
383
- const attach = (windowId) => {
384
- if (managerDisposed)
385
- return;
386
- const record = records.get(windowId);
387
- if (!record || record.disposed || record.retiring)
388
- return;
389
- for (const resolve of record.attachmentWaiters)
390
- resolve(true);
391
- record.attachmentWaiters.clear();
392
- };
393
- const requestVisibility = (windowId, visibility) => {
394
- const record = records.get(windowId);
395
- if (managerDisposed)
396
- return Promise.resolve();
397
- // A failed App still has a visible error window, even after its host was
398
- // disposed. Hide that presentation without discarding the failure.
399
- if (record && !record.retiring && record.error !== undefined) {
400
- record.visibility = visibility;
401
- record.status = visibility === "hidden" ? "minimized" : "error";
402
- notify();
403
- return Promise.resolve();
404
- }
405
- if (!record ||
406
- record.disposed ||
407
- record.retiring ||
408
- record.status === "error") {
409
- return enqueueMutation(windowId, async () => {
410
- const next = records.get(windowId);
411
- if (!next || next.disposed || next.retiring || next.status === "error")
412
- return;
413
- const nextVersion = ++next.visibilityVersion;
414
- next.visibility = visibility;
415
- next.status =
416
- visibility === "hidden"
417
- ? "minimized"
418
- : next.host
419
- ? "ready"
420
- : "loading";
421
- notify();
422
- await applyVisibility(next, nextVersion);
423
- });
424
- }
425
- const version = ++record.visibilityVersion;
426
- record.visibility = visibility;
427
- record.status =
428
- visibility === "hidden" ? "minimized" : record.host ? "ready" : "loading";
429
- notify();
430
- return enqueueMutation(windowId, () => applyVisibility(record, version));
431
- };
432
- const minimize = (windowId) => requestVisibility(windowId, "hidden");
433
- const restore = (windowId) => requestVisibility(windowId, "visible");
434
- const hideAll = async () => {
435
- if (managerDisposed)
436
- return;
437
- const visible = [...records.values()].filter((record) => !record.retiring && record.visibility === "visible");
438
- for (const record of visible)
439
- hiddenForDesktop.add(record);
440
- await Promise.all(visible.map((record) => minimize(record.launch.windowId)));
441
- };
442
- const restoreHidden = async () => {
443
- const hidden = [...hiddenForDesktop];
444
- hiddenForDesktop.clear();
445
- await Promise.all(hidden
446
- .filter((record) => records.get(record.launch.windowId) === record)
447
- .map((record) => restore(record.launch.windowId)));
448
- };
449
- const consultClose = async (record) => {
450
- // Manager disposal is the host/process teardown path; it cannot wait for
451
- // an interactive dirty-buffer decision.
452
- if (managerDisposed)
453
- return true;
454
- const requestClose = record.host?.requestClose;
455
- if (!requestClose)
456
- return true;
457
- try {
458
- return await requestClose();
459
- }
460
- catch (error) {
461
- if (isCurrent(record)) {
462
- record.status = "error";
463
- record.error = error;
464
- notify();
465
- }
466
- throw error;
467
- }
468
- };
469
- const close = (windowId) => {
470
- const current = records.get(windowId);
471
- const hadRecordAtCall = current !== undefined;
472
- // Invalidate an in-flight reconfiguration immediately. The serialized
473
- // close still performs the dirty-buffer consultation before disposal, but
474
- // a late reconfiguration result must not become the committed launch.
475
- current?.reconfigureController?.abort();
476
- if (current)
477
- current.closeRequested = true;
478
- return enqueueMutation(windowId, async () => {
479
- if (!hadRecordAtCall)
480
- return;
481
- const record = records.get(windowId);
482
- if (!record)
483
- return;
484
- // Consultation belongs inside the serialized mutation. A replacement
485
- // or second close must not race an App's dirty-buffer decision.
486
- let mayClose;
487
- try {
488
- mayClose = await consultClose(record);
489
- }
490
- catch (error) {
491
- record.closeRequested = false;
492
- throw error;
493
- }
494
- if (!mayClose) {
495
- record.closeRequested = false;
496
- return;
497
- }
498
- retire(record);
499
- try {
500
- await disposeRecordHost(record);
501
- }
502
- catch (error) {
503
- if (isCurrent(record)) {
504
- record.status = "error";
505
- // An earlier mount/visibility error remains the primary projection
506
- // error; this cleanup failure is exposed beside it.
507
- if (record.error === undefined)
508
- record.error = error;
509
- record.cleanupError = error;
510
- notify();
511
- }
512
- throw error;
513
- }
514
- if (!isCurrent(record))
515
- return;
516
- records.delete(windowId);
517
- notify();
518
- });
519
- };
520
- const update = (windowId, derive) => {
521
- const operation = enqueueMutation(windowId, async () => {
522
- if (managerDisposed)
523
- return;
524
- const record = records.get(windowId);
525
- if (!record || record.disposed || record.retiring)
526
- return;
527
- if (hasCloseRequest(record))
528
- return;
529
- const next = { ...derive(record.launch), windowId };
530
- options.validateReconfiguration?.(record.launch, next);
531
- if (options.isChromeOnlyChange?.(record.launch, next) === true) {
532
- record.launch = next;
533
- notify();
534
- return;
535
- }
536
- const controller = new AbortController();
537
- record.reconfigureController = controller;
538
- try {
539
- if (!record.host && record.mountPromise)
540
- await record.mountPromise;
541
- if (reconfigurationWasCancelled(record, controller))
542
- return;
543
- const host = record.host;
544
- if (!host)
545
- throw new Error("The App host is not ready for reconfiguration.");
546
- const hostOperation = Promise.resolve().then(() => host.reconfigure(next, controller.signal));
547
- record.hostOperation = hostOperation;
548
- try {
549
- await hostOperation;
550
- }
551
- finally {
552
- if (record.hostOperation === hostOperation)
553
- record.hostOperation = undefined;
554
- }
555
- if (reconfigurationWasCancelled(record, controller))
556
- return;
557
- // The old descriptor remains authoritative until the mounted host has
558
- // accepted its new configuration. Reconfiguration keeps the same
559
- // generation and container, so only the descriptor is committed here.
560
- record.launch = next;
561
- record.status = record.visibility === "hidden" ? "minimized" : "ready";
562
- record.error = undefined;
563
- record.cleanupError = undefined;
564
- notify();
565
- }
566
- catch (error) {
567
- if (reconfigurationWasCancelled(record, controller))
568
- return;
569
- // Keep the existing host and descriptor mounted so the caller can
570
- // report the failure and retry without creating a second iframe. The
571
- // rejected operation is the failure surface; changing the projection
572
- // to "error" would incorrectly hide that the old App is still usable.
573
- throw error;
574
- }
575
- finally {
576
- if (record.reconfigureController === controller)
577
- record.reconfigureController = undefined;
578
- }
579
- });
580
- return operation;
581
- };
582
- const reconfigure = (windowId, launch) => update(windowId, () => launch);
583
- const replace = (windowId, launch) => {
584
- let mounted;
585
- const operation = enqueueMutation(windowId, async () => {
586
- if (managerDisposed)
587
- return;
588
- const previous = records.get(windowId);
589
- let restoreWithDesktop = false;
590
- if (previous) {
591
- if (!(await consultClose(previous)))
592
- return;
593
- restoreWithDesktop = hiddenForDesktop.has(previous);
594
- retire(previous);
595
- try {
596
- await disposeRecordHost(previous);
597
- }
598
- catch (error) {
599
- if (isCurrent(previous)) {
600
- previous.status = "error";
601
- if (previous.error === undefined)
602
- previous.error = error;
603
- previous.cleanupError = error;
604
- notify();
605
- }
606
- throw error;
607
- }
608
- }
609
- const descriptor = { ...launch, windowId };
610
- const record = createRecord(descriptor, previous ? previous.generation + 1 : 1);
611
- if (previous?.visibility === "hidden") {
612
- record.visibility = "hidden";
613
- record.status = "minimized";
614
- }
615
- if (restoreWithDesktop)
616
- hiddenForDesktop.add(record);
617
- records.set(windowId, record);
618
- notify();
619
- mounted = startMount(record);
620
- });
621
- return operation.then(async () => {
622
- if (mounted)
623
- await mounted;
624
- });
625
- };
626
- const back = async (windowId) => {
627
- const record = records.get(windowId);
628
- if (!record ||
629
- !isActive(record) ||
630
- record.status !== "ready" ||
631
- record.navigation?.canGoBack !== true)
632
- return false;
633
- const requestBack = record.host?.requestBack;
634
- return requestBack ? await requestBack() : false;
635
- };
636
- const activateNavigation = async (windowId, activation) => {
637
- const record = records.get(windowId);
638
- if (!record || !isActive(record) || record.status !== "ready")
639
- return false;
640
- // Only a crumb or enabled command the App currently publishes.
641
- if (record.navigation === null ||
642
- !navigationActivationAllowed(record.navigation, activation))
643
- return false;
644
- const deliver = record.host?.activateNavigation;
645
- return deliver ? await deliver(activation) : false;
646
- };
647
- const activateStatus = async (windowId, action) => {
648
- const record = records.get(windowId);
649
- if (!record || !isActive(record) || record.status !== "ready")
650
- return false;
651
- // Only an enabled item (and option) the App currently declares.
652
- if (!statusActionAllowed(record.statusItems, action))
653
- return false;
654
- const deliver = record.host?.activateStatus;
655
- return deliver ? await deliver(action) : false;
656
- };
657
- const registerComputerChoice = (windowId, handler) => {
658
- const record = records.get(windowId);
659
- if (!record || !isActive(record))
660
- throw new AppError("App window is unavailable", {
661
- code: "DISCONNECTED",
662
- retryable: false,
663
- });
664
- record.computerChoice = handler;
665
- return () => {
666
- if (record.computerChoice === handler)
667
- record.computerChoice = undefined;
668
- };
669
- };
670
- const disposeAll = async () => {
671
- managerDisposed = true;
672
- for (const record of records.values()) {
673
- record.closeRequested = true;
674
- record.reconfigureController?.abort();
675
- }
676
- try {
677
- // Finish already-enqueued setup/state work before taking the record
678
- // snapshot. New public work is rejected by managerDisposed meanwhile.
679
- await Promise.allSettled(operations.values());
680
- const failures = [];
681
- const results = await Promise.allSettled([...records.keys()].map((windowId) => close(windowId)));
682
- for (const result of results) {
683
- if (result.status === "rejected")
684
- failures.push(result.reason);
685
- }
686
- if (failures.length > 0) {
687
- throw new AggregateError(failures, "Failed to dispose App windows");
688
- }
689
- }
690
- finally {
691
- listeners.clear();
692
- }
693
- };
694
- const dispose = () => {
695
- if (disposalInFlight)
696
- return disposalInFlight;
697
- const pending = disposeAll();
698
- disposalInFlight = pending;
699
- void pending.then(() => {
700
- if (disposalInFlight === pending)
701
- disposalInFlight = undefined;
702
- }, () => {
703
- if (disposalInFlight === pending)
704
- disposalInFlight = undefined;
705
- });
706
- return pending;
707
- };
708
- return {
709
- open,
710
- attach,
711
- minimize,
712
- restore,
713
- hideAll,
714
- restoreHidden,
715
- close,
716
- reconfigure,
717
- update,
718
- activateNavigation,
719
- activateStatus,
720
- registerComputerChoice,
721
- replace,
722
- back,
723
- dispose,
724
- read,
725
- subscribe: (listener) => {
726
- listeners.add(listener);
727
- return () => listeners.delete(listener);
728
- },
729
- };
730
- }