@jmfederico/pi-web 1.202608.2 → 1.202609.1
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/README.md +3 -3
- package/dist/.plugins-ready +1 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/apple-touch-icon-dev.png +0 -0
- package/dist/client/assets/index-AroYaCLx.js +4130 -0
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
- package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
- package/dist/client/favicon-dev.svg +8 -0
- package/dist/client/index.html +5 -5
- package/dist/client/pwa-icon-dev-192.png +0 -0
- package/dist/client/pwa-icon-dev-512.png +0 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +9 -1
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +25 -19
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/nativeServices/serviceAction.js +38 -26
- package/dist/nativeServices/serviceAction.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +3 -3
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-packages/captains-log/README.md +11 -0
- package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
- package/dist/pi-packages/captains-log/dist/browser/index.js +144 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +105 -0
- package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
- package/dist/pi-packages/captains-log/dist/companion.js +126 -0
- package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
- package/dist/pi-packages/captains-log/dist/server.js +210 -0
- package/dist/pi-packages/captains-log/dist/store.js +75 -0
- package/dist/pi-packages/captains-log/docs/usage.md +30 -0
- package/dist/pi-packages/captains-log/package.json +35 -0
- package/dist/pi-packages/captains-log/tsconfig.json +21 -0
- package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
- package/dist/pi-packages/relays/package.json +1 -1
- package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
- package/dist/pi-packages/relays/prompts/relay-worktree.md +13 -142
- package/dist/pi-packages/relays/prompts/relay.md +25 -132
- package/dist/pi-packages/relays/relayDiscovery.js +2 -2
- package/dist/pi-packages/relays/relaysPanelElement.js +1 -1
- package/dist/pi-packages/relays/skills/relay/SKILL.md +59 -88
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +253 -0
- package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
- package/dist/pi-web-plugins/files/package.json +15 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/git/git-backend.js +5 -5
- package/dist/pi-web-plugins/git/server-plugin.js +5 -3
- package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
- package/dist/pi-web-plugins/mermaid/package.json +13 -0
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
- package/dist/pi-web-plugins/terminal/package.json +16 -0
- package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
- package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +162 -65
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +45 -35
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
- package/dist/piWebVersionReport.js +12 -4
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +222 -23
- package/dist/plugin-api.js +1 -0
- package/dist/pluginRecoveryCli.js +3 -2
- package/dist/pluginRecoveryCli.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +33 -12
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +6 -1
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/deploymentIdentity.js +67 -0
- package/dist/server/deploymentIdentity.js.map +1 -0
- package/dist/server/deploymentIdentityRoutes.js +22 -0
- package/dist/server/deploymentIdentityRoutes.js.map +1 -0
- package/dist/server/knownAutoInstallPiPackages.js +1 -1
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
- package/dist/server/knownPiPackages.js +12 -0
- package/dist/server/knownPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +2 -2
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +70 -7
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/notices/serverNoticeRoutes.js +34 -0
- package/dist/server/notices/serverNoticeRoutes.js.map +1 -0
- package/dist/server/notices/serverNoticeService.js +26 -0
- package/dist/server/notices/serverNoticeService.js.map +1 -0
- package/dist/server/notices/serverNoticeStore.js +119 -0
- package/dist/server/notices/serverNoticeStore.js.map +1 -0
- package/dist/server/piPackageService.js +5 -5
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +46 -12
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginLifecycle.js +26 -3
- package/dist/server/piWebPluginLifecycle.js.map +1 -1
- package/dist/server/piWebPluginService.js +37 -2
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +1 -1
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/pluginCallbackDrain.js +35 -0
- package/dist/server/pluginCallbackDrain.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +740 -0
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +1028 -107
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
- package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond.js +82 -21
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +1 -5
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +2 -2
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/clientSessionPreview.js +14 -0
- package/dist/server/sessions/clientSessionPreview.js.map +1 -0
- package/dist/server/sessions/piSessionEventConnections.js +65 -0
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +171 -1
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +442 -90
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionActivityMarker.js +105 -0
- package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +3 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +18 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +1 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/transcriptBranchCache.js +81 -0
- package/dist/server/sessions/transcriptBranchCache.js.map +1 -0
- package/dist/server/sessions/transcriptMessages.js +54 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -0
- package/dist/server/terminals/requiredTerminalService.js +103 -0
- package/dist/server/terminals/requiredTerminalService.js.map +1 -0
- package/dist/server/webSocketBridge.js +339 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +6 -1
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceCatalog.js +3 -1
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +38 -5
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/server-plugin-api.d.ts +216 -22
- package/dist/server-plugin-api.js +319 -2
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/serverPluginRecovery.js +4 -0
- package/dist/serverPluginRecovery.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +3 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +47 -17
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +28 -7
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +21 -1
- package/dist/shared/pluginApiTypes.js +0 -1
- package/dist/shared/pluginBackendProtocol.js +163 -2
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginIds.js +8 -1
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/requiredTerminalPlugin.js +6 -0
- package/dist/shared/requiredTerminalPlugin.js.map +1 -0
- package/dist/shared/serverNoticeContract.js +88 -0
- package/dist/shared/serverNoticeContract.js.map +1 -0
- package/dist/shared/sessionDefaults.js +41 -0
- package/dist/shared/sessionDefaults.js.map +1 -0
- package/docs/config.md +77 -37
- package/docs/plugins.md +146 -1213
- package/examples/session-bridge-plugin/README.md +20 -0
- package/examples/session-bridge-plugin/docs/usage.md +54 -0
- package/examples/session-bridge-plugin/package.json +26 -0
- package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
- package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
- package/examples/session-bridge-plugin/src/companion.ts +65 -0
- package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
- package/examples/session-bridge-plugin/src/server.ts +85 -0
- package/examples/session-bridge-plugin/src/store.ts +57 -0
- package/examples/session-bridge-plugin/tsconfig.json +21 -0
- package/examples/workspace-provider-plugin/README.md +4 -4
- package/examples/workspace-provider-plugin/package.json +1 -1
- package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
- package/examples/workspace-provider-plugin/src/server.ts +15 -11
- package/package.json +17 -11
- package/dist/client/assets/CodeViewer-BMWwxG7q.js +0 -4
- package/dist/client/assets/TerminalPanel-CacQDIYn.js +0 -187
- package/dist/client/assets/index-DUW2xnoV.js +0 -4279
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
- package/dist/server/terminalProxyRoutes.js +0 -141
- package/dist/server/terminalProxyRoutes.js.map +0 -1
- package/dist/server/terminals/terminalRoutes.js +0 -155
- package/dist/server/terminals/terminalRoutes.js.map +0 -1
- package/dist/server/terminals/terminalService.js.map +0 -1
- package/dist/server/terminals/terminalSize.js +0 -17
- package/dist/server/terminals/terminalSize.js.map +0 -1
|
@@ -1,32 +1,56 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { isPiWebPluginId } from "../../shared/pluginIds.js";
|
|
1
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { parseServerNoticeScope, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES, SERVER_PLUGIN_NOTICE_SOURCE_PREFIX, serverNoticeStringExceedsUtf8ByteLimit, } from "../../shared/serverNoticeContract.js";
|
|
6
|
+
import { REQUIRED_TERMINAL_PLUGIN_ID, REQUIRED_TERMINAL_RECOVERY_GUIDANCE, } from "../../shared/requiredTerminalPlugin.js";
|
|
7
|
+
import { REQUIRED_TERMINAL_SERVICE_CAPABILITY } from "../terminals/requiredTerminalService.js";
|
|
2
8
|
import { createServerPluginExecFile } from "./serverPluginExec.js";
|
|
3
9
|
const DEFAULT_LIFECYCLE_TIMEOUT_MS = 10_000;
|
|
4
10
|
/**
|
|
5
11
|
* Resolves exactly one desired catalog snapshot and activates its server
|
|
6
12
|
* entries. The resulting runtime is immutable except for explicit shutdown;
|
|
7
|
-
*
|
|
13
|
+
* this lifecycle intentionally has no hot reload or unload path.
|
|
8
14
|
*/
|
|
9
15
|
export async function createServerPluginRuntime(options) {
|
|
10
16
|
if (options.safeStart === "none") {
|
|
11
17
|
return await ServerPluginRuntime.activate({ plugins: [], diagnostics: [] }, options);
|
|
12
18
|
}
|
|
13
19
|
const snapshot = await options.catalog.snapshot(options.safeStart === "bundled-only" ? { scope: "bundled" } : undefined);
|
|
20
|
+
if (options.enforceRequiredTerminal !== false)
|
|
21
|
+
requireTerminalCatalogEntry(snapshot);
|
|
14
22
|
return await ServerPluginRuntime.activate(snapshot, options);
|
|
15
23
|
}
|
|
16
24
|
export class ServerPluginRuntime {
|
|
17
|
-
constructor(safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs) {
|
|
25
|
+
constructor(dataDir, safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs, noticeSink, hostCapabilities, hostCapabilityFactories, lateHostCapabilities, enforceRequiredTerminal) {
|
|
26
|
+
this.dataDir = dataDir;
|
|
18
27
|
this.safeStart = safeStart;
|
|
19
28
|
this.diagnostics = diagnostics;
|
|
20
29
|
this.logger = logger;
|
|
21
30
|
this.importer = importer;
|
|
22
31
|
this.execFile = execFile;
|
|
23
32
|
this.lifecycleTimeoutMs = lifecycleTimeoutMs;
|
|
33
|
+
this.noticeSink = noticeSink;
|
|
34
|
+
this.enforceRequiredTerminal = enforceRequiredTerminal;
|
|
24
35
|
this.recordsById = new Map();
|
|
36
|
+
this.declaredCapabilitiesByKey = new Map();
|
|
37
|
+
this.activeCapabilitiesByKey = new Map();
|
|
25
38
|
this.activePlugins = [];
|
|
39
|
+
this.stagedPlugins = [];
|
|
40
|
+
this.lifetimesAborted = false;
|
|
41
|
+
this.lateHostCapabilitiesResumed = false;
|
|
26
42
|
this.stopped = false;
|
|
43
|
+
for (const provision of hostCapabilities) {
|
|
44
|
+
this.registerHostCapability(provision);
|
|
45
|
+
this.activeCapabilitiesByKey.set(provision.key, provision);
|
|
46
|
+
}
|
|
47
|
+
for (const factory of hostCapabilityFactories)
|
|
48
|
+
this.registerHostCapability(factory);
|
|
49
|
+
for (const capability of lateHostCapabilities)
|
|
50
|
+
this.registerHostCapability(capability);
|
|
27
51
|
}
|
|
28
52
|
static async activate(snapshot, options) {
|
|
29
|
-
const runtime = new ServerPluginRuntime(options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"));
|
|
53
|
+
const runtime = new ServerPluginRuntime(resolve(options.dataDir), options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"), options.noticeSink, snapshotHostCapabilities(options.hostCapabilities), snapshotHostCapabilityFactories(options.hostCapabilityFactories), snapshotLateHostCapabilities(options.lateHostCapabilities), options.enforceRequiredTerminal !== false && options.safeStart !== "none");
|
|
30
54
|
try {
|
|
31
55
|
await runtime.start(snapshot.plugins);
|
|
32
56
|
return runtime;
|
|
@@ -36,6 +60,12 @@ export class ServerPluginRuntime {
|
|
|
36
60
|
throw error;
|
|
37
61
|
}
|
|
38
62
|
}
|
|
63
|
+
registerHostCapability(declaration) {
|
|
64
|
+
if (this.declaredCapabilitiesByKey.has(declaration.key)) {
|
|
65
|
+
throw new Error(`Host capability ${formatCapability(declaration.capability)} is configured more than once`);
|
|
66
|
+
}
|
|
67
|
+
this.declaredCapabilitiesByKey.set(declaration.key, declaration);
|
|
68
|
+
}
|
|
39
69
|
safeStartLevel() {
|
|
40
70
|
return this.safeStart;
|
|
41
71
|
}
|
|
@@ -48,11 +78,67 @@ export class ServerPluginRuntime {
|
|
|
48
78
|
.map((record) => Object.freeze({ ...record })));
|
|
49
79
|
}
|
|
50
80
|
providerContributions() {
|
|
51
|
-
return Object.freeze(this.activePlugins.flatMap((active) => active.
|
|
81
|
+
return Object.freeze(this.activePlugins.flatMap((active) => active.providerContribution === undefined ? [] : [active.providerContribution]));
|
|
82
|
+
}
|
|
83
|
+
pairedBackendContributions() {
|
|
84
|
+
return Object.freeze(this.activePlugins.flatMap((active) => active.pairedBackendContribution === undefined ? [] : [active.pairedBackendContribution]));
|
|
52
85
|
}
|
|
53
|
-
|
|
86
|
+
/** Resolves one exact active capability for a core host consumer. */
|
|
87
|
+
resolve(capability) {
|
|
88
|
+
const parsed = snapshotTypedCapability(capability, "Host capability request");
|
|
89
|
+
const provision = this.activeCapabilitiesByKey.get(capabilityKey(parsed));
|
|
90
|
+
if (provision === undefined) {
|
|
91
|
+
throw new Error(`Server capability ${formatCapability(parsed)} is not active`);
|
|
92
|
+
}
|
|
93
|
+
return parseCapabilityValue(parsed, provision.value, `Server capability ${formatCapability(parsed)}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Registers every declared late host factory atomically, then resumes the
|
|
97
|
+
* plugins staged on those capabilities. This boundary is intentionally one-shot.
|
|
98
|
+
*/
|
|
99
|
+
async resumeWithHostCapabilityFactories(factories) {
|
|
100
|
+
if (this.stopped || this.lifetimesAborted) {
|
|
101
|
+
throw new Error("Server plugin runtime cannot resume after shutdown begins");
|
|
102
|
+
}
|
|
103
|
+
if (this.lateHostCapabilitiesResumed) {
|
|
104
|
+
throw new Error("Server plugin late host capabilities have already resumed");
|
|
105
|
+
}
|
|
106
|
+
const snapshottedFactories = snapshotHostCapabilityFactories(factories);
|
|
107
|
+
const lateDeclarations = [...this.declaredCapabilitiesByKey.values()]
|
|
108
|
+
.filter((declaration) => declaration.source === "late-host");
|
|
109
|
+
const factoriesByKey = new Map(snapshottedFactories.map((factory) => [factory.key, factory]));
|
|
110
|
+
for (const factory of snapshottedFactories) {
|
|
111
|
+
if (!lateDeclarations.some(({ key }) => key === factory.key)) {
|
|
112
|
+
throw new Error(`Host capability ${formatCapability(factory.capability)} was not declared for late registration`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const declaration of lateDeclarations) {
|
|
116
|
+
if (!factoriesByKey.has(declaration.key)) {
|
|
117
|
+
throw new Error(`Late host capability ${formatCapability(declaration.capability)} was not registered`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.lateHostCapabilitiesResumed = true;
|
|
121
|
+
for (const factory of snapshottedFactories)
|
|
122
|
+
this.declaredCapabilitiesByKey.set(factory.key, factory);
|
|
123
|
+
await this.startStagedPlugins(false);
|
|
124
|
+
}
|
|
125
|
+
/** Cancels plugin lifetimes while retaining publications for consumer cleanup. */
|
|
126
|
+
beginShutdown() {
|
|
127
|
+
if (this.lifetimesAborted)
|
|
128
|
+
return;
|
|
129
|
+
this.lifetimesAborted = true;
|
|
130
|
+
const reason = new DOMException("Server plugin host is shutting down", "AbortError");
|
|
131
|
+
for (const plugin of [...this.activePlugins, ...this.stagedPlugins]) {
|
|
132
|
+
if (!plugin.lifetimeController.signal.aborted)
|
|
133
|
+
plugin.lifetimeController.abort(reason);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async inspectHealth(pluginIds) {
|
|
137
|
+
const selectedPluginIds = pluginIds === undefined ? undefined : new Set(pluginIds);
|
|
54
138
|
const inspections = [];
|
|
55
139
|
for (const active of this.activePlugins) {
|
|
140
|
+
if (selectedPluginIds !== undefined && !selectedPluginIds.has(active.entry.id))
|
|
141
|
+
continue;
|
|
56
142
|
const callback = active.activation.health?.bind(active.activation);
|
|
57
143
|
if (callback === undefined) {
|
|
58
144
|
inspections.push(Object.freeze({ pluginId: active.entry.id, health: Object.freeze({ status: "healthy" }) }));
|
|
@@ -75,39 +161,43 @@ export class ServerPluginRuntime {
|
|
|
75
161
|
}
|
|
76
162
|
return Object.freeze(inspections);
|
|
77
163
|
}
|
|
78
|
-
/**
|
|
164
|
+
/** Disposes published plugins in reverse dependency/start order. */
|
|
79
165
|
async stop() {
|
|
80
166
|
if (this.stopped)
|
|
81
167
|
return;
|
|
168
|
+
this.beginShutdown();
|
|
82
169
|
this.stopped = true;
|
|
170
|
+
// During failed construction, activated entries may not have reached start.
|
|
171
|
+
// Dispose those first while any started providers remain published.
|
|
172
|
+
const stagedPlugins = [...this.stagedPlugins].reverse();
|
|
173
|
+
this.stagedPlugins = [];
|
|
174
|
+
for (const staged of stagedPlugins) {
|
|
175
|
+
await this.disposeForShutdown(staged);
|
|
176
|
+
}
|
|
83
177
|
const activePlugins = [...this.activePlugins].reverse();
|
|
84
178
|
this.activePlugins = [];
|
|
179
|
+
this.activeCapabilitiesByKey.clear();
|
|
85
180
|
for (const active of activePlugins) {
|
|
86
|
-
|
|
87
|
-
if (stop === undefined)
|
|
88
|
-
continue;
|
|
89
|
-
try {
|
|
90
|
-
await runBounded(active.entry.id, "stop", this.lifecycleTimeoutMs, (signal) => stop(signal));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.recordsById.set(active.entry.id, recordFor(active.entry, {
|
|
94
|
-
state: "failed",
|
|
95
|
-
name: active.plugin.name,
|
|
96
|
-
phase: "stop",
|
|
97
|
-
message: errorMessage(error),
|
|
98
|
-
}));
|
|
99
|
-
this.logger.error({ err: error, pluginId: active.entry.id, phase: "stop" }, "server plugin stop failed");
|
|
100
|
-
}
|
|
181
|
+
await this.disposeForShutdown(active);
|
|
101
182
|
}
|
|
102
183
|
}
|
|
103
184
|
async start(entries) {
|
|
104
185
|
const serverEntries = entries
|
|
105
186
|
.filter((entry) => entry.serverModule !== undefined)
|
|
106
|
-
.sort(
|
|
187
|
+
.sort(requiredTerminalFirst);
|
|
107
188
|
for (const entry of serverEntries)
|
|
108
|
-
await this.
|
|
189
|
+
await this.stageEntry(entry);
|
|
190
|
+
await this.startStagedPlugins(true);
|
|
191
|
+
if (this.enforceRequiredTerminal) {
|
|
192
|
+
try {
|
|
193
|
+
this.resolve(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw requiredTerminalError("Required Terminal service capability is not active", error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
109
199
|
}
|
|
110
|
-
async
|
|
200
|
+
async stageEntry(entry) {
|
|
111
201
|
const disabledMessage = disabledReason(entry, this.safeStart);
|
|
112
202
|
if (disabledMessage !== undefined) {
|
|
113
203
|
this.recordsById.set(entry.id, recordFor(entry, { state: "disabled", message: disabledMessage }));
|
|
@@ -116,7 +206,9 @@ export class ServerPluginRuntime {
|
|
|
116
206
|
}
|
|
117
207
|
let phase = "validate";
|
|
118
208
|
let plugin;
|
|
119
|
-
let
|
|
209
|
+
let rollbackDispose;
|
|
210
|
+
let noticeReporter;
|
|
211
|
+
const lifetimeController = new AbortController();
|
|
120
212
|
try {
|
|
121
213
|
const settings = cloneJsonObject(entry.settings, `settings for server plugin ${entry.id}`);
|
|
122
214
|
phase = "import";
|
|
@@ -126,51 +218,56 @@ export class ServerPluginRuntime {
|
|
|
126
218
|
const loadedPlugin = parsePluginExport(imported);
|
|
127
219
|
plugin = loadedPlugin;
|
|
128
220
|
phase = "activate";
|
|
221
|
+
if (!isPiWebPluginId(entry.id))
|
|
222
|
+
throw new Error(`Invalid PI WEB plugin id: ${entry.id}`);
|
|
223
|
+
const dataDirectory = resolve(this.dataDir, "plugin-data", entry.id);
|
|
224
|
+
await mkdir(dataDirectory, { recursive: true });
|
|
129
225
|
const scopedLogger = createScopedLogger(entry.id, this.logger);
|
|
226
|
+
noticeReporter = createScopedNoticeReporter(entry.id, this.noticeSink);
|
|
130
227
|
const activationValue = await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => loadedPlugin.activate(Object.freeze({
|
|
131
|
-
apiVersion:
|
|
228
|
+
apiVersion: 3,
|
|
132
229
|
pluginId: entry.id,
|
|
133
230
|
packageRoot: entry.packageRoot,
|
|
231
|
+
dataDirectory,
|
|
134
232
|
logger: scopedLogger,
|
|
135
233
|
settings,
|
|
234
|
+
...(noticeReporter === undefined ? {} : { notices: noticeReporter.reporter }),
|
|
136
235
|
execFile: this.execFile,
|
|
137
236
|
signal,
|
|
237
|
+
lifetimeSignal: lifetimeController.signal,
|
|
138
238
|
})));
|
|
239
|
+
rollbackDispose = activationDisposeForRollback(activationValue);
|
|
139
240
|
phase = "validate";
|
|
140
|
-
const loadedActivation = parseActivation(activationValue);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const start = loadedActivation.start?.bind(loadedActivation);
|
|
144
|
-
if (start !== undefined) {
|
|
145
|
-
await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => start(signal));
|
|
241
|
+
const loadedActivation = parseActivation(activationValue, entry.id);
|
|
242
|
+
if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
243
|
+
requireTerminalActivation(loadedActivation);
|
|
146
244
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
scope: entry.scope,
|
|
155
|
-
moduleRevision: requireServerModule(entry).revision,
|
|
156
|
-
provider: loadedActivation.workspaceProvider,
|
|
157
|
-
});
|
|
158
|
-
this.activePlugins.push(Object.freeze({
|
|
245
|
+
const provisions = internalProvisions(loadedActivation, "plugin");
|
|
246
|
+
for (const provision of provisions) {
|
|
247
|
+
if (this.declaredCapabilitiesByKey.has(provision.key)) {
|
|
248
|
+
throw new IncompatibleServerPluginError(`Server capability ${formatCapability(provision.capability)} is provided more than once`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const staged = Object.freeze({
|
|
159
252
|
entry,
|
|
160
253
|
plugin: loadedPlugin,
|
|
161
254
|
activation: loadedActivation,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
255
|
+
lifetimeController,
|
|
256
|
+
...(noticeReporter === undefined ? {} : { noticeReporter }),
|
|
257
|
+
provisions,
|
|
258
|
+
hostCapabilityInstances: [],
|
|
259
|
+
});
|
|
260
|
+
this.stagedPlugins.push(staged);
|
|
261
|
+
for (const provision of provisions)
|
|
262
|
+
this.declaredCapabilitiesByKey.set(provision.key, provision);
|
|
166
263
|
}
|
|
167
264
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
265
|
+
abortPluginLifetime(lifetimeController, entry.id, "activation failed");
|
|
266
|
+
noticeReporter?.revoke();
|
|
267
|
+
const rollbackError = rollbackDispose === undefined
|
|
268
|
+
? undefined
|
|
269
|
+
: await this.runDispose(entry.id, rollbackDispose);
|
|
270
|
+
const message = withRollbackError(errorMessage(error), rollbackError);
|
|
174
271
|
const state = error instanceof IncompatibleServerPluginError ? "incompatible" : "failed";
|
|
175
272
|
this.recordsById.set(entry.id, recordFor(entry, {
|
|
176
273
|
state,
|
|
@@ -179,20 +276,290 @@ export class ServerPluginRuntime {
|
|
|
179
276
|
message,
|
|
180
277
|
}));
|
|
181
278
|
const details = { err: error, pluginId: entry.id, phase, ...(rollbackError === undefined ? {} : { rollbackError }) };
|
|
182
|
-
if (state === "incompatible")
|
|
279
|
+
if (state === "incompatible")
|
|
183
280
|
this.logger.warn(details, "server plugin is incompatible");
|
|
281
|
+
else
|
|
282
|
+
this.logger.error(details, "server plugin activation failed");
|
|
283
|
+
if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
284
|
+
throw requiredTerminalError(`Required Terminal server entry failed during ${phase}: ${message}`, error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async startStagedPlugins(allowLateHostWait) {
|
|
289
|
+
const pending = new Map(this.stagedPlugins.map((plugin) => [plugin.entry.id, plugin]));
|
|
290
|
+
while (pending.size > 0) {
|
|
291
|
+
let progressed = false;
|
|
292
|
+
const candidates = [...pending.values()].sort((left, right) => requiredTerminalFirst(left.entry, right.entry));
|
|
293
|
+
for (const staged of candidates) {
|
|
294
|
+
const dependencyFailure = this.dependencyFailure(staged, pending);
|
|
295
|
+
if (dependencyFailure !== undefined) {
|
|
296
|
+
pending.delete(staged.entry.id);
|
|
297
|
+
const message = await this.failBeforeStart(staged, dependencyFailure);
|
|
298
|
+
progressed = true;
|
|
299
|
+
if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
300
|
+
throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, dependencyFailure);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (this.waitingForDependencies(staged))
|
|
305
|
+
continue;
|
|
306
|
+
pending.delete(staged.entry.id);
|
|
307
|
+
const startError = await this.startStagedPlugin(staged);
|
|
308
|
+
progressed = true;
|
|
309
|
+
if (startError !== undefined && this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
310
|
+
const record = this.recordsById.get(staged.entry.id);
|
|
311
|
+
throw requiredTerminalError(`Required Terminal server entry failed during start: ${record?.message ?? errorMessage(startError)}`, startError);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (progressed)
|
|
315
|
+
continue;
|
|
316
|
+
const cycleIds = findCapabilityCycle(pending, this.declaredCapabilitiesByKey);
|
|
317
|
+
if (cycleIds.length > 0) {
|
|
318
|
+
for (const pluginId of cycleIds) {
|
|
319
|
+
const staged = pending.get(pluginId);
|
|
320
|
+
if (staged === undefined)
|
|
321
|
+
continue;
|
|
322
|
+
pending.delete(pluginId);
|
|
323
|
+
const cyclicRequirement = (staged.plugin.requires ?? []).find((requirement) => cycleIds.includes(requirement.pluginId));
|
|
324
|
+
const error = new Error(cyclicRequirement === undefined
|
|
325
|
+
? `Server plugin ${pluginId} has an unresolved capability dependency cycle`
|
|
326
|
+
: `Server plugin ${pluginId} requires ${formatCapability(cyclicRequirement)} in a capability dependency cycle`);
|
|
327
|
+
const message = await this.failBeforeStart(staged, error);
|
|
328
|
+
if (this.enforceRequiredTerminal && pluginId === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
329
|
+
throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (allowLateHostWait) {
|
|
335
|
+
const providerCycles = candidates.flatMap((staged) => {
|
|
336
|
+
if (staged.activation.workspaceProvider === undefined)
|
|
337
|
+
return [];
|
|
338
|
+
const lateCapability = findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey);
|
|
339
|
+
return lateCapability === undefined ? [] : [{ staged, lateCapability }];
|
|
340
|
+
});
|
|
341
|
+
if (providerCycles.length > 0) {
|
|
342
|
+
for (const { staged, lateCapability } of providerCycles) {
|
|
343
|
+
pending.delete(staged.entry.id);
|
|
344
|
+
const error = new Error(`Server plugin ${staged.entry.id} contributes a workspace provider that depends on late host capability ${formatCapability(lateCapability)} in a capability dependency cycle`);
|
|
345
|
+
const message = await this.failBeforeStart(staged, error);
|
|
346
|
+
if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
347
|
+
throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (candidates.every((staged) => (findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey) !== undefined)))
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const unresolved = candidates[0];
|
|
356
|
+
if (unresolved === undefined)
|
|
357
|
+
return;
|
|
358
|
+
pending.delete(unresolved.entry.id);
|
|
359
|
+
const error = new Error(`Server plugin ${unresolved.entry.id} has an unresolved capability dependency cycle`);
|
|
360
|
+
const message = await this.failBeforeStart(unresolved, error);
|
|
361
|
+
if (this.enforceRequiredTerminal && unresolved.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
362
|
+
throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
dependencyFailure(staged, pending) {
|
|
367
|
+
for (const requirement of staged.plugin.requires ?? []) {
|
|
368
|
+
const key = capabilityKey(requirement);
|
|
369
|
+
const provision = this.declaredCapabilitiesByKey.get(key);
|
|
370
|
+
if (provision === undefined) {
|
|
371
|
+
return new Error(`Server plugin ${staged.entry.id} requires unavailable capability ${formatCapability(requirement)}`);
|
|
372
|
+
}
|
|
373
|
+
if (this.activeCapabilitiesByKey.has(key)
|
|
374
|
+
|| provision.source === "host"
|
|
375
|
+
|| provision.source === "host-factory"
|
|
376
|
+
|| provision.source === "late-host")
|
|
377
|
+
continue;
|
|
378
|
+
if (!pending.has(requirement.pluginId)) {
|
|
379
|
+
return new Error(`Server plugin ${staged.entry.id} requires ${formatCapability(requirement)}, but provider plugin ${requirement.pluginId} did not start`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
waitingForDependencies(staged) {
|
|
385
|
+
return (staged.plugin.requires ?? []).some((requirement) => {
|
|
386
|
+
const key = capabilityKey(requirement);
|
|
387
|
+
return this.declaredCapabilitiesByKey.get(key)?.source !== "host-factory"
|
|
388
|
+
&& !this.activeCapabilitiesByKey.has(key);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async startStagedPlugin(staged) {
|
|
392
|
+
try {
|
|
393
|
+
const capabilities = this.capabilityResolver(staged);
|
|
394
|
+
const start = staged.activation.start?.bind(staged.activation);
|
|
395
|
+
if (start !== undefined) {
|
|
396
|
+
await runBounded(staged.entry.id, "start", this.lifecycleTimeoutMs, (signal) => start(Object.freeze({
|
|
397
|
+
capabilities,
|
|
398
|
+
signal,
|
|
399
|
+
})));
|
|
400
|
+
}
|
|
401
|
+
if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
|
|
402
|
+
await this.requireHealthyTerminal(staged);
|
|
403
|
+
}
|
|
404
|
+
this.publish(staged);
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
await this.failBeforeStart(staged, error);
|
|
409
|
+
return error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
capabilityResolver(staged) {
|
|
413
|
+
const resolved = new Map();
|
|
414
|
+
for (const requirement of staged.plugin.requires ?? []) {
|
|
415
|
+
const key = capabilityKey(requirement);
|
|
416
|
+
const activeProvision = this.activeCapabilitiesByKey.get(key);
|
|
417
|
+
const declaration = this.declaredCapabilitiesByKey.get(key);
|
|
418
|
+
let provisionValue;
|
|
419
|
+
if (activeProvision !== undefined) {
|
|
420
|
+
provisionValue = activeProvision.value;
|
|
421
|
+
}
|
|
422
|
+
else if (declaration?.source === "host-factory") {
|
|
423
|
+
provisionValue = this.createHostCapabilityValue(staged, declaration);
|
|
184
424
|
}
|
|
185
425
|
else {
|
|
186
|
-
|
|
426
|
+
throw new Error(`Server plugin ${staged.entry.id} requires inactive capability ${formatCapability(requirement)}`);
|
|
187
427
|
}
|
|
428
|
+
// Validate the declared requirement before start, but retain the provider value:
|
|
429
|
+
// resolve() may supply a different parser for this same capability key.
|
|
430
|
+
parseCapabilityValue(requirement, provisionValue, `Required capability ${formatCapability(requirement)} for server plugin ${staged.entry.id}`);
|
|
431
|
+
resolved.set(key, { provisionValue });
|
|
188
432
|
}
|
|
433
|
+
return createCapabilityResolver(staged.entry.id, resolved);
|
|
434
|
+
}
|
|
435
|
+
createHostCapabilityValue(staged, factory) {
|
|
436
|
+
const context = Object.freeze({
|
|
437
|
+
pluginId: staged.entry.id,
|
|
438
|
+
packageRoot: staged.entry.packageRoot,
|
|
439
|
+
lifetimeSignal: staged.lifetimeController.signal,
|
|
440
|
+
});
|
|
441
|
+
const instance = factory.create(context);
|
|
442
|
+
staged.hostCapabilityInstances.push(instance);
|
|
443
|
+
return parseCapabilityValue(factory.capability, instance.value, `Host capability ${formatCapability(factory.capability)} for server plugin ${staged.entry.id}`);
|
|
444
|
+
}
|
|
445
|
+
async requireHealthyTerminal(staged) {
|
|
446
|
+
const health = staged.activation.health?.bind(staged.activation);
|
|
447
|
+
if (health === undefined)
|
|
448
|
+
return;
|
|
449
|
+
const result = await runBounded(staged.entry.id, "health", this.lifecycleTimeoutMs, (signal) => health(signal));
|
|
450
|
+
const parsed = parseHealth(result);
|
|
451
|
+
if (parsed.status === "unhealthy") {
|
|
452
|
+
throw new Error(`Required Terminal server entry is unhealthy: ${parsed.message ?? "health check failed"}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
publish(staged) {
|
|
456
|
+
const providerContribution = staged.activation.workspaceProvider === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: Object.freeze({
|
|
459
|
+
pluginId: staged.entry.id,
|
|
460
|
+
pluginName: staged.plugin.name,
|
|
461
|
+
packageRoot: staged.entry.packageRoot,
|
|
462
|
+
source: staged.entry.source,
|
|
463
|
+
scope: staged.entry.scope,
|
|
464
|
+
moduleRevision: requireServerModule(staged.entry).revision,
|
|
465
|
+
provider: staged.activation.workspaceProvider,
|
|
466
|
+
});
|
|
467
|
+
const pairedBackendContribution = staged.activation.peer === undefined
|
|
468
|
+
? undefined
|
|
469
|
+
: Object.freeze({
|
|
470
|
+
pluginId: staged.entry.id,
|
|
471
|
+
pluginName: staged.plugin.name,
|
|
472
|
+
packageRoot: staged.entry.packageRoot,
|
|
473
|
+
source: staged.entry.source,
|
|
474
|
+
scope: staged.entry.scope,
|
|
475
|
+
moduleRevision: requireServerModule(staged.entry).revision,
|
|
476
|
+
backend: staged.activation.peer,
|
|
477
|
+
});
|
|
478
|
+
const active = Object.freeze({
|
|
479
|
+
...staged,
|
|
480
|
+
...(providerContribution === undefined ? {} : { providerContribution }),
|
|
481
|
+
...(pairedBackendContribution === undefined ? {} : { pairedBackendContribution }),
|
|
482
|
+
});
|
|
483
|
+
this.removeStaged(staged.entry.id);
|
|
484
|
+
this.activePlugins.push(active);
|
|
485
|
+
for (const provision of staged.provisions)
|
|
486
|
+
this.activeCapabilitiesByKey.set(provision.key, provision);
|
|
487
|
+
this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
|
|
488
|
+
state: "active",
|
|
489
|
+
name: staged.plugin.name,
|
|
490
|
+
...(pairedBackendContribution?.backend.request === undefined ? {} : { pairedRequestVersion: 1 }),
|
|
491
|
+
...(pairedBackendContribution?.backend.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
|
|
492
|
+
}));
|
|
493
|
+
this.logger.info({ pluginId: staged.entry.id, pluginName: staged.plugin.name }, "server plugin activated");
|
|
494
|
+
}
|
|
495
|
+
async failBeforeStart(staged, error) {
|
|
496
|
+
abortPluginLifetime(staged.lifetimeController, staged.entry.id, "startup failed");
|
|
497
|
+
staged.noticeReporter?.revoke();
|
|
498
|
+
this.removeStaged(staged.entry.id);
|
|
499
|
+
const dispose = staged.activation.dispose?.bind(staged.activation);
|
|
500
|
+
const rollbackError = dispose === undefined ? undefined : await this.runDispose(staged.entry.id, dispose);
|
|
501
|
+
const hostCleanupError = await this.disposeHostCapabilityInstances(staged);
|
|
502
|
+
const message = withHostCleanupError(withRollbackError(errorMessage(error), rollbackError), hostCleanupError);
|
|
503
|
+
this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
|
|
504
|
+
state: "failed",
|
|
505
|
+
name: staged.plugin.name,
|
|
506
|
+
phase: "start",
|
|
507
|
+
message,
|
|
508
|
+
}));
|
|
509
|
+
this.logger.error({
|
|
510
|
+
err: error,
|
|
511
|
+
pluginId: staged.entry.id,
|
|
512
|
+
phase: "start",
|
|
513
|
+
...(rollbackError === undefined ? {} : { rollbackError }),
|
|
514
|
+
...(hostCleanupError === undefined ? {} : { hostCleanupError }),
|
|
515
|
+
}, "server plugin start failed");
|
|
516
|
+
return message;
|
|
189
517
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
518
|
+
removeStaged(pluginId) {
|
|
519
|
+
this.stagedPlugins = this.stagedPlugins.filter(({ entry }) => entry.id !== pluginId);
|
|
520
|
+
}
|
|
521
|
+
async disposeForShutdown(plugin) {
|
|
522
|
+
plugin.noticeReporter?.revoke();
|
|
523
|
+
const dispose = plugin.activation.dispose?.bind(plugin.activation);
|
|
524
|
+
const pluginDisposeError = dispose === undefined ? undefined : await this.runDispose(plugin.entry.id, dispose);
|
|
525
|
+
const hostCleanupError = await this.disposeHostCapabilityInstances(plugin);
|
|
526
|
+
const error = combinedDisposalError(pluginDisposeError, hostCleanupError);
|
|
527
|
+
if (error === undefined)
|
|
528
|
+
return;
|
|
529
|
+
this.recordsById.set(plugin.entry.id, recordFor(plugin.entry, {
|
|
530
|
+
state: "failed",
|
|
531
|
+
name: plugin.plugin.name,
|
|
532
|
+
phase: "dispose",
|
|
533
|
+
message: errorMessage(error),
|
|
534
|
+
...(plugin.activation.peer?.request === undefined ? {} : { pairedRequestVersion: 1 }),
|
|
535
|
+
...(plugin.activation.peer?.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
|
|
536
|
+
}));
|
|
537
|
+
this.logger.error({
|
|
538
|
+
err: error,
|
|
539
|
+
pluginId: plugin.entry.id,
|
|
540
|
+
phase: "dispose",
|
|
541
|
+
...(hostCleanupError === undefined ? {} : { hostCleanupError }),
|
|
542
|
+
}, "server plugin disposal failed");
|
|
543
|
+
}
|
|
544
|
+
async disposeHostCapabilityInstances(plugin) {
|
|
545
|
+
const instances = plugin.hostCapabilityInstances.splice(0).reverse();
|
|
546
|
+
const errors = [];
|
|
547
|
+
for (const instance of instances) {
|
|
548
|
+
if (instance.dispose === undefined)
|
|
549
|
+
continue;
|
|
550
|
+
const error = await this.runDispose(plugin.entry.id, instance.dispose);
|
|
551
|
+
if (error !== undefined)
|
|
552
|
+
errors.push(error);
|
|
553
|
+
}
|
|
554
|
+
if (errors.length === 0)
|
|
193
555
|
return undefined;
|
|
556
|
+
if (errors.length === 1)
|
|
557
|
+
return errors[0];
|
|
558
|
+
return new AggregateError(errors, "Multiple host capability cleanup operations failed");
|
|
559
|
+
}
|
|
560
|
+
async runDispose(pluginId, dispose) {
|
|
194
561
|
try {
|
|
195
|
-
await runBounded(pluginId, "
|
|
562
|
+
await runBounded(pluginId, "dispose", this.lifecycleTimeoutMs, (signal) => dispose(signal));
|
|
196
563
|
return undefined;
|
|
197
564
|
}
|
|
198
565
|
catch (error) {
|
|
@@ -200,6 +567,46 @@ export class ServerPluginRuntime {
|
|
|
200
567
|
}
|
|
201
568
|
}
|
|
202
569
|
}
|
|
570
|
+
function requireTerminalCatalogEntry(snapshot) {
|
|
571
|
+
const entry = snapshot.plugins.find(({ id }) => id === REQUIRED_TERMINAL_PLUGIN_ID);
|
|
572
|
+
if (entry === undefined)
|
|
573
|
+
throw requiredTerminalError("Required bundled Terminal package is missing");
|
|
574
|
+
if (entry.scope !== "bundled" || entry.source !== "bundled") {
|
|
575
|
+
throw requiredTerminalError("Required Terminal package must come from the bundled plugin scope");
|
|
576
|
+
}
|
|
577
|
+
if (entry.browserModule === undefined || entry.serverModule === undefined || !entry.machineSpecific) {
|
|
578
|
+
throw requiredTerminalError("Required Terminal package must provide machine-specific browser and server entries");
|
|
579
|
+
}
|
|
580
|
+
if (!entry.enabled)
|
|
581
|
+
throw requiredTerminalError("Required Terminal package cannot be disabled in normal startup");
|
|
582
|
+
return entry;
|
|
583
|
+
}
|
|
584
|
+
function requireTerminalActivation(activation) {
|
|
585
|
+
if (activation.peer?.request === undefined || activation.peer.openChannel === undefined) {
|
|
586
|
+
throw new IncompatibleServerPluginError("Required Terminal server entry must expose peer request and channel capabilities");
|
|
587
|
+
}
|
|
588
|
+
const requiredKey = capabilityKey(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
|
|
589
|
+
const provision = activation.provides?.find(({ capability }) => capabilityKey(capability) === requiredKey);
|
|
590
|
+
if (provision === undefined) {
|
|
591
|
+
throw new IncompatibleServerPluginError(`Required Terminal server entry must provide ${formatCapability(REQUIRED_TERMINAL_SERVICE_CAPABILITY)}`);
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
REQUIRED_TERMINAL_SERVICE_CAPABILITY.parse(provision.value);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
throw new IncompatibleServerPluginError(errorMessage(error), { cause: error });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function requiredTerminalFirst(left, right) {
|
|
601
|
+
if (left.id === REQUIRED_TERMINAL_PLUGIN_ID)
|
|
602
|
+
return right.id === REQUIRED_TERMINAL_PLUGIN_ID ? 0 : -1;
|
|
603
|
+
if (right.id === REQUIRED_TERMINAL_PLUGIN_ID)
|
|
604
|
+
return 1;
|
|
605
|
+
return left.id.localeCompare(right.id);
|
|
606
|
+
}
|
|
607
|
+
function requiredTerminalError(message, cause) {
|
|
608
|
+
return new RequiredTerminalPluginError(`${message}. ${REQUIRED_TERMINAL_RECOVERY_GUIDANCE}`, cause === undefined ? {} : { cause });
|
|
609
|
+
}
|
|
203
610
|
function disabledReason(entry, safeStart) {
|
|
204
611
|
if (!entry.enabled)
|
|
205
612
|
return "disabled in PI WEB config";
|
|
@@ -218,6 +625,8 @@ function recordFor(entry, status) {
|
|
|
218
625
|
...(entry.browserModule === undefined ? {} : { browserRevision: entry.browserModule.revision }),
|
|
219
626
|
settingsRevision: entry.settingsRevision,
|
|
220
627
|
machineSpecific: entry.machineSpecific,
|
|
628
|
+
...(status.pairedRequestVersion === undefined ? {} : { pairedRequestVersion: status.pairedRequestVersion }),
|
|
629
|
+
...(status.pairedChannelVersion === undefined ? {} : { pairedChannelVersion: status.pairedChannelVersion }),
|
|
221
630
|
state: status.state,
|
|
222
631
|
...(status.name === undefined ? {} : { name: status.name }),
|
|
223
632
|
...(status.phase === undefined ? {} : { phase: status.phase }),
|
|
@@ -246,44 +655,62 @@ function parsePluginExport(imported) {
|
|
|
246
655
|
const plugin = imported["default"];
|
|
247
656
|
if (!isRecord(plugin))
|
|
248
657
|
throw new IncompatibleServerPluginError("Server plugin module must export a default plugin object");
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (candidate.apiVersion !== 1) {
|
|
255
|
-
throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(candidate.apiVersion)}`);
|
|
658
|
+
const apiVersion = plugin["apiVersion"];
|
|
659
|
+
const name = plugin["name"];
|
|
660
|
+
const activateValue = plugin["activate"];
|
|
661
|
+
if (apiVersion !== 3) {
|
|
662
|
+
throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(apiVersion)}`);
|
|
256
663
|
}
|
|
257
|
-
if (typeof
|
|
664
|
+
if (typeof name !== "string" || name === "") {
|
|
258
665
|
throw new IncompatibleServerPluginError("Server plugin name must be a non-empty string");
|
|
259
666
|
}
|
|
260
|
-
if (typeof
|
|
667
|
+
if (typeof activateValue !== "function") {
|
|
261
668
|
throw new IncompatibleServerPluginError("Server plugin activate must be a function");
|
|
262
669
|
}
|
|
263
|
-
const
|
|
264
|
-
return Object.freeze({
|
|
670
|
+
const requires = snapshotCapabilityRequirements(plugin["requires"], `Server plugin ${name} requirements`);
|
|
671
|
+
return Object.freeze({
|
|
672
|
+
name,
|
|
673
|
+
...(requires.length === 0 ? {} : { requires }),
|
|
674
|
+
activate(context) {
|
|
675
|
+
const result = Reflect.apply(activateValue, plugin, [context]);
|
|
676
|
+
return result;
|
|
677
|
+
},
|
|
678
|
+
});
|
|
265
679
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
680
|
+
function activationDisposeForRollback(value) {
|
|
681
|
+
if (!isRecord(value))
|
|
682
|
+
return undefined;
|
|
683
|
+
const dispose = value["dispose"];
|
|
684
|
+
if (typeof dispose !== "function")
|
|
685
|
+
return undefined;
|
|
686
|
+
return async (signal) => {
|
|
687
|
+
await Reflect.apply(dispose, value, [signal]);
|
|
688
|
+
};
|
|
272
689
|
}
|
|
273
|
-
function parseActivation(value) {
|
|
690
|
+
function parseActivation(value, pluginId) {
|
|
274
691
|
if (!isRecord(value))
|
|
275
692
|
throw new IncompatibleServerPluginError("Server plugin activation must be an object");
|
|
276
693
|
if (value["workspaceProviders"] !== undefined) {
|
|
277
694
|
throw new IncompatibleServerPluginError("Server plugins may contribute only one workspaceProvider");
|
|
278
695
|
}
|
|
696
|
+
if (value["requiredTerminalService"] !== undefined) {
|
|
697
|
+
throw new IncompatibleServerPluginError("requiredTerminalService was removed in server plugin API v3; use a typed capability provision");
|
|
698
|
+
}
|
|
699
|
+
if (value["stop"] !== undefined) {
|
|
700
|
+
throw new IncompatibleServerPluginError("Server plugin stop was removed in API v3; use dispose");
|
|
701
|
+
}
|
|
279
702
|
const workspaceProviderValue = value["workspaceProvider"];
|
|
703
|
+
const peerValue = value["peer"];
|
|
704
|
+
const provides = snapshotCapabilityProvisions(value["provides"], pluginId, `Server plugin ${pluginId} provisions`);
|
|
280
705
|
const candidate = {
|
|
281
706
|
workspaceProvider: workspaceProviderValue === undefined ? undefined : snapshotWorkspaceProvider(workspaceProviderValue),
|
|
707
|
+
peer: peerValue === undefined ? undefined : snapshotPluginPeer(peerValue),
|
|
708
|
+
provides,
|
|
282
709
|
start: value["start"],
|
|
283
|
-
|
|
710
|
+
dispose: value["dispose"],
|
|
284
711
|
health: value["health"],
|
|
285
712
|
};
|
|
286
|
-
for (const callback of ["start", "
|
|
713
|
+
for (const callback of ["start", "dispose", "health"]) {
|
|
287
714
|
const callbackValue = candidate[callback];
|
|
288
715
|
if (callbackValue !== undefined && typeof callbackValue !== "function") {
|
|
289
716
|
throw new IncompatibleServerPluginError(`Server plugin ${callback} must be a function`);
|
|
@@ -292,12 +719,14 @@ function parseActivation(value) {
|
|
|
292
719
|
if (!isServerPluginActivation(candidate))
|
|
293
720
|
throw new IncompatibleServerPluginError("Server plugin activation is invalid");
|
|
294
721
|
const start = candidate.start?.bind(value);
|
|
295
|
-
const
|
|
722
|
+
const dispose = candidate.dispose?.bind(value);
|
|
296
723
|
const health = candidate.health?.bind(value);
|
|
297
724
|
return Object.freeze({
|
|
298
725
|
...(candidate.workspaceProvider === undefined ? {} : { workspaceProvider: candidate.workspaceProvider }),
|
|
299
|
-
...(
|
|
300
|
-
...(
|
|
726
|
+
...(candidate.peer === undefined ? {} : { peer: candidate.peer }),
|
|
727
|
+
...(provides.length === 0 ? {} : { provides }),
|
|
728
|
+
...(start === undefined ? {} : { start: (context) => start(context) }),
|
|
729
|
+
...(dispose === undefined ? {} : { dispose: (signal) => dispose(signal) }),
|
|
301
730
|
...(health === undefined ? {} : { health: (signal) => health(signal) }),
|
|
302
731
|
});
|
|
303
732
|
}
|
|
@@ -305,14 +734,342 @@ function isServerPluginActivation(value) {
|
|
|
305
734
|
if (!isRecord(value))
|
|
306
735
|
return false;
|
|
307
736
|
const workspaceProvider = value["workspaceProvider"];
|
|
737
|
+
const peer = value["peer"];
|
|
738
|
+
const provides = value["provides"];
|
|
308
739
|
const start = value["start"];
|
|
309
|
-
const
|
|
740
|
+
const dispose = value["dispose"];
|
|
310
741
|
const health = value["health"];
|
|
311
742
|
return (workspaceProvider === undefined || isWorkspaceProvider(workspaceProvider))
|
|
743
|
+
&& (peer === undefined || isPluginPeer(peer))
|
|
744
|
+
&& (provides === undefined || Array.isArray(provides))
|
|
312
745
|
&& (start === undefined || typeof start === "function")
|
|
313
|
-
&& (
|
|
746
|
+
&& (dispose === undefined || typeof dispose === "function")
|
|
314
747
|
&& (health === undefined || typeof health === "function");
|
|
315
748
|
}
|
|
749
|
+
function snapshotCapabilityRequirements(value, label) {
|
|
750
|
+
if (value === undefined)
|
|
751
|
+
return Object.freeze([]);
|
|
752
|
+
if (!Array.isArray(value))
|
|
753
|
+
throw new IncompatibleServerPluginError(`${label} must be an array`);
|
|
754
|
+
const seen = new Set();
|
|
755
|
+
const requirements = [];
|
|
756
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
757
|
+
if (!Object.hasOwn(value, index))
|
|
758
|
+
throw new IncompatibleServerPluginError(`${label} must not be sparse`);
|
|
759
|
+
const capability = snapshotUnknownCapability(value[index], `${label}[${String(index)}]`);
|
|
760
|
+
const key = capabilityKey(capability);
|
|
761
|
+
if (seen.has(key))
|
|
762
|
+
throw new IncompatibleServerPluginError(`${label} declares ${formatCapability(capability)} more than once`);
|
|
763
|
+
seen.add(key);
|
|
764
|
+
requirements.push(capability);
|
|
765
|
+
}
|
|
766
|
+
return Object.freeze(requirements);
|
|
767
|
+
}
|
|
768
|
+
function snapshotCapabilityProvisions(value, ownerPluginId, label) {
|
|
769
|
+
if (value === undefined)
|
|
770
|
+
return Object.freeze([]);
|
|
771
|
+
if (!Array.isArray(value))
|
|
772
|
+
throw new IncompatibleServerPluginError(`${label} must be an array`);
|
|
773
|
+
const seen = new Set();
|
|
774
|
+
const provisions = [];
|
|
775
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
776
|
+
if (!Object.hasOwn(value, index))
|
|
777
|
+
throw new IncompatibleServerPluginError(`${label} must not be sparse`);
|
|
778
|
+
const candidate = value[index];
|
|
779
|
+
if (!isRecord(candidate))
|
|
780
|
+
throw new IncompatibleServerPluginError(`${label}[${String(index)}] must be an object`);
|
|
781
|
+
const capability = snapshotUnknownCapability(candidate["capability"], `${label}[${String(index)}].capability`);
|
|
782
|
+
if (ownerPluginId !== undefined && capability.pluginId !== ownerPluginId) {
|
|
783
|
+
throw new IncompatibleServerPluginError(`Server plugin ${ownerPluginId} cannot provide capability owned by ${capability.pluginId}`);
|
|
784
|
+
}
|
|
785
|
+
const key = capabilityKey(capability);
|
|
786
|
+
if (seen.has(key))
|
|
787
|
+
throw new IncompatibleServerPluginError(`${label} publishes ${formatCapability(capability)} more than once`);
|
|
788
|
+
seen.add(key);
|
|
789
|
+
const parsedValue = parseCapabilityValue(capability, candidate["value"], `Provided capability ${formatCapability(capability)}`);
|
|
790
|
+
provisions.push(Object.freeze({ capability, value: parsedValue }));
|
|
791
|
+
}
|
|
792
|
+
return Object.freeze(provisions);
|
|
793
|
+
}
|
|
794
|
+
function snapshotHostCapabilities(value) {
|
|
795
|
+
const provisions = snapshotCapabilityProvisions(value, undefined, "Host capability provisions");
|
|
796
|
+
return internalProvisions({ provides: provisions }, "host");
|
|
797
|
+
}
|
|
798
|
+
function snapshotLateHostCapabilities(value) {
|
|
799
|
+
if (value === undefined)
|
|
800
|
+
return Object.freeze([]);
|
|
801
|
+
if (!Array.isArray(value))
|
|
802
|
+
throw new Error("Late host capabilities must be an array");
|
|
803
|
+
const seen = new Set();
|
|
804
|
+
const capabilities = [];
|
|
805
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
806
|
+
if (!Object.hasOwn(value, index))
|
|
807
|
+
throw new Error("Late host capabilities must not be sparse");
|
|
808
|
+
const capability = snapshotUnknownCapability(value[index], `Late host capabilities[${String(index)}]`);
|
|
809
|
+
const key = capabilityKey(capability);
|
|
810
|
+
if (seen.has(key))
|
|
811
|
+
throw new Error(`Late host capability ${formatCapability(capability)} is configured more than once`);
|
|
812
|
+
seen.add(key);
|
|
813
|
+
capabilities.push(Object.freeze({ capability, key, source: "late-host" }));
|
|
814
|
+
}
|
|
815
|
+
return Object.freeze(capabilities);
|
|
816
|
+
}
|
|
817
|
+
function snapshotHostCapabilityFactories(value) {
|
|
818
|
+
if (value === undefined)
|
|
819
|
+
return Object.freeze([]);
|
|
820
|
+
if (!Array.isArray(value))
|
|
821
|
+
throw new Error("Host capability factories must be an array");
|
|
822
|
+
const seen = new Set();
|
|
823
|
+
const factories = [];
|
|
824
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
825
|
+
if (!Object.hasOwn(value, index))
|
|
826
|
+
throw new Error("Host capability factories must not be sparse");
|
|
827
|
+
const candidate = value[index];
|
|
828
|
+
if (!isRecord(candidate))
|
|
829
|
+
throw new Error(`Host capability factories[${String(index)}] must be an object`);
|
|
830
|
+
const capability = snapshotUnknownCapability(candidate["capability"], `Host capability factories[${String(index)}].capability`);
|
|
831
|
+
const key = capabilityKey(capability);
|
|
832
|
+
if (seen.has(key))
|
|
833
|
+
throw new Error(`Host capability factory for ${formatCapability(capability)} is configured more than once`);
|
|
834
|
+
const create = candidate["create"];
|
|
835
|
+
if (typeof create !== "function")
|
|
836
|
+
throw new Error(`Host capability factory for ${formatCapability(capability)} must expose create`);
|
|
837
|
+
seen.add(key);
|
|
838
|
+
factories.push(Object.freeze({
|
|
839
|
+
capability,
|
|
840
|
+
key,
|
|
841
|
+
source: "host-factory",
|
|
842
|
+
create(context) {
|
|
843
|
+
const result = Reflect.apply(create, candidate, [context]);
|
|
844
|
+
return snapshotHostCapabilityInstance(result, `Host capability ${formatCapability(capability)}`);
|
|
845
|
+
},
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
return Object.freeze(factories);
|
|
849
|
+
}
|
|
850
|
+
function snapshotHostCapabilityInstance(value, label) {
|
|
851
|
+
if (!isRecord(value) || !Object.hasOwn(value, "value")) {
|
|
852
|
+
throw new Error(`${label} factory must return an object containing value`);
|
|
853
|
+
}
|
|
854
|
+
const dispose = value["dispose"];
|
|
855
|
+
if (dispose !== undefined && typeof dispose !== "function") {
|
|
856
|
+
throw new Error(`${label} factory dispose must be a function`);
|
|
857
|
+
}
|
|
858
|
+
return Object.freeze({
|
|
859
|
+
value: value["value"],
|
|
860
|
+
...(dispose === undefined
|
|
861
|
+
? {}
|
|
862
|
+
: { dispose: async (signal) => { await Reflect.apply(dispose, value, [signal]); } }),
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function internalProvisions(activation, source) {
|
|
866
|
+
return Object.freeze((activation.provides ?? []).map((provision) => Object.freeze({
|
|
867
|
+
capability: provision.capability,
|
|
868
|
+
key: capabilityKey(provision.capability),
|
|
869
|
+
value: provision.value,
|
|
870
|
+
source,
|
|
871
|
+
})));
|
|
872
|
+
}
|
|
873
|
+
function snapshotUnknownCapability(value, label) {
|
|
874
|
+
if (!isRecord(value))
|
|
875
|
+
throw new IncompatibleServerPluginError(`${label} must be an object`);
|
|
876
|
+
const pluginId = value["pluginId"];
|
|
877
|
+
const id = value["id"];
|
|
878
|
+
const version = value["version"];
|
|
879
|
+
const parse = value["parse"];
|
|
880
|
+
const validated = validateCapabilityFields(pluginId, id, version, parse, label);
|
|
881
|
+
return Object.freeze({
|
|
882
|
+
pluginId: validated.pluginId,
|
|
883
|
+
id: validated.id,
|
|
884
|
+
version: validated.version,
|
|
885
|
+
parse(input) {
|
|
886
|
+
const result = Reflect.apply(validated.parse, value, [input]);
|
|
887
|
+
return result;
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function snapshotTypedCapability(value, label) {
|
|
892
|
+
const validated = validateCapabilityFields(value.pluginId, value.id, value.version, value.parse, label);
|
|
893
|
+
const parse = value.parse.bind(value);
|
|
894
|
+
return Object.freeze({
|
|
895
|
+
pluginId: validated.pluginId,
|
|
896
|
+
id: validated.id,
|
|
897
|
+
version: validated.version,
|
|
898
|
+
parse: (input) => parse(input),
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
function validateCapabilityFields(pluginId, id, version, parse, label) {
|
|
902
|
+
if (typeof pluginId !== "string" || pluginId.trim() === "") {
|
|
903
|
+
throw new IncompatibleServerPluginError(`${label} pluginId must be a non-empty string`);
|
|
904
|
+
}
|
|
905
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
906
|
+
throw new IncompatibleServerPluginError(`${label} id must be a non-empty string`);
|
|
907
|
+
}
|
|
908
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
|
|
909
|
+
throw new IncompatibleServerPluginError(`${label} version must be a positive integer`);
|
|
910
|
+
}
|
|
911
|
+
if (!isCapabilityParser(parse))
|
|
912
|
+
throw new IncompatibleServerPluginError(`${label} parse must be a function`);
|
|
913
|
+
return { pluginId, id, version, parse };
|
|
914
|
+
}
|
|
915
|
+
function isCapabilityParser(value) {
|
|
916
|
+
return typeof value === "function";
|
|
917
|
+
}
|
|
918
|
+
function parseCapabilityValue(capability, value, label) {
|
|
919
|
+
try {
|
|
920
|
+
return capability.parse(value);
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
throw new IncompatibleServerPluginError(`${label} is invalid: ${errorMessage(error)}`, { cause: error });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function capabilityKey(capability) {
|
|
927
|
+
return JSON.stringify([capability.pluginId, capability.id, capability.version]);
|
|
928
|
+
}
|
|
929
|
+
function formatCapability(capability) {
|
|
930
|
+
return `${capability.pluginId}/${capability.id} v${String(capability.version)}`;
|
|
931
|
+
}
|
|
932
|
+
function createCapabilityResolver(pluginId, resolved) {
|
|
933
|
+
return Object.freeze({
|
|
934
|
+
resolve(value) {
|
|
935
|
+
const capability = snapshotTypedCapability(value, `Capability request from server plugin ${pluginId}`);
|
|
936
|
+
const requirement = resolved.get(capabilityKey(capability));
|
|
937
|
+
if (requirement === undefined) {
|
|
938
|
+
throw new Error(`Server plugin ${pluginId} did not declare required capability ${formatCapability(capability)}`);
|
|
939
|
+
}
|
|
940
|
+
return parseCapabilityValue(capability, requirement.provisionValue, `Required capability ${formatCapability(capability)} for server plugin ${pluginId}`);
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function findLateHostDependency(staged, pending, declared, visiting = new Set()) {
|
|
945
|
+
if (visiting.has(staged.entry.id))
|
|
946
|
+
return undefined;
|
|
947
|
+
const nextVisiting = new Set(visiting);
|
|
948
|
+
nextVisiting.add(staged.entry.id);
|
|
949
|
+
for (const requirement of staged.plugin.requires ?? []) {
|
|
950
|
+
const declaration = declared.get(capabilityKey(requirement));
|
|
951
|
+
if (declaration?.source === "late-host")
|
|
952
|
+
return declaration.capability;
|
|
953
|
+
if (declaration?.source !== "plugin")
|
|
954
|
+
continue;
|
|
955
|
+
const provider = pending.get(requirement.pluginId);
|
|
956
|
+
if (provider === undefined)
|
|
957
|
+
continue;
|
|
958
|
+
const lateCapability = findLateHostDependency(provider, pending, declared, nextVisiting);
|
|
959
|
+
if (lateCapability !== undefined)
|
|
960
|
+
return lateCapability;
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
function findCapabilityCycle(pending, declared) {
|
|
965
|
+
const state = new Map();
|
|
966
|
+
const stack = [];
|
|
967
|
+
const visit = (pluginId) => {
|
|
968
|
+
state.set(pluginId, "visiting");
|
|
969
|
+
stack.push(pluginId);
|
|
970
|
+
const plugin = pending.get(pluginId);
|
|
971
|
+
const providers = (plugin?.plugin.requires ?? [])
|
|
972
|
+
.filter((requirement) => declared.get(capabilityKey(requirement))?.source === "plugin" && pending.has(requirement.pluginId))
|
|
973
|
+
.map((requirement) => requirement.pluginId)
|
|
974
|
+
.sort((left, right) => left.localeCompare(right));
|
|
975
|
+
for (const providerId of providers) {
|
|
976
|
+
if (state.get(providerId) === "visiting") {
|
|
977
|
+
const start = stack.indexOf(providerId);
|
|
978
|
+
return stack.slice(start);
|
|
979
|
+
}
|
|
980
|
+
if (state.get(providerId) !== "visited") {
|
|
981
|
+
const cycle = visit(providerId);
|
|
982
|
+
if (cycle !== undefined)
|
|
983
|
+
return cycle;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
stack.pop();
|
|
987
|
+
state.set(pluginId, "visited");
|
|
988
|
+
return undefined;
|
|
989
|
+
};
|
|
990
|
+
for (const pluginId of [...pending.keys()].sort((left, right) => left.localeCompare(right))) {
|
|
991
|
+
if (state.has(pluginId))
|
|
992
|
+
continue;
|
|
993
|
+
const cycle = visit(pluginId);
|
|
994
|
+
if (cycle !== undefined)
|
|
995
|
+
return cycle;
|
|
996
|
+
}
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
function abortPluginLifetime(controller, pluginId, outcome) {
|
|
1000
|
+
if (!controller.signal.aborted) {
|
|
1001
|
+
controller.abort(new DOMException(`Server plugin ${pluginId} ${outcome}`, "AbortError"));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function withRollbackError(message, rollbackError) {
|
|
1005
|
+
return rollbackError === undefined
|
|
1006
|
+
? message
|
|
1007
|
+
: `${message}; startup rollback failed: ${errorMessage(rollbackError)}`;
|
|
1008
|
+
}
|
|
1009
|
+
function withHostCleanupError(message, cleanupError) {
|
|
1010
|
+
return cleanupError === undefined
|
|
1011
|
+
? message
|
|
1012
|
+
: `${message}; host capability cleanup failed: ${errorMessage(cleanupError)}`;
|
|
1013
|
+
}
|
|
1014
|
+
function combinedDisposalError(pluginError, hostCleanupError) {
|
|
1015
|
+
if (pluginError === undefined)
|
|
1016
|
+
return hostCleanupError;
|
|
1017
|
+
if (hostCleanupError === undefined)
|
|
1018
|
+
return pluginError;
|
|
1019
|
+
return new AggregateError([pluginError, hostCleanupError], `${errorMessage(pluginError)}; host capability cleanup failed: ${errorMessage(hostCleanupError)}`);
|
|
1020
|
+
}
|
|
1021
|
+
function snapshotPluginPeer(value) {
|
|
1022
|
+
if (!isPluginPeer(value)) {
|
|
1023
|
+
throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
|
|
1024
|
+
}
|
|
1025
|
+
const request = value.request?.bind(value);
|
|
1026
|
+
const openChannel = value.openChannel?.bind(value);
|
|
1027
|
+
const snapshotRequest = request === undefined
|
|
1028
|
+
? undefined
|
|
1029
|
+
: (context) => request(context);
|
|
1030
|
+
const snapshotOpenChannel = openChannel === undefined
|
|
1031
|
+
? undefined
|
|
1032
|
+
: async (context) => (snapshotPluginPeerChannel(await openChannel(context)));
|
|
1033
|
+
if (snapshotRequest !== undefined && snapshotOpenChannel !== undefined) {
|
|
1034
|
+
return Object.freeze({ request: snapshotRequest, openChannel: snapshotOpenChannel });
|
|
1035
|
+
}
|
|
1036
|
+
if (snapshotRequest !== undefined)
|
|
1037
|
+
return Object.freeze({ request: snapshotRequest });
|
|
1038
|
+
if (snapshotOpenChannel !== undefined)
|
|
1039
|
+
return Object.freeze({ openChannel: snapshotOpenChannel });
|
|
1040
|
+
throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
|
|
1041
|
+
}
|
|
1042
|
+
function isPluginPeer(value) {
|
|
1043
|
+
if (!isRecord(value))
|
|
1044
|
+
return false;
|
|
1045
|
+
const request = value["request"];
|
|
1046
|
+
const openChannel = value["openChannel"];
|
|
1047
|
+
return (typeof request === "function" || typeof openChannel === "function")
|
|
1048
|
+
&& (request === undefined || typeof request === "function")
|
|
1049
|
+
&& (openChannel === undefined || typeof openChannel === "function");
|
|
1050
|
+
}
|
|
1051
|
+
function snapshotPluginPeerChannel(value) {
|
|
1052
|
+
if (!isPluginPeerChannel(value)) {
|
|
1053
|
+
throw new Error("Server plugin openChannel must return a channel with receive, optional completion, and optional close callbacks");
|
|
1054
|
+
}
|
|
1055
|
+
const receive = value.receive.bind(value);
|
|
1056
|
+
const closed = value.closed === undefined ? undefined : Promise.resolve(value.closed);
|
|
1057
|
+
const close = value.close?.bind(value);
|
|
1058
|
+
return Object.freeze({
|
|
1059
|
+
receive: (data, signal) => receive(data, signal),
|
|
1060
|
+
...(closed === undefined ? {} : { closed }),
|
|
1061
|
+
...(close === undefined ? {} : { close: (context) => close(context) }),
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
function isPluginPeerChannel(value) {
|
|
1065
|
+
return isRecord(value)
|
|
1066
|
+
&& typeof value["receive"] === "function"
|
|
1067
|
+
&& (value["closed"] === undefined || isPromiseLike(value["closed"]))
|
|
1068
|
+
&& (value["close"] === undefined || typeof value["close"] === "function");
|
|
1069
|
+
}
|
|
1070
|
+
function isPromiseLike(value) {
|
|
1071
|
+
return isRecord(value) && typeof value["then"] === "function";
|
|
1072
|
+
}
|
|
316
1073
|
function snapshotWorkspaceProvider(value) {
|
|
317
1074
|
if (!isRecord(value))
|
|
318
1075
|
throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
|
|
@@ -320,20 +1077,17 @@ function snapshotWorkspaceProvider(value) {
|
|
|
320
1077
|
fallback: value["fallback"],
|
|
321
1078
|
probe: value["probe"],
|
|
322
1079
|
list: value["list"],
|
|
323
|
-
request: value["request"],
|
|
324
1080
|
prepareRemove: value["prepareRemove"],
|
|
325
1081
|
};
|
|
326
1082
|
if (!isWorkspaceProvider(candidate))
|
|
327
1083
|
throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
|
|
328
1084
|
const probe = candidate.probe.bind(value);
|
|
329
1085
|
const list = candidate.list.bind(value);
|
|
330
|
-
const request = candidate.request?.bind(value);
|
|
331
1086
|
const prepareRemove = candidate.prepareRemove?.bind(value);
|
|
332
1087
|
return Object.freeze({
|
|
333
1088
|
...(candidate.fallback === undefined ? {} : { fallback: candidate.fallback }),
|
|
334
1089
|
probe: (project, signal) => probe(project, signal),
|
|
335
1090
|
list: (project, signal) => list(project, signal),
|
|
336
|
-
...(request === undefined ? {} : { request: (context) => request(context) }),
|
|
337
1091
|
...(prepareRemove === undefined ? {} : { prepareRemove: (context) => prepareRemove(context) }),
|
|
338
1092
|
});
|
|
339
1093
|
}
|
|
@@ -343,12 +1097,10 @@ function isWorkspaceProvider(value) {
|
|
|
343
1097
|
const fallback = value["fallback"];
|
|
344
1098
|
const probe = value["probe"];
|
|
345
1099
|
const list = value["list"];
|
|
346
|
-
const request = value["request"];
|
|
347
1100
|
const prepareRemove = value["prepareRemove"];
|
|
348
1101
|
return (fallback === undefined || typeof fallback === "boolean")
|
|
349
1102
|
&& typeof probe === "function"
|
|
350
1103
|
&& typeof list === "function"
|
|
351
|
-
&& (request === undefined || typeof request === "function")
|
|
352
1104
|
&& (prepareRemove === undefined || typeof prepareRemove === "function");
|
|
353
1105
|
}
|
|
354
1106
|
function parseHealth(value) {
|
|
@@ -370,6 +1122,67 @@ function parseHealth(value) {
|
|
|
370
1122
|
...(clonedDetails === undefined ? {} : { details: clonedDetails }),
|
|
371
1123
|
});
|
|
372
1124
|
}
|
|
1125
|
+
function createScopedNoticeReporter(pluginId, sink) {
|
|
1126
|
+
if (sink === undefined)
|
|
1127
|
+
return undefined;
|
|
1128
|
+
const source = `${SERVER_PLUGIN_NOTICE_SOURCE_PREFIX}${pluginId}`;
|
|
1129
|
+
let active = true;
|
|
1130
|
+
const reporter = Object.freeze({
|
|
1131
|
+
version: 1,
|
|
1132
|
+
record(input) {
|
|
1133
|
+
if (!active)
|
|
1134
|
+
throw new Error(`Server plugin notice reporter for ${pluginId} is no longer active`);
|
|
1135
|
+
sink(source, parseServerPluginNoticeInput(input));
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
return Object.freeze({
|
|
1139
|
+
reporter,
|
|
1140
|
+
revoke() { active = false; },
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
function parseServerPluginNoticeInput(value) {
|
|
1144
|
+
if (!isPlainRecord(value))
|
|
1145
|
+
throw new Error("Server plugin notice input must be an object");
|
|
1146
|
+
if ("source" in value)
|
|
1147
|
+
throw new Error("Server plugin notices cannot set their source");
|
|
1148
|
+
for (const key in value) {
|
|
1149
|
+
if (!Object.hasOwn(value, key))
|
|
1150
|
+
continue;
|
|
1151
|
+
if (key !== "severity" && key !== "message" && key !== "scope" && key !== "context") {
|
|
1152
|
+
throw new Error(`Unsupported server plugin notice field: ${key}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const severity = value["severity"];
|
|
1156
|
+
if (severity !== "info" && severity !== "warning" && severity !== "error") {
|
|
1157
|
+
throw new Error("Server plugin notice severity must be info, warning, or error");
|
|
1158
|
+
}
|
|
1159
|
+
const message = value["message"];
|
|
1160
|
+
if (typeof message !== "string") {
|
|
1161
|
+
throw new Error("Server plugin notice message must be a non-empty string");
|
|
1162
|
+
}
|
|
1163
|
+
if (serverNoticeStringExceedsUtf8ByteLimit(message, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)) {
|
|
1164
|
+
throw new Error(`Server plugin notice message exceeds the ${String(SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)} byte limit`);
|
|
1165
|
+
}
|
|
1166
|
+
// This scan and allocation are bounded because the UTF-8 limit passed first.
|
|
1167
|
+
if (message.trim() === "") {
|
|
1168
|
+
throw new Error("Server plugin notice message must be a non-empty string");
|
|
1169
|
+
}
|
|
1170
|
+
const scope = value["scope"] === undefined
|
|
1171
|
+
? undefined
|
|
1172
|
+
: parseServerNoticeScope(value["scope"], "Server plugin notice scope");
|
|
1173
|
+
const context = value["context"] === undefined
|
|
1174
|
+
? undefined
|
|
1175
|
+
: cloneJsonObject(value["context"], "server plugin notice context", {
|
|
1176
|
+
maxDepth: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH,
|
|
1177
|
+
maxBytes: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES,
|
|
1178
|
+
});
|
|
1179
|
+
return Object.freeze({
|
|
1180
|
+
severity,
|
|
1181
|
+
message,
|
|
1182
|
+
...(scope === undefined ? {} : { scope }),
|
|
1183
|
+
...(context === undefined ? {} : { context }),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
373
1186
|
function createScopedLogger(pluginId, logger) {
|
|
374
1187
|
return Object.freeze({
|
|
375
1188
|
debug(message, details) {
|
|
@@ -404,40 +1217,136 @@ async function runBounded(pluginId, phase, timeoutMs, operation) {
|
|
|
404
1217
|
controller.abort(new DOMException("Server plugin operation completed", "AbortError"));
|
|
405
1218
|
}
|
|
406
1219
|
}
|
|
407
|
-
function cloneJsonObject(value, label) {
|
|
408
|
-
if (!
|
|
1220
|
+
function cloneJsonObject(value, label, limits = {}) {
|
|
1221
|
+
if (!isPlainRecord(value))
|
|
409
1222
|
throw new IncompatibleServerPluginError(`${label} must be a JSON object`);
|
|
410
|
-
|
|
1223
|
+
const byteBudget = limits.maxBytes === undefined
|
|
1224
|
+
? undefined
|
|
1225
|
+
: { label, maxBytes: limits.maxBytes, remaining: limits.maxBytes };
|
|
1226
|
+
return cloneJsonRecord(value, new Set(), label, 0, limits.maxDepth, byteBudget);
|
|
411
1227
|
}
|
|
412
|
-
function cloneJsonRecord(value, ancestors, label) {
|
|
1228
|
+
function cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget) {
|
|
1229
|
+
requireJsonDepth(depth, maxDepth, label);
|
|
413
1230
|
if (ancestors.has(value))
|
|
414
1231
|
throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
|
|
1232
|
+
consumeJsonBytes(byteBudget, 2); // Opening and closing braces.
|
|
415
1233
|
ancestors.add(value);
|
|
416
1234
|
const output = {};
|
|
417
|
-
|
|
418
|
-
|
|
1235
|
+
let propertyCount = 0;
|
|
1236
|
+
for (const key in value) {
|
|
1237
|
+
if (!Object.hasOwn(value, key))
|
|
1238
|
+
continue;
|
|
1239
|
+
if (propertyCount > 0)
|
|
1240
|
+
consumeJsonBytes(byteBudget, 1); // Comma.
|
|
1241
|
+
consumeJsonStringBytes(byteBudget, key);
|
|
1242
|
+
consumeJsonBytes(byteBudget, 1); // Colon.
|
|
1243
|
+
defineJsonProperty(output, key, cloneJsonValue(value[key], ancestors, label, depth + 1, maxDepth, byteBudget));
|
|
1244
|
+
propertyCount += 1;
|
|
1245
|
+
}
|
|
419
1246
|
ancestors.delete(value);
|
|
420
1247
|
return Object.freeze(output);
|
|
421
1248
|
}
|
|
422
|
-
function cloneJsonValue(value, ancestors, label) {
|
|
423
|
-
|
|
1249
|
+
function cloneJsonValue(value, ancestors, label, depth, maxDepth, byteBudget) {
|
|
1250
|
+
requireJsonDepth(depth, maxDepth, label);
|
|
1251
|
+
if (value === null) {
|
|
1252
|
+
consumeJsonBytes(byteBudget, 4);
|
|
1253
|
+
return value;
|
|
1254
|
+
}
|
|
1255
|
+
if (typeof value === "string") {
|
|
1256
|
+
consumeJsonStringBytes(byteBudget, value);
|
|
1257
|
+
return value;
|
|
1258
|
+
}
|
|
1259
|
+
if (typeof value === "boolean") {
|
|
1260
|
+
consumeJsonBytes(byteBudget, value ? 4 : 5);
|
|
424
1261
|
return value;
|
|
1262
|
+
}
|
|
425
1263
|
if (typeof value === "number") {
|
|
426
1264
|
if (!Number.isFinite(value))
|
|
427
1265
|
throw new IncompatibleServerPluginError(`${label} must contain only finite JSON numbers`);
|
|
1266
|
+
consumeJsonBytes(byteBudget, String(value).length);
|
|
428
1267
|
return value;
|
|
429
1268
|
}
|
|
430
|
-
if (Array.isArray(value))
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1269
|
+
if (Array.isArray(value))
|
|
1270
|
+
return cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget);
|
|
1271
|
+
if (isPlainRecord(value))
|
|
1272
|
+
return cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget);
|
|
1273
|
+
throw new IncompatibleServerPluginError(`${label} must contain only JSON values`);
|
|
1274
|
+
}
|
|
1275
|
+
function cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget) {
|
|
1276
|
+
requireJsonDepth(depth, maxDepth, label);
|
|
1277
|
+
if (ancestors.has(value))
|
|
1278
|
+
throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
|
|
1279
|
+
consumeJsonBytes(byteBudget, 2); // Opening and closing brackets.
|
|
1280
|
+
ancestors.add(value);
|
|
1281
|
+
try {
|
|
1282
|
+
const output = [];
|
|
1283
|
+
const length = value.length;
|
|
1284
|
+
// Plugin-owned arrays may override iteration helpers; inspect each dense element directly.
|
|
1285
|
+
for (let index = 0; index < length; index += 1) {
|
|
1286
|
+
if (!Object.hasOwn(value, index)) {
|
|
1287
|
+
throw new IncompatibleServerPluginError(`${label} must not contain sparse arrays`);
|
|
1288
|
+
}
|
|
1289
|
+
if (index > 0)
|
|
1290
|
+
consumeJsonBytes(byteBudget, 1); // Comma.
|
|
1291
|
+
output[index] = cloneJsonValue(value[index], ancestors, label, depth + 1, maxDepth, byteBudget);
|
|
1292
|
+
}
|
|
436
1293
|
return Object.freeze(output);
|
|
437
1294
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1295
|
+
finally {
|
|
1296
|
+
ancestors.delete(value);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function consumeJsonStringBytes(byteBudget, value) {
|
|
1300
|
+
if (byteBudget === undefined)
|
|
1301
|
+
return;
|
|
1302
|
+
consumeJsonBytes(byteBudget, 2); // Opening and closing quotes.
|
|
1303
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1304
|
+
const codeUnit = value.charCodeAt(index);
|
|
1305
|
+
if (codeUnit === 0x22 || codeUnit === 0x5c
|
|
1306
|
+
|| codeUnit === 0x08 || codeUnit === 0x09 || codeUnit === 0x0a
|
|
1307
|
+
|| codeUnit === 0x0c || codeUnit === 0x0d) {
|
|
1308
|
+
consumeJsonBytes(byteBudget, 2);
|
|
1309
|
+
}
|
|
1310
|
+
else if (codeUnit <= 0x1f) {
|
|
1311
|
+
consumeJsonBytes(byteBudget, 6);
|
|
1312
|
+
}
|
|
1313
|
+
else if (codeUnit <= 0x7f) {
|
|
1314
|
+
consumeJsonBytes(byteBudget, 1);
|
|
1315
|
+
}
|
|
1316
|
+
else if (codeUnit <= 0x7ff) {
|
|
1317
|
+
consumeJsonBytes(byteBudget, 2);
|
|
1318
|
+
}
|
|
1319
|
+
else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff
|
|
1320
|
+
&& index + 1 < value.length
|
|
1321
|
+
&& value.charCodeAt(index + 1) >= 0xdc00
|
|
1322
|
+
&& value.charCodeAt(index + 1) <= 0xdfff) {
|
|
1323
|
+
consumeJsonBytes(byteBudget, 4);
|
|
1324
|
+
index += 1;
|
|
1325
|
+
}
|
|
1326
|
+
else if (codeUnit >= 0xd800 && codeUnit <= 0xdfff) {
|
|
1327
|
+
// Well-formed JSON.stringify escapes isolated UTF-16 surrogates as \uXXXX.
|
|
1328
|
+
consumeJsonBytes(byteBudget, 6);
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
consumeJsonBytes(byteBudget, 3);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function consumeJsonBytes(byteBudget, count) {
|
|
1336
|
+
if (byteBudget === undefined)
|
|
1337
|
+
return;
|
|
1338
|
+
byteBudget.remaining -= count;
|
|
1339
|
+
if (byteBudget.remaining < 0) {
|
|
1340
|
+
throw new Error(`${byteBudget.label} exceeds the ${String(byteBudget.maxBytes)} byte limit`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function requireJsonDepth(depth, maxDepth, label) {
|
|
1344
|
+
if (maxDepth !== undefined && depth > maxDepth) {
|
|
1345
|
+
throw new IncompatibleServerPluginError(`${label} exceeds the maximum JSON depth of ${String(maxDepth)}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function defineJsonProperty(record, key, value) {
|
|
1349
|
+
Object.defineProperty(record, key, { value, enumerable: true, configurable: true, writable: true });
|
|
441
1350
|
}
|
|
442
1351
|
function abortError(signal) {
|
|
443
1352
|
const reason = signal.reason;
|
|
@@ -449,6 +1358,12 @@ class IncompatibleServerPluginError extends Error {
|
|
|
449
1358
|
this.name = "IncompatibleServerPluginError";
|
|
450
1359
|
}
|
|
451
1360
|
}
|
|
1361
|
+
export class RequiredTerminalPluginError extends Error {
|
|
1362
|
+
constructor() {
|
|
1363
|
+
super(...arguments);
|
|
1364
|
+
this.name = "RequiredTerminalPluginError";
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
452
1367
|
class ServerPluginTimeoutError extends Error {
|
|
453
1368
|
constructor() {
|
|
454
1369
|
super(...arguments);
|
|
@@ -477,4 +1392,10 @@ function formatUnknown(value) {
|
|
|
477
1392
|
function isRecord(value) {
|
|
478
1393
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
479
1394
|
}
|
|
1395
|
+
function isPlainRecord(value) {
|
|
1396
|
+
if (!isRecord(value))
|
|
1397
|
+
return false;
|
|
1398
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1399
|
+
return prototype === Object.prototype || prototype === null;
|
|
1400
|
+
}
|
|
480
1401
|
//# sourceMappingURL=serverPluginRuntime.js.map
|