overmux 0.0.1 → 0.0.2
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 +16 -0
- package/README.md +7 -0
- package/dist/auth/.vite/manifest.json +11 -0
- package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
- package/dist/auth/assets/index-CHIE7qYj.js +72 -0
- package/dist/auth/index.html +14 -0
- package/dist/bin.js +4504 -7870
- package/dist/bin.js.map +1 -0
- package/dist/docs/000-index.md +21 -0
- package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/dist/docs/100-introduction/300-why-overmux.md +22 -0
- package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
- package/dist/docs/300-fundamentals/200-configuration.md +3 -0
- package/dist/docs/300-fundamentals/300-theming.md +3 -0
- package/dist/docs/300-fundamentals/400-server.md +3 -0
- package/dist/docs/300-fundamentals/500-client.md +3 -0
- package/dist/docs/300-fundamentals/600-operations.md +3 -0
- package/dist/docs/300-fundamentals/700-resources.md +3 -0
- package/dist/docs/300-fundamentals/800-streams.md +3 -0
- package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/dist/docs/400-reference/100-configuration.md +3 -0
- package/dist/docs/400-reference/200-server-api.md +3 -0
- package/dist/docs/400-reference/300-client-api.md +3 -0
- package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
- package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
- package/dist/docs/400-reference/400-cli/300-call.md +28 -0
- package/dist/docs/400-reference/400-cli/400-check.md +20 -0
- package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
- package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/dist/docs/500-hosted-pages.md +31 -0
- package/dist/exports/client.d.ts +237 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +1643 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
- package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.js +49 -0
- package/dist/exports/index.js.map +1 -0
- package/dist/exports/notifications-kDp16bU_.js +56 -0
- package/dist/exports/notifications-kDp16bU_.js.map +1 -0
- package/dist/exports/server.d.ts +63 -0
- package/dist/exports/server.d.ts.map +1 -0
- package/dist/exports/server.js +1120 -0
- package/dist/exports/server.js.map +1 -0
- package/dist/exports/style.css +368 -0
- package/dist/internal/server/coordinator/server-child.d.ts +1 -0
- package/dist/internal/server/coordinator/server-child.js +3299 -0
- package/dist/internal/server/coordinator/server-child.js.map +1 -0
- package/docs/000-index.md +21 -0
- package/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/docs/100-introduction/300-why-overmux.md +22 -0
- package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/docs/300-fundamentals/100-project-structure.md +3 -0
- package/docs/300-fundamentals/200-configuration.md +3 -0
- package/docs/300-fundamentals/300-theming.md +3 -0
- package/docs/300-fundamentals/400-server.md +3 -0
- package/docs/300-fundamentals/500-client.md +3 -0
- package/docs/300-fundamentals/600-operations.md +3 -0
- package/docs/300-fundamentals/700-resources.md +3 -0
- package/docs/300-fundamentals/800-streams.md +3 -0
- package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/docs/400-reference/100-configuration.md +3 -0
- package/docs/400-reference/200-server-api.md +3 -0
- package/docs/400-reference/300-client-api.md +3 -0
- package/docs/400-reference/400-cli/100-serve.md +33 -0
- package/docs/400-reference/400-cli/200-auth.md +52 -0
- package/docs/400-reference/400-cli/300-call.md +28 -0
- package/docs/400-reference/400-cli/400-check.md +20 -0
- package/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/docs/400-reference/400-cli/600-docs.md +23 -0
- package/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/docs/500-hosted-pages.md +31 -0
- package/package.json +59 -18
- package/src/internal/cli/app.ts +72 -0
- package/src/internal/cli/bin.ts +3 -0
- package/src/internal/cli/commands/ai.ts +44 -0
- package/src/internal/cli/commands/auth.ts +195 -0
- package/src/internal/cli/commands/call.ts +128 -0
- package/src/internal/cli/commands/check.ts +48 -0
- package/src/internal/cli/commands/desktop-installer.ts +446 -0
- package/src/internal/cli/commands/desktop-release.ts +190 -0
- package/src/internal/cli/commands/desktop.ts +47 -0
- package/src/internal/cli/commands/docs.ts +34 -0
- package/src/internal/cli/commands/serve.ts +322 -0
- package/src/internal/cli/environment.ts +54 -0
- package/src/internal/cli/login.ts +46 -0
- package/src/internal/cli/managed-vite.ts +267 -0
- package/src/internal/cli/parse-port.ts +8 -0
- package/src/internal/client/auth/auth-api.ts +53 -0
- package/src/internal/client/auth/auth-shell.css +74 -0
- package/src/internal/client/auth/auth-shell.tsx +167 -0
- package/src/internal/client/auth/entry.tsx +26 -0
- package/src/internal/client/auth/index.html +13 -0
- package/src/internal/client/background-notifications.ts +194 -0
- package/src/internal/client/browser-api.ts +97 -0
- package/src/internal/client/client-definition.ts +184 -0
- package/src/internal/client/commands.tsx +260 -0
- package/src/internal/client/host/browser-log-forwarding.ts +149 -0
- package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
- package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
- package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
- package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
- package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
- package/src/internal/client/host/notification-forwarding.ts +22 -0
- package/src/internal/client/host/overmux-host.tsx +200 -0
- package/src/internal/client/host/recovery-screen.css +75 -0
- package/src/internal/client/host/recovery-screen.tsx +17 -0
- package/src/internal/client/host/update-popover.css +82 -0
- package/src/internal/client/host/update-popover.tsx +166 -0
- package/src/internal/client/index.ts +45 -0
- package/src/internal/client/overmux-react.ts +283 -0
- package/src/internal/client/shortcuts.tsx +373 -0
- package/src/internal/client/theme-scope.css +114 -0
- package/src/internal/client/theme-scope.tsx +107 -0
- package/src/internal/client/transport.ts +69 -0
- package/src/internal/client/websocket.ts +441 -0
- package/src/internal/server/auth/auth-http.ts +414 -0
- package/src/internal/server/auth/auth-service.ts +627 -0
- package/src/internal/server/auth/auth-shell-http.ts +90 -0
- package/src/internal/server/auth/instance-control.ts +381 -0
- package/src/internal/server/check-overmux.ts +364 -0
- package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
- package/src/internal/server/coordinator/server-child.ts +108 -0
- package/src/internal/server/coordinator/server-coordinator.ts +262 -0
- package/src/internal/server/coordinator/update-watcher.ts +54 -0
- package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
- package/src/internal/server/http/background-notification-handlers.ts +83 -0
- package/src/internal/server/http/control-handlers.ts +44 -0
- package/src/internal/server/http/create-http-app.ts +53 -0
- package/src/internal/server/http/development-web-backend.ts +83 -0
- package/src/internal/server/http/operation-handler.ts +93 -0
- package/src/internal/server/http/request-logging.ts +34 -0
- package/src/internal/server/http/routes.ts +241 -0
- package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
- package/src/internal/server/index.ts +16 -0
- package/src/internal/server/notifications/background-notification-payload.ts +74 -0
- package/src/internal/server/notifications/background-notification-service.ts +139 -0
- package/src/internal/server/notifications/background-notification-store.ts +162 -0
- package/src/internal/server/notifications/notification-service.ts +70 -0
- package/src/internal/server/notifications/private-json-file.ts +84 -0
- package/src/internal/server/paths.ts +13 -0
- package/src/internal/server/rate-limiter.ts +58 -0
- package/src/internal/server/runtime/create-runtime.ts +87 -0
- package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
- package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
- package/src/internal/server/runtime/runtime-operations.ts +78 -0
- package/src/internal/server/runtime/runtime-resources.ts +201 -0
- package/src/internal/server/runtime/runtime-streams.ts +130 -0
- package/src/internal/server/server-logger.ts +110 -0
- package/src/internal/server/server-startup-options.ts +51 -0
- package/src/internal/server/start-application-server.ts +412 -0
- package/src/internal/server/start-overmux-server.ts +26 -0
- package/src/internal/server/test-port.ts +23 -0
- package/src/internal/server/vite-websocket-proxy.ts +201 -0
- package/src/internal/server/websocket-connection.ts +586 -0
- package/src/internal/server/websocket-limits.ts +2 -0
- package/src/internal/server/websocket-server.ts +311 -0
- package/src/internal/shared/background-notifications.ts +37 -0
- package/src/internal/shared/index.ts +2 -0
- package/src/internal/shared/protocol.ts +317 -0
- package/src/internal/shared/routes.ts +3 -0
- package/src/public/ai-context.ts +44 -0
- package/src/public/client.ts +53 -0
- package/src/public/config.ts +287 -0
- package/src/public/contracts.ts +55 -0
- package/src/public/index.ts +57 -0
- package/src/public/notifications.ts +10 -0
- package/src/public/server.ts +16 -0
- package/dist/public/core.ts +0 -119
- package/dist/public/protocol.ts +0 -51
- package/dist/public/terminal.ts +0 -23
- package/dist/web/assets/index-DC___IUw.css +0 -2
- package/dist/web/assets/index-DF9Cptse.js +0 -74
- package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
- package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
- package/dist/web/index.html +0 -14
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Registers named operations with validated input, output, and notification context.
|
|
2
|
+
// Lookup metadata stays aligned with the names published in the runtime manifest.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
HandlerContext,
|
|
6
|
+
OperationDefinition,
|
|
7
|
+
} from "../../../public/index";
|
|
8
|
+
import type { Notifications } from "../../../public/index";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export class OperationValidationError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
readonly phase: "input" | "output",
|
|
14
|
+
cause: z.ZodError,
|
|
15
|
+
) {
|
|
16
|
+
super(cause.message, { cause });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RuntimeOperation = {
|
|
21
|
+
execute: (input: unknown, signal?: AbortSignal) => Promise<unknown>;
|
|
22
|
+
returnsVoid: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RuntimeOperations = {
|
|
26
|
+
get: (name: string) => RuntimeOperation | undefined;
|
|
27
|
+
names: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type AnyOperation = OperationDefinition<z.ZodTypeAny, z.ZodTypeAny>;
|
|
31
|
+
|
|
32
|
+
export type RuntimeOperationDefinitions = Readonly<
|
|
33
|
+
Record<string, AnyOperation>
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
type CreateRuntimeOperationsOptions = {
|
|
37
|
+
context: (signal?: AbortSignal) => HandlerContext;
|
|
38
|
+
definitions: RuntimeOperationDefinitions;
|
|
39
|
+
notifications: Notifications;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const createRuntimeOperations = ({
|
|
43
|
+
context,
|
|
44
|
+
definitions,
|
|
45
|
+
notifications,
|
|
46
|
+
}: CreateRuntimeOperationsOptions): RuntimeOperations => {
|
|
47
|
+
const operations = new Map<string, RuntimeOperation>();
|
|
48
|
+
Object.entries(definitions).forEach(([name, definition]) => {
|
|
49
|
+
if (!name) {
|
|
50
|
+
throw new Error("Operation names must not be empty");
|
|
51
|
+
}
|
|
52
|
+
operations.set(name, {
|
|
53
|
+
execute: async (rawInput, signal) => {
|
|
54
|
+
const operationContext = context(signal);
|
|
55
|
+
operationContext.signal.throwIfAborted();
|
|
56
|
+
const input = definition.input.safeParse(rawInput);
|
|
57
|
+
if (!input.success) {
|
|
58
|
+
throw new OperationValidationError("input", input.error);
|
|
59
|
+
}
|
|
60
|
+
const rawOutput = await definition.handle(input.data, {
|
|
61
|
+
...operationContext,
|
|
62
|
+
notifications,
|
|
63
|
+
});
|
|
64
|
+
operationContext.signal.throwIfAborted();
|
|
65
|
+
const output = definition.output.safeParse(rawOutput);
|
|
66
|
+
if (!output.success) {
|
|
67
|
+
throw new OperationValidationError("output", output.error);
|
|
68
|
+
}
|
|
69
|
+
return output.data;
|
|
70
|
+
},
|
|
71
|
+
returnsVoid: definition.output instanceof z.ZodVoid,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
get: (name) => operations.get(name),
|
|
76
|
+
names: [...operations.keys()],
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Owns named runtime resource lookup, validated reads, dependency watchers, and invalidation.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type HandlerContext,
|
|
5
|
+
type ResourceDefinition,
|
|
6
|
+
type RuntimeDisposer,
|
|
7
|
+
} from "../../../public/index";
|
|
8
|
+
import type { ResourceContract } from "../../../public/index";
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createResourceDependencyGraph,
|
|
13
|
+
getInvalidatedResourceIds,
|
|
14
|
+
} from "./resource-dependency-graph";
|
|
15
|
+
import type { RuntimeLifecycle } from "./runtime-lifecycle";
|
|
16
|
+
|
|
17
|
+
export type RuntimeResource = {
|
|
18
|
+
read: (input: unknown, signal?: AbortSignal) => Promise<unknown>;
|
|
19
|
+
subscribe: (
|
|
20
|
+
input: unknown,
|
|
21
|
+
listener: () => void,
|
|
22
|
+
signal?: AbortSignal,
|
|
23
|
+
) => RuntimeDisposer;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RuntimeResourceDefinitions = Readonly<
|
|
27
|
+
Record<string, ResourceDefinition>
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export type RuntimeResources = {
|
|
31
|
+
context: (signal?: AbortSignal) => HandlerContext;
|
|
32
|
+
get: (name: string) => RuntimeResource | undefined;
|
|
33
|
+
invalidate: (contract: ResourceContract, input?: unknown) => void;
|
|
34
|
+
names: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type InvalidationListener = {
|
|
38
|
+
input: unknown;
|
|
39
|
+
listener: () => void;
|
|
40
|
+
resourceId: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type CreateRuntimeResourcesOptions = {
|
|
44
|
+
definitions: RuntimeResourceDefinitions;
|
|
45
|
+
lifecycle: RuntimeLifecycle;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const createRuntimeResources = ({
|
|
49
|
+
definitions,
|
|
50
|
+
lifecycle,
|
|
51
|
+
}: CreateRuntimeResourcesOptions): RuntimeResources => {
|
|
52
|
+
const graph = createResourceDependencyGraph(definitions);
|
|
53
|
+
const invalidationListeners = new Set<InvalidationListener>();
|
|
54
|
+
const contracts = new Map<ResourceContract, string[]>();
|
|
55
|
+
const resources = new Map<string, RuntimeResource>();
|
|
56
|
+
|
|
57
|
+
Object.entries(definitions).forEach(([id, definition]) => {
|
|
58
|
+
contracts.set(definition.contract, [
|
|
59
|
+
...(contracts.get(definition.contract) ?? []),
|
|
60
|
+
id,
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const invalidateResource = (
|
|
65
|
+
id: string,
|
|
66
|
+
input: unknown,
|
|
67
|
+
allInputs = false,
|
|
68
|
+
) => {
|
|
69
|
+
if (lifecycle.signal.aborted) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const ids = getInvalidatedResourceIds(graph, id);
|
|
73
|
+
invalidationListeners.forEach((entry) => {
|
|
74
|
+
if (
|
|
75
|
+
ids.has(entry.resourceId) &&
|
|
76
|
+
(allInputs || isDeepStrictEqual(entry.input, input))
|
|
77
|
+
) {
|
|
78
|
+
entry.listener();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const invalidate = (contract: ResourceContract, input?: unknown) => {
|
|
84
|
+
const ids =
|
|
85
|
+
contracts.get(contract) ??
|
|
86
|
+
Object.entries(definitions)
|
|
87
|
+
.filter(
|
|
88
|
+
([, definition]) =>
|
|
89
|
+
definition.contract.input === contract.input &&
|
|
90
|
+
definition.contract.output === contract.output,
|
|
91
|
+
)
|
|
92
|
+
.map(([id]) => id);
|
|
93
|
+
if (!ids.length) {
|
|
94
|
+
throw new Error("Cannot invalidate a resource outside this runtime");
|
|
95
|
+
}
|
|
96
|
+
ids.forEach((id) => {
|
|
97
|
+
const definition = definitions[id]!;
|
|
98
|
+
const parsedInput =
|
|
99
|
+
input === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: definition.contract.input.parse(input);
|
|
102
|
+
invalidateResource(id, parsedInput, input === undefined);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const context = (signal?: AbortSignal): HandlerContext => ({
|
|
107
|
+
invalidate,
|
|
108
|
+
signal: lifecycle.requestSignal(signal),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const read = async (
|
|
112
|
+
id: string,
|
|
113
|
+
rawInput: unknown,
|
|
114
|
+
signal?: AbortSignal,
|
|
115
|
+
): Promise<unknown> => {
|
|
116
|
+
const definition = definitions[id]!;
|
|
117
|
+
const operationSignal = lifecycle.requestSignal(signal);
|
|
118
|
+
operationSignal.throwIfAborted();
|
|
119
|
+
const input = definition.contract.input.parse(rawInput);
|
|
120
|
+
const rawOutput =
|
|
121
|
+
definition.kind === "derived"
|
|
122
|
+
? definition.combine(
|
|
123
|
+
Object.fromEntries(
|
|
124
|
+
await Promise.all(
|
|
125
|
+
Object.entries(
|
|
126
|
+
definition.dependencies as Readonly<Record<string, string>>,
|
|
127
|
+
).map(async ([name, dependencyId]) => [
|
|
128
|
+
name,
|
|
129
|
+
await read(dependencyId, rawInput, signal),
|
|
130
|
+
]),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
: await definition.read(input, context(signal));
|
|
135
|
+
operationSignal.throwIfAborted();
|
|
136
|
+
return definition.contract.output.parse(rawOutput);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const activateWatchers = (
|
|
140
|
+
id: string,
|
|
141
|
+
rawInput: unknown,
|
|
142
|
+
signal: AbortSignal | undefined,
|
|
143
|
+
activated: Set<string>,
|
|
144
|
+
disposers: RuntimeDisposer[],
|
|
145
|
+
) => {
|
|
146
|
+
if (activated.has(id)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
activated.add(id);
|
|
150
|
+
const definition = definitions[id]!;
|
|
151
|
+
const input = definition.contract.input.parse(rawInput);
|
|
152
|
+
if (definition.kind === "subscription") {
|
|
153
|
+
const dispose = definition.subscribe(
|
|
154
|
+
input,
|
|
155
|
+
() => invalidateResource(id, input),
|
|
156
|
+
context(signal),
|
|
157
|
+
);
|
|
158
|
+
if (typeof dispose !== "function") {
|
|
159
|
+
throw new Error(`Resource ${id} subscribe must return a disposer`);
|
|
160
|
+
}
|
|
161
|
+
disposers.push(dispose);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (definition.kind === "derived") {
|
|
165
|
+
graph.dependencies.get(id)?.forEach((dependencyId) => {
|
|
166
|
+
activateWatchers(dependencyId, rawInput, signal, activated, disposers);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
Object.keys(definitions).forEach((name) => {
|
|
172
|
+
resources.set(name, {
|
|
173
|
+
read: (input, signal) => read(name, input, signal),
|
|
174
|
+
subscribe: (rawInput, listener, signal) => {
|
|
175
|
+
lifecycle.requestSignal(signal).throwIfAborted();
|
|
176
|
+
const input = definitions[name]!.contract.input.parse(rawInput);
|
|
177
|
+
const entry = { input, listener, resourceId: name };
|
|
178
|
+
const watcherDisposers: RuntimeDisposer[] = [];
|
|
179
|
+
invalidationListeners.add(entry);
|
|
180
|
+
try {
|
|
181
|
+
activateWatchers(name, rawInput, signal, new Set(), watcherDisposers);
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
invalidationListeners.delete(entry);
|
|
184
|
+
void lifecycle.disposeAll(watcherDisposers).catch(() => undefined);
|
|
185
|
+
throw cause;
|
|
186
|
+
}
|
|
187
|
+
return lifecycle.track(async () => {
|
|
188
|
+
invalidationListeners.delete(entry);
|
|
189
|
+
await lifecycle.disposeAll(watcherDisposers);
|
|
190
|
+
}, signal);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
context,
|
|
197
|
+
get: (name) => resources.get(name),
|
|
198
|
+
invalidate,
|
|
199
|
+
names: [...resources.keys()],
|
|
200
|
+
};
|
|
201
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Owns named stream registration and ordered, validated session messaging.
|
|
2
|
+
// Session abort, failure, and disposal remain exactly-once across every close path.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
HandlerContext,
|
|
6
|
+
StreamHandlerDefinition,
|
|
7
|
+
} from "../../../public/index";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import type { RuntimeLifecycle } from "./runtime-lifecycle";
|
|
11
|
+
|
|
12
|
+
export type RuntimeStreamSession = {
|
|
13
|
+
dispose: () => Promise<void>;
|
|
14
|
+
send: (message: unknown) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RuntimeStream = {
|
|
18
|
+
open: (
|
|
19
|
+
input: unknown,
|
|
20
|
+
emit: (message: unknown) => void,
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
onError?: (cause: unknown) => void,
|
|
23
|
+
) => Promise<RuntimeStreamSession>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RuntimeStreams = {
|
|
27
|
+
get: (name: string) => RuntimeStream | undefined;
|
|
28
|
+
names: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type AnyStream = StreamHandlerDefinition<
|
|
32
|
+
z.ZodTypeAny,
|
|
33
|
+
z.ZodTypeAny,
|
|
34
|
+
z.ZodTypeAny
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export type RuntimeStreamDefinitions = Readonly<Record<string, AnyStream>>;
|
|
38
|
+
|
|
39
|
+
type CreateRuntimeStreamsOptions = {
|
|
40
|
+
context: (signal?: AbortSignal) => HandlerContext;
|
|
41
|
+
definitions: RuntimeStreamDefinitions;
|
|
42
|
+
lifecycle: RuntimeLifecycle;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const createRuntimeStreams = ({
|
|
46
|
+
context,
|
|
47
|
+
definitions,
|
|
48
|
+
lifecycle,
|
|
49
|
+
}: CreateRuntimeStreamsOptions): RuntimeStreams => {
|
|
50
|
+
const streams = new Map<string, RuntimeStream>();
|
|
51
|
+
Object.entries(definitions).forEach(([name, definition]) => {
|
|
52
|
+
if (!name) {
|
|
53
|
+
throw new Error("Stream names must not be empty");
|
|
54
|
+
}
|
|
55
|
+
streams.set(name, {
|
|
56
|
+
open: async (rawInput, emit, requestSignal, onError) => {
|
|
57
|
+
const sessionController = new AbortController();
|
|
58
|
+
const sessionSignal = requestSignal
|
|
59
|
+
? AbortSignal.any([requestSignal, sessionController.signal])
|
|
60
|
+
: sessionController.signal;
|
|
61
|
+
const operationSignal = lifecycle.requestSignal(sessionSignal);
|
|
62
|
+
operationSignal.throwIfAborted();
|
|
63
|
+
const input = definition.contract.input.parse(rawInput);
|
|
64
|
+
let acceptingMessages = true;
|
|
65
|
+
let failed = false;
|
|
66
|
+
let failure: unknown;
|
|
67
|
+
let opened = false;
|
|
68
|
+
const fail = (cause: unknown) => {
|
|
69
|
+
if (failed || operationSignal.aborted) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
failed = true;
|
|
73
|
+
failure = cause;
|
|
74
|
+
acceptingMessages = false;
|
|
75
|
+
sessionController.abort(cause);
|
|
76
|
+
if (opened) {
|
|
77
|
+
onError?.(cause);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const session = await definition.open(input, {
|
|
81
|
+
...context(sessionSignal),
|
|
82
|
+
emit: (message) => {
|
|
83
|
+
if (operationSignal.aborted) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
emit(definition.contract.serverMessage.parse(message));
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
fail(cause);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
fail,
|
|
93
|
+
signal: operationSignal,
|
|
94
|
+
});
|
|
95
|
+
opened = true;
|
|
96
|
+
if (failed || operationSignal.aborted) {
|
|
97
|
+
await session.dispose?.();
|
|
98
|
+
operationSignal.throwIfAborted();
|
|
99
|
+
throw failure;
|
|
100
|
+
}
|
|
101
|
+
let messages = Promise.resolve();
|
|
102
|
+
const dispose = lifecycle.track(async () => {
|
|
103
|
+
acceptingMessages = false;
|
|
104
|
+
sessionController.abort(new Error("Stream session disposed"));
|
|
105
|
+
await messages.catch(() => undefined);
|
|
106
|
+
await session.dispose?.();
|
|
107
|
+
}, requestSignal);
|
|
108
|
+
return {
|
|
109
|
+
dispose: async () => dispose(),
|
|
110
|
+
send: async (rawMessage) => {
|
|
111
|
+
if (!acceptingMessages) {
|
|
112
|
+
throw new Error("Stream session is closed");
|
|
113
|
+
}
|
|
114
|
+
const message = definition.contract.clientMessage.parse(rawMessage);
|
|
115
|
+
messages = messages.then(async () => {
|
|
116
|
+
operationSignal.throwIfAborted();
|
|
117
|
+
await session.onMessage?.(message);
|
|
118
|
+
operationSignal.throwIfAborted();
|
|
119
|
+
});
|
|
120
|
+
return messages;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
get: (name) => streams.get(name),
|
|
128
|
+
names: [...streams.keys()],
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { getOvermuxPaths } from "./paths";
|
|
6
|
+
|
|
7
|
+
export type ServerLogLevel = "debug" | "error" | "info" | "warn";
|
|
8
|
+
|
|
9
|
+
export type ServerLogEntry = {
|
|
10
|
+
// Groups related request, connection, and operation logs for diagnostics.
|
|
11
|
+
correlationId?: string;
|
|
12
|
+
details?: Record<string, unknown>;
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
event: string;
|
|
15
|
+
level?: ServerLogLevel;
|
|
16
|
+
message?: string;
|
|
17
|
+
source?: "browser" | "server";
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ServerLogger = {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
id: () => string;
|
|
24
|
+
log: (entry: ServerLogEntry) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ConfigurableServerLogger = ServerLogger & {
|
|
28
|
+
setEnabled: (enabled: boolean) => void;
|
|
29
|
+
setLogFile: (logFile: string) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const lifecycleEvents = new Set([
|
|
33
|
+
"server-start",
|
|
34
|
+
"server-stop",
|
|
35
|
+
"websocket-close",
|
|
36
|
+
"websocket-close-event",
|
|
37
|
+
"websocket-connect",
|
|
38
|
+
"websocket-open",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const getDefaultServerLogFile = ({
|
|
42
|
+
homeDir,
|
|
43
|
+
xdgStateHome,
|
|
44
|
+
}: {
|
|
45
|
+
homeDir?: string;
|
|
46
|
+
xdgStateHome?: string;
|
|
47
|
+
} = {}) =>
|
|
48
|
+
join(getOvermuxPaths({ homeDir, xdgStateHome }).stateDir, "overmux.log");
|
|
49
|
+
|
|
50
|
+
const safeJson = (value: unknown) => {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
} catch {
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
event: "log-serialization-error",
|
|
56
|
+
level: "error",
|
|
57
|
+
source: "server",
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const createServerLogger = ({
|
|
64
|
+
enabled,
|
|
65
|
+
logFile = getDefaultServerLogFile(),
|
|
66
|
+
}: {
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
logFile?: string;
|
|
69
|
+
}): ConfigurableServerLogger => {
|
|
70
|
+
let activeLogFile = logFile;
|
|
71
|
+
let debugEnabled = enabled;
|
|
72
|
+
return {
|
|
73
|
+
get enabled() {
|
|
74
|
+
return debugEnabled;
|
|
75
|
+
},
|
|
76
|
+
id: randomUUID,
|
|
77
|
+
log: (entry) => {
|
|
78
|
+
if (
|
|
79
|
+
!debugEnabled &&
|
|
80
|
+
entry.level !== "error" &&
|
|
81
|
+
!lifecycleEvents.has(entry.event)
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const record = safeJson({
|
|
86
|
+
timestamp: entry.timestamp ?? new Date().toISOString(),
|
|
87
|
+
level: entry.level ?? "info",
|
|
88
|
+
source: entry.source ?? "server",
|
|
89
|
+
...entry,
|
|
90
|
+
});
|
|
91
|
+
mkdirSync(dirname(activeLogFile), { recursive: true });
|
|
92
|
+
appendFileSync(activeLogFile, `${record}\n`, "utf8");
|
|
93
|
+
process.stderr.write(`[overmux:debug] ${record}\n`);
|
|
94
|
+
},
|
|
95
|
+
setEnabled: (nextEnabled) => {
|
|
96
|
+
debugEnabled = nextEnabled;
|
|
97
|
+
},
|
|
98
|
+
setLogFile: (nextLogFile) => {
|
|
99
|
+
activeLogFile = nextLogFile;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const errorMessage = (cause: unknown) =>
|
|
105
|
+
cause instanceof Error ? cause.message : String(cause);
|
|
106
|
+
|
|
107
|
+
export const errorDetails = (cause: unknown) => ({
|
|
108
|
+
message: errorMessage(cause),
|
|
109
|
+
...(cause instanceof Error && cause.stack ? { stack: cause.stack } : {}),
|
|
110
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serverConfigRuntimeSchema,
|
|
3
|
+
type RuntimeConfigDefinition,
|
|
4
|
+
} from "@overmux/shared/node";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import type { ServerConfigDefinition } from "../../public/index";
|
|
9
|
+
|
|
10
|
+
export type ResolvedServerStartupOptions = {
|
|
11
|
+
host: string;
|
|
12
|
+
logFile?: string;
|
|
13
|
+
port: number;
|
|
14
|
+
productionWebAssetsDir?: string;
|
|
15
|
+
watch: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const resolveServerStartupOptions = ({
|
|
19
|
+
config,
|
|
20
|
+
configPath,
|
|
21
|
+
overrides = {},
|
|
22
|
+
}: {
|
|
23
|
+
config: RuntimeConfigDefinition;
|
|
24
|
+
configPath: string;
|
|
25
|
+
overrides?: ServerConfigDefinition;
|
|
26
|
+
}): ResolvedServerStartupOptions => {
|
|
27
|
+
const validatedOverrides = serverConfigRuntimeSchema
|
|
28
|
+
.omit({ port: true })
|
|
29
|
+
.extend({ port: z.number().int().min(0).max(65_535).optional() })
|
|
30
|
+
.parse(overrides);
|
|
31
|
+
const configuredLogFile = config.logFile
|
|
32
|
+
? resolve(dirname(configPath), config.logFile)
|
|
33
|
+
: undefined;
|
|
34
|
+
const logFile = validatedOverrides.logFile
|
|
35
|
+
? resolve(validatedOverrides.logFile)
|
|
36
|
+
: configuredLogFile;
|
|
37
|
+
const configuredWebAssetsDir = config.productionWebAssetsDir
|
|
38
|
+
? resolve(dirname(configPath), config.productionWebAssetsDir)
|
|
39
|
+
: undefined;
|
|
40
|
+
const productionWebAssetsDir = validatedOverrides.productionWebAssetsDir
|
|
41
|
+
? resolve(validatedOverrides.productionWebAssetsDir)
|
|
42
|
+
: configuredWebAssetsDir;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
host: validatedOverrides.host ?? config.host ?? "localhost",
|
|
46
|
+
...(logFile === undefined ? {} : { logFile }),
|
|
47
|
+
port: validatedOverrides.port ?? config.port ?? 4200,
|
|
48
|
+
...(productionWebAssetsDir === undefined ? {} : { productionWebAssetsDir }),
|
|
49
|
+
watch: validatedOverrides.watch ?? config.watch ?? true,
|
|
50
|
+
};
|
|
51
|
+
};
|