agl 1.0.17 → 21.0.2
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/.smartconfig.json +55 -0
- package/changelog.md +1026 -0
- package/cli.js +14 -0
- package/dist_serve/bundle.js +44778 -0
- package/dist_serve/index.html +13 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.authmodels.d.ts +126 -0
- package/dist_ts/classes.authmodels.js +1422 -0
- package/dist_ts/classes.authstore.d.ts +160 -0
- package/dist_ts/classes.authstore.js +2483 -0
- package/dist_ts/classes.browserresourcehost.d.ts +85 -0
- package/dist_ts/classes.browserresourcehost.js +912 -0
- package/dist_ts/classes.cli.d.ts +21 -0
- package/dist_ts/classes.cli.js +1082 -0
- package/dist_ts/classes.config.d.ts +23 -0
- package/dist_ts/classes.config.js +267 -0
- package/dist_ts/classes.controller.d.ts +523 -0
- package/dist_ts/classes.controller.js +10611 -0
- package/dist_ts/classes.controllermanagement.d.ts +32 -0
- package/dist_ts/classes.controllermanagement.js +222 -0
- package/dist_ts/classes.draftmanager.d.ts +30 -0
- package/dist_ts/classes.draftmanager.js +179 -0
- package/dist_ts/classes.embeddeddb.d.ts +30 -0
- package/dist_ts/classes.embeddeddb.js +159 -0
- package/dist_ts/classes.flexframedtransport.d.ts +56 -0
- package/dist_ts/classes.flexframedtransport.js +275 -0
- package/dist_ts/classes.flexmodels.private.d.ts +231 -0
- package/dist_ts/classes.flexmodels.private.js +985 -0
- package/dist_ts/classes.flexmodels.public.d.ts +207 -0
- package/dist_ts/classes.flexmodels.public.js +885 -0
- package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
- package/dist_ts/classes.flexprojectionreader.js +580 -0
- package/dist_ts/classes.flexservice.d.ts +196 -0
- package/dist_ts/classes.flexservice.js +3704 -0
- package/dist_ts/classes.flexstore.d.ts +174 -0
- package/dist_ts/classes.flexstore.js +3400 -0
- package/dist_ts/classes.flexsupervisor.d.ts +105 -0
- package/dist_ts/classes.flexsupervisor.js +1191 -0
- package/dist_ts/classes.gitreversion.d.ts +431 -0
- package/dist_ts/classes.gitreversion.js +10592 -0
- package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
- package/dist_ts/classes.harnessoperationmanager.js +602 -0
- package/dist_ts/classes.opencodeclient.d.ts +146 -0
- package/dist_ts/classes.opencodeclient.js +3003 -0
- package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
- package/dist_ts/classes.opencodesupervisor.js +1003 -0
- package/dist_ts/classes.passkeymanager.d.ts +34 -0
- package/dist_ts/classes.passkeymanager.js +456 -0
- package/dist_ts/classes.processinspection.d.ts +50 -0
- package/dist_ts/classes.processinspection.js +484 -0
- package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
- package/dist_ts/classes.resourcecoordinator.js +294 -0
- package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
- package/dist_ts/classes.sessionintelligencereader.js +40 -0
- package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
- package/dist_ts/classes.systemmetricssampler.js +168 -0
- package/dist_ts/classes.terminalmanager.d.ts +95 -0
- package/dist_ts/classes.terminalmanager.js +981 -0
- package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
- package/dist_ts/classes.terminalresourcehost.js +84 -0
- package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
- package/dist_ts/classes.upgradecoordinator.js +2155 -0
- package/dist_ts/classes.upgradetransaction.d.ts +92 -0
- package/dist_ts/classes.upgradetransaction.js +2125 -0
- package/dist_ts/classes.uploadmanager.d.ts +24 -0
- package/dist_ts/classes.uploadmanager.js +210 -0
- package/dist_ts/classes.validation.d.ts +91 -0
- package/dist_ts/classes.validation.js +1087 -0
- package/dist_ts/flexharness.child.d.ts +1 -0
- package/dist_ts/flexharness.child.js +28 -0
- package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
- package/dist_ts/functions.canonicaldirectory.js +144 -0
- package/dist_ts/functions.controllerdataroot.d.ts +15 -0
- package/dist_ts/functions.controllerdataroot.js +76 -0
- package/dist_ts/functions.embeddeddb.d.ts +2 -0
- package/dist_ts/functions.embeddeddb.js +21 -0
- package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
- package/dist_ts/functions.flexcredentialstore.js +93 -0
- package/dist_ts/functions.opencodeauth.d.ts +3 -0
- package/dist_ts/functions.opencodeauth.js +31 -0
- package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
- package/dist_ts/functions.runtimeenvironment.js +79 -0
- package/dist_ts/functions.upgradefollower.d.ts +3 -0
- package/dist_ts/functions.upgradefollower.js +95 -0
- package/dist_ts/index.d.ts +24 -0
- package/dist_ts/index.js +25 -0
- package/dist_ts/interfaces.auth.d.ts +181 -0
- package/dist_ts/interfaces.auth.js +82 -0
- package/dist_ts/interfaces.config.d.ts +34 -0
- package/dist_ts/interfaces.config.js +2 -0
- package/dist_ts/interfaces.flexipc.d.ts +939 -0
- package/dist_ts/interfaces.flexipc.js +1492 -0
- package/dist_ts/interfaces.gitreversion.d.ts +246 -0
- package/dist_ts/interfaces.gitreversion.js +2 -0
- package/dist_ts/interfaces.lifecycle.d.ts +37 -0
- package/dist_ts/interfaces.lifecycle.js +2 -0
- package/dist_ts/interfaces.opencode.d.ts +101 -0
- package/dist_ts/interfaces.opencode.js +3 -0
- package/dist_ts/interfaces.projects.d.ts +156 -0
- package/dist_ts/interfaces.projects.js +5 -0
- package/dist_ts/plugins.d.ts +36 -0
- package/dist_ts/plugins.js +40 -0
- package/dist_ts_interfaces/index.d.ts +2 -0
- package/dist_ts_interfaces/index.js +3 -0
- package/dist_ts_interfaces/interfaces.d.ts +747 -0
- package/dist_ts_interfaces/interfaces.js +47 -0
- package/dist_ts_interfaces/plugins.d.ts +4 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +806 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
- package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
- package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
- package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.migrationrunner.js +34 -0
- package/dist_ts_migration/index.d.ts +12 -0
- package/dist_ts_migration/index.js +13 -0
- package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
- package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
- package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
- package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
- package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
- package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
- package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
- package/dist_ts_migration/v18_resourcelayout.js +23 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
- package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
- package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
- package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
- package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
- package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocuments.js +138 -0
- package/html/index.html +13 -0
- package/{LICENSE → license.md} +2 -2
- package/package.json +86 -48
- package/readme.md +480 -0
- package/readme.plan.md +188 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.authmodels.ts +1245 -0
- package/ts/classes.authstore.ts +3274 -0
- package/ts/classes.browserresourcehost.ts +1205 -0
- package/ts/classes.cli.ts +1254 -0
- package/ts/classes.config.ts +351 -0
- package/ts/classes.controller.ts +14426 -0
- package/ts/classes.controllermanagement.ts +330 -0
- package/ts/classes.draftmanager.ts +232 -0
- package/ts/classes.embeddeddb.ts +166 -0
- package/ts/classes.flexframedtransport.ts +283 -0
- package/ts/classes.flexmodels.private.ts +823 -0
- package/ts/classes.flexmodels.public.ts +732 -0
- package/ts/classes.flexprojectionreader.ts +816 -0
- package/ts/classes.flexservice.ts +4302 -0
- package/ts/classes.flexstore.ts +4565 -0
- package/ts/classes.flexsupervisor.ts +1453 -0
- package/ts/classes.gitreversion.ts +13353 -0
- package/ts/classes.harnessoperationmanager.ts +870 -0
- package/ts/classes.opencodeclient.ts +4110 -0
- package/ts/classes.opencodesupervisor.ts +1280 -0
- package/ts/classes.passkeymanager.ts +570 -0
- package/ts/classes.processinspection.ts +623 -0
- package/ts/classes.resourcecoordinator.ts +436 -0
- package/ts/classes.sessionintelligencereader.ts +52 -0
- package/ts/classes.systemmetricssampler.ts +215 -0
- package/ts/classes.terminalmanager.ts +1207 -0
- package/ts/classes.terminalresourcehost.ts +122 -0
- package/ts/classes.upgradecoordinator.ts +2675 -0
- package/ts/classes.upgradetransaction.ts +2900 -0
- package/ts/classes.uploadmanager.ts +240 -0
- package/ts/classes.validation.ts +1409 -0
- package/ts/flexharness.child.ts +35 -0
- package/ts/functions.canonicaldirectory.ts +213 -0
- package/ts/functions.controllerdataroot.ts +107 -0
- package/ts/functions.embeddeddb.ts +22 -0
- package/ts/functions.flexcredentialstore.ts +128 -0
- package/ts/functions.opencodeauth.ts +34 -0
- package/ts/functions.runtimeenvironment.ts +90 -0
- package/ts/functions.upgradefollower.ts +115 -0
- package/ts/index.ts +36 -6
- package/ts/interfaces.auth.ts +316 -0
- package/ts/interfaces.config.ts +37 -0
- package/ts/interfaces.flexipc.ts +2586 -0
- package/ts/interfaces.gitreversion.ts +336 -0
- package/ts/interfaces.lifecycle.ts +46 -0
- package/ts/interfaces.opencode.ts +153 -0
- package/ts/interfaces.projects.ts +182 -0
- package/ts/plugins.ts +64 -0
- package/ts_interfaces/index.ts +2 -0
- package/ts_interfaces/interfaces.ts +827 -0
- package/ts_interfaces/plugins.ts +15 -0
- package/ts_interfaces/requests.ts +1186 -0
- package/ts_migration/classes.documentmigrationrunner.ts +32 -0
- package/ts_migration/classes.flexmigrationrunner.ts +33 -0
- package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
- package/ts_migration/classes.migrationrunner.ts +40 -0
- package/ts_migration/index.ts +12 -0
- package/ts_migration/v10_flexharnessv3migration.ts +35 -0
- package/ts_migration/v13_flexprojectionorder.ts +33 -0
- package/ts_migration/v16_flexprovidercredentials.ts +196 -0
- package/ts_migration/v18_resourcelayout.ts +55 -0
- package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
- package/ts_migration/v19_flexprojectmanagement.ts +28 -0
- package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
- package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
- package/ts_migration/v2_controllerdataroot.ts +2349 -0
- package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
- package/ts_migration/v6_protocoldocuments.ts +198 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/classes.controllersocketclient.ts +517 -0
- package/ts_web/classes.sessiondraftsync.ts +390 -0
- package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
- package/ts_web/index.ts +2 -0
- package/ts_web/plugins.ts +31 -0
- package/.gitlab-ci.yml +0 -71
- package/.npmignore +0 -5
- package/README.md +0 -40
- package/dist/cli.js +0 -2
- package/dist/index.d.ts +0 -0
- package/dist/index.js +0 -9
- package/dist/mod_bundle/index.d.ts +0 -1
- package/dist/mod_bundle/index.js +0 -18
- package/dist/mod_bundle/mod.plugins.d.ts +0 -4
- package/dist/mod_bundle/mod.plugins.js +0 -11
- package/dist/mod_install/index.d.ts +0 -4
- package/dist/mod_install/index.js +0 -21
- package/dist/mod_install/mod.plugins.d.ts +0 -1
- package/dist/mod_install/mod.plugins.js +0 -7
- package/dist/mod_standard/index.d.ts +0 -4
- package/dist/mod_standard/index.js +0 -21
- package/dist/mod_standard/mod.browser.d.ts +0 -6
- package/dist/mod_standard/mod.browser.js +0 -14
- package/dist/mod_standard/mod.clean.d.ts +0 -4
- package/dist/mod_standard/mod.clean.js +0 -12
- package/dist/mod_standard/mod.compile.d.ts +0 -4
- package/dist/mod_standard/mod.compile.js +0 -21
- package/dist/mod_standard/mod.plugins.d.ts +0 -8
- package/dist/mod_standard/mod.plugins.js +0 -19
- package/dist/mod_standard/mod.test.d.ts +0 -4
- package/dist/mod_standard/mod.test.js +0 -12
- package/dist/plr.cli.d.ts +0 -3
- package/dist/plr.cli.js +0 -57
- package/dist/plr.config.d.ts +0 -11
- package/dist/plr.config.js +0 -26
- package/dist/plr.mods.d.ts +0 -17
- package/dist/plr.mods.js +0 -17
- package/dist/plr.paths.d.ts +0 -19
- package/dist/plr.paths.js +0 -26
- package/dist/plr.plugins.d.ts +0 -8
- package/dist/plr.plugins.js +0 -16
- package/npmextra.json +0 -10
- package/ts/mod_bundle/index.ts +0 -20
- package/ts/mod_bundle/mod.plugins.ts +0 -9
- package/ts/mod_install/index.ts +0 -20
- package/ts/mod_install/mod.plugins.ts +0 -1
- package/ts/mod_standard/index.ts +0 -23
- package/ts/mod_standard/mod.browser.ts +0 -17
- package/ts/mod_standard/mod.clean.ts +0 -13
- package/ts/mod_standard/mod.compile.ts +0 -23
- package/ts/mod_standard/mod.plugins.ts +0 -17
- package/ts/mod_standard/mod.test.ts +0 -13
- package/ts/plr.cli.ts +0 -60
- package/ts/plr.config.ts +0 -36
- package/ts/plr.mods.ts +0 -25
- package/ts/plr.paths.ts +0 -29
- package/ts/plr.plugins.ts +0 -17
- package/tslint.json +0 -3
- package/yarn.lock +0 -3464
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IControllerResourceRuntimeHost } from './classes.resourcecoordinator.js';
|
|
3
|
+
import type { IControllerResourceDocument } from './interfaces.projects.js';
|
|
4
|
+
import {
|
|
5
|
+
flexIpcMaximumBrowserChannels,
|
|
6
|
+
type IFlexBrowserChannelBinding,
|
|
7
|
+
} from './interfaces.flexipc.js';
|
|
8
|
+
|
|
9
|
+
const maximumPendingFlexCapabilityIssuances = 128;
|
|
10
|
+
const maximumActiveHumanBrowserViews = 64;
|
|
11
|
+
const maximumTrackedCapabilityRevocations = 512;
|
|
12
|
+
const maximumCleanupOperationWaiters = 512;
|
|
13
|
+
const browserCapabilityAuthorizationTimeoutMs = 10_000;
|
|
14
|
+
const browserResourceCleanupTimeoutMs = 40_000;
|
|
15
|
+
|
|
16
|
+
const waitForSignal = async <TValue>(
|
|
17
|
+
promiseArg: Promise<TValue>,
|
|
18
|
+
signalArg: AbortSignal,
|
|
19
|
+
): Promise<TValue> => new Promise<TValue>((resolve, reject) => {
|
|
20
|
+
const abort = () => {
|
|
21
|
+
signalArg.removeEventListener('abort', abort);
|
|
22
|
+
reject(signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
|
|
23
|
+
};
|
|
24
|
+
signalArg.addEventListener('abort', abort, { once: true });
|
|
25
|
+
if (signalArg.aborted) {
|
|
26
|
+
abort();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
void promiseArg.then(
|
|
30
|
+
(value) => {
|
|
31
|
+
signalArg.removeEventListener('abort', abort);
|
|
32
|
+
resolve(value);
|
|
33
|
+
},
|
|
34
|
+
(errorArg) => {
|
|
35
|
+
signalArg.removeEventListener('abort', abort);
|
|
36
|
+
reject(errorArg);
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const waitForCleanupDeadline = async <TValue>(
|
|
42
|
+
promiseArg: Promise<TValue>,
|
|
43
|
+
deadlineAtArg: number,
|
|
44
|
+
messageArg: string,
|
|
45
|
+
signalArg?: AbortSignal,
|
|
46
|
+
): Promise<TValue> => new Promise<TValue>((resolve, reject) => {
|
|
47
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
48
|
+
let settled = false;
|
|
49
|
+
const finish = (operationArg: () => void) => {
|
|
50
|
+
if (settled) return;
|
|
51
|
+
settled = true;
|
|
52
|
+
if (timeout) clearTimeout(timeout);
|
|
53
|
+
signalArg?.removeEventListener('abort', abort);
|
|
54
|
+
operationArg();
|
|
55
|
+
};
|
|
56
|
+
const abort = () => finish(() => reject(
|
|
57
|
+
signalArg?.reason ?? new DOMException('The operation was aborted.', 'AbortError'),
|
|
58
|
+
));
|
|
59
|
+
void promiseArg.then(
|
|
60
|
+
(value) => finish(() => resolve(value)),
|
|
61
|
+
(errorArg) => finish(() => reject(errorArg)),
|
|
62
|
+
);
|
|
63
|
+
if (signalArg?.aborted) {
|
|
64
|
+
abort();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
signalArg?.addEventListener('abort', abort, { once: true });
|
|
68
|
+
const remainingMs = deadlineAtArg - Date.now();
|
|
69
|
+
if (remainingMs <= 0) {
|
|
70
|
+
finish(() => reject(new Error(messageArg)));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
timeout = setTimeout(() => finish(() => reject(new Error(messageArg))), remainingMs);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export interface IControllerBrowserResourceHostOptions {
|
|
77
|
+
runtimeDirectory: string;
|
|
78
|
+
authorizeCapability: plugins.browserRuntime.IBrowserRuntimeOptions['authorizeCapability'];
|
|
79
|
+
isFlexRunActive?(bindingArg: IFlexBrowserChannelBinding, runIdArg: string): boolean;
|
|
80
|
+
beforeOperation?: plugins.browserRuntime.IBrowserRuntimeOptions['beforeOperation'];
|
|
81
|
+
audit?: plugins.browserRuntime.IBrowserRuntimeOptions['audit'];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IControllerBrowserHumanView {
|
|
85
|
+
lease: plugins.browserRuntime.BrowserRuntimeLease;
|
|
86
|
+
activate(listenerArg: (eventArg: plugins.browserRuntime.TBrowserRuntimeEvent) => void): Promise<void>;
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface IControllerFlexBrowserChannel {
|
|
91
|
+
binding: IFlexBrowserChannelBinding;
|
|
92
|
+
capabilityToken: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface IActiveFlexBrowserChannel {
|
|
96
|
+
binding: IFlexBrowserChannelBinding;
|
|
97
|
+
runId: string;
|
|
98
|
+
capabilityId: string;
|
|
99
|
+
inbound: plugins.stream.PassThrough;
|
|
100
|
+
peer: plugins.browserRuntime.BrowserRuntimeFramedServerPeer;
|
|
101
|
+
notifyClosed: boolean;
|
|
102
|
+
closing: boolean;
|
|
103
|
+
peerCloseOperation?: IObservedCleanupOperation;
|
|
104
|
+
peerClosed: boolean;
|
|
105
|
+
capabilityRevoked: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface IFlexBrowserAuthorization {
|
|
109
|
+
binding: IFlexBrowserChannelBinding;
|
|
110
|
+
runId: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface IFlexBrowserChannelAdmission {
|
|
114
|
+
projectId: string;
|
|
115
|
+
sessionId: string;
|
|
116
|
+
runId: string;
|
|
117
|
+
peerId: string;
|
|
118
|
+
abortController: AbortController;
|
|
119
|
+
settled: Promise<void>;
|
|
120
|
+
resolveSettled(): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface IHumanBrowserViewAdmission {
|
|
124
|
+
abortController: AbortController;
|
|
125
|
+
settled: Promise<void>;
|
|
126
|
+
resolveSettled(): void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type TBrowserCapabilityOwner = 'flex' | 'human';
|
|
130
|
+
|
|
131
|
+
type TCleanupOperationOutcome =
|
|
132
|
+
| { status: 'fulfilled' }
|
|
133
|
+
| { status: 'rejected'; reason: unknown };
|
|
134
|
+
|
|
135
|
+
interface IObservedCleanupOperation {
|
|
136
|
+
outcome?: TCleanupOperationOutcome;
|
|
137
|
+
waiters: Set<(outcomeArg: TCleanupOperationOutcome) => void>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ICapabilityRevocationOperation extends IObservedCleanupOperation {
|
|
141
|
+
runtime: plugins.browserRuntime.BrowserRuntime;
|
|
142
|
+
owner: TBrowserCapabilityOwner;
|
|
143
|
+
onFulfilled: Set<() => void>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface IRuntimeStopOperation extends IObservedCleanupOperation {
|
|
147
|
+
runtime: plugins.browserRuntime.BrowserRuntime;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const observeCleanupOperation = (
|
|
151
|
+
operationArg: () => Promise<void>,
|
|
152
|
+
onSettledArg?: (outcomeArg: TCleanupOperationOutcome) => void,
|
|
153
|
+
): IObservedCleanupOperation => {
|
|
154
|
+
const operation: IObservedCleanupOperation = { waiters: new Set() };
|
|
155
|
+
const settle = (outcomeArg: TCleanupOperationOutcome) => {
|
|
156
|
+
if (operation.outcome) return;
|
|
157
|
+
operation.outcome = outcomeArg;
|
|
158
|
+
onSettledArg?.(outcomeArg);
|
|
159
|
+
const waiters = [...operation.waiters];
|
|
160
|
+
operation.waiters.clear();
|
|
161
|
+
for (const waiter of waiters) waiter(outcomeArg);
|
|
162
|
+
};
|
|
163
|
+
void Promise.resolve().then(operationArg).then(
|
|
164
|
+
() => settle({ status: 'fulfilled' }),
|
|
165
|
+
(reason) => settle({ status: 'rejected', reason }),
|
|
166
|
+
);
|
|
167
|
+
return operation;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const waitForObservedCleanup = async (
|
|
171
|
+
operationArg: IObservedCleanupOperation,
|
|
172
|
+
deadlineAtArg: number,
|
|
173
|
+
messageArg: string,
|
|
174
|
+
signalArg?: AbortSignal,
|
|
175
|
+
): Promise<TCleanupOperationOutcome> => {
|
|
176
|
+
if (operationArg.outcome) return operationArg.outcome;
|
|
177
|
+
signalArg?.throwIfAborted();
|
|
178
|
+
const remainingMs = deadlineAtArg - Date.now();
|
|
179
|
+
if (remainingMs <= 0) throw new Error(messageArg);
|
|
180
|
+
if (operationArg.waiters.size >= maximumCleanupOperationWaiters) {
|
|
181
|
+
throw new Error('Browser cleanup waiter capacity was reached.');
|
|
182
|
+
}
|
|
183
|
+
return new Promise<TCleanupOperationOutcome>((resolve, reject) => {
|
|
184
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
185
|
+
let settled = false;
|
|
186
|
+
const finish = (finishArg: () => void) => {
|
|
187
|
+
if (settled) return;
|
|
188
|
+
settled = true;
|
|
189
|
+
if (timeout) clearTimeout(timeout);
|
|
190
|
+
operationArg.waiters.delete(complete);
|
|
191
|
+
signalArg?.removeEventListener('abort', abort);
|
|
192
|
+
finishArg();
|
|
193
|
+
};
|
|
194
|
+
const complete = (outcomeArg: TCleanupOperationOutcome) => {
|
|
195
|
+
finish(() => resolve(outcomeArg));
|
|
196
|
+
};
|
|
197
|
+
const abort = () => finish(() => reject(
|
|
198
|
+
signalArg?.reason ?? new DOMException('The operation was aborted.', 'AbortError'),
|
|
199
|
+
));
|
|
200
|
+
operationArg.waiters.add(complete);
|
|
201
|
+
signalArg?.addEventListener('abort', abort, { once: true });
|
|
202
|
+
if (operationArg.outcome) {
|
|
203
|
+
complete(operationArg.outcome);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (signalArg?.aborted) {
|
|
207
|
+
abort();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
timeout = setTimeout(() => finish(() => reject(new Error(messageArg))), remainingMs);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export class ControllerBrowserResourceHost implements IControllerResourceRuntimeHost {
|
|
215
|
+
private runtime?: plugins.browserRuntime.BrowserRuntime;
|
|
216
|
+
private started = false;
|
|
217
|
+
private unavailable = false;
|
|
218
|
+
private stopping = false;
|
|
219
|
+
private cleanupTimeoutMs = browserResourceCleanupTimeoutMs;
|
|
220
|
+
private startPromise?: Promise<void>;
|
|
221
|
+
private stopPromise?: Promise<void>;
|
|
222
|
+
private readonly flexAuthorizationBindings = new Map<string, IFlexBrowserAuthorization>();
|
|
223
|
+
private readonly flexChannels = new Map<string, IActiveFlexBrowserChannel>();
|
|
224
|
+
private readonly flexChannelAdmissions = new Map<string, IFlexBrowserChannelAdmission>();
|
|
225
|
+
private readonly capabilityRevocationOperations = new Map<string, ICapabilityRevocationOperation>();
|
|
226
|
+
private readonly lateCapabilityIssuanceTasks = new Map<Promise<void>, TBrowserCapabilityOwner>();
|
|
227
|
+
private readonly humanViews = new Set<IControllerBrowserHumanView>();
|
|
228
|
+
private readonly humanViewAdmissions = new Set<IHumanBrowserViewAdmission>();
|
|
229
|
+
private admissionAbortController = new AbortController();
|
|
230
|
+
private readonly runtimeStopOperations = new WeakMap<
|
|
231
|
+
plugins.browserRuntime.BrowserRuntime,
|
|
232
|
+
IRuntimeStopOperation
|
|
233
|
+
>();
|
|
234
|
+
|
|
235
|
+
constructor(private readonly options: IControllerBrowserResourceHostOptions) {}
|
|
236
|
+
|
|
237
|
+
public async start(signalArg?: AbortSignal): Promise<void> {
|
|
238
|
+
if (this.started || this.unavailable) return;
|
|
239
|
+
if (this.startPromise) return await waitForSignal(
|
|
240
|
+
this.startPromise,
|
|
241
|
+
signalArg ?? new AbortController().signal,
|
|
242
|
+
);
|
|
243
|
+
if (this.stopping) throw new Error('Browser resource host cleanup is incomplete.');
|
|
244
|
+
if (this.admissionAbortController.signal.aborted) {
|
|
245
|
+
this.admissionAbortController = new AbortController();
|
|
246
|
+
}
|
|
247
|
+
const startPromise = this.performStart(signalArg);
|
|
248
|
+
this.startPromise = startPromise;
|
|
249
|
+
try {
|
|
250
|
+
await startPromise;
|
|
251
|
+
} finally {
|
|
252
|
+
if (this.startPromise === startPromise) this.startPromise = undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async performStart(signalArg?: AbortSignal): Promise<void> {
|
|
257
|
+
signalArg?.throwIfAborted();
|
|
258
|
+
const runtime = new plugins.browserRuntime.BrowserRuntime({
|
|
259
|
+
runtimeDirectory: this.options.runtimeDirectory,
|
|
260
|
+
authorizationTimeoutMs: browserCapabilityAuthorizationTimeoutMs,
|
|
261
|
+
authorizeCapability: this.options.authorizeCapability,
|
|
262
|
+
...(this.options.beforeOperation === undefined ? {} : {
|
|
263
|
+
beforeOperation: this.options.beforeOperation,
|
|
264
|
+
}),
|
|
265
|
+
...(this.options.audit === undefined ? {} : { audit: this.options.audit }),
|
|
266
|
+
});
|
|
267
|
+
this.runtime = runtime;
|
|
268
|
+
try {
|
|
269
|
+
await (signalArg ? waitForSignal(runtime.start(), signalArg) : runtime.start());
|
|
270
|
+
signalArg?.throwIfAborted();
|
|
271
|
+
if (this.stopping) throw new Error('Browser resource host cleanup began during startup.');
|
|
272
|
+
this.started = true;
|
|
273
|
+
} catch (errorArg) {
|
|
274
|
+
if (
|
|
275
|
+
errorArg instanceof plugins.browserRuntime.BrowserRuntimeError
|
|
276
|
+
&& (errorArg.code === 'CONFINEMENT_FAILED' || errorArg.code === 'INVALID_INPUT')
|
|
277
|
+
) {
|
|
278
|
+
await runtime.stop().catch((cleanupErrorArg) => {
|
|
279
|
+
throw new AggregateError(
|
|
280
|
+
[errorArg, cleanupErrorArg],
|
|
281
|
+
'BrowserRuntime startup and cleanup failed.',
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
if (this.runtime === runtime) this.runtime = undefined;
|
|
285
|
+
this.unavailable = true;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await runtime.stop().catch((cleanupErrorArg) => {
|
|
289
|
+
throw new AggregateError(
|
|
290
|
+
[errorArg, cleanupErrorArg],
|
|
291
|
+
'BrowserRuntime startup and cleanup failed.',
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
if (this.runtime === runtime) this.runtime = undefined;
|
|
295
|
+
throw errorArg;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public async stop(): Promise<void> {
|
|
300
|
+
if (this.stopPromise) return this.stopPromise;
|
|
301
|
+
const stopPromise = this.performStop();
|
|
302
|
+
this.stopPromise = stopPromise;
|
|
303
|
+
try {
|
|
304
|
+
await stopPromise;
|
|
305
|
+
} finally {
|
|
306
|
+
if (this.stopPromise === stopPromise) this.stopPromise = undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async performStop(): Promise<void> {
|
|
311
|
+
const runtime = this.runtime;
|
|
312
|
+
if (!runtime) {
|
|
313
|
+
if (
|
|
314
|
+
this.humanViews.size > 0
|
|
315
|
+
|| this.flexChannels.size > 0
|
|
316
|
+
|| this.flexChannelAdmissions.size > 0
|
|
317
|
+
|| this.humanViewAdmissions.size > 0
|
|
318
|
+
|| this.capabilityRevocationOperations.size > 0
|
|
319
|
+
|| this.lateCapabilityIssuanceTasks.size > 0
|
|
320
|
+
) throw new Error('Browser resource host cleanup has no runtime owner.');
|
|
321
|
+
this.stopping = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.stopping = true;
|
|
325
|
+
if (!this.admissionAbortController.signal.aborted) {
|
|
326
|
+
this.admissionAbortController.abort(new Error('Browser resource host is stopping.'));
|
|
327
|
+
}
|
|
328
|
+
const deadlineAt = Date.now() + this.cleanupTimeoutMs;
|
|
329
|
+
const cleanupErrors: unknown[] = [];
|
|
330
|
+
for (const cleanup of [
|
|
331
|
+
() => this.cancelAndDrainFlexChannelAdmissions(deadlineAt),
|
|
332
|
+
() => this.drainHumanViewAdmissions(deadlineAt),
|
|
333
|
+
() => this.closeAllHumanViews(deadlineAt),
|
|
334
|
+
() => this.closeAllFlexChannels(undefined, deadlineAt),
|
|
335
|
+
() => this.drainLateCapabilityIssuanceTasks(deadlineAt),
|
|
336
|
+
() => this.retryPendingCapabilityRevocations(deadlineAt),
|
|
337
|
+
]) {
|
|
338
|
+
await cleanup().catch((errorArg) => cleanupErrors.push(errorArg));
|
|
339
|
+
}
|
|
340
|
+
if (cleanupErrors.length > 0) {
|
|
341
|
+
throw new AggregateError(cleanupErrors, 'Browser resource host cleanup is incomplete.');
|
|
342
|
+
}
|
|
343
|
+
await this.stopRuntime(runtime, deadlineAt);
|
|
344
|
+
if (this.runtime === runtime) this.runtime = undefined;
|
|
345
|
+
this.started = false;
|
|
346
|
+
this.stopping = false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public authorizeFlexCapability(
|
|
350
|
+
requestArg: plugins.browserRuntime.TBrowserCapabilityAuthorizationRequest,
|
|
351
|
+
): boolean {
|
|
352
|
+
if (requestArg.role !== 'agent' || requestArg.source !== 'flex') return false;
|
|
353
|
+
const expected = this.flexAuthorizationBindings.get(requestArg.channelId);
|
|
354
|
+
return expected !== undefined
|
|
355
|
+
&& this.isFlexRunActive(expected.binding, expected.runId)
|
|
356
|
+
&& expected.binding.projectId === requestArg.projectId
|
|
357
|
+
&& expected.binding.browserResourceId === requestArg.browserResourceId
|
|
358
|
+
&& expected.binding.attachmentAuthorityId === requestArg.attachmentAuthorityId
|
|
359
|
+
&& expected.binding.attachmentRevision === requestArg.attachmentRevision
|
|
360
|
+
&& expected.binding.actorId === requestArg.actorId
|
|
361
|
+
&& expected.binding.peerId === requestArg.peerId
|
|
362
|
+
&& expected.binding.scopeId === requestArg.scopeId
|
|
363
|
+
&& expected.binding.channelId === requestArg.channelId
|
|
364
|
+
&& expected.binding.runId === requestArg.runId
|
|
365
|
+
&& expected.binding.sessionId.harnessId === requestArg.sessionId.harnessId
|
|
366
|
+
&& expected.binding.sessionId.nativeId === requestArg.sessionId.nativeId;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public async openFlexChannel(inputArg: {
|
|
370
|
+
resource: IControllerResourceDocument;
|
|
371
|
+
scopeId: string;
|
|
372
|
+
sessionId: string;
|
|
373
|
+
runId: string;
|
|
374
|
+
channelId: string;
|
|
375
|
+
peerId: string;
|
|
376
|
+
signal?: AbortSignal;
|
|
377
|
+
sendFrame(channelIdArg: string, bytesArg: Buffer): Promise<void>;
|
|
378
|
+
notifyClosed(channelIdArg: string): Promise<void>;
|
|
379
|
+
}): Promise<IControllerFlexBrowserChannel> {
|
|
380
|
+
const runtime = this.runtime;
|
|
381
|
+
const resource = inputArg.resource;
|
|
382
|
+
if (
|
|
383
|
+
!runtime
|
|
384
|
+
|| !this.started
|
|
385
|
+
|| resource.kind !== 'browser'
|
|
386
|
+
|| resource.lifecycle !== 'active'
|
|
387
|
+
|| resource.pendingAttachment !== undefined
|
|
388
|
+
|| resource.sessionId?.harnessId !== 'flex'
|
|
389
|
+
|| resource.sessionId.nativeId !== inputArg.sessionId
|
|
390
|
+
|| resource.projectId !== inputArg.scopeId
|
|
391
|
+
|| this.flexChannels.has(inputArg.channelId)
|
|
392
|
+
|| this.flexChannelAdmissions.has(inputArg.channelId)
|
|
393
|
+
|| this.stopping
|
|
394
|
+
) throw new Error('The Flex browser channel is unavailable.');
|
|
395
|
+
if (
|
|
396
|
+
this.flexChannels.size + this.flexChannelAdmissions.size
|
|
397
|
+
>= flexIpcMaximumBrowserChannels
|
|
398
|
+
) throw new Error('The Flex browser channel capacity was reached.');
|
|
399
|
+
inputArg.signal?.throwIfAborted();
|
|
400
|
+
this.admissionAbortController.signal.throwIfAborted();
|
|
401
|
+
let resolveAdmissionSettled!: () => void;
|
|
402
|
+
const admission: IFlexBrowserChannelAdmission = {
|
|
403
|
+
projectId: resource.projectId,
|
|
404
|
+
sessionId: inputArg.sessionId,
|
|
405
|
+
runId: inputArg.runId,
|
|
406
|
+
peerId: inputArg.peerId,
|
|
407
|
+
abortController: new AbortController(),
|
|
408
|
+
settled: new Promise<void>((resolve) => { resolveAdmissionSettled = resolve; }),
|
|
409
|
+
resolveSettled: () => resolveAdmissionSettled(),
|
|
410
|
+
};
|
|
411
|
+
const abortAdmission = (signalArg: AbortSignal) => {
|
|
412
|
+
if (!admission.abortController.signal.aborted) {
|
|
413
|
+
admission.abortController.abort(
|
|
414
|
+
signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
const abortFromInput = () => abortAdmission(inputArg.signal!);
|
|
419
|
+
const abortFromHost = () => abortAdmission(this.admissionAbortController.signal);
|
|
420
|
+
inputArg.signal?.addEventListener('abort', abortFromInput, { once: true });
|
|
421
|
+
this.admissionAbortController.signal.addEventListener('abort', abortFromHost, { once: true });
|
|
422
|
+
this.flexChannelAdmissions.set(inputArg.channelId, admission);
|
|
423
|
+
if (inputArg.signal?.aborted) abortFromInput();
|
|
424
|
+
if (this.admissionAbortController.signal.aborted) abortFromHost();
|
|
425
|
+
const binding: IFlexBrowserChannelBinding = {
|
|
426
|
+
projectId: resource.projectId,
|
|
427
|
+
browserResourceId: resource.id,
|
|
428
|
+
attachmentAuthorityId: resource.attachmentAuthorityId,
|
|
429
|
+
attachmentRevision: resource.attachmentRevision,
|
|
430
|
+
actorId: `flex-run:${plugins.crypto.createHash('sha256')
|
|
431
|
+
.update(JSON.stringify([inputArg.scopeId, inputArg.sessionId, inputArg.runId]))
|
|
432
|
+
.digest('hex')}`,
|
|
433
|
+
peerId: inputArg.peerId,
|
|
434
|
+
role: 'agent',
|
|
435
|
+
source: 'flex',
|
|
436
|
+
scopeId: inputArg.scopeId,
|
|
437
|
+
channelId: inputArg.channelId,
|
|
438
|
+
runId: inputArg.runId,
|
|
439
|
+
sessionId: { harnessId: 'flex', nativeId: inputArg.sessionId },
|
|
440
|
+
};
|
|
441
|
+
const inbound = new plugins.stream.PassThrough();
|
|
442
|
+
const outbound = new plugins.stream.Writable({
|
|
443
|
+
write: (chunkArg, _encodingArg, callbackArg) => {
|
|
444
|
+
const bytes = Buffer.from(chunkArg as Uint8Array);
|
|
445
|
+
void inputArg.sendFrame(inputArg.channelId, bytes).then(
|
|
446
|
+
() => callbackArg(),
|
|
447
|
+
(errorArg) => callbackArg(errorArg instanceof Error ? errorArg : new Error('Frame send failed.')),
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
destroy: (errorArg, callbackArg) => {
|
|
451
|
+
const channel = this.flexChannels.get(inputArg.channelId);
|
|
452
|
+
if (channel?.notifyClosed) {
|
|
453
|
+
channel.notifyClosed = false;
|
|
454
|
+
void inputArg.notifyClosed(inputArg.channelId).catch(() => undefined);
|
|
455
|
+
void this.closeFlexChannel(inputArg.channelId, true).catch(() => undefined);
|
|
456
|
+
}
|
|
457
|
+
callbackArg(errorArg);
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
this.flexAuthorizationBindings.set(inputArg.channelId, {
|
|
461
|
+
binding,
|
|
462
|
+
runId: inputArg.runId,
|
|
463
|
+
});
|
|
464
|
+
let capabilityId = '';
|
|
465
|
+
let issuancePromise: ReturnType<plugins.browserRuntime.BrowserRuntime['issueCapability']> | undefined;
|
|
466
|
+
let channelRegistered = false;
|
|
467
|
+
try {
|
|
468
|
+
admission.abortController.signal.throwIfAborted();
|
|
469
|
+
if (!this.isFlexRunActive(binding, inputArg.runId)) {
|
|
470
|
+
throw new Error('The Flex browser run is no longer active.');
|
|
471
|
+
}
|
|
472
|
+
await this.retryPendingCapabilityRevocations(
|
|
473
|
+
Date.now() + browserCapabilityAuthorizationTimeoutMs,
|
|
474
|
+
admission.abortController.signal,
|
|
475
|
+
);
|
|
476
|
+
if (this.lateCapabilityIssuanceTasks.size >= maximumPendingFlexCapabilityIssuances) {
|
|
477
|
+
throw new Error('Browser capability issuance capacity was reached.');
|
|
478
|
+
}
|
|
479
|
+
issuancePromise = runtime.issueCapability({
|
|
480
|
+
...binding,
|
|
481
|
+
expiresInMs: 24 * 60 * 60 * 1000,
|
|
482
|
+
});
|
|
483
|
+
const issued = await waitForSignal(issuancePromise, admission.abortController.signal);
|
|
484
|
+
issuancePromise = undefined;
|
|
485
|
+
capabilityId = issued.capabilityId;
|
|
486
|
+
const peer = runtime.attachTrustedFramedPeer({
|
|
487
|
+
...binding,
|
|
488
|
+
readable: inbound,
|
|
489
|
+
writable: outbound,
|
|
490
|
+
});
|
|
491
|
+
this.flexChannels.set(inputArg.channelId, {
|
|
492
|
+
binding,
|
|
493
|
+
runId: inputArg.runId,
|
|
494
|
+
capabilityId,
|
|
495
|
+
inbound,
|
|
496
|
+
peer,
|
|
497
|
+
notifyClosed: true,
|
|
498
|
+
closing: false,
|
|
499
|
+
peerClosed: false,
|
|
500
|
+
capabilityRevoked: false,
|
|
501
|
+
});
|
|
502
|
+
channelRegistered = true;
|
|
503
|
+
admission.abortController.signal.throwIfAborted();
|
|
504
|
+
if (!this.isFlexRunActive(binding, inputArg.runId)) {
|
|
505
|
+
throw new Error('The Flex browser run is no longer active.');
|
|
506
|
+
}
|
|
507
|
+
return { binding, capabilityToken: issued.capabilityToken };
|
|
508
|
+
} catch (errorArg) {
|
|
509
|
+
const cleanupErrors: unknown[] = [];
|
|
510
|
+
if (issuancePromise) {
|
|
511
|
+
this.trackLateCapabilityIssuance(runtime, issuancePromise, 'flex');
|
|
512
|
+
issuancePromise = undefined;
|
|
513
|
+
}
|
|
514
|
+
if (channelRegistered) {
|
|
515
|
+
const channel = this.flexChannels.get(inputArg.channelId);
|
|
516
|
+
if (channel) channel.notifyClosed = false;
|
|
517
|
+
await this.closeFlexChannel(
|
|
518
|
+
inputArg.channelId,
|
|
519
|
+
false,
|
|
520
|
+
undefined,
|
|
521
|
+
Date.now() + this.cleanupTimeoutMs,
|
|
522
|
+
).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg));
|
|
523
|
+
} else {
|
|
524
|
+
this.flexAuthorizationBindings.delete(inputArg.channelId);
|
|
525
|
+
inbound.destroy();
|
|
526
|
+
outbound.destroy();
|
|
527
|
+
if (capabilityId) {
|
|
528
|
+
this.trackCapabilityRevocation(runtime, capabilityId, 'flex');
|
|
529
|
+
await this.awaitCapabilityRevocation(
|
|
530
|
+
capabilityId,
|
|
531
|
+
Date.now() + this.cleanupTimeoutMs,
|
|
532
|
+
).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (cleanupErrors.length > 0) {
|
|
536
|
+
throw new AggregateError(
|
|
537
|
+
[errorArg, ...cleanupErrors],
|
|
538
|
+
'Flex browser channel admission and cleanup failed.',
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
throw errorArg;
|
|
542
|
+
} finally {
|
|
543
|
+
inputArg.signal?.removeEventListener('abort', abortFromInput);
|
|
544
|
+
this.admissionAbortController.signal.removeEventListener('abort', abortFromHost);
|
|
545
|
+
this.flexChannelAdmissions.delete(inputArg.channelId);
|
|
546
|
+
admission.resolveSettled();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
public async receiveFlexFrame(channelIdArg: string, bytesArg: Buffer, peerIdArg: string): Promise<void> {
|
|
551
|
+
const channel = this.flexChannels.get(channelIdArg);
|
|
552
|
+
if (
|
|
553
|
+
!channel
|
|
554
|
+
|| channel.binding.peerId !== peerIdArg
|
|
555
|
+
|| channel.closing
|
|
556
|
+
|| !this.isFlexRunActive(channel.binding, channel.runId)
|
|
557
|
+
) {
|
|
558
|
+
if (channel && !channel.closing && !this.isFlexRunActive(channel.binding, channel.runId)) {
|
|
559
|
+
void this.closeFlexChannel(channelIdArg, true).catch(() => undefined);
|
|
560
|
+
}
|
|
561
|
+
throw new Error('The Flex browser channel is unavailable.');
|
|
562
|
+
}
|
|
563
|
+
await new Promise<void>((resolve, reject) => {
|
|
564
|
+
channel.inbound.write(bytesArg, (errorArg) => errorArg ? reject(errorArg) : resolve());
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
public async closeFlexChannel(
|
|
569
|
+
channelIdArg: string,
|
|
570
|
+
notifyChildArg = false,
|
|
571
|
+
peerIdArg?: string,
|
|
572
|
+
deadlineAtArg = Date.now() + this.cleanupTimeoutMs,
|
|
573
|
+
): Promise<void> {
|
|
574
|
+
const channel = this.flexChannels.get(channelIdArg);
|
|
575
|
+
if (!channel) return;
|
|
576
|
+
if (peerIdArg !== undefined && channel.binding.peerId !== peerIdArg) {
|
|
577
|
+
throw new Error('The Flex browser channel is unavailable.');
|
|
578
|
+
}
|
|
579
|
+
if (notifyChildArg) channel.notifyClosed = true;
|
|
580
|
+
else channel.notifyClosed = false;
|
|
581
|
+
channel.closing = true;
|
|
582
|
+
this.flexAuthorizationBindings.delete(channelIdArg);
|
|
583
|
+
const peerCleanup = (async () => {
|
|
584
|
+
if (channel.peerClosed) return;
|
|
585
|
+
channel.peerCloseOperation ??= observeCleanupOperation(
|
|
586
|
+
() => channel.peer.close(),
|
|
587
|
+
(outcomeArg) => {
|
|
588
|
+
if (outcomeArg.status === 'fulfilled') channel.peerClosed = true;
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
const outcome = await waitForObservedCleanup(
|
|
592
|
+
channel.peerCloseOperation,
|
|
593
|
+
deadlineAtArg,
|
|
594
|
+
'Flex browser peer cleanup timed out.',
|
|
595
|
+
);
|
|
596
|
+
if (outcome.status === 'rejected') {
|
|
597
|
+
channel.peerCloseOperation = undefined;
|
|
598
|
+
throw outcome.reason;
|
|
599
|
+
}
|
|
600
|
+
})();
|
|
601
|
+
const capabilityCleanup = (async () => {
|
|
602
|
+
if (channel.capabilityRevoked) return;
|
|
603
|
+
const runtime = this.runtime;
|
|
604
|
+
if (!runtime) throw new Error('BrowserRuntime is unavailable during channel cleanup.');
|
|
605
|
+
this.trackCapabilityRevocation(
|
|
606
|
+
runtime,
|
|
607
|
+
channel.capabilityId,
|
|
608
|
+
'flex',
|
|
609
|
+
() => { channel.capabilityRevoked = true; },
|
|
610
|
+
);
|
|
611
|
+
await this.awaitCapabilityRevocation(channel.capabilityId, deadlineAtArg);
|
|
612
|
+
channel.capabilityRevoked = true;
|
|
613
|
+
})();
|
|
614
|
+
const results = await Promise.allSettled([peerCleanup, capabilityCleanup]);
|
|
615
|
+
if (channel.peerClosed && channel.capabilityRevoked) {
|
|
616
|
+
channel.inbound.destroy();
|
|
617
|
+
this.flexChannels.delete(channelIdArg);
|
|
618
|
+
}
|
|
619
|
+
const errors = results
|
|
620
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
621
|
+
.map((result) => result.reason);
|
|
622
|
+
if (errors.length > 0) {
|
|
623
|
+
throw new AggregateError(errors, 'Flex browser channel cleanup is incomplete.');
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
public async closeAllFlexChannels(
|
|
628
|
+
peerIdArg?: string,
|
|
629
|
+
deadlineAtArg = Date.now() + this.cleanupTimeoutMs,
|
|
630
|
+
): Promise<void> {
|
|
631
|
+
const admissions = [...this.flexChannelAdmissions.entries()].filter(([, admission]) => (
|
|
632
|
+
peerIdArg === undefined || admission.peerId === peerIdArg
|
|
633
|
+
));
|
|
634
|
+
for (const [channelId, admission] of admissions) {
|
|
635
|
+
this.flexAuthorizationBindings.delete(channelId);
|
|
636
|
+
if (!admission.abortController.signal.aborted) {
|
|
637
|
+
admission.abortController.abort(new Error('Flex browser channel admission was cancelled.'));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const admissionResults = await Promise.allSettled(admissions.map(([, admission]) => (
|
|
641
|
+
waitForCleanupDeadline(
|
|
642
|
+
admission.settled,
|
|
643
|
+
deadlineAtArg,
|
|
644
|
+
'Flex browser channel admission cleanup timed out.',
|
|
645
|
+
)
|
|
646
|
+
)));
|
|
647
|
+
const channels = [...this.flexChannels.values()].filter((channel) => (
|
|
648
|
+
peerIdArg === undefined || channel.binding.peerId === peerIdArg
|
|
649
|
+
));
|
|
650
|
+
const results = await Promise.allSettled(
|
|
651
|
+
channels.map((channel) => this.closeFlexChannel(
|
|
652
|
+
channel.binding.channelId,
|
|
653
|
+
true,
|
|
654
|
+
undefined,
|
|
655
|
+
deadlineAtArg,
|
|
656
|
+
)),
|
|
657
|
+
);
|
|
658
|
+
const errors = [...admissionResults, ...results]
|
|
659
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
660
|
+
.map((result) => result.reason);
|
|
661
|
+
if (peerIdArg === undefined) {
|
|
662
|
+
await this.drainLateCapabilityIssuanceTasks(deadlineAtArg, 'flex')
|
|
663
|
+
.catch((errorArg) => errors.push(errorArg));
|
|
664
|
+
await this.retryPendingCapabilityRevocations(deadlineAtArg, undefined, 'flex')
|
|
665
|
+
.catch((errorArg) => errors.push(errorArg));
|
|
666
|
+
}
|
|
667
|
+
if (errors.length > 0) throw new AggregateError(errors, 'Flex browser channel cleanup is incomplete.');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
public async closeFlexChannelsForRun(
|
|
671
|
+
projectIdArg: string,
|
|
672
|
+
sessionIdArg: string,
|
|
673
|
+
runIdArg: string,
|
|
674
|
+
): Promise<void> {
|
|
675
|
+
const deadlineAt = Date.now() + this.cleanupTimeoutMs;
|
|
676
|
+
const admissions = [...this.flexChannelAdmissions.entries()].filter(([, admission]) => (
|
|
677
|
+
admission.projectId === projectIdArg
|
|
678
|
+
&& admission.sessionId === sessionIdArg
|
|
679
|
+
&& admission.runId === runIdArg
|
|
680
|
+
));
|
|
681
|
+
for (const [channelId, admission] of admissions) {
|
|
682
|
+
this.flexAuthorizationBindings.delete(channelId);
|
|
683
|
+
if (!admission.abortController.signal.aborted) {
|
|
684
|
+
admission.abortController.abort(new Error('Flex browser run ended.'));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const admissionResults = await Promise.allSettled(admissions.map(([, admission]) => (
|
|
688
|
+
waitForCleanupDeadline(
|
|
689
|
+
admission.settled,
|
|
690
|
+
deadlineAt,
|
|
691
|
+
'Flex browser channel admission cleanup timed out.',
|
|
692
|
+
)
|
|
693
|
+
)));
|
|
694
|
+
const channels = [...this.flexChannels.values()].filter((channel) => (
|
|
695
|
+
channel.binding.projectId === projectIdArg
|
|
696
|
+
&& channel.binding.sessionId.nativeId === sessionIdArg
|
|
697
|
+
&& channel.runId === runIdArg
|
|
698
|
+
));
|
|
699
|
+
const channelResults = await Promise.allSettled(channels.map((channel) => (
|
|
700
|
+
this.closeFlexChannel(channel.binding.channelId, true, undefined, deadlineAt)
|
|
701
|
+
)));
|
|
702
|
+
const errors = [...admissionResults, ...channelResults]
|
|
703
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
704
|
+
.map((result) => result.reason);
|
|
705
|
+
if (errors.length > 0) {
|
|
706
|
+
throw new AggregateError(errors, 'Flex browser run channel cleanup is incomplete.');
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
public async disconnectPeer(peerIdArg: string): Promise<void> {
|
|
711
|
+
await this.runtime?.disconnectPeer(peerIdArg);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
public async openHumanView(inputArg: {
|
|
715
|
+
resource: IControllerResourceDocument;
|
|
716
|
+
actorId: string;
|
|
717
|
+
peerId: string;
|
|
718
|
+
signal: AbortSignal;
|
|
719
|
+
onDisconnect(signalArg: AbortSignal): Promise<void> | void;
|
|
720
|
+
}): Promise<IControllerBrowserHumanView> {
|
|
721
|
+
const runtime = this.runtime;
|
|
722
|
+
if (!runtime || !this.started || inputArg.resource.kind !== 'browser' || this.stopping) {
|
|
723
|
+
throw new Error('BrowserRuntime is unavailable.');
|
|
724
|
+
}
|
|
725
|
+
if (this.humanViews.size + this.humanViewAdmissions.size >= maximumActiveHumanBrowserViews) {
|
|
726
|
+
throw new Error('Human browser view capacity was reached.');
|
|
727
|
+
}
|
|
728
|
+
inputArg.signal.throwIfAborted();
|
|
729
|
+
this.admissionAbortController.signal.throwIfAborted();
|
|
730
|
+
let resolveAdmissionSettled!: () => void;
|
|
731
|
+
const admission: IHumanBrowserViewAdmission = {
|
|
732
|
+
abortController: new AbortController(),
|
|
733
|
+
settled: new Promise<void>((resolve) => { resolveAdmissionSettled = resolve; }),
|
|
734
|
+
resolveSettled: () => resolveAdmissionSettled(),
|
|
735
|
+
};
|
|
736
|
+
const abortAdmission = (signalArg: AbortSignal) => {
|
|
737
|
+
if (!admission.abortController.signal.aborted) {
|
|
738
|
+
admission.abortController.abort(
|
|
739
|
+
signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const abortFromInput = () => abortAdmission(inputArg.signal);
|
|
744
|
+
const abortFromHost = () => abortAdmission(this.admissionAbortController.signal);
|
|
745
|
+
inputArg.signal.addEventListener('abort', abortFromInput, { once: true });
|
|
746
|
+
this.admissionAbortController.signal.addEventListener('abort', abortFromHost, { once: true });
|
|
747
|
+
this.humanViewAdmissions.add(admission);
|
|
748
|
+
if (inputArg.signal.aborted) abortFromInput();
|
|
749
|
+
if (this.admissionAbortController.signal.aborted) abortFromHost();
|
|
750
|
+
const binding = {
|
|
751
|
+
projectId: inputArg.resource.projectId,
|
|
752
|
+
browserResourceId: inputArg.resource.id,
|
|
753
|
+
attachmentAuthorityId: inputArg.resource.attachmentAuthorityId,
|
|
754
|
+
attachmentRevision: inputArg.resource.attachmentRevision,
|
|
755
|
+
actorId: inputArg.actorId,
|
|
756
|
+
peerId: inputArg.peerId,
|
|
757
|
+
role: 'human' as const,
|
|
758
|
+
source: 'human' as const,
|
|
759
|
+
};
|
|
760
|
+
let humanView: IControllerBrowserHumanView | undefined;
|
|
761
|
+
let subscription: plugins.browserRuntime.IBrowserRuntimeFrameSubscription | undefined;
|
|
762
|
+
let activationPromise: Promise<void> | undefined;
|
|
763
|
+
let subscriptionCloseOperation: IObservedCleanupOperation | undefined;
|
|
764
|
+
let subscriptionClosed = false;
|
|
765
|
+
let capabilityRevoked = false;
|
|
766
|
+
let closing = false;
|
|
767
|
+
let runtimeDisconnectActive = false;
|
|
768
|
+
let runtimeDisconnectComplete = false;
|
|
769
|
+
let capabilityId = '';
|
|
770
|
+
let issuancePromise: ReturnType<plugins.browserRuntime.BrowserRuntime['issueCapability']> | undefined;
|
|
771
|
+
const closeSubscription = async (deadlineAtArg: number): Promise<void> => {
|
|
772
|
+
if (subscriptionClosed || !subscription) return;
|
|
773
|
+
subscriptionCloseOperation ??= observeCleanupOperation(
|
|
774
|
+
() => subscription!.close(),
|
|
775
|
+
(outcomeArg) => {
|
|
776
|
+
if (outcomeArg.status === 'fulfilled') subscriptionClosed = true;
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
const outcome = await waitForObservedCleanup(
|
|
780
|
+
subscriptionCloseOperation,
|
|
781
|
+
deadlineAtArg,
|
|
782
|
+
'Browser view subscription cleanup timed out.',
|
|
783
|
+
);
|
|
784
|
+
if (outcome.status === 'rejected') {
|
|
785
|
+
subscriptionCloseOperation = undefined;
|
|
786
|
+
throw outcome.reason;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
try {
|
|
790
|
+
await this.retryPendingCapabilityRevocations(
|
|
791
|
+
Date.now() + browserCapabilityAuthorizationTimeoutMs,
|
|
792
|
+
admission.abortController.signal,
|
|
793
|
+
);
|
|
794
|
+
if (this.lateCapabilityIssuanceTasks.size >= maximumPendingFlexCapabilityIssuances) {
|
|
795
|
+
throw new Error('Browser capability issuance capacity was reached.');
|
|
796
|
+
}
|
|
797
|
+
issuancePromise = runtime.issueCapability({
|
|
798
|
+
...binding,
|
|
799
|
+
disconnect: async (signalArg) => {
|
|
800
|
+
closing = true;
|
|
801
|
+
runtimeDisconnectActive = true;
|
|
802
|
+
try {
|
|
803
|
+
await inputArg.onDisconnect(signalArg);
|
|
804
|
+
} finally {
|
|
805
|
+
runtimeDisconnectActive = false;
|
|
806
|
+
runtimeDisconnectComplete = true;
|
|
807
|
+
subscriptionClosed = true;
|
|
808
|
+
capabilityRevoked = true;
|
|
809
|
+
if (humanView) this.humanViews.delete(humanView);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
const issued = await waitForSignal(issuancePromise, admission.abortController.signal);
|
|
814
|
+
issuancePromise = undefined;
|
|
815
|
+
capabilityId = issued.capabilityId;
|
|
816
|
+
const lease = await runtime.acquireLease({
|
|
817
|
+
...binding,
|
|
818
|
+
capabilityToken: issued.capabilityToken,
|
|
819
|
+
signal: admission.abortController.signal,
|
|
820
|
+
});
|
|
821
|
+
admission.abortController.signal.throwIfAborted();
|
|
822
|
+
humanView = {
|
|
823
|
+
lease,
|
|
824
|
+
activate: (listenerArg) => {
|
|
825
|
+
if (closing) return Promise.reject(new Error('Browser view cleanup has begun.'));
|
|
826
|
+
if (subscription && !subscriptionClosed) return Promise.resolve();
|
|
827
|
+
if (subscriptionClosed) {
|
|
828
|
+
subscription = undefined;
|
|
829
|
+
subscriptionCloseOperation = undefined;
|
|
830
|
+
subscriptionClosed = false;
|
|
831
|
+
}
|
|
832
|
+
activationPromise ??= (async () => {
|
|
833
|
+
let initialStateReceived = false;
|
|
834
|
+
const candidate = await lease.subscribeEvents((eventArg) => {
|
|
835
|
+
if (!initialStateReceived) {
|
|
836
|
+
if (eventArg.type !== 'state') {
|
|
837
|
+
throw new Error('BrowserRuntime did not publish an initial state event.');
|
|
838
|
+
}
|
|
839
|
+
initialStateReceived = true;
|
|
840
|
+
}
|
|
841
|
+
listenerArg(eventArg);
|
|
842
|
+
});
|
|
843
|
+
subscription = candidate;
|
|
844
|
+
if (!initialStateReceived) {
|
|
845
|
+
await closeSubscription(Date.now() + this.cleanupTimeoutMs);
|
|
846
|
+
throw new Error('BrowserRuntime did not publish an initial state event.');
|
|
847
|
+
}
|
|
848
|
+
if (closing) {
|
|
849
|
+
await closeSubscription(Date.now() + this.cleanupTimeoutMs);
|
|
850
|
+
throw new Error('Browser view cleanup began during activation.');
|
|
851
|
+
}
|
|
852
|
+
})().finally(() => {
|
|
853
|
+
if (!subscription || subscriptionClosed) activationPromise = undefined;
|
|
854
|
+
});
|
|
855
|
+
return activationPromise;
|
|
856
|
+
},
|
|
857
|
+
close: async () => {
|
|
858
|
+
closing = true;
|
|
859
|
+
const deadlineAt = Date.now() + this.cleanupTimeoutMs;
|
|
860
|
+
if (runtimeDisconnectActive) {
|
|
861
|
+
await closeSubscription(deadlineAt);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const errors: unknown[] = [];
|
|
865
|
+
if (activationPromise) {
|
|
866
|
+
await waitForCleanupDeadline(
|
|
867
|
+
activationPromise.catch(() => undefined),
|
|
868
|
+
deadlineAt,
|
|
869
|
+
'Browser view activation cleanup timed out.',
|
|
870
|
+
).catch((errorArg) => errors.push(errorArg));
|
|
871
|
+
}
|
|
872
|
+
if (!subscriptionClosed) {
|
|
873
|
+
if (subscription) {
|
|
874
|
+
await closeSubscription(deadlineAt).catch((errorArg) => errors.push(errorArg));
|
|
875
|
+
} else {
|
|
876
|
+
subscriptionClosed = true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (!capabilityRevoked) {
|
|
880
|
+
this.trackCapabilityRevocation(
|
|
881
|
+
runtime,
|
|
882
|
+
capabilityId,
|
|
883
|
+
'human',
|
|
884
|
+
() => { capabilityRevoked = true; },
|
|
885
|
+
);
|
|
886
|
+
await this.awaitCapabilityRevocation(capabilityId, deadlineAt).then(
|
|
887
|
+
() => { capabilityRevoked = true; },
|
|
888
|
+
(errorArg) => errors.push(errorArg),
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
if (runtimeDisconnectComplete || capabilityRevoked) subscriptionClosed = true;
|
|
892
|
+
if (errors.length > 0) throw new AggregateError(errors, 'Browser view cleanup is incomplete.');
|
|
893
|
+
this.humanViews.delete(humanView!);
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
this.humanViews.add(humanView);
|
|
897
|
+
return humanView;
|
|
898
|
+
} catch (errorArg) {
|
|
899
|
+
const cleanupErrors: unknown[] = [];
|
|
900
|
+
if (issuancePromise) {
|
|
901
|
+
this.trackLateCapabilityIssuance(runtime, issuancePromise, 'human');
|
|
902
|
+
} else if (capabilityId && !capabilityRevoked) {
|
|
903
|
+
this.trackCapabilityRevocation(runtime, capabilityId, 'human');
|
|
904
|
+
await this.awaitCapabilityRevocation(
|
|
905
|
+
capabilityId,
|
|
906
|
+
Date.now() + this.cleanupTimeoutMs,
|
|
907
|
+
).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg));
|
|
908
|
+
}
|
|
909
|
+
if (humanView) this.humanViews.delete(humanView);
|
|
910
|
+
if (cleanupErrors.length > 0) {
|
|
911
|
+
throw new AggregateError(
|
|
912
|
+
[errorArg, ...cleanupErrors],
|
|
913
|
+
'Browser view admission and cleanup failed.',
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
throw errorArg;
|
|
917
|
+
} finally {
|
|
918
|
+
inputArg.signal.removeEventListener('abort', abortFromInput);
|
|
919
|
+
this.admissionAbortController.signal.removeEventListener('abort', abortFromHost);
|
|
920
|
+
this.humanViewAdmissions.delete(admission);
|
|
921
|
+
admission.resolveSettled();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
public async startResource(
|
|
926
|
+
resourceArg: IControllerResourceDocument,
|
|
927
|
+
signalArg: AbortSignal,
|
|
928
|
+
): Promise<void> {
|
|
929
|
+
signalArg.throwIfAborted();
|
|
930
|
+
await this.reconcileAttachment(resourceArg, signalArg);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
public async stopResource(
|
|
934
|
+
resourceArg: IControllerResourceDocument,
|
|
935
|
+
signalArg: AbortSignal,
|
|
936
|
+
): Promise<void> {
|
|
937
|
+
signalArg.throwIfAborted();
|
|
938
|
+
if (!this.runtime || this.stopping) return;
|
|
939
|
+
await this.runtime.terminateResource(resourceArg.projectId, resourceArg.id);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
public isRunning(resourceArg: IControllerResourceDocument): boolean {
|
|
943
|
+
if (!this.runtime || this.stopping) return false;
|
|
944
|
+
const registration = this.runtime.listResources().find((candidate) => (
|
|
945
|
+
candidate.projectId === resourceArg.projectId
|
|
946
|
+
&& candidate.browserResourceId === resourceArg.id
|
|
947
|
+
));
|
|
948
|
+
return registration?.incarnationStatus === 'running';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
public isAvailable(): boolean {
|
|
952
|
+
return this.started && !this.stopping;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
public async reconcileAttachment(
|
|
956
|
+
resourceArg: IControllerResourceDocument,
|
|
957
|
+
signalArg: AbortSignal,
|
|
958
|
+
): Promise<void> {
|
|
959
|
+
signalArg.throwIfAborted();
|
|
960
|
+
if (resourceArg.kind !== 'browser') return;
|
|
961
|
+
if (this.unavailable) return;
|
|
962
|
+
if (!this.runtime || this.stopping) throw new Error('BrowserRuntime is not started.');
|
|
963
|
+
const attachmentBinding = {
|
|
964
|
+
attachmentAuthorityId: resourceArg.attachmentAuthorityId,
|
|
965
|
+
attachmentRevision: resourceArg.attachmentRevision,
|
|
966
|
+
sessionId: resourceArg.sessionId,
|
|
967
|
+
};
|
|
968
|
+
const existing = this.runtime.listResources().find((candidate) => (
|
|
969
|
+
candidate.projectId === resourceArg.projectId
|
|
970
|
+
&& candidate.browserResourceId === resourceArg.id
|
|
971
|
+
));
|
|
972
|
+
if (!existing) {
|
|
973
|
+
this.runtime.registerResource({
|
|
974
|
+
projectId: resourceArg.projectId,
|
|
975
|
+
browserResourceId: resourceArg.id,
|
|
976
|
+
attachmentBinding,
|
|
977
|
+
});
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
await this.runtime.applyAttachmentBinding({
|
|
981
|
+
projectId: resourceArg.projectId,
|
|
982
|
+
browserResourceId: resourceArg.id,
|
|
983
|
+
attachmentBinding,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
public async retireResource(
|
|
988
|
+
resourceArg: IControllerResourceDocument,
|
|
989
|
+
signalArg: AbortSignal,
|
|
990
|
+
): Promise<void> {
|
|
991
|
+
signalArg.throwIfAborted();
|
|
992
|
+
if (!this.runtime || this.stopping || resourceArg.kind !== 'browser') return;
|
|
993
|
+
const registered = this.runtime.listResources().some((candidate) => (
|
|
994
|
+
candidate.projectId === resourceArg.projectId
|
|
995
|
+
&& candidate.browserResourceId === resourceArg.id
|
|
996
|
+
));
|
|
997
|
+
if (!registered) {
|
|
998
|
+
this.runtime.registerResource({
|
|
999
|
+
projectId: resourceArg.projectId,
|
|
1000
|
+
browserResourceId: resourceArg.id,
|
|
1001
|
+
attachmentBinding: {
|
|
1002
|
+
attachmentAuthorityId: resourceArg.attachmentAuthorityId,
|
|
1003
|
+
attachmentRevision: resourceArg.attachmentRevision,
|
|
1004
|
+
sessionId: resourceArg.sessionId,
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
await this.runtime.retireResource(resourceArg.projectId, resourceArg.id);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
public get availability(): 'available' | 'unavailable' {
|
|
1012
|
+
return this.started && !this.stopping ? 'available' : 'unavailable';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private isFlexRunActive(bindingArg: IFlexBrowserChannelBinding, runIdArg: string): boolean {
|
|
1016
|
+
if (bindingArg.runId !== runIdArg) return false;
|
|
1017
|
+
return this.options.isFlexRunActive?.(bindingArg, runIdArg) ?? true;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private trackCapabilityRevocation(
|
|
1021
|
+
runtimeArg: plugins.browserRuntime.BrowserRuntime,
|
|
1022
|
+
capabilityIdArg: string,
|
|
1023
|
+
ownerArg: TBrowserCapabilityOwner,
|
|
1024
|
+
onFulfilledArg?: () => void,
|
|
1025
|
+
): void {
|
|
1026
|
+
const existing = this.capabilityRevocationOperations.get(capabilityIdArg);
|
|
1027
|
+
if (existing && existing.owner !== ownerArg) {
|
|
1028
|
+
throw new Error('Browser capability cleanup ownership changed unexpectedly.');
|
|
1029
|
+
}
|
|
1030
|
+
if (existing && existing.outcome?.status !== 'rejected') {
|
|
1031
|
+
if (onFulfilledArg) existing.onFulfilled.add(onFulfilledArg);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const onFulfilled = new Set(existing?.onFulfilled ?? []);
|
|
1035
|
+
if (onFulfilledArg) onFulfilled.add(onFulfilledArg);
|
|
1036
|
+
if (existing) this.capabilityRevocationOperations.delete(capabilityIdArg);
|
|
1037
|
+
if (this.capabilityRevocationOperations.size >= maximumTrackedCapabilityRevocations) {
|
|
1038
|
+
throw new Error('Browser capability cleanup capacity was reached.');
|
|
1039
|
+
}
|
|
1040
|
+
const operation = observeCleanupOperation(
|
|
1041
|
+
() => runtimeArg.revokeCapability(capabilityIdArg),
|
|
1042
|
+
(outcomeArg) => {
|
|
1043
|
+
if (outcomeArg.status === 'fulfilled') {
|
|
1044
|
+
for (const callback of operation.onFulfilled) callback();
|
|
1045
|
+
operation.onFulfilled.clear();
|
|
1046
|
+
if (this.capabilityRevocationOperations.get(capabilityIdArg) === operation) {
|
|
1047
|
+
this.capabilityRevocationOperations.delete(capabilityIdArg);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
) as ICapabilityRevocationOperation;
|
|
1052
|
+
operation.runtime = runtimeArg;
|
|
1053
|
+
operation.owner = ownerArg;
|
|
1054
|
+
operation.onFulfilled = onFulfilled;
|
|
1055
|
+
this.capabilityRevocationOperations.set(capabilityIdArg, operation);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
private async awaitCapabilityRevocation(
|
|
1059
|
+
capabilityIdArg: string,
|
|
1060
|
+
deadlineAtArg: number,
|
|
1061
|
+
signalArg?: AbortSignal,
|
|
1062
|
+
): Promise<void> {
|
|
1063
|
+
const operation = this.capabilityRevocationOperations.get(capabilityIdArg);
|
|
1064
|
+
if (!operation) return;
|
|
1065
|
+
const outcome = await waitForObservedCleanup(
|
|
1066
|
+
operation,
|
|
1067
|
+
deadlineAtArg,
|
|
1068
|
+
'Browser capability cleanup timed out.',
|
|
1069
|
+
signalArg,
|
|
1070
|
+
);
|
|
1071
|
+
if (outcome.status === 'rejected') throw outcome.reason;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private trackLateCapabilityIssuance(
|
|
1075
|
+
runtimeArg: plugins.browserRuntime.BrowserRuntime,
|
|
1076
|
+
issuancePromiseArg: ReturnType<plugins.browserRuntime.BrowserRuntime['issueCapability']>,
|
|
1077
|
+
ownerArg: TBrowserCapabilityOwner,
|
|
1078
|
+
): void {
|
|
1079
|
+
const task = (async () => {
|
|
1080
|
+
const issued = await issuancePromiseArg;
|
|
1081
|
+
this.trackCapabilityRevocation(runtimeArg, issued.capabilityId, ownerArg);
|
|
1082
|
+
await this.awaitCapabilityRevocation(
|
|
1083
|
+
issued.capabilityId,
|
|
1084
|
+
Date.now() + this.cleanupTimeoutMs,
|
|
1085
|
+
);
|
|
1086
|
+
})();
|
|
1087
|
+
this.lateCapabilityIssuanceTasks.set(task, ownerArg);
|
|
1088
|
+
void task.catch(() => undefined).finally(() => this.lateCapabilityIssuanceTasks.delete(task));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private async closeAllHumanViews(deadlineAtArg: number): Promise<void> {
|
|
1092
|
+
const results = await Promise.allSettled(
|
|
1093
|
+
[...this.humanViews].map((view) => waitForCleanupDeadline(
|
|
1094
|
+
view.close(),
|
|
1095
|
+
deadlineAtArg,
|
|
1096
|
+
'Human browser view cleanup timed out.',
|
|
1097
|
+
)),
|
|
1098
|
+
);
|
|
1099
|
+
const errors = results
|
|
1100
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
1101
|
+
.map((result) => result.reason);
|
|
1102
|
+
if (errors.length > 0) {
|
|
1103
|
+
throw new AggregateError(errors, 'Human browser view cleanup is incomplete.');
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
private async retryPendingCapabilityRevocations(
|
|
1108
|
+
deadlineAtArg: number,
|
|
1109
|
+
signalArg?: AbortSignal,
|
|
1110
|
+
ownerArg?: TBrowserCapabilityOwner,
|
|
1111
|
+
): Promise<void> {
|
|
1112
|
+
const entries = [...this.capabilityRevocationOperations.entries()].filter(([, operation]) => (
|
|
1113
|
+
ownerArg === undefined || operation.owner === ownerArg
|
|
1114
|
+
));
|
|
1115
|
+
for (const [capabilityId, operation] of entries) {
|
|
1116
|
+
if (operation.outcome?.status === 'rejected') {
|
|
1117
|
+
this.trackCapabilityRevocation(operation.runtime, capabilityId, operation.owner);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const results = await Promise.allSettled(
|
|
1121
|
+
[...this.capabilityRevocationOperations.entries()]
|
|
1122
|
+
.filter(([, operation]) => ownerArg === undefined || operation.owner === ownerArg)
|
|
1123
|
+
.map(([capabilityId]) => (
|
|
1124
|
+
this.awaitCapabilityRevocation(capabilityId, deadlineAtArg, signalArg)
|
|
1125
|
+
)),
|
|
1126
|
+
);
|
|
1127
|
+
const errors = results
|
|
1128
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
1129
|
+
.map((result) => result.reason);
|
|
1130
|
+
if (errors.length > 0) {
|
|
1131
|
+
throw new AggregateError(errors, 'Pending browser capability cleanup is incomplete.');
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private async cancelAndDrainFlexChannelAdmissions(deadlineAtArg: number): Promise<void> {
|
|
1136
|
+
const admissions = [...this.flexChannelAdmissions.entries()];
|
|
1137
|
+
for (const [channelId, admission] of admissions) {
|
|
1138
|
+
this.flexAuthorizationBindings.delete(channelId);
|
|
1139
|
+
if (!admission.abortController.signal.aborted) {
|
|
1140
|
+
admission.abortController.abort(new Error('Browser resource host is stopping.'));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
await Promise.all(admissions.map(([, admission]) => waitForCleanupDeadline(
|
|
1144
|
+
admission.settled,
|
|
1145
|
+
deadlineAtArg,
|
|
1146
|
+
'Flex browser channel admission cleanup timed out.',
|
|
1147
|
+
)));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private async drainHumanViewAdmissions(deadlineAtArg: number): Promise<void> {
|
|
1151
|
+
const admissions = [...this.humanViewAdmissions];
|
|
1152
|
+
for (const admission of admissions) {
|
|
1153
|
+
if (!admission.abortController.signal.aborted) {
|
|
1154
|
+
admission.abortController.abort(new Error('Browser resource host is stopping.'));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
await Promise.all(admissions.map((admission) => waitForCleanupDeadline(
|
|
1158
|
+
admission.settled,
|
|
1159
|
+
deadlineAtArg,
|
|
1160
|
+
'Human browser view admission cleanup timed out.',
|
|
1161
|
+
)));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private async drainLateCapabilityIssuanceTasks(
|
|
1165
|
+
deadlineAtArg: number,
|
|
1166
|
+
ownerArg?: TBrowserCapabilityOwner,
|
|
1167
|
+
): Promise<void> {
|
|
1168
|
+
const tasks = [...this.lateCapabilityIssuanceTasks.entries()]
|
|
1169
|
+
.filter(([, owner]) => ownerArg === undefined || owner === ownerArg)
|
|
1170
|
+
.map(([task]) => task);
|
|
1171
|
+
const results = await Promise.allSettled(tasks.map((task) => waitForCleanupDeadline(
|
|
1172
|
+
task,
|
|
1173
|
+
deadlineAtArg,
|
|
1174
|
+
'Late browser capability issuance cleanup timed out.',
|
|
1175
|
+
)));
|
|
1176
|
+
const errors = results
|
|
1177
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
1178
|
+
.map((result) => result.reason);
|
|
1179
|
+
if (errors.length > 0) {
|
|
1180
|
+
throw new AggregateError(errors, 'Late browser capability issuance cleanup is incomplete.');
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
private async stopRuntime(
|
|
1185
|
+
runtimeArg: plugins.browserRuntime.BrowserRuntime,
|
|
1186
|
+
deadlineAtArg: number,
|
|
1187
|
+
): Promise<void> {
|
|
1188
|
+
let operation = this.runtimeStopOperations.get(runtimeArg);
|
|
1189
|
+
if (operation?.outcome?.status === 'rejected') {
|
|
1190
|
+
this.runtimeStopOperations.delete(runtimeArg);
|
|
1191
|
+
operation = undefined;
|
|
1192
|
+
}
|
|
1193
|
+
if (!operation) {
|
|
1194
|
+
operation = observeCleanupOperation(() => runtimeArg.stop()) as IRuntimeStopOperation;
|
|
1195
|
+
operation.runtime = runtimeArg;
|
|
1196
|
+
this.runtimeStopOperations.set(runtimeArg, operation);
|
|
1197
|
+
}
|
|
1198
|
+
const outcome = await waitForObservedCleanup(
|
|
1199
|
+
operation,
|
|
1200
|
+
deadlineAtArg,
|
|
1201
|
+
'BrowserRuntime shutdown timed out.',
|
|
1202
|
+
);
|
|
1203
|
+
if (outcome.status === 'rejected') throw outcome.reason;
|
|
1204
|
+
}
|
|
1205
|
+
}
|