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.
- package/CHANGELOG.md +22 -0
- package/dist/bin.js +145 -58
- package/dist/bin.js.map +1 -1
- package/dist/docs/400-reference/200-configuration.md +54 -4
- package/dist/docs/400-reference/500-server/000-index.md +4 -0
- package/dist/docs/400-reference/500-server/100-resources.md +5 -0
- package/dist/docs/400-reference/500-server/200-operations.md +1 -0
- package/dist/docs/400-reference/500-server/300-streams.md +7 -1
- package/dist/docs/400-reference/500-server/400-notifications.md +4 -0
- package/dist/docs/400-reference/500-server/500-api.md +26 -5
- package/dist/exports/client.d.ts +1 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +55 -14
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/{index-Dk11nKIv.d.ts → index-C-8Kzrnc.d.ts} +23 -8
- package/dist/exports/index-C-8Kzrnc.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -2
- package/dist/exports/index.js +1 -1
- package/dist/exports/index.js.map +1 -1
- package/dist/exports/{notifications-BFAD3QQl.js → notifications-BBKvKZgI.js} +8 -2
- package/dist/exports/{notifications-BFAD3QQl.js.map → notifications-BBKvKZgI.js.map} +1 -1
- package/dist/exports/server.d.ts.map +1 -1
- package/dist/exports/server.js +8 -2
- package/dist/exports/server.js.map +1 -1
- package/dist/internal/server/coordinator/server-child.js +616 -883
- package/dist/internal/server/coordinator/server-child.js.map +1 -1
- package/dist/internal/server/worker-entry.d.ts +1 -0
- package/dist/internal/server/worker-entry.js +728 -0
- package/dist/internal/server/worker-entry.js.map +1 -0
- package/dist/push/THIRD_PARTY_NOTICES.md +30 -0
- package/dist/push/badge.png +0 -0
- package/dist/push/icon.png +0 -0
- package/dist/push/sw.js +88 -0
- package/dist/push/sw.js.map +1 -0
- package/dist/worker-protocol-hXs4FXCH.js +638 -0
- package/dist/worker-protocol-hXs4FXCH.js.map +1 -0
- package/docs/400-reference/200-configuration.md +54 -4
- package/docs/400-reference/500-server/000-index.md +4 -0
- package/docs/400-reference/500-server/100-resources.md +5 -0
- package/docs/400-reference/500-server/200-operations.md +1 -0
- package/docs/400-reference/500-server/300-streams.md +7 -1
- package/docs/400-reference/500-server/400-notifications.md +4 -0
- package/docs/400-reference/500-server/500-api.md +26 -5
- package/package.json +4 -3
- package/src/internal/client/background-notifications.ts +3 -1
- package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +8 -3
- package/src/internal/client/host/overmux-host.tsx +4 -8
- package/src/internal/client/push-assets/badge.png +0 -0
- package/src/internal/client/push-assets/icon.png +0 -0
- package/src/internal/client/push-service-worker.ts +118 -0
- package/src/internal/client/register-push-worker.ts +63 -0
- package/src/internal/server/coordinator/ipc-protocol.ts +2 -1
- package/src/internal/server/http/operation-handler.ts +3 -0
- package/src/internal/server/http/request-logging.ts +1 -1
- package/src/internal/server/http/routes.ts +47 -2
- package/src/internal/server/protocol-error.ts +19 -0
- package/src/internal/server/runtime/create-runtime.ts +11 -5
- package/src/internal/server/runtime/handler-logger.ts +54 -0
- package/src/internal/server/runtime/runtime-operations.ts +23 -5
- package/src/internal/server/runtime/runtime-resources.ts +158 -88
- package/src/internal/server/runtime/runtime-streams.ts +24 -8
- package/src/internal/server/runtime/worker-capabilities.ts +67 -0
- package/src/internal/server/runtime/worker-connection.ts +549 -0
- package/src/internal/server/runtime/worker-protocol.ts +173 -0
- package/src/internal/server/runtime/worker-runtime.ts +214 -0
- package/src/internal/server/server-logger.ts +25 -37
- package/src/internal/server/server-startup-options.ts +7 -6
- package/src/internal/server/start-application-server.ts +22 -18
- package/src/internal/server/start-overmux-server.ts +2 -2
- package/src/internal/server/websocket-connection.ts +97 -32
- package/src/internal/server/websocket-server.ts +5 -1
- package/src/internal/server/worker-entry.ts +504 -0
- package/src/internal/shared/protocol.ts +3 -3
- package/src/internal/shared/routes.ts +2 -0
- package/src/public/config.ts +15 -23
- package/src/public/handler-logger.ts +7 -0
- package/src/public/index.ts +3 -1
- 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
|