@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
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { isPiWebPluginId } from "../../shared/pluginIds.js";
|
|
3
|
+
import { boundedPluginBackendChannelCloseReason, cloneBoundedPluginBackendJson, PLUGIN_BACKEND_CHANNEL_CALLBACK_TIMEOUT_MS, PLUGIN_BACKEND_CHANNEL_DATA_JSON_MAX_BYTES, PLUGIN_BACKEND_CHANNEL_MAX_LIFETIME_MS, PLUGIN_BACKEND_CHANNEL_MAX_PER_PLUGIN, PLUGIN_BACKEND_CHANNEL_MAX_PER_PLUGIN_WORKSPACE, PLUGIN_BACKEND_CHANNEL_MAX_TOTAL, PLUGIN_BACKEND_CHANNEL_OPEN_TIMEOUT_MS, PLUGIN_BACKEND_CHANNEL_TEARDOWN_TIMEOUT_MS, PLUGIN_BACKEND_DISPATCH_TIMEOUT_MS, PLUGIN_BACKEND_REQUEST_TIMEOUT_MS, PLUGIN_BACKEND_RESPONSE_JSON_MAX_BYTES, requirePluginBackendOperation, requirePluginBackendRevision, } from "../../shared/pluginBackendProtocol.js";
|
|
4
|
+
import { PluginCallbackDrain } from "../pluginCallbackDrain.js";
|
|
5
|
+
export class PluginBackendRequestError extends Error {
|
|
6
|
+
constructor(code, statusCode, message, options = {}) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = "PluginBackendRequestError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class PluginBackendChannelError extends Error {
|
|
14
|
+
constructor(code, closeCode, message, options = {}) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.closeCode = closeCode;
|
|
18
|
+
this.name = "PluginBackendChannelError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Keep active paired backends whose bounded startup health is not unhealthy. */
|
|
22
|
+
export function eligiblePluginBackendContributions(contributions, inspections) {
|
|
23
|
+
const healthByPluginId = new Map(inspections.map(({ pluginId, health }) => [pluginId, health.status]));
|
|
24
|
+
return Object.freeze(contributions.filter(({ pluginId }) => {
|
|
25
|
+
const status = healthByPluginId.get(pluginId);
|
|
26
|
+
return status === "healthy" || status === "degraded";
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/** Dispatches only to a browser package's revision-matched paired server entry. */
|
|
30
|
+
export class PluginBackendRegistry {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.channels = new Set();
|
|
34
|
+
this.channelAdmissions = new Set();
|
|
35
|
+
this.openingChannelControllers = new Set();
|
|
36
|
+
this.openingChannelTasks = new Set();
|
|
37
|
+
this.openingChannelCleanup = new PluginCallbackDrain();
|
|
38
|
+
this.requestTasks = new Set();
|
|
39
|
+
this.requestCallbacks = new PluginCallbackDrain();
|
|
40
|
+
this.shutdown = new AbortController();
|
|
41
|
+
this.channelAdmissionCount = 0;
|
|
42
|
+
this.channelsByPlugin = new Map();
|
|
43
|
+
this.channelsByPluginWorkspace = new Map();
|
|
44
|
+
this.contributions = snapshotContributions(options.contributions);
|
|
45
|
+
this.callbackTimeoutMs = positiveInteger(options.callbackTimeoutMs, PLUGIN_BACKEND_REQUEST_TIMEOUT_MS, "callbackTimeoutMs");
|
|
46
|
+
this.dispatchTimeoutMs = positiveInteger(options.dispatchTimeoutMs, PLUGIN_BACKEND_DISPATCH_TIMEOUT_MS, "dispatchTimeoutMs");
|
|
47
|
+
this.channelCallbackTimeoutMs = positiveInteger(options.channelCallbackTimeoutMs, PLUGIN_BACKEND_CHANNEL_CALLBACK_TIMEOUT_MS, "channelCallbackTimeoutMs");
|
|
48
|
+
this.channelOpenTimeoutMs = positiveInteger(options.channelOpenTimeoutMs, PLUGIN_BACKEND_CHANNEL_OPEN_TIMEOUT_MS, "channelOpenTimeoutMs");
|
|
49
|
+
this.channelLifetimeMs = positiveInteger(options.channelLifetimeMs, PLUGIN_BACKEND_CHANNEL_MAX_LIFETIME_MS, "channelLifetimeMs");
|
|
50
|
+
this.channelMaxTotal = positiveInteger(options.channelMaxTotal, PLUGIN_BACKEND_CHANNEL_MAX_TOTAL, "channelMaxTotal");
|
|
51
|
+
this.channelMaxPerPlugin = positiveInteger(options.channelMaxPerPlugin, PLUGIN_BACKEND_CHANNEL_MAX_PER_PLUGIN, "channelMaxPerPlugin");
|
|
52
|
+
this.channelMaxPerPluginWorkspace = positiveInteger(options.channelMaxPerPluginWorkspace, PLUGIN_BACKEND_CHANNEL_MAX_PER_PLUGIN_WORKSPACE, "channelMaxPerPluginWorkspace");
|
|
53
|
+
}
|
|
54
|
+
async request(request, signal) {
|
|
55
|
+
if (this.channelsAreShuttingDown())
|
|
56
|
+
throw requestShutdownError(request.pluginId);
|
|
57
|
+
const operationSignal = signal === undefined
|
|
58
|
+
? this.shutdown.signal
|
|
59
|
+
: AbortSignal.any([signal, this.shutdown.signal]);
|
|
60
|
+
const task = runBoundedPluginBackendOperation(request.pluginId, "dispatch", this.dispatchTimeoutMs, (dispatchSignal) => this.dispatch(request, dispatchSignal), operationSignal);
|
|
61
|
+
this.requestTasks.add(task);
|
|
62
|
+
try {
|
|
63
|
+
return await task;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (signal?.aborted === true) {
|
|
67
|
+
throw backendError("request-cancelled", 499, `Server plugin ${request.pluginId} backend request was cancelled`, error);
|
|
68
|
+
}
|
|
69
|
+
if (this.channelsAreShuttingDown())
|
|
70
|
+
throw requestShutdownError(request.pluginId, error);
|
|
71
|
+
if (error instanceof PluginBackendTimeoutError) {
|
|
72
|
+
throw backendError("request-timeout", 504, boundedErrorMessage(error), error);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
this.requestTasks.delete(task);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
reserveChannel(request) {
|
|
81
|
+
if (this.channelsAreShuttingDown())
|
|
82
|
+
throw channelError("shutdown", 1012, "Plugin backend channels are shutting down");
|
|
83
|
+
if (!isPiWebPluginId(request.pluginId))
|
|
84
|
+
throw channelError("inactive-plugin", 1008, `Server plugin is not active: ${request.pluginId}`);
|
|
85
|
+
if (request.projectId === "" || request.workspaceId === "") {
|
|
86
|
+
throw channelError("workspace-not-found", 1008, `Workspace not found for server plugin ${request.pluginId} channel`);
|
|
87
|
+
}
|
|
88
|
+
const pluginCount = this.channelsByPlugin.get(request.pluginId) ?? 0;
|
|
89
|
+
const scopeKey = channelScopeKey(request.pluginId, request.projectId, request.workspaceId);
|
|
90
|
+
const scopeCount = this.channelsByPluginWorkspace.get(scopeKey) ?? 0;
|
|
91
|
+
if (this.channelAdmissionCount >= this.channelMaxTotal || pluginCount >= this.channelMaxPerPlugin || scopeCount >= this.channelMaxPerPluginWorkspace) {
|
|
92
|
+
throw channelError("admission-denied", 1013, `Server plugin ${request.pluginId} channel admission limit was reached`);
|
|
93
|
+
}
|
|
94
|
+
this.channelAdmissionCount += 1;
|
|
95
|
+
this.channelsByPlugin.set(request.pluginId, pluginCount + 1);
|
|
96
|
+
this.channelsByPluginWorkspace.set(scopeKey, scopeCount + 1);
|
|
97
|
+
const admission = new ManagedPluginBackendChannelAdmission(request, () => {
|
|
98
|
+
this.channelAdmissions.delete(admission);
|
|
99
|
+
this.channelAdmissionCount -= 1;
|
|
100
|
+
decrementCount(this.channelsByPlugin, request.pluginId);
|
|
101
|
+
decrementCount(this.channelsByPluginWorkspace, scopeKey);
|
|
102
|
+
});
|
|
103
|
+
this.channelAdmissions.add(admission);
|
|
104
|
+
return admission;
|
|
105
|
+
}
|
|
106
|
+
async openChannel(request, transport, signal, reservedAdmission) {
|
|
107
|
+
if (this.channelsAreShuttingDown())
|
|
108
|
+
throw channelError("shutdown", 1012, "Plugin backend channels are shutting down");
|
|
109
|
+
const pluginId = parsePluginId(request.pluginId);
|
|
110
|
+
const operation = parseOperation(request.operation);
|
|
111
|
+
const moduleRevision = parseRevision(request.moduleRevision, operation);
|
|
112
|
+
const input = parseInput(request.input, pluginId, operation, "channel open input");
|
|
113
|
+
const contribution = this.contributions.find((candidate) => candidate.pluginId === pluginId);
|
|
114
|
+
const openChannel = contribution?.backend.openChannel?.bind(contribution.backend);
|
|
115
|
+
if (contribution === undefined || openChannel === undefined) {
|
|
116
|
+
throw channelError("channel-unavailable", 1008, `Server plugin ${pluginId} does not expose channel operation ${operation}`);
|
|
117
|
+
}
|
|
118
|
+
if (contribution.moduleRevision !== moduleRevision) {
|
|
119
|
+
throw channelError("stale-plugin-revision", 1008, `Server plugin ${pluginId} backend revision is stale for channel operation ${operation}; reload after the session daemon restarts`);
|
|
120
|
+
}
|
|
121
|
+
const admission = reservedAdmission === undefined
|
|
122
|
+
? this.reserveChannel({ pluginId, projectId: request.project.id, workspaceId: request.workspaceId })
|
|
123
|
+
: requireManagedChannelAdmission(reservedAdmission, { pluginId, projectId: request.project.id, workspaceId: request.workspaceId });
|
|
124
|
+
const releaseAdmission = () => { admission.release(); };
|
|
125
|
+
const releaseAdmissionOnManagedClose = reservedAdmission === undefined;
|
|
126
|
+
const lifetimeController = new AbortController();
|
|
127
|
+
this.openingChannelControllers.add(lifetimeController);
|
|
128
|
+
let resolveOpeningTask = () => undefined;
|
|
129
|
+
const openingTask = new Promise((resolve) => { resolveOpeningTask = resolve; });
|
|
130
|
+
this.openingChannelTasks.add(openingTask);
|
|
131
|
+
const finishOpeningTask = () => {
|
|
132
|
+
this.openingChannelControllers.delete(lifetimeController);
|
|
133
|
+
this.openingChannelTasks.delete(openingTask);
|
|
134
|
+
resolveOpeningTask();
|
|
135
|
+
};
|
|
136
|
+
const abortFromCaller = () => {
|
|
137
|
+
if (!lifetimeController.signal.aborted)
|
|
138
|
+
lifetimeController.abort(abortError(signal ?? lifetimeController.signal));
|
|
139
|
+
};
|
|
140
|
+
if (signal?.aborted === true)
|
|
141
|
+
abortFromCaller();
|
|
142
|
+
else
|
|
143
|
+
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
144
|
+
let managed;
|
|
145
|
+
let openingChannel;
|
|
146
|
+
let sendFailure;
|
|
147
|
+
const send = (data) => {
|
|
148
|
+
if (lifetimeController.signal.aborted)
|
|
149
|
+
throw channelError("channel-closed", 1008, `Server plugin ${pluginId} channel ${operation} is closed`);
|
|
150
|
+
try {
|
|
151
|
+
const cloned = cloneBoundedPluginBackendJson(data, `Server plugin ${pluginId} channel ${operation} data`, PLUGIN_BACKEND_CHANNEL_DATA_JSON_MAX_BYTES);
|
|
152
|
+
transport.send(cloned);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
156
|
+
sendFailure = failure;
|
|
157
|
+
if (managed !== undefined) {
|
|
158
|
+
void managed.fail("send-failed", `Server plugin ${pluginId} channel ${operation} send failed: ${boundedErrorMessage(failure)}`, 1011).catch(() => undefined);
|
|
159
|
+
}
|
|
160
|
+
throw failure;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
const channel = await runBoundedPluginBackendOperation(pluginId, `channel ${operation} open`, this.channelOpenTimeoutMs, async (openSignal) => {
|
|
165
|
+
const { project, workspace } = await resolveDirectScope(this.options.workspaces, request, pluginId, operation, openSignal);
|
|
166
|
+
const context = Object.freeze({
|
|
167
|
+
project,
|
|
168
|
+
workspace,
|
|
169
|
+
operation,
|
|
170
|
+
input,
|
|
171
|
+
signal: lifetimeController.signal,
|
|
172
|
+
send,
|
|
173
|
+
});
|
|
174
|
+
openingChannel = Promise.resolve().then(() => openChannel(context));
|
|
175
|
+
return await openingChannel;
|
|
176
|
+
}, lifetimeController.signal);
|
|
177
|
+
if (sendFailure !== undefined)
|
|
178
|
+
throw sendFailure;
|
|
179
|
+
managed = new ManagedPluginBackendChannel({
|
|
180
|
+
pluginId,
|
|
181
|
+
workspaceId: request.workspaceId,
|
|
182
|
+
operation,
|
|
183
|
+
channel,
|
|
184
|
+
transport,
|
|
185
|
+
lifetimeController,
|
|
186
|
+
callbackTimeoutMs: this.channelCallbackTimeoutMs,
|
|
187
|
+
lifetimeMs: this.channelLifetimeMs,
|
|
188
|
+
onReleased: (released) => {
|
|
189
|
+
this.channels.delete(released);
|
|
190
|
+
if (releaseAdmissionOnManagedClose)
|
|
191
|
+
releaseAdmission();
|
|
192
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
193
|
+
},
|
|
194
|
+
onCleanupFailure: (error) => {
|
|
195
|
+
this.options.logger?.error({ err: error, pluginId, operation }, "plugin backend channel cleanup failed");
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
finishOpeningTask();
|
|
199
|
+
this.channels.add(managed);
|
|
200
|
+
return managed;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
204
|
+
if (!lifetimeController.signal.aborted)
|
|
205
|
+
lifetimeController.abort(error);
|
|
206
|
+
// A route-provided reservation remains tied to its physical socket; the
|
|
207
|
+
// route releases it only after attributed teardown completes.
|
|
208
|
+
if (reservedAdmission === undefined)
|
|
209
|
+
releaseAdmission();
|
|
210
|
+
if (openingChannel !== undefined) {
|
|
211
|
+
const cleanup = openingChannel.then(async (channel) => {
|
|
212
|
+
await closeUnpublishedChannel(channel, pluginId, operation, this.channelCallbackTimeoutMs, (callback) => { this.openingChannelCleanup.track(callback); });
|
|
213
|
+
}).catch((cleanupError) => {
|
|
214
|
+
this.options.logger?.error({ err: cleanupError, pluginId, operation }, "unpublished plugin backend channel cleanup failed");
|
|
215
|
+
});
|
|
216
|
+
this.openingChannelCleanup.track(cleanup);
|
|
217
|
+
}
|
|
218
|
+
// Let shutdown snapshot the cleanup task only after this failed opening
|
|
219
|
+
// has either registered its late-result cleanup or proved none can run.
|
|
220
|
+
finishOpeningTask();
|
|
221
|
+
if (error instanceof PluginBackendChannelError)
|
|
222
|
+
throw error;
|
|
223
|
+
if (error instanceof PluginBackendTimeoutError) {
|
|
224
|
+
throw channelError("open-timeout", 1011, boundedErrorMessage(error), error);
|
|
225
|
+
}
|
|
226
|
+
if (this.channelsAreShuttingDown()) {
|
|
227
|
+
throw channelError("shutdown", 1012, `Server plugin ${pluginId} channel ${operation} was interrupted by shutdown`, error);
|
|
228
|
+
}
|
|
229
|
+
if (signal?.aborted === true) {
|
|
230
|
+
throw channelError("channel-closed", 1008, `Server plugin ${pluginId} channel ${operation} was cancelled`, error);
|
|
231
|
+
}
|
|
232
|
+
throw channelError("open-failed", 1011, `Server plugin ${pluginId} channel ${operation} failed to open: ${boundedErrorMessage(error)}`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
closeAll(reason = "Session daemon shutdown") {
|
|
236
|
+
this.closePromise ??= this.performCloseAll(reason);
|
|
237
|
+
return this.closePromise;
|
|
238
|
+
}
|
|
239
|
+
async performCloseAll(reason) {
|
|
240
|
+
if (!this.shutdown.signal.aborted)
|
|
241
|
+
this.shutdown.abort(new DOMException(reason, "AbortError"));
|
|
242
|
+
const admissions = [...this.channelAdmissions];
|
|
243
|
+
for (const admission of admissions)
|
|
244
|
+
admission.abort(reason);
|
|
245
|
+
for (const controller of this.openingChannelControllers) {
|
|
246
|
+
if (!controller.signal.aborted)
|
|
247
|
+
controller.abort(new DOMException(reason, "AbortError"));
|
|
248
|
+
}
|
|
249
|
+
await Promise.allSettled([...this.requestTasks, ...this.openingChannelTasks]);
|
|
250
|
+
const channels = [...this.channels];
|
|
251
|
+
const results = await Promise.allSettled([
|
|
252
|
+
this.requestCallbacks.waitForSettled(this.callbackTimeoutMs, "Plugin backend request callbacks"),
|
|
253
|
+
// Allow a late open, its bounded close invocation, and one final
|
|
254
|
+
// cooperative settlement window after the close signal aborts.
|
|
255
|
+
this.openingChannelCleanup.waitForSettled(this.channelOpenTimeoutMs + (2 * this.channelCallbackTimeoutMs), "Plugin backend abandoned channel cleanup callbacks"),
|
|
256
|
+
...channels.map(async (channel) => {
|
|
257
|
+
await channel.fail("shutdown", reason, 1012);
|
|
258
|
+
}),
|
|
259
|
+
]);
|
|
260
|
+
const failures = [];
|
|
261
|
+
for (const result of results) {
|
|
262
|
+
if (result.status === "rejected") {
|
|
263
|
+
const reason = result.reason;
|
|
264
|
+
failures.push(reason);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
await Promise.all(admissions.map(async (admission) => {
|
|
268
|
+
try {
|
|
269
|
+
await waitForPromise(admission.released, PLUGIN_BACKEND_CHANNEL_CALLBACK_TIMEOUT_MS + (2 * PLUGIN_BACKEND_CHANNEL_TEARDOWN_TIMEOUT_MS), "Plugin backend channel admission teardown");
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Do not counterfeit teardown by dropping admission while a physical
|
|
273
|
+
// owner has not confirmed close; shutdown reports the bounded failure.
|
|
274
|
+
failures.push(error);
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
if (failures.length !== 0)
|
|
278
|
+
throw new AggregateError(failures, "One or more plugin backend channels failed to close");
|
|
279
|
+
}
|
|
280
|
+
activeChannelCount() {
|
|
281
|
+
return this.channelAdmissionCount;
|
|
282
|
+
}
|
|
283
|
+
channelsAreShuttingDown() {
|
|
284
|
+
return this.shutdown.signal.aborted;
|
|
285
|
+
}
|
|
286
|
+
async dispatch(request, dispatchSignal) {
|
|
287
|
+
const pluginId = parsePluginId(request.pluginId);
|
|
288
|
+
const operation = parseOperation(request.operation);
|
|
289
|
+
const moduleRevision = parseRevision(request.moduleRevision, operation);
|
|
290
|
+
const input = parseInput(request.input, pluginId, operation);
|
|
291
|
+
const contribution = this.contributions.find((candidate) => candidate.pluginId === pluginId);
|
|
292
|
+
if (contribution === undefined) {
|
|
293
|
+
throw backendError("inactive-plugin", 409, `Server plugin ${pluginId} does not expose a paired backend for operation ${operation}`);
|
|
294
|
+
}
|
|
295
|
+
if (contribution.moduleRevision !== moduleRevision) {
|
|
296
|
+
throw backendError("stale-plugin-revision", 409, `Server plugin ${pluginId} backend revision is stale for operation ${operation}; reload after the session daemon restarts`);
|
|
297
|
+
}
|
|
298
|
+
const pairedRequest = contribution.backend.request?.bind(contribution.backend);
|
|
299
|
+
if (pairedRequest === undefined) {
|
|
300
|
+
throw backendError("operation-unavailable", 501, `Server plugin ${pluginId} does not expose paired request operation ${operation}`);
|
|
301
|
+
}
|
|
302
|
+
if (request.workspaceId === "") {
|
|
303
|
+
throw backendError("workspace-not-found", 404, `Workspace not found for server plugin ${pluginId} operation ${operation}`);
|
|
304
|
+
}
|
|
305
|
+
const project = snapshotProject(request.project);
|
|
306
|
+
let target;
|
|
307
|
+
try {
|
|
308
|
+
const resolution = await this.options.workspaces.resolve(request.project, dispatchSignal);
|
|
309
|
+
target = resolution.workspaces.find((workspace) => workspace.id === request.workspaceId);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (dispatchSignal.aborted)
|
|
313
|
+
throw abortError(dispatchSignal);
|
|
314
|
+
throw backendError("resolution-failed", 502, `Server plugin ${pluginId} could not resolve workspace scope for operation ${operation}: ${boundedErrorMessage(error)}`, error);
|
|
315
|
+
}
|
|
316
|
+
if (target === undefined) {
|
|
317
|
+
throw backendError("workspace-not-found", 404, `Workspace ${request.workspaceId} is stale or unavailable for server plugin ${pluginId} operation ${operation}`);
|
|
318
|
+
}
|
|
319
|
+
let workspace;
|
|
320
|
+
try {
|
|
321
|
+
workspace = snapshotWorkspace(target, project.id);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
throw backendError("invalid-scope", 502, `Server plugin ${pluginId} received an invalid host workspace scope for operation ${operation}: ${boundedErrorMessage(error)}`, error);
|
|
325
|
+
}
|
|
326
|
+
const context = Object.freeze({
|
|
327
|
+
project,
|
|
328
|
+
workspace,
|
|
329
|
+
operation,
|
|
330
|
+
input,
|
|
331
|
+
signal: dispatchSignal,
|
|
332
|
+
});
|
|
333
|
+
let result;
|
|
334
|
+
try {
|
|
335
|
+
result = await runBoundedPluginBackendOperation(pluginId, operation, this.callbackTimeoutMs, (callbackSignal) => pairedRequest(Object.freeze({ ...context, signal: callbackSignal })), dispatchSignal, (callback) => { this.requestCallbacks.track(callback); });
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (dispatchSignal.aborted)
|
|
339
|
+
throw abortError(dispatchSignal);
|
|
340
|
+
if (error instanceof PluginBackendTimeoutError) {
|
|
341
|
+
throw backendError("request-timeout", 504, boundedErrorMessage(error), error);
|
|
342
|
+
}
|
|
343
|
+
throw backendError("request-failed", 502, `Server plugin ${pluginId} operation ${operation} failed: ${boundedErrorMessage(error)}`, error);
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return cloneBoundedPluginBackendJson(result, `Server plugin ${pluginId} operation ${operation} result`, PLUGIN_BACKEND_RESPONSE_JSON_MAX_BYTES);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
throw backendError("invalid-result", 502, boundedErrorMessage(error), error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
class ManagedPluginBackendChannelAdmission {
|
|
354
|
+
constructor(request, onRelease) {
|
|
355
|
+
this.request = request;
|
|
356
|
+
this.onRelease = onRelease;
|
|
357
|
+
this.controller = new AbortController();
|
|
358
|
+
this.isReleased = false;
|
|
359
|
+
this.resolveReleased = () => undefined;
|
|
360
|
+
this.released = new Promise((resolve) => { this.resolveReleased = resolve; });
|
|
361
|
+
}
|
|
362
|
+
get signal() {
|
|
363
|
+
return this.controller.signal;
|
|
364
|
+
}
|
|
365
|
+
abort(reason) {
|
|
366
|
+
if (!this.controller.signal.aborted)
|
|
367
|
+
this.controller.abort(new DOMException(reason, "AbortError"));
|
|
368
|
+
}
|
|
369
|
+
release() {
|
|
370
|
+
if (this.isReleased)
|
|
371
|
+
return;
|
|
372
|
+
this.isReleased = true;
|
|
373
|
+
this.onRelease();
|
|
374
|
+
this.resolveReleased();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function requireManagedChannelAdmission(admission, expected) {
|
|
378
|
+
if (!(admission instanceof ManagedPluginBackendChannelAdmission)
|
|
379
|
+
|| admission.request.pluginId !== expected.pluginId
|
|
380
|
+
|| admission.request.projectId !== expected.projectId
|
|
381
|
+
|| admission.request.workspaceId !== expected.workspaceId) {
|
|
382
|
+
throw channelError("admission-denied", 1013, `Server plugin ${expected.pluginId} channel admission reservation is invalid`);
|
|
383
|
+
}
|
|
384
|
+
if (admission.signal.aborted) {
|
|
385
|
+
throw channelError("shutdown", 1012, `Server plugin ${expected.pluginId} channel admission was cancelled`);
|
|
386
|
+
}
|
|
387
|
+
return admission;
|
|
388
|
+
}
|
|
389
|
+
class ManagedPluginBackendChannel {
|
|
390
|
+
constructor(options) {
|
|
391
|
+
this.options = options;
|
|
392
|
+
this.receiveCallbacks = new PluginCallbackDrain();
|
|
393
|
+
this.closeCallbacks = new PluginCallbackDrain();
|
|
394
|
+
this.failureSignalled = false;
|
|
395
|
+
this.pluginId = options.pluginId;
|
|
396
|
+
this.workspaceId = options.workspaceId;
|
|
397
|
+
this.lifetimeTimer = setTimeout(() => {
|
|
398
|
+
void this.fail("lifetime-expired", `Server plugin ${this.pluginId} channel ${options.operation} reached its maximum lifetime`, 1001).catch(() => undefined);
|
|
399
|
+
}, options.lifetimeMs);
|
|
400
|
+
this.lifetimeTimer.unref();
|
|
401
|
+
this.observePluginCompletion();
|
|
402
|
+
}
|
|
403
|
+
async receive(data) {
|
|
404
|
+
if (this.isClosed()) {
|
|
405
|
+
throw channelError("channel-closed", 1008, `Server plugin ${this.pluginId} channel ${this.options.operation} is closed`);
|
|
406
|
+
}
|
|
407
|
+
let cloned;
|
|
408
|
+
try {
|
|
409
|
+
cloned = cloneBoundedPluginBackendJson(data, `Server plugin ${this.pluginId} channel ${this.options.operation} data`, PLUGIN_BACKEND_CHANNEL_DATA_JSON_MAX_BYTES);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
throw channelError("invalid-input", 1008, boundedErrorMessage(error), error);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await runBoundedPluginBackendOperation(this.pluginId, `channel ${this.options.operation} receive`, this.options.callbackTimeoutMs, (signal) => this.options.channel.receive(cloned, signal), this.options.lifetimeController.signal, (callback) => { this.receiveCallbacks.track(callback); });
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (this.isClosed()) {
|
|
419
|
+
throw channelError("channel-closed", 1008, `Server plugin ${this.pluginId} channel ${this.options.operation} closed during receive`, error);
|
|
420
|
+
}
|
|
421
|
+
if (error instanceof PluginBackendTimeoutError) {
|
|
422
|
+
throw channelError("receive-timeout", 1011, boundedErrorMessage(error), error);
|
|
423
|
+
}
|
|
424
|
+
throw channelError("receive-failed", 1011, `Server plugin ${this.pluginId} channel ${this.options.operation} receive failed: ${boundedErrorMessage(error)}`, error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
isClosed() {
|
|
428
|
+
return this.closePromise !== undefined || this.options.lifetimeController.signal.aborted;
|
|
429
|
+
}
|
|
430
|
+
observePluginCompletion() {
|
|
431
|
+
const closed = this.options.channel.closed;
|
|
432
|
+
if (closed === undefined)
|
|
433
|
+
return;
|
|
434
|
+
void Promise.resolve(closed).then(async () => {
|
|
435
|
+
if (this.isClosed())
|
|
436
|
+
return;
|
|
437
|
+
const reason = "Plugin completed channel";
|
|
438
|
+
try {
|
|
439
|
+
await this.close(1000, reason);
|
|
440
|
+
await this.options.transport.close(1000, reason);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
await this.fail("channel-closed", `Server plugin ${this.pluginId} channel ${this.options.operation} completion cleanup failed: ${boundedErrorMessage(error)}`, 1011);
|
|
444
|
+
}
|
|
445
|
+
}, async (error) => {
|
|
446
|
+
if (this.isClosed())
|
|
447
|
+
return;
|
|
448
|
+
await this.fail("channel-closed", `Server plugin ${this.pluginId} channel ${this.options.operation} completion failed: ${boundedErrorMessage(error)}`, 1011);
|
|
449
|
+
}).catch((error) => {
|
|
450
|
+
this.options.onCleanupFailure(error);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
close(code = 1000, reason = "Channel closed") {
|
|
454
|
+
this.closePromise ??= this.performClose(code, boundedPluginBackendChannelCloseReason(reason));
|
|
455
|
+
return this.closePromise;
|
|
456
|
+
}
|
|
457
|
+
async fail(code, message, closeCode) {
|
|
458
|
+
if (!this.failureSignalled) {
|
|
459
|
+
this.failureSignalled = true;
|
|
460
|
+
try {
|
|
461
|
+
this.options.transport.sendError(code, message);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// The transport may already be gone; cleanup and admission release remain authoritative.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
await this.close(closeCode, message);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
try {
|
|
472
|
+
await this.options.transport.close(closeCode, boundedPluginBackendChannelCloseReason(message));
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Closing an already-disconnected transport is expected and does not change cleanup ownership.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async performClose(code, reason) {
|
|
480
|
+
clearTimeout(this.lifetimeTimer);
|
|
481
|
+
if (!this.options.lifetimeController.signal.aborted) {
|
|
482
|
+
this.options.lifetimeController.abort(new DOMException(reason || "Plugin backend channel closed", "AbortError"));
|
|
483
|
+
}
|
|
484
|
+
const failures = [];
|
|
485
|
+
try {
|
|
486
|
+
try {
|
|
487
|
+
await this.receiveCallbacks.waitForSettled(this.options.callbackTimeoutMs, `Server plugin ${this.pluginId} channel ${this.options.operation} receive callbacks`);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
failures.push(error);
|
|
491
|
+
}
|
|
492
|
+
const close = this.options.channel.close?.bind(this.options.channel);
|
|
493
|
+
if (close !== undefined) {
|
|
494
|
+
try {
|
|
495
|
+
await runBoundedPluginBackendOperation(this.pluginId, `channel ${this.options.operation} close`, this.options.callbackTimeoutMs, (signal) => close(Object.freeze({ code, reason, signal })), undefined, (callback) => { this.closeCallbacks.track(callback); });
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
failures.push(error);
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
await this.closeCallbacks.waitForSettled(this.options.callbackTimeoutMs, `Server plugin ${this.pluginId} channel ${this.options.operation} close callbacks`);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
failures.push(error);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
this.options.onReleased(this);
|
|
510
|
+
}
|
|
511
|
+
if (failures.length !== 0) {
|
|
512
|
+
const failure = failures.length === 1
|
|
513
|
+
? failures[0]
|
|
514
|
+
: new AggregateError(failures, `Server plugin ${this.pluginId} channel ${this.options.operation} cleanup failed`);
|
|
515
|
+
this.options.onCleanupFailure(failure);
|
|
516
|
+
throw channelError("close-failed", 1011, `Server plugin ${this.pluginId} channel ${this.options.operation} close failed: ${boundedErrorMessage(failure)}`, failure);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function closeUnpublishedChannel(channel, pluginId, operation, callbackTimeoutMs, onCallbackStarted) {
|
|
521
|
+
const close = channel.close?.bind(channel);
|
|
522
|
+
if (close === undefined)
|
|
523
|
+
return;
|
|
524
|
+
await runBoundedPluginBackendOperation(pluginId, `channel ${operation} abandoned-open close`, callbackTimeoutMs, (signal) => close(Object.freeze({
|
|
525
|
+
code: 1011,
|
|
526
|
+
reason: "Channel open did not complete",
|
|
527
|
+
signal,
|
|
528
|
+
})), undefined, onCallbackStarted);
|
|
529
|
+
}
|
|
530
|
+
async function resolveDirectScope(workspaces, request, pluginId, operation, signal) {
|
|
531
|
+
if (request.workspaceId === "") {
|
|
532
|
+
throw channelError("workspace-not-found", 1008, `Workspace not found for server plugin ${pluginId} channel ${operation}`);
|
|
533
|
+
}
|
|
534
|
+
let project;
|
|
535
|
+
try {
|
|
536
|
+
project = snapshotProject(request.project);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
throw channelError("invalid-scope", 1011, boundedErrorMessage(error), error);
|
|
540
|
+
}
|
|
541
|
+
let target;
|
|
542
|
+
try {
|
|
543
|
+
const resolution = await workspaces.resolve(request.project, signal);
|
|
544
|
+
target = resolution.workspaces.find((workspace) => workspace.id === request.workspaceId);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (signal.aborted)
|
|
548
|
+
throw abortError(signal);
|
|
549
|
+
throw channelError("resolution-failed", 1011, `Server plugin ${pluginId} could not resolve workspace scope for channel ${operation}: ${boundedErrorMessage(error)}`, error);
|
|
550
|
+
}
|
|
551
|
+
if (target === undefined) {
|
|
552
|
+
throw channelError("workspace-not-found", 1008, `Workspace ${request.workspaceId} is stale or unavailable for server plugin ${pluginId} channel ${operation}`);
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
return { project, workspace: snapshotWorkspace(target, project.id) };
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
throw channelError("invalid-scope", 1011, `Server plugin ${pluginId} received an invalid host workspace scope for channel ${operation}: ${boundedErrorMessage(error)}`, error);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function channelScopeKey(pluginId, projectId, workspaceId) {
|
|
562
|
+
return `${pluginId}\u0000${projectId}\u0000${workspaceId}`;
|
|
563
|
+
}
|
|
564
|
+
function decrementCount(map, key) {
|
|
565
|
+
const count = map.get(key);
|
|
566
|
+
if (count === undefined || count <= 1)
|
|
567
|
+
map.delete(key);
|
|
568
|
+
else
|
|
569
|
+
map.set(key, count - 1);
|
|
570
|
+
}
|
|
571
|
+
function channelError(code, closeCode, message, cause) {
|
|
572
|
+
return new PluginBackendChannelError(code, closeCode, message, cause === undefined ? {} : { cause });
|
|
573
|
+
}
|
|
574
|
+
function snapshotContributions(contributions) {
|
|
575
|
+
const sorted = [...contributions].sort((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
576
|
+
for (let index = 1; index < sorted.length; index += 1) {
|
|
577
|
+
if (sorted[index - 1]?.pluginId === sorted[index]?.pluginId) {
|
|
578
|
+
throw new Error(`Duplicate paired plugin backend contribution: ${String(sorted[index]?.pluginId)}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return Object.freeze(sorted);
|
|
582
|
+
}
|
|
583
|
+
function parsePluginId(value) {
|
|
584
|
+
if (!isPiWebPluginId(value)) {
|
|
585
|
+
throw backendError("inactive-plugin", 409, `Server plugin is not active: ${value}`);
|
|
586
|
+
}
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
function parseOperation(value) {
|
|
590
|
+
try {
|
|
591
|
+
return requirePluginBackendOperation(value);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
throw backendError("invalid-operation", 400, boundedErrorMessage(error), error);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function parseRevision(value, operation) {
|
|
598
|
+
try {
|
|
599
|
+
return requirePluginBackendRevision(value);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
throw backendError("stale-plugin-revision", 409, `Plugin backend revision is unavailable for operation ${operation}: ${boundedErrorMessage(error)}`, error);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function parseInput(value, pluginId, operation, suffix = "input") {
|
|
606
|
+
try {
|
|
607
|
+
return cloneBoundedPluginBackendJson(value, `Server plugin ${pluginId} operation ${operation} ${suffix}`);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
throw backendError("invalid-input", 400, boundedErrorMessage(error), error);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function snapshotProject(project) {
|
|
614
|
+
if (typeof project.id !== "string" || project.id === "") {
|
|
615
|
+
throw backendError("invalid-scope", 500, "Project id must be a non-empty string");
|
|
616
|
+
}
|
|
617
|
+
if (typeof project.name !== "string" || project.name === "") {
|
|
618
|
+
throw backendError("invalid-scope", 500, "Project name must be a non-empty string");
|
|
619
|
+
}
|
|
620
|
+
if (!isAbsolute(project.path)) {
|
|
621
|
+
throw backendError("invalid-scope", 500, "Project path must be absolute");
|
|
622
|
+
}
|
|
623
|
+
return Object.freeze({ id: project.id, name: project.name, path: resolve(project.path) });
|
|
624
|
+
}
|
|
625
|
+
function snapshotWorkspace(workspace, projectId) {
|
|
626
|
+
if (workspace.id === "")
|
|
627
|
+
throw new Error("Workspace id must be non-empty");
|
|
628
|
+
if (workspace.projectId !== projectId)
|
|
629
|
+
throw new Error("Workspace project scope does not match the resolved project");
|
|
630
|
+
if (!isAbsolute(workspace.path))
|
|
631
|
+
throw new Error("Workspace path must be absolute");
|
|
632
|
+
if (workspace.label === "")
|
|
633
|
+
throw new Error("Workspace label must be non-empty");
|
|
634
|
+
const provider = workspace.provider === undefined ? undefined : snapshotProvider(workspace.provider);
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
id: workspace.id,
|
|
637
|
+
projectId: workspace.projectId,
|
|
638
|
+
path: resolve(workspace.path),
|
|
639
|
+
label: workspace.label,
|
|
640
|
+
isMain: workspace.isMain,
|
|
641
|
+
...(provider === undefined ? {} : { provider }),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function snapshotProvider(provider) {
|
|
645
|
+
if (!isPiWebPluginId(provider.pluginId))
|
|
646
|
+
throw new Error("Workspace provider plugin id is invalid");
|
|
647
|
+
const metadata = provider.metadata === undefined
|
|
648
|
+
? undefined
|
|
649
|
+
: cloneJsonObject(provider.metadata, "Workspace provider public metadata");
|
|
650
|
+
return Object.freeze({
|
|
651
|
+
pluginId: provider.pluginId,
|
|
652
|
+
capabilities: Object.freeze({ ...provider.capabilities }),
|
|
653
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function cloneJsonObject(value, label) {
|
|
657
|
+
const cloned = cloneBoundedPluginBackendJson(value, label);
|
|
658
|
+
if (!isJsonObject(cloned))
|
|
659
|
+
throw new Error(`${label} must be a JSON object`);
|
|
660
|
+
return cloned;
|
|
661
|
+
}
|
|
662
|
+
function isJsonObject(value) {
|
|
663
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
664
|
+
}
|
|
665
|
+
async function waitForPromise(operation, timeoutMs, label) {
|
|
666
|
+
let timer;
|
|
667
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
668
|
+
timer = setTimeout(() => { reject(new Error(`${label} timed out after ${String(timeoutMs)}ms`)); }, timeoutMs);
|
|
669
|
+
timer.unref();
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
return await Promise.race([operation, deadline]);
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
if (timer !== undefined)
|
|
676
|
+
clearTimeout(timer);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function runBoundedPluginBackendOperation(pluginId, operation, timeoutMs, callback, parentSignal, onCallbackStarted) {
|
|
680
|
+
const controller = new AbortController();
|
|
681
|
+
const abortFromParent = () => {
|
|
682
|
+
if (parentSignal !== undefined && !controller.signal.aborted) {
|
|
683
|
+
controller.abort(abortError(parentSignal));
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
if (parentSignal?.aborted === true)
|
|
687
|
+
abortFromParent();
|
|
688
|
+
else
|
|
689
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
690
|
+
const timeoutError = new PluginBackendTimeoutError(`Server plugin ${pluginId} operation ${operation} timed out after ${String(timeoutMs)}ms`);
|
|
691
|
+
const timeout = setTimeout(() => { controller.abort(timeoutError); }, timeoutMs);
|
|
692
|
+
timeout.unref();
|
|
693
|
+
const deadline = controller.signal.aborted
|
|
694
|
+
? Promise.reject(abortError(controller.signal))
|
|
695
|
+
: new Promise((_resolve, rejectPromise) => {
|
|
696
|
+
controller.signal.addEventListener("abort", () => { rejectPromise(abortError(controller.signal)); }, { once: true });
|
|
697
|
+
});
|
|
698
|
+
const result = controller.signal.aborted
|
|
699
|
+
? new Promise(() => { })
|
|
700
|
+
: Promise.resolve().then(() => callback(controller.signal));
|
|
701
|
+
if (!controller.signal.aborted)
|
|
702
|
+
onCallbackStarted?.(result);
|
|
703
|
+
try {
|
|
704
|
+
return await Promise.race([result, deadline]);
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
clearTimeout(timeout);
|
|
708
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
709
|
+
if (!controller.signal.aborted) {
|
|
710
|
+
controller.abort(new DOMException("Plugin backend operation completed", "AbortError"));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function requestShutdownError(pluginId, cause) {
|
|
715
|
+
return backendError("shutdown", 503, `Server plugin ${pluginId} backend requests are shutting down`, cause);
|
|
716
|
+
}
|
|
717
|
+
function backendError(code, statusCode, message, cause) {
|
|
718
|
+
return new PluginBackendRequestError(code, statusCode, message, cause === undefined ? {} : { cause });
|
|
719
|
+
}
|
|
720
|
+
function abortError(signal) {
|
|
721
|
+
const reason = signal.reason;
|
|
722
|
+
return reason instanceof Error ? reason : new Error("Plugin backend operation aborted", { cause: reason });
|
|
723
|
+
}
|
|
724
|
+
function positiveInteger(value, fallback, key) {
|
|
725
|
+
const resolved = value ?? fallback;
|
|
726
|
+
if (!Number.isInteger(resolved) || resolved <= 0)
|
|
727
|
+
throw new Error(`${key} must be a positive integer`);
|
|
728
|
+
return resolved;
|
|
729
|
+
}
|
|
730
|
+
function boundedErrorMessage(error) {
|
|
731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
732
|
+
return message.length <= 2_048 ? message : `${message.slice(0, 2_045)}...`;
|
|
733
|
+
}
|
|
734
|
+
class PluginBackendTimeoutError extends Error {
|
|
735
|
+
constructor() {
|
|
736
|
+
super(...arguments);
|
|
737
|
+
this.name = "TimeoutError";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=pluginBackendRegistry.js.map
|