@hulla/api-message-port 0.0.0-stage → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { a as encodeDesktopMessage, i as decodeDesktopMessage, n as DesktopEndpointOptions, r as desktopEndpoint, t as DesktopBridge } from "./desktop-q9GwZShg.js";
2
+ export { DesktopBridge, DesktopEndpointOptions, decodeDesktopMessage, desktopEndpoint, encodeDesktopMessage };
@@ -0,0 +1,3 @@
1
+ import { n as decodeDesktopMessage, r as encodeDesktopMessage, t as desktopEndpoint } from "./desktop-BojxzZgV.js";
2
+
3
+ export { decodeDesktopMessage, desktopEndpoint, encodeDesktopMessage };
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { a as messagePortEndpoint, i as MessagePortLike } from "./endpoint-CPsj0RmN.js";
2
+ export { type MessagePortLike as ElectronPort, messagePortEndpoint as electronEndpoint };
@@ -0,0 +1,3 @@
1
+ import { t as messagePortEndpoint } from "./endpoint-y_ZFW2WR.js";
2
+
3
+ export { messagePortEndpoint as electronEndpoint };
@@ -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 };
@@ -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 };
@@ -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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.1",
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.1",
52
+ "@types/node": "^26.2.0",
53
+ "zod": "^4.4.3"
54
+ },
55
+ "peerDependencies": {
56
+ "@hulla/api": "^2.0.1"
57
+ }
58
+ }
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.