overmux 0.0.10 → 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 (78) hide show
  1. package/CHANGELOG.md +22 -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 +54 -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/400-notifications.md +4 -0
  10. package/dist/docs/400-reference/500-server/500-api.md +26 -5
  11. package/dist/exports/client.d.ts +1 -1
  12. package/dist/exports/client.d.ts.map +1 -1
  13. package/dist/exports/client.js +55 -14
  14. package/dist/exports/client.js.map +1 -1
  15. package/dist/exports/{index-Dk11nKIv.d.ts → index-C-8Kzrnc.d.ts} +23 -8
  16. package/dist/exports/index-C-8Kzrnc.d.ts.map +1 -0
  17. package/dist/exports/index.d.ts +2 -2
  18. package/dist/exports/index.js +1 -1
  19. package/dist/exports/index.js.map +1 -1
  20. package/dist/exports/{notifications-BFAD3QQl.js → notifications-BBKvKZgI.js} +8 -2
  21. package/dist/exports/{notifications-BFAD3QQl.js.map → notifications-BBKvKZgI.js.map} +1 -1
  22. package/dist/exports/server.d.ts.map +1 -1
  23. package/dist/exports/server.js +8 -2
  24. package/dist/exports/server.js.map +1 -1
  25. package/dist/internal/server/coordinator/server-child.js +616 -883
  26. package/dist/internal/server/coordinator/server-child.js.map +1 -1
  27. package/dist/internal/server/worker-entry.d.ts +1 -0
  28. package/dist/internal/server/worker-entry.js +728 -0
  29. package/dist/internal/server/worker-entry.js.map +1 -0
  30. package/dist/push/THIRD_PARTY_NOTICES.md +30 -0
  31. package/dist/push/badge.png +0 -0
  32. package/dist/push/icon.png +0 -0
  33. package/dist/push/sw.js +88 -0
  34. package/dist/push/sw.js.map +1 -0
  35. package/dist/worker-protocol-hXs4FXCH.js +638 -0
  36. package/dist/worker-protocol-hXs4FXCH.js.map +1 -0
  37. package/docs/400-reference/200-configuration.md +54 -4
  38. package/docs/400-reference/500-server/000-index.md +4 -0
  39. package/docs/400-reference/500-server/100-resources.md +5 -0
  40. package/docs/400-reference/500-server/200-operations.md +1 -0
  41. package/docs/400-reference/500-server/300-streams.md +7 -1
  42. package/docs/400-reference/500-server/400-notifications.md +4 -0
  43. package/docs/400-reference/500-server/500-api.md +26 -5
  44. package/package.json +4 -3
  45. package/src/internal/client/background-notifications.ts +3 -1
  46. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +8 -3
  47. package/src/internal/client/host/overmux-host.tsx +4 -8
  48. package/src/internal/client/push-assets/badge.png +0 -0
  49. package/src/internal/client/push-assets/icon.png +0 -0
  50. package/src/internal/client/push-service-worker.ts +118 -0
  51. package/src/internal/client/register-push-worker.ts +63 -0
  52. package/src/internal/server/coordinator/ipc-protocol.ts +2 -1
  53. package/src/internal/server/http/operation-handler.ts +3 -0
  54. package/src/internal/server/http/request-logging.ts +1 -1
  55. package/src/internal/server/http/routes.ts +47 -2
  56. package/src/internal/server/protocol-error.ts +19 -0
  57. package/src/internal/server/runtime/create-runtime.ts +11 -5
  58. package/src/internal/server/runtime/handler-logger.ts +54 -0
  59. package/src/internal/server/runtime/runtime-operations.ts +23 -5
  60. package/src/internal/server/runtime/runtime-resources.ts +158 -88
  61. package/src/internal/server/runtime/runtime-streams.ts +24 -8
  62. package/src/internal/server/runtime/worker-capabilities.ts +67 -0
  63. package/src/internal/server/runtime/worker-connection.ts +549 -0
  64. package/src/internal/server/runtime/worker-protocol.ts +173 -0
  65. package/src/internal/server/runtime/worker-runtime.ts +214 -0
  66. package/src/internal/server/server-logger.ts +25 -37
  67. package/src/internal/server/server-startup-options.ts +7 -6
  68. package/src/internal/server/start-application-server.ts +22 -18
  69. package/src/internal/server/start-overmux-server.ts +2 -2
  70. package/src/internal/server/websocket-connection.ts +97 -32
  71. package/src/internal/server/websocket-server.ts +5 -1
  72. package/src/internal/server/worker-entry.ts +504 -0
  73. package/src/internal/shared/protocol.ts +3 -3
  74. package/src/internal/shared/routes.ts +2 -0
  75. package/src/public/config.ts +15 -23
  76. package/src/public/handler-logger.ts +7 -0
  77. package/src/public/index.ts +3 -1
  78. 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 { 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
  };
@@ -1574,6 +1107,8 @@ const createRuntimeManifestHandler = ({ runtime }) => (context) => {
1574
1107
  };
1575
1108
  //#endregion
1576
1109
  //#region src/internal/shared/routes.ts
1110
+ const pushNotificationIconPath = "/_overmux/push/icon.png";
1111
+ const pushNotificationBadgePath = "/_overmux/push/badge.png";
1577
1112
  const runtimeWebSocketPath = "/api/socket";
1578
1113
  const viteInfrastructurePath = "/_overmux/vite/*";
1579
1114
  //#endregion
@@ -1608,6 +1143,17 @@ const registerHttpRoutes = ({ app, auth, authAssetsDirectory, backgroundNotifica
1608
1143
  path: "*",
1609
1144
  handler: createRequestLoggingMiddleware({ serverLogger })
1610
1145
  },
1146
+ ...[pushNotificationIconPath, pushNotificationBadgePath].map((path) => ({
1147
+ method: "GET",
1148
+ path,
1149
+ handler: async (context) => {
1150
+ const asset = await readFile(fileURLToPath(new URL(`../push/${basename(path)}`, import.meta.resolve("overmux"))));
1151
+ context.header("Cache-Control", "public, max-age=86400");
1152
+ context.header("Content-Type", "image/png");
1153
+ context.header("X-Content-Type-Options", "nosniff");
1154
+ return context.body(new Uint8Array(asset));
1155
+ }
1156
+ })),
1611
1157
  {
1612
1158
  method: "GET",
1613
1159
  path: "/_overmux/auth-shell/*",
@@ -1639,6 +1185,17 @@ const registerHttpRoutes = ({ app, auth, authAssetsDirectory, backgroundNotifica
1639
1185
  path: "*",
1640
1186
  handler: authentication.authenticate
1641
1187
  },
1188
+ {
1189
+ method: "GET",
1190
+ path: "/sw.js",
1191
+ handler: async (context) => {
1192
+ const script = await readFile(fileURLToPath(new URL("../push/sw.js", import.meta.resolve("overmux"))), "utf8");
1193
+ context.header("Cache-Control", "no-cache");
1194
+ context.header("Content-Type", "text/javascript; charset=UTF-8");
1195
+ context.header("X-Content-Type-Options", "nosniff");
1196
+ return context.body(script);
1197
+ }
1198
+ },
1642
1199
  {
1643
1200
  method: "POST",
1644
1201
  path: "/api/auth/logout",
@@ -1746,13 +1303,6 @@ const createHttpApp = ({ auth, authAssetsDirectory, backgroundNotifications, dev
1746
1303
  //#endregion
1747
1304
  //#region src/internal/server/websocket-connection.ts
1748
1305
  const maxPendingStreamOutputBytes = 1048576;
1749
- var OperationError = class extends Error {
1750
- code;
1751
- constructor(code, message) {
1752
- super(message);
1753
- this.code = code;
1754
- }
1755
- };
1756
1306
  const bytesOf = (data) => {
1757
1307
  if (Array.isArray(data)) return Buffer.concat(data);
1758
1308
  if (data instanceof ArrayBuffer) return new Uint8Array(data);
@@ -1769,7 +1319,7 @@ const operationIdOf = (value) => {
1769
1319
  return typeof operationId === "string" && operationId.length <= 256 ? operationId : void 0;
1770
1320
  };
1771
1321
  const errorDetails = (cause) => {
1772
- if (cause instanceof OperationError) return {
1322
+ if (cause instanceof ProtocolError) return {
1773
1323
  code: cause.code,
1774
1324
  message: cause.message
1775
1325
  };
@@ -1809,6 +1359,14 @@ const sendError = (state, cause, operationId, identity) => {
1809
1359
  type: "error"
1810
1360
  });
1811
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
+ };
1812
1370
  const disposeSubscription = async (state, subscriptionId) => {
1813
1371
  const entry = state.subscriptions.get(subscriptionId);
1814
1372
  if (!entry) return;
@@ -1827,11 +1385,11 @@ const disposeStream = async (state, streamId, notify = true) => {
1827
1385
  }
1828
1386
  state.streams.delete(streamId);
1829
1387
  entry.controller.abort(/* @__PURE__ */ new Error("Stream closed"));
1830
- await entry.session?.dispose();
1831
1388
  if (notify) send$1(state, {
1832
1389
  streamId,
1833
1390
  type: "stream-closed"
1834
1391
  });
1392
+ await entry.session?.dispose();
1835
1393
  };
1836
1394
  const disposeConnectionOperations = async (state, notify) => {
1837
1395
  const operationControllers = [...state.operations.values()];
@@ -1843,7 +1401,7 @@ const disposeConnectionOperations = async (state, notify) => {
1843
1401
  };
1844
1402
  const runOperation = (state, identity, operation) => {
1845
1403
  if (state.operations.has(identity.operationId)) {
1846
- 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);
1847
1405
  return;
1848
1406
  }
1849
1407
  const controller = new AbortController();
@@ -1852,14 +1410,16 @@ const runOperation = (state, identity, operation) => {
1852
1410
  state.serverLogger?.log({
1853
1411
  correlationId: identity.operationId,
1854
1412
  details: identity,
1855
- event: "operation-start"
1413
+ event: "operation-start",
1414
+ level: "debug"
1856
1415
  });
1857
1416
  operation(controller.signal).then(() => {
1858
1417
  state.serverLogger?.log({
1859
1418
  correlationId: identity.operationId,
1860
1419
  details: identity,
1861
1420
  durationMs: Date.now() - startedAt,
1862
- event: "operation-complete"
1421
+ event: "operation-complete",
1422
+ level: "debug"
1863
1423
  });
1864
1424
  }).catch((cause) => {
1865
1425
  if (!controller.signal.aborted && !state.closed) {
@@ -1887,8 +1447,11 @@ const readResource = (state, message) => {
1887
1447
  requestedName: message.resourceName
1888
1448
  };
1889
1449
  runOperation(state, identity, async (signal) => {
1890
- if (!resource) throw new OperationError("not-found", "Resource not found");
1891
- 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
+ });
1892
1455
  send$1(state, {
1893
1456
  operationId: message.operationId,
1894
1457
  output,
@@ -1904,20 +1467,31 @@ const subscribeResource = (state, message) => {
1904
1467
  requestedName: message.resourceName
1905
1468
  };
1906
1469
  if (state.subscriptions.has(message.subscriptionId)) {
1907
- 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);
1908
1471
  return;
1909
1472
  }
1910
1473
  runOperation(state, identity, async (operationSignal) => {
1911
- if (!resource) throw new OperationError("not-found", "Resource not found");
1474
+ if (!resource) throw new ProtocolError("not-found", "Resource not found");
1912
1475
  const controller = new AbortController();
1913
1476
  const entry = { controller };
1914
1477
  state.subscriptions.set(message.subscriptionId, entry);
1915
1478
  const signal = AbortSignal.any([operationSignal, controller.signal]);
1916
1479
  try {
1917
- const dispose = await resource.subscribe(message.input, () => void send$1(state, {
1918
- subscriptionId: message.subscriptionId,
1919
- type: "resource-invalidated"
1920
- }), 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
+ });
1921
1495
  if (state.subscriptions.get(message.subscriptionId) === entry) entry.dispose = dispose;
1922
1496
  else await dispose();
1923
1497
  } catch (cause) {
@@ -1946,11 +1520,11 @@ const openStream = (state, message) => {
1946
1520
  requestedName: message.streamName
1947
1521
  };
1948
1522
  if (state.streams.has(message.streamId)) {
1949
- 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);
1950
1524
  return;
1951
1525
  }
1952
1526
  runOperation(state, identity, async (operationSignal) => {
1953
- if (!stream) throw new OperationError("not-found", "Stream not found");
1527
+ if (!stream) throw new ProtocolError("not-found", "Stream not found");
1954
1528
  const controller = new AbortController();
1955
1529
  const entry = {
1956
1530
  controller,
@@ -1960,8 +1534,10 @@ const openStream = (state, message) => {
1960
1534
  const pending = [];
1961
1535
  let pendingBytes = 0;
1962
1536
  let opened = false;
1537
+ const signal = AbortSignal.any([operationSignal, controller.signal]);
1963
1538
  try {
1964
- 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;
1965
1541
  if (opened) {
1966
1542
  if (!send$1(state, {
1967
1543
  message: output,
@@ -1973,12 +1549,22 @@ const openStream = (state, message) => {
1973
1549
  pendingBytes += frameLength(output);
1974
1550
  if (pendingBytes > maxPendingStreamOutputBytes) throw new Error("Stream produced too much output while opening");
1975
1551
  pending.push(output);
1976
- }, AbortSignal.any([operationSignal, controller.signal]), (cause) => {
1552
+ }, signal, (cause) => {
1553
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) return;
1977
1554
  sendError(state, cause, message.streamId, identity);
1978
1555
  disposeStream(state, message.streamId).catch((disposeCause) => {
1979
- sendError(state, disposeCause, message.streamId, identity);
1556
+ reportCleanupFailure(state, disposeCause);
1980
1557
  });
1558
+ }, {
1559
+ correlationId: message.operationId,
1560
+ connectionId: state.connectionId,
1561
+ streamId: message.streamId
1981
1562
  });
1563
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) {
1564
+ await session.dispose();
1565
+ return;
1566
+ }
1567
+ entry.session = session;
1982
1568
  send$1(state, {
1983
1569
  operationId: message.operationId,
1984
1570
  streamId: message.streamId,
@@ -1991,6 +1577,7 @@ const openStream = (state, message) => {
1991
1577
  type: "stream-output"
1992
1578
  })) throw new Error("Stream output could not be delivered");
1993
1579
  } catch (cause) {
1580
+ if (signal.aborted || state.streams.get(message.streamId) !== entry) return;
1994
1581
  await disposeStream(state, message.streamId, false);
1995
1582
  throw cause;
1996
1583
  }
@@ -2004,18 +1591,19 @@ const sendStreamMessage = (state, message) => {
2004
1591
  operationType: message.type
2005
1592
  };
2006
1593
  if (!entry?.session) {
2007
- 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);
2008
1595
  return;
2009
1596
  }
2010
1597
  entry.session.send(message.message).catch((cause) => {
1598
+ if (entry.controller.signal.aborted || state.streams.get(message.streamId) !== entry) return;
2011
1599
  sendError(state, cause, message.streamId, identity);
2012
1600
  disposeStream(state, message.streamId).catch((disposeCause) => {
2013
- sendError(state, disposeCause, message.streamId, identity);
1601
+ reportCleanupFailure(state, disposeCause);
2014
1602
  });
2015
1603
  });
2016
1604
  };
2017
1605
  const logClientDiagnostic = (state, message) => {
2018
- if (!state.serverLogger?.enabled) return;
1606
+ if (state.serverLogger?.logLevel !== "debug") return;
2019
1607
  state.serverLogger.log({
2020
1608
  details: {
2021
1609
  arguments: message.arguments,
@@ -2044,7 +1632,7 @@ const handleMessage = (state, message) => {
2044
1632
  }
2045
1633
  if (message.type === "resource-unsubscribe") {
2046
1634
  disposeSubscription(state, message.subscriptionId).catch((cause) => {
2047
- sendError(state, cause, message.subscriptionId);
1635
+ reportCleanupFailure(state, cause);
2048
1636
  });
2049
1637
  return;
2050
1638
  }
@@ -2054,7 +1642,7 @@ const handleMessage = (state, message) => {
2054
1642
  }
2055
1643
  if (message.type === "stream-close") {
2056
1644
  disposeStream(state, message.streamId).catch((cause) => {
2057
- sendError(state, cause, message.streamId);
1645
+ reportCleanupFailure(state, cause);
2058
1646
  });
2059
1647
  return;
2060
1648
  }
@@ -2091,7 +1679,8 @@ const disposeConnection = async (state) => {
2091
1679
  state.closed = true;
2092
1680
  state.serverLogger?.log({
2093
1681
  correlationId: state.connectionId,
2094
- event: "websocket-close"
1682
+ event: "websocket-close",
1683
+ level: "debug"
2095
1684
  });
2096
1685
  await disposeConnectionOperations(state, false);
2097
1686
  };
@@ -2113,7 +1702,8 @@ const attachWebSocketConnection = ({ runtime, serverLogger, socket }) => {
2113
1702
  });
2114
1703
  serverLogger?.log({
2115
1704
  correlationId: state.connectionId,
2116
- event: "websocket-open"
1705
+ event: "websocket-open",
1706
+ level: "debug"
2117
1707
  });
2118
1708
  socket.on("message", (data, isBinary) => handleFrame(state, data, isBinary));
2119
1709
  socket.once("close", (code, reason) => {
@@ -2123,7 +1713,8 @@ const attachWebSocketConnection = ({ runtime, serverLogger, socket }) => {
2123
1713
  code,
2124
1714
  reason: reason?.toString() ?? ""
2125
1715
  },
2126
- event: "websocket-close-event"
1716
+ event: "websocket-close-event",
1717
+ level: "debug"
2127
1718
  });
2128
1719
  disposeConnection(state);
2129
1720
  });
@@ -2312,7 +1903,8 @@ const createWebSocketServer = ({ auth, developmentWebTarget, runtime, server, se
2312
1903
  const correlationId = serverLogger?.id();
2313
1904
  serverLogger?.log({
2314
1905
  correlationId,
2315
- event: "websocket-connect"
1906
+ event: "websocket-connect",
1907
+ level: "debug"
2316
1908
  });
2317
1909
  const path = requestPath(request);
2318
1910
  const isRuntimeSocket = path === runtimeWebSocketPath;
@@ -2615,15 +2207,6 @@ const createFileBackgroundNotificationStore = ({ environment = process.env, home
2615
2207
  }
2616
2208
  };
2617
2209
  };
2618
- z.void();
2619
- const jsonValueSchema = z.lazy(() => z.union([
2620
- z.string(),
2621
- z.number().finite(),
2622
- z.boolean(),
2623
- z.null(),
2624
- z.array(jsonValueSchema),
2625
- z.record(z.string(), jsonValueSchema)
2626
- ]));
2627
2210
  //#endregion
2628
2211
  //#region src/internal/server/notifications/notification-service.ts
2629
2212
  const deliverSafely = async ({ deliver, event, logger }) => {
@@ -2661,389 +2244,537 @@ const createNotificationService = ({ backgroundNotifications, logger }) => {
2661
2244
  };
2662
2245
  };
2663
2246
  //#endregion
2664
- //#region src/internal/server/runtime/runtime-instance.ts
2665
- const defaultInstanceId = ({ port }) => `${hostname()}-${port}`;
2666
- const createRuntimeInstance = (configured) => {
2667
- let identity;
2668
- const get = () => {
2669
- if (!identity) throw new Error("Overmux instance identity is not ready");
2670
- 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();
2671
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();
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 });
2672
2334
  return {
2673
- context: {
2674
- getInstanceId: () => get().instanceId,
2675
- 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
+ };
2346
+ },
2347
+ opened: () => {
2348
+ if (state.status === "opening") state = { status: "open" };
2349
+ assertOpen();
2676
2350
  },
2677
- establish: (port) => {
2678
- if (identity) throw new Error("Overmux instance identity is already established");
2351
+ deliver: (value) => {
2352
+ if (state.status !== "opening" && state.status !== "open") return;
2679
2353
  try {
2680
- identity = createInstanceIdentity(typeof configured === "function" ? configured({ port }) : configured);
2354
+ options.onEvent(value);
2681
2355
  } catch (cause) {
2682
- throw new Error("Invalid Overmux instance ID; configure instanceId with a canonical lowercase ID", { cause });
2356
+ fail(asError(cause));
2683
2357
  }
2684
- return identity;
2358
+ },
2359
+ terminate: (cause) => {
2360
+ if (state.status === "closed") return;
2361
+ if (state.status === "closing") {
2362
+ finishClose(cause);
2363
+ return;
2364
+ }
2365
+ detach();
2366
+ state = {
2367
+ status: "closed",
2368
+ completion: Promise.resolve(),
2369
+ failure: cause
2370
+ };
2371
+ release();
2372
+ contain(() => options.onError?.(cause));
2685
2373
  }
2686
2374
  };
2687
2375
  };
2688
- //#endregion
2689
- //#region src/internal/server/runtime/runtime-lifecycle.ts
2690
- const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
2691
- const disposeAll = async (disposers) => {
2692
- const errors = (await Promise.allSettled([...disposers].map(async (dispose) => dispose()))).flatMap((result) => result.status === "rejected" ? [asError(result.reason)] : []);
2693
- if (errors.length) throw new AggregateError(errors, "Runtime disposal failed");
2694
- };
2695
- const createRuntimeLifecycle = (preparationSignal) => {
2696
- const controller = new AbortController();
2697
- const activeDisposers = /* @__PURE__ */ new Set();
2698
- let disposal;
2699
- const abortPreparation = () => controller.abort(preparationSignal?.reason);
2700
- preparationSignal?.addEventListener("abort", abortPreparation, { once: true });
2701
- if (preparationSignal?.aborted) abortPreparation();
2702
- const completePreparation = () => {
2703
- preparationSignal?.removeEventListener("abort", abortPreparation);
2704
- };
2705
- const requestSignal = (signal) => signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
2706
- const track = (dispose, signal) => {
2707
- let pending;
2708
- const disposeOnce = () => {
2709
- if (pending) return pending;
2710
- controller.signal.removeEventListener("abort", onAbort);
2711
- signal?.removeEventListener("abort", onAbort);
2712
- activeDisposers.delete(disposeOnce);
2713
- pending = Promise.resolve().then(dispose);
2714
- return pending;
2715
- };
2716
- const onAbort = () => {
2717
- disposeOnce().catch(() => void 0);
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
2718
2397
  };
2719
- activeDisposers.add(disposeOnce);
2720
- controller.signal.addEventListener("abort", onAbort, { once: true });
2721
- signal?.addEventListener("abort", onAbort, { once: true });
2722
- if (controller.signal.aborted || signal?.aborted) onAbort();
2723
- return disposeOnce;
2724
- };
2725
- const dispose = (reason = /* @__PURE__ */ new Error("Runtime disposed")) => {
2726
- if (disposal) return disposal;
2727
- completePreparation();
2728
- const pending = [...activeDisposers];
2729
- activeDisposers.clear();
2730
- controller.abort(reason);
2731
- disposal = disposeAll(pending);
2732
- 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;
2733
2408
  };
2734
- return {
2735
- completePreparation,
2736
- dispose,
2737
- disposeAll,
2738
- requestSignal,
2739
- signal: controller.signal,
2740
- 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
+ }
2741
2416
  };
2742
- };
2743
- //#endregion
2744
- //#region src/internal/server/runtime/resource-dependency-graph.ts
2745
- const emptyGraphFor = (resourceIds) => new Map(resourceIds.map((resourceId) => [resourceId, /* @__PURE__ */ new Set()]));
2746
- const visitDependencies = ({ dependencies, path, resourceId, visited, visiting }) => {
2747
- if (visiting.has(resourceId)) throw new Error(`Resource dependency cycle: ${[...path, resourceId].join(" -> ")}`);
2748
- if (visited.has(resourceId)) return;
2749
- visiting.add(resourceId);
2750
- dependencies.get(resourceId)?.forEach((dependencyId) => {
2751
- visitDependencies({
2752
- dependencies,
2753
- path: [...path, resourceId],
2754
- resourceId: dependencyId,
2755
- visited,
2756
- 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
2757
2428
  });
2758
- });
2759
- visiting.delete(resourceId);
2760
- visited.add(resourceId);
2761
- };
2762
- const assertAcyclic = (dependencies) => {
2763
- const visited = /* @__PURE__ */ new Set();
2764
- const visiting = /* @__PURE__ */ new Set();
2765
- dependencies.forEach((_, resourceId) => {
2766
- visitDependencies({
2767
- dependencies,
2768
- path: [],
2769
- resourceId,
2770
- visited,
2771
- 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
+ }
2772
2467
  });
2773
- });
2774
- };
2775
- const createResourceDependencyGraph = (resources) => {
2776
- const resourceIds = Object.keys(resources);
2777
- if (resourceIds.some((resourceId) => !resourceId)) throw new Error("Resource IDs must not be empty");
2778
- const dependencies = emptyGraphFor(resourceIds);
2779
- const dependants = emptyGraphFor(resourceIds);
2780
- Object.entries(resources).forEach(([resourceId, resource]) => {
2781
- if (resource.kind !== "derived") return;
2782
- Object.entries(resource.dependencies).forEach(([dependencyName, dependencyId]) => {
2783
- if (!dependencyName) throw new Error(`Resource ${resourceId} has an empty dependency name`);
2784
- if (!dependencyId) throw new Error(`Resource ${resourceId} has an empty dependency ID`);
2785
- if (!dependencies.has(dependencyId)) throw new Error(`Unknown resource dependency: ${dependencyId}`);
2786
- dependencies.get(resourceId)?.add(dependencyId);
2787
- 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)
2788
2482
  });
2789
- });
2790
- assertAcyclic(dependencies);
2791
- return Object.freeze({
2792
- dependencies,
2793
- dependants
2794
- });
2795
- };
2796
- const collectInvalidatedResourceIds = (graph, resourceId, invalidated) => {
2797
- if (invalidated.has(resourceId)) return;
2798
- invalidated.add(resourceId);
2799
- graph.dependants.get(resourceId)?.forEach((dependantId) => {
2800
- collectInvalidatedResourceIds(graph, dependantId, invalidated);
2801
- });
2802
- };
2803
- const getInvalidatedResourceIds = (graph, resourceId) => {
2804
- const invalidated = /* @__PURE__ */ new Set();
2805
- collectInvalidatedResourceIds(graph, resourceId, invalidated);
2806
- return invalidated;
2807
- };
2808
- //#endregion
2809
- //#region src/internal/server/runtime/runtime-resources.ts
2810
- const createSubscriptionLifetime = ({ entry, invalidationListeners, lifecycle, signal }) => {
2811
- const controller = new AbortController();
2812
- const subscriptionSignal = AbortSignal.any([lifecycle.requestSignal(signal), controller.signal]);
2813
- subscriptionSignal.throwIfAborted();
2814
- const registeredEntry = {
2815
- ...entry,
2816
- listener: () => {
2817
- 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;
2818
2500
  }
2819
2501
  };
2820
- const disposers = [];
2821
- const release = lifecycle.track(async () => {
2822
- invalidationListeners.delete(registeredEntry);
2823
- await lifecycle.disposeAll(disposers);
2824
- }, subscriptionSignal);
2825
- const dispose = () => {
2826
- controller.abort(/* @__PURE__ */ new Error("Resource subscription closed"));
2827
- 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);
2828
2521
  };
2829
- invalidationListeners.add(registeredEntry);
2830
- return {
2831
- dispose,
2832
- disposers,
2833
- refresh: registeredEntry.listener,
2834
- signal: subscriptionSignal
2835
- };
2836
- };
2837
- const createRuntimeResources = ({ definitions, instance, lifecycle }) => {
2838
- const graph = createResourceDependencyGraph(definitions);
2839
- const invalidationListeners = /* @__PURE__ */ new Set();
2840
- const resources = /* @__PURE__ */ new Map();
2841
- const invalidateResource = (id, input, allInputs = false) => {
2842
- if (lifecycle.signal.aborted) return;
2843
- const ids = getInvalidatedResourceIds(graph, id);
2844
- invalidationListeners.forEach((entry) => {
2845
- if (ids.has(entry.resourceId) && (allInputs || isDeepStrictEqual(entry.input, input))) entry.listener();
2846
- });
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
+ }
2847
2552
  };
2848
- const invalidate = (resourceId, input) => {
2849
- if (!resources.has(resourceId)) throw new Error(`Unknown resource ID: ${resourceId}`);
2850
- const definition = definitions[resourceId];
2851
- const parsedInput = input === void 0 ? void 0 : definition.contract.input.parse(input);
2852
- invalidateResource(resourceId, parsedInput, input === void 0);
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
+ }
2853
2566
  };
2854
- const context = (signal) => ({
2855
- instance,
2856
- invalidate,
2857
- signal: lifecycle.requestSignal(signal)
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;
2588
+ };
2589
+ worker.on("message", receive);
2590
+ worker.on("error", (cause) => {
2591
+ terminate(cause);
2858
2592
  });
2859
- const read = async (id, rawInput, signal) => {
2860
- const definition = definitions[id];
2861
- const operationSignal = lifecycle.requestSignal(signal);
2862
- operationSignal.throwIfAborted();
2863
- const input = definition.contract.input.parse(rawInput);
2864
- 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));
2865
- operationSignal.throwIfAborted();
2866
- return definition.contract.output.parse(rawOutput);
2867
- };
2868
- const activateSubscriptions = async (id, rawInput, signal, activated, disposers) => {
2869
- signal.throwIfAborted();
2870
- if (activated.has(id)) return;
2871
- activated.add(id);
2872
- const definition = definitions[id];
2873
- const input = definition.contract.input.parse(rawInput);
2874
- if (definition.kind === "subscription") {
2875
- const dispose = await definition.subscribe(input, () => {
2876
- if (!signal.aborted) invalidateResource(id, input);
2877
- }, context(signal));
2878
- if (typeof dispose !== "function") throw new Error(`Resource ${id} subscribe must return a disposer`);
2879
- if (signal.aborted) {
2880
- await dispose();
2881
- signal.throwIfAborted();
2882
- }
2883
- disposers.push(dispose);
2884
- return;
2885
- }
2886
- if (definition.kind === "derived") for (const dependencyId of graph.dependencies.get(id) ?? []) await activateSubscriptions(dependencyId, rawInput, signal, activated, disposers);
2887
- };
2888
- Object.keys(definitions).forEach((name) => {
2889
- resources.set(name, {
2890
- read: (input, signal) => read(name, input, signal),
2891
- subscribe: async (rawInput, listener, signal) => {
2892
- lifecycle.requestSignal(signal).throwIfAborted();
2893
- const input = definitions[name].contract.input.parse(rawInput);
2894
- const subscription = createSubscriptionLifetime({
2895
- entry: {
2896
- input,
2897
- listener,
2898
- resourceId: name
2899
- },
2900
- invalidationListeners,
2901
- lifecycle,
2902
- signal
2903
- });
2904
- try {
2905
- await activateSubscriptions(name, rawInput, subscription.signal, /* @__PURE__ */ new Set(), subscription.disposers);
2906
- subscription.signal.throwIfAborted();
2907
- subscription.refresh();
2908
- subscription.signal.throwIfAborted();
2909
- return subscription.dispose;
2910
- } catch (cause) {
2911
- try {
2912
- await subscription.dispose();
2913
- } catch (cleanupError) {
2914
- throw new AggregateError([cause, cleanupError], "Resource subscription setup and cleanup failed");
2915
- }
2916
- throw cause;
2917
- }
2918
- }
2919
- });
2593
+ worker.on("exit", (code) => {
2594
+ const phase = state.status === "starting" ? " during startup" : "";
2595
+ terminate(/* @__PURE__ */ new Error(`Worker ${options.name} exited${phase} (${code})`));
2920
2596
  });
2597
+ postControl(options.init);
2921
2598
  return {
2922
- context,
2923
- get: (name) => resources.get(name),
2924
- invalidate,
2925
- names: [...resources.keys()]
2599
+ ready: startup.promise,
2600
+ request,
2601
+ openSession,
2602
+ stop,
2603
+ terminate
2926
2604
  };
2927
2605
  };
2928
2606
  //#endregion
2929
- //#region src/internal/server/runtime/runtime-streams.ts
2930
- const createRuntimeStreams = ({ context, definitions, lifecycle }) => {
2931
- const streams = /* @__PURE__ */ new Map();
2932
- Object.entries(definitions).forEach(([name, definition]) => {
2933
- if (!name) throw new Error("Stream names must not be empty");
2934
- streams.set(name, { open: async (rawInput, emit, requestSignal, onError) => {
2935
- const sessionController = new AbortController();
2936
- const sessionSignal = requestSignal ? AbortSignal.any([requestSignal, sessionController.signal]) : sessionController.signal;
2937
- const operationSignal = lifecycle.requestSignal(sessionSignal);
2938
- operationSignal.throwIfAborted();
2939
- const input = definition.contract.input.parse(rawInput);
2940
- let acceptingMessages = true;
2941
- let failed = false;
2942
- let failure;
2943
- let opened = false;
2944
- const fail = (cause) => {
2945
- if (failed || operationSignal.aborted) return;
2946
- failed = true;
2947
- failure = cause;
2948
- acceptingMessages = false;
2949
- sessionController.abort(cause);
2950
- if (opened) onError?.(cause);
2951
- };
2952
- const session = await definition.open(input, {
2953
- ...context(sessionSignal),
2954
- emit: (message) => {
2955
- if (operationSignal.aborted) return;
2956
- try {
2957
- emit(definition.contract.serverMessage.parse(message));
2958
- } catch (cause) {
2959
- fail(cause);
2960
- }
2961
- },
2962
- fail,
2963
- signal: operationSignal
2964
- });
2965
- opened = true;
2966
- if (failed || operationSignal.aborted) {
2967
- await session.dispose?.();
2968
- operationSignal.throwIfAborted();
2969
- throw failure;
2970
- }
2971
- let messages = Promise.resolve();
2972
- const dispose = lifecycle.track(async () => {
2973
- acceptingMessages = false;
2974
- sessionController.abort(/* @__PURE__ */ new Error("Stream session disposed"));
2975
- await messages.catch(() => void 0);
2976
- await session.dispose?.();
2977
- }, requestSignal);
2978
- return {
2979
- dispose: async () => dispose(),
2980
- send: async (rawMessage) => {
2981
- if (!acceptingMessages) throw new Error("Stream session is closed");
2982
- const message = definition.contract.clientMessage.parse(rawMessage);
2983
- messages = messages.then(async () => {
2984
- operationSignal.throwIfAborted();
2985
- await session.onMessage?.(message);
2986
- operationSignal.throwIfAborted();
2987
- });
2988
- return messages;
2989
- }
2990
- };
2991
- } });
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
2992
2645
  });
2993
2646
  return {
2994
- get: (name) => streams.get(name),
2995
- names: [...streams.keys()]
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
+ }
2996
2656
  };
2997
- };
2657
+ } });
2998
2658
  //#endregion
2999
- //#region src/internal/server/runtime/create-runtime.ts
3000
- const createRuntime = async ({ config, debug = true, notifications = { send: async () => void 0 }, signal }) => {
3001
- const lifecycle = createRuntimeLifecycle(signal);
3002
- const instance = createRuntimeInstance(config.instanceId ?? defaultInstanceId);
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
2683
+ });
2684
+ workers.push(connection);
2685
+ return connection;
2686
+ };
2687
+ const loadWorkerConfig = async (options) => {
2688
+ const workers = [];
3003
2689
  try {
3004
- const resources = createRuntimeResources({
3005
- definitions: config.server.resources ?? {},
3006
- instance: instance.context,
3007
- lifecycle
3008
- });
3009
- const context = resources.context;
3010
- const operations = createRuntimeOperations({
3011
- context,
3012
- definitions: config.server.operations ?? {},
3013
- notifications
3014
- });
3015
- const streams = createRuntimeStreams({
3016
- context,
3017
- definitions: config.server.streams ?? {},
3018
- lifecycle
3019
- });
3020
- lifecycle.signal.throwIfAborted();
3021
- lifecycle.completePreparation();
3022
- const manifest = {
3023
- debug,
3024
- operations: [...operations.names],
3025
- protocolVersion: 10,
3026
- resources: [...resources.names],
3027
- streams: [...streams.names]
3028
- };
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);
2715
+ return {
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
+ }
2734
+ };
2735
+ };
2736
+ const createWorkerRuntime = async (input) => {
2737
+ const options = resolveOptions(input);
2738
+ const workers = [];
2739
+ let disposal;
2740
+ try {
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))]));
3029
2749
  return {
3030
- instance: instance.context,
3031
- establishInstance: instance.establish,
3032
- manifest,
3033
- dispose: lifecycle.dispose,
3034
- getOperation: operations.get,
3035
- getResource: resources.get,
3036
- 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
+ }
3037
2767
  };
3038
2768
  } catch (cause) {
3039
- 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)));
3040
2771
  throw cause;
3041
2772
  }
3042
2773
  };
3043
2774
  //#endregion
3044
2775
  //#region src/internal/server/server-startup-options.ts
3045
2776
  const serverStartupOverridesRuntimeSchema = serverConfigRuntimeSchema.omit({ port: true }).extend({
3046
- debug: z.boolean().optional(),
2777
+ logLevel: logLevelSchema.optional(),
3047
2778
  port: z.number().int().min(0).max(65535).optional()
3048
2779
  });
3049
2780
  const resolveServerStartupOptions = ({ config, configPath, overrides = {} }) => {
@@ -3051,7 +2782,7 @@ const resolveServerStartupOptions = ({ config, configPath, overrides = {} }) =>
3051
2782
  const configuredWebAssetsDir = config.productionWebAssetsDir ? resolve(dirname(configPath), config.productionWebAssetsDir) : void 0;
3052
2783
  const productionWebAssetsDir = validatedOverrides.productionWebAssetsDir ? resolve(validatedOverrides.productionWebAssetsDir) : configuredWebAssetsDir;
3053
2784
  return {
3054
- debug: validatedOverrides.debug ?? config.debug ?? true,
2785
+ logLevel: validatedOverrides.logLevel ?? config.logLevel ?? "info",
3055
2786
  host: validatedOverrides.host ?? config.host ?? "localhost",
3056
2787
  port: validatedOverrides.port ?? config.port ?? 4242,
3057
2788
  ...productionWebAssetsDir === void 0 ? {} : { productionWebAssetsDir },
@@ -3113,7 +2844,7 @@ const registerProcessErrorLogging = (logger) => {
3113
2844
  };
3114
2845
  };
3115
2846
  const loadServerSettings = async ({ configPath, options }) => {
3116
- const { config } = await loadOvermuxConfig({
2847
+ const { config } = await loadWorkerConfig({
3117
2848
  aliases: options.configAliases,
3118
2849
  configPath
3119
2850
  });
@@ -3137,7 +2868,7 @@ const createAuthOrigins = ({ configuredOrigins, startup }) => {
3137
2868
  get: () => values
3138
2869
  };
3139
2870
  };
3140
- const createApplicationServices = async ({ config, logger, resources, startup }) => {
2871
+ const createApplicationServices = async ({ aliases, config, configPath, logger, resources, startup }) => {
3141
2872
  const authOrigins = createAuthOrigins({
3142
2873
  configuredOrigins: config.auth.origins,
3143
2874
  startup
@@ -3166,10 +2897,11 @@ const createApplicationServices = async ({ config, logger, resources, startup })
3166
2897
  backgroundNotifications,
3167
2898
  logger
3168
2899
  });
3169
- const runtime = await createRuntime({
3170
- config,
3171
- debug: logger.enabled,
3172
- notifications
2900
+ const runtime = await createWorkerRuntime({
2901
+ configPath,
2902
+ serverLogger: logger,
2903
+ notifications,
2904
+ aliases
3173
2905
  });
3174
2906
  resources.add(runtime.dispose);
3175
2907
  return {
@@ -3203,7 +2935,7 @@ const startApplicationTransport = async ({ auth, authOrigins, authService, backg
3203
2935
  });
3204
2936
  resources.add(() => closeNodeServer(server, runtime.dispose));
3205
2937
  const address = await getServerAddress(server);
3206
- runtime.establishInstance(address.port);
2938
+ await runtime.establishInstance(address.port);
3207
2939
  const urlHost = startup.host.includes(":") ? `[${startup.host}]` : startup.host;
3208
2940
  const localUrl = `http://${urlHost}:${address.port}`;
3209
2941
  const controlApiHost = startup.host === "0.0.0.0" ? "127.0.0.1" : startup.host === "::" ? "[::1]" : urlHost;
@@ -3249,25 +2981,26 @@ const failApplicationServerStartup = async (cause, resources) => {
3249
2981
  throw cause;
3250
2982
  };
3251
2983
  const startApplicationServer = async ({ onRestartRequested, options }) => {
2984
+ const configPath = resolve(options.configPath);
2985
+ const { config, startup } = await loadServerSettings({
2986
+ configPath,
2987
+ options
2988
+ });
3252
2989
  const logger = createServerLogger({
3253
- enabled: options.debug ?? true,
2990
+ logLevel: startup.logLevel,
3254
2991
  logFile: getServerLogFile()
3255
2992
  });
3256
2993
  const resources = createResources();
3257
2994
  resources.add(registerProcessErrorLogging(logger));
3258
2995
  try {
3259
- const configPath = resolve(options.configPath);
3260
2996
  logger.log({
3261
2997
  details: { configPath },
3262
2998
  event: "server-start"
3263
2999
  });
3264
- const { config, startup } = await loadServerSettings({
3265
- configPath,
3266
- options
3267
- });
3268
- logger.setEnabled(startup.debug);
3269
3000
  const { auth, authOrigins, authService, backgroundNotifications, notifications, runtime } = await createApplicationServices({
3001
+ aliases: options.configAliases,
3270
3002
  config,
3003
+ configPath,
3271
3004
  logger,
3272
3005
  resources,
3273
3006
  startup
@@ -3306,7 +3039,7 @@ const startApplicationServer = async ({ onRestartRequested, options }) => {
3306
3039
  const childOptionsSchema = z.object({
3307
3040
  configAliases: z.record(z.string(), z.string()).optional(),
3308
3041
  configPath: z.string(),
3309
- debug: z.boolean().optional(),
3042
+ logLevel: logLevelSchema.optional(),
3310
3043
  developmentWebTarget: z.url().optional(),
3311
3044
  host: z.string().optional(),
3312
3045
  port: z.number().int().min(0).max(65535).optional(),