@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.
@@ -0,0 +1,203 @@
1
+ import { DEFAULT_MAX_FRAME_LENGTH, encodeClientMessage, PROTOCOL_VERSION, ProtocolValidationError, ServerMessageDecoder, } from "@diffexai/diffex-protocol";
2
+ import { DiffexDisconnectedError, DiffexServerError, toDisconnectedError, toError } from "./errors.js";
3
+ import { createPromiseResolvers } from "./promise.js";
4
+ const MAX_UINT32 = 0xffff_ffff;
5
+ export class Connection {
6
+ #options;
7
+ #maxFrameLength;
8
+ #lifecycle = { state: "disconnected" };
9
+ #sequence = 0;
10
+ constructor(options) {
11
+ this.#options = options;
12
+ this.#maxFrameLength = options.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;
13
+ if (!Number.isSafeInteger(this.#maxFrameLength) ||
14
+ this.#maxFrameLength <= 0 ||
15
+ this.#maxFrameLength > MAX_UINT32) {
16
+ throw new TypeError(`DiffexClient maxFrameLength must be an integer between 1 and ${MAX_UINT32}`);
17
+ }
18
+ }
19
+ get state() {
20
+ return this.#lifecycle.state;
21
+ }
22
+ get maxFrameLength() {
23
+ return this.#maxFrameLength;
24
+ }
25
+ connect() {
26
+ if (this.#lifecycle.state !== "disconnected") {
27
+ return Promise.reject(new DiffexDisconnectedError(`DiffexClient is already ${this.#lifecycle.state}`));
28
+ }
29
+ const id = ++this.#sequence;
30
+ const handshake = createPromiseResolvers();
31
+ this.#lifecycle = {
32
+ state: "connecting",
33
+ id,
34
+ decoder: new ServerMessageDecoder({ maxFrameLength: this.#maxFrameLength }),
35
+ handshake,
36
+ };
37
+ this.#options.onStateChange({ state: "connecting" });
38
+ const handlers = {
39
+ onData: (chunk) => this.#handleData(id, chunk),
40
+ onClose: () => {
41
+ if (this.#isCurrent(id))
42
+ this.#handleClose();
43
+ },
44
+ onError: (error) => {
45
+ if (this.#isCurrent(id))
46
+ this.#failAndClose(toDisconnectedError(error));
47
+ },
48
+ };
49
+ void this.#openTransport(id, handlers);
50
+ return handshake.promise;
51
+ }
52
+ disconnect(reason = "Client disconnected") {
53
+ if (this.#lifecycle.state === "disconnected")
54
+ return;
55
+ this.#failAndClose(typeof reason === "string" ? new DiffexDisconnectedError(reason) : reason);
56
+ }
57
+ fail(error) {
58
+ this.#failAndClose(error);
59
+ }
60
+ send(frame) {
61
+ const lifecycle = this.#lifecycle;
62
+ if (lifecycle.state !== "connected")
63
+ throw new DiffexDisconnectedError();
64
+ let sending;
65
+ try {
66
+ sending = lifecycle.transport.send(frame);
67
+ }
68
+ catch (error) {
69
+ this.#failAndClose(toDisconnectedError(error));
70
+ return;
71
+ }
72
+ void sending.catch((error) => {
73
+ const current = this.#lifecycle;
74
+ if (current.state !== "disconnected" && current.transport === lifecycle.transport) {
75
+ this.#failAndClose(toDisconnectedError(error));
76
+ }
77
+ });
78
+ }
79
+ async #openTransport(id, handlers) {
80
+ let transport;
81
+ try {
82
+ transport = await this.#options.transportFactory(handlers);
83
+ }
84
+ catch (error) {
85
+ if (this.#isCurrent(id))
86
+ this.#fail(toDisconnectedError(error));
87
+ return;
88
+ }
89
+ const lifecycle = this.#lifecycle;
90
+ if (lifecycle.state !== "connecting" || lifecycle.id !== id) {
91
+ transport.close();
92
+ return;
93
+ }
94
+ this.#lifecycle = { ...lifecycle, transport };
95
+ try {
96
+ await transport.send(encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION }, { maxFrameLength: this.#maxFrameLength }));
97
+ }
98
+ catch (error) {
99
+ if (this.#isCurrent(id))
100
+ this.#failAndClose(toDisconnectedError(error));
101
+ }
102
+ }
103
+ #handleData(id, chunk) {
104
+ const lifecycle = this.#lifecycle;
105
+ if (lifecycle.state === "disconnected" || lifecycle.id !== id)
106
+ return;
107
+ if (lifecycle.state === "connecting" && !lifecycle.transport) {
108
+ this.#failAndClose(new ProtocolValidationError("Received server data before the client hello was sent"));
109
+ return;
110
+ }
111
+ let messages;
112
+ try {
113
+ messages = lifecycle.decoder.push(chunk);
114
+ }
115
+ catch (error) {
116
+ this.#failAndClose(toError(error));
117
+ return;
118
+ }
119
+ for (const message of messages) {
120
+ if (this.#lifecycle.state === "disconnected")
121
+ return;
122
+ this.#handleMessage(message);
123
+ }
124
+ }
125
+ #handleMessage(message) {
126
+ const lifecycle = this.#lifecycle;
127
+ if (lifecycle.state === "connecting") {
128
+ if (message.type === "hello_error") {
129
+ this.#failAndClose(new DiffexServerError(message.error));
130
+ return;
131
+ }
132
+ if (message.type !== "hello") {
133
+ this.#failAndClose(new ProtocolValidationError("Expected server hello as first message"));
134
+ return;
135
+ }
136
+ if (!lifecycle.transport) {
137
+ this.#failAndClose(new ProtocolValidationError("Received server hello before the client hello was sent"));
138
+ return;
139
+ }
140
+ const connected = {
141
+ state: "connected",
142
+ id: lifecycle.id,
143
+ decoder: lifecycle.decoder,
144
+ transport: lifecycle.transport,
145
+ handshake: lifecycle.handshake,
146
+ };
147
+ this.#lifecycle = connected;
148
+ try {
149
+ this.#options.onHandshake(message.snapshot);
150
+ }
151
+ catch (error) {
152
+ if (this.#lifecycle === connected)
153
+ this.#failAndClose(toError(error));
154
+ return;
155
+ }
156
+ if (this.#lifecycle !== connected)
157
+ return;
158
+ this.#options.onStateChange({ state: "connected" });
159
+ if (this.#lifecycle !== connected)
160
+ return;
161
+ this.#lifecycle = { ...connected, handshake: undefined };
162
+ lifecycle.handshake.resolve(message.snapshot);
163
+ return;
164
+ }
165
+ if (lifecycle.state !== "connected")
166
+ return;
167
+ if (message.type === "hello" || message.type === "hello_error") {
168
+ this.#failAndClose(new ProtocolValidationError("Unexpected handshake message"));
169
+ return;
170
+ }
171
+ this.#options.onMessage(message);
172
+ }
173
+ #handleClose() {
174
+ const lifecycle = this.#lifecycle;
175
+ if (lifecycle.state === "disconnected")
176
+ return;
177
+ let error = new DiffexDisconnectedError("Byte transport closed");
178
+ try {
179
+ lifecycle.decoder.end();
180
+ }
181
+ catch (decoderError) {
182
+ error = toError(decoderError);
183
+ }
184
+ this.#fail(error);
185
+ }
186
+ #failAndClose(error) {
187
+ const lifecycle = this.#lifecycle;
188
+ const transport = lifecycle.state === "disconnected" ? undefined : lifecycle.transport;
189
+ this.#fail(error);
190
+ transport?.close();
191
+ }
192
+ #fail(error) {
193
+ const lifecycle = this.#lifecycle;
194
+ if (lifecycle.state === "disconnected")
195
+ return;
196
+ this.#lifecycle = { state: "disconnected" };
197
+ lifecycle.handshake?.reject(error);
198
+ this.#options.onStateChange({ state: "disconnected", error });
199
+ }
200
+ #isCurrent(id) {
201
+ return this.#lifecycle.state !== "disconnected" && this.#lifecycle.id === id;
202
+ }
203
+ }
@@ -0,0 +1,22 @@
1
+ import type { JsonValue, ProtocolError, ProtocolErrorCode } from "@diffexai/diffex-protocol";
2
+ export declare class DiffexServerError extends Error {
3
+ readonly code: ProtocolErrorCode;
4
+ readonly details: JsonValue | undefined;
5
+ constructor(error: ProtocolError);
6
+ }
7
+ export declare class DiffexDisconnectedError extends Error {
8
+ constructor(message?: string);
9
+ }
10
+ export declare class DiffexClientDisposedError extends Error {
11
+ constructor();
12
+ }
13
+ export declare class DiffexSessionOwnershipError extends Error {
14
+ readonly sessionId: string;
15
+ constructor(sessionId: string, message: string);
16
+ }
17
+ export declare class DiffexSessionDetachedError extends Error {
18
+ readonly sessionId: string;
19
+ constructor(sessionId: string);
20
+ }
21
+ export declare function toError(error: unknown): Error;
22
+ export declare function toDisconnectedError(error: unknown): DiffexDisconnectedError;
package/dist/errors.js ADDED
@@ -0,0 +1,45 @@
1
+ export class DiffexServerError extends Error {
2
+ code;
3
+ details;
4
+ constructor(error) {
5
+ super(error.message);
6
+ this.name = "DiffexServerError";
7
+ this.code = error.code;
8
+ this.details = error.details;
9
+ }
10
+ }
11
+ export class DiffexDisconnectedError extends Error {
12
+ constructor(message = "Diffex client is disconnected") {
13
+ super(message);
14
+ this.name = "DiffexDisconnectedError";
15
+ }
16
+ }
17
+ export class DiffexClientDisposedError extends Error {
18
+ constructor() {
19
+ super("Diffex client is disposed");
20
+ this.name = "DiffexClientDisposedError";
21
+ }
22
+ }
23
+ export class DiffexSessionOwnershipError extends Error {
24
+ sessionId;
25
+ constructor(sessionId, message) {
26
+ super(message);
27
+ this.name = "DiffexSessionOwnershipError";
28
+ this.sessionId = sessionId;
29
+ }
30
+ }
31
+ export class DiffexSessionDetachedError extends Error {
32
+ sessionId;
33
+ constructor(sessionId) {
34
+ super(`Session ${sessionId} is not attached`);
35
+ this.name = "DiffexSessionDetachedError";
36
+ this.sessionId = sessionId;
37
+ }
38
+ }
39
+ export function toError(error) {
40
+ return error instanceof Error ? error : new Error(String(error));
41
+ }
42
+ export function toDisconnectedError(error) {
43
+ const cause = toError(error);
44
+ return cause instanceof DiffexDisconnectedError ? cause : new DiffexDisconnectedError(cause.message);
45
+ }
@@ -0,0 +1,5 @@
1
+ export { DiffexClient } from "./client.ts";
2
+ export { DiffexClientDisposedError, DiffexDisconnectedError, DiffexServerError, DiffexSessionDetachedError, DiffexSessionOwnershipError, } from "./errors.ts";
3
+ export type { AcquireSessionOptions, DiffexSessionHandle, SessionLease, SessionLeaseMode } from "./session-handle.ts";
4
+ export type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from "./transport.ts";
5
+ export type { ConnectionState, ConnectionStateChange, CreateSessionOptions, DiffexClientOptions, ListenerErrorHandler, Unsubscribe, } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { DiffexClient } from "./client.js";
2
+ export { DiffexClientDisposedError, DiffexDisconnectedError, DiffexServerError, DiffexSessionDetachedError, DiffexSessionOwnershipError, } from "./errors.js";
@@ -0,0 +1,7 @@
1
+ export interface PromiseResolvers<T> {
2
+ promise: Promise<T>;
3
+ resolve(value: T | PromiseLike<T>): void;
4
+ reject(reason?: unknown): void;
5
+ }
6
+ /** Remove in favor of `Promise.withResolvers()` when the repository's TypeScript lib baseline moves to ES2024. */
7
+ export declare function createPromiseResolvers<T>(): PromiseResolvers<T>;
@@ -0,0 +1,10 @@
1
+ /** Remove in favor of `Promise.withResolvers()` when the repository's TypeScript lib baseline moves to ES2024. */
2
+ export function createPromiseResolvers() {
3
+ let resolve;
4
+ let reject;
5
+ const promise = new Promise((resolvePromise, rejectPromise) => {
6
+ resolve = resolvePromise;
7
+ reject = rejectPromise;
8
+ });
9
+ return { promise, resolve, reject };
10
+ }
@@ -0,0 +1,53 @@
1
+ import type { Command, ModelRef, ResultForCommand, ServerEvent, SessionSnapshot, ThinkingLevel } from "@diffexai/diffex-protocol";
2
+ import type { Unsubscribe } from "./types.ts";
3
+ type SessionCommand = Extract<Command, {
4
+ sessionId: string;
5
+ }>;
6
+ export type SessionLeaseMode = "shared" | "exclusive";
7
+ export interface AcquireSessionOptions {
8
+ mode: SessionLeaseMode;
9
+ }
10
+ export interface SessionLease extends AsyncDisposable {
11
+ readonly id: string;
12
+ readonly active: boolean;
13
+ readonly attached: boolean;
14
+ readonly snapshot: SessionSnapshot | undefined;
15
+ subscribe(listener: (snapshot: SessionSnapshot) => void): Unsubscribe;
16
+ onEvent(listener: (event: ServerEvent) => void): Unsubscribe;
17
+ detach(): Promise<void>;
18
+ dispose(): Promise<void>;
19
+ prompt(text: string): Promise<SessionSnapshot>;
20
+ steer(text: string): Promise<SessionSnapshot>;
21
+ abort(): Promise<SessionSnapshot>;
22
+ setModel(model: ModelRef): Promise<SessionSnapshot>;
23
+ setThinking(thinkingLevel: ThinkingLevel): Promise<SessionSnapshot>;
24
+ }
25
+ export type DiffexSessionHandle = SessionLease;
26
+ export interface SessionHandleCallbacks {
27
+ isAttached(): boolean;
28
+ getSnapshot(): SessionSnapshot | undefined;
29
+ subscribe(listener: (snapshot: SessionSnapshot) => void): Unsubscribe;
30
+ onEvent(listener: (event: ServerEvent) => void): Unsubscribe;
31
+ detach(): Promise<void>;
32
+ dispose(): Promise<void>;
33
+ request<const TCommand extends SessionCommand>(command: TCommand): Promise<ResultForCommand<TCommand>>;
34
+ }
35
+ export declare class SessionHandle implements SessionLease {
36
+ #private;
37
+ readonly id: string;
38
+ constructor(id: string, callbacks: SessionHandleCallbacks);
39
+ get attached(): boolean;
40
+ get active(): boolean;
41
+ get snapshot(): SessionSnapshot | undefined;
42
+ subscribe(listener: (snapshot: SessionSnapshot) => void): Unsubscribe;
43
+ onEvent(listener: (event: ServerEvent) => void): Unsubscribe;
44
+ detach(): Promise<void>;
45
+ dispose(): Promise<void>;
46
+ [Symbol.asyncDispose](): Promise<void>;
47
+ prompt(text: string): Promise<SessionSnapshot>;
48
+ steer(text: string): Promise<SessionSnapshot>;
49
+ abort(): Promise<SessionSnapshot>;
50
+ setModel(model: ModelRef): Promise<SessionSnapshot>;
51
+ setThinking(thinkingLevel: ThinkingLevel): Promise<SessionSnapshot>;
52
+ }
53
+ export {};
@@ -0,0 +1,50 @@
1
+ export class SessionHandle {
2
+ id;
3
+ #callbacks;
4
+ constructor(id, callbacks) {
5
+ this.id = id;
6
+ this.#callbacks = callbacks;
7
+ }
8
+ get attached() {
9
+ return this.#callbacks.isAttached();
10
+ }
11
+ get active() {
12
+ return this.attached;
13
+ }
14
+ get snapshot() {
15
+ return this.#callbacks.getSnapshot();
16
+ }
17
+ subscribe(listener) {
18
+ return this.#callbacks.subscribe(listener);
19
+ }
20
+ onEvent(listener) {
21
+ return this.#callbacks.onEvent(listener);
22
+ }
23
+ async detach() {
24
+ await this.#callbacks.detach();
25
+ }
26
+ dispose() {
27
+ return this.#callbacks.dispose();
28
+ }
29
+ [Symbol.asyncDispose]() {
30
+ return this.dispose();
31
+ }
32
+ async prompt(text) {
33
+ return (await this.#request({ command: "prompt", sessionId: this.id, text })).session;
34
+ }
35
+ async steer(text) {
36
+ return (await this.#request({ command: "steer", sessionId: this.id, text })).session;
37
+ }
38
+ async abort() {
39
+ return (await this.#request({ command: "abort", sessionId: this.id })).session;
40
+ }
41
+ async setModel(model) {
42
+ return (await this.#request({ command: "set_model", sessionId: this.id, model })).session;
43
+ }
44
+ async setThinking(thinkingLevel) {
45
+ return (await this.#request({ command: "set_thinking", sessionId: this.id, thinkingLevel })).session;
46
+ }
47
+ #request(command) {
48
+ return this.#callbacks.request(command);
49
+ }
50
+ }
@@ -0,0 +1,21 @@
1
+ import type { CommandResult, ServerEvent, ServerSnapshot, SessionSnapshot } from "@diffexai/diffex-protocol";
2
+ import type { ListenerErrorHandler, Unsubscribe } from "./types.ts";
3
+ export declare class ClientState {
4
+ #private;
5
+ constructor(onListenerError?: ListenerErrorHandler);
6
+ get snapshot(): ServerSnapshot | undefined;
7
+ reset(): void;
8
+ clearAttachments(): void;
9
+ dispose(): void;
10
+ getSessionSnapshot(sessionId: string): SessionSnapshot | undefined;
11
+ isSessionAttached(sessionId: string): boolean;
12
+ forgetSessionSnapshot(sessionId: string): SessionSnapshot | undefined;
13
+ restoreSessionSnapshot(snapshot: SessionSnapshot): void;
14
+ subscribe(listener: (snapshot: ServerSnapshot) => void): Unsubscribe;
15
+ onEvent(listener: (event: ServerEvent) => void): Unsubscribe;
16
+ subscribeSession(sessionId: string, listener: (snapshot: SessionSnapshot) => void): Unsubscribe;
17
+ onSessionEvent(sessionId: string, listener: (event: ServerEvent) => void): Unsubscribe;
18
+ applyResult(result: CommandResult): void;
19
+ applyEvent(event: ServerEvent): void;
20
+ applyServerSnapshot(snapshot: ServerSnapshot): void;
21
+ }
package/dist/state.js ADDED
@@ -0,0 +1,144 @@
1
+ import { toError } from "./errors.js";
2
+ export class ClientState {
3
+ #sessionSnapshots = new Map();
4
+ #attachedSessionIds = new Set();
5
+ #snapshotListeners = new Set();
6
+ #eventListeners = new Set();
7
+ #sessionSnapshotListeners = new Map();
8
+ #sessionEventListeners = new Map();
9
+ #onListenerError;
10
+ #snapshot;
11
+ constructor(onListenerError) {
12
+ this.#onListenerError = onListenerError;
13
+ }
14
+ get snapshot() {
15
+ return this.#snapshot;
16
+ }
17
+ reset() {
18
+ this.#snapshot = undefined;
19
+ this.#sessionSnapshots.clear();
20
+ this.#attachedSessionIds.clear();
21
+ }
22
+ clearAttachments() {
23
+ this.#attachedSessionIds.clear();
24
+ }
25
+ dispose() {
26
+ this.reset();
27
+ this.#snapshotListeners.clear();
28
+ this.#eventListeners.clear();
29
+ this.#sessionSnapshotListeners.clear();
30
+ this.#sessionEventListeners.clear();
31
+ }
32
+ getSessionSnapshot(sessionId) {
33
+ return this.#sessionSnapshots.get(sessionId);
34
+ }
35
+ isSessionAttached(sessionId) {
36
+ return this.#attachedSessionIds.has(sessionId);
37
+ }
38
+ forgetSessionSnapshot(sessionId) {
39
+ const previous = this.#sessionSnapshots.get(sessionId);
40
+ this.#sessionSnapshots.delete(sessionId);
41
+ return previous;
42
+ }
43
+ restoreSessionSnapshot(snapshot) {
44
+ if (!this.#sessionSnapshots.has(snapshot.id))
45
+ this.#sessionSnapshots.set(snapshot.id, snapshot);
46
+ }
47
+ subscribe(listener) {
48
+ this.#snapshotListeners.add(listener);
49
+ return () => this.#snapshotListeners.delete(listener);
50
+ }
51
+ onEvent(listener) {
52
+ this.#eventListeners.add(listener);
53
+ return () => this.#eventListeners.delete(listener);
54
+ }
55
+ subscribeSession(sessionId, listener) {
56
+ return addMappedListener(this.#sessionSnapshotListeners, sessionId, listener);
57
+ }
58
+ onSessionEvent(sessionId, listener) {
59
+ return addMappedListener(this.#sessionEventListeners, sessionId, listener);
60
+ }
61
+ applyResult(result) {
62
+ if (result.command === "list")
63
+ return;
64
+ if (result.command === "detach") {
65
+ this.#attachedSessionIds.delete(result.sessionId);
66
+ const snapshot = this.#sessionSnapshots.get(result.sessionId);
67
+ if (snapshot)
68
+ this.#applySessionSnapshot({ ...snapshot, attached: false }, true);
69
+ return;
70
+ }
71
+ this.#applySessionSnapshot(result.session);
72
+ }
73
+ applyEvent(event) {
74
+ if (event.type === "server_snapshot")
75
+ this.applyServerSnapshot(event.snapshot);
76
+ if (event.type === "session_snapshot")
77
+ this.#applySessionSnapshot(event.snapshot);
78
+ if (event.type === "session_removed") {
79
+ this.#sessionSnapshots.delete(event.sessionId);
80
+ this.#attachedSessionIds.delete(event.sessionId);
81
+ }
82
+ this.#notify(this.#eventListeners, event);
83
+ const sessionId = getEventSessionId(event);
84
+ if (sessionId)
85
+ this.#notify(this.#sessionEventListeners.get(sessionId), event);
86
+ }
87
+ applyServerSnapshot(snapshot) {
88
+ if (this.#snapshot && snapshot.revision < this.#snapshot.revision)
89
+ return;
90
+ this.#snapshot = snapshot;
91
+ this.#notify(this.#snapshotListeners, snapshot);
92
+ }
93
+ #applySessionSnapshot(snapshot, force = false) {
94
+ const current = this.#sessionSnapshots.get(snapshot.id);
95
+ if (!force && current && snapshot.revision < current.revision)
96
+ return;
97
+ this.#sessionSnapshots.set(snapshot.id, snapshot);
98
+ if (snapshot.attached)
99
+ this.#attachedSessionIds.add(snapshot.id);
100
+ else
101
+ this.#attachedSessionIds.delete(snapshot.id);
102
+ this.#notify(this.#sessionSnapshotListeners.get(snapshot.id), snapshot);
103
+ }
104
+ #notify(listeners, value) {
105
+ for (const listener of listeners ?? []) {
106
+ try {
107
+ listener(value);
108
+ }
109
+ catch (error) {
110
+ this.#reportListenerError(error);
111
+ }
112
+ }
113
+ }
114
+ #reportListenerError(error) {
115
+ if (!this.#onListenerError)
116
+ return;
117
+ try {
118
+ this.#onListenerError(toError(error));
119
+ }
120
+ catch {
121
+ // Diagnostics cannot affect client state.
122
+ }
123
+ }
124
+ }
125
+ function addMappedListener(listenersById, id, listener) {
126
+ let listeners = listenersById.get(id);
127
+ if (!listeners) {
128
+ listeners = new Set();
129
+ listenersById.set(id, listeners);
130
+ }
131
+ listeners.add(listener);
132
+ return () => {
133
+ listeners.delete(listener);
134
+ if (listeners.size === 0)
135
+ listenersById.delete(id);
136
+ };
137
+ }
138
+ function getEventSessionId(event) {
139
+ if (event.type === "session_snapshot")
140
+ return event.snapshot.id;
141
+ if (event.type === "session_progress" || event.type === "session_removed")
142
+ return event.sessionId;
143
+ return undefined;
144
+ }
@@ -0,0 +1,16 @@
1
+ export interface ByteTransport {
2
+ /** Sends one byte chunk. Calls must be delivered in invocation order. */
3
+ send(chunk: Uint8Array): Promise<void>;
4
+ /** Closes the transport. Implementations must make repeated calls harmless. */
5
+ close(): void;
6
+ }
7
+ export interface ByteTransportHandlers {
8
+ /** Delivers an arbitrary inbound byte chunk. */
9
+ onData(chunk: Uint8Array): void;
10
+ /** Reports an orderly terminal close. */
11
+ onClose(): void;
12
+ /** Reports a terminal transport failure. */
13
+ onError(error: Error): void;
14
+ }
15
+ /** Creates a fresh connected, authenticated transport. Exactly one terminal handler is expected. */
16
+ export type ByteTransportFactory = (handlers: ByteTransportHandlers) => ByteTransport | Promise<ByteTransport>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { ModelRef, ThinkingLevel } from "@diffexai/diffex-protocol";
2
+ import type { ByteTransportFactory } from "./transport.ts";
3
+ export type ConnectionState = "disconnected" | "connecting" | "connected";
4
+ export interface ConnectionStateChange {
5
+ state: ConnectionState;
6
+ error?: Error;
7
+ }
8
+ export type Unsubscribe = () => void;
9
+ export type ListenerErrorHandler = (error: Error) => void;
10
+ export interface DiffexClientOptions {
11
+ transportFactory: ByteTransportFactory;
12
+ maxFrameLength?: number;
13
+ /** Reports subscriber failures without allowing them to corrupt client state. */
14
+ onListenerError?: ListenerErrorHandler;
15
+ }
16
+ export interface CreateSessionOptions {
17
+ cwd?: string;
18
+ name?: string;
19
+ model?: ModelRef;
20
+ thinkingLevel?: ThinkingLevel;
21
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/unix.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ByteTransportFactory } from "./transport.ts";
2
+ export interface UnixTransportOptions {
3
+ path: string;
4
+ maxPendingBytes?: number;
5
+ }
6
+ /** Creates fresh Unix-domain socket transports for DiffexClient connection attempts in Node-compatible runtimes. */
7
+ export declare function createUnixTransportFactory(options: UnixTransportOptions): ByteTransportFactory;