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.
Files changed (103) hide show
  1. package/dist/_shared-CttmCMBi.mjs +70 -0
  2. package/dist/_shared-DmpWW-HH.d.mts +43 -0
  3. package/dist/adapters/build.d.mts +1 -1
  4. package/dist/adapters/build.mjs +6 -5
  5. package/dist/adapters/cac.d.mts +1 -1
  6. package/dist/adapters/cac.mjs +157 -1
  7. package/dist/adapters/dev.d.mts +20 -42
  8. package/dist/adapters/dev.mjs +2 -1
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +223 -0
  11. package/dist/adapters/initiate.mjs +2 -0
  12. package/dist/adapters/mcp.d.mts +26 -2
  13. package/dist/adapters/mcp.mjs +2 -2
  14. package/dist/cli/main.mjs +2 -2
  15. package/dist/client/index.d.mts +70 -6
  16. package/dist/client/index.mjs +743 -408
  17. package/dist/constants.d.mts +19 -6
  18. package/dist/constants.mjs +19 -6
  19. package/dist/{context-CBg8iRbQ.mjs → context-C33rR7n0.mjs} +1 -1
  20. package/dist/{context-DoSozfvx.d.mts → context-CSCwoABx.d.mts} +1 -1
  21. package/dist/{host-h3-D2hYZvDK.mjs → context-Du11TLcO.mjs} +5 -255
  22. package/dist/dev-BEveeA5H.mjs +233 -0
  23. package/dist/{devframe-mQ4qZAuo.d.mts → devframe-Bdd9YXTy.d.mts} +88 -25
  24. package/dist/{diagnostics-D7wM2cwM.mjs → diagnostics-DLYH5oD3.mjs} +16 -0
  25. package/dist/dist-D8jAMwxU.mjs +227 -0
  26. package/dist/helpers/vite.d.mts +43 -39
  27. package/dist/helpers/vite.mjs +26 -38
  28. package/dist/host-agent-CpbAoqBx.mjs +230 -0
  29. package/dist/host-h3-CzCZwtuV.mjs +26 -0
  30. package/dist/{fetch-BKaD0Vb3.mjs → http-DLr_cJZ7.mjs} +42 -4
  31. package/dist/{index-b64Uuhy2.d.mts → index-CZRd2K1C.d.mts} +2 -2
  32. package/dist/{index-Dbw1p5ch.d.mts → index-DSCq4M4q.d.mts} +2 -31
  33. package/dist/index.d.mts +9 -4
  34. package/dist/index.mjs +8 -1
  35. package/dist/{instance-registry-BnnA2Eoh.mjs → instance-registry-BQqGoDO9.mjs} +9 -2
  36. package/dist/instance-shell-DBTXTSjN.d.mts +264 -0
  37. package/dist/instance-shell-KRdee1QF.mjs +485 -0
  38. package/dist/internal/index.d.mts +150 -0
  39. package/dist/internal/index.mjs +7 -0
  40. package/dist/node/auth.d.mts +2 -2
  41. package/dist/node/hub-internals.d.mts +2 -14
  42. package/dist/node/hub-internals.mjs +2 -2
  43. package/dist/node/index.d.mts +3 -267
  44. package/dist/node/index.mjs +3 -5
  45. package/dist/recipes/common-rpc-functions.d.mts +3 -3
  46. package/dist/recipes/interactive-auth.d.mts +3 -3
  47. package/dist/recipes/interactive-auth.mjs +6 -6
  48. package/dist/rolldown-runtime-JspESFgx.mjs +35 -0
  49. package/dist/rpc/dump.d.mts +77 -1
  50. package/dist/rpc/dump.mjs +1 -1
  51. package/dist/rpc/index.d.mts +2 -2
  52. package/dist/rpc/index.mjs +3 -17
  53. package/dist/rpc/transports/sse-client.d.mts +2 -0
  54. package/dist/rpc/transports/sse-client.mjs +142 -0
  55. package/dist/rpc/transports/sse-server.d.mts +61 -0
  56. package/dist/rpc/transports/sse-server.mjs +209 -0
  57. package/dist/rpc/transports/ws-bun.d.mts +35 -0
  58. package/dist/rpc/transports/ws-bun.mjs +31 -0
  59. package/dist/rpc/transports/ws-client.d.mts +1 -1
  60. package/dist/rpc/transports/ws-client.mjs +10 -18
  61. package/dist/rpc/transports/ws-server.d.mts +2 -2
  62. package/dist/rpc/transports/ws-server.mjs +2 -235
  63. package/dist/rpc-core-CGctXToO.d.mts +48 -0
  64. package/dist/rpc-core-CqcJkAu9.mjs +81 -0
  65. package/dist/{serialization-C8Mnw9hK.mjs → serialization-BHY8M5NQ.mjs} +1 -1
  66. package/dist/{shared-state-6kHolYZy.mjs → shared-state-fRRbRUtD.mjs} +1 -1
  67. package/dist/sse-client-BsIK3hMD.d.mts +37 -0
  68. package/dist/{dump-CgZShDRB.mjs → static-CvjkO8QE.mjs} +2 -265
  69. package/dist/{storage-CTNVbuFB.mjs → storage-BcmVEJRc.mjs} +2 -2
  70. package/dist/types/index.d.mts +3 -3
  71. package/dist/types/index.mjs +1 -6
  72. package/dist/utils/events.d.mts +1 -1
  73. package/dist/utils/launch-editor.mjs +1 -25
  74. package/dist/utils/open.mjs +1 -1
  75. package/dist/utils/serve-static.mjs +1 -1
  76. package/dist/utils/shared-state.d.mts +1 -1
  77. package/dist/utils/shared-state.mjs +1 -1
  78. package/dist/utils/simple-schema.d.mts +101 -1
  79. package/dist/utils/streaming-channel.d.mts +1 -1
  80. package/dist/validation-BiuDXTKW.mjs +266 -0
  81. package/dist/wire-codec-9VhIKnyp.mjs +58 -0
  82. package/dist/{ws-client-B6Tnr0ic.d.mts → ws-client-CdNuATYa.d.mts} +6 -1
  83. package/dist/ws-server-Bk1hOG3X.mjs +267 -0
  84. package/dist/{ws-server-uKQcClcJ.d.mts → ws-server-Ces08fjP.d.mts} +89 -8
  85. package/package.json +10 -9
  86. package/skills/devframe/SKILL.md +84 -85
  87. package/dist/_shared-bWRzeSa0.mjs +0 -19
  88. package/dist/adapters/cli.d.mts +0 -11
  89. package/dist/adapters/cli.mjs +0 -6
  90. package/dist/cac-D4_9ok0z.mjs +0 -157
  91. package/dist/dev-Bc49u3qg.mjs +0 -439
  92. package/dist/http-C4T68YV6.mjs +0 -38
  93. package/dist/index-vAX6bYuc.d.mts +0 -78
  94. package/dist/recipes/open-helpers.d.mts +0 -6
  95. package/dist/recipes/open-helpers.mjs +0 -6
  96. package/dist/server-CD8XmBXl.mjs +0 -167
  97. package/dist/server-UHWae43Q.d.mts +0 -149
  98. package/dist/simple-schema-BDzLeJDk.d.mts +0 -102
  99. package/dist/utils/promise.d.mts +0 -8
  100. package/dist/utils/promise.mjs +0 -15
  101. package/dist/utils/scope.d.mts +0 -11
  102. package/dist/utils/scope.mjs +0 -15
  103. package/dist/{open-Deb5xmIT.mjs → open-Dz4V4irZ.mjs} +1 -1
@@ -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-6kHolYZy.mjs";
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/client/static-rpc.ts
710
- function isStaticEntry(value) {
711
- return typeof value === "object" && value !== null && value.type === "static" && typeof value.path === "string";
712
- }
713
- function isQueryEntry(value) {
714
- return typeof value === "object" && value !== null && value.type === "query" && typeof value.records === "object" && value.records !== null;
715
- }
716
- function isRecord(value) {
717
- return typeof value === "object" && value !== null && ("output" in value || "error" in value);
718
- }
719
- function resolveRecordOutput(record) {
720
- if (record.error) throw reviveDumpError(record.error);
721
- return record.output;
722
- }
723
- function hasMeaningfulArgs(args) {
724
- return args.some((arg) => arg !== null && arg !== void 0);
725
- }
726
- function unwrapEnvelope(raw) {
727
- if (raw !== null && typeof raw === "object" && "serialization" in raw && "data" in raw) return raw.data;
728
- return raw;
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
- function createStaticRpcCaller(manifest, fetchJson) {
731
- const staticCache = /* @__PURE__ */ new Map();
732
- const queryRecordCache = /* @__PURE__ */ new Map();
733
- function reviveIfStructuredClone(value, serialization) {
734
- if (serialization === "structured-clone" && Array.isArray(value)) return structuredCloneDeserialize(value);
735
- return value;
736
- }
737
- function decode(raw, serialization) {
738
- return reviveIfStructuredClone(unwrapEnvelope(raw), serialization);
739
- }
740
- async function loadStatic(entry) {
741
- if (!staticCache.has(entry.path)) staticCache.set(entry.path, fetchJson(entry.path).then((raw) => decode(raw, entry.serialization)));
742
- const data = await staticCache.get(entry.path);
743
- if (isRecord(data)) return resolveRecordOutput(data);
744
- return data;
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
- async function loadQueryRecord(path, serialization) {
747
- if (!queryRecordCache.has(path)) queryRecordCache.set(path, fetchJson(path).then((raw) => decode(raw, serialization)));
748
- return await queryRecordCache.get(path);
767
+ function end() {
768
+ if (closed) return;
769
+ closed = true;
770
+ rejectSession(/* @__PURE__ */ new Error("Devframe SSE stream closed"));
771
+ onDisconnected();
749
772
  }
750
- async function call(functionName, args) {
751
- if (!(functionName in manifest)) throw new Error(`[devframe-rpc] Function "${functionName}" not found in dump store`);
752
- const entry = manifest[functionName];
753
- if (isStaticEntry(entry)) {
754
- if (hasMeaningfulArgs(args)) throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
755
- return await loadStatic(entry);
773
+ function dispatch(event, data) {
774
+ if (event === "session") {
775
+ resolveSession(data);
776
+ onConnected();
777
+ return;
756
778
  }
757
- if (isQueryEntry(entry)) {
758
- const argsHash = hash(args);
759
- const recordPath = entry.records[argsHash];
760
- if (recordPath) return resolveRecordOutput(await loadQueryRecord(recordPath, entry.serialization));
761
- if (entry.fallback) return resolveRecordOutput(await loadQueryRecord(entry.fallback, entry.serialization));
762
- throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
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
- if (!hasMeaningfulArgs(args)) return entry;
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
- call: async (functionName, args) => await call(functionName, args),
769
- callOptional: async (functionName, args) => {
770
- if (!(functionName in manifest)) return void 0;
771
- return await call(functionName, args);
825
+ close: () => {
826
+ closed = true;
827
+ abort.abort();
828
+ activeReader?.cancel().catch(() => {});
772
829
  },
773
- callEvent: async (_functionName, _args) => {}
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/rpc-static.ts
778
- async function createStaticRpcClientMode(options) {
779
- const staticCaller = createStaticRpcCaller(await options.fetchJsonFromBases(DEVFRAME_RPC_DUMP_MANIFEST_FILENAME), options.fetchJsonFromBases);
780
- return {
781
- isTrusted: true,
782
- status: "connected",
783
- connectionError: null,
784
- requestTrust: async () => true,
785
- requestTrustWithToken: async () => true,
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/client/rpc-ws.ts
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
- * Resolve a {@link ConnectionMeta.websocket} descriptor into a concrete
2139
- * `ws(s)://` URL.
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 resolveWsUrl(websocket, metaBaseUrl, loc) {
2150
- const base = (() => {
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: createWsRpcChannel({
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/settings.ts
2380
- function createClientSettingsStore(rpc, namespace, scope) {
2381
- const stateKey = `devframe:settings:${scope}:${namespace}`;
2382
- let statePromise;
2383
- function store() {
2384
- if (!statePromise) statePromise = rpc.sharedState.get(stateKey, { initialValue: {} });
2385
- return statePromise;
2386
- }
2387
- return {
2388
- async get(key) {
2389
- return (await store()).value()[key];
2390
- },
2391
- async set(key, value) {
2392
- (await store()).mutate((draft) => {
2393
- draft[key] = value;
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 mode = connectionMeta.backend === "static" ? await createStaticRpcClientMode({ fetchJsonFromBases }) : createWsRpcClientMode({
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 };