@diffexai/diffex-client 0.2.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/unix.js ADDED
@@ -0,0 +1,150 @@
1
+ import { createConnection } from "node:net";
2
+ import { DEFAULT_MAX_FRAME_LENGTH } from "@diffexai/diffex-protocol";
3
+ const MAX_UNIX_SOCKET_PATH_BYTES = process.platform === "linux" ? 107 : 103;
4
+ /** Creates fresh Unix-domain socket transports for DiffexClient connection attempts in Node-compatible runtimes. */
5
+ export function createUnixTransportFactory(options) {
6
+ if (options.path.length === 0)
7
+ throw new TypeError("Unix transport path must not be empty");
8
+ if (Buffer.byteLength(options.path) > MAX_UNIX_SOCKET_PATH_BYTES) {
9
+ throw new TypeError(`Unix transport path is too long; maximum is ${MAX_UNIX_SOCKET_PATH_BYTES} UTF-8 bytes`);
10
+ }
11
+ const maxPendingBytes = options.maxPendingBytes ?? DEFAULT_MAX_FRAME_LENGTH * 4;
12
+ if (!Number.isSafeInteger(maxPendingBytes) || maxPendingBytes <= 0) {
13
+ throw new TypeError("Unix transport maxPendingBytes must be a positive safe integer");
14
+ }
15
+ if (process.platform === "win32")
16
+ throw new Error("Unix transport is not supported on Windows");
17
+ return (handlers) => connectUnixSocket(options.path, maxPendingBytes, handlers);
18
+ }
19
+ function connectUnixSocket(path, maxPendingBytes, handlers) {
20
+ return new Promise((resolve, reject) => {
21
+ const socket = createConnection(path);
22
+ let connected = false;
23
+ let terminal = false;
24
+ const close = () => {
25
+ if (terminal)
26
+ return;
27
+ terminal = true;
28
+ socket.destroy();
29
+ if (connected)
30
+ handlers.onClose();
31
+ else
32
+ reject(new Error("Unix transport closed before connecting"));
33
+ };
34
+ socket.once("connect", () => {
35
+ if (terminal)
36
+ return;
37
+ connected = true;
38
+ resolve(new UnixByteTransport(socket, maxPendingBytes, () => {
39
+ terminal = true;
40
+ }));
41
+ });
42
+ socket.on("data", (chunk) => {
43
+ if (!terminal)
44
+ handlers.onData(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
45
+ });
46
+ socket.once("end", close);
47
+ socket.once("close", close);
48
+ socket.once("error", (error) => {
49
+ if (terminal)
50
+ return;
51
+ terminal = true;
52
+ socket.destroy();
53
+ if (connected)
54
+ handlers.onError(error);
55
+ else
56
+ reject(error);
57
+ });
58
+ });
59
+ }
60
+ class UnixByteTransport {
61
+ #socket;
62
+ #maxPendingBytes;
63
+ #markLocalClose;
64
+ #closed = false;
65
+ #pendingBytes = 0;
66
+ #writeTail = Promise.resolve();
67
+ constructor(socket, maxPendingBytes, markLocalClose) {
68
+ this.#socket = socket;
69
+ this.#maxPendingBytes = maxPendingBytes;
70
+ this.#markLocalClose = markLocalClose;
71
+ }
72
+ send(chunk) {
73
+ if (!(chunk instanceof Uint8Array)) {
74
+ return Promise.reject(new TypeError("Unix transport chunks must be Uint8Array"));
75
+ }
76
+ if (this.#closed)
77
+ return Promise.reject(new Error("Unix transport is closed"));
78
+ if (this.#pendingBytes + chunk.byteLength > this.#maxPendingBytes) {
79
+ return Promise.reject(new Error("Unix transport exceeded its pending byte limit"));
80
+ }
81
+ this.#pendingBytes += chunk.byteLength;
82
+ const bytes = chunk.slice();
83
+ const write = this.#writeTail.then(() => this.#write(bytes));
84
+ const tracked = write.finally(() => {
85
+ this.#pendingBytes -= bytes.byteLength;
86
+ });
87
+ this.#writeTail = tracked.catch(() => { });
88
+ return tracked;
89
+ }
90
+ close() {
91
+ if (this.#closed)
92
+ return;
93
+ this.#closed = true;
94
+ this.#markLocalClose();
95
+ this.#socket.destroy();
96
+ }
97
+ #write(chunk) {
98
+ if (this.#closed || !this.#socket.writable)
99
+ return Promise.reject(new Error("Unix transport is closed"));
100
+ return new Promise((resolve, reject) => {
101
+ let callbackComplete = false;
102
+ let drainComplete = false;
103
+ let requiresDrain;
104
+ let settled = false;
105
+ const onDrain = () => {
106
+ drainComplete = true;
107
+ finish();
108
+ };
109
+ const cleanup = () => {
110
+ this.#socket.off("drain", onDrain);
111
+ this.#socket.off("close", onClose);
112
+ };
113
+ const fail = (error) => {
114
+ if (settled)
115
+ return;
116
+ settled = true;
117
+ cleanup();
118
+ reject(error);
119
+ };
120
+ const finish = () => {
121
+ if (settled || !callbackComplete || requiresDrain === undefined)
122
+ return;
123
+ if (requiresDrain && !drainComplete)
124
+ return;
125
+ settled = true;
126
+ cleanup();
127
+ resolve();
128
+ };
129
+ const onClose = () => fail(new Error("Unix transport closed during write"));
130
+ try {
131
+ this.#socket.once("close", onClose);
132
+ const accepted = this.#socket.write(chunk, (error) => {
133
+ if (error) {
134
+ fail(error);
135
+ return;
136
+ }
137
+ callbackComplete = true;
138
+ finish();
139
+ });
140
+ requiresDrain = !accepted;
141
+ if (requiresDrain)
142
+ this.#socket.once("drain", onDrain);
143
+ finish();
144
+ }
145
+ catch (error) {
146
+ fail(error instanceof Error ? error : new Error(String(error)));
147
+ }
148
+ });
149
+ }
150
+ }
@@ -0,0 +1,42 @@
1
+ import type { ByteTransportFactory } from "./transport.ts";
2
+ type WebSocketData = ArrayBuffer | ArrayBufferView;
3
+ interface WebSocketMessageEvent {
4
+ readonly data: unknown;
5
+ }
6
+ interface WebSocketCloseEvent {
7
+ readonly code: number;
8
+ readonly reason: string;
9
+ }
10
+ interface WebSocketErrorEvent {
11
+ readonly error?: unknown;
12
+ }
13
+ interface WebSocketLike {
14
+ binaryType: string;
15
+ readonly bufferedAmount: number;
16
+ readonly readyState: number;
17
+ send(data: WebSocketData): void;
18
+ close(code?: number, reason?: string): void;
19
+ addEventListener(type: "open", listener: () => void, options?: {
20
+ once?: boolean;
21
+ }): void;
22
+ addEventListener(type: "message", listener: (event: WebSocketMessageEvent) => void): void;
23
+ addEventListener(type: "close", listener: (event: WebSocketCloseEvent) => void, options?: {
24
+ once?: boolean;
25
+ }): void;
26
+ addEventListener(type: "error", listener: (event: WebSocketErrorEvent) => void, options?: {
27
+ once?: boolean;
28
+ }): void;
29
+ }
30
+ export interface WebSocketConstructor {
31
+ new (url: string | URL, protocols?: string | string[]): WebSocketLike;
32
+ readonly OPEN: number;
33
+ }
34
+ export interface WebSocketTransportOptions {
35
+ url: string | URL | (() => string | URL | Promise<string | URL>);
36
+ protocols?: string | string[];
37
+ maxPendingBytes?: number;
38
+ webSocket?: WebSocketConstructor;
39
+ }
40
+ /** Creates authenticated browser-compatible WebSocket transports for DiffexClient connection attempts. */
41
+ export declare function createWebSocketTransportFactory(options: WebSocketTransportOptions): ByteTransportFactory;
42
+ export {};
@@ -0,0 +1,124 @@
1
+ import { DEFAULT_MAX_FRAME_LENGTH } from "@diffexai/diffex-protocol";
2
+ /** Creates authenticated browser-compatible WebSocket transports for DiffexClient connection attempts. */
3
+ export function createWebSocketTransportFactory(options) {
4
+ const maxPendingBytes = options.maxPendingBytes ?? DEFAULT_MAX_FRAME_LENGTH * 4;
5
+ if (!Number.isSafeInteger(maxPendingBytes) || maxPendingBytes <= 0) {
6
+ throw new TypeError("WebSocket transport maxPendingBytes must be a positive safe integer");
7
+ }
8
+ return async (handlers) => {
9
+ const url = typeof options.url === "function" ? await options.url() : options.url;
10
+ const WebSocketImpl = options.webSocket ?? resolveGlobalWebSocket();
11
+ return connectWebSocket(WebSocketImpl, url, options.protocols, maxPendingBytes, handlers);
12
+ };
13
+ }
14
+ function resolveGlobalWebSocket() {
15
+ const candidate = globalThis.WebSocket;
16
+ if (!candidate)
17
+ throw new Error("WebSocket is not available in this runtime");
18
+ return candidate;
19
+ }
20
+ function connectWebSocket(WebSocketImpl, url, protocols, maxPendingBytes, handlers) {
21
+ return new Promise((resolve, reject) => {
22
+ const socket = protocols === undefined ? new WebSocketImpl(url) : new WebSocketImpl(url, protocols);
23
+ socket.binaryType = "arraybuffer";
24
+ let connected = false;
25
+ let terminal = false;
26
+ socket.addEventListener("open", () => {
27
+ if (terminal)
28
+ return;
29
+ connected = true;
30
+ resolve(new WebSocketByteTransport(socket, WebSocketImpl.OPEN, maxPendingBytes, () => {
31
+ terminal = true;
32
+ }));
33
+ }, { once: true });
34
+ socket.addEventListener("message", (event) => {
35
+ if (terminal)
36
+ return;
37
+ const bytes = binaryMessage(event.data);
38
+ if (!bytes) {
39
+ terminal = true;
40
+ socket.close(1003, "Binary protocol frames required");
41
+ handlers.onError(new Error("WebSocket transport received a non-binary message"));
42
+ return;
43
+ }
44
+ handlers.onData(bytes);
45
+ });
46
+ socket.addEventListener("close", (event) => {
47
+ if (terminal)
48
+ return;
49
+ terminal = true;
50
+ if (connected)
51
+ handlers.onClose();
52
+ else
53
+ reject(new Error(`WebSocket transport closed before connecting (${event.code}: ${event.reason})`));
54
+ }, { once: true });
55
+ socket.addEventListener("error", (event) => {
56
+ if (terminal)
57
+ return;
58
+ terminal = true;
59
+ const error = event.error instanceof Error ? event.error : new Error("WebSocket transport failed");
60
+ socket.close();
61
+ if (connected)
62
+ handlers.onError(error);
63
+ else
64
+ reject(error);
65
+ }, { once: true });
66
+ });
67
+ }
68
+ function binaryMessage(data) {
69
+ if (data instanceof ArrayBuffer)
70
+ return new Uint8Array(data);
71
+ if (ArrayBuffer.isView(data))
72
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
73
+ return undefined;
74
+ }
75
+ class WebSocketByteTransport {
76
+ #socket;
77
+ #openState;
78
+ #maxPendingBytes;
79
+ #markLocalClose;
80
+ #closed = false;
81
+ #pendingBytes = 0;
82
+ #sendTail = Promise.resolve();
83
+ constructor(socket, openState, maxPendingBytes, markLocalClose) {
84
+ this.#socket = socket;
85
+ this.#openState = openState;
86
+ this.#maxPendingBytes = maxPendingBytes;
87
+ this.#markLocalClose = markLocalClose;
88
+ }
89
+ send(chunk) {
90
+ if (!(chunk instanceof Uint8Array)) {
91
+ return Promise.reject(new TypeError("WebSocket transport chunks must be Uint8Array"));
92
+ }
93
+ if (this.#closed || this.#socket.readyState !== this.#openState) {
94
+ return Promise.reject(new Error("WebSocket transport is closed"));
95
+ }
96
+ if (this.#pendingBytes + chunk.byteLength > this.#maxPendingBytes) {
97
+ return Promise.reject(new Error("WebSocket transport exceeded its pending byte limit"));
98
+ }
99
+ const bytes = chunk.slice();
100
+ this.#pendingBytes += bytes.byteLength;
101
+ const send = this.#sendTail.then(() => {
102
+ if (this.#closed || this.#socket.readyState !== this.#openState) {
103
+ throw new Error("WebSocket transport is closed");
104
+ }
105
+ this.#socket.send(bytes);
106
+ if (this.#socket.bufferedAmount > this.#maxPendingBytes) {
107
+ this.close();
108
+ throw new Error("WebSocket transport exceeded its buffered byte limit");
109
+ }
110
+ });
111
+ const tracked = send.finally(() => {
112
+ this.#pendingBytes -= bytes.byteLength;
113
+ });
114
+ this.#sendTail = tracked.catch(() => { });
115
+ return tracked;
116
+ }
117
+ close() {
118
+ if (this.#closed)
119
+ return;
120
+ this.#closed = true;
121
+ this.#markLocalClose();
122
+ this.#socket.close(1000, "Client closed");
123
+ }
124
+ }
@@ -0,0 +1,16 @@
1
+ [
2
+ {
3
+ "name": "@diffexai/diffex-client",
4
+ "version": "0.2.0",
5
+ "sourceCommit": "e3f96b95ebc7fdad96220e315ec144f74383de22",
6
+ "license": "Apache-2.0",
7
+ "portions": "Diffex contributions; Pi-derived portions retain MIT terms in LICENSE",
8
+ "notice": "LICENSE",
9
+ "noticeSha256": "35a4476d361a333186451f77937c9a070027ab3a83e3661fad4ccca8ac2b0a99"
10
+ },
11
+ {
12
+ "name": "@diffexai/diffex-protocol",
13
+ "version": "^0.2.0",
14
+ "distribution": "resolved separately by package manager; not bundled in this npm tarball"
15
+ }
16
+ ]
@@ -0,0 +1,102 @@
1
+ {
2
+ "sourceCommit": "e3f96b95ebc7fdad96220e315ec144f74383de22",
3
+ "algorithm": "sha256",
4
+ "files": [
5
+ {
6
+ "path": "dist/client.d.ts",
7
+ "sha256": "d129da89227c4c2ec85566693f5596535c589348593974fa53742507a295a5c3"
8
+ },
9
+ {
10
+ "path": "dist/client.js",
11
+ "sha256": "b4ea00041469ce9b0959d8fa0560769bdd3751669bbf12bea5970eef0eb3c704"
12
+ },
13
+ {
14
+ "path": "dist/cloud.d.ts",
15
+ "sha256": "4f3b497020a08d5f5fa967d916a174c5357ae0351bc33f973ff6dbfa65f97484"
16
+ },
17
+ {
18
+ "path": "dist/cloud.js",
19
+ "sha256": "0b69877596d402dbba21482ecf861a8139832fa7b92ceba7585f24466735302b"
20
+ },
21
+ {
22
+ "path": "dist/connection.d.ts",
23
+ "sha256": "4575d98b9aa96d7d597ba36f2058de92cfa40abf4cf31918def4d9af5b87d382"
24
+ },
25
+ {
26
+ "path": "dist/connection.js",
27
+ "sha256": "a473c9463cd050b467dcdae2f69b994488b09fbfb2724facd6888a4fc996250c"
28
+ },
29
+ {
30
+ "path": "dist/errors.d.ts",
31
+ "sha256": "3f51d09d4f7182f16e644171d7b677e53fc5c4129175a202090fd0c388e6f81e"
32
+ },
33
+ {
34
+ "path": "dist/errors.js",
35
+ "sha256": "b95eb6cd279adf2e5d004096f688fd95f7871ac518a02dd97d6d72095c581c63"
36
+ },
37
+ {
38
+ "path": "dist/index.d.ts",
39
+ "sha256": "ce080e9967e6e9762b55036459255093f76da8a5c36929e8d8ff3ff7b7b7638f"
40
+ },
41
+ {
42
+ "path": "dist/index.js",
43
+ "sha256": "18dfab689d3f9f8837f3e6aaa2c7071f8916535e8879975ef1510193ac65fda8"
44
+ },
45
+ {
46
+ "path": "dist/promise.d.ts",
47
+ "sha256": "fda1d50aeb352e40207ec7d868008d66145300111f567d6b1a4bbfed2e514388"
48
+ },
49
+ {
50
+ "path": "dist/promise.js",
51
+ "sha256": "5f419928b687ac9e50174e8b88d009815e4689e1ee447b7e84de254731d2fe70"
52
+ },
53
+ {
54
+ "path": "dist/session-handle.d.ts",
55
+ "sha256": "c2739c9cab31c8d09be6d18792623ca0d57528bddee867de407d3a35646da4e4"
56
+ },
57
+ {
58
+ "path": "dist/session-handle.js",
59
+ "sha256": "906eedd65ea5235af8a5e4da5ecfc4cd7c5765e6c44c1d752bc1fe583f6ad6c7"
60
+ },
61
+ {
62
+ "path": "dist/state.d.ts",
63
+ "sha256": "e953045245a6679e909a53333271ef5cc30f84c84b50d568de3fa973cb54e328"
64
+ },
65
+ {
66
+ "path": "dist/state.js",
67
+ "sha256": "ad96a633dcbc517cdb1332b46fcfcef745523d4a1c5ccb945fff8cdab5316239"
68
+ },
69
+ {
70
+ "path": "dist/transport.d.ts",
71
+ "sha256": "89390f23871babf64e807a7d2b751e9da413a6a5bafeb48572fcc41dc080199b"
72
+ },
73
+ {
74
+ "path": "dist/transport.js",
75
+ "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
76
+ },
77
+ {
78
+ "path": "dist/types.d.ts",
79
+ "sha256": "6a55a5a3d36ec1941afe3413488bce80612980eb246fde95d5e50ec11dcfa511"
80
+ },
81
+ {
82
+ "path": "dist/types.js",
83
+ "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
84
+ },
85
+ {
86
+ "path": "dist/unix.d.ts",
87
+ "sha256": "721f19d9f726c27f62e23b47fee331072f45f7e1e7db2f234f47ec6c86c5c3ea"
88
+ },
89
+ {
90
+ "path": "dist/unix.js",
91
+ "sha256": "fdd40193e2d1d29267b47efa2506c0162807476402a7a3722190851002950bc3"
92
+ },
93
+ {
94
+ "path": "dist/websocket.d.ts",
95
+ "sha256": "6d0e19b1ecd9229f1e8160670707178574c50c112d13510d072d9e258792b706"
96
+ },
97
+ {
98
+ "path": "dist/websocket.js",
99
+ "sha256": "a9a3b3a488122ec0bdb01c8e6ed991d94cae6306cbb074d7fb2dfe21bcb46faa"
100
+ }
101
+ ]
102
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@diffexai/diffex-client",
3
+ "version": "0.2.0",
4
+ "description": "Transport-neutral client for remote Diffex sessions over framed CBOR bytes",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./unix": {
14
+ "types": "./dist/unix.d.ts",
15
+ "import": "./dist/unix.js"
16
+ },
17
+ "./websocket": {
18
+ "types": "./dist/websocket.d.ts",
19
+ "import": "./dist/websocket.js"
20
+ },
21
+ "./cloud": {
22
+ "types": "./dist/cloud.d.ts",
23
+ "import": "./dist/cloud.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "CHANGELOG.md",
32
+ "LICENSE",
33
+ "NOTICE",
34
+ "THIRD-PARTY-NOTICES.txt",
35
+ "distribution-components.json",
36
+ "distribution-files.json"
37
+ ],
38
+ "scripts": {
39
+ "clean": "shx rm -rf dist",
40
+ "build": "tsgo -p tsconfig.build.json",
41
+ "test": "vitest --run",
42
+ "typecheck": "tsgo -p tsconfig.test.json",
43
+ "prepublishOnly": "npm run clean && npm run build",
44
+ "prepack": "node ../../scripts/prepare-distribution.mjs packages/client"
45
+ },
46
+ "keywords": [
47
+ "diffex",
48
+ "client",
49
+ "protocol",
50
+ "cbor",
51
+ "binary"
52
+ ],
53
+ "author": "DiffexAI",
54
+ "license": "Apache-2.0",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/diffexai/diffex.git",
58
+ "directory": "packages/client"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.19.0"
62
+ },
63
+ "dependencies": {
64
+ "@diffexai/diffex-protocol": "^0.2.0"
65
+ },
66
+ "devDependencies": {
67
+ "shx": "0.4.0",
68
+ "vitest": "4.1.9"
69
+ }
70
+ }