devframe 0.8.2 → 0.9.0-beta.1
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/dist/_shared-CttmCMBi.mjs +70 -0
- package/dist/_shared-DmpWW-HH.d.mts +43 -0
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +6 -5
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +157 -1
- package/dist/adapters/dev.d.mts +20 -42
- package/dist/adapters/dev.mjs +2 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +223 -0
- package/dist/adapters/initiate.mjs +2 -0
- package/dist/adapters/mcp.d.mts +26 -2
- package/dist/adapters/mcp.mjs +2 -2
- package/dist/cli/main.mjs +2 -2
- package/dist/client/index.d.mts +70 -6
- package/dist/client/index.mjs +743 -408
- package/dist/constants.d.mts +19 -6
- package/dist/constants.mjs +19 -6
- package/dist/{context-CBg8iRbQ.mjs → context-C33rR7n0.mjs} +1 -1
- package/dist/{context-DoSozfvx.d.mts → context-CSCwoABx.d.mts} +1 -1
- package/dist/{host-h3-D2hYZvDK.mjs → context-Du11TLcO.mjs} +5 -255
- package/dist/dev-BEveeA5H.mjs +233 -0
- package/dist/{devframe-mQ4qZAuo.d.mts → devframe-Bdd9YXTy.d.mts} +88 -25
- package/dist/{diagnostics-D7wM2cwM.mjs → diagnostics-DLYH5oD3.mjs} +16 -0
- package/dist/dist-D8jAMwxU.mjs +227 -0
- package/dist/helpers/vite.d.mts +43 -39
- package/dist/helpers/vite.mjs +26 -38
- package/dist/host-agent-CpbAoqBx.mjs +230 -0
- package/dist/host-h3-CzCZwtuV.mjs +26 -0
- package/dist/{fetch-BKaD0Vb3.mjs → http-DLr_cJZ7.mjs} +42 -4
- package/dist/{index-b64Uuhy2.d.mts → index-CZRd2K1C.d.mts} +2 -2
- package/dist/{index-Dbw1p5ch.d.mts → index-DSCq4M4q.d.mts} +2 -31
- package/dist/index.d.mts +9 -4
- package/dist/index.mjs +8 -1
- package/dist/{instance-registry-BnnA2Eoh.mjs → instance-registry-BQqGoDO9.mjs} +9 -2
- package/dist/instance-shell-DBTXTSjN.d.mts +264 -0
- package/dist/instance-shell-KRdee1QF.mjs +485 -0
- package/dist/internal/index.d.mts +150 -0
- package/dist/internal/index.mjs +7 -0
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/hub-internals.d.mts +2 -14
- package/dist/node/hub-internals.mjs +2 -2
- package/dist/node/index.d.mts +3 -267
- package/dist/node/index.mjs +3 -5
- package/dist/recipes/common-rpc-functions.d.mts +3 -3
- package/dist/recipes/interactive-auth.d.mts +3 -3
- package/dist/recipes/interactive-auth.mjs +6 -6
- package/dist/rolldown-runtime-JspESFgx.mjs +35 -0
- package/dist/rpc/dump.d.mts +77 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/index.mjs +3 -17
- package/dist/rpc/transports/sse-client.d.mts +2 -0
- package/dist/rpc/transports/sse-client.mjs +142 -0
- package/dist/rpc/transports/sse-server.d.mts +61 -0
- package/dist/rpc/transports/sse-server.mjs +209 -0
- package/dist/rpc/transports/ws-bun.d.mts +35 -0
- package/dist/rpc/transports/ws-bun.mjs +31 -0
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +10 -18
- package/dist/rpc/transports/ws-server.d.mts +2 -2
- package/dist/rpc/transports/ws-server.mjs +2 -235
- package/dist/rpc-core-CGctXToO.d.mts +48 -0
- package/dist/rpc-core-CqcJkAu9.mjs +81 -0
- package/dist/{serialization-C8Mnw9hK.mjs → serialization-BHY8M5NQ.mjs} +1 -1
- package/dist/{shared-state-6kHolYZy.mjs → shared-state-fRRbRUtD.mjs} +1 -1
- package/dist/sse-client-BsIK3hMD.d.mts +37 -0
- package/dist/{dump-CgZShDRB.mjs → static-CvjkO8QE.mjs} +2 -265
- package/dist/{storage-CTNVbuFB.mjs → storage-BcmVEJRc.mjs} +2 -2
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -6
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/launch-editor.mjs +1 -25
- package/dist/utils/open.mjs +1 -1
- package/dist/utils/serve-static.mjs +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/shared-state.mjs +1 -1
- package/dist/utils/simple-schema.d.mts +101 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/validation-BiuDXTKW.mjs +266 -0
- package/dist/wire-codec-9VhIKnyp.mjs +58 -0
- package/dist/{ws-client-B6Tnr0ic.d.mts → ws-client-CdNuATYa.d.mts} +6 -1
- package/dist/ws-server-Bk1hOG3X.mjs +267 -0
- package/dist/{ws-server-uKQcClcJ.d.mts → ws-server-Ces08fjP.d.mts} +89 -8
- package/package.json +10 -9
- package/skills/devframe/SKILL.md +84 -85
- package/dist/_shared-bWRzeSa0.mjs +0 -19
- package/dist/adapters/cli.d.mts +0 -11
- package/dist/adapters/cli.mjs +0 -6
- package/dist/cac-D4_9ok0z.mjs +0 -157
- package/dist/dev-Bc49u3qg.mjs +0 -439
- package/dist/http-C4T68YV6.mjs +0 -38
- package/dist/index-vAX6bYuc.d.mts +0 -78
- package/dist/recipes/open-helpers.d.mts +0 -6
- package/dist/recipes/open-helpers.mjs +0 -6
- package/dist/server-CD8XmBXl.mjs +0 -167
- package/dist/server-UHWae43Q.d.mts +0 -149
- package/dist/simple-schema-BDzLeJDk.d.mts +0 -102
- package/dist/utils/promise.d.mts +0 -8
- package/dist/utils/promise.mjs +0 -15
- package/dist/utils/scope.d.mts +0 -11
- package/dist/utils/scope.mjs +0 -15
- package/dist/{open-Deb5xmIT.mjs → open-Dz4V4irZ.mjs} +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_SSE_SESSION_HEADER } from "../../constants.mjs";
|
|
2
|
+
import { t as createRpcWireCodec } from "../../wire-codec-9VhIKnyp.mjs";
|
|
3
|
+
//#region src/rpc/transports/sse-client.ts
|
|
4
|
+
function NOOP() {}
|
|
5
|
+
/**
|
|
6
|
+
* Build a birpc `ChannelOptions` object backed by an SSE stream (server →
|
|
7
|
+
* client) and HTTP `POST` (client → server) — the WebSocket-free counterpart
|
|
8
|
+
* to `createWsRpcChannel`, wire-compatible with `attachSseRpcTransport`.
|
|
9
|
+
*
|
|
10
|
+
* The stream is consumed via `fetch` streaming (not `EventSource`), so it
|
|
11
|
+
* works in browsers and server runtimes alike. The server's first frame
|
|
12
|
+
* (`event: session`) carries the session id; every `POST` echoes it in the
|
|
13
|
+
* `x-birpc-session` header. A response to a client-initiated request comes
|
|
14
|
+
* back in the `POST`'s own body and is re-injected into the channel; a
|
|
15
|
+
* dropped stream terminates the channel — there is no reconnect, matching
|
|
16
|
+
* the WS channel's closed-is-done semantics.
|
|
17
|
+
*/
|
|
18
|
+
function createSseRpcChannel(options) {
|
|
19
|
+
const { onConnected = NOOP, onError = NOOP, onDisconnected = NOOP, definitions, fetch: fetchImpl = globalThis.fetch.bind(globalThis) } = options;
|
|
20
|
+
let url = options.url;
|
|
21
|
+
if (options.authToken) url = `${url}${url.includes("?") ? "&" : "?"}${DEVFRAME_AUTH_TOKEN_QUERY_PARAM}=${encodeURIComponent(options.authToken)}`;
|
|
22
|
+
const codec = createRpcWireCodec(definitions);
|
|
23
|
+
const abort = new AbortController();
|
|
24
|
+
let closed = false;
|
|
25
|
+
let onMessage;
|
|
26
|
+
let activeReader;
|
|
27
|
+
let resolveSession;
|
|
28
|
+
let rejectSession;
|
|
29
|
+
const sessionReady = new Promise((resolve, reject) => {
|
|
30
|
+
resolveSession = resolve;
|
|
31
|
+
rejectSession = reject;
|
|
32
|
+
});
|
|
33
|
+
sessionReady.catch(() => {});
|
|
34
|
+
function fail(error) {
|
|
35
|
+
if (closed) return;
|
|
36
|
+
closed = true;
|
|
37
|
+
rejectSession(error);
|
|
38
|
+
onError(error);
|
|
39
|
+
onDisconnected();
|
|
40
|
+
}
|
|
41
|
+
function end() {
|
|
42
|
+
if (closed) return;
|
|
43
|
+
closed = true;
|
|
44
|
+
rejectSession(/* @__PURE__ */ new Error("Devframe SSE stream closed"));
|
|
45
|
+
onDisconnected();
|
|
46
|
+
}
|
|
47
|
+
function dispatch(event, data) {
|
|
48
|
+
if (event === "session") {
|
|
49
|
+
resolveSession(data);
|
|
50
|
+
onConnected();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
onMessage?.(data);
|
|
54
|
+
}
|
|
55
|
+
/** Parse the SSE byte stream frame by frame and dispatch each event. */
|
|
56
|
+
async function consume(stream) {
|
|
57
|
+
const reader = stream.getReader();
|
|
58
|
+
activeReader = reader;
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = "";
|
|
61
|
+
for (;;) {
|
|
62
|
+
const { done, value } = await reader.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
buffer += decoder.decode(value, { stream: true });
|
|
65
|
+
for (;;) {
|
|
66
|
+
const boundary = buffer.search(/\n\n|\r\n\r\n/);
|
|
67
|
+
if (boundary < 0) break;
|
|
68
|
+
const frame = buffer.slice(0, boundary);
|
|
69
|
+
buffer = buffer.slice(boundary + (buffer[boundary] === "\r" ? 4 : 2));
|
|
70
|
+
let event = "message";
|
|
71
|
+
const data = [];
|
|
72
|
+
for (const rawLine of frame.split(/\r?\n/)) {
|
|
73
|
+
if (rawLine.startsWith(":")) continue;
|
|
74
|
+
if (rawLine.startsWith("event:")) event = rawLine.slice(6).trimStart();
|
|
75
|
+
else if (rawLine.startsWith("data:")) data.push(rawLine.slice(5).replace(/^ /, ""));
|
|
76
|
+
}
|
|
77
|
+
if (data.length > 0) dispatch(event, data.join("\n"));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
end();
|
|
81
|
+
}
|
|
82
|
+
(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetchImpl(url, {
|
|
85
|
+
headers: { accept: "text/event-stream" },
|
|
86
|
+
signal: abort.signal
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok || !response.body) throw new Error(`Devframe SSE stream request failed: ${response.status}`);
|
|
89
|
+
await consume(response.body);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (abort.signal.aborted) {
|
|
92
|
+
end();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return {
|
|
99
|
+
close: () => {
|
|
100
|
+
closed = true;
|
|
101
|
+
abort.abort();
|
|
102
|
+
activeReader?.cancel().catch(() => {});
|
|
103
|
+
},
|
|
104
|
+
on: (handler) => {
|
|
105
|
+
onMessage = handler;
|
|
106
|
+
},
|
|
107
|
+
post: async (data) => {
|
|
108
|
+
let sessionId;
|
|
109
|
+
try {
|
|
110
|
+
sessionId = await sessionReady;
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (closed) {
|
|
115
|
+
onError(/* @__PURE__ */ new Error("Devframe SSE channel is closed; message dropped"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetchImpl(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"content-type": "text/plain; charset=utf-8",
|
|
123
|
+
[DEVFRAME_SSE_SESSION_HEADER]: sessionId
|
|
124
|
+
},
|
|
125
|
+
body: data
|
|
126
|
+
});
|
|
127
|
+
if (response.status === 200) {
|
|
128
|
+
const body = await response.text();
|
|
129
|
+
if (body) onMessage?.(body);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!response.ok) throw new Error(`Devframe SSE POST failed: ${response.status}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
serialize: codec.serialize,
|
|
138
|
+
deserialize: codec.deserialize
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { createSseRpcChannel };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { v as RpcFunctionDefinitionAny } from "../../types-CnJSgRVa.mjs";
|
|
2
|
+
import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "../../ws-server-Ces08fjP.mjs";
|
|
3
|
+
import { BirpcGroup } from "birpc";
|
|
4
|
+
//#region src/rpc/transports/sse-server.d.ts
|
|
5
|
+
interface SseRpcTransportOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Same contract as `WsRpcTransportOptions.allowedOrigins`: extra origins
|
|
8
|
+
* to accept beyond the loopback default, a live origin registry, or
|
|
9
|
+
* `false` to disable the gate. Applied to the stream `GET`, the RPC
|
|
10
|
+
* `POST`, and the CORS preflight alike.
|
|
11
|
+
*/
|
|
12
|
+
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
|
|
13
|
+
/**
|
|
14
|
+
* RPC function definitions, used by the per-call wire serializer to
|
|
15
|
+
* dispatch between strict-JSON and structured-clone encoding based on
|
|
16
|
+
* each function's `jsonSerializable` flag. When omitted, all messages
|
|
17
|
+
* fall back to structured-clone — same contract as the WS transport.
|
|
18
|
+
*/
|
|
19
|
+
definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>;
|
|
20
|
+
onConnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void;
|
|
21
|
+
onDisconnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Milliseconds between keep-alive comment frames (`: ping`) written to
|
|
24
|
+
* every open stream, so intermediaries (reverse proxies, tunnels) don't
|
|
25
|
+
* drop an idle connection. `0` disables. Default: `30_000`.
|
|
26
|
+
*/
|
|
27
|
+
keepAliveInterval?: number;
|
|
28
|
+
/** Mint a session id. Default: `crypto.randomUUID`. */
|
|
29
|
+
generateSessionId?: () => string;
|
|
30
|
+
}
|
|
31
|
+
interface SseRpcTransport {
|
|
32
|
+
/**
|
|
33
|
+
* Web-standard handler for the SSE route: `GET` opens the event stream,
|
|
34
|
+
* `POST` carries RPC frames (session id in the `x-birpc-session` header),
|
|
35
|
+
* `OPTIONS` answers the CORS preflight for registered cross-origin
|
|
36
|
+
* viewers. Mount it on the route the connection meta advertises.
|
|
37
|
+
*/
|
|
38
|
+
handler: (request: Request) => Promise<Response> | Response;
|
|
39
|
+
/** Number of currently connected SSE sessions. */
|
|
40
|
+
sessionCount: () => number;
|
|
41
|
+
/** Terminate every open stream and detach their birpc channels. */
|
|
42
|
+
close: () => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Attach an SSE + HTTP POST transport to an existing RPC group — the
|
|
46
|
+
* WebSocket-free counterpart to `attachWsRpcTransport`, for hosts and
|
|
47
|
+
* proxies where the upgrade isn't available. Speaks the same birpc wire
|
|
48
|
+
* protocol (one channel per session, per-method `jsonSerializable`
|
|
49
|
+
* dispatch) over a half-duplex pair, mirroring birpc's own SSE helpers:
|
|
50
|
+
*
|
|
51
|
+
* - the server mints a session id as the stream's first frame
|
|
52
|
+
* (`event: session`);
|
|
53
|
+
* - every client `POST` echoes it in the `x-birpc-session` header;
|
|
54
|
+
* - a client-initiated request's response comes back in that `POST`'s
|
|
55
|
+
* own body (parked until the handler answers);
|
|
56
|
+
* - server-initiated calls/events stream down as `event: message`
|
|
57
|
+
* frames, and the client's replies up via `POST` get a bare `202`.
|
|
58
|
+
*/
|
|
59
|
+
declare function attachSseRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: SseRpcTransportOptions): SseRpcTransport;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { SseRpcTransport, SseRpcTransportOptions, attachSseRpcTransport };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { DEVFRAME_SSE_SESSION_HEADER } from "../../constants.mjs";
|
|
2
|
+
import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../../wire-codec-9VhIKnyp.mjs";
|
|
3
|
+
import { i as isAllowedOrigin, o as createRpcSessionMeta } from "../../ws-server-Bk1hOG3X.mjs";
|
|
4
|
+
//#region src/rpc/transports/sse-server.ts
|
|
5
|
+
const SSE_STREAM_HEADERS = {
|
|
6
|
+
"content-type": "text/event-stream",
|
|
7
|
+
"cache-control": "no-cache, no-transform",
|
|
8
|
+
"x-accel-buffering": "no"
|
|
9
|
+
};
|
|
10
|
+
function NOOP() {}
|
|
11
|
+
/**
|
|
12
|
+
* Attach an SSE + HTTP POST transport to an existing RPC group — the
|
|
13
|
+
* WebSocket-free counterpart to `attachWsRpcTransport`, for hosts and
|
|
14
|
+
* proxies where the upgrade isn't available. Speaks the same birpc wire
|
|
15
|
+
* protocol (one channel per session, per-method `jsonSerializable`
|
|
16
|
+
* dispatch) over a half-duplex pair, mirroring birpc's own SSE helpers:
|
|
17
|
+
*
|
|
18
|
+
* - the server mints a session id as the stream's first frame
|
|
19
|
+
* (`event: session`);
|
|
20
|
+
* - every client `POST` echoes it in the `x-birpc-session` header;
|
|
21
|
+
* - a client-initiated request's response comes back in that `POST`'s
|
|
22
|
+
* own body (parked until the handler answers);
|
|
23
|
+
* - server-initiated calls/events stream down as `event: message`
|
|
24
|
+
* frames, and the client's replies up via `POST` get a bare `202`.
|
|
25
|
+
*/
|
|
26
|
+
function attachSseRpcTransport(rpcGroup, options = {}) {
|
|
27
|
+
const { allowedOrigins, definitions, onConnected = NOOP, onDisconnected = NOOP, keepAliveInterval = 3e4, generateSessionId = () => crypto.randomUUID() } = options;
|
|
28
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
function originAllowed(request) {
|
|
31
|
+
if (allowedOrigins === false) return true;
|
|
32
|
+
const origin = request.headers.get("origin") ?? void 0;
|
|
33
|
+
return allowedOrigins && !Array.isArray(allowedOrigins) ? allowedOrigins.isAllowed(origin) : isAllowedOrigin(origin, allowedOrigins ?? []);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* CORS headers for a browser on another (allowed) origin — the side-car /
|
|
37
|
+
* registered-viewer case. Same-origin requests carry no `Origin` (or their
|
|
38
|
+
* own), where these headers are inert.
|
|
39
|
+
*/
|
|
40
|
+
function corsHeaders(request) {
|
|
41
|
+
const origin = request.headers.get("origin");
|
|
42
|
+
if (!origin) return {};
|
|
43
|
+
return {
|
|
44
|
+
"access-control-allow-origin": origin,
|
|
45
|
+
"vary": "origin"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function openStream(request) {
|
|
49
|
+
const meta = createRpcSessionMeta();
|
|
50
|
+
const id = generateSessionId();
|
|
51
|
+
const codec = createRpcWireCodec(definitions);
|
|
52
|
+
let controller;
|
|
53
|
+
let keepAlive;
|
|
54
|
+
let closeSession = () => {};
|
|
55
|
+
function write(text) {
|
|
56
|
+
try {
|
|
57
|
+
controller?.enqueue(encoder.encode(text));
|
|
58
|
+
} catch {
|
|
59
|
+
closeSession();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeEvent(event, data) {
|
|
63
|
+
write(`event: ${event}\n${data.split("\n").map((line) => `data: ${line}`).join("\n")}\n\n`);
|
|
64
|
+
}
|
|
65
|
+
const session = {
|
|
66
|
+
id,
|
|
67
|
+
meta,
|
|
68
|
+
connection: void 0,
|
|
69
|
+
channel: void 0,
|
|
70
|
+
parked: /* @__PURE__ */ new Map(),
|
|
71
|
+
closed: false,
|
|
72
|
+
close: () => {
|
|
73
|
+
if (session.closed) return;
|
|
74
|
+
session.closed = true;
|
|
75
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
76
|
+
sessions.delete(id);
|
|
77
|
+
for (const resolve of session.parked.values()) resolve(null);
|
|
78
|
+
session.parked.clear();
|
|
79
|
+
try {
|
|
80
|
+
controller?.close();
|
|
81
|
+
} catch {}
|
|
82
|
+
rpcGroup.updateChannels((channels) => {
|
|
83
|
+
const index = channels.indexOf(session.channel);
|
|
84
|
+
if (index >= 0) channels.splice(index, 1);
|
|
85
|
+
});
|
|
86
|
+
onDisconnected(session.connection, meta);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
closeSession = session.close;
|
|
90
|
+
const connection = {
|
|
91
|
+
id: meta.id,
|
|
92
|
+
transport: "sse",
|
|
93
|
+
request,
|
|
94
|
+
send: (data) => writeEvent("message", data),
|
|
95
|
+
close: () => session.close()
|
|
96
|
+
};
|
|
97
|
+
session.connection = connection;
|
|
98
|
+
const channel = {
|
|
99
|
+
post: (data) => {
|
|
100
|
+
const { t, i } = peekRpcWireFrame(data);
|
|
101
|
+
if (t === "s" && i) {
|
|
102
|
+
const resolve = session.parked.get(i);
|
|
103
|
+
if (resolve) {
|
|
104
|
+
session.parked.delete(i);
|
|
105
|
+
resolve(data);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
writeEvent("message", data);
|
|
110
|
+
},
|
|
111
|
+
on: (fn) => {
|
|
112
|
+
session.onMessage = fn;
|
|
113
|
+
},
|
|
114
|
+
serialize: codec.serialize,
|
|
115
|
+
deserialize: codec.deserialize,
|
|
116
|
+
meta
|
|
117
|
+
};
|
|
118
|
+
session.channel = channel;
|
|
119
|
+
const body = new ReadableStream({
|
|
120
|
+
start(c) {
|
|
121
|
+
controller = c;
|
|
122
|
+
sessions.set(id, session);
|
|
123
|
+
rpcGroup.updateChannels((channels) => {
|
|
124
|
+
channels.push(channel);
|
|
125
|
+
});
|
|
126
|
+
write(`event: session\ndata: ${id}\n\n`);
|
|
127
|
+
if (keepAliveInterval > 0) {
|
|
128
|
+
keepAlive = setInterval(write, keepAliveInterval, ": ping\n\n");
|
|
129
|
+
keepAlive.unref?.();
|
|
130
|
+
}
|
|
131
|
+
onConnected(connection, meta);
|
|
132
|
+
},
|
|
133
|
+
cancel() {
|
|
134
|
+
session.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return new Response(body, {
|
|
138
|
+
status: 200,
|
|
139
|
+
headers: {
|
|
140
|
+
...SSE_STREAM_HEADERS,
|
|
141
|
+
...corsHeaders(request)
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async function handlePost(request) {
|
|
146
|
+
const id = request.headers.get(DEVFRAME_SSE_SESSION_HEADER);
|
|
147
|
+
const session = id ? sessions.get(id) : void 0;
|
|
148
|
+
if (!session || session.closed) return new Response("Unknown SSE session", {
|
|
149
|
+
status: 400,
|
|
150
|
+
headers: corsHeaders(request)
|
|
151
|
+
});
|
|
152
|
+
const frame = await request.text();
|
|
153
|
+
const { t, i } = peekRpcWireFrame(frame);
|
|
154
|
+
if (t === "q" && i) {
|
|
155
|
+
const parked = new Promise((resolve) => {
|
|
156
|
+
session.parked.set(i, resolve);
|
|
157
|
+
});
|
|
158
|
+
session.onMessage?.(frame);
|
|
159
|
+
const responseBody = await parked;
|
|
160
|
+
if (responseBody == null) return new Response("SSE session closed before the response settled", {
|
|
161
|
+
status: 409,
|
|
162
|
+
headers: corsHeaders(request)
|
|
163
|
+
});
|
|
164
|
+
return new Response(responseBody, {
|
|
165
|
+
status: 200,
|
|
166
|
+
headers: {
|
|
167
|
+
"content-type": "text/plain; charset=utf-8",
|
|
168
|
+
...corsHeaders(request)
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
session.onMessage?.(frame);
|
|
173
|
+
return new Response(null, {
|
|
174
|
+
status: 202,
|
|
175
|
+
headers: corsHeaders(request)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function handlePreflight(request) {
|
|
179
|
+
return new Response(null, {
|
|
180
|
+
status: 204,
|
|
181
|
+
headers: {
|
|
182
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
183
|
+
"access-control-allow-headers": `content-type, ${DEVFRAME_SSE_SESSION_HEADER}`,
|
|
184
|
+
"access-control-max-age": "86400",
|
|
185
|
+
...corsHeaders(request)
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
handler: (request) => {
|
|
191
|
+
if (!originAllowed(request)) return new Response("Forbidden", { status: 403 });
|
|
192
|
+
switch (request.method) {
|
|
193
|
+
case "GET": return openStream(request);
|
|
194
|
+
case "POST": return handlePost(request);
|
|
195
|
+
case "OPTIONS": return handlePreflight(request);
|
|
196
|
+
default: return new Response("Method Not Allowed", {
|
|
197
|
+
status: 405,
|
|
198
|
+
headers: { allow: "GET, POST, OPTIONS" }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
sessionCount: () => sessions.size,
|
|
203
|
+
close: () => {
|
|
204
|
+
for (const session of [...sessions.values()]) session.close();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
export { attachSseRpcTransport };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { n as WsOriginRegistry } from "../../ws-server-Ces08fjP.mjs";
|
|
2
|
+
import { t as ContextRpcServer } from "../../rpc-core-CGctXToO.mjs";
|
|
3
|
+
//#region src/rpc/transports/ws-bun.d.ts
|
|
4
|
+
interface AttachBunWsTransportOptions {
|
|
5
|
+
/** Same contract as `WsRpcTransportOptions.allowedOrigins`. */
|
|
6
|
+
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Structural view of the Bun `Bun.serve({ websocket })` handler object the
|
|
10
|
+
* crossws Bun adapter produces — typed loosely so devframe carries no
|
|
11
|
+
* dependency on Bun's own types.
|
|
12
|
+
*/
|
|
13
|
+
interface BunWsTierWebSocket {
|
|
14
|
+
open?: (ws: unknown) => unknown;
|
|
15
|
+
message: (ws: unknown, message: unknown) => unknown;
|
|
16
|
+
close?: (ws: unknown, code?: number, reason?: string) => unknown;
|
|
17
|
+
drain?: (ws: unknown) => unknown;
|
|
18
|
+
}
|
|
19
|
+
interface BunWsTier {
|
|
20
|
+
/** Complete a WS upgrade request — `Bun.serve`'s server as 2nd argument. */
|
|
21
|
+
handleUpgrade: (request: Request, server: unknown) => Promise<Response | undefined>;
|
|
22
|
+
/** The handlers to spread into `Bun.serve({ websocket })`. */
|
|
23
|
+
websocket: BunWsTierWebSocket;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The Bun fetch-upgrade WebSocket tier for `initDevframe` / `initHub` — the
|
|
28
|
+
* same RPC peer wiring as `attachWsRpcTransport`, driven by crossws's Bun
|
|
29
|
+
* adapter so upgrades complete through `handler(request, server)` on the
|
|
30
|
+
* app's own origin, with no side-car server. Load it dynamically so the Bun
|
|
31
|
+
* adapter never enters a Node-only bundle path.
|
|
32
|
+
*/
|
|
33
|
+
declare function attachBunWsTransport(core: ContextRpcServer, options?: AttachBunWsTransportOptions): Promise<BunWsTier>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { AttachBunWsTransportOptions, BunWsTier, BunWsTierWebSocket, attachBunWsTransport };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { i as isAllowedOrigin, r as createWsRpcPeerHooks } from "../../ws-server-Bk1hOG3X.mjs";
|
|
2
|
+
//#region src/rpc/transports/ws-bun.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Bun fetch-upgrade WebSocket tier for `initDevframe` / `initHub` — the
|
|
5
|
+
* same RPC peer wiring as `attachWsRpcTransport`, driven by crossws's Bun
|
|
6
|
+
* adapter so upgrades complete through `handler(request, server)` on the
|
|
7
|
+
* app's own origin, with no side-car server. Load it dynamically so the Bun
|
|
8
|
+
* adapter never enters a Node-only bundle path.
|
|
9
|
+
*/
|
|
10
|
+
async function attachBunWsTransport(core, options = {}) {
|
|
11
|
+
const { default: bunAdapter } = await import("crossws/adapters/bun");
|
|
12
|
+
const { allowedOrigins } = options;
|
|
13
|
+
const ws = bunAdapter({ hooks: {
|
|
14
|
+
...createWsRpcPeerHooks(core.rpcGroup, {
|
|
15
|
+
onConnected: core.onConnected,
|
|
16
|
+
onDisconnected: core.onDisconnected
|
|
17
|
+
}),
|
|
18
|
+
upgrade(request) {
|
|
19
|
+
const origin = request.headers.get("origin") ?? void 0;
|
|
20
|
+
const allowed = allowedOrigins && !Array.isArray(allowedOrigins) ? allowedOrigins.isAllowed(origin) : isAllowedOrigin(origin, allowedOrigins || []);
|
|
21
|
+
if (allowedOrigins !== false && !allowed) return new Response("Forbidden", { status: 403 });
|
|
22
|
+
}
|
|
23
|
+
} });
|
|
24
|
+
return {
|
|
25
|
+
handleUpgrade: (request, server) => ws.handleUpgrade(request, server),
|
|
26
|
+
websocket: ws.websocket,
|
|
27
|
+
close: () => ws.close()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { attachBunWsTransport };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createWsRpcChannel, t as WsRpcChannelOptions } from "../../ws-client-
|
|
1
|
+
import { n as createWsRpcChannel, t as WsRpcChannelOptions } from "../../ws-client-CdNuATYa.mjs";
|
|
2
2
|
export { WsRpcChannelOptions, createWsRpcChannel };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { DEVFRAME_AUTH_TOKEN_QUERY_PARAM } from "../../constants.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as structuredCloneStringify, t as structuredCloneParse } from "../../structured-clone-CbAV5rFI.mjs";
|
|
2
|
+
import { t as createRpcWireCodec } from "../../wire-codec-9VhIKnyp.mjs";
|
|
4
3
|
//#region src/rpc/transports/ws-client.ts
|
|
5
4
|
function NOOP() {}
|
|
6
5
|
const EMPTY_DEFS = /* @__PURE__ */ new Map();
|
|
7
6
|
/**
|
|
8
7
|
* Build a birpc `ChannelOptions` object backed by a browser `WebSocket`.
|
|
9
8
|
* Pass the result straight to `createRpcClient`'s `channel` option.
|
|
9
|
+
*
|
|
10
|
+
* Also returns `close()`, closing the underlying socket — mirroring the server transport's
|
|
11
|
+
* existing `WsRpcTransport.close()`. `birpc`'s own `ChannelOptions` has no teardown of its own.
|
|
10
12
|
*/
|
|
11
13
|
function createWsRpcChannel(options) {
|
|
12
14
|
let url = options.url;
|
|
@@ -23,8 +25,11 @@ function createWsRpcChannel(options) {
|
|
|
23
25
|
ws.addEventListener("close", (e) => {
|
|
24
26
|
onDisconnected(e);
|
|
25
27
|
});
|
|
26
|
-
const
|
|
28
|
+
const codec = createRpcWireCodec(definitions);
|
|
27
29
|
return {
|
|
30
|
+
close: () => {
|
|
31
|
+
ws.close();
|
|
32
|
+
},
|
|
28
33
|
on: (handler) => {
|
|
29
34
|
ws.addEventListener("message", (e) => {
|
|
30
35
|
handler(e.data);
|
|
@@ -51,21 +56,8 @@ function createWsRpcChannel(options) {
|
|
|
51
56
|
}
|
|
52
57
|
onError(/* @__PURE__ */ new Error("Devframe WebSocket is not open; message dropped"));
|
|
53
58
|
},
|
|
54
|
-
serialize:
|
|
55
|
-
|
|
56
|
-
if (msg.t === "q") method = msg.m;
|
|
57
|
-
else {
|
|
58
|
-
method = pendingRequestMethods.get(msg.i);
|
|
59
|
-
pendingRequestMethods.delete(msg.i);
|
|
60
|
-
}
|
|
61
|
-
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
|
|
62
|
-
return `s:${structuredCloneStringify(msg)}`;
|
|
63
|
-
},
|
|
64
|
-
deserialize: (raw) => {
|
|
65
|
-
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
|
|
66
|
-
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
|
|
67
|
-
return msg;
|
|
68
|
-
}
|
|
59
|
+
serialize: codec.serialize,
|
|
60
|
+
deserialize: codec.deserialize
|
|
69
61
|
};
|
|
70
62
|
}
|
|
71
63
|
//#endregion
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { CreateWsOriginRegistryOptions, DevframeNodeRpcSessionMeta, WsOriginRegistry, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, createWsOriginRegistry, isAllowedOrigin, isLoopbackHostname };
|
|
1
|
+
import { a as attachWsRpcTransport, c as isAllowedOrigin, d as DevframeRpcConnection, f as DevframeRpcConnectionRequest, i as WsRpcTransportOptions, l as isLoopbackHostname, n as WsOriginRegistry, o as createWsOriginRegistry, p as DevframeRpcTransportKind, r as WsRpcTransport, s as createWsRpcPeerHooks, t as CreateWsOriginRegistryOptions, u as DevframeNodeRpcSessionMeta } from "../../ws-server-Ces08fjP.mjs";
|
|
2
|
+
export { CreateWsOriginRegistryOptions, type DevframeNodeRpcSessionMeta, type DevframeRpcConnection, type DevframeRpcConnectionRequest, type DevframeRpcTransportKind, WsOriginRegistry, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, createWsOriginRegistry, createWsRpcPeerHooks, isAllowedOrigin, isLoopbackHostname };
|