overmux 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/bin.js +145 -58
  3. package/dist/bin.js.map +1 -1
  4. package/dist/docs/400-reference/200-configuration.md +19 -4
  5. package/dist/docs/400-reference/500-server/000-index.md +4 -0
  6. package/dist/docs/400-reference/500-server/100-resources.md +5 -0
  7. package/dist/docs/400-reference/500-server/200-operations.md +1 -0
  8. package/dist/docs/400-reference/500-server/300-streams.md +7 -1
  9. package/dist/docs/400-reference/500-server/500-api.md +26 -5
  10. package/dist/exports/client.d.ts +1 -1
  11. package/dist/exports/client.d.ts.map +1 -1
  12. package/dist/exports/client.js +6 -7
  13. package/dist/exports/client.js.map +1 -1
  14. package/dist/exports/{index-Dk11nKIv.d.ts → index-C-8Kzrnc.d.ts} +23 -8
  15. package/dist/exports/index-C-8Kzrnc.d.ts.map +1 -0
  16. package/dist/exports/index.d.ts +2 -2
  17. package/dist/exports/index.js +1 -1
  18. package/dist/exports/index.js.map +1 -1
  19. package/dist/exports/{notifications-BFAD3QQl.js → notifications-BBKvKZgI.js} +8 -2
  20. package/dist/exports/{notifications-BFAD3QQl.js.map → notifications-BBKvKZgI.js.map} +1 -1
  21. package/dist/exports/server.d.ts.map +1 -1
  22. package/dist/exports/server.js +8 -2
  23. package/dist/exports/server.js.map +1 -1
  24. package/dist/internal/server/coordinator/server-child.js +592 -883
  25. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  26. package/dist/internal/server/worker-entry.d.ts +1 -0
  27. package/dist/internal/server/worker-entry.js +728 -0
  28. package/dist/internal/server/worker-entry.js.map +1 -0
  29. package/dist/push/sw.js +4 -4
  30. package/dist/push/sw.js.map +1 -1
  31. package/dist/worker-protocol-hXs4FXCH.js +638 -0
  32. package/dist/worker-protocol-hXs4FXCH.js.map +1 -0
  33. package/docs/400-reference/200-configuration.md +19 -4
  34. package/docs/400-reference/500-server/000-index.md +4 -0
  35. package/docs/400-reference/500-server/100-resources.md +5 -0
  36. package/docs/400-reference/500-server/200-operations.md +1 -0
  37. package/docs/400-reference/500-server/300-streams.md +7 -1
  38. package/docs/400-reference/500-server/500-api.md +26 -5
  39. package/package.json +3 -2
  40. package/src/internal/client/host/overmux-host.tsx +4 -8
  41. package/src/internal/server/coordinator/ipc-protocol.ts +2 -1
  42. package/src/internal/server/http/operation-handler.ts +3 -0
  43. package/src/internal/server/http/request-logging.ts +1 -1
  44. package/src/internal/server/protocol-error.ts +19 -0
  45. package/src/internal/server/runtime/create-runtime.ts +11 -5
  46. package/src/internal/server/runtime/handler-logger.ts +54 -0
  47. package/src/internal/server/runtime/runtime-operations.ts +23 -5
  48. package/src/internal/server/runtime/runtime-resources.ts +158 -88
  49. package/src/internal/server/runtime/runtime-streams.ts +24 -8
  50. package/src/internal/server/runtime/worker-capabilities.ts +67 -0
  51. package/src/internal/server/runtime/worker-connection.ts +549 -0
  52. package/src/internal/server/runtime/worker-protocol.ts +173 -0
  53. package/src/internal/server/runtime/worker-runtime.ts +214 -0
  54. package/src/internal/server/server-logger.ts +25 -37
  55. package/src/internal/server/server-startup-options.ts +7 -6
  56. package/src/internal/server/start-application-server.ts +22 -18
  57. package/src/internal/server/start-overmux-server.ts +2 -2
  58. package/src/internal/server/websocket-connection.ts +97 -32
  59. package/src/internal/server/websocket-server.ts +5 -1
  60. package/src/internal/server/worker-entry.ts +504 -0
  61. package/src/internal/shared/protocol.ts +3 -3
  62. package/src/public/config.ts +15 -23
  63. package/src/public/handler-logger.ts +7 -0
  64. package/src/public/index.ts +3 -1
  65. package/dist/exports/index-Dk11nKIv.d.ts.map +0 -1
@@ -1,16 +1,16 @@
1
+ import { A as overmuxLogoutPath, C as serverProtocolMessageSchema, D as instanceIdSchema, E as backgroundNotificationSubscriptionSchema, F as serverConfigRuntimeSchema, M as authDurationSchema, N as getOvermuxPaths, O as logLevelSchema, S as notificationEventSchema, T as backgroundNotificationDisableRequestSchema, b as decodeProtocolFrame, c as defaultInstanceId, g as getServerLogFile, h as errorMessage, i as postWorkerMessage, j as overmuxSettingsPath, k as notificationSchema, l as ProtocolError, m as errorDetails$1, n as deserializeWorkerError, o as workerLimits, p as createServerLogger, r as encodeWorkerMessage, s as createRuntimeInstance, t as decodeWorkerMessage, u as OperationValidationError, w as webSocketCloseCode, x as encodeProtocolFrame, y as clientProtocolMessageSchema } from "../../../worker-protocol-hXs4FXCH.js";
1
2
  import { serve } from "@hono/node-server";
2
- import { chmod, link, mkdir, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
3
- import { createServer, isIP } from "node:net";
4
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
- import { createJiti } from "jiti";
6
- import { ZodError, z } from "zod";
7
- import { homedir, hostname } from "node:os";
8
3
  import { once } from "node:events";
4
+ import { basename, dirname, join, resolve } from "node:path";
9
5
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import { chmod, link, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { createServer } from "node:net";
9
+ import { ZodError, z } from "zod";
10
10
  import { generateCookie } from "hono/cookie";
11
11
  import { bodyLimit } from "hono/body-limit";
12
12
  import { parse } from "hono/utils/cookie";
13
- import { appendFileSync, constants, mkdirSync, readFileSync } from "node:fs";
13
+ import { constants, readFileSync } from "node:fs";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { Hono } from "hono";
16
16
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -18,146 +18,7 @@ import { proxyFetch, proxyUpgrade } from "httpxy";
18
18
  import { WebSocket, WebSocketServer } from "ws";
19
19
  import webPush from "web-push";
20
20
  import { isDeepStrictEqual } from "node:util";
21
- //#region ../shared/dist/node/index.js
22
- const durationPattern$1 = /^([1-9]\d*)(ms|s|m|h|d)$/;
23
- const millisecondsByUnit$1 = {
24
- d: 1440 * 60 * 1e3,
25
- h: 3600 * 1e3,
26
- m: 60 * 1e3,
27
- ms: 1,
28
- s: 1e3
29
- };
30
- const durationToMilliseconds$1 = (duration) => {
31
- const match = durationPattern$1.exec(duration);
32
- if (!match) return;
33
- const unit = match[2];
34
- const milliseconds = Number(match[1]) * millisecondsByUnit$1[unit];
35
- return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
36
- };
37
- const aiContextSnippets = ["package-source", "tech-stack-recommendations"];
38
- const instanceIdSchema$1 = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?![\s\S])/, "Expected a lowercase instance ID with only ASCII letters, digits, internal dots and hyphens");
39
- const zodTypeSchema = z.custom((value) => value instanceof z.ZodType, "Expected a Zod schema");
40
- const functionSchema = z.custom((value) => typeof value === "function", "Expected a function");
41
- const contractRuntimeSchema = z.object({
42
- input: zodTypeSchema,
43
- output: zodTypeSchema
44
- });
45
- const operationRuntimeSchema = z.object({
46
- handle: functionSchema,
47
- input: zodTypeSchema,
48
- output: zodTypeSchema
49
- });
50
- const streamHandlerRuntimeSchema = z.object({
51
- contract: z.object({
52
- clientMessage: zodTypeSchema,
53
- input: zodTypeSchema,
54
- serverMessage: zodTypeSchema
55
- }),
56
- open: functionSchema
57
- });
58
- const resourceDefinitionRuntimeSchema = z.discriminatedUnion("kind", [
59
- z.object({
60
- contract: contractRuntimeSchema,
61
- kind: z.literal("query"),
62
- read: functionSchema
63
- }),
64
- z.object({
65
- contract: contractRuntimeSchema,
66
- kind: z.literal("subscription"),
67
- read: functionSchema,
68
- subscribe: functionSchema
69
- }),
70
- z.object({
71
- combine: functionSchema,
72
- contract: contractRuntimeSchema,
73
- dependencies: z.record(z.string(), z.string()),
74
- kind: z.literal("derived")
75
- })
76
- ]);
77
- const authDurationSchema = z.string().regex(/^[1-9]\d*[mhd]$/, "Expected a positive duration such as 30d").refine((value) => durationToMilliseconds$1(value) !== void 0, "Duration is too large");
78
- const isLoopbackHostname$1 = (hostname) => hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
79
- const exactOriginSchema = z.url().refine((value) => new URL(value).origin === value, "Expected an origin").refine((value) => {
80
- const url = new URL(value);
81
- return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHostname$1(url.hostname);
82
- }, "Non-loopback Overmux origins must use HTTPS");
83
- const loopbackPeerSchema = z.string().refine((value) => isIP(value) !== 0 && (value === "::1" || value.startsWith("127.")), "Expected a loopback IP address");
84
- const authConfigRuntimeSchema = z.object({
85
- mode: z.literal("cli-login"),
86
- origins: z.array(exactOriginSchema).min(1).refine((origins) => new Set(origins).size === origins.length, "Expected unique origins").optional(),
87
- sessionLifetime: z.union([z.literal("forever"), authDurationSchema]).default("forever"),
88
- trustedProxyPeer: loopbackPeerSchema.optional()
89
- }).strict();
90
- const serverConfigRuntimeSchema = z.object({
91
- host: z.string().min(1).optional(),
92
- port: z.number().int().min(1).max(65535).optional(),
93
- productionWebAssetsDir: z.string().min(1).optional(),
94
- watch: z.boolean().optional()
95
- });
96
- const serverDefinitionRuntimeSchema = z.object({
97
- operations: z.record(z.string(), operationRuntimeSchema).optional(),
98
- resources: z.record(z.string(), resourceDefinitionRuntimeSchema),
99
- streams: z.record(z.string(), streamHandlerRuntimeSchema).optional()
100
- });
101
- const configDefinitionRuntimeSchema = serverConfigRuntimeSchema.extend({
102
- aiContextSnippets: z.array(z.enum(aiContextSnippets)).optional(),
103
- auth: authConfigRuntimeSchema,
104
- debug: z.boolean().optional(),
105
- instanceId: z.union([instanceIdSchema$1, z.custom((value) => typeof value === "function", "Expected an instance ID or a function of the bound port")]).optional(),
106
- server: serverDefinitionRuntimeSchema,
107
- vite: z.string().min(1).optional()
108
- });
109
- const resolveLogicalPath = async (path) => {
110
- const logicalPath = resolve(path);
111
- return {
112
- logicalPath,
113
- realPath: await realpath(logicalPath)
114
- };
115
- };
116
- const resolveOvermuxConfigPaths = async (configPath) => {
117
- const { logicalPath, realPath } = await resolveLogicalPath(configPath);
118
- return {
119
- logicalApplicationRoot: dirname(logicalPath),
120
- logicalConfigPath: logicalPath,
121
- realApplicationRoot: dirname(realPath),
122
- realConfigPath: realPath
123
- };
124
- };
125
- const unwrapDefaultExport = (module) => {
126
- if (typeof module === "object" && module !== null && "default" in module) return module.default;
127
- return module;
128
- };
129
- const loadOvermuxConfig = async ({ aliases = {}, configPath }) => {
130
- const paths = await resolveOvermuxConfigPaths(configPath);
131
- const module = await createJiti(paths.logicalConfigPath, {
132
- alias: aliases,
133
- fsCache: true,
134
- jsx: {
135
- importSource: "react",
136
- runtime: "automatic"
137
- },
138
- moduleCache: false,
139
- sourceMaps: true,
140
- tsconfigPaths: true
141
- }).import(paths.logicalConfigPath);
142
- return {
143
- config: configDefinitionRuntimeSchema.parse(unwrapDefaultExport(module)),
144
- paths
145
- };
146
- };
147
- const xdgDirectory = (configured, fallback) => configured && isAbsolute(configured) ? configured : fallback;
148
- const overmuxDirectory = (root) => join(root, "overmux");
149
- const getOvermuxPaths = ({ environment = process.env, homeDir = homedir(), xdgCacheHome = environment.XDG_CACHE_HOME, xdgConfigHome = environment.XDG_CONFIG_HOME, xdgDataHome = environment.XDG_DATA_HOME, xdgRuntimeDir = environment.XDG_RUNTIME_DIR, xdgStateHome = environment.XDG_STATE_HOME } = {}) => {
150
- if (!isAbsolute(homeDir)) throw new TypeError("The home directory must be absolute.");
151
- const runtimeDir = xdgRuntimeDir && isAbsolute(xdgRuntimeDir) ? overmuxDirectory(xdgRuntimeDir) : void 0;
152
- return {
153
- cacheDir: overmuxDirectory(xdgDirectory(xdgCacheHome, join(homeDir, ".cache"))),
154
- configDir: overmuxDirectory(xdgDirectory(xdgConfigHome, join(homeDir, ".config"))),
155
- dataDir: overmuxDirectory(xdgDirectory(xdgDataHome, join(homeDir, ".local", "share"))),
156
- ...runtimeDir ? { runtimeDir } : {},
157
- stateDir: overmuxDirectory(xdgDirectory(xdgStateHome, join(homeDir, ".local", "state")))
158
- };
159
- };
160
- //#endregion
21
+ import { Worker } from "node:worker_threads";
161
22
  //#region ../lib/dist/index.js
162
23
  const durationPattern = /^([1-9]\d*)(ms|s|m|h|d)$/;
163
24
  const millisecondsByUnit = {
@@ -845,37 +706,6 @@ const createHttpAuthenticationHandlers = ({ auth, authAssetsDirectory }) => {
845
706
  };
846
707
  };
847
708
  //#endregion
848
- //#region ../shared/dist/index.js
849
- const overmuxSettingsPath = "/_overmux/settings";
850
- const overmuxLogoutPath = "/_overmux/logout";
851
- const safeRelativeNotificationLink = (link) => {
852
- let decoded;
853
- try {
854
- decoded = decodeURIComponent(link);
855
- } catch {
856
- return false;
857
- }
858
- return !decoded.startsWith("//") && !decoded.includes("\\") && ![...decoded].some((character) => {
859
- const codePoint = character.codePointAt(0) ?? 0;
860
- return codePoint < 32 || codePoint === 127;
861
- });
862
- };
863
- const notificationLinkSchema = z.union([z.string().max(4096).startsWith("/").refine(safeRelativeNotificationLink), z.string().max(4096).url().refine((link) => {
864
- if (!URL.canParse(link)) return false;
865
- const url = new URL(link);
866
- return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
867
- })]);
868
- const notificationSchema = z.object({
869
- body: z.string().max(4096).optional(),
870
- open: z.object({ link: notificationLinkSchema }).strict().optional(),
871
- title: z.string().min(1).max(4096)
872
- }).strict();
873
- const instanceIdSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?![\s\S])/, "Expected a lowercase instance ID with only ASCII letters, digits, internal dots and hyphens");
874
- const createInstanceIdentity = (instanceId) => ({
875
- instanceId: instanceIdSchema.parse(instanceId),
876
- deepLinkPrefix: `overmux://${instanceId}`
877
- });
878
- //#endregion
879
709
  //#region src/internal/server/auth/instance-control.ts
880
710
  const maxControlMessageBytes = 16384;
881
711
  const controlRequestSchema = z.discriminatedUnion("type", [
@@ -1098,214 +928,6 @@ const startInstanceControl = async ({ auth, apiUrl, instanceId, port, url }) =>
1098
928
  return { close };
1099
929
  };
1100
930
  //#endregion
1101
- //#region src/internal/shared/background-notifications.ts
1102
- const deliveryKeySchema = z.string().min(1).max(1024).regex(/^[\w-]+$/u);
1103
- const backgroundNotificationSubscriptionSchema = z.object({
1104
- endpoint: z.url().max(4096).refine((endpoint) => {
1105
- const url = new URL(endpoint);
1106
- return url.protocol === "https:" && !url.username && !url.password;
1107
- }),
1108
- expirationTime: z.number().int().nonnegative().nullable(),
1109
- keys: z.object({
1110
- auth: deliveryKeySchema,
1111
- p256dh: deliveryKeySchema
1112
- }).strict()
1113
- }).strict();
1114
- z.object({ publicKey: deliveryKeySchema }).strict();
1115
- const backgroundNotificationDisableRequestSchema = z.object({ endpoint: backgroundNotificationSubscriptionSchema.shape.endpoint }).strict();
1116
- const webSocketCloseCode = {
1117
- authenticationRevoked: 4001,
1118
- authenticationUnavailable: 4002
1119
- };
1120
- z.object({
1121
- debug: z.boolean(),
1122
- operations: z.array(z.string().min(1)),
1123
- protocolVersion: z.literal(10),
1124
- resources: z.array(z.string().min(1)),
1125
- streams: z.array(z.string().min(1))
1126
- });
1127
- const updateAvailableEventSchema = z.object({ type: z.literal("update-available") });
1128
- const restartingEventSchema = z.object({ type: z.literal("restarting") });
1129
- z.discriminatedUnion("type", [updateAvailableEventSchema, restartingEventSchema]);
1130
- const notificationEventSchema = z.object({
1131
- notification: notificationSchema,
1132
- type: z.literal("notification")
1133
- }).strict();
1134
- const operationIdSchema = z.string().min(1).max(256);
1135
- const registrationNameSchema = z.string().min(1).max(256);
1136
- const resourceReadMessageSchema = z.object({
1137
- input: z.unknown().optional(),
1138
- operationId: operationIdSchema,
1139
- resourceName: registrationNameSchema,
1140
- type: z.literal("resource-read")
1141
- });
1142
- const resourceSubscribeMessageSchema = z.object({
1143
- input: z.unknown().optional(),
1144
- operationId: operationIdSchema,
1145
- resourceName: registrationNameSchema,
1146
- subscriptionId: operationIdSchema,
1147
- type: z.literal("resource-subscribe")
1148
- });
1149
- const resourceUnsubscribeMessageSchema = z.object({
1150
- subscriptionId: operationIdSchema,
1151
- type: z.literal("resource-unsubscribe")
1152
- });
1153
- const streamOpenMessageSchema = z.object({
1154
- input: z.unknown().optional(),
1155
- operationId: operationIdSchema,
1156
- streamId: operationIdSchema,
1157
- streamName: registrationNameSchema,
1158
- type: z.literal("stream-open")
1159
- });
1160
- const streamMessageSchema = z.object({
1161
- message: z.unknown(),
1162
- streamId: operationIdSchema,
1163
- type: z.literal("stream-message")
1164
- });
1165
- const streamCloseMessageSchema = z.object({
1166
- streamId: operationIdSchema,
1167
- type: z.literal("stream-close")
1168
- });
1169
- const clientDiagnosticEventSchema = z.object({
1170
- arguments: z.string().max(16100),
1171
- level: z.enum([
1172
- "log",
1173
- "info",
1174
- "warn",
1175
- "error",
1176
- "debug"
1177
- ]),
1178
- message: z.string().max(4100),
1179
- stack: z.string().max(8100).optional(),
1180
- timestamp: z.iso.datetime(),
1181
- type: z.literal("client-diagnostic"),
1182
- url: z.string().max(2100).optional()
1183
- });
1184
- const clientProtocolMessageSchema = z.discriminatedUnion("type", [
1185
- resourceReadMessageSchema,
1186
- resourceSubscribeMessageSchema,
1187
- resourceUnsubscribeMessageSchema,
1188
- streamOpenMessageSchema,
1189
- streamMessageSchema,
1190
- streamCloseMessageSchema,
1191
- clientDiagnosticEventSchema
1192
- ]);
1193
- const resourceResultMessageSchema = z.object({
1194
- operationId: operationIdSchema,
1195
- output: z.unknown(),
1196
- type: z.literal("resource-result")
1197
- });
1198
- const resourceInvalidatedMessageSchema = z.object({
1199
- subscriptionId: operationIdSchema,
1200
- type: z.literal("resource-invalidated")
1201
- });
1202
- const streamOpenedMessageSchema = z.object({
1203
- operationId: operationIdSchema,
1204
- streamId: operationIdSchema,
1205
- type: z.literal("stream-opened")
1206
- });
1207
- const streamOutputMessageSchema = z.object({
1208
- message: z.unknown(),
1209
- streamId: operationIdSchema,
1210
- type: z.literal("stream-output")
1211
- });
1212
- const streamClosedMessageSchema = z.object({
1213
- streamId: operationIdSchema,
1214
- type: z.literal("stream-closed")
1215
- });
1216
- const protocolErrorMessageSchema = z.object({
1217
- code: z.enum([
1218
- "bad-request",
1219
- "conflict",
1220
- "internal",
1221
- "not-found"
1222
- ]),
1223
- message: z.string(),
1224
- operationId: operationIdSchema.optional(),
1225
- streamId: operationIdSchema.optional(),
1226
- subscriptionId: operationIdSchema.optional(),
1227
- type: z.literal("error")
1228
- });
1229
- const serverInfoMessageSchema = z.object({
1230
- type: z.literal("server-info"),
1231
- instanceId: instanceIdSchema
1232
- }).strict();
1233
- const serverProtocolMessageSchema = z.discriminatedUnion("type", [
1234
- serverInfoMessageSchema,
1235
- resourceResultMessageSchema,
1236
- resourceInvalidatedMessageSchema,
1237
- streamOpenedMessageSchema,
1238
- streamOutputMessageSchema,
1239
- streamClosedMessageSchema,
1240
- protocolErrorMessageSchema,
1241
- updateAvailableEventSchema,
1242
- restartingEventSchema,
1243
- notificationEventSchema
1244
- ]);
1245
- const byteMarker = "$overmuxBytes";
1246
- const decodeValue = (value) => {
1247
- if (Array.isArray(value)) return value.map(decodeValue);
1248
- if (typeof value !== "object" || value === null) return value;
1249
- const record = value;
1250
- if (Object.keys(record).length === 1 && Array.isArray(record[byteMarker]) && record[byteMarker].every((item) => Number.isInteger(item) && item >= 0 && item <= 255)) return Uint8Array.from(record[byteMarker]);
1251
- return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeValue(item)]));
1252
- };
1253
- const binaryMarker = "$overmuxBinary";
1254
- const binaryMagic = 1330468865;
1255
- const encodeBinaryValue = (value, payloads, offset) => {
1256
- if (value instanceof Uint8Array) {
1257
- const marker = { [binaryMarker]: [offset.value, value.byteLength] };
1258
- payloads.push(value);
1259
- offset.value += value.byteLength;
1260
- return marker;
1261
- }
1262
- if (Array.isArray(value)) return value.map((item) => encodeBinaryValue(item, payloads, offset));
1263
- if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeBinaryValue(item, payloads, offset)]));
1264
- return value;
1265
- };
1266
- const decodeBinaryValue = (value, payload) => {
1267
- if (Array.isArray(value)) return value.map((item) => decodeBinaryValue(item, payload));
1268
- if (typeof value !== "object" || value === null) return value;
1269
- const record = value;
1270
- const marker = record[binaryMarker];
1271
- if (Object.keys(record).length === 1 && Array.isArray(marker) && marker.length === 2 && marker.every((item) => Number.isInteger(item) && item >= 0)) {
1272
- const [offset, length] = marker;
1273
- if (offset + length > payload.byteLength) throw new Error("Invalid Overmux binary payload range");
1274
- return payload.slice(offset, offset + length);
1275
- }
1276
- return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeBinaryValue(item, payload)]));
1277
- };
1278
- const decodeProtocolMessage = (message) => decodeValue(JSON.parse(message));
1279
- const encodeProtocolFrame = (message) => {
1280
- const payloads = [];
1281
- const encoded = encodeBinaryValue(message, payloads, { value: 0 });
1282
- if (!payloads.length) return JSON.stringify(encoded);
1283
- const header = new TextEncoder().encode(JSON.stringify(encoded));
1284
- const payloadLength = payloads.reduce((total, payload) => total + payload.byteLength, 0);
1285
- const frame = new Uint8Array(8 + header.byteLength + payloadLength);
1286
- const view = new DataView(frame.buffer);
1287
- view.setUint32(0, binaryMagic);
1288
- view.setUint32(4, header.byteLength);
1289
- frame.set(header, 8);
1290
- let offset = 8 + header.byteLength;
1291
- payloads.forEach((payload) => {
1292
- frame.set(payload, offset);
1293
- offset += payload.byteLength;
1294
- });
1295
- return frame;
1296
- };
1297
- const decodeProtocolFrame = (frame) => {
1298
- if (typeof frame === "string") return decodeProtocolMessage(frame);
1299
- const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame, 0, frame.byteLength);
1300
- if (bytes.byteLength < 8) throw new Error("Invalid Overmux binary frame");
1301
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1302
- if (view.getUint32(0) !== binaryMagic) throw new Error("Invalid Overmux binary frame magic");
1303
- const headerLength = view.getUint32(4);
1304
- if (8 + headerLength > bytes.byteLength) throw new Error("Invalid Overmux binary frame header");
1305
- const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)));
1306
- return decodeBinaryValue(header, bytes.subarray(8 + headerLength));
1307
- };
1308
- //#endregion
1309
931
  //#region src/internal/server/http/background-notification-handlers.ts
1310
932
  const requestLimit = 16384;
1311
933
  const parseJson = async (request, schema) => {
@@ -1339,61 +961,6 @@ const createBackgroundNotificationHandlers = ({ backgroundNotifications }) => ({
1339
961
  }
1340
962
  });
1341
963
  //#endregion
1342
- //#region src/internal/server/server-logger.ts
1343
- const lifecycleEvents = /* @__PURE__ */ new Set([
1344
- "server-start",
1345
- "server-stop",
1346
- "websocket-close",
1347
- "websocket-close-event",
1348
- "websocket-connect",
1349
- "websocket-open"
1350
- ]);
1351
- const getServerLogFile = ({ homeDir, xdgStateHome } = {}) => join(getOvermuxPaths({
1352
- homeDir,
1353
- xdgStateHome
1354
- }).stateDir, "overmux.log");
1355
- const safeJson = (value) => {
1356
- try {
1357
- return JSON.stringify(value);
1358
- } catch {
1359
- return JSON.stringify({
1360
- event: "log-serialization-error",
1361
- level: "error",
1362
- source: "server",
1363
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1364
- });
1365
- }
1366
- };
1367
- const createServerLogger = ({ enabled, logFile }) => {
1368
- let debugEnabled = enabled;
1369
- return {
1370
- get enabled() {
1371
- return debugEnabled;
1372
- },
1373
- id: randomUUID,
1374
- log: (entry) => {
1375
- if (!debugEnabled && entry.level !== "error" && !lifecycleEvents.has(entry.event)) return;
1376
- const record = safeJson({
1377
- timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1378
- level: entry.level ?? "info",
1379
- source: entry.source ?? "server",
1380
- ...entry
1381
- });
1382
- mkdirSync(dirname(logFile), { recursive: true });
1383
- appendFileSync(logFile, `${record}\n`, "utf8");
1384
- process.stderr.write(`[overmux:debug] ${record}\n`);
1385
- },
1386
- setEnabled: (nextEnabled) => {
1387
- debugEnabled = nextEnabled;
1388
- }
1389
- };
1390
- };
1391
- const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
1392
- const errorDetails$1 = (cause) => ({
1393
- message: errorMessage(cause),
1394
- ...cause instanceof Error && cause.stack ? { stack: cause.stack } : {}
1395
- });
1396
- //#endregion
1397
964
  //#region src/internal/server/http/development-web-backend.ts
1398
965
  const createForwardingHeaders = ({ origin, remoteAddress, request }) => {
1399
966
  const headers = new Headers(request.headers);
@@ -1449,42 +1016,6 @@ const createRestartHandler = ({ onRestartRequested, shutdown }) => (context) =>
1449
1016
  return response;
1450
1017
  };
1451
1018
  //#endregion
1452
- //#region src/internal/server/runtime/runtime-operations.ts
1453
- var OperationValidationError = class extends Error {
1454
- phase;
1455
- constructor(phase, cause) {
1456
- super(cause.message, { cause });
1457
- this.phase = phase;
1458
- }
1459
- };
1460
- const createRuntimeOperations = ({ context, definitions, notifications }) => {
1461
- const operations = /* @__PURE__ */ new Map();
1462
- Object.entries(definitions).forEach(([name, definition]) => {
1463
- if (!name) throw new Error("Operation names must not be empty");
1464
- operations.set(name, {
1465
- execute: async (rawInput, signal) => {
1466
- const operationContext = context(signal);
1467
- operationContext.signal.throwIfAborted();
1468
- const input = definition.input.safeParse(rawInput);
1469
- if (!input.success) throw new OperationValidationError("input", input.error);
1470
- const rawOutput = await definition.handle(input.data, {
1471
- ...operationContext,
1472
- notifications
1473
- });
1474
- operationContext.signal.throwIfAborted();
1475
- const output = definition.output.safeParse(rawOutput);
1476
- if (!output.success) throw new OperationValidationError("output", output.error);
1477
- return output.data;
1478
- },
1479
- returnsVoid: definition.output instanceof z.ZodVoid
1480
- });
1481
- });
1482
- return {
1483
- get: (name) => operations.get(name),
1484
- names: [...operations.keys()]
1485
- };
1486
- };
1487
- //#endregion
1488
1019
  //#region src/internal/server/http/operation-handler.ts
1489
1020
  const maxOperationRequestBytes = 1048576;
1490
1021
  const parseOperationRequest = async (request) => {
@@ -1521,15 +1052,17 @@ const createOperationHandler = ({ runtime, serverLogger }) => async (context) =>
1521
1052
  serverLogger?.log({
1522
1053
  correlationId,
1523
1054
  details: { registeredName: context.req.param("name") },
1524
- event: "operation-start"
1055
+ event: "operation-start",
1056
+ level: "debug"
1525
1057
  });
1526
1058
  try {
1527
- const output = await operation.execute(parsed.input, context.req.raw.signal);
1059
+ const output = await operation.execute(parsed.input, context.req.raw.signal, { correlationId });
1528
1060
  serverLogger?.log({
1529
1061
  correlationId,
1530
1062
  details: { registeredName: context.req.param("name") },
1531
1063
  durationMs: Date.now() - startedAt,
1532
- event: "operation-complete"
1064
+ event: "operation-complete",
1065
+ level: "debug"
1533
1066
  });
1534
1067
  return operation.returnsVoid ? new Response(void 0, { status: 204 }) : context.json(output);
1535
1068
  } catch (cause) {
@@ -1562,7 +1095,7 @@ const createRequestLoggingMiddleware = ({ serverLogger }) => async (context, nex
1562
1095
  },
1563
1096
  durationMs: Date.now() - startedAt,
1564
1097
  event: failed ? "http-request-failure" : "http-request",
1565
- level: failed ? "error" : void 0,
1098
+ level: failed ? "error" : "debug",
1566
1099
  message: context.error?.message
1567
1100
  });
1568
1101
  };
@@ -1770,13 +1303,6 @@ const createHttpApp = ({ auth, authAssetsDirectory, backgroundNotifications, dev
1770
1303
  //#endregion
1771
1304
  //#region src/internal/server/websocket-connection.ts
1772
1305
  const maxPendingStreamOutputBytes = 1048576;
1773
- var OperationError = class extends Error {
1774
- code;
1775
- constructor(code, message) {
1776
- super(message);
1777
- this.code = code;
1778
- }
1779
- };
1780
1306
  const bytesOf = (data) => {
1781
1307
  if (Array.isArray(data)) return Buffer.concat(data);
1782
1308
  if (data instanceof ArrayBuffer) return new Uint8Array(data);
@@ -1793,7 +1319,7 @@ const operationIdOf = (value) => {
1793
1319
  return typeof operationId === "string" && operationId.length <= 256 ? operationId : void 0;
1794
1320
  };
1795
1321
  const errorDetails = (cause) => {
1796
- if (cause instanceof OperationError) return {
1322
+ if (cause instanceof ProtocolError) return {
1797
1323
  code: cause.code,
1798
1324
  message: cause.message
1799
1325
  };
@@ -1833,6 +1359,14 @@ const sendError = (state, cause, operationId, identity) => {
1833
1359
  type: "error"
1834
1360
  });
1835
1361
  };
1362
+ const reportCleanupFailure = (state, cause) => {
1363
+ state.serverLogger?.log({
1364
+ correlationId: state.connectionId,
1365
+ details: errorDetails$1(cause),
1366
+ event: "protocol-cleanup-error",
1367
+ level: "error"
1368
+ });
1369
+ };
1836
1370
  const disposeSubscription = async (state, subscriptionId) => {
1837
1371
  const entry = state.subscriptions.get(subscriptionId);
1838
1372
  if (!entry) return;
@@ -1851,11 +1385,11 @@ const disposeStream = async (state, streamId, notify = true) => {
1851
1385
  }
1852
1386
  state.streams.delete(streamId);
1853
1387
  entry.controller.abort(/* @__PURE__ */ new Error("Stream closed"));
1854
- await entry.session?.dispose();
1855
1388
  if (notify) send$1(state, {
1856
1389
  streamId,
1857
1390
  type: "stream-closed"
1858
1391
  });
1392
+ await entry.session?.dispose();
1859
1393
  };
1860
1394
  const disposeConnectionOperations = async (state, notify) => {
1861
1395
  const operationControllers = [...state.operations.values()];
@@ -1867,7 +1401,7 @@ const disposeConnectionOperations = async (state, notify) => {
1867
1401
  };
1868
1402
  const runOperation = (state, identity, operation) => {
1869
1403
  if (state.operations.has(identity.operationId)) {
1870
- sendError(state, new OperationError("conflict", "Operation ID is already active"), identity.operationId, identity);
1404
+ sendError(state, new ProtocolError("conflict", "Operation ID is already active"), identity.operationId, identity);
1871
1405
  return;
1872
1406
  }
1873
1407
  const controller = new AbortController();
@@ -1876,14 +1410,16 @@ const runOperation = (state, identity, operation) => {
1876
1410
  state.serverLogger?.log({
1877
1411
  correlationId: identity.operationId,
1878
1412
  details: identity,
1879
- event: "operation-start"
1413
+ event: "operation-start",
1414
+ level: "debug"
1880
1415
  });
1881
1416
  operation(controller.signal).then(() => {
1882
1417
  state.serverLogger?.log({
1883
1418
  correlationId: identity.operationId,
1884
1419
  details: identity,
1885
1420
  durationMs: Date.now() - startedAt,
1886
- event: "operation-complete"
1421
+ event: "operation-complete",
1422
+ level: "debug"
1887
1423
  });
1888
1424
  }).catch((cause) => {
1889
1425
  if (!controller.signal.aborted && !state.closed) {
@@ -1911,8 +1447,11 @@ const readResource = (state, message) => {
1911
1447
  requestedName: message.resourceName
1912
1448
  };
1913
1449
  runOperation(state, identity, async (signal) => {
1914
- if (!resource) throw new OperationError("not-found", "Resource not found");
1915
- const output = await resource.read(message.input, signal);
1450
+ if (!resource) throw new ProtocolError("not-found", "Resource not found");
1451
+ const output = await resource.read(message.input, signal, {
1452
+ correlationId: message.operationId,
1453
+ connectionId: state.connectionId
1454
+ });
1916
1455
  send$1(state, {
1917
1456
  operationId: message.operationId,
1918
1457
  output,
@@ -1928,20 +1467,31 @@ const subscribeResource = (state, message) => {
1928
1467
  requestedName: message.resourceName
1929
1468
  };
1930
1469
  if (state.subscriptions.has(message.subscriptionId)) {
1931
- sendError(state, new OperationError("conflict", "Subscription ID is already active"), message.operationId, identity);
1470
+ sendError(state, new ProtocolError("conflict", "Subscription ID is already active"), message.operationId, identity);
1932
1471
  return;
1933
1472
  }
1934
1473
  runOperation(state, identity, async (operationSignal) => {
1935
- if (!resource) throw new OperationError("not-found", "Resource not found");
1474
+ if (!resource) throw new ProtocolError("not-found", "Resource not found");
1936
1475
  const controller = new AbortController();
1937
1476
  const entry = { controller };
1938
1477
  state.subscriptions.set(message.subscriptionId, entry);
1939
1478
  const signal = AbortSignal.any([operationSignal, controller.signal]);
1940
1479
  try {
1941
- const dispose = await resource.subscribe(message.input, () => void send$1(state, {
1942
- subscriptionId: message.subscriptionId,
1943
- type: "resource-invalidated"
1944
- }), signal);
1480
+ const dispose = await resource.subscribe(message.input, () => {
1481
+ if (signal.aborted || state.subscriptions.get(message.subscriptionId) !== entry) return;
1482
+ send$1(state, {
1483
+ subscriptionId: message.subscriptionId,
1484
+ type: "resource-invalidated"
1485
+ });
1486
+ }, signal, (cause) => {
1487
+ if (signal.aborted || state.subscriptions.get(message.subscriptionId) !== entry) return;
1488
+ sendError(state, cause, message.subscriptionId, identity);
1489
+ disposeSubscription(state, message.subscriptionId).catch((disposeCause) => sendError(state, disposeCause, message.subscriptionId, identity));
1490
+ }, {
1491
+ correlationId: message.operationId,
1492
+ connectionId: state.connectionId,
1493
+ subscriptionId: message.subscriptionId
1494
+ });
1945
1495
  if (state.subscriptions.get(message.subscriptionId) === entry) entry.dispose = dispose;
1946
1496
  else await dispose();
1947
1497
  } catch (cause) {
@@ -1970,11 +1520,11 @@ const openStream = (state, message) => {
1970
1520
  requestedName: message.streamName
1971
1521
  };
1972
1522
  if (state.streams.has(message.streamId)) {
1973
- sendError(state, new OperationError("conflict", "Stream ID is already active"), message.operationId, identity);
1523
+ sendError(state, new ProtocolError("conflict", "Stream ID is already active"), message.operationId, identity);
1974
1524
  return;
1975
1525
  }
1976
1526
  runOperation(state, identity, async (operationSignal) => {
1977
- if (!stream) throw new OperationError("not-found", "Stream not found");
1527
+ if (!stream) throw new ProtocolError("not-found", "Stream not found");
1978
1528
  const controller = new AbortController();
1979
1529
  const entry = {
1980
1530
  controller,
@@ -1984,8 +1534,10 @@ const openStream = (state, message) => {
1984
1534
  const pending = [];
1985
1535
  let pendingBytes = 0;
1986
1536
  let opened = false;
1537
+ const signal = AbortSignal.any([operationSignal, controller.signal]);
1987
1538
  try {
1988
- entry.session = await stream.open(message.input, (output) => {
1539
+ const session = await stream.open(message.input, (output) => {
1540
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) return;
1989
1541
  if (opened) {
1990
1542
  if (!send$1(state, {
1991
1543
  message: output,
@@ -1997,12 +1549,22 @@ const openStream = (state, message) => {
1997
1549
  pendingBytes += frameLength(output);
1998
1550
  if (pendingBytes > maxPendingStreamOutputBytes) throw new Error("Stream produced too much output while opening");
1999
1551
  pending.push(output);
2000
- }, AbortSignal.any([operationSignal, controller.signal]), (cause) => {
1552
+ }, signal, (cause) => {
1553
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) return;
2001
1554
  sendError(state, cause, message.streamId, identity);
2002
1555
  disposeStream(state, message.streamId).catch((disposeCause) => {
2003
- sendError(state, disposeCause, message.streamId, identity);
1556
+ reportCleanupFailure(state, disposeCause);
2004
1557
  });
1558
+ }, {
1559
+ correlationId: message.operationId,
1560
+ connectionId: state.connectionId,
1561
+ streamId: message.streamId
2005
1562
  });
1563
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) {
1564
+ await session.dispose();
1565
+ return;
1566
+ }
1567
+ entry.session = session;
2006
1568
  send$1(state, {
2007
1569
  operationId: message.operationId,
2008
1570
  streamId: message.streamId,
@@ -2015,6 +1577,7 @@ const openStream = (state, message) => {
2015
1577
  type: "stream-output"
2016
1578
  })) throw new Error("Stream output could not be delivered");
2017
1579
  } catch (cause) {
1580
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) return;
2018
1581
  await disposeStream(state, message.streamId, false);
2019
1582
  throw cause;
2020
1583
  }
@@ -2028,18 +1591,19 @@ const sendStreamMessage = (state, message) => {
2028
1591
  operationType: message.type
2029
1592
  };
2030
1593
  if (!entry?.session) {
2031
- sendError(state, new OperationError("not-found", "Stream not found"), message.streamId, identity);
1594
+ sendError(state, new ProtocolError("not-found", "Stream not found"), message.streamId, identity);
2032
1595
  return;
2033
1596
  }
2034
1597
  entry.session.send(message.message).catch((cause) => {
1598
+ if (entry.controller.signal.aborted || state.streams.get(message.streamId) !== entry) return;
2035
1599
  sendError(state, cause, message.streamId, identity);
2036
1600
  disposeStream(state, message.streamId).catch((disposeCause) => {
2037
- sendError(state, disposeCause, message.streamId, identity);
1601
+ reportCleanupFailure(state, disposeCause);
2038
1602
  });
2039
1603
  });
2040
1604
  };
2041
1605
  const logClientDiagnostic = (state, message) => {
2042
- if (!state.serverLogger?.enabled) return;
1606
+ if (state.serverLogger?.logLevel !== "debug") return;
2043
1607
  state.serverLogger.log({
2044
1608
  details: {
2045
1609
  arguments: message.arguments,
@@ -2068,7 +1632,7 @@ const handleMessage = (state, message) => {
2068
1632
  }
2069
1633
  if (message.type === "resource-unsubscribe") {
2070
1634
  disposeSubscription(state, message.subscriptionId).catch((cause) => {
2071
- sendError(state, cause, message.subscriptionId);
1635
+ reportCleanupFailure(state, cause);
2072
1636
  });
2073
1637
  return;
2074
1638
  }
@@ -2078,7 +1642,7 @@ const handleMessage = (state, message) => {
2078
1642
  }
2079
1643
  if (message.type === "stream-close") {
2080
1644
  disposeStream(state, message.streamId).catch((cause) => {
2081
- sendError(state, cause, message.streamId);
1645
+ reportCleanupFailure(state, cause);
2082
1646
  });
2083
1647
  return;
2084
1648
  }
@@ -2115,7 +1679,8 @@ const disposeConnection = async (state) => {
2115
1679
  state.closed = true;
2116
1680
  state.serverLogger?.log({
2117
1681
  correlationId: state.connectionId,
2118
- event: "websocket-close"
1682
+ event: "websocket-close",
1683
+ level: "debug"
2119
1684
  });
2120
1685
  await disposeConnectionOperations(state, false);
2121
1686
  };
@@ -2137,7 +1702,8 @@ const attachWebSocketConnection = ({ runtime, serverLogger, socket }) => {
2137
1702
  });
2138
1703
  serverLogger?.log({
2139
1704
  correlationId: state.connectionId,
2140
- event: "websocket-open"
1705
+ event: "websocket-open",
1706
+ level: "debug"
2141
1707
  });
2142
1708
  socket.on("message", (data, isBinary) => handleFrame(state, data, isBinary));
2143
1709
  socket.once("close", (code, reason) => {
@@ -2147,7 +1713,8 @@ const attachWebSocketConnection = ({ runtime, serverLogger, socket }) => {
2147
1713
  code,
2148
1714
  reason: reason?.toString() ?? ""
2149
1715
  },
2150
- event: "websocket-close-event"
1716
+ event: "websocket-close-event",
1717
+ level: "debug"
2151
1718
  });
2152
1719
  disposeConnection(state);
2153
1720
  });
@@ -2336,7 +1903,8 @@ const createWebSocketServer = ({ auth, developmentWebTarget, runtime, server, se
2336
1903
  const correlationId = serverLogger?.id();
2337
1904
  serverLogger?.log({
2338
1905
  correlationId,
2339
- event: "websocket-connect"
1906
+ event: "websocket-connect",
1907
+ level: "debug"
2340
1908
  });
2341
1909
  const path = requestPath(request);
2342
1910
  const isRuntimeSocket = path === runtimeWebSocketPath;
@@ -2639,15 +2207,6 @@ const createFileBackgroundNotificationStore = ({ environment = process.env, home
2639
2207
  }
2640
2208
  };
2641
2209
  };
2642
- z.void();
2643
- const jsonValueSchema = z.lazy(() => z.union([
2644
- z.string(),
2645
- z.number().finite(),
2646
- z.boolean(),
2647
- z.null(),
2648
- z.array(jsonValueSchema),
2649
- z.record(z.string(), jsonValueSchema)
2650
- ]));
2651
2210
  //#endregion
2652
2211
  //#region src/internal/server/notifications/notification-service.ts
2653
2212
  const deliverSafely = async ({ deliver, event, logger }) => {
@@ -2685,389 +2244,537 @@ const createNotificationService = ({ backgroundNotifications, logger }) => {
2685
2244
  };
2686
2245
  };
2687
2246
  //#endregion
2688
- //#region src/internal/server/runtime/runtime-instance.ts
2689
- const defaultInstanceId = ({ port }) => `${hostname()}-${port}`;
2690
- const createRuntimeInstance = (configured) => {
2691
- let identity;
2692
- const get = () => {
2693
- if (!identity) throw new Error("Overmux instance identity is not ready");
2694
- return identity;
2247
+ //#region src/internal/server/runtime/worker-connection.ts
2248
+ const deliverNotification = async (message, notifications, postControl) => {
2249
+ await Promise.resolve();
2250
+ try {
2251
+ await notifications.send(message.notification);
2252
+ postControl({
2253
+ type: "notification-result",
2254
+ id: message.id,
2255
+ ok: true
2256
+ });
2257
+ } catch (cause) {
2258
+ postControl({
2259
+ type: "notification-result",
2260
+ id: message.id,
2261
+ ok: false,
2262
+ error: String(cause).slice(0, 8192)
2263
+ });
2264
+ }
2265
+ };
2266
+ let sequence = 0;
2267
+ const nextId = () => ++sequence;
2268
+ const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
2269
+ const contain = (callback) => {
2270
+ try {
2271
+ callback();
2272
+ } catch {}
2273
+ };
2274
+ const settleCaller = (pending, result) => {
2275
+ if (pending.caller.status === "settled") return;
2276
+ const { settle } = pending.caller;
2277
+ pending.caller = { status: "settled" };
2278
+ settle(result);
2279
+ };
2280
+ const ownSession = ({ id, options, postClose, release }) => {
2281
+ let state = {
2282
+ status: "opening",
2283
+ admission: "local"
2284
+ };
2285
+ const detach = () => options.signal?.removeEventListener("abort", abort);
2286
+ const close = (failure) => {
2287
+ if (state.status === "closed" || state.status === "closing") return state.completion;
2288
+ detach();
2289
+ if (state.status === "opening" && state.admission === "local") {
2290
+ state = {
2291
+ status: "closed",
2292
+ completion: Promise.resolve(),
2293
+ failure
2294
+ };
2295
+ release();
2296
+ return state.completion;
2297
+ }
2298
+ const { promise, resolve, reject } = Promise.withResolvers();
2299
+ state = {
2300
+ status: "closing",
2301
+ completion: promise,
2302
+ resolve,
2303
+ reject,
2304
+ failure
2305
+ };
2306
+ promise.catch(() => void 0);
2307
+ postClose();
2308
+ return promise;
2309
+ };
2310
+ const abort = () => {
2311
+ close();
2312
+ };
2313
+ const fail = (cause) => {
2314
+ if (state.status === "closing" || state.status === "closed") return;
2315
+ close(cause);
2316
+ contain(() => options.onError?.(cause));
2317
+ };
2318
+ const finishClose = (cause) => {
2319
+ if (state.status !== "closing") return;
2320
+ const closing = state;
2321
+ state = {
2322
+ status: "closed",
2323
+ completion: closing.completion,
2324
+ failure: closing.failure ?? cause
2325
+ };
2326
+ release();
2327
+ cause ? closing.reject(cause) : closing.resolve();
2695
2328
  };
2329
+ const assertOpen = () => {
2330
+ if (state.status === "opening") throw new Error("Session is still opening");
2331
+ if (state.status === "closing" || state.status === "closed") throw state.failure ?? /* @__PURE__ */ new Error(options.action === "stream-open" ? "Stream is closed" : "Subscription is closed");
2332
+ };
2333
+ options.signal?.addEventListener("abort", abort, { once: true });
2696
2334
  return {
2697
- context: {
2698
- getInstanceId: () => get().instanceId,
2699
- getDeepLinkPrefix: () => get().deepLinkPrefix
2335
+ id,
2336
+ close: () => close(),
2337
+ assertOpen,
2338
+ fail,
2339
+ finishClose,
2340
+ getFailure: () => state.status === "closing" || state.status === "closed" ? state.failure : void 0,
2341
+ markSent: () => {
2342
+ if (state.status === "opening") state = {
2343
+ status: "opening",
2344
+ admission: "remote"
2345
+ };
2700
2346
  },
2701
- establish: (port) => {
2702
- if (identity) throw new Error("Overmux instance identity is already established");
2347
+ opened: () => {
2348
+ if (state.status === "opening") state = { status: "open" };
2349
+ assertOpen();
2350
+ },
2351
+ deliver: (value) => {
2352
+ if (state.status !== "opening" && state.status !== "open") return;
2703
2353
  try {
2704
- identity = createInstanceIdentity(typeof configured === "function" ? configured({ port }) : configured);
2354
+ options.onEvent(value);
2705
2355
  } catch (cause) {
2706
- throw new Error("Invalid Overmux instance ID; configure instanceId with a canonical lowercase ID", { cause });
2356
+ fail(asError(cause));
2357
+ }
2358
+ },
2359
+ terminate: (cause) => {
2360
+ if (state.status === "closed") return;
2361
+ if (state.status === "closing") {
2362
+ finishClose(cause);
2363
+ return;
2707
2364
  }
2708
- return identity;
2365
+ detach();
2366
+ state = {
2367
+ status: "closed",
2368
+ completion: Promise.resolve(),
2369
+ failure: cause
2370
+ };
2371
+ release();
2372
+ contain(() => options.onError?.(cause));
2709
2373
  }
2710
2374
  };
2711
2375
  };
2712
- //#endregion
2713
- //#region src/internal/server/runtime/runtime-lifecycle.ts
2714
- const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
2715
- const disposeAll = async (disposers) => {
2716
- const errors = (await Promise.allSettled([...disposers].map(async (dispose) => dispose()))).flatMap((result) => result.status === "rejected" ? [asError(result.reason)] : []);
2717
- if (errors.length) throw new AggregateError(errors, "Runtime disposal failed");
2718
- };
2719
- const createRuntimeLifecycle = (preparationSignal) => {
2720
- const controller = new AbortController();
2721
- const activeDisposers = /* @__PURE__ */ new Set();
2722
- let disposal;
2723
- const abortPreparation = () => controller.abort(preparationSignal?.reason);
2724
- preparationSignal?.addEventListener("abort", abortPreparation, { once: true });
2725
- if (preparationSignal?.aborted) abortPreparation();
2726
- const completePreparation = () => {
2727
- preparationSignal?.removeEventListener("abort", abortPreparation);
2728
- };
2729
- const requestSignal = (signal) => signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
2730
- const track = (dispose, signal) => {
2731
- let pending;
2732
- const disposeOnce = () => {
2733
- if (pending) return pending;
2734
- controller.signal.removeEventListener("abort", onAbort);
2735
- signal?.removeEventListener("abort", onAbort);
2736
- activeDisposers.delete(disposeOnce);
2737
- pending = Promise.resolve().then(dispose);
2738
- return pending;
2376
+ const createWorkerConnection = (options) => {
2377
+ const worker = new Worker(new URL(import.meta.resolve("#worker-entry")));
2378
+ const startup = Promise.withResolvers();
2379
+ const pending = /* @__PURE__ */ new Map();
2380
+ const sessions = /* @__PURE__ */ new Map();
2381
+ let pendingBytes = 0;
2382
+ let state = {
2383
+ status: "starting",
2384
+ timer: setTimeout(() => {
2385
+ terminate(/* @__PURE__ */ new Error(`Worker ${options.name} startup deadline exceeded`));
2386
+ }, options.startupMs)
2387
+ };
2388
+ const terminate = (cause) => {
2389
+ if (state.status === "stopped") return state.completion;
2390
+ const previous = state;
2391
+ if (previous.status === "starting" || previous.status === "stopping") clearTimeout(previous.timer);
2392
+ const completion = worker.terminate().then(() => void 0);
2393
+ state = {
2394
+ status: "stopped",
2395
+ cause,
2396
+ completion
2739
2397
  };
2740
- const onAbort = () => {
2741
- disposeOnce().catch(() => void 0);
2742
- };
2743
- activeDisposers.add(disposeOnce);
2744
- controller.signal.addEventListener("abort", onAbort, { once: true });
2745
- signal?.addEventListener("abort", onAbort, { once: true });
2746
- if (controller.signal.aborted || signal?.aborted) onAbort();
2747
- return disposeOnce;
2748
- };
2749
- const dispose = (reason = /* @__PURE__ */ new Error("Runtime disposed")) => {
2750
- if (disposal) return disposal;
2751
- completePreparation();
2752
- const pending = [...activeDisposers];
2753
- activeDisposers.clear();
2754
- controller.abort(reason);
2755
- disposal = disposeAll(pending);
2756
- return disposal;
2398
+ if (previous.status === "starting") startup.reject(cause);
2399
+ if (previous.status === "stopping") completion.then(previous.finish, previous.finish);
2400
+ pending.forEach((request) => settleCaller(request, {
2401
+ ok: false,
2402
+ error: cause
2403
+ }));
2404
+ pending.clear();
2405
+ pendingBytes = 0;
2406
+ sessions.forEach((session) => session.terminate(cause));
2407
+ return completion;
2757
2408
  };
2758
- return {
2759
- completePreparation,
2760
- dispose,
2761
- disposeAll,
2762
- requestSignal,
2763
- signal: controller.signal,
2764
- track
2409
+ const postControl = (message) => {
2410
+ if (state.status === "stopped") return;
2411
+ try {
2412
+ postWorkerMessage(worker, encodeWorkerMessage(message));
2413
+ } catch (cause) {
2414
+ terminate(asError(cause));
2415
+ }
2765
2416
  };
2766
- };
2767
- //#endregion
2768
- //#region src/internal/server/runtime/resource-dependency-graph.ts
2769
- const emptyGraphFor = (resourceIds) => new Map(resourceIds.map((resourceId) => [resourceId, /* @__PURE__ */ new Set()]));
2770
- const visitDependencies = ({ dependencies, path, resourceId, visited, visiting }) => {
2771
- if (visiting.has(resourceId)) throw new Error(`Resource dependency cycle: ${[...path, resourceId].join(" -> ")}`);
2772
- if (visited.has(resourceId)) return;
2773
- visiting.add(resourceId);
2774
- dependencies.get(resourceId)?.forEach((dependencyId) => {
2775
- visitDependencies({
2776
- dependencies,
2777
- path: [...path, resourceId],
2778
- resourceId: dependencyId,
2779
- visited,
2780
- visiting
2417
+ const requireReady = () => {
2418
+ if (state.status === "stopping" || state.status === "stopped") throw state.cause;
2419
+ if (state.status !== "ready") throw new Error(`Worker ${options.name} is not ready`);
2420
+ };
2421
+ const request = async (message, signal, onSent) => {
2422
+ signal?.throwIfAborted();
2423
+ requireReady();
2424
+ const id = nextId();
2425
+ const packet = encodeWorkerMessage({
2426
+ ...message,
2427
+ id
2781
2428
  });
2782
- });
2783
- visiting.delete(resourceId);
2784
- visited.add(resourceId);
2785
- };
2786
- const assertAcyclic = (dependencies) => {
2787
- const visited = /* @__PURE__ */ new Set();
2788
- const visiting = /* @__PURE__ */ new Set();
2789
- dependencies.forEach((_, resourceId) => {
2790
- visitDependencies({
2791
- dependencies,
2792
- path: [],
2793
- resourceId,
2794
- visited,
2795
- visiting
2429
+ signal?.throwIfAborted();
2430
+ const bytes = packet.byteLength;
2431
+ if (pending.size >= workerLimits.pendingRequests || pendingBytes + bytes > workerLimits.pendingBytes) throw new Error(`Worker ${options.name} request queue is full or payload exceeds 1 MiB`);
2432
+ return new Promise((resolve, reject) => {
2433
+ const abort = () => {
2434
+ settleCaller(owned, {
2435
+ ok: false,
2436
+ error: signal?.reason
2437
+ });
2438
+ postControl({
2439
+ type: "cancel",
2440
+ requestId: id
2441
+ });
2442
+ };
2443
+ const owned = {
2444
+ bytes,
2445
+ caller: {
2446
+ status: "waiting",
2447
+ settle: (result) => {
2448
+ signal?.removeEventListener("abort", abort);
2449
+ result.ok ? resolve(result.value) : reject(result.error);
2450
+ }
2451
+ }
2452
+ };
2453
+ pending.set(id, owned);
2454
+ pendingBytes += bytes;
2455
+ signal?.addEventListener("abort", abort, { once: true });
2456
+ try {
2457
+ postWorkerMessage(worker, packet);
2458
+ onSent?.();
2459
+ } catch (cause) {
2460
+ pending.delete(id);
2461
+ pendingBytes -= bytes;
2462
+ settleCaller(owned, {
2463
+ ok: false,
2464
+ error: cause
2465
+ });
2466
+ }
2796
2467
  });
2797
- });
2798
- };
2799
- const createResourceDependencyGraph = (resources) => {
2800
- const resourceIds = Object.keys(resources);
2801
- if (resourceIds.some((resourceId) => !resourceId)) throw new Error("Resource IDs must not be empty");
2802
- const dependencies = emptyGraphFor(resourceIds);
2803
- const dependants = emptyGraphFor(resourceIds);
2804
- Object.entries(resources).forEach(([resourceId, resource]) => {
2805
- if (resource.kind !== "derived") return;
2806
- Object.entries(resource.dependencies).forEach(([dependencyName, dependencyId]) => {
2807
- if (!dependencyName) throw new Error(`Resource ${resourceId} has an empty dependency name`);
2808
- if (!dependencyId) throw new Error(`Resource ${resourceId} has an empty dependency ID`);
2809
- if (!dependencies.has(dependencyId)) throw new Error(`Unknown resource dependency: ${dependencyId}`);
2810
- dependencies.get(resourceId)?.add(dependencyId);
2811
- dependants.get(dependencyId)?.add(resourceId);
2468
+ };
2469
+ const openSession = async (sessionOptions) => {
2470
+ sessionOptions.signal?.throwIfAborted();
2471
+ requireReady();
2472
+ if (sessions.size >= workerLimits.lifetimes) throw new Error("Worker lifetime limit reached");
2473
+ const id = nextId();
2474
+ const session = ownSession({
2475
+ id,
2476
+ options: sessionOptions,
2477
+ postClose: () => postControl({
2478
+ type: "close",
2479
+ sessionId: id
2480
+ }),
2481
+ release: () => sessions.delete(id)
2812
2482
  });
2813
- });
2814
- assertAcyclic(dependencies);
2815
- return Object.freeze({
2816
- dependencies,
2817
- dependants
2818
- });
2819
- };
2820
- const collectInvalidatedResourceIds = (graph, resourceId, invalidated) => {
2821
- if (invalidated.has(resourceId)) return;
2822
- invalidated.add(resourceId);
2823
- graph.dependants.get(resourceId)?.forEach((dependantId) => {
2824
- collectInvalidatedResourceIds(graph, dependantId, invalidated);
2825
- });
2826
- };
2827
- const getInvalidatedResourceIds = (graph, resourceId) => {
2828
- const invalidated = /* @__PURE__ */ new Set();
2829
- collectInvalidatedResourceIds(graph, resourceId, invalidated);
2830
- return invalidated;
2831
- };
2832
- //#endregion
2833
- //#region src/internal/server/runtime/runtime-resources.ts
2834
- const createSubscriptionLifetime = ({ entry, invalidationListeners, lifecycle, signal }) => {
2835
- const controller = new AbortController();
2836
- const subscriptionSignal = AbortSignal.any([lifecycle.requestSignal(signal), controller.signal]);
2837
- subscriptionSignal.throwIfAborted();
2838
- const registeredEntry = {
2839
- ...entry,
2840
- listener: () => {
2841
- if (!subscriptionSignal.aborted) entry.listener();
2483
+ sessions.set(id, session);
2484
+ const { action, name, input, signal, correlation } = sessionOptions;
2485
+ try {
2486
+ await request({
2487
+ action,
2488
+ name,
2489
+ input,
2490
+ correlation,
2491
+ sessionId: id
2492
+ }, signal, session.markSent);
2493
+ signal?.throwIfAborted();
2494
+ session.opened();
2495
+ return session;
2496
+ } catch (cause) {
2497
+ const failure = session.getFailure();
2498
+ session.close();
2499
+ throw failure ?? cause;
2842
2500
  }
2843
2501
  };
2844
- const disposers = [];
2845
- const release = lifecycle.track(async () => {
2846
- invalidationListeners.delete(registeredEntry);
2847
- await lifecycle.disposeAll(disposers);
2848
- }, subscriptionSignal);
2849
- const dispose = () => {
2850
- controller.abort(/* @__PURE__ */ new Error("Resource subscription closed"));
2851
- return release();
2502
+ const handleReady = (message) => {
2503
+ if (state.status !== "starting") throw new Error("Unexpected worker ready message");
2504
+ clearTimeout(state.timer);
2505
+ state = { status: "ready" };
2506
+ startup.resolve(message);
2507
+ };
2508
+ const handleRequestReply = (message) => {
2509
+ const result = message.ok ? {
2510
+ ok: true,
2511
+ value: message.value
2512
+ } : {
2513
+ ok: false,
2514
+ error: deserializeWorkerError(message.error)
2515
+ };
2516
+ const owned = pending.get(message.id);
2517
+ if (!owned) return;
2518
+ pending.delete(message.id);
2519
+ pendingBytes -= owned.bytes;
2520
+ settleCaller(owned, result);
2852
2521
  };
2853
- invalidationListeners.add(registeredEntry);
2854
- return {
2855
- dispose,
2856
- disposers,
2857
- refresh: registeredEntry.listener,
2858
- signal: subscriptionSignal
2859
- };
2860
- };
2861
- const createRuntimeResources = ({ definitions, instance, lifecycle }) => {
2862
- const graph = createResourceDependencyGraph(definitions);
2863
- const invalidationListeners = /* @__PURE__ */ new Set();
2864
- const resources = /* @__PURE__ */ new Map();
2865
- const invalidateResource = (id, input, allInputs = false) => {
2866
- if (lifecycle.signal.aborted) return;
2867
- const ids = getInvalidatedResourceIds(graph, id);
2868
- invalidationListeners.forEach((entry) => {
2869
- if (ids.has(entry.resourceId) && (allInputs || isDeepStrictEqual(entry.input, input))) entry.listener();
2870
- });
2522
+ const handleDelivery = (message) => {
2523
+ try {
2524
+ if (message.type === "reply") handleRequestReply(message);
2525
+ if (message.type === "closed") {
2526
+ const error = message.error === void 0 ? void 0 : deserializeWorkerError(message.error);
2527
+ sessions.get(message.sessionId)?.finishClose(error);
2528
+ }
2529
+ if (message.type === "invalidate") sessions.get(message.sessionId)?.deliver(void 0);
2530
+ if (message.type === "stream-output") sessions.get(message.sessionId)?.deliver(message.message);
2531
+ if (message.type === "stream-error") {
2532
+ const error = deserializeWorkerError(message.error);
2533
+ sessions.get(message.sessionId)?.fail(error);
2534
+ }
2535
+ if (message.type === "log") contain(() => {
2536
+ const entry = JSON.parse(message.entry);
2537
+ options.serverLogger?.log({
2538
+ ...entry,
2539
+ details: {
2540
+ ...entry.details,
2541
+ worker: options.name
2542
+ }
2543
+ });
2544
+ });
2545
+ if (message.type === "notification") deliverNotification(message, options.notifications, postControl);
2546
+ } finally {
2547
+ postControl({
2548
+ type: "ack",
2549
+ deliveryId: message.deliveryId
2550
+ });
2551
+ }
2552
+ };
2553
+ const receive = (packet) => {
2554
+ if (state.status === "stopped") return;
2555
+ try {
2556
+ const message = decodeWorkerMessage(packet);
2557
+ if (message.type === "ready") handleReady(message);
2558
+ else if (message.type === "fatal") terminate(deserializeWorkerError(message.error));
2559
+ else if (message.type === "disposed") {
2560
+ if (state.status !== "stopping") throw new Error("Unexpected worker disposed message");
2561
+ terminate(state.cause);
2562
+ } else handleDelivery(message);
2563
+ } catch (cause) {
2564
+ terminate(asError(cause));
2565
+ }
2871
2566
  };
2872
- const invalidate = (resourceId, input) => {
2873
- if (!resources.has(resourceId)) throw new Error(`Unknown resource ID: ${resourceId}`);
2874
- const definition = definitions[resourceId];
2875
- const parsedInput = input === void 0 ? void 0 : definition.contract.input.parse(input);
2876
- invalidateResource(resourceId, parsedInput, input === void 0);
2567
+ const stop = () => {
2568
+ if (state.status === "stopped" || state.status === "stopping") return state.completion;
2569
+ const cause = /* @__PURE__ */ new Error("Worker runtime disposed");
2570
+ if (state.status === "starting") return terminate(cause);
2571
+ const { promise, resolve } = Promise.withResolvers();
2572
+ state = {
2573
+ status: "stopping",
2574
+ cause,
2575
+ completion: promise,
2576
+ finish: resolve,
2577
+ timer: setTimeout(() => {
2578
+ terminate(cause);
2579
+ }, options.shutdownMs)
2580
+ };
2581
+ pending.forEach((owned) => settleCaller(owned, {
2582
+ ok: false,
2583
+ error: cause
2584
+ }));
2585
+ sessions.forEach((session) => session.fail(cause));
2586
+ postControl({ type: "shutdown" });
2587
+ return promise;
2877
2588
  };
2878
- const context = (signal) => ({
2879
- instance,
2880
- invalidate,
2881
- signal: lifecycle.requestSignal(signal)
2589
+ worker.on("message", receive);
2590
+ worker.on("error", (cause) => {
2591
+ terminate(cause);
2882
2592
  });
2883
- const read = async (id, rawInput, signal) => {
2884
- const definition = definitions[id];
2885
- const operationSignal = lifecycle.requestSignal(signal);
2886
- operationSignal.throwIfAborted();
2887
- const input = definition.contract.input.parse(rawInput);
2888
- const rawOutput = definition.kind === "derived" ? definition.combine(Object.fromEntries(await Promise.all(Object.entries(definition.dependencies).map(async ([name, dependencyId]) => [name, await read(dependencyId, rawInput, signal)])))) : await definition.read(input, context(signal));
2889
- operationSignal.throwIfAborted();
2890
- return definition.contract.output.parse(rawOutput);
2891
- };
2892
- const activateSubscriptions = async (id, rawInput, signal, activated, disposers) => {
2893
- signal.throwIfAborted();
2894
- if (activated.has(id)) return;
2895
- activated.add(id);
2896
- const definition = definitions[id];
2897
- const input = definition.contract.input.parse(rawInput);
2898
- if (definition.kind === "subscription") {
2899
- const dispose = await definition.subscribe(input, () => {
2900
- if (!signal.aborted) invalidateResource(id, input);
2901
- }, context(signal));
2902
- if (typeof dispose !== "function") throw new Error(`Resource ${id} subscribe must return a disposer`);
2903
- if (signal.aborted) {
2904
- await dispose();
2905
- signal.throwIfAborted();
2906
- }
2907
- disposers.push(dispose);
2908
- return;
2909
- }
2910
- if (definition.kind === "derived") for (const dependencyId of graph.dependencies.get(id) ?? []) await activateSubscriptions(dependencyId, rawInput, signal, activated, disposers);
2911
- };
2912
- Object.keys(definitions).forEach((name) => {
2913
- resources.set(name, {
2914
- read: (input, signal) => read(name, input, signal),
2915
- subscribe: async (rawInput, listener, signal) => {
2916
- lifecycle.requestSignal(signal).throwIfAborted();
2917
- const input = definitions[name].contract.input.parse(rawInput);
2918
- const subscription = createSubscriptionLifetime({
2919
- entry: {
2920
- input,
2921
- listener,
2922
- resourceId: name
2923
- },
2924
- invalidationListeners,
2925
- lifecycle,
2926
- signal
2927
- });
2928
- try {
2929
- await activateSubscriptions(name, rawInput, subscription.signal, /* @__PURE__ */ new Set(), subscription.disposers);
2930
- subscription.signal.throwIfAborted();
2931
- subscription.refresh();
2932
- subscription.signal.throwIfAborted();
2933
- return subscription.dispose;
2934
- } catch (cause) {
2935
- try {
2936
- await subscription.dispose();
2937
- } catch (cleanupError) {
2938
- throw new AggregateError([cause, cleanupError], "Resource subscription setup and cleanup failed");
2939
- }
2940
- throw cause;
2941
- }
2942
- }
2943
- });
2593
+ worker.on("exit", (code) => {
2594
+ const phase = state.status === "starting" ? " during startup" : "";
2595
+ terminate(/* @__PURE__ */ new Error(`Worker ${options.name} exited${phase} (${code})`));
2944
2596
  });
2597
+ postControl(options.init);
2945
2598
  return {
2946
- context,
2947
- get: (name) => resources.get(name),
2948
- invalidate,
2949
- names: [...resources.keys()]
2599
+ ready: startup.promise,
2600
+ request,
2601
+ openSession,
2602
+ stop,
2603
+ terminate
2950
2604
  };
2951
2605
  };
2952
2606
  //#endregion
2953
- //#region src/internal/server/runtime/runtime-streams.ts
2954
- const createRuntimeStreams = ({ context, definitions, lifecycle }) => {
2955
- const streams = /* @__PURE__ */ new Map();
2956
- Object.entries(definitions).forEach(([name, definition]) => {
2957
- if (!name) throw new Error("Stream names must not be empty");
2958
- streams.set(name, { open: async (rawInput, emit, requestSignal, onError) => {
2959
- const sessionController = new AbortController();
2960
- const sessionSignal = requestSignal ? AbortSignal.any([requestSignal, sessionController.signal]) : sessionController.signal;
2961
- const operationSignal = lifecycle.requestSignal(sessionSignal);
2962
- operationSignal.throwIfAborted();
2963
- const input = definition.contract.input.parse(rawInput);
2964
- let acceptingMessages = true;
2965
- let failed = false;
2966
- let failure;
2967
- let opened = false;
2968
- const fail = (cause) => {
2969
- if (failed || operationSignal.aborted) return;
2970
- failed = true;
2971
- failure = cause;
2972
- acceptingMessages = false;
2973
- sessionController.abort(cause);
2974
- if (opened) onError?.(cause);
2975
- };
2976
- const session = await definition.open(input, {
2977
- ...context(sessionSignal),
2978
- emit: (message) => {
2979
- if (operationSignal.aborted) return;
2980
- try {
2981
- emit(definition.contract.serverMessage.parse(message));
2982
- } catch (cause) {
2983
- fail(cause);
2984
- }
2985
- },
2986
- fail,
2987
- signal: operationSignal
2988
- });
2989
- opened = true;
2990
- if (failed || operationSignal.aborted) {
2991
- await session.dispose?.();
2992
- operationSignal.throwIfAborted();
2993
- throw failure;
2994
- }
2995
- let messages = Promise.resolve();
2996
- const dispose = lifecycle.track(async () => {
2997
- acceptingMessages = false;
2998
- sessionController.abort(/* @__PURE__ */ new Error("Stream session disposed"));
2999
- await messages.catch(() => void 0);
3000
- await session.dispose?.();
3001
- }, requestSignal);
3002
- return {
3003
- dispose: async () => dispose(),
3004
- send: async (rawMessage) => {
3005
- if (!acceptingMessages) throw new Error("Stream session is closed");
3006
- const message = definition.contract.clientMessage.parse(rawMessage);
3007
- messages = messages.then(async () => {
3008
- operationSignal.throwIfAborted();
3009
- await session.onMessage?.(message);
3010
- operationSignal.throwIfAborted();
3011
- });
3012
- return messages;
3013
- }
3014
- };
3015
- } });
2607
+ //#region src/internal/server/runtime/worker-capabilities.ts
2608
+ const createWorkerResource = (connection, name) => ({
2609
+ read: (input, signal, correlation) => connection.request({
2610
+ action: "resource-read",
2611
+ input,
2612
+ name,
2613
+ correlation
2614
+ }, signal),
2615
+ subscribe: async (input, listener, signal, onError, correlation) => {
2616
+ return (await connection.openSession({
2617
+ action: "resource-subscribe",
2618
+ name,
2619
+ input,
2620
+ signal,
2621
+ onEvent: listener,
2622
+ onError,
2623
+ correlation
2624
+ })).close;
2625
+ }
2626
+ });
2627
+ const createWorkerOperation = (connection, name, returnsVoid) => ({
2628
+ execute: (input, signal, correlation) => connection.request({
2629
+ action: "operation",
2630
+ input,
2631
+ name,
2632
+ correlation
2633
+ }, signal),
2634
+ returnsVoid
2635
+ });
2636
+ const createWorkerStream = (connection, name) => ({ open: async (input, emit, signal, onError, correlation) => {
2637
+ const session = await connection.openSession({
2638
+ action: "stream-open",
2639
+ name,
2640
+ input,
2641
+ signal,
2642
+ onEvent: emit,
2643
+ onError,
2644
+ correlation
2645
+ });
2646
+ return {
2647
+ dispose: session.close,
2648
+ send: async (message) => {
2649
+ session.assertOpen();
2650
+ await connection.request({
2651
+ action: "stream-message",
2652
+ message,
2653
+ sessionId: session.id
2654
+ }, signal);
2655
+ }
2656
+ };
2657
+ } });
2658
+ //#endregion
2659
+ //#region src/internal/server/runtime/worker-runtime.ts
2660
+ const silentNotifications = { send: async () => void 0 };
2661
+ const resolveOptions = (options) => ({
2662
+ ...options,
2663
+ notifications: options.notifications ?? silentNotifications,
2664
+ aliases: options.aliases ?? {},
2665
+ startupMs: options.startupMs ?? workerLimits.startupMs,
2666
+ shutdownMs: options.shutdownMs ?? workerLimits.shutdownMs
2667
+ });
2668
+ const launchWorker = (selection, options, workers) => {
2669
+ const connection = createWorkerConnection({
2670
+ name: selection.name,
2671
+ init: {
2672
+ type: "init",
2673
+ configPath: options.configPath,
2674
+ aliases: options.aliases,
2675
+ logLevel: options.serverLogger?.logLevel,
2676
+ role: selection.role,
2677
+ stream: selection.stream
2678
+ },
2679
+ notifications: options.notifications,
2680
+ serverLogger: options.serverLogger,
2681
+ startupMs: options.startupMs,
2682
+ shutdownMs: options.shutdownMs
3016
2683
  });
2684
+ workers.push(connection);
2685
+ return connection;
2686
+ };
2687
+ const loadWorkerConfig = async (options) => {
2688
+ const workers = [];
2689
+ try {
2690
+ return { config: (await launchWorker({
2691
+ role: "metadata",
2692
+ name: "config"
2693
+ }, resolveOptions(options), workers).ready).settings };
2694
+ } finally {
2695
+ await Promise.all(workers.map((worker) => worker.terminate(/* @__PURE__ */ new Error("Config inspection finished"))));
2696
+ }
2697
+ };
2698
+ const startStreams = async (ready, options, workers) => {
2699
+ const streams = /* @__PURE__ */ new Map();
2700
+ const names = ready.manifest.streams;
2701
+ for (let offset = 0; offset < names.length; offset += 4) await Promise.all(names.slice(offset, offset + 4).map(async (name) => {
2702
+ const owner = launchWorker({
2703
+ role: "stream",
2704
+ stream: name,
2705
+ name: `stream:${name}`
2706
+ }, options, workers);
2707
+ const streamReady = await owner.ready;
2708
+ if (JSON.stringify(streamReady.manifest) !== JSON.stringify(ready.manifest)) throw new Error("Server definitions changed between worker imports");
2709
+ streams.set(name, owner);
2710
+ }));
2711
+ return streams;
2712
+ };
2713
+ const createWorkerIdentity = ({ application, streams, startupMs }) => {
2714
+ let instance = createRuntimeInstance(defaultInstanceId);
3017
2715
  return {
3018
- get: (name) => streams.get(name),
3019
- names: [...streams.keys()]
2716
+ instance: {
2717
+ getInstanceId: () => instance.context.getInstanceId(),
2718
+ getDeepLinkPrefix: () => instance.context.getDeepLinkPrefix()
2719
+ },
2720
+ establishInstance: async (port) => {
2721
+ const signal = AbortSignal.timeout(startupMs);
2722
+ const identity = await application.request({
2723
+ action: "initialize-instance-identity",
2724
+ port
2725
+ }, signal);
2726
+ await Promise.all([...streams.values()].map((stream) => stream.request({
2727
+ action: "initialize-instance-identity",
2728
+ port,
2729
+ instanceId: identity.instanceId
2730
+ }, signal)));
2731
+ instance = createRuntimeInstance(identity.instanceId);
2732
+ return instance.establish(port);
2733
+ }
3020
2734
  };
3021
2735
  };
3022
- //#endregion
3023
- //#region src/internal/server/runtime/create-runtime.ts
3024
- const createRuntime = async ({ config, debug = true, notifications = { send: async () => void 0 }, signal }) => {
3025
- const lifecycle = createRuntimeLifecycle(signal);
3026
- const instance = createRuntimeInstance(config.instanceId ?? defaultInstanceId);
2736
+ const createWorkerRuntime = async (input) => {
2737
+ const options = resolveOptions(input);
2738
+ const workers = [];
2739
+ let disposal;
3027
2740
  try {
3028
- const resources = createRuntimeResources({
3029
- definitions: config.server.resources ?? {},
3030
- instance: instance.context,
3031
- lifecycle
3032
- });
3033
- const context = resources.context;
3034
- const operations = createRuntimeOperations({
3035
- context,
3036
- definitions: config.server.operations ?? {},
3037
- notifications
3038
- });
3039
- const streams = createRuntimeStreams({
3040
- context,
3041
- definitions: config.server.streams ?? {},
3042
- lifecycle
3043
- });
3044
- lifecycle.signal.throwIfAborted();
3045
- lifecycle.completePreparation();
3046
- const manifest = {
3047
- debug,
3048
- operations: [...operations.names],
3049
- protocolVersion: 10,
3050
- resources: [...resources.names],
3051
- streams: [...streams.names]
3052
- };
2741
+ const application = launchWorker({
2742
+ role: "application",
2743
+ name: "resources/operations"
2744
+ }, options, workers);
2745
+ const ready = await application.ready;
2746
+ const streams = await startStreams(ready, options, workers);
2747
+ const resources = new Map(ready.manifest.resources.map((name) => [name, createWorkerResource(application, name)]));
2748
+ const operations = new Map(ready.manifest.operations.map((name) => [name, createWorkerOperation(application, name, ready.voidOperations.includes(name))]));
3053
2749
  return {
3054
- instance: instance.context,
3055
- establishInstance: instance.establish,
3056
- manifest,
3057
- dispose: lifecycle.dispose,
3058
- getOperation: operations.get,
3059
- getResource: resources.get,
3060
- getStream: streams.get
2750
+ settings: ready.settings,
2751
+ ...createWorkerIdentity({
2752
+ application,
2753
+ streams,
2754
+ startupMs: options.startupMs
2755
+ }),
2756
+ manifest: ready.manifest,
2757
+ dispose: () => {
2758
+ disposal = disposal ?? Promise.all(workers.map((worker) => worker.stop())).then(() => void 0);
2759
+ return disposal;
2760
+ },
2761
+ getOperation: (name) => operations.get(name),
2762
+ getResource: (name) => resources.get(name),
2763
+ getStream: (name) => {
2764
+ const owner = streams.get(name);
2765
+ return owner ? createWorkerStream(owner, name) : void 0;
2766
+ }
3061
2767
  };
3062
2768
  } catch (cause) {
3063
- await lifecycle.dispose(cause).catch(() => void 0);
2769
+ const error = cause instanceof Error ? cause : new Error(String(cause));
2770
+ await Promise.all(workers.map((worker) => worker.terminate(error)));
3064
2771
  throw cause;
3065
2772
  }
3066
2773
  };
3067
2774
  //#endregion
3068
2775
  //#region src/internal/server/server-startup-options.ts
3069
2776
  const serverStartupOverridesRuntimeSchema = serverConfigRuntimeSchema.omit({ port: true }).extend({
3070
- debug: z.boolean().optional(),
2777
+ logLevel: logLevelSchema.optional(),
3071
2778
  port: z.number().int().min(0).max(65535).optional()
3072
2779
  });
3073
2780
  const resolveServerStartupOptions = ({ config, configPath, overrides = {} }) => {
@@ -3075,7 +2782,7 @@ const resolveServerStartupOptions = ({ config, configPath, overrides = {} }) =>
3075
2782
  const configuredWebAssetsDir = config.productionWebAssetsDir ? resolve(dirname(configPath), config.productionWebAssetsDir) : void 0;
3076
2783
  const productionWebAssetsDir = validatedOverrides.productionWebAssetsDir ? resolve(validatedOverrides.productionWebAssetsDir) : configuredWebAssetsDir;
3077
2784
  return {
3078
- debug: validatedOverrides.debug ?? config.debug ?? true,
2785
+ logLevel: validatedOverrides.logLevel ?? config.logLevel ?? "info",
3079
2786
  host: validatedOverrides.host ?? config.host ?? "localhost",
3080
2787
  port: validatedOverrides.port ?? config.port ?? 4242,
3081
2788
  ...productionWebAssetsDir === void 0 ? {} : { productionWebAssetsDir },
@@ -3137,7 +2844,7 @@ const registerProcessErrorLogging = (logger) => {
3137
2844
  };
3138
2845
  };
3139
2846
  const loadServerSettings = async ({ configPath, options }) => {
3140
- const { config } = await loadOvermuxConfig({
2847
+ const { config } = await loadWorkerConfig({
3141
2848
  aliases: options.configAliases,
3142
2849
  configPath
3143
2850
  });
@@ -3161,7 +2868,7 @@ const createAuthOrigins = ({ configuredOrigins, startup }) => {
3161
2868
  get: () => values
3162
2869
  };
3163
2870
  };
3164
- const createApplicationServices = async ({ config, logger, resources, startup }) => {
2871
+ const createApplicationServices = async ({ aliases, config, configPath, logger, resources, startup }) => {
3165
2872
  const authOrigins = createAuthOrigins({
3166
2873
  configuredOrigins: config.auth.origins,
3167
2874
  startup
@@ -3190,10 +2897,11 @@ const createApplicationServices = async ({ config, logger, resources, startup })
3190
2897
  backgroundNotifications,
3191
2898
  logger
3192
2899
  });
3193
- const runtime = await createRuntime({
3194
- config,
3195
- debug: logger.enabled,
3196
- notifications
2900
+ const runtime = await createWorkerRuntime({
2901
+ configPath,
2902
+ serverLogger: logger,
2903
+ notifications,
2904
+ aliases
3197
2905
  });
3198
2906
  resources.add(runtime.dispose);
3199
2907
  return {
@@ -3227,7 +2935,7 @@ const startApplicationTransport = async ({ auth, authOrigins, authService, backg
3227
2935
  });
3228
2936
  resources.add(() => closeNodeServer(server, runtime.dispose));
3229
2937
  const address = await getServerAddress(server);
3230
- runtime.establishInstance(address.port);
2938
+ await runtime.establishInstance(address.port);
3231
2939
  const urlHost = startup.host.includes(":") ? `[${startup.host}]` : startup.host;
3232
2940
  const localUrl = `http://${urlHost}:${address.port}`;
3233
2941
  const controlApiHost = startup.host === "0.0.0.0" ? "127.0.0.1" : startup.host === "::" ? "[::1]" : urlHost;
@@ -3273,25 +2981,26 @@ const failApplicationServerStartup = async (cause, resources) => {
3273
2981
  throw cause;
3274
2982
  };
3275
2983
  const startApplicationServer = async ({ onRestartRequested, options }) => {
2984
+ const configPath = resolve(options.configPath);
2985
+ const { config, startup } = await loadServerSettings({
2986
+ configPath,
2987
+ options
2988
+ });
3276
2989
  const logger = createServerLogger({
3277
- enabled: options.debug ?? true,
2990
+ logLevel: startup.logLevel,
3278
2991
  logFile: getServerLogFile()
3279
2992
  });
3280
2993
  const resources = createResources();
3281
2994
  resources.add(registerProcessErrorLogging(logger));
3282
2995
  try {
3283
- const configPath = resolve(options.configPath);
3284
2996
  logger.log({
3285
2997
  details: { configPath },
3286
2998
  event: "server-start"
3287
2999
  });
3288
- const { config, startup } = await loadServerSettings({
3289
- configPath,
3290
- options
3291
- });
3292
- logger.setEnabled(startup.debug);
3293
3000
  const { auth, authOrigins, authService, backgroundNotifications, notifications, runtime } = await createApplicationServices({
3001
+ aliases: options.configAliases,
3294
3002
  config,
3003
+ configPath,
3295
3004
  logger,
3296
3005
  resources,
3297
3006
  startup
@@ -3330,7 +3039,7 @@ const startApplicationServer = async ({ onRestartRequested, options }) => {
3330
3039
  const childOptionsSchema = z.object({
3331
3040
  configAliases: z.record(z.string(), z.string()).optional(),
3332
3041
  configPath: z.string(),
3333
- debug: z.boolean().optional(),
3042
+ logLevel: logLevelSchema.optional(),
3334
3043
  developmentWebTarget: z.url().optional(),
3335
3044
  host: z.string().optional(),
3336
3045
  port: z.number().int().min(0).max(65535).optional(),