@dormer/renderer-bridge 0.0.0-stage → 0.1.0

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 (3) hide show
  1. package/package.json +16 -4
  2. package/src/bridge.js +164 -0
  3. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "@dormer/renderer-bridge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The script the host injects into every page at document start (dependency-free IIFE)",
5
+ "type": "module",
6
+ "license": "Apache-2.0 OR MIT",
7
+ "files": [
8
+ "src/bridge.js"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/veerareddyvishal144/dormer.git",
16
+ "directory": "runtime/renderer-bridge"
17
+ }
18
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,164 @@
1
+ // Dormer renderer bridge, injected by the host at document start (main frame only).
2
+ // Placeholders are substituted by the host before injection:
3
+ // __DORMER_ROUTE__ window route id (number)
4
+ // __DORMER_STREAM_BASE__ base URL for pull-based stream chunks, e.g. dormer://ipc/stream
5
+ // __DORMER_IPC_BASE__ origin of the bulk path, e.g. dormer://ipc (POST <base>/call/<channel>)
6
+ // __DORMER_KEY__ per-window random key sent with custom-scheme requests
7
+ // __DORMER_PLATFORM__ "darwin" | "win32" | "linux"
8
+ // Security model (ADR-0005): identity is stamped by the host from the delivering webview; nothing in
9
+ // this script is trusted by the host. Everything here is captured in a closure before page code runs.
10
+ (() => {
11
+ "use strict";
12
+ if (Object.prototype.hasOwnProperty.call(window, "dormer")) return;
13
+ const w = window;
14
+ // Capture the platform primitive directly (independent of other init scripts' ordering):
15
+ // WebKit: window.webkit.messageHandlers.ipc (registered by the host); WebView2: chrome.webview.
16
+ function resolvePost() {
17
+ try {
18
+ const h = w.webkit && w.webkit.messageHandlers && w.webkit.messageHandlers.ipc;
19
+ if (h && typeof h.postMessage === "function") return h.postMessage.bind(h);
20
+ } catch {}
21
+ try { if (w.chrome && w.chrome.webview && typeof w.chrome.webview.postMessage === "function") return w.chrome.webview.postMessage.bind(w.chrome.webview); } catch {}
22
+ try { if (w.ipc && typeof w.ipc.postMessage === "function") return w.ipc.postMessage.bind(w.ipc); } catch {}
23
+ return null;
24
+ }
25
+ const postRaw = resolvePost();
26
+ const JSONstringify = JSON.stringify, JSONparse = JSON.parse;
27
+ const Fetch = w.fetch.bind(w), PromiseCtor = w.Promise, MapCtor = w.Map, ReadableStreamCtor = w.ReadableStream;
28
+ const freeze = Object.freeze, defineProperty = Object.defineProperty;
29
+ const route = __DORMER_ROUTE__;
30
+ const streamBase = "__DORMER_STREAM_BASE__";
31
+ const ipcBase = "__DORMER_IPC_BASE__";
32
+ const key = "__DORMER_KEY__";
33
+ // Bulk path limits (ADR-0005 §3, PROTOCOL.md §8): one POST body stays under the 16 MiB frame cap;
34
+ // larger uploads are split into sequential calls carrying a trailing {__dormerChunk} argument.
35
+ const MAX_BULK_BODY = 16 * 1024 * 1024 - 4096;
36
+ const BULK_CHUNK = 8 * 1024 * 1024;
37
+ const encodeURI = w.encodeURIComponent;
38
+ const platform = "__DORMER_PLATFORM__";
39
+
40
+ const T = { REQ: 2, RES: 3, ERR: 4, EVT: 5, SUB: 6, UNSUB: 7, CANCEL: 11 };
41
+ const pending = new MapCtor(); // id -> {resolve, reject}
42
+ const listeners = new MapCtor(); // channel -> Set<fn>
43
+ let nextId = 1; // odd ids: renderer-initiated
44
+
45
+ class DormerError extends Error {
46
+ constructor(code, message, data, retry) { super(message); this.name = "DormerError"; this.code = code; this.data = data; this.retry = !!retry; }
47
+ }
48
+
49
+ function post(t, i, p) {
50
+ if (!postRaw) throw new DormerError("ENOTSUP", "bridge transport unavailable");
51
+ postRaw(JSONstringify({ t, i, p }));
52
+ }
53
+ function request(method, args, opts) {
54
+ return new PromiseCtor((resolve, reject) => {
55
+ const id = nextId; nextId = (nextId + 2) >>> 0 || 1;
56
+ pending.set(id, { resolve, reject });
57
+ const signal = opts && opts.signal;
58
+ if (signal) {
59
+ if (signal.aborted) { pending.delete(id); reject(new DormerError("ECANCELED", "aborted")); return; }
60
+ signal.addEventListener("abort", () => { if (pending.has(id)) { pending.delete(id); post(T.CANCEL, id); reject(new DormerError("ECANCELED", "aborted")); } }, { once: true });
61
+ }
62
+ try { post(T.REQ, id, { m: method, a: args }); } catch (e) { pending.delete(id); reject(e); }
63
+ });
64
+ }
65
+
66
+ function invoke(channel, ...args) { return request("ipc:" + channel, args); }
67
+ function invokeWith(opts, channel, ...args) { return request("ipc:" + channel, args, opts); }
68
+ function send(channel, ...args) { post(T.EVT, 0, { ch: "send:" + channel, data: args }); }
69
+ function on(channel, fn) {
70
+ let set = listeners.get(channel);
71
+ if (!set) { set = new Set(); listeners.set(channel, set); }
72
+ set.add(fn);
73
+ return () => { set.delete(fn); };
74
+ }
75
+ function once(channel, fn) { const off = on(channel, (...a) => { off(); fn(...a); }); return off; }
76
+ function off(channel, fn) { const set = listeners.get(channel); if (set) set.delete(fn); }
77
+
78
+ // Pull-based streaming: the host answers each chunk request with the next bytes from Node.
79
+ function stream(channel, ...args) {
80
+ let streamId = null, seq = 0, done = false;
81
+ const opened = request("ipc:" + channel, args).then((r) => {
82
+ if (!r || typeof r.__dormerStream !== "number") throw new DormerError("EINVAL", "handler is not a stream handler");
83
+ streamId = r.__dormerStream;
84
+ });
85
+ return new ReadableStreamCtor({
86
+ async pull(controller) {
87
+ await opened;
88
+ if (done) return;
89
+ const res = await Fetch(streamBase + "/" + streamId + "/" + (seq++) + "?k=" + key, { cache: "no-store" });
90
+ if (res.status === 204) { done = true; controller.close(); return; }
91
+ if (!res.ok) { done = true; const txt = await res.text(); controller.error(new DormerError(res.headers.get("x-dormer-code") || "EINTERNAL", txt || res.statusText)); return; }
92
+ const buf = new Uint8Array(await res.arrayBuffer());
93
+ if (buf.byteLength) controller.enqueue(buf);
94
+ if (res.headers.get("x-dormer-more") === "0") { done = true; controller.close(); }
95
+ },
96
+ cancel() { done = true; if (streamId !== null) post(T.CANCEL, streamId); },
97
+ }, { highWaterMark: 2 });
98
+ }
99
+
100
+ // Bulk/binary upload (ADR-0005 §3): POST the bytes to the host's custom scheme; the host forwards
101
+ // them to Node as the first argument (a Buffer) of `ipcMain.handle(channel, (e, bytes, ...args))`.
102
+ // The response is a Uint8Array when the handler returns a Buffer/Uint8Array, else the JSON value.
103
+ // Bodies above MAX_BULK_BODY are sent as sequential chunk calls; each call then receives an extra
104
+ // trailing argument {__dormerChunk: {upload, chunk, of, total}} and the result of the last call
105
+ // is returned. Handlers that accept large uploads must reassemble by `upload` id.
106
+ function toBytes(data) {
107
+ if (data instanceof Uint8Array) return data;
108
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
109
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
110
+ throw new DormerError("EINVAL", "invokeBinary expects a Uint8Array or ArrayBuffer");
111
+ }
112
+ async function postBulk(channel, bytes, args, signal) {
113
+ const url = ipcBase + "/call/" + encodeURI("" + channel) + "?a=" + encodeURI(JSONstringify(args)) + "&k=" + key;
114
+ let res;
115
+ try { res = await Fetch(url, { method: "POST", body: bytes, cache: "no-store", signal }); }
116
+ catch (e) { if (e && e.name === "AbortError") throw new DormerError("ECANCELED", "aborted"); throw new DormerError("ECONNRESET", "bulk transport failed: " + (e && e.message || e)); }
117
+ if (!res.ok) {
118
+ let body = null; try { body = await res.json(); } catch {}
119
+ throw new DormerError(res.headers.get("x-dormer-code") || (body && body.c) || "EINTERNAL", (body && body.msg) || res.statusText || "error", body && body.data, body && body.retry);
120
+ }
121
+ const ct = res.headers.get("content-type") || "";
122
+ if (ct.indexOf("application/octet-stream") === 0) return new Uint8Array(await res.arrayBuffer());
123
+ const text = await res.text();
124
+ return text === "" ? null : JSONparse(text);
125
+ }
126
+ async function invokeBinaryWith(opts, channel, data, ...args) {
127
+ const bytes = toBytes(data);
128
+ const signal = opts && opts.signal;
129
+ if (bytes.byteLength <= MAX_BULK_BODY) return postBulk(channel, bytes, args, signal);
130
+ const of = Math.ceil(bytes.byteLength / BULK_CHUNK);
131
+ const upload = key.slice(0, 8) + "-" + Date.now().toString(36) + "-" + Math.floor(Math.random() * 1e9).toString(36);
132
+ let result = null;
133
+ for (let i = 0; i < of; i++) {
134
+ const part = bytes.subarray(i * BULK_CHUNK, Math.min(bytes.byteLength, (i + 1) * BULK_CHUNK));
135
+ result = await postBulk(channel, part, args.concat([{ __dormerChunk: { upload, chunk: i, of, total: bytes.byteLength } }]), signal);
136
+ }
137
+ return result;
138
+ }
139
+ function invokeBinary(channel, data, ...args) { return invokeBinaryWith(null, channel, data, ...args); }
140
+
141
+ // The backend died (host:app.backend-crashed): every pending request belongs to a process that is
142
+ // gone, so fail them now instead of hanging until the page reloads.
143
+ function failPending(code, message) {
144
+ const entries = [...pending.values()]; pending.clear();
145
+ for (const pr of entries) { try { pr.reject(new DormerError(code, message, undefined, true)); } catch {} }
146
+ }
147
+
148
+ // Host -> page entry point. Only ever called by the host with a JSON literal it produced.
149
+ function dispatch(json) {
150
+ let f; try { f = typeof json === "string" ? JSONparse(json) : json; } catch { return; }
151
+ if (!f || typeof f !== "object") return;
152
+ const p = f.p || {};
153
+ if (f.t === T.RES) { const pr = pending.get(f.i); if (pr) { pending.delete(f.i); pr.resolve(p.r); } }
154
+ else if (f.t === T.ERR) { const pr = pending.get(f.i); if (pr) { pending.delete(f.i); pr.reject(new DormerError(p.c || "EINTERNAL", p.msg || "error", p.data, p.retry)); } }
155
+ else if (f.t === T.EVT) {
156
+ if (p.ch === "host:app.backend-crashed") failPending("ECONNRESET", "backend crashed");
157
+ const set = listeners.get(p.ch); if (set) { const args = Array.isArray(p.data) ? p.data : [p.data]; for (const fn of [...set]) { try { fn(...args); } catch (e) { console.error("[dormer] listener threw", e); } } }
158
+ }
159
+ }
160
+
161
+ const api = freeze({ invoke, invokeWith, invokeBinary, invokeBinaryWith, send, on, once, off, stream, windowId: route, platform, DormerError, version: 1 });
162
+ defineProperty(w, "dormer", { value: api, writable: false, configurable: false, enumerable: false });
163
+ defineProperty(w, "__dormer", { value: freeze({ dispatch }), writable: false, configurable: false, enumerable: false });
164
+ })();
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.