devframe 0.8.2 → 0.9.0-beta.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/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
package/dist/client/index.mjs
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { t as hash } from "../hash-KtDZYXDN.mjs";
|
|
2
2
|
import { colors } from "../utils/colors.mjs";
|
|
3
3
|
import { createEventEmitter } from "../utils/events.mjs";
|
|
4
|
-
import { t as createSharedState } from "../shared-state-
|
|
4
|
+
import { t as createSharedState } from "../shared-state-fRRbRUtD.mjs";
|
|
5
5
|
import { i as structuredCloneStringify, n as structuredCloneParse, t as structuredCloneDeserialize } from "../structured-clone-CgtQ61SA.mjs";
|
|
6
6
|
import { createStreamReader, createStreamSink } from "../utils/streaming-channel.mjs";
|
|
7
|
-
import { promiseWithResolver } from "../utils/promise.mjs";
|
|
8
|
-
import { isQualifiedName, qualifyName } from "../utils/scope.mjs";
|
|
9
7
|
import { defineDiagnostics } from "nostics";
|
|
10
8
|
import { ansiFormatter } from "nostics/formatters/ansi";
|
|
11
9
|
import { withBase, withProtocol } from "ufo";
|
|
@@ -18,6 +16,13 @@ const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
|
|
|
18
16
|
* connection from another JavaScript realm.
|
|
19
17
|
*/
|
|
20
18
|
const DEVFRAME_CONNECTION_KEY = "__DEVFRAME_CONNECTION__";
|
|
19
|
+
/**
|
|
20
|
+
* Request header carrying the SSE session id on every RPC `POST`. The
|
|
21
|
+
* server mints the id as the stream's first event (`event: session`);
|
|
22
|
+
* the client echoes it here so the POST joins that stream's birpc
|
|
23
|
+
* channel. Mirrors birpc's own SSE helpers for wire compatibility.
|
|
24
|
+
*/
|
|
25
|
+
const DEVFRAME_SSE_SESSION_HEADER = "x-birpc-session";
|
|
21
26
|
const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
|
|
22
27
|
/**
|
|
23
28
|
* Page-URL **fragment** parameter carrying a one-time authentication code (OTP)
|
|
@@ -43,6 +48,45 @@ const DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM = "devframe_viewer_origin";
|
|
|
43
48
|
/** Token that authorizes an external viewer origin registration. */
|
|
44
49
|
const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
|
|
45
50
|
//#endregion
|
|
51
|
+
//#region src/rpc/cache.ts
|
|
52
|
+
/**
|
|
53
|
+
* @experimental API is expected to change.
|
|
54
|
+
*/
|
|
55
|
+
var RpcCacheManager = class {
|
|
56
|
+
cacheMap = /* @__PURE__ */ new Map();
|
|
57
|
+
options;
|
|
58
|
+
keySerializer;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.keySerializer = options.keySerializer || ((args) => hash(args));
|
|
62
|
+
}
|
|
63
|
+
updateOptions(options) {
|
|
64
|
+
this.options = {
|
|
65
|
+
...this.options,
|
|
66
|
+
...options
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
cached(m, a) {
|
|
70
|
+
const methodCache = this.cacheMap.get(m);
|
|
71
|
+
if (methodCache) return methodCache.get(this.keySerializer(a));
|
|
72
|
+
}
|
|
73
|
+
has(m, a) {
|
|
74
|
+
return this.cacheMap.get(m)?.has(this.keySerializer(a)) ?? false;
|
|
75
|
+
}
|
|
76
|
+
apply(req, res) {
|
|
77
|
+
const methodCache = this.cacheMap.get(req.m) || /* @__PURE__ */ new Map();
|
|
78
|
+
methodCache.set(this.keySerializer(req.a), res);
|
|
79
|
+
this.cacheMap.set(req.m, methodCache);
|
|
80
|
+
}
|
|
81
|
+
validate(m) {
|
|
82
|
+
return this.options.functions.includes(m);
|
|
83
|
+
}
|
|
84
|
+
clear(fn) {
|
|
85
|
+
if (fn) this.cacheMap.delete(fn);
|
|
86
|
+
else this.cacheMap.clear();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
46
90
|
//#region src/utils/diagnostics-reporter.ts
|
|
47
91
|
const formatAnsi = ansiFormatter(colors);
|
|
48
92
|
function devframeReporter(d, { method = "warn" } = {}) {
|
|
@@ -87,26 +131,6 @@ const diagnostics = defineDiagnostics({
|
|
|
87
131
|
}
|
|
88
132
|
});
|
|
89
133
|
//#endregion
|
|
90
|
-
//#region src/rpc/dump/error.ts
|
|
91
|
-
/**
|
|
92
|
-
* Inverse of {@link serializeDumpError}: rebuild a thrown `Error` from
|
|
93
|
-
* the plain object stored in a dump record. Preserves `cause`, restores
|
|
94
|
-
* the original `name`, and re-attaches any custom own properties.
|
|
95
|
-
*/
|
|
96
|
-
function reviveDumpError(stored) {
|
|
97
|
-
const cause = stored.cause instanceof Error ? stored.cause : isPlainErrorShape(stored.cause) ? reviveDumpError(stored.cause) : stored.cause;
|
|
98
|
-
const error = cause !== void 0 ? new Error(stored.message, { cause }) : new Error(stored.message);
|
|
99
|
-
error.name = stored.name;
|
|
100
|
-
for (const key of Object.keys(stored)) {
|
|
101
|
-
if (key === "name" || key === "message" || key === "cause") continue;
|
|
102
|
-
error[key] = stored[key];
|
|
103
|
-
}
|
|
104
|
-
return error;
|
|
105
|
-
}
|
|
106
|
-
function isPlainErrorShape(value) {
|
|
107
|
-
return typeof value === "object" && value !== null && typeof value.message === "string" && typeof value.name === "string";
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
134
|
//#region src/rpc/validate-io.ts
|
|
111
135
|
/**
|
|
112
136
|
* Run a single [Standard Schema](https://standardschema.dev) validator,
|
|
@@ -215,45 +239,6 @@ async function getRpcHandler(definition, context) {
|
|
|
215
239
|
return validating;
|
|
216
240
|
}
|
|
217
241
|
//#endregion
|
|
218
|
-
//#region src/rpc/cache.ts
|
|
219
|
-
/**
|
|
220
|
-
* @experimental API is expected to change.
|
|
221
|
-
*/
|
|
222
|
-
var RpcCacheManager = class {
|
|
223
|
-
cacheMap = /* @__PURE__ */ new Map();
|
|
224
|
-
options;
|
|
225
|
-
keySerializer;
|
|
226
|
-
constructor(options) {
|
|
227
|
-
this.options = options;
|
|
228
|
-
this.keySerializer = options.keySerializer || ((args) => hash(args));
|
|
229
|
-
}
|
|
230
|
-
updateOptions(options) {
|
|
231
|
-
this.options = {
|
|
232
|
-
...this.options,
|
|
233
|
-
...options
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
cached(m, a) {
|
|
237
|
-
const methodCache = this.cacheMap.get(m);
|
|
238
|
-
if (methodCache) return methodCache.get(this.keySerializer(a));
|
|
239
|
-
}
|
|
240
|
-
has(m, a) {
|
|
241
|
-
return this.cacheMap.get(m)?.has(this.keySerializer(a)) ?? false;
|
|
242
|
-
}
|
|
243
|
-
apply(req, res) {
|
|
244
|
-
const methodCache = this.cacheMap.get(req.m) || /* @__PURE__ */ new Map();
|
|
245
|
-
methodCache.set(this.keySerializer(req.a), res);
|
|
246
|
-
this.cacheMap.set(req.m, methodCache);
|
|
247
|
-
}
|
|
248
|
-
validate(m) {
|
|
249
|
-
return this.options.functions.includes(m);
|
|
250
|
-
}
|
|
251
|
-
clear(fn) {
|
|
252
|
-
if (fn) this.cacheMap.delete(fn);
|
|
253
|
-
else this.cacheMap.clear();
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
//#endregion
|
|
257
242
|
//#region src/rpc/collector.ts
|
|
258
243
|
var RpcFunctionsCollectorBase = class {
|
|
259
244
|
context;
|
|
@@ -706,269 +691,189 @@ function createRpcSharedStateClientHost(rpc) {
|
|
|
706
691
|
};
|
|
707
692
|
}
|
|
708
693
|
//#endregion
|
|
709
|
-
//#region src/
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
function
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
694
|
+
//#region src/rpc/wire-codec.ts
|
|
695
|
+
const EMPTY_WIRE_DEFS = /* @__PURE__ */ new Map();
|
|
696
|
+
/**
|
|
697
|
+
* Build the per-connection wire codec every live transport (WS server, WS
|
|
698
|
+
* client, SSE server, SSE client) shares: per-method dispatch between strict
|
|
699
|
+
* JSON (methods declared `jsonSerializable: true`) and `s:`-prefixed
|
|
700
|
+
* structured-clone (everything else, including all error envelopes), with a
|
|
701
|
+
* request-id → method map so a response independently picks the same
|
|
702
|
+
* encoder as its request. One codec per connection — request-id spaces
|
|
703
|
+
* don't collide across connections.
|
|
704
|
+
*
|
|
705
|
+
* @internal
|
|
706
|
+
* implementations; not part of the stable public API.
|
|
707
|
+
*/
|
|
708
|
+
function createRpcWireCodec(definitions = EMPTY_WIRE_DEFS) {
|
|
709
|
+
const pendingRequestMethods = /* @__PURE__ */ new Map();
|
|
710
|
+
return {
|
|
711
|
+
serialize: (msg) => {
|
|
712
|
+
let method;
|
|
713
|
+
if (msg.t === "q") method = msg.m;
|
|
714
|
+
else {
|
|
715
|
+
method = pendingRequestMethods.get(msg.i);
|
|
716
|
+
pendingRequestMethods.delete(msg.i);
|
|
717
|
+
}
|
|
718
|
+
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
|
|
719
|
+
return `s:${structuredCloneStringify(msg)}`;
|
|
720
|
+
},
|
|
721
|
+
deserialize: (raw) => {
|
|
722
|
+
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
|
|
723
|
+
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
|
|
724
|
+
return msg;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
729
727
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/rpc/transports/sse-client.ts
|
|
730
|
+
function NOOP$1() {}
|
|
731
|
+
/**
|
|
732
|
+
* Build a birpc `ChannelOptions` object backed by an SSE stream (server →
|
|
733
|
+
* client) and HTTP `POST` (client → server) — the WebSocket-free counterpart
|
|
734
|
+
* to `createWsRpcChannel`, wire-compatible with `attachSseRpcTransport`.
|
|
735
|
+
*
|
|
736
|
+
* The stream is consumed via `fetch` streaming (not `EventSource`), so it
|
|
737
|
+
* works in browsers and server runtimes alike. The server's first frame
|
|
738
|
+
* (`event: session`) carries the session id; every `POST` echoes it in the
|
|
739
|
+
* `x-birpc-session` header. A response to a client-initiated request comes
|
|
740
|
+
* back in the `POST`'s own body and is re-injected into the channel; a
|
|
741
|
+
* dropped stream terminates the channel — there is no reconnect, matching
|
|
742
|
+
* the WS channel's closed-is-done semantics.
|
|
743
|
+
*/
|
|
744
|
+
function createSseRpcChannel(options) {
|
|
745
|
+
const { onConnected = NOOP$1, onError = NOOP$1, onDisconnected = NOOP$1, definitions, fetch: fetchImpl = globalThis.fetch.bind(globalThis) } = options;
|
|
746
|
+
let url = options.url;
|
|
747
|
+
if (options.authToken) url = `${url}${url.includes("?") ? "&" : "?"}${DEVFRAME_AUTH_TOKEN_QUERY_PARAM}=${encodeURIComponent(options.authToken)}`;
|
|
748
|
+
const codec = createRpcWireCodec(definitions);
|
|
749
|
+
const abort = new AbortController();
|
|
750
|
+
let closed = false;
|
|
751
|
+
let onMessage;
|
|
752
|
+
let activeReader;
|
|
753
|
+
let resolveSession;
|
|
754
|
+
let rejectSession;
|
|
755
|
+
const sessionReady = new Promise((resolve, reject) => {
|
|
756
|
+
resolveSession = resolve;
|
|
757
|
+
rejectSession = reject;
|
|
758
|
+
});
|
|
759
|
+
sessionReady.catch(() => {});
|
|
760
|
+
function fail(error) {
|
|
761
|
+
if (closed) return;
|
|
762
|
+
closed = true;
|
|
763
|
+
rejectSession(error);
|
|
764
|
+
onError(error);
|
|
765
|
+
onDisconnected();
|
|
745
766
|
}
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
|
|
767
|
+
function end() {
|
|
768
|
+
if (closed) return;
|
|
769
|
+
closed = true;
|
|
770
|
+
rejectSession(/* @__PURE__ */ new Error("Devframe SSE stream closed"));
|
|
771
|
+
onDisconnected();
|
|
749
772
|
}
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return await loadStatic(entry);
|
|
773
|
+
function dispatch(event, data) {
|
|
774
|
+
if (event === "session") {
|
|
775
|
+
resolveSession(data);
|
|
776
|
+
onConnected();
|
|
777
|
+
return;
|
|
756
778
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
779
|
+
onMessage?.(data);
|
|
780
|
+
}
|
|
781
|
+
/** Parse the SSE byte stream frame by frame and dispatch each event. */
|
|
782
|
+
async function consume(stream) {
|
|
783
|
+
const reader = stream.getReader();
|
|
784
|
+
activeReader = reader;
|
|
785
|
+
const decoder = new TextDecoder();
|
|
786
|
+
let buffer = "";
|
|
787
|
+
for (;;) {
|
|
788
|
+
const { done, value } = await reader.read();
|
|
789
|
+
if (done) break;
|
|
790
|
+
buffer += decoder.decode(value, { stream: true });
|
|
791
|
+
for (;;) {
|
|
792
|
+
const boundary = buffer.search(/\n\n|\r\n\r\n/);
|
|
793
|
+
if (boundary < 0) break;
|
|
794
|
+
const frame = buffer.slice(0, boundary);
|
|
795
|
+
buffer = buffer.slice(boundary + (buffer[boundary] === "\r" ? 4 : 2));
|
|
796
|
+
let event = "message";
|
|
797
|
+
const data = [];
|
|
798
|
+
for (const rawLine of frame.split(/\r?\n/)) {
|
|
799
|
+
if (rawLine.startsWith(":")) continue;
|
|
800
|
+
if (rawLine.startsWith("event:")) event = rawLine.slice(6).trimStart();
|
|
801
|
+
else if (rawLine.startsWith("data:")) data.push(rawLine.slice(5).replace(/^ /, ""));
|
|
802
|
+
}
|
|
803
|
+
if (data.length > 0) dispatch(event, data.join("\n"));
|
|
804
|
+
}
|
|
763
805
|
}
|
|
764
|
-
|
|
765
|
-
throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
|
|
806
|
+
end();
|
|
766
807
|
}
|
|
808
|
+
(async () => {
|
|
809
|
+
try {
|
|
810
|
+
const response = await fetchImpl(url, {
|
|
811
|
+
headers: { accept: "text/event-stream" },
|
|
812
|
+
signal: abort.signal
|
|
813
|
+
});
|
|
814
|
+
if (!response.ok || !response.body) throw new Error(`Devframe SSE stream request failed: ${response.status}`);
|
|
815
|
+
await consume(response.body);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (abort.signal.aborted) {
|
|
818
|
+
end();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
822
|
+
}
|
|
823
|
+
})();
|
|
767
824
|
return {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
825
|
+
close: () => {
|
|
826
|
+
closed = true;
|
|
827
|
+
abort.abort();
|
|
828
|
+
activeReader?.cancel().catch(() => {});
|
|
772
829
|
},
|
|
773
|
-
|
|
830
|
+
on: (handler) => {
|
|
831
|
+
onMessage = handler;
|
|
832
|
+
},
|
|
833
|
+
post: async (data) => {
|
|
834
|
+
let sessionId;
|
|
835
|
+
try {
|
|
836
|
+
sessionId = await sessionReady;
|
|
837
|
+
} catch {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (closed) {
|
|
841
|
+
onError(/* @__PURE__ */ new Error("Devframe SSE channel is closed; message dropped"));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const response = await fetchImpl(url, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: {
|
|
848
|
+
"content-type": "text/plain; charset=utf-8",
|
|
849
|
+
[DEVFRAME_SSE_SESSION_HEADER]: sessionId
|
|
850
|
+
},
|
|
851
|
+
body: data
|
|
852
|
+
});
|
|
853
|
+
if (response.status === 200) {
|
|
854
|
+
const body = await response.text();
|
|
855
|
+
if (body) onMessage?.(body);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (!response.ok) throw new Error(`Devframe SSE POST failed: ${response.status}`);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
serialize: codec.serialize,
|
|
864
|
+
deserialize: codec.deserialize
|
|
774
865
|
};
|
|
775
866
|
}
|
|
776
867
|
//#endregion
|
|
777
|
-
//#region src/client
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
return {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
requestTrustWithCode: async () => null,
|
|
787
|
-
ensureTrusted: async () => true,
|
|
788
|
-
call: (...args) => staticCaller.call(args[0], args.slice(1)),
|
|
789
|
-
callEvent: (...args) => staticCaller.callEvent(args[0], args.slice(1)),
|
|
790
|
-
callOptional: (...args) => staticCaller.callOptional(args[0], args.slice(1))
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
//#endregion
|
|
794
|
-
//#region src/client/rpc-streaming.ts
|
|
795
|
-
const STREAM_KEY_SEPARATOR = "";
|
|
796
|
-
function streamKey(channel, id) {
|
|
797
|
-
return `${channel}${STREAM_KEY_SEPARATOR}${id}`;
|
|
798
|
-
}
|
|
799
|
-
/**
|
|
800
|
-
* Client-side streaming host. Mirrors `createRpcSharedStateClientHost`:
|
|
801
|
-
* registers the two `:chunk` / `:end` event handlers once, then per-stream
|
|
802
|
-
* state lives in a `Map<streamKey, StreamReader>`.
|
|
803
|
-
*/
|
|
804
|
-
function createRpcStreamingClientHost(rpc) {
|
|
805
|
-
const readers = /* @__PURE__ */ new Map();
|
|
806
|
-
const uploads = /* @__PURE__ */ new Map();
|
|
807
|
-
rpc.client.register({
|
|
808
|
-
name: "devframe:streaming:chunk",
|
|
809
|
-
type: "event",
|
|
810
|
-
handler(channel, id, seq, chunk) {
|
|
811
|
-
readers.get(streamKey(channel, id))?._push(seq, chunk);
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
rpc.client.register({
|
|
815
|
-
name: "devframe:streaming:end",
|
|
816
|
-
type: "event",
|
|
817
|
-
handler(channel, id, error) {
|
|
818
|
-
const key = streamKey(channel, id);
|
|
819
|
-
const reader = readers.get(key);
|
|
820
|
-
if (!reader) return;
|
|
821
|
-
reader._end(error);
|
|
822
|
-
readers.delete(key);
|
|
823
|
-
}
|
|
824
|
-
});
|
|
825
|
-
rpc.client.register({
|
|
826
|
-
name: "devframe:streaming:upload-cancel",
|
|
827
|
-
type: "event",
|
|
828
|
-
handler(channel, id) {
|
|
829
|
-
const key = streamKey(channel, id);
|
|
830
|
-
const sink = uploads.get(key);
|
|
831
|
-
if (!sink) return;
|
|
832
|
-
sink.abort("server cancelled upload");
|
|
833
|
-
uploads.delete(key);
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
rpc.events.on("rpc:is-trusted:updated", (isTrusted) => {
|
|
837
|
-
if (!isTrusted) return;
|
|
838
|
-
for (const [key, reader] of readers) {
|
|
839
|
-
if (reader.cancelled || reader.done) continue;
|
|
840
|
-
const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR);
|
|
841
|
-
if (sepIdx < 0) continue;
|
|
842
|
-
const channel = key.slice(0, sepIdx);
|
|
843
|
-
const id = key.slice(sepIdx + 1);
|
|
844
|
-
rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: reader.lastSeenSeq });
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
function subscribe(channel, id, options = {}) {
|
|
848
|
-
const key = streamKey(channel, id);
|
|
849
|
-
const existing = readers.get(key);
|
|
850
|
-
if (existing) return existing;
|
|
851
|
-
const reader = createStreamReader({
|
|
852
|
-
id,
|
|
853
|
-
highWaterMark: options.highWaterMark,
|
|
854
|
-
onOverflow(dropped) {
|
|
855
|
-
console.warn(`[devframe] DF0029: Stream "${channel}#${id}" dropped ${dropped} chunk(s) after exceeding the client high-water mark.`);
|
|
856
|
-
},
|
|
857
|
-
onCancel() {
|
|
858
|
-
rpc.callEvent("devframe:streaming:cancel", channel, id);
|
|
859
|
-
readers.delete(key);
|
|
860
|
-
}
|
|
861
|
-
});
|
|
862
|
-
readers.set(key, reader);
|
|
863
|
-
if (rpc.isTrusted) rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: 0 });
|
|
864
|
-
else {
|
|
865
|
-
const off = rpc.events.on("rpc:is-trusted:updated", (trusted) => {
|
|
866
|
-
if (trusted) {
|
|
867
|
-
off();
|
|
868
|
-
if (readers.has(key) && !reader.cancelled && !reader.done) rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: reader.lastSeenSeq });
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
return reader;
|
|
873
|
-
}
|
|
874
|
-
function upload(channel, id) {
|
|
875
|
-
const key = streamKey(channel, id);
|
|
876
|
-
const existing = uploads.get(key);
|
|
877
|
-
if (existing) return existing;
|
|
878
|
-
const sink = createStreamSink({ id });
|
|
879
|
-
sink.events.on("chunk", (seq, chunk) => {
|
|
880
|
-
rpc.callEvent("devframe:streaming:upload-chunk", channel, id, seq, chunk);
|
|
881
|
-
});
|
|
882
|
-
sink.events.on("end", (error) => {
|
|
883
|
-
rpc.callEvent("devframe:streaming:upload-end", channel, id, error);
|
|
884
|
-
uploads.delete(key);
|
|
885
|
-
});
|
|
886
|
-
uploads.set(key, sink);
|
|
887
|
-
return sink;
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
subscribe,
|
|
891
|
-
upload
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
//#endregion
|
|
895
|
-
//#region src/rpc/client.ts
|
|
896
|
-
function createRpcClient(functions, options) {
|
|
897
|
-
const { channel, rpcOptions = {} } = options;
|
|
898
|
-
return createBirpc(functions, {
|
|
899
|
-
...channel,
|
|
900
|
-
timeout: -1,
|
|
901
|
-
...rpcOptions,
|
|
902
|
-
proxify: false
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
//#endregion
|
|
906
|
-
//#region src/rpc/transports/ws-client.ts
|
|
907
|
-
function NOOP() {}
|
|
908
|
-
const EMPTY_DEFS = /* @__PURE__ */ new Map();
|
|
909
|
-
/**
|
|
910
|
-
* Build a birpc `ChannelOptions` object backed by a browser `WebSocket`.
|
|
911
|
-
* Pass the result straight to `createRpcClient`'s `channel` option.
|
|
912
|
-
*/
|
|
913
|
-
function createWsRpcChannel(options) {
|
|
914
|
-
let url = options.url;
|
|
915
|
-
if (options.authToken) url = `${url}?${DEVFRAME_AUTH_TOKEN_QUERY_PARAM}=${encodeURIComponent(options.authToken)}`;
|
|
916
|
-
const ws = new WebSocket(url);
|
|
917
|
-
const { onConnected = NOOP, onError = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS } = options;
|
|
918
|
-
ws.addEventListener("open", (e) => {
|
|
919
|
-
onConnected(e);
|
|
920
|
-
});
|
|
921
|
-
ws.addEventListener("error", (e) => {
|
|
922
|
-
const _e = e instanceof Error ? e : new Error(e.type);
|
|
923
|
-
onError(_e);
|
|
924
|
-
});
|
|
925
|
-
ws.addEventListener("close", (e) => {
|
|
926
|
-
onDisconnected(e);
|
|
927
|
-
});
|
|
928
|
-
const pendingRequestMethods = /* @__PURE__ */ new Map();
|
|
929
|
-
return {
|
|
930
|
-
on: (handler) => {
|
|
931
|
-
ws.addEventListener("message", (e) => {
|
|
932
|
-
handler(e.data);
|
|
933
|
-
});
|
|
934
|
-
},
|
|
935
|
-
post: (data) => {
|
|
936
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
937
|
-
ws.send(data);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
if (ws.readyState === WebSocket.CONNECTING) {
|
|
941
|
-
const onOpen = () => {
|
|
942
|
-
cleanup();
|
|
943
|
-
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
944
|
-
};
|
|
945
|
-
const onClose = () => cleanup();
|
|
946
|
-
function cleanup() {
|
|
947
|
-
ws.removeEventListener("open", onOpen);
|
|
948
|
-
ws.removeEventListener("close", onClose);
|
|
949
|
-
}
|
|
950
|
-
ws.addEventListener("open", onOpen);
|
|
951
|
-
ws.addEventListener("close", onClose);
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
onError(/* @__PURE__ */ new Error("Devframe WebSocket is not open; message dropped"));
|
|
955
|
-
},
|
|
956
|
-
serialize: (msg) => {
|
|
957
|
-
let method;
|
|
958
|
-
if (msg.t === "q") method = msg.m;
|
|
959
|
-
else {
|
|
960
|
-
method = pendingRequestMethods.get(msg.i);
|
|
961
|
-
pendingRequestMethods.delete(msg.i);
|
|
962
|
-
}
|
|
963
|
-
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
|
|
964
|
-
return `s:${structuredCloneStringify(msg)}`;
|
|
965
|
-
},
|
|
966
|
-
deserialize: (raw) => {
|
|
967
|
-
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
|
|
968
|
-
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
|
|
969
|
-
return msg;
|
|
970
|
-
}
|
|
971
|
-
};
|
|
868
|
+
//#region src/rpc/client.ts
|
|
869
|
+
function createRpcClient(functions, options) {
|
|
870
|
+
const { channel, rpcOptions = {} } = options;
|
|
871
|
+
return createBirpc(functions, {
|
|
872
|
+
...channel,
|
|
873
|
+
timeout: -1,
|
|
874
|
+
...rpcOptions,
|
|
875
|
+
proxify: false
|
|
876
|
+
});
|
|
972
877
|
}
|
|
973
878
|
//#endregion
|
|
974
879
|
//#region ../../node_modules/.pnpm/ua-parser-modern@0.1.1/node_modules/ua-parser-modern/dist/index.mjs
|
|
@@ -2133,54 +2038,32 @@ function parseUA(uastringOrExtensions, extensions) {
|
|
|
2133
2038
|
};
|
|
2134
2039
|
}
|
|
2135
2040
|
//#endregion
|
|
2136
|
-
//#region src/
|
|
2041
|
+
//#region src/utils/promise.ts
|
|
2042
|
+
function promiseWithResolver() {
|
|
2043
|
+
let resolve;
|
|
2044
|
+
let reject;
|
|
2045
|
+
return {
|
|
2046
|
+
promise: new Promise((_resolve, _reject) => {
|
|
2047
|
+
resolve = _resolve;
|
|
2048
|
+
reject = _reject;
|
|
2049
|
+
}),
|
|
2050
|
+
resolve,
|
|
2051
|
+
reject
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#region src/client/rpc-live.ts
|
|
2137
2056
|
/**
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
* The object / relative-path forms connect to the page's own origin (only the
|
|
2142
|
-
* `http`→`ws` protocol swap is applied), resolving the path against where
|
|
2143
|
-
* `__connection.json` was loaded. This is deliberately host-agnostic so the
|
|
2144
|
-
* connection survives a reverse proxy that changes the domain or port — the
|
|
2145
|
-
* client trusts its own location, never a server-baked hostname. An explicit
|
|
2146
|
-
* `port`/`host` (or a full `ws(s)://` URL string) opts into a cross-origin
|
|
2147
|
-
* endpoint, e.g. a side-car server on its own port.
|
|
2057
|
+
* The transport-independent half of a live RPC client mode — connection
|
|
2058
|
+
* status, pending-call guarding, and the trust handshake — shared by the
|
|
2059
|
+
* WebSocket and SSE modes. Each transport supplies only its channel.
|
|
2148
2060
|
*/
|
|
2149
|
-
function
|
|
2150
|
-
const
|
|
2151
|
-
try {
|
|
2152
|
-
return new URL(metaBaseUrl, loc.href);
|
|
2153
|
-
} catch {
|
|
2154
|
-
return new URL(loc.href);
|
|
2155
|
-
}
|
|
2156
|
-
})();
|
|
2157
|
-
const wsProtocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
2158
|
-
if (websocket && typeof websocket === "object") {
|
|
2159
|
-
if (websocket.host != null || websocket.port != null) {
|
|
2160
|
-
const host = websocket.host ?? `${base.hostname}:${websocket.port}`;
|
|
2161
|
-
const target = new URL(websocket.path ?? "/", `${wsProtocol}//${host}`);
|
|
2162
|
-
target.protocol = wsProtocol;
|
|
2163
|
-
return target.href;
|
|
2164
|
-
}
|
|
2165
|
-
const target = new URL(websocket.path ?? "", base);
|
|
2166
|
-
target.protocol = wsProtocol;
|
|
2167
|
-
return target.href;
|
|
2168
|
-
}
|
|
2169
|
-
if (typeof websocket === "number") return `${wsProtocol}//${base.hostname}:${websocket}`;
|
|
2170
|
-
const str = websocket ?? "";
|
|
2171
|
-
if (/^wss?:\/\//i.test(str)) return str;
|
|
2172
|
-
if (/^https?:\/\//i.test(str)) return withProtocol(str, /^https/i.test(str) ? "wss://" : "ws://");
|
|
2173
|
-
const target = new URL(str, base);
|
|
2174
|
-
target.protocol = wsProtocol;
|
|
2175
|
-
return target.href;
|
|
2176
|
-
}
|
|
2177
|
-
function createWsRpcClientMode(options) {
|
|
2178
|
-
const { authToken, connectionMeta, metaBaseUrl, events, clientRpc, rpcOptions = {}, wsOptions = {}, callTimeout = 0 } = options;
|
|
2061
|
+
function createLiveRpcClientMode(options) {
|
|
2062
|
+
const { transport, authToken, connectionMeta, events, clientRpc, rpcOptions = {}, callTimeout = 0 } = options;
|
|
2179
2063
|
let isTrusted = false;
|
|
2180
2064
|
let status = "connecting";
|
|
2181
2065
|
let connectionError = null;
|
|
2182
2066
|
const trustedPromise = promiseWithResolver();
|
|
2183
|
-
const url = resolveWsUrl(connectionMeta.websocket, metaBaseUrl ?? "./", location);
|
|
2184
2067
|
function setStatus(next, error = null) {
|
|
2185
2068
|
if (error) connectionError = error;
|
|
2186
2069
|
else if (next === "connected") connectionError = null;
|
|
@@ -2236,27 +2119,20 @@ function createWsRpcClientMode(options) {
|
|
|
2236
2119
|
}
|
|
2237
2120
|
const definitions = /* @__PURE__ */ new Map();
|
|
2238
2121
|
for (const name of connectionMeta.jsonSerializableMethods ?? []) definitions.set(name, { jsonSerializable: true });
|
|
2122
|
+
const channel = options.createChannel({
|
|
2123
|
+
definitions,
|
|
2124
|
+
onError(error) {
|
|
2125
|
+
setStatus("error", error);
|
|
2126
|
+
events.emit("connection:error", error);
|
|
2127
|
+
rejectAllPending(new DevframeConnectionError("connection", "[devframe] Connection to the devframe server failed", { cause: error }));
|
|
2128
|
+
},
|
|
2129
|
+
onDisconnected() {
|
|
2130
|
+
if (status !== "error") setStatus("disconnected");
|
|
2131
|
+
rejectAllPending(new DevframeConnectionError("connection", "[devframe] Disconnected from the devframe server", { cause: connectionError ?? void 0 }));
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2239
2134
|
const serverRpc = createRpcClient(clientRpc.functions, {
|
|
2240
|
-
channel
|
|
2241
|
-
url,
|
|
2242
|
-
authToken,
|
|
2243
|
-
definitions,
|
|
2244
|
-
...wsOptions,
|
|
2245
|
-
onConnected(event) {
|
|
2246
|
-
wsOptions.onConnected?.(event);
|
|
2247
|
-
},
|
|
2248
|
-
onError(error) {
|
|
2249
|
-
setStatus("error", error);
|
|
2250
|
-
events.emit("connection:error", error);
|
|
2251
|
-
rejectAllPending(new DevframeConnectionError("connection", "[devframe] Connection to the devframe server failed", { cause: error }));
|
|
2252
|
-
wsOptions.onError?.(error);
|
|
2253
|
-
},
|
|
2254
|
-
onDisconnected(event) {
|
|
2255
|
-
if (status !== "error") setStatus("disconnected");
|
|
2256
|
-
rejectAllPending(new DevframeConnectionError("connection", "[devframe] Disconnected from the devframe server", { cause: connectionError ?? void 0 }));
|
|
2257
|
-
wsOptions.onDisconnected?.(event);
|
|
2258
|
-
}
|
|
2259
|
-
}),
|
|
2135
|
+
channel,
|
|
2260
2136
|
rpcOptions
|
|
2261
2137
|
});
|
|
2262
2138
|
clientRpc.register({
|
|
@@ -2334,6 +2210,7 @@ function createWsRpcClientMode(options) {
|
|
|
2334
2210
|
return isTrusted;
|
|
2335
2211
|
}
|
|
2336
2212
|
return {
|
|
2213
|
+
transport,
|
|
2337
2214
|
get isTrusted() {
|
|
2338
2215
|
return isTrusted;
|
|
2339
2216
|
},
|
|
@@ -2372,39 +2249,462 @@ function createWsRpcClientMode(options) {
|
|
|
2372
2249
|
return Promise.reject(failFast);
|
|
2373
2250
|
}
|
|
2374
2251
|
return guardCall(serverRpc.$callOptional(...args), method);
|
|
2252
|
+
},
|
|
2253
|
+
close: () => {
|
|
2254
|
+
channel.close();
|
|
2375
2255
|
}
|
|
2376
2256
|
};
|
|
2377
2257
|
}
|
|
2378
2258
|
//#endregion
|
|
2379
|
-
//#region src/client/
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
});
|
|
2395
|
-
},
|
|
2396
|
-
async delete(key) {
|
|
2397
|
-
(await store()).mutate((draft) => {
|
|
2398
|
-
delete draft[key];
|
|
2399
|
-
});
|
|
2400
|
-
},
|
|
2401
|
-
async all() {
|
|
2402
|
-
return (await store()).value();
|
|
2403
|
-
},
|
|
2404
|
-
async onChange(fn) {
|
|
2405
|
-
return (await store()).on("updated", (full) => fn(full));
|
|
2259
|
+
//#region src/client/rpc-sse.ts
|
|
2260
|
+
/**
|
|
2261
|
+
* Resolve a {@link ConnectionMeta.sse} descriptor into a concrete
|
|
2262
|
+
* `http(s)://` URL, with the same proxy-safe rules as `resolveWsUrl`: the
|
|
2263
|
+
* object / relative-path forms resolve against where `__connection.json`
|
|
2264
|
+
* was loaded (the client trusts its own location, never a server-baked
|
|
2265
|
+
* hostname), while an explicit `port`/`host` (or a full `http(s)://` URL
|
|
2266
|
+
* string) opts into a cross-origin endpoint.
|
|
2267
|
+
*/
|
|
2268
|
+
function resolveSseUrl(sse, metaBaseUrl, loc) {
|
|
2269
|
+
const base = (() => {
|
|
2270
|
+
try {
|
|
2271
|
+
return new URL(metaBaseUrl, loc.href);
|
|
2272
|
+
} catch {
|
|
2273
|
+
return new URL(loc.href);
|
|
2406
2274
|
}
|
|
2407
|
-
};
|
|
2275
|
+
})();
|
|
2276
|
+
if (sse && typeof sse === "object") {
|
|
2277
|
+
if (sse.host != null || sse.port != null) {
|
|
2278
|
+
const host = sse.host ?? `${base.hostname}:${sse.port}`;
|
|
2279
|
+
return new URL(sse.path ?? "/", `${base.protocol}//${host}`).href;
|
|
2280
|
+
}
|
|
2281
|
+
return new URL(sse.path ?? "", base).href;
|
|
2282
|
+
}
|
|
2283
|
+
const str = sse ?? "";
|
|
2284
|
+
if (/^https?:\/\//i.test(str)) return str;
|
|
2285
|
+
return new URL(str, base).href;
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* The SSE-backed live client mode — picked by `connectDevframe` when the
|
|
2289
|
+
* server advertises SSE as its primary transport (`backend: 'sse'`) or when
|
|
2290
|
+
* the caller pins `transport: 'sse'`. Same status machine, call guarding,
|
|
2291
|
+
* and trust handshake as the WebSocket mode, over `createSseRpcChannel`.
|
|
2292
|
+
*/
|
|
2293
|
+
function createSseRpcClientMode(options) {
|
|
2294
|
+
const { authToken, connectionMeta, metaBaseUrl, events, clientRpc, rpcOptions = {}, sseOptions = {}, callTimeout = 0 } = options;
|
|
2295
|
+
const url = resolveSseUrl(connectionMeta.sse, metaBaseUrl ?? "./", location);
|
|
2296
|
+
return createLiveRpcClientMode({
|
|
2297
|
+
transport: "sse",
|
|
2298
|
+
authToken,
|
|
2299
|
+
connectionMeta,
|
|
2300
|
+
events,
|
|
2301
|
+
clientRpc,
|
|
2302
|
+
rpcOptions,
|
|
2303
|
+
callTimeout,
|
|
2304
|
+
createChannel: (handlers) => createSseRpcChannel({
|
|
2305
|
+
url,
|
|
2306
|
+
authToken,
|
|
2307
|
+
definitions: handlers.definitions,
|
|
2308
|
+
...sseOptions,
|
|
2309
|
+
onConnected() {
|
|
2310
|
+
sseOptions.onConnected?.();
|
|
2311
|
+
},
|
|
2312
|
+
onError(error) {
|
|
2313
|
+
handlers.onError(error);
|
|
2314
|
+
sseOptions.onError?.(error);
|
|
2315
|
+
},
|
|
2316
|
+
onDisconnected() {
|
|
2317
|
+
handlers.onDisconnected();
|
|
2318
|
+
sseOptions.onDisconnected?.();
|
|
2319
|
+
}
|
|
2320
|
+
})
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
//#endregion
|
|
2324
|
+
//#region src/rpc/dump/error.ts
|
|
2325
|
+
/**
|
|
2326
|
+
* Inverse of {@link serializeDumpError}: rebuild a thrown `Error` from
|
|
2327
|
+
* the plain object stored in a dump record. Preserves `cause`, restores
|
|
2328
|
+
* the original `name`, and re-attaches any custom own properties.
|
|
2329
|
+
*/
|
|
2330
|
+
function reviveDumpError(stored) {
|
|
2331
|
+
const cause = stored.cause instanceof Error ? stored.cause : isPlainErrorShape(stored.cause) ? reviveDumpError(stored.cause) : stored.cause;
|
|
2332
|
+
const error = cause !== void 0 ? new Error(stored.message, { cause }) : new Error(stored.message);
|
|
2333
|
+
error.name = stored.name;
|
|
2334
|
+
for (const key of Object.keys(stored)) {
|
|
2335
|
+
if (key === "name" || key === "message" || key === "cause") continue;
|
|
2336
|
+
error[key] = stored[key];
|
|
2337
|
+
}
|
|
2338
|
+
return error;
|
|
2339
|
+
}
|
|
2340
|
+
function isPlainErrorShape(value) {
|
|
2341
|
+
return typeof value === "object" && value !== null && typeof value.message === "string" && typeof value.name === "string";
|
|
2342
|
+
}
|
|
2343
|
+
//#endregion
|
|
2344
|
+
//#region src/client/static-rpc.ts
|
|
2345
|
+
function isStaticEntry(value) {
|
|
2346
|
+
return typeof value === "object" && value !== null && value.type === "static" && typeof value.path === "string";
|
|
2347
|
+
}
|
|
2348
|
+
function isQueryEntry(value) {
|
|
2349
|
+
return typeof value === "object" && value !== null && value.type === "query" && typeof value.records === "object" && value.records !== null;
|
|
2350
|
+
}
|
|
2351
|
+
function isRecord(value) {
|
|
2352
|
+
return typeof value === "object" && value !== null && ("output" in value || "error" in value);
|
|
2353
|
+
}
|
|
2354
|
+
function resolveRecordOutput(record) {
|
|
2355
|
+
if (record.error) throw reviveDumpError(record.error);
|
|
2356
|
+
return record.output;
|
|
2357
|
+
}
|
|
2358
|
+
function hasMeaningfulArgs(args) {
|
|
2359
|
+
return args.some((arg) => arg !== null && arg !== void 0);
|
|
2360
|
+
}
|
|
2361
|
+
function unwrapEnvelope(raw) {
|
|
2362
|
+
if (raw !== null && typeof raw === "object" && "serialization" in raw && "data" in raw) return raw.data;
|
|
2363
|
+
return raw;
|
|
2364
|
+
}
|
|
2365
|
+
function createStaticRpcCaller(manifest, fetchJson) {
|
|
2366
|
+
const staticCache = /* @__PURE__ */ new Map();
|
|
2367
|
+
const queryRecordCache = /* @__PURE__ */ new Map();
|
|
2368
|
+
function reviveIfStructuredClone(value, serialization) {
|
|
2369
|
+
if (serialization === "structured-clone" && Array.isArray(value)) return structuredCloneDeserialize(value);
|
|
2370
|
+
return value;
|
|
2371
|
+
}
|
|
2372
|
+
function decode(raw, serialization) {
|
|
2373
|
+
return reviveIfStructuredClone(unwrapEnvelope(raw), serialization);
|
|
2374
|
+
}
|
|
2375
|
+
async function loadStatic(entry) {
|
|
2376
|
+
if (!staticCache.has(entry.path)) staticCache.set(entry.path, fetchJson(entry.path).then((raw) => decode(raw, entry.serialization)));
|
|
2377
|
+
const data = await staticCache.get(entry.path);
|
|
2378
|
+
if (isRecord(data)) return resolveRecordOutput(data);
|
|
2379
|
+
return data;
|
|
2380
|
+
}
|
|
2381
|
+
async function loadQueryRecord(path, serialization) {
|
|
2382
|
+
if (!queryRecordCache.has(path)) queryRecordCache.set(path, fetchJson(path).then((raw) => decode(raw, serialization)));
|
|
2383
|
+
return await queryRecordCache.get(path);
|
|
2384
|
+
}
|
|
2385
|
+
async function call(functionName, args) {
|
|
2386
|
+
if (!(functionName in manifest)) throw new Error(`[devframe-rpc] Function "${functionName}" not found in dump store`);
|
|
2387
|
+
const entry = manifest[functionName];
|
|
2388
|
+
if (isStaticEntry(entry)) {
|
|
2389
|
+
if (hasMeaningfulArgs(args)) throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
|
|
2390
|
+
return await loadStatic(entry);
|
|
2391
|
+
}
|
|
2392
|
+
if (isQueryEntry(entry)) {
|
|
2393
|
+
const argsHash = hash(args);
|
|
2394
|
+
const recordPath = entry.records[argsHash];
|
|
2395
|
+
if (recordPath) return resolveRecordOutput(await loadQueryRecord(recordPath, entry.serialization));
|
|
2396
|
+
if (entry.fallback) return resolveRecordOutput(await loadQueryRecord(entry.fallback, entry.serialization));
|
|
2397
|
+
throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (!hasMeaningfulArgs(args)) return entry;
|
|
2400
|
+
throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
call: async (functionName, args) => await call(functionName, args),
|
|
2404
|
+
callOptional: async (functionName, args) => {
|
|
2405
|
+
if (!(functionName in manifest)) return void 0;
|
|
2406
|
+
return await call(functionName, args);
|
|
2407
|
+
},
|
|
2408
|
+
callEvent: async (_functionName, _args) => {}
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
//#endregion
|
|
2412
|
+
//#region src/client/rpc-static.ts
|
|
2413
|
+
async function createStaticRpcClientMode(options) {
|
|
2414
|
+
const staticCaller = createStaticRpcCaller(await options.fetchJsonFromBases(DEVFRAME_RPC_DUMP_MANIFEST_FILENAME), options.fetchJsonFromBases);
|
|
2415
|
+
return {
|
|
2416
|
+
transport: "static",
|
|
2417
|
+
isTrusted: true,
|
|
2418
|
+
status: "connected",
|
|
2419
|
+
connectionError: null,
|
|
2420
|
+
requestTrust: async () => true,
|
|
2421
|
+
requestTrustWithToken: async () => true,
|
|
2422
|
+
requestTrustWithCode: async () => null,
|
|
2423
|
+
ensureTrusted: async () => true,
|
|
2424
|
+
call: (...args) => staticCaller.call(args[0], args.slice(1)),
|
|
2425
|
+
callEvent: (...args) => staticCaller.callEvent(args[0], args.slice(1)),
|
|
2426
|
+
callOptional: (...args) => staticCaller.callOptional(args[0], args.slice(1)),
|
|
2427
|
+
close: () => {}
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
//#endregion
|
|
2431
|
+
//#region src/client/rpc-streaming.ts
|
|
2432
|
+
const STREAM_KEY_SEPARATOR = "";
|
|
2433
|
+
function streamKey(channel, id) {
|
|
2434
|
+
return `${channel}${STREAM_KEY_SEPARATOR}${id}`;
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Client-side streaming host. Mirrors `createRpcSharedStateClientHost`:
|
|
2438
|
+
* registers the two `:chunk` / `:end` event handlers once, then per-stream
|
|
2439
|
+
* state lives in a `Map<streamKey, StreamReader>`.
|
|
2440
|
+
*/
|
|
2441
|
+
function createRpcStreamingClientHost(rpc) {
|
|
2442
|
+
const readers = /* @__PURE__ */ new Map();
|
|
2443
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
2444
|
+
rpc.client.register({
|
|
2445
|
+
name: "devframe:streaming:chunk",
|
|
2446
|
+
type: "event",
|
|
2447
|
+
handler(channel, id, seq, chunk) {
|
|
2448
|
+
readers.get(streamKey(channel, id))?._push(seq, chunk);
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
rpc.client.register({
|
|
2452
|
+
name: "devframe:streaming:end",
|
|
2453
|
+
type: "event",
|
|
2454
|
+
handler(channel, id, error) {
|
|
2455
|
+
const key = streamKey(channel, id);
|
|
2456
|
+
const reader = readers.get(key);
|
|
2457
|
+
if (!reader) return;
|
|
2458
|
+
reader._end(error);
|
|
2459
|
+
readers.delete(key);
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
rpc.client.register({
|
|
2463
|
+
name: "devframe:streaming:upload-cancel",
|
|
2464
|
+
type: "event",
|
|
2465
|
+
handler(channel, id) {
|
|
2466
|
+
const key = streamKey(channel, id);
|
|
2467
|
+
const sink = uploads.get(key);
|
|
2468
|
+
if (!sink) return;
|
|
2469
|
+
sink.abort("server cancelled upload");
|
|
2470
|
+
uploads.delete(key);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
rpc.events.on("rpc:is-trusted:updated", (isTrusted) => {
|
|
2474
|
+
if (!isTrusted) return;
|
|
2475
|
+
for (const [key, reader] of readers) {
|
|
2476
|
+
if (reader.cancelled || reader.done) continue;
|
|
2477
|
+
const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR);
|
|
2478
|
+
if (sepIdx < 0) continue;
|
|
2479
|
+
const channel = key.slice(0, sepIdx);
|
|
2480
|
+
const id = key.slice(sepIdx + 1);
|
|
2481
|
+
rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: reader.lastSeenSeq });
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
function subscribe(channel, id, options = {}) {
|
|
2485
|
+
const key = streamKey(channel, id);
|
|
2486
|
+
const existing = readers.get(key);
|
|
2487
|
+
if (existing) return existing;
|
|
2488
|
+
const reader = createStreamReader({
|
|
2489
|
+
id,
|
|
2490
|
+
highWaterMark: options.highWaterMark,
|
|
2491
|
+
onOverflow(dropped) {
|
|
2492
|
+
console.warn(`[devframe] DF0029: Stream "${channel}#${id}" dropped ${dropped} chunk(s) after exceeding the client high-water mark.`);
|
|
2493
|
+
},
|
|
2494
|
+
onCancel() {
|
|
2495
|
+
rpc.callEvent("devframe:streaming:cancel", channel, id);
|
|
2496
|
+
readers.delete(key);
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
readers.set(key, reader);
|
|
2500
|
+
if (rpc.isTrusted) rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: 0 });
|
|
2501
|
+
else {
|
|
2502
|
+
const off = rpc.events.on("rpc:is-trusted:updated", (trusted) => {
|
|
2503
|
+
if (trusted) {
|
|
2504
|
+
off();
|
|
2505
|
+
if (readers.has(key) && !reader.cancelled && !reader.done) rpc.callEvent("devframe:streaming:subscribe", channel, id, { afterSeq: reader.lastSeenSeq });
|
|
2506
|
+
}
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return reader;
|
|
2510
|
+
}
|
|
2511
|
+
function upload(channel, id) {
|
|
2512
|
+
const key = streamKey(channel, id);
|
|
2513
|
+
const existing = uploads.get(key);
|
|
2514
|
+
if (existing) return existing;
|
|
2515
|
+
const sink = createStreamSink({ id });
|
|
2516
|
+
sink.events.on("chunk", (seq, chunk) => {
|
|
2517
|
+
rpc.callEvent("devframe:streaming:upload-chunk", channel, id, seq, chunk);
|
|
2518
|
+
});
|
|
2519
|
+
sink.events.on("end", (error) => {
|
|
2520
|
+
rpc.callEvent("devframe:streaming:upload-end", channel, id, error);
|
|
2521
|
+
uploads.delete(key);
|
|
2522
|
+
});
|
|
2523
|
+
uploads.set(key, sink);
|
|
2524
|
+
return sink;
|
|
2525
|
+
}
|
|
2526
|
+
return {
|
|
2527
|
+
subscribe,
|
|
2528
|
+
upload
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
//#endregion
|
|
2532
|
+
//#region src/rpc/transports/ws-client.ts
|
|
2533
|
+
function NOOP() {}
|
|
2534
|
+
const EMPTY_DEFS = /* @__PURE__ */ new Map();
|
|
2535
|
+
/**
|
|
2536
|
+
* Build a birpc `ChannelOptions` object backed by a browser `WebSocket`.
|
|
2537
|
+
* Pass the result straight to `createRpcClient`'s `channel` option.
|
|
2538
|
+
*
|
|
2539
|
+
* Also returns `close()`, closing the underlying socket — mirroring the server transport's
|
|
2540
|
+
* existing `WsRpcTransport.close()`. `birpc`'s own `ChannelOptions` has no teardown of its own.
|
|
2541
|
+
*/
|
|
2542
|
+
function createWsRpcChannel(options) {
|
|
2543
|
+
let url = options.url;
|
|
2544
|
+
if (options.authToken) url = `${url}?${DEVFRAME_AUTH_TOKEN_QUERY_PARAM}=${encodeURIComponent(options.authToken)}`;
|
|
2545
|
+
const ws = new WebSocket(url);
|
|
2546
|
+
const { onConnected = NOOP, onError = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS } = options;
|
|
2547
|
+
ws.addEventListener("open", (e) => {
|
|
2548
|
+
onConnected(e);
|
|
2549
|
+
});
|
|
2550
|
+
ws.addEventListener("error", (e) => {
|
|
2551
|
+
const _e = e instanceof Error ? e : new Error(e.type);
|
|
2552
|
+
onError(_e);
|
|
2553
|
+
});
|
|
2554
|
+
ws.addEventListener("close", (e) => {
|
|
2555
|
+
onDisconnected(e);
|
|
2556
|
+
});
|
|
2557
|
+
const codec = createRpcWireCodec(definitions);
|
|
2558
|
+
return {
|
|
2559
|
+
close: () => {
|
|
2560
|
+
ws.close();
|
|
2561
|
+
},
|
|
2562
|
+
on: (handler) => {
|
|
2563
|
+
ws.addEventListener("message", (e) => {
|
|
2564
|
+
handler(e.data);
|
|
2565
|
+
});
|
|
2566
|
+
},
|
|
2567
|
+
post: (data) => {
|
|
2568
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2569
|
+
ws.send(data);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
2573
|
+
const onOpen = () => {
|
|
2574
|
+
cleanup();
|
|
2575
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
2576
|
+
};
|
|
2577
|
+
const onClose = () => cleanup();
|
|
2578
|
+
function cleanup() {
|
|
2579
|
+
ws.removeEventListener("open", onOpen);
|
|
2580
|
+
ws.removeEventListener("close", onClose);
|
|
2581
|
+
}
|
|
2582
|
+
ws.addEventListener("open", onOpen);
|
|
2583
|
+
ws.addEventListener("close", onClose);
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
onError(/* @__PURE__ */ new Error("Devframe WebSocket is not open; message dropped"));
|
|
2587
|
+
},
|
|
2588
|
+
serialize: codec.serialize,
|
|
2589
|
+
deserialize: codec.deserialize
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
//#endregion
|
|
2593
|
+
//#region src/client/rpc-ws.ts
|
|
2594
|
+
/**
|
|
2595
|
+
* Resolve a {@link ConnectionMeta.websocket} descriptor into a concrete
|
|
2596
|
+
* `ws(s)://` URL.
|
|
2597
|
+
*
|
|
2598
|
+
* The object / relative-path forms connect to the page's own origin (only the
|
|
2599
|
+
* `http`→`ws` protocol swap is applied), resolving the path against where
|
|
2600
|
+
* `__connection.json` was loaded. This is deliberately host-agnostic so the
|
|
2601
|
+
* connection survives a reverse proxy that changes the domain or port — the
|
|
2602
|
+
* client trusts its own location, never a server-baked hostname. An explicit
|
|
2603
|
+
* `port`/`host` (or a full `ws(s)://` URL string) opts into a cross-origin
|
|
2604
|
+
* endpoint, e.g. a side-car server on its own port.
|
|
2605
|
+
*/
|
|
2606
|
+
function resolveWsUrl(websocket, metaBaseUrl, loc) {
|
|
2607
|
+
const base = (() => {
|
|
2608
|
+
try {
|
|
2609
|
+
return new URL(metaBaseUrl, loc.href);
|
|
2610
|
+
} catch {
|
|
2611
|
+
return new URL(loc.href);
|
|
2612
|
+
}
|
|
2613
|
+
})();
|
|
2614
|
+
const wsProtocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
2615
|
+
if (websocket && typeof websocket === "object") {
|
|
2616
|
+
if (websocket.host != null || websocket.port != null) {
|
|
2617
|
+
const host = websocket.host ?? `${base.hostname}:${websocket.port}`;
|
|
2618
|
+
const target = new URL(websocket.path ?? "/", `${wsProtocol}//${host}`);
|
|
2619
|
+
target.protocol = wsProtocol;
|
|
2620
|
+
return target.href;
|
|
2621
|
+
}
|
|
2622
|
+
const target = new URL(websocket.path ?? "", base);
|
|
2623
|
+
target.protocol = wsProtocol;
|
|
2624
|
+
return target.href;
|
|
2625
|
+
}
|
|
2626
|
+
if (typeof websocket === "number") return `${wsProtocol}//${base.hostname}:${websocket}`;
|
|
2627
|
+
const str = websocket ?? "";
|
|
2628
|
+
if (/^wss?:\/\//i.test(str)) return str;
|
|
2629
|
+
if (/^https?:\/\//i.test(str)) return withProtocol(str, /^https/i.test(str) ? "wss://" : "ws://");
|
|
2630
|
+
const target = new URL(str, base);
|
|
2631
|
+
target.protocol = wsProtocol;
|
|
2632
|
+
return target.href;
|
|
2633
|
+
}
|
|
2634
|
+
function createWsRpcClientMode(options) {
|
|
2635
|
+
const { authToken, connectionMeta, metaBaseUrl, events, clientRpc, rpcOptions = {}, wsOptions = {}, callTimeout = 0 } = options;
|
|
2636
|
+
const url = resolveWsUrl(connectionMeta.websocket, metaBaseUrl ?? "./", location);
|
|
2637
|
+
return createLiveRpcClientMode({
|
|
2638
|
+
transport: "websocket",
|
|
2639
|
+
authToken,
|
|
2640
|
+
connectionMeta,
|
|
2641
|
+
events,
|
|
2642
|
+
clientRpc,
|
|
2643
|
+
rpcOptions,
|
|
2644
|
+
callTimeout,
|
|
2645
|
+
createChannel: (handlers) => createWsRpcChannel({
|
|
2646
|
+
url,
|
|
2647
|
+
authToken,
|
|
2648
|
+
definitions: handlers.definitions,
|
|
2649
|
+
...wsOptions,
|
|
2650
|
+
onConnected(event) {
|
|
2651
|
+
wsOptions.onConnected?.(event);
|
|
2652
|
+
},
|
|
2653
|
+
onError(error) {
|
|
2654
|
+
handlers.onError(error);
|
|
2655
|
+
wsOptions.onError?.(error);
|
|
2656
|
+
},
|
|
2657
|
+
onDisconnected(event) {
|
|
2658
|
+
handlers.onDisconnected();
|
|
2659
|
+
wsOptions.onDisconnected?.(event);
|
|
2660
|
+
}
|
|
2661
|
+
})
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/utils/scope.ts
|
|
2666
|
+
/** Whether a name is already namespaced (contains a `:` separator). */
|
|
2667
|
+
function isQualifiedName(name) {
|
|
2668
|
+
return name.includes(":");
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Prefix a bare name with `<namespace>:`. Names that already contain a
|
|
2672
|
+
* `:` are returned unchanged, so callers can reference another scope's
|
|
2673
|
+
* ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`).
|
|
2674
|
+
*/
|
|
2675
|
+
function qualifyName(namespace, name) {
|
|
2676
|
+
return isQualifiedName(name) ? name : `${namespace}:${name}`;
|
|
2677
|
+
}
|
|
2678
|
+
//#endregion
|
|
2679
|
+
//#region src/client/settings.ts
|
|
2680
|
+
function createClientSettingsStore(rpc, namespace, scope) {
|
|
2681
|
+
const stateKey = `devframe:settings:${scope}:${namespace}`;
|
|
2682
|
+
let statePromise;
|
|
2683
|
+
function store() {
|
|
2684
|
+
if (!statePromise) statePromise = rpc.sharedState.get(stateKey, { initialValue: {} });
|
|
2685
|
+
return statePromise;
|
|
2686
|
+
}
|
|
2687
|
+
return {
|
|
2688
|
+
async get(key) {
|
|
2689
|
+
return (await store()).value()[key];
|
|
2690
|
+
},
|
|
2691
|
+
async set(key, value) {
|
|
2692
|
+
(await store()).mutate((draft) => {
|
|
2693
|
+
draft[key] = value;
|
|
2694
|
+
});
|
|
2695
|
+
},
|
|
2696
|
+
async delete(key) {
|
|
2697
|
+
(await store()).mutate((draft) => {
|
|
2698
|
+
delete draft[key];
|
|
2699
|
+
});
|
|
2700
|
+
},
|
|
2701
|
+
async all() {
|
|
2702
|
+
return (await store()).value();
|
|
2703
|
+
},
|
|
2704
|
+
async onChange(fn) {
|
|
2705
|
+
return (await store()).on("updated", (full) => fn(full));
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2408
2708
|
}
|
|
2409
2709
|
/**
|
|
2410
2710
|
* Build the client-side `settings` surface for a scope namespace. Mirrors
|
|
@@ -2451,6 +2751,30 @@ function createScopedClientContext(rpc, namespace) {
|
|
|
2451
2751
|
}
|
|
2452
2752
|
//#endregion
|
|
2453
2753
|
//#region src/client/rpc.ts
|
|
2754
|
+
/**
|
|
2755
|
+
* Resolve the requested `transport` option against what the server
|
|
2756
|
+
* advertises. `'auto'` trusts the advertisement — the server's declared
|
|
2757
|
+
* primary (`backend`), preferring the WebSocket when both endpoints are
|
|
2758
|
+
* present; an explicit `'websocket'` / `'sse'` pins that transport and
|
|
2759
|
+
* throws when the server doesn't advertise it.
|
|
2760
|
+
*/
|
|
2761
|
+
function resolveClientTransport(requested, meta) {
|
|
2762
|
+
if (meta.backend === "static") return "static";
|
|
2763
|
+
const hasWebsocket = meta.websocket !== void 0;
|
|
2764
|
+
const hasSse = meta.sse !== void 0;
|
|
2765
|
+
if (requested === "websocket") {
|
|
2766
|
+
if (!hasWebsocket) throw new Error("[devframe] transport: 'websocket' was requested, but this server does not advertise a WebSocket endpoint");
|
|
2767
|
+
return "websocket";
|
|
2768
|
+
}
|
|
2769
|
+
if (requested === "sse") {
|
|
2770
|
+
if (!hasSse) throw new Error("[devframe] transport: 'sse' was requested, but this server does not advertise an SSE endpoint");
|
|
2771
|
+
return "sse";
|
|
2772
|
+
}
|
|
2773
|
+
if (meta.backend === "sse" && hasSse) return "sse";
|
|
2774
|
+
if (hasWebsocket) return "websocket";
|
|
2775
|
+
if (hasSse) return "sse";
|
|
2776
|
+
throw new Error("[devframe] This server advertises no RPC transport (backend \"none\") — nothing to connect to. Enable the WebSocket or SSE endpoint on the server, or use its static/MCP surfaces instead.");
|
|
2777
|
+
}
|
|
2454
2778
|
async function getDevframeRpcClient(options = {}) {
|
|
2455
2779
|
const { baseURL = "./", rpcOptions = {}, cacheOptions = false } = options;
|
|
2456
2780
|
const events = createEventEmitter();
|
|
@@ -2480,7 +2804,7 @@ async function getDevframeRpcClient(options = {}) {
|
|
|
2480
2804
|
}
|
|
2481
2805
|
throw new Error(`Failed to load ${path} from ${candidates.join(", ")}`, { cause: errors });
|
|
2482
2806
|
}
|
|
2483
|
-
const
|
|
2807
|
+
const liveModeOptions = {
|
|
2484
2808
|
authToken,
|
|
2485
2809
|
connectionMeta,
|
|
2486
2810
|
metaBaseUrl,
|
|
@@ -2497,7 +2821,14 @@ async function getDevframeRpcClient(options = {}) {
|
|
|
2497
2821
|
cacheManager.apply(req, res);
|
|
2498
2822
|
} else await next(req);
|
|
2499
2823
|
}
|
|
2500
|
-
}
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
const transport = resolveClientTransport(options.transport ?? "auto", connectionMeta);
|
|
2827
|
+
const mode = transport === "static" ? await createStaticRpcClientMode({ fetchJsonFromBases }) : transport === "sse" ? createSseRpcClientMode({
|
|
2828
|
+
...liveModeOptions,
|
|
2829
|
+
sseOptions: options.sseOptions
|
|
2830
|
+
}) : createWsRpcClientMode({
|
|
2831
|
+
...liveModeOptions,
|
|
2501
2832
|
wsOptions: options.wsOptions
|
|
2502
2833
|
});
|
|
2503
2834
|
let authChannel;
|
|
@@ -2523,6 +2854,9 @@ async function getDevframeRpcClient(options = {}) {
|
|
|
2523
2854
|
get connectionError() {
|
|
2524
2855
|
return mode.connectionError;
|
|
2525
2856
|
},
|
|
2857
|
+
get transport() {
|
|
2858
|
+
return mode.transport ?? transport;
|
|
2859
|
+
},
|
|
2526
2860
|
get connection() {
|
|
2527
2861
|
return connection;
|
|
2528
2862
|
},
|
|
@@ -2560,7 +2894,8 @@ async function getDevframeRpcClient(options = {}) {
|
|
|
2560
2894
|
sharedState: void 0,
|
|
2561
2895
|
streaming: void 0,
|
|
2562
2896
|
cacheManager,
|
|
2563
|
-
scope: void 0
|
|
2897
|
+
scope: void 0,
|
|
2898
|
+
close: () => mode.close?.()
|
|
2564
2899
|
};
|
|
2565
2900
|
rpc.sharedState = createRpcSharedStateClientHost(rpc);
|
|
2566
2901
|
rpc.streaming = createRpcStreamingClientHost(rpc);
|
|
@@ -2614,4 +2949,4 @@ async function getDevframeRpcClient(options = {}) {
|
|
|
2614
2949
|
//#region src/client/index.ts
|
|
2615
2950
|
const connectDevframe = getDevframeRpcClient;
|
|
2616
2951
|
//#endregion
|
|
2617
|
-
export { DevframeConnectionError, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, registerDevframeViewerOrigin, resolveWsUrl, setupDevframeConnection };
|
|
2952
|
+
export { DevframeConnectionError, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, registerDevframeViewerOrigin, resolveClientTransport, resolveSseUrl, resolveWsUrl, setupDevframeConnection };
|