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
@@ -0,0 +1,728 @@
1
+ import { P as loadOvermuxConfig, _ as isServerLogEnabled, a as serializeWorkerError, c as defaultInstanceId, d as createRuntimeOperations, f as createHandlerLogger, i as postWorkerMessage, o as workerLimits, r as encodeWorkerMessage, s as createRuntimeInstance, t as decodeWorkerMessage, v as serializeServerLogEntry } from "../../worker-protocol-hXs4FXCH.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import "zod";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { parentPort } from "node:worker_threads";
6
+ //#region src/internal/server/runtime/runtime-lifecycle.ts
7
+ const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
8
+ const disposeAll = async (disposers) => {
9
+ const errors = (await Promise.allSettled([...disposers].map(async (dispose) => dispose()))).flatMap((result) => result.status === "rejected" ? [asError(result.reason)] : []);
10
+ if (errors.length) throw new AggregateError(errors, "Runtime disposal failed");
11
+ };
12
+ const createRuntimeLifecycle = (preparationSignal) => {
13
+ const controller = new AbortController();
14
+ const activeDisposers = /* @__PURE__ */ new Set();
15
+ let disposal;
16
+ const abortPreparation = () => controller.abort(preparationSignal?.reason);
17
+ preparationSignal?.addEventListener("abort", abortPreparation, { once: true });
18
+ if (preparationSignal?.aborted) abortPreparation();
19
+ const completePreparation = () => {
20
+ preparationSignal?.removeEventListener("abort", abortPreparation);
21
+ };
22
+ const requestSignal = (signal) => signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
23
+ const track = (dispose, signal) => {
24
+ let pending;
25
+ const disposeOnce = () => {
26
+ if (pending) return pending;
27
+ controller.signal.removeEventListener("abort", onAbort);
28
+ signal?.removeEventListener("abort", onAbort);
29
+ activeDisposers.delete(disposeOnce);
30
+ pending = Promise.resolve().then(dispose);
31
+ return pending;
32
+ };
33
+ const onAbort = () => {
34
+ disposeOnce().catch(() => void 0);
35
+ };
36
+ activeDisposers.add(disposeOnce);
37
+ controller.signal.addEventListener("abort", onAbort, { once: true });
38
+ signal?.addEventListener("abort", onAbort, { once: true });
39
+ if (controller.signal.aborted || signal?.aborted) onAbort();
40
+ return disposeOnce;
41
+ };
42
+ const dispose = (reason = /* @__PURE__ */ new Error("Runtime disposed")) => {
43
+ if (disposal) return disposal;
44
+ completePreparation();
45
+ const pending = [...activeDisposers];
46
+ activeDisposers.clear();
47
+ controller.abort(reason);
48
+ disposal = disposeAll(pending);
49
+ return disposal;
50
+ };
51
+ return {
52
+ completePreparation,
53
+ dispose,
54
+ disposeAll,
55
+ requestSignal,
56
+ signal: controller.signal,
57
+ track
58
+ };
59
+ };
60
+ //#endregion
61
+ //#region src/internal/server/runtime/resource-dependency-graph.ts
62
+ const emptyGraphFor = (resourceIds) => new Map(resourceIds.map((resourceId) => [resourceId, /* @__PURE__ */ new Set()]));
63
+ const visitDependencies = ({ dependencies, path, resourceId, visited, visiting }) => {
64
+ if (visiting.has(resourceId)) throw new Error(`Resource dependency cycle: ${[...path, resourceId].join(" -> ")}`);
65
+ if (visited.has(resourceId)) return;
66
+ visiting.add(resourceId);
67
+ dependencies.get(resourceId)?.forEach((dependencyId) => {
68
+ visitDependencies({
69
+ dependencies,
70
+ path: [...path, resourceId],
71
+ resourceId: dependencyId,
72
+ visited,
73
+ visiting
74
+ });
75
+ });
76
+ visiting.delete(resourceId);
77
+ visited.add(resourceId);
78
+ };
79
+ const assertAcyclic = (dependencies) => {
80
+ const visited = /* @__PURE__ */ new Set();
81
+ const visiting = /* @__PURE__ */ new Set();
82
+ dependencies.forEach((_, resourceId) => {
83
+ visitDependencies({
84
+ dependencies,
85
+ path: [],
86
+ resourceId,
87
+ visited,
88
+ visiting
89
+ });
90
+ });
91
+ };
92
+ const createResourceDependencyGraph = (resources) => {
93
+ const resourceIds = Object.keys(resources);
94
+ if (resourceIds.some((resourceId) => !resourceId)) throw new Error("Resource IDs must not be empty");
95
+ const dependencies = emptyGraphFor(resourceIds);
96
+ const dependants = emptyGraphFor(resourceIds);
97
+ Object.entries(resources).forEach(([resourceId, resource]) => {
98
+ if (resource.kind !== "derived") return;
99
+ Object.entries(resource.dependencies).forEach(([dependencyName, dependencyId]) => {
100
+ if (!dependencyName) throw new Error(`Resource ${resourceId} has an empty dependency name`);
101
+ if (!dependencyId) throw new Error(`Resource ${resourceId} has an empty dependency ID`);
102
+ if (!dependencies.has(dependencyId)) throw new Error(`Unknown resource dependency: ${dependencyId}`);
103
+ dependencies.get(resourceId)?.add(dependencyId);
104
+ dependants.get(dependencyId)?.add(resourceId);
105
+ });
106
+ });
107
+ assertAcyclic(dependencies);
108
+ return Object.freeze({
109
+ dependencies,
110
+ dependants
111
+ });
112
+ };
113
+ const collectInvalidatedResourceIds = (graph, resourceId, invalidated) => {
114
+ if (invalidated.has(resourceId)) return;
115
+ invalidated.add(resourceId);
116
+ graph.dependants.get(resourceId)?.forEach((dependantId) => {
117
+ collectInvalidatedResourceIds(graph, dependantId, invalidated);
118
+ });
119
+ };
120
+ const getInvalidatedResourceIds = (graph, resourceId) => {
121
+ const invalidated = /* @__PURE__ */ new Set();
122
+ collectInvalidatedResourceIds(graph, resourceId, invalidated);
123
+ return invalidated;
124
+ };
125
+ //#endregion
126
+ //#region src/internal/server/runtime/runtime-resources.ts
127
+ const parseDependencyInputs = (definitions, graph, id, rawInput, inputs = /* @__PURE__ */ new Map()) => {
128
+ if (inputs.has(id)) return inputs;
129
+ inputs.set(id, definitions[id].contract.input.parse(rawInput));
130
+ graph.dependencies.get(id)?.forEach((dependencyId) => {
131
+ parseDependencyInputs(definitions, graph, dependencyId, rawInput, inputs);
132
+ });
133
+ return inputs;
134
+ };
135
+ const createSubscriptionLifetime = ({ entry, invalidationListeners, lifecycle, signal }) => {
136
+ const controller = new AbortController();
137
+ const subscriptionSignal = AbortSignal.any([lifecycle.requestSignal(signal), controller.signal]);
138
+ subscriptionSignal.throwIfAborted();
139
+ const registeredEntry = {
140
+ ...entry,
141
+ listener: () => {
142
+ if (!subscriptionSignal.aborted) entry.listener();
143
+ }
144
+ };
145
+ const disposers = [];
146
+ const subscription = {
147
+ disposers,
148
+ dispose: () => {
149
+ controller.abort(/* @__PURE__ */ new Error("Resource subscription closed"));
150
+ return release();
151
+ },
152
+ refresh: registeredEntry.listener,
153
+ signal: subscriptionSignal
154
+ };
155
+ const release = lifecycle.track(async () => {
156
+ invalidationListeners.delete(registeredEntry);
157
+ await lifecycle.disposeAll(disposers);
158
+ }, subscriptionSignal);
159
+ invalidationListeners.add(registeredEntry);
160
+ return subscription;
161
+ };
162
+ const createRuntimeResources = ({ definitions, instance, lifecycle, serverLogger }) => {
163
+ const graph = createResourceDependencyGraph(definitions);
164
+ const invalidationListeners = /* @__PURE__ */ new Set();
165
+ const resources = /* @__PURE__ */ new Map();
166
+ const invalidateResource = (id, input, allInputs = false) => {
167
+ if (lifecycle.signal.aborted) return;
168
+ const ids = getInvalidatedResourceIds(graph, id);
169
+ invalidationListeners.forEach((entry) => {
170
+ if (ids.has(entry.resourceId) && (allInputs || isDeepStrictEqual(entry.inputs.get(id), input))) entry.listener();
171
+ });
172
+ };
173
+ const invalidate = (resourceId, input) => {
174
+ if (!Object.hasOwn(definitions, resourceId)) throw new Error(`Unknown resource ID: ${resourceId}`);
175
+ const definition = definitions[resourceId];
176
+ const parsedInput = input === void 0 ? void 0 : definition.contract.input.parse(input);
177
+ invalidateResource(resourceId, parsedInput, input === void 0);
178
+ };
179
+ const context = (signal) => ({
180
+ instance,
181
+ invalidate,
182
+ signal: lifecycle.requestSignal(signal)
183
+ });
184
+ const read = (id, rawInput, signal, correlation = { correlationId: randomUUID() }, reads = /* @__PURE__ */ new Map()) => {
185
+ const existing = reads.get(id);
186
+ if (existing) return existing;
187
+ const pending = readDefinition(id, rawInput, signal, correlation, reads);
188
+ reads.set(id, pending);
189
+ return pending;
190
+ };
191
+ const readDefinition = async (id, rawInput, signal, correlation, reads) => {
192
+ const definition = definitions[id];
193
+ const handlerContext = {
194
+ ...context(signal),
195
+ logger: createHandlerLogger({
196
+ serverLogger,
197
+ correlation,
198
+ capabilityKind: "resource",
199
+ registeredName: id,
200
+ handler: "read"
201
+ })
202
+ };
203
+ handlerContext.signal.throwIfAborted();
204
+ const input = definition.contract.input.parse(rawInput);
205
+ const dependencies = definition.kind === "derived" ? Object.fromEntries(await Promise.all(Object.entries(definition.dependencies).map(async ([name, dependencyId]) => [name, await read(dependencyId, rawInput, handlerContext.signal, correlation, reads)]))) : void 0;
206
+ handlerContext.signal.throwIfAborted();
207
+ const output = definition.kind === "derived" ? definition.combine(dependencies) : await definition.read(input, handlerContext);
208
+ handlerContext.signal.throwIfAborted();
209
+ return definition.contract.output.parse(output);
210
+ };
211
+ const activateSubscriptions = async (inputs, subscription, correlation) => {
212
+ for (const [id, input] of inputs) {
213
+ subscription.signal.throwIfAborted();
214
+ const definition = definitions[id];
215
+ if (definition.kind === "subscription") {
216
+ const dispose = await definition.subscribe(input, () => {
217
+ if (!subscription.signal.aborted) invalidateResource(id, input);
218
+ }, {
219
+ ...context(subscription.signal),
220
+ logger: createHandlerLogger({
221
+ serverLogger,
222
+ correlation,
223
+ capabilityKind: "resource",
224
+ registeredName: id,
225
+ handler: "subscribe"
226
+ })
227
+ });
228
+ if (typeof dispose !== "function") throw new Error(`Resource ${id} subscribe must return a disposer`);
229
+ if (subscription.signal.aborted) {
230
+ await dispose();
231
+ subscription.signal.throwIfAborted();
232
+ }
233
+ subscription.disposers.push(dispose);
234
+ }
235
+ }
236
+ };
237
+ Object.keys(definitions).forEach((name) => {
238
+ resources.set(name, {
239
+ read: (rawInput, signal, correlation) => read(name, rawInput, signal, correlation),
240
+ subscribe: async (rawInput, listener, signal, _onError, correlation = { correlationId: randomUUID() }) => {
241
+ lifecycle.requestSignal(signal).throwIfAborted();
242
+ const inputs = parseDependencyInputs(definitions, graph, name, rawInput);
243
+ const subscription = createSubscriptionLifetime({
244
+ entry: {
245
+ inputs,
246
+ listener,
247
+ resourceId: name
248
+ },
249
+ invalidationListeners,
250
+ lifecycle,
251
+ signal
252
+ });
253
+ try {
254
+ await activateSubscriptions(inputs, subscription, correlation);
255
+ subscription.signal.throwIfAborted();
256
+ subscription.refresh();
257
+ subscription.signal.throwIfAborted();
258
+ return subscription.dispose;
259
+ } catch (cause) {
260
+ try {
261
+ await subscription.dispose();
262
+ } catch (cleanupError) {
263
+ throw new AggregateError([cause, cleanupError], "Resource subscription setup and cleanup failed");
264
+ }
265
+ throw cause;
266
+ }
267
+ }
268
+ });
269
+ });
270
+ return {
271
+ context,
272
+ get: (name) => resources.get(name),
273
+ invalidate,
274
+ names: [...resources.keys()]
275
+ };
276
+ };
277
+ //#endregion
278
+ //#region src/internal/server/runtime/runtime-streams.ts
279
+ const createRuntimeStreams = ({ instance, definitions, lifecycle, serverLogger }) => {
280
+ const streams = /* @__PURE__ */ new Map();
281
+ Object.entries(definitions).forEach(([name, definition]) => {
282
+ if (!name) throw new Error("Stream names must not be empty");
283
+ streams.set(name, { open: async (rawInput, emit, requestSignal, onError, correlation) => {
284
+ const sessionController = new AbortController();
285
+ const sessionSignal = requestSignal ? AbortSignal.any([requestSignal, sessionController.signal]) : sessionController.signal;
286
+ const operationSignal = lifecycle.requestSignal(sessionSignal);
287
+ operationSignal.throwIfAborted();
288
+ const input = definition.contract.input.parse(rawInput);
289
+ let acceptingMessages = true;
290
+ let failed = false;
291
+ let failure;
292
+ let opened = false;
293
+ const fail = (cause) => {
294
+ if (failed || operationSignal.aborted) return;
295
+ failed = true;
296
+ failure = cause;
297
+ acceptingMessages = false;
298
+ sessionController.abort(cause);
299
+ if (opened) onError?.(cause);
300
+ };
301
+ const session = await definition.open(input, {
302
+ instance,
303
+ logger: createHandlerLogger({
304
+ serverLogger,
305
+ correlation,
306
+ capabilityKind: "stream",
307
+ registeredName: name,
308
+ handler: "open"
309
+ }),
310
+ emit: (message) => {
311
+ if (operationSignal.aborted) return;
312
+ try {
313
+ emit(definition.contract.serverMessage.parse(message));
314
+ } catch (cause) {
315
+ fail(cause);
316
+ }
317
+ },
318
+ fail,
319
+ signal: operationSignal
320
+ });
321
+ opened = true;
322
+ if (failed || operationSignal.aborted) {
323
+ await session.dispose?.();
324
+ operationSignal.throwIfAborted();
325
+ throw failure;
326
+ }
327
+ let messages = Promise.resolve();
328
+ const dispose = lifecycle.track(async () => {
329
+ acceptingMessages = false;
330
+ sessionController.abort(/* @__PURE__ */ new Error("Stream session disposed"));
331
+ await messages.catch(() => void 0);
332
+ await session.dispose?.();
333
+ }, requestSignal);
334
+ return {
335
+ dispose: async () => dispose(),
336
+ send: async (rawMessage) => {
337
+ if (!acceptingMessages) throw new Error("Stream session is closed");
338
+ const message = definition.contract.clientMessage.parse(rawMessage);
339
+ messages = messages.then(async () => {
340
+ operationSignal.throwIfAborted();
341
+ await session.onMessage?.(message);
342
+ operationSignal.throwIfAborted();
343
+ });
344
+ return messages;
345
+ }
346
+ };
347
+ } });
348
+ });
349
+ return {
350
+ get: (name) => streams.get(name),
351
+ names: [...streams.keys()]
352
+ };
353
+ };
354
+ //#endregion
355
+ //#region src/internal/server/runtime/create-runtime.ts
356
+ const createRuntime = async ({ config, notifications = { send: async () => void 0 }, signal, serverLogger }) => {
357
+ const lifecycle = createRuntimeLifecycle(signal);
358
+ const instance = createRuntimeInstance(config.instanceId ?? defaultInstanceId);
359
+ try {
360
+ const resources = createRuntimeResources({
361
+ definitions: config.server.resources ?? {},
362
+ instance: instance.context,
363
+ lifecycle,
364
+ serverLogger
365
+ });
366
+ const context = resources.context;
367
+ const operations = createRuntimeOperations({
368
+ context,
369
+ definitions: config.server.operations ?? {},
370
+ notifications,
371
+ serverLogger
372
+ });
373
+ const streams = createRuntimeStreams({
374
+ instance: instance.context,
375
+ definitions: config.server.streams ?? {},
376
+ lifecycle,
377
+ serverLogger
378
+ });
379
+ lifecycle.signal.throwIfAborted();
380
+ lifecycle.completePreparation();
381
+ const manifest = {
382
+ logLevel: serverLogger?.logLevel ?? config.logLevel ?? "info",
383
+ operations: [...operations.names],
384
+ protocolVersion: 11,
385
+ resources: [...resources.names],
386
+ streams: [...streams.names]
387
+ };
388
+ return {
389
+ instance: instance.context,
390
+ establishInstance: instance.establish,
391
+ manifest,
392
+ dispose: lifecycle.dispose,
393
+ getOperation: operations.get,
394
+ getResource: resources.get,
395
+ getStream: streams.get
396
+ };
397
+ } catch (cause) {
398
+ await lifecycle.dispose(cause).catch(() => void 0);
399
+ throw cause;
400
+ }
401
+ };
402
+ //#endregion
403
+ //#region src/internal/server/worker-entry.ts
404
+ if (!parentPort) throw new Error("Overmux worker requires a parent port");
405
+ const port = parentPort;
406
+ const requests = /* @__PURE__ */ new Map();
407
+ const lifetimes = /* @__PURE__ */ new Map();
408
+ const invalidations = /* @__PURE__ */ new Map();
409
+ const deliveries = /* @__PURE__ */ new Map();
410
+ const notifications = /* @__PURE__ */ new Map();
411
+ let runtime;
412
+ let instanceId;
413
+ let stopping = false;
414
+ let deliveryId = 0;
415
+ let notificationId = 0;
416
+ let deliveryBytes = 0;
417
+ let logCount = 0;
418
+ let logBytes = 0;
419
+ let logsSaturated = false;
420
+ const send = (message) => {
421
+ const packet = encodeWorkerMessage({
422
+ ...message,
423
+ deliveryId: ++deliveryId
424
+ });
425
+ const bytes = packet.byteLength;
426
+ const data = message.type === "stream-output" || message.type === "invalidate" || message.type === "notification" || message.type === "log";
427
+ const countLimit = data ? workerLimits.eventCount - workerLimits.pendingRequests - 2 * workerLimits.lifetimes : workerLimits.eventCount;
428
+ const byteLimit = data ? workerLimits.eventBytes - 256 * 1024 : workerLimits.eventBytes;
429
+ if (bytes > workerLimits.messageBytes || deliveries.size >= countLimit || deliveryBytes + bytes > byteLimit || message.type === "log" && (logCount >= workerLimits.logCount || logBytes + bytes > workerLimits.logBytes)) {
430
+ if (message.type === "log") logsSaturated = logCount > 0;
431
+ throw new Error("Worker output queue is full or message exceeds 1 MiB");
432
+ }
433
+ deliveries.set(deliveryId, {
434
+ bytes,
435
+ log: message.type === "log",
436
+ ...message.type === "invalidate" ? { sessionId: message.sessionId } : {}
437
+ });
438
+ deliveryBytes += bytes;
439
+ if (message.type === "log") {
440
+ logCount += 1;
441
+ logBytes += bytes;
442
+ }
443
+ postWorkerMessage(port, packet);
444
+ return deliveryId;
445
+ };
446
+ const closeLifetime = (id) => {
447
+ const lifetime = lifetimes.get(id);
448
+ if (!lifetime) return Promise.resolve();
449
+ if (lifetime.closing) return lifetime.closing;
450
+ invalidations.delete(id);
451
+ lifetime.controller.abort(/* @__PURE__ */ new Error("Worker lifetime closed"));
452
+ lifetime.closing = lifetime.setup.then(async () => {
453
+ try {
454
+ await lifetime.dispose?.();
455
+ } finally {
456
+ if (lifetimes.get(id) === lifetime) lifetimes.delete(id);
457
+ }
458
+ });
459
+ return lifetime.closing;
460
+ };
461
+ const failStream = (id, cause) => {
462
+ const lifetime = lifetimes.get(id);
463
+ if (!lifetime || lifetime.closing) return;
464
+ closeLifetime(id).catch(fatal);
465
+ send({
466
+ type: "stream-error",
467
+ sessionId: id,
468
+ error: serializeWorkerError(cause)
469
+ });
470
+ };
471
+ const invalidate = (id) => {
472
+ if (!lifetimes.has(id) || lifetimes.get(id)?.controller.signal.aborted || stopping) return;
473
+ const pending = invalidations.get(id);
474
+ if (pending) {
475
+ pending.dirty = true;
476
+ return;
477
+ }
478
+ invalidations.set(id, {
479
+ deliveryId: send({
480
+ type: "invalidate",
481
+ sessionId: id
482
+ }),
483
+ dirty: false
484
+ });
485
+ };
486
+ const acknowledge = (id) => {
487
+ const delivery = deliveries.get(id);
488
+ if (!delivery) return;
489
+ deliveries.delete(id);
490
+ deliveryBytes -= delivery.bytes;
491
+ if (delivery.log) {
492
+ logsSaturated = false;
493
+ logCount -= 1;
494
+ logBytes -= delivery.bytes;
495
+ }
496
+ if (delivery.sessionId !== void 0) {
497
+ const pending = invalidations.get(delivery.sessionId);
498
+ invalidations.delete(delivery.sessionId);
499
+ if (pending?.dirty) invalidate(delivery.sessionId);
500
+ }
501
+ };
502
+ const fatal = (cause) => {
503
+ if (stopping) return;
504
+ stopping = true;
505
+ postWorkerMessage(port, encodeWorkerMessage({
506
+ type: "fatal",
507
+ error: serializeWorkerError(cause)
508
+ }));
509
+ process.exit(1);
510
+ };
511
+ const load = async ({ configPath, aliases, logLevel, role, stream }) => {
512
+ const { config } = await loadOvermuxConfig({
513
+ configPath,
514
+ aliases
515
+ });
516
+ const { server, instanceId: configuredIdentity, ...settings } = config;
517
+ const manifest = {
518
+ logLevel: logLevel ?? config.logLevel ?? "info",
519
+ protocolVersion: 11,
520
+ resources: Object.keys(server.resources),
521
+ operations: Object.keys(server.operations ?? {}),
522
+ streams: Object.keys(server.streams ?? {})
523
+ };
524
+ if (manifest.streams.length > workerLimits.streams) throw new Error("Application stream worker limit exceeded");
525
+ if (role === "stream" && (!stream || !server.streams?.[stream])) throw new Error("Stream definition not found");
526
+ if (role !== "metadata") runtime = await createRuntime({
527
+ config: {
528
+ ...config,
529
+ instanceId: role === "application" ? configuredIdentity : () => {
530
+ if (!instanceId) throw new Error("Worker identity is not established");
531
+ return instanceId;
532
+ },
533
+ server: role === "application" ? {
534
+ resources: server.resources,
535
+ operations: server.operations
536
+ } : {
537
+ resources: {},
538
+ streams: { [stream]: server.streams[stream] }
539
+ }
540
+ },
541
+ serverLogger: {
542
+ logLevel: manifest.logLevel,
543
+ id: randomUUID,
544
+ log: (entry) => {
545
+ if (!isServerLogEnabled(manifest.logLevel, entry) || logsSaturated || logCount >= workerLimits.logCount || logBytes >= workerLimits.logBytes) return;
546
+ try {
547
+ send({
548
+ type: "log",
549
+ entry: serializeServerLogEntry(entry)
550
+ });
551
+ } catch {}
552
+ }
553
+ },
554
+ notifications: { send: async (notification) => {
555
+ if (stopping || notifications.size >= workerLimits.notifications) throw new Error("Worker notification queue is full or disposed");
556
+ const id = ++notificationId;
557
+ await new Promise((resolve, reject) => {
558
+ notifications.set(id, {
559
+ resolve,
560
+ reject
561
+ });
562
+ try {
563
+ send({
564
+ type: "notification",
565
+ id,
566
+ notification
567
+ });
568
+ } catch (cause) {
569
+ notifications.delete(id);
570
+ reject(cause);
571
+ }
572
+ });
573
+ } }
574
+ });
575
+ postWorkerMessage(port, encodeWorkerMessage({
576
+ type: "ready",
577
+ manifest,
578
+ settings,
579
+ voidOperations: manifest.operations.filter((name) => runtime?.getOperation(name)?.returnsVoid)
580
+ }));
581
+ };
582
+ const createLifetime = (id, controller) => {
583
+ if (lifetimes.size >= workerLimits.lifetimes) throw new Error("Worker lifetime limit reached");
584
+ if (lifetimes.has(id)) throw new Error("Duplicate worker lifetime ID");
585
+ let prepared;
586
+ const lifetime = {
587
+ controller,
588
+ setup: new Promise((resolveSetup) => {
589
+ prepared = resolveSetup;
590
+ }),
591
+ prepared
592
+ };
593
+ lifetimes.set(id, lifetime);
594
+ return lifetime;
595
+ };
596
+ const execute = async (message, controller) => {
597
+ if (!runtime || stopping) throw new Error("Worker is unavailable");
598
+ if (message.action === "initialize-instance-identity") {
599
+ instanceId = message.instanceId;
600
+ return runtime.establishInstance(message.port);
601
+ }
602
+ if (message.action === "resource-read") {
603
+ const resource = runtime.getResource(message.name);
604
+ if (!resource) throw new Error("Resource not found");
605
+ return resource.read(message.input, controller.signal, message.correlation);
606
+ }
607
+ if (message.action === "operation") {
608
+ const operation = runtime.getOperation(message.name);
609
+ if (!operation) throw new Error("Operation not found");
610
+ return operation.execute(message.input, controller.signal, message.correlation);
611
+ }
612
+ if (message.action === "stream-message") {
613
+ const lifetime = lifetimes.get(message.sessionId);
614
+ if (!lifetime?.send) throw new Error("Stream not found");
615
+ return lifetime.send(message.message);
616
+ }
617
+ const id = message.sessionId;
618
+ const lifetime = createLifetime(id, controller);
619
+ try {
620
+ if (message.action === "stream-open") {
621
+ const stream = runtime.getStream(message.name);
622
+ if (!stream) throw new Error("Stream not found");
623
+ const session = await stream.open(message.input, (output) => {
624
+ if (lifetimes.get(id) === lifetime && !controller.signal.aborted && !stopping) send({
625
+ type: "stream-output",
626
+ sessionId: id,
627
+ message: output
628
+ });
629
+ }, controller.signal, (cause) => failStream(id, cause), message.correlation);
630
+ lifetime.dispose = session.dispose;
631
+ lifetime.send = session.send;
632
+ } else {
633
+ const resource = runtime.getResource(message.name);
634
+ if (!resource) throw new Error("Resource not found");
635
+ const dispose = await resource.subscribe(message.input, () => invalidate(id), controller.signal, void 0, message.correlation);
636
+ lifetime.dispose = async () => dispose();
637
+ }
638
+ if (controller.signal.aborted || lifetimes.get(id) !== lifetime) {
639
+ await lifetime.dispose?.();
640
+ controller.signal.throwIfAborted();
641
+ throw new Error("Worker lifetime closed during setup");
642
+ }
643
+ } catch (cause) {
644
+ if (lifetimes.get(id) === lifetime && !lifetime.closing) lifetimes.delete(id);
645
+ invalidations.delete(id);
646
+ throw cause;
647
+ } finally {
648
+ lifetime.prepared();
649
+ }
650
+ };
651
+ const handleRequest = async (message) => {
652
+ const controller = new AbortController();
653
+ requests.set(message.id, controller);
654
+ try {
655
+ const value = await execute(message, controller);
656
+ send({
657
+ type: "reply",
658
+ id: message.id,
659
+ ok: true,
660
+ value
661
+ });
662
+ } catch (cause) {
663
+ try {
664
+ send({
665
+ type: "reply",
666
+ id: message.id,
667
+ ok: false,
668
+ error: serializeWorkerError(cause)
669
+ });
670
+ } catch (sendCause) {
671
+ fatal(sendCause);
672
+ }
673
+ } finally {
674
+ requests.delete(message.id);
675
+ }
676
+ };
677
+ const shutdown = async () => {
678
+ if (stopping) return;
679
+ stopping = true;
680
+ requests.forEach((controller) => controller.abort(/* @__PURE__ */ new Error("Worker disposed")));
681
+ notifications.forEach(({ reject }) => reject(/* @__PURE__ */ new Error("Worker disposed")));
682
+ notifications.clear();
683
+ await Promise.all([...[...lifetimes.keys()].map(closeLifetime), runtime?.dispose()]);
684
+ postWorkerMessage(port, encodeWorkerMessage({ type: "disposed" }));
685
+ };
686
+ const handleControl = (message) => {
687
+ if (message.type === "ack") acknowledge(message.deliveryId);
688
+ if (message.type === "cancel") requests.get(message.requestId)?.abort(/* @__PURE__ */ new Error("Request cancelled"));
689
+ if (message.type === "close") closeLifetime(message.sessionId).then(() => send({
690
+ type: "closed",
691
+ sessionId: message.sessionId
692
+ }), (cause) => send({
693
+ type: "closed",
694
+ sessionId: message.sessionId,
695
+ error: serializeWorkerError(cause)
696
+ })).catch(fatal);
697
+ if (message.type === "shutdown") shutdown().catch((cause) => postWorkerMessage(port, encodeWorkerMessage({
698
+ type: "fatal",
699
+ error: serializeWorkerError(cause)
700
+ })));
701
+ if (message.type === "notification-result") {
702
+ const pending = notifications.get(message.id);
703
+ notifications.delete(message.id);
704
+ message.ok ? pending?.resolve() : pending?.reject(new Error(message.error));
705
+ }
706
+ };
707
+ port.on("message", (packet) => {
708
+ try {
709
+ const message = decodeWorkerMessage(packet);
710
+ if ("action" in message) {
711
+ handleRequest(message);
712
+ return;
713
+ }
714
+ if (message.type === "init") {
715
+ load(message).catch(fatal);
716
+ return;
717
+ }
718
+ handleControl(message);
719
+ } catch (cause) {
720
+ fatal(cause);
721
+ }
722
+ });
723
+ process.once("uncaughtException", fatal);
724
+ process.once("unhandledRejection", fatal);
725
+ //#endregion
726
+ export {};
727
+
728
+ //# sourceMappingURL=worker-entry.js.map