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.
Files changed (193) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +7 -0
  3. package/dist/auth/.vite/manifest.json +11 -0
  4. package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
  5. package/dist/auth/assets/index-CHIE7qYj.js +72 -0
  6. package/dist/auth/index.html +14 -0
  7. package/dist/bin.js +4504 -7870
  8. package/dist/bin.js.map +1 -0
  9. package/dist/docs/000-index.md +21 -0
  10. package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
  11. package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
  12. package/dist/docs/100-introduction/300-why-overmux.md +22 -0
  13. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  14. package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  15. package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  16. package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  17. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  18. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  19. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  20. package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
  21. package/dist/docs/300-fundamentals/200-configuration.md +3 -0
  22. package/dist/docs/300-fundamentals/300-theming.md +3 -0
  23. package/dist/docs/300-fundamentals/400-server.md +3 -0
  24. package/dist/docs/300-fundamentals/500-client.md +3 -0
  25. package/dist/docs/300-fundamentals/600-operations.md +3 -0
  26. package/dist/docs/300-fundamentals/700-resources.md +3 -0
  27. package/dist/docs/300-fundamentals/800-streams.md +3 -0
  28. package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  29. package/dist/docs/400-reference/100-configuration.md +3 -0
  30. package/dist/docs/400-reference/200-server-api.md +3 -0
  31. package/dist/docs/400-reference/300-client-api.md +3 -0
  32. package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
  33. package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
  34. package/dist/docs/400-reference/400-cli/300-call.md +28 -0
  35. package/dist/docs/400-reference/400-cli/400-check.md +20 -0
  36. package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
  37. package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
  38. package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
  39. package/dist/docs/500-hosted-pages.md +31 -0
  40. package/dist/exports/client.d.ts +237 -0
  41. package/dist/exports/client.d.ts.map +1 -0
  42. package/dist/exports/client.js +1643 -0
  43. package/dist/exports/client.js.map +1 -0
  44. package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
  45. package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
  46. package/dist/exports/index.d.ts +2 -0
  47. package/dist/exports/index.js +49 -0
  48. package/dist/exports/index.js.map +1 -0
  49. package/dist/exports/notifications-kDp16bU_.js +56 -0
  50. package/dist/exports/notifications-kDp16bU_.js.map +1 -0
  51. package/dist/exports/server.d.ts +63 -0
  52. package/dist/exports/server.d.ts.map +1 -0
  53. package/dist/exports/server.js +1120 -0
  54. package/dist/exports/server.js.map +1 -0
  55. package/dist/exports/style.css +368 -0
  56. package/dist/internal/server/coordinator/server-child.d.ts +1 -0
  57. package/dist/internal/server/coordinator/server-child.js +3299 -0
  58. package/dist/internal/server/coordinator/server-child.js.map +1 -0
  59. package/docs/000-index.md +21 -0
  60. package/docs/100-introduction/100-what-is-overmux.md +7 -0
  61. package/docs/100-introduction/200-how-overmux-works.md +7 -0
  62. package/docs/100-introduction/300-why-overmux.md +22 -0
  63. package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  64. package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  65. package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  66. package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  67. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  68. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  69. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  70. package/docs/300-fundamentals/100-project-structure.md +3 -0
  71. package/docs/300-fundamentals/200-configuration.md +3 -0
  72. package/docs/300-fundamentals/300-theming.md +3 -0
  73. package/docs/300-fundamentals/400-server.md +3 -0
  74. package/docs/300-fundamentals/500-client.md +3 -0
  75. package/docs/300-fundamentals/600-operations.md +3 -0
  76. package/docs/300-fundamentals/700-resources.md +3 -0
  77. package/docs/300-fundamentals/800-streams.md +3 -0
  78. package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  79. package/docs/400-reference/100-configuration.md +3 -0
  80. package/docs/400-reference/200-server-api.md +3 -0
  81. package/docs/400-reference/300-client-api.md +3 -0
  82. package/docs/400-reference/400-cli/100-serve.md +33 -0
  83. package/docs/400-reference/400-cli/200-auth.md +52 -0
  84. package/docs/400-reference/400-cli/300-call.md +28 -0
  85. package/docs/400-reference/400-cli/400-check.md +20 -0
  86. package/docs/400-reference/400-cli/500-ai-context.md +102 -0
  87. package/docs/400-reference/400-cli/600-docs.md +23 -0
  88. package/docs/400-reference/400-cli/700-desktop.md +35 -0
  89. package/docs/500-hosted-pages.md +31 -0
  90. package/package.json +59 -18
  91. package/src/internal/cli/app.ts +72 -0
  92. package/src/internal/cli/bin.ts +3 -0
  93. package/src/internal/cli/commands/ai.ts +44 -0
  94. package/src/internal/cli/commands/auth.ts +195 -0
  95. package/src/internal/cli/commands/call.ts +128 -0
  96. package/src/internal/cli/commands/check.ts +48 -0
  97. package/src/internal/cli/commands/desktop-installer.ts +446 -0
  98. package/src/internal/cli/commands/desktop-release.ts +190 -0
  99. package/src/internal/cli/commands/desktop.ts +47 -0
  100. package/src/internal/cli/commands/docs.ts +34 -0
  101. package/src/internal/cli/commands/serve.ts +322 -0
  102. package/src/internal/cli/environment.ts +54 -0
  103. package/src/internal/cli/login.ts +46 -0
  104. package/src/internal/cli/managed-vite.ts +267 -0
  105. package/src/internal/cli/parse-port.ts +8 -0
  106. package/src/internal/client/auth/auth-api.ts +53 -0
  107. package/src/internal/client/auth/auth-shell.css +74 -0
  108. package/src/internal/client/auth/auth-shell.tsx +167 -0
  109. package/src/internal/client/auth/entry.tsx +26 -0
  110. package/src/internal/client/auth/index.html +13 -0
  111. package/src/internal/client/background-notifications.ts +194 -0
  112. package/src/internal/client/browser-api.ts +97 -0
  113. package/src/internal/client/client-definition.ts +184 -0
  114. package/src/internal/client/commands.tsx +260 -0
  115. package/src/internal/client/host/browser-log-forwarding.ts +149 -0
  116. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
  117. package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
  118. package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
  119. package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
  120. package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
  121. package/src/internal/client/host/notification-forwarding.ts +22 -0
  122. package/src/internal/client/host/overmux-host.tsx +200 -0
  123. package/src/internal/client/host/recovery-screen.css +75 -0
  124. package/src/internal/client/host/recovery-screen.tsx +17 -0
  125. package/src/internal/client/host/update-popover.css +82 -0
  126. package/src/internal/client/host/update-popover.tsx +166 -0
  127. package/src/internal/client/index.ts +45 -0
  128. package/src/internal/client/overmux-react.ts +283 -0
  129. package/src/internal/client/shortcuts.tsx +373 -0
  130. package/src/internal/client/theme-scope.css +114 -0
  131. package/src/internal/client/theme-scope.tsx +107 -0
  132. package/src/internal/client/transport.ts +69 -0
  133. package/src/internal/client/websocket.ts +441 -0
  134. package/src/internal/server/auth/auth-http.ts +414 -0
  135. package/src/internal/server/auth/auth-service.ts +627 -0
  136. package/src/internal/server/auth/auth-shell-http.ts +90 -0
  137. package/src/internal/server/auth/instance-control.ts +381 -0
  138. package/src/internal/server/check-overmux.ts +364 -0
  139. package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
  140. package/src/internal/server/coordinator/server-child.ts +108 -0
  141. package/src/internal/server/coordinator/server-coordinator.ts +262 -0
  142. package/src/internal/server/coordinator/update-watcher.ts +54 -0
  143. package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
  144. package/src/internal/server/http/background-notification-handlers.ts +83 -0
  145. package/src/internal/server/http/control-handlers.ts +44 -0
  146. package/src/internal/server/http/create-http-app.ts +53 -0
  147. package/src/internal/server/http/development-web-backend.ts +83 -0
  148. package/src/internal/server/http/operation-handler.ts +93 -0
  149. package/src/internal/server/http/request-logging.ts +34 -0
  150. package/src/internal/server/http/routes.ts +241 -0
  151. package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
  152. package/src/internal/server/index.ts +16 -0
  153. package/src/internal/server/notifications/background-notification-payload.ts +74 -0
  154. package/src/internal/server/notifications/background-notification-service.ts +139 -0
  155. package/src/internal/server/notifications/background-notification-store.ts +162 -0
  156. package/src/internal/server/notifications/notification-service.ts +70 -0
  157. package/src/internal/server/notifications/private-json-file.ts +84 -0
  158. package/src/internal/server/paths.ts +13 -0
  159. package/src/internal/server/rate-limiter.ts +58 -0
  160. package/src/internal/server/runtime/create-runtime.ts +87 -0
  161. package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
  162. package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
  163. package/src/internal/server/runtime/runtime-operations.ts +78 -0
  164. package/src/internal/server/runtime/runtime-resources.ts +201 -0
  165. package/src/internal/server/runtime/runtime-streams.ts +130 -0
  166. package/src/internal/server/server-logger.ts +110 -0
  167. package/src/internal/server/server-startup-options.ts +51 -0
  168. package/src/internal/server/start-application-server.ts +412 -0
  169. package/src/internal/server/start-overmux-server.ts +26 -0
  170. package/src/internal/server/test-port.ts +23 -0
  171. package/src/internal/server/vite-websocket-proxy.ts +201 -0
  172. package/src/internal/server/websocket-connection.ts +586 -0
  173. package/src/internal/server/websocket-limits.ts +2 -0
  174. package/src/internal/server/websocket-server.ts +311 -0
  175. package/src/internal/shared/background-notifications.ts +37 -0
  176. package/src/internal/shared/index.ts +2 -0
  177. package/src/internal/shared/protocol.ts +317 -0
  178. package/src/internal/shared/routes.ts +3 -0
  179. package/src/public/ai-context.ts +44 -0
  180. package/src/public/client.ts +53 -0
  181. package/src/public/config.ts +287 -0
  182. package/src/public/contracts.ts +55 -0
  183. package/src/public/index.ts +57 -0
  184. package/src/public/notifications.ts +10 -0
  185. package/src/public/server.ts +16 -0
  186. package/dist/public/core.ts +0 -119
  187. package/dist/public/protocol.ts +0 -51
  188. package/dist/public/terminal.ts +0 -23
  189. package/dist/web/assets/index-DC___IUw.css +0 -2
  190. package/dist/web/assets/index-DF9Cptse.js +0 -74
  191. package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
  192. package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
  193. package/dist/web/index.html +0 -14
@@ -0,0 +1,586 @@
1
+ // Owns protocol and lifecycle state for one WebSocket connection.
2
+ // Independent resource, stream, notification, diagnostics, and lifecycle traffic shares it.
3
+ import {
4
+ clientProtocolMessageSchema,
5
+ decodeProtocolFrame,
6
+ encodeProtocolFrame,
7
+ serverProtocolMessageSchema,
8
+ type ClientProtocolMessage,
9
+ type ServerProtocolMessage,
10
+ } from "../shared/index";
11
+ import { WebSocket, type RawData } from "ws";
12
+ import { ZodError } from "zod";
13
+
14
+ import {
15
+ errorDetails as diagnosticErrorDetails,
16
+ type ServerLogger,
17
+ } from "./server-logger";
18
+ import type { Runtime } from "./runtime/create-runtime";
19
+ import type { RuntimeStreamSession } from "./runtime/runtime-streams";
20
+ import { maxWebSocketBufferedBytes } from "./websocket-limits";
21
+
22
+ const maxPendingStreamOutputBytes = 1_048_576;
23
+
24
+ type ErrorCode = "bad-request" | "conflict" | "internal" | "not-found";
25
+
26
+ type SubscriptionEntry = {
27
+ controller: AbortController;
28
+ dispose?: () => Promise<void> | void;
29
+ };
30
+
31
+ type OperationIdentity = {
32
+ operationId: string;
33
+ operationType: string;
34
+ requestedName?: string;
35
+ };
36
+
37
+ type StreamEntry = {
38
+ controller: AbortController;
39
+ identity: OperationIdentity;
40
+ session?: RuntimeStreamSession;
41
+ };
42
+
43
+ type ConnectionState = {
44
+ closed: boolean;
45
+ connectionId?: string;
46
+ operations: Map<string, AbortController>;
47
+ runtime: Runtime;
48
+ serverLogger?: ServerLogger;
49
+ socket: WebSocket;
50
+ streams: Map<string, StreamEntry>;
51
+ subscriptions: Map<string, SubscriptionEntry>;
52
+ };
53
+
54
+ class OperationError extends Error {
55
+ constructor(
56
+ readonly code: ErrorCode,
57
+ message: string,
58
+ ) {
59
+ super(message);
60
+ }
61
+ }
62
+
63
+ const bytesOf = (data: RawData) => {
64
+ if (Array.isArray(data)) {
65
+ return Buffer.concat(data);
66
+ }
67
+ if (data instanceof ArrayBuffer) {
68
+ return new Uint8Array(data);
69
+ }
70
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
71
+ };
72
+
73
+ const textOf = (data: RawData) => Buffer.from(bytesOf(data)).toString("utf8");
74
+
75
+ const frameLength = (message: unknown) => {
76
+ const frame = encodeProtocolFrame(message);
77
+ return typeof frame === "string"
78
+ ? Buffer.byteLength(frame, "utf8")
79
+ : frame.byteLength;
80
+ };
81
+
82
+ const operationIdOf = (value: unknown) => {
83
+ if (typeof value !== "object" || value === null) {
84
+ return undefined;
85
+ }
86
+ const operationId = (value as Record<string, unknown>).operationId;
87
+ return typeof operationId === "string" && operationId.length <= 256
88
+ ? operationId
89
+ : undefined;
90
+ };
91
+
92
+ const errorDetails = (cause: unknown): { code: ErrorCode; message: string } => {
93
+ if (cause instanceof OperationError) {
94
+ return { code: cause.code, message: cause.message };
95
+ }
96
+ if (cause instanceof ZodError || cause instanceof SyntaxError) {
97
+ return {
98
+ code: "bad-request",
99
+ message:
100
+ cause instanceof ZodError
101
+ ? cause.issues
102
+ .map(
103
+ (issue) =>
104
+ `${issue.path.join(".") || "value"}: ${issue.message}`,
105
+ )
106
+ .join("; ")
107
+ : cause.message,
108
+ };
109
+ }
110
+ return {
111
+ code: "internal",
112
+ message: cause instanceof Error ? cause.message : String(cause),
113
+ };
114
+ };
115
+
116
+ const send = (state: ConnectionState, message: ServerProtocolMessage) => {
117
+ if (state.closed || state.socket.readyState !== WebSocket.OPEN) {
118
+ return false;
119
+ }
120
+ // Apply outbound backpressure before queued messages consume unbounded memory.
121
+ if (state.socket.bufferedAmount > maxWebSocketBufferedBytes) {
122
+ state.socket.close(1013, "client could not keep up");
123
+ return false;
124
+ }
125
+ state.socket.send(
126
+ encodeProtocolFrame(serverProtocolMessageSchema.parse(message)),
127
+ );
128
+ return true;
129
+ };
130
+
131
+ const sendError = (
132
+ state: ConnectionState,
133
+ cause: unknown,
134
+ operationId?: string,
135
+ identity?: OperationIdentity,
136
+ ) => {
137
+ const error = errorDetails(cause);
138
+ state.serverLogger?.log({
139
+ correlationId: identity?.operationId ?? operationId ?? state.connectionId,
140
+ details: {
141
+ code: error.code,
142
+ ...identity,
143
+ ...diagnosticErrorDetails(cause),
144
+ },
145
+ event: "protocol-handler-error",
146
+ level: "error",
147
+ });
148
+ send(state, { ...error, operationId, type: "error" });
149
+ };
150
+
151
+ const disposeSubscription = async (
152
+ state: ConnectionState,
153
+ subscriptionId: string,
154
+ ) => {
155
+ const entry = state.subscriptions.get(subscriptionId);
156
+ if (!entry) {
157
+ return;
158
+ }
159
+ state.subscriptions.delete(subscriptionId);
160
+ entry.controller.abort(new Error("Resource subscription closed"));
161
+ await entry.dispose?.();
162
+ };
163
+
164
+ const disposeStream = async (
165
+ state: ConnectionState,
166
+ streamId: string,
167
+ notify = true,
168
+ ) => {
169
+ const entry = state.streams.get(streamId);
170
+ if (!entry) {
171
+ if (notify) {
172
+ send(state, { streamId, type: "stream-closed" });
173
+ }
174
+ return;
175
+ }
176
+ state.streams.delete(streamId);
177
+ entry.controller.abort(new Error("Stream closed"));
178
+ await entry.session?.dispose();
179
+ if (notify) {
180
+ send(state, { streamId, type: "stream-closed" });
181
+ }
182
+ };
183
+
184
+ const disposeConnectionOperations = async (
185
+ state: ConnectionState,
186
+ notify: boolean,
187
+ ) => {
188
+ const operationControllers = [...state.operations.values()];
189
+ state.operations.clear();
190
+ operationControllers.forEach((controller) =>
191
+ controller.abort(new Error("WebSocket connection closed")),
192
+ );
193
+ const subscriptionIds = [...state.subscriptions.keys()];
194
+ const streamIds = [...state.streams.keys()];
195
+ await Promise.allSettled([
196
+ ...subscriptionIds.map((subscriptionId) =>
197
+ disposeSubscription(state, subscriptionId),
198
+ ),
199
+ ...streamIds.map((streamId) => disposeStream(state, streamId, notify)),
200
+ ]);
201
+ };
202
+
203
+ const runOperation = (
204
+ state: ConnectionState,
205
+ identity: OperationIdentity,
206
+ operation: (signal: AbortSignal) => Promise<void>,
207
+ ) => {
208
+ if (state.operations.has(identity.operationId)) {
209
+ sendError(
210
+ state,
211
+ new OperationError("conflict", "Operation ID is already active"),
212
+ identity.operationId,
213
+ identity,
214
+ );
215
+ return;
216
+ }
217
+ const controller = new AbortController();
218
+ const startedAt = Date.now();
219
+ state.operations.set(identity.operationId, controller);
220
+ state.serverLogger?.log({
221
+ correlationId: identity.operationId,
222
+ details: identity,
223
+ event: "operation-start",
224
+ });
225
+ void operation(controller.signal)
226
+ .then(() => {
227
+ state.serverLogger?.log({
228
+ correlationId: identity.operationId,
229
+ details: identity,
230
+ durationMs: Date.now() - startedAt,
231
+ event: "operation-complete",
232
+ });
233
+ })
234
+ .catch((cause: unknown) => {
235
+ if (!controller.signal.aborted && !state.closed) {
236
+ state.serverLogger?.log({
237
+ correlationId: identity.operationId,
238
+ details: { ...identity, ...diagnosticErrorDetails(cause) },
239
+ durationMs: Date.now() - startedAt,
240
+ event: "operation-error",
241
+ level: "error",
242
+ });
243
+ sendError(state, cause, identity.operationId, identity);
244
+ }
245
+ })
246
+ .finally(() => {
247
+ if (state.operations.get(identity.operationId) === controller) {
248
+ state.operations.delete(identity.operationId);
249
+ }
250
+ });
251
+ };
252
+
253
+ const readResource = (
254
+ state: ConnectionState,
255
+ message: Extract<ClientProtocolMessage, { type: "resource-read" }>,
256
+ ) => {
257
+ const resource = state.runtime.getResource(message.resourceName);
258
+ const identity: OperationIdentity = {
259
+ operationId: message.operationId,
260
+ operationType: message.type,
261
+ requestedName: message.resourceName,
262
+ };
263
+ runOperation(state, identity, async (signal) => {
264
+ if (!resource) {
265
+ throw new OperationError("not-found", "Resource not found");
266
+ }
267
+ const output = await resource.read(message.input, signal);
268
+ send(state, {
269
+ operationId: message.operationId,
270
+ output,
271
+ type: "resource-result",
272
+ });
273
+ });
274
+ };
275
+
276
+ const subscribeResource = (
277
+ state: ConnectionState,
278
+ message: Extract<ClientProtocolMessage, { type: "resource-subscribe" }>,
279
+ ) => {
280
+ const resource = state.runtime.getResource(message.resourceName);
281
+ const identity: OperationIdentity = {
282
+ operationId: message.operationId,
283
+ operationType: message.type,
284
+ requestedName: message.resourceName,
285
+ };
286
+ if (state.subscriptions.has(message.subscriptionId)) {
287
+ sendError(
288
+ state,
289
+ new OperationError("conflict", "Subscription ID is already active"),
290
+ message.operationId,
291
+ identity,
292
+ );
293
+ return;
294
+ }
295
+ runOperation(state, identity, async (operationSignal) => {
296
+ if (!resource) {
297
+ throw new OperationError("not-found", "Resource not found");
298
+ }
299
+ const controller = new AbortController();
300
+ const entry: SubscriptionEntry = { controller };
301
+ state.subscriptions.set(message.subscriptionId, entry);
302
+ const signal = AbortSignal.any([operationSignal, controller.signal]);
303
+ try {
304
+ entry.dispose = resource.subscribe(
305
+ message.input,
306
+ () =>
307
+ void send(state, {
308
+ subscriptionId: message.subscriptionId,
309
+ type: "resource-invalidated",
310
+ }),
311
+ signal,
312
+ );
313
+ } catch (cause) {
314
+ await disposeSubscription(state, message.subscriptionId);
315
+ throw cause;
316
+ }
317
+ });
318
+ };
319
+
320
+ const openStream = (
321
+ state: ConnectionState,
322
+ message: Extract<ClientProtocolMessage, { type: "stream-open" }>,
323
+ ) => {
324
+ const stream = state.runtime.getStream(message.streamName);
325
+ const identity: OperationIdentity = {
326
+ operationId: message.operationId,
327
+ operationType: message.type,
328
+ requestedName: message.streamName,
329
+ };
330
+ if (state.streams.has(message.streamId)) {
331
+ sendError(
332
+ state,
333
+ new OperationError("conflict", "Stream ID is already active"),
334
+ message.operationId,
335
+ identity,
336
+ );
337
+ return;
338
+ }
339
+ runOperation(state, identity, async (operationSignal) => {
340
+ if (!stream) {
341
+ throw new OperationError("not-found", "Stream not found");
342
+ }
343
+ const controller = new AbortController();
344
+ const entry: StreamEntry = { controller, identity };
345
+ state.streams.set(message.streamId, entry);
346
+ const pending: unknown[] = [];
347
+ let pendingBytes = 0;
348
+ let opened = false;
349
+ try {
350
+ entry.session = await stream.open(
351
+ message.input,
352
+ (output) => {
353
+ if (opened) {
354
+ const sent = send(state, {
355
+ message: output,
356
+ streamId: message.streamId,
357
+ type: "stream-output",
358
+ });
359
+ if (!sent) {
360
+ throw new Error("Stream output could not be delivered");
361
+ }
362
+ return;
363
+ }
364
+ pendingBytes += frameLength(output);
365
+ if (pendingBytes > maxPendingStreamOutputBytes) {
366
+ throw new Error("Stream produced too much output while opening");
367
+ }
368
+ pending.push(output);
369
+ },
370
+ AbortSignal.any([operationSignal, controller.signal]),
371
+ (cause) => {
372
+ sendError(state, cause, message.streamId, identity);
373
+ void disposeStream(state, message.streamId).catch(
374
+ (disposeCause: unknown) => {
375
+ sendError(state, disposeCause, message.streamId, identity);
376
+ },
377
+ );
378
+ },
379
+ );
380
+ send(state, {
381
+ operationId: message.operationId,
382
+ streamId: message.streamId,
383
+ type: "stream-opened",
384
+ });
385
+ opened = true;
386
+ for (const output of pending) {
387
+ if (
388
+ !send(state, {
389
+ message: output,
390
+ streamId: message.streamId,
391
+ type: "stream-output",
392
+ })
393
+ ) {
394
+ throw new Error("Stream output could not be delivered");
395
+ }
396
+ }
397
+ } catch (cause) {
398
+ await disposeStream(state, message.streamId, false);
399
+ throw cause;
400
+ }
401
+ });
402
+ };
403
+
404
+ const sendStreamMessage = (
405
+ state: ConnectionState,
406
+ message: Extract<ClientProtocolMessage, { type: "stream-message" }>,
407
+ ) => {
408
+ const entry = state.streams.get(message.streamId);
409
+ const identity: OperationIdentity = {
410
+ ...entry?.identity,
411
+ operationId: message.streamId,
412
+ operationType: message.type,
413
+ };
414
+ if (!entry?.session) {
415
+ sendError(
416
+ state,
417
+ new OperationError("not-found", "Stream not found"),
418
+ message.streamId,
419
+ identity,
420
+ );
421
+ return;
422
+ }
423
+ void entry.session.send(message.message).catch((cause: unknown) => {
424
+ sendError(state, cause, message.streamId, identity);
425
+ void disposeStream(state, message.streamId).catch(
426
+ (disposeCause: unknown) => {
427
+ sendError(state, disposeCause, message.streamId, identity);
428
+ },
429
+ );
430
+ });
431
+ };
432
+
433
+ const logClientDiagnostic = (
434
+ state: ConnectionState,
435
+ message: Extract<ClientProtocolMessage, { type: "client-diagnostic" }>,
436
+ ) => {
437
+ if (!state.serverLogger?.enabled) {
438
+ return;
439
+ }
440
+ state.serverLogger.log({
441
+ details: {
442
+ arguments: message.arguments,
443
+ ...(message.stack ? { stack: message.stack } : {}),
444
+ ...(message.url ? { url: message.url } : {}),
445
+ },
446
+ event: "browser-console",
447
+ level: message.level === "log" ? "info" : message.level,
448
+ message: message.message,
449
+ source: "browser",
450
+ timestamp: message.timestamp,
451
+ });
452
+ };
453
+
454
+ const handleMessage = (
455
+ state: ConnectionState,
456
+ message: ClientProtocolMessage,
457
+ ) => {
458
+ if (message.type === "client-diagnostic") {
459
+ logClientDiagnostic(state, message);
460
+ return;
461
+ }
462
+ if (message.type === "resource-read") {
463
+ readResource(state, message);
464
+ return;
465
+ }
466
+ if (message.type === "resource-subscribe") {
467
+ subscribeResource(state, message);
468
+ return;
469
+ }
470
+ if (message.type === "resource-unsubscribe") {
471
+ void disposeSubscription(state, message.subscriptionId).catch(
472
+ (cause: unknown) => {
473
+ sendError(state, cause, message.subscriptionId);
474
+ },
475
+ );
476
+ return;
477
+ }
478
+ if (message.type === "stream-open") {
479
+ openStream(state, message);
480
+ return;
481
+ }
482
+ if (message.type === "stream-close") {
483
+ void disposeStream(state, message.streamId).catch((cause: unknown) => {
484
+ sendError(state, cause, message.streamId);
485
+ });
486
+ return;
487
+ }
488
+ sendStreamMessage(state, message);
489
+ };
490
+
491
+ const handleFrame = (
492
+ state: ConnectionState,
493
+ data: RawData,
494
+ isBinary: boolean,
495
+ ) => {
496
+ let raw: unknown;
497
+ try {
498
+ raw = decodeProtocolFrame(isBinary ? bytesOf(data) : textOf(data));
499
+ const result = clientProtocolMessageSchema.safeParse(raw);
500
+ if (!result.success) {
501
+ state.serverLogger?.log({
502
+ correlationId: state.connectionId,
503
+ details: diagnosticErrorDetails(result.error),
504
+ event: "protocol-parse-error",
505
+ level: "error",
506
+ });
507
+ sendError(state, result.error, operationIdOf(raw));
508
+ return;
509
+ }
510
+ handleMessage(state, result.data);
511
+ } catch (cause) {
512
+ state.serverLogger?.log({
513
+ correlationId: state.connectionId,
514
+ details: diagnosticErrorDetails(cause),
515
+ event: "protocol-parse-error",
516
+ level: "error",
517
+ });
518
+ sendError(state, cause, operationIdOf(raw));
519
+ }
520
+ };
521
+
522
+ const disposeConnection = async (state: ConnectionState) => {
523
+ if (state.closed) {
524
+ return;
525
+ }
526
+ state.closed = true;
527
+ state.serverLogger?.log({
528
+ correlationId: state.connectionId,
529
+ event: "websocket-close",
530
+ });
531
+ await disposeConnectionOperations(state, false);
532
+ };
533
+
534
+ const createConnectionState = ({
535
+ runtime,
536
+ serverLogger,
537
+ socket,
538
+ }: {
539
+ runtime: Runtime;
540
+ serverLogger?: ServerLogger;
541
+ socket: WebSocket;
542
+ }): ConnectionState => ({
543
+ closed: false,
544
+ connectionId: serverLogger?.id(),
545
+ operations: new Map(),
546
+ runtime,
547
+ serverLogger,
548
+ socket,
549
+ streams: new Map(),
550
+ subscriptions: new Map(),
551
+ });
552
+
553
+ export const attachWebSocketConnection = ({
554
+ runtime,
555
+ serverLogger,
556
+ socket,
557
+ }: {
558
+ runtime: Runtime;
559
+ serverLogger?: ServerLogger;
560
+ socket: WebSocket;
561
+ }) => {
562
+ const state = createConnectionState({ runtime, serverLogger, socket });
563
+ serverLogger?.log({
564
+ correlationId: state.connectionId,
565
+ event: "websocket-open",
566
+ });
567
+ socket.on("message", (data, isBinary) => handleFrame(state, data, isBinary));
568
+ socket.once("close", (code, reason) => {
569
+ serverLogger?.log({
570
+ correlationId: state.connectionId,
571
+ details: { code, reason: reason?.toString() ?? "" },
572
+ event: "websocket-close-event",
573
+ });
574
+ void disposeConnection(state);
575
+ });
576
+ socket.once("error", (cause) => {
577
+ serverLogger?.log({
578
+ correlationId: state.connectionId,
579
+ details: diagnosticErrorDetails(cause),
580
+ event: "websocket-error",
581
+ level: "error",
582
+ });
583
+ void disposeConnection(state);
584
+ });
585
+ return () => disposeConnection(state);
586
+ };
@@ -0,0 +1,2 @@
1
+ // Shares the server's outbound WebSocket backpressure policy.
2
+ export const maxWebSocketBufferedBytes = 1_048_576;