@hulla/api-message-port 0.0.0-stage → 2.0.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.
- package/dist/desktop-BojxzZgV.js +155 -0
- package/dist/desktop-q9GwZShg.d.ts +21 -0
- package/dist/desktop.d.ts +2 -0
- package/dist/desktop.js +3 -0
- package/dist/dioxus.d.ts +11 -0
- package/dist/dioxus.js +37 -0
- package/dist/electron.d.ts +2 -0
- package/dist/electron.js +3 -0
- package/dist/endpoint-CPsj0RmN.d.ts +108 -0
- package/dist/endpoint-y_ZFW2WR.js +34 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +685 -0
- package/dist/protocol-DrDtLBvi.js +21 -0
- package/dist/tauri.d.ts +19 -0
- package/dist/tauri.js +28 -0
- package/package.json +56 -4
- package/README.md +0 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { n as MESSAGE_PORT_PROTOCOL_VERSION, t as DEFAULT_MESSAGE_PORT_CHANNEL } from "./protocol-DrDtLBvi.js";
|
|
2
|
+
|
|
3
|
+
//#region src/desktop-wire.ts
|
|
4
|
+
function base64(bytes) {
|
|
5
|
+
let binary = "";
|
|
6
|
+
for (let offset = 0; offset < bytes.length; offset += 32768) binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
7
|
+
return btoa(binary);
|
|
8
|
+
}
|
|
9
|
+
function bytes(value) {
|
|
10
|
+
if (typeof value !== "string") throw new TypeError("Invalid Desktop IPC binary value");
|
|
11
|
+
return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
|
|
12
|
+
}
|
|
13
|
+
async function encodeDesktopMessage(value) {
|
|
14
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
15
|
+
async function encode(value) {
|
|
16
|
+
if (value === void 0) return ["undefined"];
|
|
17
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
18
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
19
|
+
if (typeof value !== "object" || value === null) throw new TypeError("Desktop IPC values must be JSON-compatible, bytes or FormData");
|
|
20
|
+
if (seen.has(value)) throw new TypeError("Desktop IPC values must not contain cycles");
|
|
21
|
+
seen.add(value);
|
|
22
|
+
try {
|
|
23
|
+
if (value instanceof Uint8Array) return ["bytes", base64(value)];
|
|
24
|
+
if (value instanceof FormData) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const [name, field] of value) entries.push([name, typeof field === "string" ? field : [
|
|
27
|
+
"file",
|
|
28
|
+
base64(new Uint8Array(await field.arrayBuffer())),
|
|
29
|
+
field.name,
|
|
30
|
+
field.type,
|
|
31
|
+
field.lastModified
|
|
32
|
+
]]);
|
|
33
|
+
return ["form-data", entries];
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const field of value) entries.push(await encode(field));
|
|
38
|
+
return ["array", entries];
|
|
39
|
+
}
|
|
40
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("Desktop IPC objects must be plain records; encode native values with a contract codec");
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const [name, field] of Object.entries(value)) entries.push([name, await encode(field)]);
|
|
43
|
+
return ["object", entries];
|
|
44
|
+
} finally {
|
|
45
|
+
seen.delete(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(await encode(value));
|
|
49
|
+
}
|
|
50
|
+
function decodeDesktopMessage(text) {
|
|
51
|
+
function decode(value) {
|
|
52
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
53
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
54
|
+
if (!Array.isArray(value)) throw new TypeError("Invalid Desktop IPC wire value");
|
|
55
|
+
if (value.length === 1 && value[0] === "undefined") return void 0;
|
|
56
|
+
if (value.length !== 2) throw new TypeError("Invalid Desktop IPC wire container");
|
|
57
|
+
if (value[0] === "bytes") return bytes(value[1]);
|
|
58
|
+
const entries = value[1];
|
|
59
|
+
if (!Array.isArray(entries)) throw new TypeError("Invalid Desktop IPC wire entries");
|
|
60
|
+
if (value[0] === "array") return entries.map(decode);
|
|
61
|
+
if (value[0] === "object" || value[0] === "form-data") {
|
|
62
|
+
const record = {};
|
|
63
|
+
const form = value[0] === "form-data" ? new FormData() : void 0;
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") throw new TypeError("Invalid Desktop IPC wire entry");
|
|
66
|
+
const [name, field] = entry;
|
|
67
|
+
if (form === void 0) {
|
|
68
|
+
if (Object.hasOwn(record, name)) throw new TypeError("Duplicate Desktop IPC object key");
|
|
69
|
+
Object.defineProperty(record, name, {
|
|
70
|
+
value: decode(field),
|
|
71
|
+
enumerable: true,
|
|
72
|
+
writable: true,
|
|
73
|
+
configurable: true
|
|
74
|
+
});
|
|
75
|
+
} else if (typeof field === "string") form.append(name, field);
|
|
76
|
+
else {
|
|
77
|
+
if (!Array.isArray(field) || field.length !== 5 || field[0] !== "file" || typeof field[2] !== "string" || typeof field[3] !== "string" || typeof field[4] !== "number") throw new TypeError("Invalid Desktop IPC multipart file");
|
|
78
|
+
form.append(name, new File([bytes(field[1])], field[2], {
|
|
79
|
+
type: field[3],
|
|
80
|
+
lastModified: field[4]
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return form ?? record;
|
|
85
|
+
}
|
|
86
|
+
throw new TypeError("Unknown Desktop IPC wire container");
|
|
87
|
+
}
|
|
88
|
+
return decode(JSON.parse(text));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/desktop.ts
|
|
93
|
+
/** JSON-safe bridge with ordered sends, async listener readiness, and explicit host lifetime. */
|
|
94
|
+
function desktopEndpoint(bridge, options) {
|
|
95
|
+
let tail = Promise.resolve();
|
|
96
|
+
let subscribed = false;
|
|
97
|
+
let closed = false;
|
|
98
|
+
return {
|
|
99
|
+
send: (message) => {
|
|
100
|
+
const task = tail.then(async () => {
|
|
101
|
+
if (closed || options.signal.aborted) throw new Error("Desktop IPC endpoint closed");
|
|
102
|
+
await bridge.send(await encodeDesktopMessage(message));
|
|
103
|
+
});
|
|
104
|
+
tail = task.catch(() => {});
|
|
105
|
+
return task;
|
|
106
|
+
},
|
|
107
|
+
subscribe: async (listener) => {
|
|
108
|
+
if (subscribed) throw new Error("Desktop IPC endpoint supports one active session");
|
|
109
|
+
subscribed = true;
|
|
110
|
+
let active = true;
|
|
111
|
+
let unsubscribe;
|
|
112
|
+
const cleanup = async () => {
|
|
113
|
+
active = false;
|
|
114
|
+
closed = true;
|
|
115
|
+
options.signal.removeEventListener("abort", abort);
|
|
116
|
+
const release = unsubscribe;
|
|
117
|
+
unsubscribe = void 0;
|
|
118
|
+
await release?.();
|
|
119
|
+
};
|
|
120
|
+
const abort = () => {
|
|
121
|
+
if (!active) return;
|
|
122
|
+
listener({
|
|
123
|
+
protocol: 1,
|
|
124
|
+
channel: options.channel ?? "@hulla/api",
|
|
125
|
+
type: "close",
|
|
126
|
+
id: "desktop-close"
|
|
127
|
+
});
|
|
128
|
+
cleanup().catch(() => {});
|
|
129
|
+
};
|
|
130
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
131
|
+
try {
|
|
132
|
+
if (options.signal.aborted) {
|
|
133
|
+
abort();
|
|
134
|
+
return cleanup;
|
|
135
|
+
}
|
|
136
|
+
unsubscribe = await bridge.subscribe((message) => {
|
|
137
|
+
if (!active) return;
|
|
138
|
+
try {
|
|
139
|
+
listener(decodeDesktopMessage(message));
|
|
140
|
+
} catch {
|
|
141
|
+
abort();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
if (!active) await cleanup();
|
|
145
|
+
return cleanup;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await cleanup();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { decodeDesktopMessage as n, encodeDesktopMessage as r, desktopEndpoint as t };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { r as MessageEndpointSubscription, t as MessageEndpoint } from "./endpoint-CPsj0RmN.js";
|
|
2
|
+
//#region src/desktop-wire.d.ts
|
|
3
|
+
declare function encodeDesktopMessage(value: unknown): Promise<string>;
|
|
4
|
+
declare function decodeDesktopMessage(text: string): unknown;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/desktop.d.ts
|
|
7
|
+
/** Host relays carry opaque strings without interpreting or reserializing their contents. */
|
|
8
|
+
type DesktopBridge = {
|
|
9
|
+
readonly send: (message: string) => void | PromiseLike<void>;
|
|
10
|
+
readonly subscribe: (receive: (message: string) => void) => MessageEndpointSubscription | PromiseLike<MessageEndpointSubscription>;
|
|
11
|
+
};
|
|
12
|
+
type DesktopEndpointOptions = {
|
|
13
|
+
/** Abort on window destruction, native channel closure, or host shutdown. */
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
/** Must match the transport/adapter channel when overriding its default. */
|
|
16
|
+
readonly channel?: string;
|
|
17
|
+
};
|
|
18
|
+
/** JSON-safe bridge with ordered sends, async listener readiness, and explicit host lifetime. */
|
|
19
|
+
declare function desktopEndpoint(bridge: DesktopBridge, options: DesktopEndpointOptions): MessageEndpoint;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { encodeDesktopMessage as a, decodeDesktopMessage as i, DesktopEndpointOptions as n, desktopEndpoint as r, DesktopBridge as t };
|
package/dist/desktop.js
ADDED
package/dist/dioxus.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { t as MessageEndpoint } from "./endpoint-CPsj0RmN.js";
|
|
2
|
+
import { n as DesktopEndpointOptions } from "./desktop-q9GwZShg.js";
|
|
3
|
+
//#region src/dioxus.d.ts
|
|
4
|
+
/** The dioxus object injected into a document.eval script. One eval per API session. */
|
|
5
|
+
type DioxusEval = {
|
|
6
|
+
readonly send: (message: string) => void | PromiseLike<void>;
|
|
7
|
+
readonly recv: () => PromiseLike<unknown>;
|
|
8
|
+
};
|
|
9
|
+
declare function dioxusEndpoint(evalChannel: DioxusEval, options: DesktopEndpointOptions): MessageEndpoint;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { DioxusEval, dioxusEndpoint };
|
package/dist/dioxus.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { t as desktopEndpoint } from "./desktop-BojxzZgV.js";
|
|
2
|
+
|
|
3
|
+
//#region src/dioxus.ts
|
|
4
|
+
function dioxusEndpoint(evalChannel, options) {
|
|
5
|
+
const lifetime = new AbortController();
|
|
6
|
+
const abort = () => lifetime.abort(options.signal.reason);
|
|
7
|
+
return desktopEndpoint({
|
|
8
|
+
send: (message) => evalChannel.send(message),
|
|
9
|
+
subscribe: (receive) => {
|
|
10
|
+
let active = true;
|
|
11
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
12
|
+
if (options.signal.aborted) abort();
|
|
13
|
+
(async () => {
|
|
14
|
+
try {
|
|
15
|
+
while (active && !lifetime.signal.aborted) {
|
|
16
|
+
const message = await evalChannel.recv();
|
|
17
|
+
if (!active || lifetime.signal.aborted) break;
|
|
18
|
+
if (typeof message !== "string") throw new TypeError("Dioxus relay must send strings");
|
|
19
|
+
receive(message);
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
lifetime.abort(/* @__PURE__ */ new Error("Dioxus eval channel closed"));
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
options.signal.removeEventListener("abort", abort);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}, {
|
|
31
|
+
...options,
|
|
32
|
+
signal: lifetime.signal
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { dioxusEndpoint };
|
package/dist/electron.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ResponseHeaderValues } from "@hulla/api";
|
|
2
|
+
//#region src/protocol.d.ts
|
|
3
|
+
declare const MESSAGE_PORT_PROTOCOL_VERSION: 1;
|
|
4
|
+
declare const DEFAULT_MESSAGE_PORT_CHANNEL: '@hulla/api';
|
|
5
|
+
type MessagePortFormDataFile = {
|
|
6
|
+
readonly kind: 'file';
|
|
7
|
+
readonly bytes: Uint8Array;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly lastModified: number;
|
|
11
|
+
};
|
|
12
|
+
type MessagePortFormDataEntry = readonly [name: string, value: string | MessagePortFormDataFile];
|
|
13
|
+
type MessagePortBody = {
|
|
14
|
+
readonly kind: 'bytes' | 'json' | 'raw' | 'text';
|
|
15
|
+
readonly value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'empty';
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'form-data';
|
|
20
|
+
readonly value: readonly MessagePortFormDataEntry[];
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'stream';
|
|
23
|
+
};
|
|
24
|
+
type MessagePortRequestBody = {
|
|
25
|
+
readonly kind: 'bytes' | 'json' | 'text';
|
|
26
|
+
readonly value: unknown;
|
|
27
|
+
readonly contentType: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'form-data';
|
|
30
|
+
readonly value: readonly MessagePortFormDataEntry[];
|
|
31
|
+
readonly contentType: string;
|
|
32
|
+
};
|
|
33
|
+
type MessagePortRequest = {
|
|
34
|
+
readonly key: readonly string[];
|
|
35
|
+
readonly method: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly query?: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
38
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
39
|
+
readonly body?: MessagePortRequestBody;
|
|
40
|
+
};
|
|
41
|
+
type MessagePortEnvelope = {
|
|
42
|
+
readonly channel: string;
|
|
43
|
+
readonly protocol: typeof MESSAGE_PORT_PROTOCOL_VERSION;
|
|
44
|
+
readonly id: string;
|
|
45
|
+
};
|
|
46
|
+
type MessagePortRequestMessage = MessagePortEnvelope & {
|
|
47
|
+
readonly type: 'request';
|
|
48
|
+
readonly request: MessagePortRequest;
|
|
49
|
+
};
|
|
50
|
+
type MessagePortResponseMessage = MessagePortEnvelope & {
|
|
51
|
+
readonly type: 'response';
|
|
52
|
+
readonly response: {
|
|
53
|
+
readonly status: number;
|
|
54
|
+
readonly headers: ResponseHeaderValues;
|
|
55
|
+
readonly body: MessagePortBody;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
type MessagePortCancelMessage = MessagePortEnvelope & {
|
|
59
|
+
readonly type: 'cancel';
|
|
60
|
+
};
|
|
61
|
+
type MessagePortStreamPullMessage = MessagePortEnvelope & {
|
|
62
|
+
readonly type: 'stream-pull';
|
|
63
|
+
};
|
|
64
|
+
type MessagePortStreamChunkMessage = MessagePortEnvelope & {
|
|
65
|
+
readonly type: 'stream-chunk';
|
|
66
|
+
readonly chunk: Uint8Array;
|
|
67
|
+
};
|
|
68
|
+
type MessagePortStreamEndMessage = MessagePortEnvelope & {
|
|
69
|
+
readonly type: 'stream-end';
|
|
70
|
+
};
|
|
71
|
+
type MessagePortErrorMessage = MessagePortEnvelope & {
|
|
72
|
+
readonly type: 'error' | 'stream-error';
|
|
73
|
+
readonly error: {
|
|
74
|
+
readonly message: string;
|
|
75
|
+
readonly name?: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
type MessagePortCloseMessage = Omit<MessagePortEnvelope, 'id'> & {
|
|
79
|
+
readonly type: 'close';
|
|
80
|
+
};
|
|
81
|
+
type MessagePortMessage = MessagePortRequestMessage | MessagePortResponseMessage | MessagePortCancelMessage | MessagePortStreamPullMessage | MessagePortStreamChunkMessage | MessagePortStreamEndMessage | MessagePortErrorMessage | MessagePortCloseMessage;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/endpoint.d.ts
|
|
84
|
+
type MessageEndpointSubscription = () => void | PromiseLike<void>;
|
|
85
|
+
type MessageEndpointListener = (message: unknown) => void;
|
|
86
|
+
/** An ordered, bidirectional message endpoint suitable for custom IPC bridges. */
|
|
87
|
+
type MessageEndpoint = {
|
|
88
|
+
readonly send: (message: MessagePortMessage) => void | PromiseLike<void>;
|
|
89
|
+
readonly subscribe: (listener: MessageEndpointListener) => MessageEndpointSubscription | PromiseLike<MessageEndpointSubscription>;
|
|
90
|
+
};
|
|
91
|
+
type MessageEventLike = {
|
|
92
|
+
readonly data: unknown;
|
|
93
|
+
};
|
|
94
|
+
type MessagePortListener = (event: MessageEventLike) => void;
|
|
95
|
+
/** The common surface shared by DOM MessagePort, worker_threads MessagePort, and Electron MessagePortMain. */
|
|
96
|
+
type MessagePortLike = {
|
|
97
|
+
postMessage(message: unknown): void;
|
|
98
|
+
start?(): void;
|
|
99
|
+
addEventListener?(type: 'message', listener: MessagePortListener): void;
|
|
100
|
+
removeEventListener?(type: 'message', listener: MessagePortListener): void;
|
|
101
|
+
on?(type: 'message', listener: MessagePortListener): unknown;
|
|
102
|
+
off?(type: 'message', listener: MessagePortListener): unknown;
|
|
103
|
+
removeListener?(type: 'message', listener: MessagePortListener): unknown;
|
|
104
|
+
};
|
|
105
|
+
/** Normalizes browser, worker, Node, and Electron message ports to the async endpoint API. */
|
|
106
|
+
declare function messagePortEndpoint(port: MessagePortLike): MessageEndpoint;
|
|
107
|
+
//#endregion
|
|
108
|
+
export { MessagePortRequestMessage as _, messagePortEndpoint as a, MessagePortStreamEndMessage as b, MessagePortBody as c, MessagePortErrorMessage as d, MessagePortFormDataEntry as f, MessagePortRequestBody as g, MessagePortRequest as h, MessagePortLike as i, MessagePortCancelMessage as l, MessagePortMessage as m, MessageEndpointListener as n, DEFAULT_MESSAGE_PORT_CHANNEL as o, MessagePortFormDataFile as p, MessageEndpointSubscription as r, MESSAGE_PORT_PROTOCOL_VERSION as s, MessageEndpoint as t, MessagePortCloseMessage as u, MessagePortResponseMessage as v, MessagePortStreamPullMessage as x, MessagePortStreamChunkMessage as y };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/endpoint.ts
|
|
2
|
+
function eventData(event) {
|
|
3
|
+
return typeof event === "object" && event !== null && "data" in event ? event.data : event;
|
|
4
|
+
}
|
|
5
|
+
/** Normalizes browser, worker, Node, and Electron message ports to the async endpoint API. */
|
|
6
|
+
function messagePortEndpoint(port) {
|
|
7
|
+
if (typeof port.postMessage !== "function") throw new TypeError("Message port must provide postMessage()");
|
|
8
|
+
return {
|
|
9
|
+
send: (message) => port.postMessage(message),
|
|
10
|
+
subscribe: (listener) => {
|
|
11
|
+
const receive = (event) => listener(eventData(event));
|
|
12
|
+
if (typeof port.addEventListener === "function" && typeof port.removeEventListener === "function") {
|
|
13
|
+
port.addEventListener("message", receive);
|
|
14
|
+
port.start?.();
|
|
15
|
+
return () => port.removeEventListener?.("message", receive);
|
|
16
|
+
}
|
|
17
|
+
if (typeof port.on === "function" && (typeof port.off === "function" || typeof port.removeListener === "function")) {
|
|
18
|
+
port.on("message", receive);
|
|
19
|
+
port.start?.();
|
|
20
|
+
return () => {
|
|
21
|
+
if (typeof port.off === "function") port.off("message", receive);
|
|
22
|
+
else port.removeListener?.("message", receive);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
throw new TypeError("Message port must provide EventTarget or EventEmitter message listeners");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveMessageEndpoint(endpoint) {
|
|
30
|
+
return "send" in endpoint && "subscribe" in endpoint ? endpoint : messagePortEndpoint(endpoint);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { resolveMessageEndpoint as n, messagePortEndpoint as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { _ as MessagePortRequestMessage, a as messagePortEndpoint, b as MessagePortStreamEndMessage, c as MessagePortBody, d as MessagePortErrorMessage, f as MessagePortFormDataEntry, g as MessagePortRequestBody, h as MessagePortRequest, i as MessagePortLike, l as MessagePortCancelMessage, m as MessagePortMessage, n as MessageEndpointListener, o as DEFAULT_MESSAGE_PORT_CHANNEL, p as MessagePortFormDataFile, r as MessageEndpointSubscription, s as MESSAGE_PORT_PROTOCOL_VERSION, t as MessageEndpoint, u as MessagePortCloseMessage, v as MessagePortResponseMessage, x as MessagePortStreamPullMessage, y as MessagePortStreamChunkMessage } from "./endpoint-CPsj0RmN.js";
|
|
2
|
+
import { AdapterErrorInput, AdapterResponse } from "@hulla/api/adapters";
|
|
3
|
+
import { Awaitable, ServerAdapter, ServerContextInput, ServerExecutableFor } from "@hulla/api/server";
|
|
4
|
+
import { Contract } from "@hulla/api";
|
|
5
|
+
import { ClientTransport, ClientTransportRequest } from "@hulla/api/client";
|
|
6
|
+
//#region src/client.d.ts
|
|
7
|
+
type MessagePortTransportOptions = {
|
|
8
|
+
readonly channel?: string;
|
|
9
|
+
};
|
|
10
|
+
type MessagePortTransport = ClientTransport & {
|
|
11
|
+
/** Resolves after the endpoint listener has been installed. */
|
|
12
|
+
readonly ready: Promise<void>;
|
|
13
|
+
/** Stops listening and rejects all unfinished calls. The underlying endpoint remains open. */
|
|
14
|
+
readonly close: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
type MessagePortTransportErrorCode = 'closed' | 'invalid-message' | 'remote-error';
|
|
17
|
+
declare class MessagePortTransportError extends Error {
|
|
18
|
+
readonly code: MessagePortTransportErrorCode;
|
|
19
|
+
constructor(code: MessagePortTransportErrorCode, message: string, options?: ErrorOptions);
|
|
20
|
+
}
|
|
21
|
+
/** Creates a concurrent request transport over a MessagePort or custom ordered IPC endpoint. */
|
|
22
|
+
declare function messagePortTransport(port: MessagePortLike | MessageEndpoint, options?: MessagePortTransportOptions): MessagePortTransport;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/server.d.ts
|
|
25
|
+
type MessagePortServerRequest = Omit<ClientTransportRequest, 'signal'> & {
|
|
26
|
+
readonly signal: AbortSignal;
|
|
27
|
+
};
|
|
28
|
+
type MessagePortAdapterContext = {
|
|
29
|
+
readonly endpoint: MessageEndpoint;
|
|
30
|
+
readonly message: MessagePortRequestMessage;
|
|
31
|
+
readonly port: MessagePortLike | MessageEndpoint;
|
|
32
|
+
readonly request: MessagePortServerRequest;
|
|
33
|
+
};
|
|
34
|
+
type MessagePortContextInput<ContractType extends Contract = Contract> = ServerContextInput<ContractType> & MessagePortAdapterContext;
|
|
35
|
+
type MessagePortServerErrorInput = Omit<AdapterErrorInput, 'request'> & {
|
|
36
|
+
readonly request: MessagePortServerRequest;
|
|
37
|
+
};
|
|
38
|
+
type MessagePortAdapterOptions = {
|
|
39
|
+
readonly channel?: string;
|
|
40
|
+
};
|
|
41
|
+
type MessagePortMountOptions = {
|
|
42
|
+
readonly onError?: ((input: MessagePortServerErrorInput) => Awaitable<AdapterResponse | undefined | void>) | undefined;
|
|
43
|
+
};
|
|
44
|
+
type MessagePortServer = {
|
|
45
|
+
/** Resolves after the endpoint listener has been installed. */
|
|
46
|
+
readonly ready: Promise<void>;
|
|
47
|
+
/** Stops listening and cancels active handlers and streams. The underlying endpoint remains open. */
|
|
48
|
+
readonly close: () => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
type MessagePortAdapter = ServerAdapter<'message-port', MessagePortAdapterContext> & {
|
|
51
|
+
readonly mount: <const ContractType extends Contract, const Context extends object>(implementation: ServerExecutableFor<ContractType, Context, 'message-port', MessagePortAdapterContext>, options?: MessagePortMountOptions) => MessagePortServer;
|
|
52
|
+
};
|
|
53
|
+
/** Creates a server adapter over a MessagePort or custom ordered IPC endpoint. */
|
|
54
|
+
declare function messagePortAdapter(port: MessagePortLike | MessageEndpoint, options?: MessagePortAdapterOptions): MessagePortAdapter;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { DEFAULT_MESSAGE_PORT_CHANNEL, MESSAGE_PORT_PROTOCOL_VERSION, type MessageEndpoint, type MessageEndpointListener, type MessageEndpointSubscription, type MessagePortAdapter, type MessagePortAdapterOptions, type MessagePortBody, type MessagePortCancelMessage, type MessagePortCloseMessage, type MessagePortContextInput, type MessagePortErrorMessage, type MessagePortFormDataEntry, type MessagePortFormDataFile, type MessagePortLike, type MessagePortMessage, type MessagePortMountOptions, type MessagePortRequest, type MessagePortRequestBody, type MessagePortRequestMessage, type MessagePortResponseMessage, type MessagePortServer, type MessagePortServerErrorInput, type MessagePortServerRequest, type MessagePortStreamChunkMessage, type MessagePortStreamEndMessage, type MessagePortStreamPullMessage, type MessagePortTransport, MessagePortTransportError, type MessagePortTransportErrorCode, type MessagePortTransportOptions, messagePortAdapter, messagePortEndpoint, messagePortTransport };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { n as resolveMessageEndpoint, t as messagePortEndpoint } from "./endpoint-y_ZFW2WR.js";
|
|
2
|
+
import { a as isRecord, i as messagePortError, n as MESSAGE_PORT_PROTOCOL_VERSION, r as isMessagePortEnvelope, t as DEFAULT_MESSAGE_PORT_CHANNEL } from "./protocol-DrDtLBvi.js";
|
|
3
|
+
import { createAdapterHandler } from "@hulla/api/adapters";
|
|
4
|
+
import { assertAdapterContext, createServerAdapter, serverContextAdapterId } from "@hulla/api/server";
|
|
5
|
+
|
|
6
|
+
//#region src/body.ts
|
|
7
|
+
function isFile(value) {
|
|
8
|
+
return typeof File !== "undefined" && value instanceof File;
|
|
9
|
+
}
|
|
10
|
+
async function encodeFormData(value) {
|
|
11
|
+
if (!(value instanceof FormData)) throw new TypeError("Message-port form data must be FormData");
|
|
12
|
+
return Promise.all([...value.entries()].map(async ([name, entry]) => {
|
|
13
|
+
if (typeof entry === "string") return [name, entry];
|
|
14
|
+
return [name, {
|
|
15
|
+
kind: "file",
|
|
16
|
+
bytes: new Uint8Array(await entry.arrayBuffer()),
|
|
17
|
+
name: isFile(entry) ? entry.name : "blob",
|
|
18
|
+
type: entry.type,
|
|
19
|
+
lastModified: isFile(entry) ? entry.lastModified : 0
|
|
20
|
+
}];
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function decodeMessagePortFormData(entries) {
|
|
24
|
+
const formData = new FormData();
|
|
25
|
+
for (const [name, value] of entries) {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
formData.append(name, value);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const bytes = value.bytes.slice().buffer;
|
|
31
|
+
const file = typeof File === "undefined" ? new Blob([bytes], { type: value.type }) : new File([bytes], value.name, {
|
|
32
|
+
type: value.type,
|
|
33
|
+
lastModified: value.lastModified
|
|
34
|
+
});
|
|
35
|
+
formData.append(name, file, value.name);
|
|
36
|
+
}
|
|
37
|
+
return formData;
|
|
38
|
+
}
|
|
39
|
+
async function encodeRequestBody(body) {
|
|
40
|
+
if (body.kind === "form-data") return {
|
|
41
|
+
kind: "form-data",
|
|
42
|
+
contentType: body.contentType,
|
|
43
|
+
value: await encodeFormData(body.value)
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
kind: body.kind,
|
|
47
|
+
contentType: body.contentType,
|
|
48
|
+
value: body.value
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function encodeResponseBody(body) {
|
|
52
|
+
switch (body.kind) {
|
|
53
|
+
case "empty": return { kind: "empty" };
|
|
54
|
+
case "stream": return { kind: "stream" };
|
|
55
|
+
case "form-data": return {
|
|
56
|
+
kind: "form-data",
|
|
57
|
+
value: await encodeFormData(body.value)
|
|
58
|
+
};
|
|
59
|
+
case "bytes":
|
|
60
|
+
case "json":
|
|
61
|
+
case "raw":
|
|
62
|
+
case "text": return {
|
|
63
|
+
kind: body.kind,
|
|
64
|
+
value: body.value
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client.ts
|
|
71
|
+
var MessagePortTransportError = class extends Error {
|
|
72
|
+
code;
|
|
73
|
+
constructor(code, message, options) {
|
|
74
|
+
super(message, options);
|
|
75
|
+
this.name = "MessagePortTransportError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function channelName$1(options) {
|
|
80
|
+
const channel = options.channel ?? "@hulla/api";
|
|
81
|
+
if (channel.length === 0) throw new TypeError("Message-port channel must not be empty");
|
|
82
|
+
return channel;
|
|
83
|
+
}
|
|
84
|
+
function requestIdFactory() {
|
|
85
|
+
const prefix = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
86
|
+
let sequence = 0;
|
|
87
|
+
return () => `${prefix}:${++sequence}`;
|
|
88
|
+
}
|
|
89
|
+
function remoteError(value, fallback) {
|
|
90
|
+
const error = isRecord(value) ? value : void 0;
|
|
91
|
+
const message = typeof error?.["message"] === "string" ? error["message"] : fallback;
|
|
92
|
+
const name = typeof error?.["name"] === "string" ? error["name"] : void 0;
|
|
93
|
+
return new MessagePortTransportError("remote-error", name === void 0 ? message : `${name}: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
function stringHeaders(value) {
|
|
96
|
+
return isRecord(value) && Object.values(value).every((field) => typeof field === "string" || Array.isArray(field) && field.every((item) => typeof item === "string"));
|
|
97
|
+
}
|
|
98
|
+
function responseBody(value) {
|
|
99
|
+
if (!isRecord(value) || typeof value["kind"] !== "string") return void 0;
|
|
100
|
+
switch (value["kind"]) {
|
|
101
|
+
case "empty":
|
|
102
|
+
case "stream": return { kind: value["kind"] };
|
|
103
|
+
case "bytes": return value["value"] instanceof Uint8Array ? {
|
|
104
|
+
kind: "bytes",
|
|
105
|
+
value: value["value"]
|
|
106
|
+
} : void 0;
|
|
107
|
+
case "text": return typeof value["value"] === "string" ? {
|
|
108
|
+
kind: "text",
|
|
109
|
+
value: value["value"]
|
|
110
|
+
} : void 0;
|
|
111
|
+
case "form-data": return Array.isArray(value["value"]) ? value : void 0;
|
|
112
|
+
case "json":
|
|
113
|
+
case "raw": return {
|
|
114
|
+
kind: value["kind"],
|
|
115
|
+
value: value["value"]
|
|
116
|
+
};
|
|
117
|
+
default: return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var MessagePortRemoteStream = class {
|
|
121
|
+
#channel;
|
|
122
|
+
#endpoint;
|
|
123
|
+
#id;
|
|
124
|
+
#onFinish;
|
|
125
|
+
#waiters = [];
|
|
126
|
+
#finished = false;
|
|
127
|
+
#failed = false;
|
|
128
|
+
#error;
|
|
129
|
+
constructor(endpoint, channel, id, onFinish) {
|
|
130
|
+
this.#endpoint = endpoint;
|
|
131
|
+
this.#channel = channel;
|
|
132
|
+
this.#id = id;
|
|
133
|
+
this.#onFinish = onFinish;
|
|
134
|
+
}
|
|
135
|
+
next() {
|
|
136
|
+
if (this.#failed) return Promise.reject(this.#error);
|
|
137
|
+
if (this.#finished) return Promise.resolve({
|
|
138
|
+
done: true,
|
|
139
|
+
value: void 0
|
|
140
|
+
});
|
|
141
|
+
const result = new Promise((resolve, reject) => {
|
|
142
|
+
this.#waiters.push({
|
|
143
|
+
resolve,
|
|
144
|
+
reject
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
Promise.resolve(this.#endpoint.send({
|
|
149
|
+
protocol: 1,
|
|
150
|
+
channel: this.#channel,
|
|
151
|
+
type: "stream-pull",
|
|
152
|
+
id: this.#id
|
|
153
|
+
})).catch((error) => this.fail(error));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.fail(error);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
return() {
|
|
160
|
+
if (!this.#finished) {
|
|
161
|
+
this.#sendDetached({
|
|
162
|
+
protocol: 1,
|
|
163
|
+
channel: this.#channel,
|
|
164
|
+
type: "cancel",
|
|
165
|
+
id: this.#id
|
|
166
|
+
});
|
|
167
|
+
this.end();
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve({
|
|
170
|
+
done: true,
|
|
171
|
+
value: void 0
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
abort(error) {
|
|
175
|
+
if (this.#finished) return;
|
|
176
|
+
this.#sendDetached({
|
|
177
|
+
protocol: 1,
|
|
178
|
+
channel: this.#channel,
|
|
179
|
+
type: "cancel",
|
|
180
|
+
id: this.#id
|
|
181
|
+
});
|
|
182
|
+
this.fail(error);
|
|
183
|
+
}
|
|
184
|
+
[Symbol.asyncIterator]() {
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
#sendDetached(message) {
|
|
188
|
+
try {
|
|
189
|
+
Promise.resolve(this.#endpoint.send(message)).catch(() => {});
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
chunk(value) {
|
|
193
|
+
if (this.#finished) return;
|
|
194
|
+
if (!(value instanceof Uint8Array)) {
|
|
195
|
+
this.abort(new MessagePortTransportError("invalid-message", "Received a non-binary stream chunk"));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const waiter = this.#waiters.shift();
|
|
199
|
+
if (waiter === void 0) {
|
|
200
|
+
this.abort(new MessagePortTransportError("invalid-message", "Received an unrequested stream chunk"));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
waiter.resolve({
|
|
204
|
+
done: false,
|
|
205
|
+
value
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
end() {
|
|
209
|
+
if (this.#finished) return;
|
|
210
|
+
this.#finished = true;
|
|
211
|
+
for (const waiter of this.#waiters.splice(0)) waiter.resolve({
|
|
212
|
+
done: true,
|
|
213
|
+
value: void 0
|
|
214
|
+
});
|
|
215
|
+
this.#onFinish();
|
|
216
|
+
}
|
|
217
|
+
fail(error) {
|
|
218
|
+
if (this.#finished) return;
|
|
219
|
+
this.#failed = true;
|
|
220
|
+
this.#error = error;
|
|
221
|
+
this.#finished = true;
|
|
222
|
+
for (const waiter of this.#waiters.splice(0)) waiter.reject(error);
|
|
223
|
+
this.#onFinish();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/** Creates a concurrent request transport over a MessagePort or custom ordered IPC endpoint. */
|
|
227
|
+
function messagePortTransport(port, options = {}) {
|
|
228
|
+
const endpoint = resolveMessageEndpoint(port);
|
|
229
|
+
const channel = channelName$1(options);
|
|
230
|
+
const nextRequestId = requestIdFactory();
|
|
231
|
+
const pending = /* @__PURE__ */ new Map();
|
|
232
|
+
const streams = /* @__PURE__ */ new Map();
|
|
233
|
+
let closed = false;
|
|
234
|
+
let unsubscribe;
|
|
235
|
+
const sendDetached = (message) => {
|
|
236
|
+
try {
|
|
237
|
+
Promise.resolve(endpoint.send(message)).catch(() => {});
|
|
238
|
+
} catch {}
|
|
239
|
+
};
|
|
240
|
+
const clearPending = (id, request) => {
|
|
241
|
+
pending.delete(id);
|
|
242
|
+
if (request.signal !== void 0 && request.onAbort !== void 0) request.signal.removeEventListener("abort", request.onAbort);
|
|
243
|
+
};
|
|
244
|
+
const failAll = (error, notifyRemote) => {
|
|
245
|
+
for (const [id, request] of [...pending]) if (notifyRemote) request.cancel(error);
|
|
246
|
+
else {
|
|
247
|
+
clearPending(id, request);
|
|
248
|
+
request.reject(error);
|
|
249
|
+
}
|
|
250
|
+
for (const stream of [...streams.values()]) if (notifyRemote) stream.abort(error);
|
|
251
|
+
else stream.fail(error);
|
|
252
|
+
streams.clear();
|
|
253
|
+
};
|
|
254
|
+
const receive = (value) => {
|
|
255
|
+
if (!isMessagePortEnvelope(value, channel)) return;
|
|
256
|
+
if (value.type === "close") {
|
|
257
|
+
closed = true;
|
|
258
|
+
failAll(new MessagePortTransportError("closed", "The remote message-port server closed"), false);
|
|
259
|
+
const cleanup = unsubscribe;
|
|
260
|
+
unsubscribe = void 0;
|
|
261
|
+
if (cleanup !== void 0) Promise.resolve(cleanup()).catch(() => {});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (typeof value.id !== "string") return;
|
|
265
|
+
const stream = streams.get(value.id);
|
|
266
|
+
if (stream !== void 0) {
|
|
267
|
+
if (value.type === "stream-chunk") stream.chunk(value.chunk);
|
|
268
|
+
else if (value.type === "stream-end") stream.end();
|
|
269
|
+
else if (value.type === "stream-error") stream.fail(remoteError(value.error, "Remote stream failed"));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const request = pending.get(value.id);
|
|
273
|
+
if (request === void 0) return;
|
|
274
|
+
if (value.type === "error") {
|
|
275
|
+
clearPending(value.id, request);
|
|
276
|
+
request.reject(remoteError(value.error, "Remote request failed"));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (value.type !== "response") return;
|
|
280
|
+
const responseValue = value.response;
|
|
281
|
+
const body = isRecord(responseValue) ? responseBody(responseValue["body"]) : void 0;
|
|
282
|
+
if (!isRecord(responseValue) || typeof responseValue["status"] !== "number" || !stringHeaders(responseValue["headers"]) || body === void 0) {
|
|
283
|
+
clearPending(value.id, request);
|
|
284
|
+
request.reject(new MessagePortTransportError("invalid-message", "Received an invalid response message"));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
pending.delete(value.id);
|
|
288
|
+
let streamAbort;
|
|
289
|
+
const finish = () => {
|
|
290
|
+
streams.delete(value.id);
|
|
291
|
+
const abortListener = streamAbort ?? request.onAbort;
|
|
292
|
+
if (request.signal !== void 0 && abortListener !== void 0) request.signal.removeEventListener("abort", abortListener);
|
|
293
|
+
};
|
|
294
|
+
const remoteStream = body.kind === "stream" ? new MessagePortRemoteStream(endpoint, channel, value.id, finish) : void 0;
|
|
295
|
+
if (remoteStream === void 0) finish();
|
|
296
|
+
else {
|
|
297
|
+
streams.set(value.id, remoteStream);
|
|
298
|
+
if (request.signal !== void 0) {
|
|
299
|
+
if (request.onAbort !== void 0) request.signal.removeEventListener("abort", request.onAbort);
|
|
300
|
+
streamAbort = () => remoteStream.abort(request.signal?.reason);
|
|
301
|
+
request.signal.addEventListener("abort", streamAbort, { once: true });
|
|
302
|
+
if (request.signal.aborted) streamAbort();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const response = {
|
|
306
|
+
status: responseValue["status"],
|
|
307
|
+
headers: responseValue["headers"],
|
|
308
|
+
native: value,
|
|
309
|
+
dispose: async () => {
|
|
310
|
+
await remoteStream?.return();
|
|
311
|
+
},
|
|
312
|
+
readBody: (kind) => {
|
|
313
|
+
if (body.kind !== kind) {
|
|
314
|
+
remoteStream?.return();
|
|
315
|
+
throw new TypeError(`Message-port response body is ${body.kind}, but the client selected ${kind}`);
|
|
316
|
+
}
|
|
317
|
+
if (body.kind === "stream") return remoteStream;
|
|
318
|
+
if (body.kind === "form-data") return decodeMessagePortFormData(body.value);
|
|
319
|
+
return body.value;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
request.resolve(response);
|
|
323
|
+
};
|
|
324
|
+
const ready = Promise.resolve(endpoint.subscribe(receive)).then(async (subscription) => {
|
|
325
|
+
unsubscribe = subscription;
|
|
326
|
+
if (closed) {
|
|
327
|
+
unsubscribe = void 0;
|
|
328
|
+
await subscription();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
const transport = (async (request) => {
|
|
332
|
+
if (closed) throw new MessagePortTransportError("closed", "Message-port transport is closed");
|
|
333
|
+
request.signal?.throwIfAborted();
|
|
334
|
+
await new Promise((resolve, reject) => {
|
|
335
|
+
const signal = request.signal;
|
|
336
|
+
const abort = () => {
|
|
337
|
+
signal?.removeEventListener("abort", abort);
|
|
338
|
+
reject(signal?.reason);
|
|
339
|
+
};
|
|
340
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
341
|
+
ready.then(() => {
|
|
342
|
+
signal?.removeEventListener("abort", abort);
|
|
343
|
+
resolve();
|
|
344
|
+
}, (error) => {
|
|
345
|
+
signal?.removeEventListener("abort", abort);
|
|
346
|
+
reject(error);
|
|
347
|
+
});
|
|
348
|
+
if (signal?.aborted) abort();
|
|
349
|
+
});
|
|
350
|
+
request.signal?.throwIfAborted();
|
|
351
|
+
const id = nextRequestId();
|
|
352
|
+
const encodedRequest = {
|
|
353
|
+
key: request.key,
|
|
354
|
+
method: request.method,
|
|
355
|
+
path: request.path,
|
|
356
|
+
...request.query === void 0 ? {} : { query: request.query },
|
|
357
|
+
headers: request.headers,
|
|
358
|
+
...request.body === void 0 ? {} : { body: await encodeRequestBody(request.body) }
|
|
359
|
+
};
|
|
360
|
+
if (closed) throw new MessagePortTransportError("closed", "Message-port transport is closed");
|
|
361
|
+
request.signal?.throwIfAborted();
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const cancel = (error = request.signal?.reason) => {
|
|
364
|
+
const active = pending.get(id);
|
|
365
|
+
if (active === void 0) return;
|
|
366
|
+
clearPending(id, active);
|
|
367
|
+
sendDetached({
|
|
368
|
+
protocol: 1,
|
|
369
|
+
channel,
|
|
370
|
+
type: "cancel",
|
|
371
|
+
id
|
|
372
|
+
});
|
|
373
|
+
reject(error);
|
|
374
|
+
};
|
|
375
|
+
const onAbort = () => cancel(request.signal?.reason);
|
|
376
|
+
const active = {
|
|
377
|
+
resolve,
|
|
378
|
+
reject,
|
|
379
|
+
...request.signal === void 0 ? {} : {
|
|
380
|
+
signal: request.signal,
|
|
381
|
+
onAbort
|
|
382
|
+
},
|
|
383
|
+
cancel
|
|
384
|
+
};
|
|
385
|
+
pending.set(id, active);
|
|
386
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
387
|
+
if (request.signal?.aborted) {
|
|
388
|
+
cancel();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const message = {
|
|
392
|
+
protocol: 1,
|
|
393
|
+
channel,
|
|
394
|
+
type: "request",
|
|
395
|
+
id,
|
|
396
|
+
request: encodedRequest
|
|
397
|
+
};
|
|
398
|
+
try {
|
|
399
|
+
Promise.resolve(endpoint.send(message)).catch((error) => {
|
|
400
|
+
if (pending.get(id) !== active) return;
|
|
401
|
+
clearPending(id, active);
|
|
402
|
+
reject(error);
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
clearPending(id, active);
|
|
406
|
+
reject(error);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
Object.defineProperties(transport, {
|
|
411
|
+
ready: {
|
|
412
|
+
value: ready,
|
|
413
|
+
enumerable: true
|
|
414
|
+
},
|
|
415
|
+
close: {
|
|
416
|
+
enumerable: true,
|
|
417
|
+
value: async () => {
|
|
418
|
+
if (!closed) {
|
|
419
|
+
closed = true;
|
|
420
|
+
failAll(new MessagePortTransportError("closed", "Message-port transport is closed"), true);
|
|
421
|
+
}
|
|
422
|
+
await ready;
|
|
423
|
+
const cleanup = unsubscribe;
|
|
424
|
+
unsubscribe = void 0;
|
|
425
|
+
await cleanup?.();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return transport;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/server.ts
|
|
434
|
+
function channelName(options) {
|
|
435
|
+
const channel = options.channel ?? "@hulla/api";
|
|
436
|
+
if (channel.length === 0) throw new TypeError("Message-port channel must not be empty");
|
|
437
|
+
return channel;
|
|
438
|
+
}
|
|
439
|
+
function requestHeaders(value) {
|
|
440
|
+
return isRecord(value) && Object.values(value).every((field) => typeof field === "string");
|
|
441
|
+
}
|
|
442
|
+
function decodeRequest(message, signal) {
|
|
443
|
+
const request = message.request;
|
|
444
|
+
if (!isRecord(request) || !Array.isArray(request["key"]) || !request["key"].every((part) => typeof part === "string") || typeof request["method"] !== "string" || typeof request["path"] !== "string" || !requestHeaders(request["headers"])) throw new TypeError("Received an invalid message-port request");
|
|
445
|
+
const body = request["body"];
|
|
446
|
+
let decodedBody;
|
|
447
|
+
if (body !== void 0) {
|
|
448
|
+
if (!isRecord(body) || typeof body["contentType"] !== "string") throw new TypeError("Received an invalid message-port request body");
|
|
449
|
+
switch (body["kind"]) {
|
|
450
|
+
case "bytes":
|
|
451
|
+
if (!(body["value"] instanceof Uint8Array)) throw new TypeError("Message-port byte body must be Uint8Array");
|
|
452
|
+
decodedBody = {
|
|
453
|
+
kind: "bytes",
|
|
454
|
+
contentType: body["contentType"],
|
|
455
|
+
value: body["value"]
|
|
456
|
+
};
|
|
457
|
+
break;
|
|
458
|
+
case "text":
|
|
459
|
+
if (typeof body["value"] !== "string") throw new TypeError("Message-port text body must be a string");
|
|
460
|
+
decodedBody = {
|
|
461
|
+
kind: "text",
|
|
462
|
+
contentType: body["contentType"],
|
|
463
|
+
value: body["value"]
|
|
464
|
+
};
|
|
465
|
+
break;
|
|
466
|
+
case "json":
|
|
467
|
+
decodedBody = {
|
|
468
|
+
kind: "json",
|
|
469
|
+
contentType: body["contentType"],
|
|
470
|
+
value: body["value"]
|
|
471
|
+
};
|
|
472
|
+
break;
|
|
473
|
+
case "form-data":
|
|
474
|
+
if (!Array.isArray(body["value"])) throw new TypeError("Message-port form data body must contain entries");
|
|
475
|
+
decodedBody = {
|
|
476
|
+
kind: "form-data",
|
|
477
|
+
contentType: body["contentType"],
|
|
478
|
+
value: decodeMessagePortFormData(body["value"])
|
|
479
|
+
};
|
|
480
|
+
break;
|
|
481
|
+
default: throw new TypeError("Received an unsupported message-port request body");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
key: request["key"],
|
|
486
|
+
method: request["method"],
|
|
487
|
+
path: request["path"],
|
|
488
|
+
...request["query"] === void 0 ? {} : { query: request["query"] },
|
|
489
|
+
headers: request["headers"],
|
|
490
|
+
...decodedBody === void 0 ? {} : { body: decodedBody },
|
|
491
|
+
signal
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function streamIterator(value) {
|
|
495
|
+
const source = value;
|
|
496
|
+
if (typeof source[Symbol.asyncIterator] === "function") return source[Symbol.asyncIterator]();
|
|
497
|
+
const iterator = source[Symbol.iterator]();
|
|
498
|
+
return {
|
|
499
|
+
next: async () => iterator.next(),
|
|
500
|
+
...iterator.return === void 0 ? {} : { return: async () => iterator.return() }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function createMessagePortServer(port, endpoint, channel, implementation, options) {
|
|
504
|
+
assertAdapterContext(implementation.context, "message-port");
|
|
505
|
+
const usesNativeContext = serverContextAdapterId(implementation.context) !== void 0;
|
|
506
|
+
const dispatch = createAdapterHandler(implementation, options.onError === void 0 ? {} : { onError: (input) => options.onError?.({
|
|
507
|
+
...input,
|
|
508
|
+
request: input.request
|
|
509
|
+
}) });
|
|
510
|
+
const active = /* @__PURE__ */ new Map();
|
|
511
|
+
let closed = false;
|
|
512
|
+
let unsubscribe;
|
|
513
|
+
const send = (message) => closed ? Promise.resolve() : Promise.resolve(endpoint.send(message));
|
|
514
|
+
const cancel = async (id, request) => {
|
|
515
|
+
if (request.cancelled) return;
|
|
516
|
+
request.cancelled = true;
|
|
517
|
+
request.controller.abort();
|
|
518
|
+
active.delete(id);
|
|
519
|
+
await request.iterator?.return?.();
|
|
520
|
+
};
|
|
521
|
+
const pump = async (id, request) => {
|
|
522
|
+
if (request.pumping || request.iterator === void 0) return;
|
|
523
|
+
request.pumping = true;
|
|
524
|
+
try {
|
|
525
|
+
while (!request.cancelled && request.pulls > 0) {
|
|
526
|
+
request.pulls--;
|
|
527
|
+
const result = await request.iterator.next();
|
|
528
|
+
if (request.cancelled) break;
|
|
529
|
+
if (result.done) {
|
|
530
|
+
active.delete(id);
|
|
531
|
+
await send({
|
|
532
|
+
protocol: 1,
|
|
533
|
+
channel,
|
|
534
|
+
type: "stream-end",
|
|
535
|
+
id
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!(result.value instanceof Uint8Array)) throw new TypeError("Message-port stream chunks must be Uint8Array");
|
|
540
|
+
await send({
|
|
541
|
+
protocol: 1,
|
|
542
|
+
channel,
|
|
543
|
+
type: "stream-chunk",
|
|
544
|
+
id,
|
|
545
|
+
chunk: result.value
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
try {
|
|
550
|
+
await cancel(id, request);
|
|
551
|
+
} catch {}
|
|
552
|
+
await send({
|
|
553
|
+
protocol: 1,
|
|
554
|
+
channel,
|
|
555
|
+
type: "stream-error",
|
|
556
|
+
id,
|
|
557
|
+
error: messagePortError(error)
|
|
558
|
+
});
|
|
559
|
+
} finally {
|
|
560
|
+
request.pumping = false;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const execute = async (message) => {
|
|
564
|
+
const id = message.id;
|
|
565
|
+
if (active.has(id)) {
|
|
566
|
+
await send({
|
|
567
|
+
protocol: 1,
|
|
568
|
+
channel,
|
|
569
|
+
type: "error",
|
|
570
|
+
id,
|
|
571
|
+
error: { message: "Duplicate message-port request id" }
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const state = {
|
|
576
|
+
controller: new AbortController(),
|
|
577
|
+
cancelled: false,
|
|
578
|
+
pulls: 0,
|
|
579
|
+
pumping: false
|
|
580
|
+
};
|
|
581
|
+
active.set(id, state);
|
|
582
|
+
try {
|
|
583
|
+
const request = decodeRequest(message, state.controller.signal);
|
|
584
|
+
const response = await dispatch({
|
|
585
|
+
request,
|
|
586
|
+
signal: request.signal,
|
|
587
|
+
...usesNativeContext ? { contextInput: {
|
|
588
|
+
endpoint,
|
|
589
|
+
message,
|
|
590
|
+
port,
|
|
591
|
+
request
|
|
592
|
+
} } : {},
|
|
593
|
+
method: request.method,
|
|
594
|
+
pathname: request.path,
|
|
595
|
+
headers: request.headers,
|
|
596
|
+
...request.query === void 0 ? {} : { query: request.query },
|
|
597
|
+
...request.body === void 0 ? {} : { body: {
|
|
598
|
+
value: request.body.value,
|
|
599
|
+
contentType: request.body.contentType
|
|
600
|
+
} }
|
|
601
|
+
});
|
|
602
|
+
if (state.cancelled) return;
|
|
603
|
+
if (response.body.kind === "stream") state.iterator = streamIterator(response.body.value);
|
|
604
|
+
const body = await encodeResponseBody(response.body);
|
|
605
|
+
if (state.cancelled) return;
|
|
606
|
+
await send({
|
|
607
|
+
protocol: 1,
|
|
608
|
+
channel,
|
|
609
|
+
type: "response",
|
|
610
|
+
id,
|
|
611
|
+
response: {
|
|
612
|
+
status: response.status,
|
|
613
|
+
headers: response.body.kind === "form-data" && response.headers["content-type"] === void 0 ? {
|
|
614
|
+
...response.headers,
|
|
615
|
+
"content-type": "multipart/form-data"
|
|
616
|
+
} : response.headers,
|
|
617
|
+
body
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
if (response.body.kind !== "stream") active.delete(id);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
active.delete(id);
|
|
623
|
+
try {
|
|
624
|
+
await state.iterator?.return?.();
|
|
625
|
+
} catch {}
|
|
626
|
+
if (state.cancelled) return;
|
|
627
|
+
await send({
|
|
628
|
+
protocol: 1,
|
|
629
|
+
channel,
|
|
630
|
+
type: "error",
|
|
631
|
+
id,
|
|
632
|
+
error: messagePortError(error)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const receive = (value) => {
|
|
637
|
+
if (!isMessagePortEnvelope(value, channel) || value.type === "close") return;
|
|
638
|
+
if (typeof value.id !== "string") return;
|
|
639
|
+
if (value.type === "request") {
|
|
640
|
+
execute(value).catch(() => {});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const request = active.get(value.id);
|
|
644
|
+
if (request === void 0) return;
|
|
645
|
+
if (value.type === "cancel") cancel(value.id, request).catch(() => {});
|
|
646
|
+
else if (value.type === "stream-pull" && request.iterator !== void 0) {
|
|
647
|
+
request.pulls++;
|
|
648
|
+
pump(value.id, request).catch(() => {});
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const ready = Promise.resolve(endpoint.subscribe(receive)).then((subscription) => {
|
|
652
|
+
unsubscribe = subscription;
|
|
653
|
+
if (closed) return subscription();
|
|
654
|
+
});
|
|
655
|
+
return Object.freeze({
|
|
656
|
+
ready,
|
|
657
|
+
close: async () => {
|
|
658
|
+
if (closed) return;
|
|
659
|
+
await ready;
|
|
660
|
+
try {
|
|
661
|
+
await send({
|
|
662
|
+
protocol: 1,
|
|
663
|
+
channel,
|
|
664
|
+
type: "close"
|
|
665
|
+
});
|
|
666
|
+
} finally {
|
|
667
|
+
closed = true;
|
|
668
|
+
try {
|
|
669
|
+
await unsubscribe?.();
|
|
670
|
+
} finally {
|
|
671
|
+
await Promise.all([...active].map(([id, request]) => cancel(id, request)));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
/** Creates a server adapter over a MessagePort or custom ordered IPC endpoint. */
|
|
678
|
+
function messagePortAdapter(port, options = {}) {
|
|
679
|
+
const endpoint = resolveMessageEndpoint(port);
|
|
680
|
+
const channel = channelName(options);
|
|
681
|
+
return createServerAdapter("message-port", { mount: (implementation, mountOptions = {}) => createMessagePortServer(port, endpoint, channel, implementation, mountOptions) });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
export { DEFAULT_MESSAGE_PORT_CHANNEL, MESSAGE_PORT_PROTOCOL_VERSION, MessagePortTransportError, messagePortAdapter, messagePortEndpoint, messagePortTransport };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/object.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/protocol.ts
|
|
8
|
+
const MESSAGE_PORT_PROTOCOL_VERSION = 1;
|
|
9
|
+
const DEFAULT_MESSAGE_PORT_CHANNEL = "@hulla/api";
|
|
10
|
+
function isMessagePortEnvelope(value, channel) {
|
|
11
|
+
return isRecord(value) && value["channel"] === channel && value["protocol"] === 1 && typeof value["type"] === "string";
|
|
12
|
+
}
|
|
13
|
+
function messagePortError(error) {
|
|
14
|
+
return error instanceof Error ? {
|
|
15
|
+
message: error.message,
|
|
16
|
+
...error.name === "Error" ? {} : { name: error.name }
|
|
17
|
+
} : { message: String(error) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { isRecord as a, messagePortError as i, MESSAGE_PORT_PROTOCOL_VERSION as n, isMessagePortEnvelope as r, DEFAULT_MESSAGE_PORT_CHANNEL as t };
|
package/dist/tauri.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { t as MessageEndpoint } from "./endpoint-CPsj0RmN.js";
|
|
2
|
+
import { n as DesktopEndpointOptions } from "./desktop-q9GwZShg.js";
|
|
3
|
+
//#region src/tauri.d.ts
|
|
4
|
+
type TauriMessageChannel = {
|
|
5
|
+
onmessage: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
type TauriBridgeOptions = DesktopEndpointOptions & {
|
|
8
|
+
/** Dedicated Channel<string> supplied by @tauri-apps/api/core. */
|
|
9
|
+
readonly messages: TauriMessageChannel;
|
|
10
|
+
/** Register the channel with the native relay; resolve only when routing is ready. */
|
|
11
|
+
readonly connect: () => PromiseLike<unknown>;
|
|
12
|
+
/** Invoke an application command that forwards one opaque string to the peer. */
|
|
13
|
+
readonly send: (message: string) => PromiseLike<unknown>;
|
|
14
|
+
/** Unregister native routing for this session. Must also handle a failed connect. */
|
|
15
|
+
readonly disconnect: () => void | PromiseLike<void>;
|
|
16
|
+
};
|
|
17
|
+
declare function tauriEndpoint(options: TauriBridgeOptions): MessageEndpoint;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { TauriBridgeOptions, TauriMessageChannel, tauriEndpoint };
|
package/dist/tauri.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as desktopEndpoint } from "./desktop-BojxzZgV.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tauri.ts
|
|
4
|
+
function tauriEndpoint(options) {
|
|
5
|
+
return desktopEndpoint({
|
|
6
|
+
send: async (message) => {
|
|
7
|
+
await options.send(message);
|
|
8
|
+
},
|
|
9
|
+
subscribe: async (receive) => {
|
|
10
|
+
const previous = options.messages.onmessage;
|
|
11
|
+
options.messages.onmessage = receive;
|
|
12
|
+
try {
|
|
13
|
+
await options.connect();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
options.messages.onmessage = previous;
|
|
16
|
+
await options.disconnect();
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return async () => {
|
|
20
|
+
if (options.messages.onmessage === receive) options.messages.onmessage = previous;
|
|
21
|
+
await options.disconnect();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}, options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { tauriEndpoint };
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hulla/api-message-port",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MessagePort and ordered IPC transport for @hulla/api",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/hulladev/api.git",
|
|
9
|
+
"directory": "packages/message-port"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./desktop": {
|
|
22
|
+
"types": "./dist/desktop.d.ts",
|
|
23
|
+
"import": "./dist/desktop.js"
|
|
24
|
+
},
|
|
25
|
+
"./electron": {
|
|
26
|
+
"types": "./dist/electron.d.ts",
|
|
27
|
+
"import": "./dist/electron.js"
|
|
28
|
+
},
|
|
29
|
+
"./tauri": {
|
|
30
|
+
"types": "./dist/tauri.d.ts",
|
|
31
|
+
"import": "./dist/tauri.js"
|
|
32
|
+
},
|
|
33
|
+
"./dioxus": {
|
|
34
|
+
"types": "./dist/dioxus.d.ts",
|
|
35
|
+
"import": "./dist/dioxus.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"clean": "rm -rf dist coverage",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@hulla/api": "2.0.0",
|
|
52
|
+
"@types/node": "^26.2.0",
|
|
53
|
+
"zod": "^4.4.3"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@hulla/api": "^2.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/README.md
DELETED