@m4ike1/ion-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import { type ServerHello, type ServerMessage } from "@m4ike1/ion-protocol";
2
+ import type { ByteTransportFactory } from "./transport.ts";
3
+ import type { ConnectionState, ConnectionStateChange } from "./types.ts";
4
+ interface ConnectionOptions {
5
+ transportFactory: ByteTransportFactory;
6
+ serverId: string;
7
+ maxFrameLength?: number;
8
+ onHandshake(hello: ServerHello): void;
9
+ onMessage(message: Exclude<ServerMessage, {
10
+ type: "hello" | "hello_error";
11
+ }>): void;
12
+ onStateChange(change: ConnectionStateChange): void;
13
+ }
14
+ export declare class Connection {
15
+ #private;
16
+ constructor(options: ConnectionOptions);
17
+ get state(): ConnectionState;
18
+ get maxFrameLength(): number;
19
+ connect(): Promise<ServerHello>;
20
+ disconnect(reason?: string | Error): void;
21
+ fail(error: Error): void;
22
+ send(frame: Uint8Array): void;
23
+ }
24
+ export {};
25
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAiB,oBAAoB,EAAyB,MAAM,gBAAgB,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAmBzE,UAAU,iBAAiB;IAC1B,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,OAAO,GAAG,aAAa,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IACpF,aAAa,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACnD;AAED,qBAAa,UAAU;;IAMtB,YAAY,OAAO,EAAE,iBAAiB,EAUrC;IAED,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,CAwB9B;IAED,UAAU,CAAC,MAAM,GAAE,MAAM,GAAG,KAA6B,GAAG,IAAI,CAG/D;IAED,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAEvB;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAgB5B;CA+HD","sourcesContent":["import {\n\tDEFAULT_MAX_FRAME_LENGTH,\n\tencodeClientMessage,\n\tPROTOCOL_VERSION,\n\tProtocolValidationError,\n\ttype ServerHello,\n\ttype ServerMessage,\n\tServerMessageDecoder,\n} from \"@m4ike1/ion-protocol\";\nimport { DisconnectedError, ServerError, toDisconnectedError, toError } from \"./errors.ts\";\nimport { createPromiseResolvers, type PromiseResolvers } from \"./promise.ts\";\nimport type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from \"./transport.ts\";\nimport type { ConnectionState, ConnectionStateChange } from \"./types.ts\";\n\nconst MAX_UINT32 = 0xffff_ffff;\n\ntype ActiveConnection = {\n\tid: number;\n\tdecoder: ServerMessageDecoder;\n\ttransport?: ByteTransport;\n};\n\ntype ConnectionLifecycle =\n\t| { state: \"disconnected\" }\n\t| ({ state: \"connecting\"; handshake: PromiseResolvers<ServerHello> } & ActiveConnection)\n\t| ({\n\t\t\tstate: \"connected\";\n\t\t\ttransport: ByteTransport;\n\t\t\thandshake: PromiseResolvers<ServerHello> | undefined;\n\t } & ActiveConnection);\n\ninterface ConnectionOptions {\n\ttransportFactory: ByteTransportFactory;\n\tserverId: string;\n\tmaxFrameLength?: number;\n\tonHandshake(hello: ServerHello): void;\n\tonMessage(message: Exclude<ServerMessage, { type: \"hello\" | \"hello_error\" }>): void;\n\tonStateChange(change: ConnectionStateChange): void;\n}\n\nexport class Connection {\n\treadonly #options: ConnectionOptions;\n\treadonly #maxFrameLength: number;\n\t#lifecycle: ConnectionLifecycle = { state: \"disconnected\" };\n\t#sequence = 0;\n\n\tconstructor(options: ConnectionOptions) {\n\t\tthis.#options = options;\n\t\tthis.#maxFrameLength = options.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;\n\t\tif (\n\t\t\t!Number.isSafeInteger(this.#maxFrameLength) ||\n\t\t\tthis.#maxFrameLength <= 0 ||\n\t\t\tthis.#maxFrameLength > MAX_UINT32\n\t\t) {\n\t\t\tthrow new TypeError(`Client maxFrameLength must be an integer between 1 and ${MAX_UINT32}`);\n\t\t}\n\t}\n\n\tget state(): ConnectionState {\n\t\treturn this.#lifecycle.state;\n\t}\n\n\tget maxFrameLength(): number {\n\t\treturn this.#maxFrameLength;\n\t}\n\n\tconnect(): Promise<ServerHello> {\n\t\tif (this.#lifecycle.state !== \"disconnected\") {\n\t\t\treturn Promise.reject(new DisconnectedError(`Client is already ${this.#lifecycle.state}`));\n\t\t}\n\t\tconst id = ++this.#sequence;\n\t\tconst handshake = createPromiseResolvers<ServerHello>();\n\t\tthis.#lifecycle = {\n\t\t\tstate: \"connecting\",\n\t\t\tid,\n\t\t\tdecoder: new ServerMessageDecoder({ maxFrameLength: this.#maxFrameLength }),\n\t\t\thandshake,\n\t\t};\n\t\tthis.#options.onStateChange({ state: \"connecting\" });\n\t\tconst handlers = {\n\t\t\tonData: (chunk) => this.#handleData(id, chunk),\n\t\t\tonClose: () => {\n\t\t\t\tif (this.#isCurrent(id)) this.#handleClose();\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tif (this.#isCurrent(id)) this.#failAndClose(toDisconnectedError(error));\n\t\t\t},\n\t\t} satisfies ByteTransportHandlers;\n\t\tvoid this.#openTransport(id, handlers);\n\t\treturn handshake.promise;\n\t}\n\n\tdisconnect(reason: string | Error = \"Client disconnected\"): void {\n\t\tif (this.#lifecycle.state === \"disconnected\") return;\n\t\tthis.#failAndClose(typeof reason === \"string\" ? new DisconnectedError(reason) : reason);\n\t}\n\n\tfail(error: Error): void {\n\t\tthis.#failAndClose(error);\n\t}\n\n\tsend(frame: Uint8Array): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state !== \"connected\") throw new DisconnectedError();\n\t\tlet sending: Promise<void>;\n\t\ttry {\n\t\t\tsending = lifecycle.transport.send(frame);\n\t\t} catch (error) {\n\t\t\tthis.#failAndClose(toDisconnectedError(error));\n\t\t\treturn;\n\t\t}\n\t\tvoid sending.catch((error: unknown) => {\n\t\t\tconst current = this.#lifecycle;\n\t\t\tif (current.state !== \"disconnected\" && current.transport === lifecycle.transport) {\n\t\t\t\tthis.#failAndClose(toDisconnectedError(error));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync #openTransport(id: number, handlers: ByteTransportHandlers): Promise<void> {\n\t\tlet transport: ByteTransport;\n\t\ttry {\n\t\t\ttransport = await this.#options.transportFactory(handlers);\n\t\t} catch (error) {\n\t\t\tif (this.#isCurrent(id)) this.#fail(toDisconnectedError(error));\n\t\t\treturn;\n\t\t}\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state !== \"connecting\" || lifecycle.id !== id) {\n\t\t\ttransport.close();\n\t\t\treturn;\n\t\t}\n\t\tthis.#lifecycle = { ...lifecycle, transport };\n\t\ttry {\n\t\t\tawait transport.send(\n\t\t\t\tencodeClientMessage({ type: \"hello\", version: PROTOCOL_VERSION }, { maxFrameLength: this.#maxFrameLength }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (this.#isCurrent(id)) this.#failAndClose(toDisconnectedError(error));\n\t\t}\n\t}\n\n\t#handleData(id: number, chunk: Uint8Array): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\" || lifecycle.id !== id) return;\n\t\tif (lifecycle.state === \"connecting\" && !lifecycle.transport) {\n\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Received server data before the client hello was sent\"));\n\t\t\treturn;\n\t\t}\n\t\tlet messages: ServerMessage[];\n\t\ttry {\n\t\t\tmessages = lifecycle.decoder.push(chunk);\n\t\t} catch (error) {\n\t\t\tthis.#failAndClose(toError(error));\n\t\t\treturn;\n\t\t}\n\t\tfor (const message of messages) {\n\t\t\tif (this.#lifecycle.state === \"disconnected\") return;\n\t\t\tthis.#handleMessage(message);\n\t\t}\n\t}\n\n\t#handleMessage(message: ServerMessage): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"connecting\") {\n\t\t\tif (message.type === \"hello_error\") {\n\t\t\t\tthis.#failAndClose(new ServerError(message.error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== \"hello\") {\n\t\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Expected server hello as first message\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.serverId !== this.#options.serverId) {\n\t\t\t\tthis.#failAndClose(\n\t\t\t\t\tnew ProtocolValidationError(\n\t\t\t\t\t\t`Connected server ${JSON.stringify(message.serverId)} does not match ${JSON.stringify(this.#options.serverId)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!lifecycle.transport) {\n\t\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Received server hello before the client hello was sent\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connected = {\n\t\t\t\tstate: \"connected\",\n\t\t\t\tid: lifecycle.id,\n\t\t\t\tdecoder: lifecycle.decoder,\n\t\t\t\ttransport: lifecycle.transport,\n\t\t\t\thandshake: lifecycle.handshake,\n\t\t\t} satisfies Extract<ConnectionLifecycle, { state: \"connected\" }>;\n\t\t\tthis.#lifecycle = connected;\n\t\t\ttry {\n\t\t\t\tthis.#options.onHandshake(message);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.#lifecycle === connected) this.#failAndClose(toError(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.#lifecycle !== connected) return;\n\t\t\tthis.#options.onStateChange({ state: \"connected\" });\n\t\t\tif (this.#lifecycle !== connected) return;\n\t\t\tthis.#lifecycle = { ...connected, handshake: undefined };\n\t\t\tlifecycle.handshake.resolve(message);\n\t\t\treturn;\n\t\t}\n\t\tif (lifecycle.state !== \"connected\") return;\n\t\tif (message.type === \"hello\" || message.type === \"hello_error\") {\n\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Unexpected handshake message\"));\n\t\t\treturn;\n\t\t}\n\t\tthis.#options.onMessage(message);\n\t}\n\n\t#handleClose(): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\") return;\n\t\tlet error: Error = new DisconnectedError(\"Byte transport closed\");\n\t\ttry {\n\t\t\tlifecycle.decoder.end();\n\t\t} catch (decoderError) {\n\t\t\terror = toError(decoderError);\n\t\t}\n\t\tthis.#fail(error);\n\t}\n\n\t#failAndClose(error: Error): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tconst transport = lifecycle.state === \"disconnected\" ? undefined : lifecycle.transport;\n\t\tthis.#fail(error);\n\t\ttransport?.close();\n\t}\n\n\t#fail(error: Error): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\") return;\n\t\tthis.#lifecycle = { state: \"disconnected\" };\n\t\tlifecycle.handshake?.reject(error);\n\t\tthis.#options.onStateChange({ state: \"disconnected\", error });\n\t}\n\n\t#isCurrent(id: number): boolean {\n\t\treturn this.#lifecycle.state !== \"disconnected\" && this.#lifecycle.id === id;\n\t}\n}\n"]}
@@ -0,0 +1,208 @@
1
+ import { DEFAULT_MAX_FRAME_LENGTH, encodeClientMessage, PROTOCOL_VERSION, ProtocolValidationError, ServerMessageDecoder, } from "@m4ike1/ion-protocol";
2
+ import { DisconnectedError, ServerError, 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(`Client 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 DisconnectedError(`Client 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 DisconnectedError(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 DisconnectedError();
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 ServerError(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 (message.serverId !== this.#options.serverId) {
137
+ this.#failAndClose(new ProtocolValidationError(`Connected server ${JSON.stringify(message.serverId)} does not match ${JSON.stringify(this.#options.serverId)}`));
138
+ return;
139
+ }
140
+ if (!lifecycle.transport) {
141
+ this.#failAndClose(new ProtocolValidationError("Received server hello before the client hello was sent"));
142
+ return;
143
+ }
144
+ const connected = {
145
+ state: "connected",
146
+ id: lifecycle.id,
147
+ decoder: lifecycle.decoder,
148
+ transport: lifecycle.transport,
149
+ handshake: lifecycle.handshake,
150
+ };
151
+ this.#lifecycle = connected;
152
+ try {
153
+ this.#options.onHandshake(message);
154
+ }
155
+ catch (error) {
156
+ if (this.#lifecycle === connected)
157
+ this.#failAndClose(toError(error));
158
+ return;
159
+ }
160
+ if (this.#lifecycle !== connected)
161
+ return;
162
+ this.#options.onStateChange({ state: "connected" });
163
+ if (this.#lifecycle !== connected)
164
+ return;
165
+ this.#lifecycle = { ...connected, handshake: undefined };
166
+ lifecycle.handshake.resolve(message);
167
+ return;
168
+ }
169
+ if (lifecycle.state !== "connected")
170
+ return;
171
+ if (message.type === "hello" || message.type === "hello_error") {
172
+ this.#failAndClose(new ProtocolValidationError("Unexpected handshake message"));
173
+ return;
174
+ }
175
+ this.#options.onMessage(message);
176
+ }
177
+ #handleClose() {
178
+ const lifecycle = this.#lifecycle;
179
+ if (lifecycle.state === "disconnected")
180
+ return;
181
+ let error = new DisconnectedError("Byte transport closed");
182
+ try {
183
+ lifecycle.decoder.end();
184
+ }
185
+ catch (decoderError) {
186
+ error = toError(decoderError);
187
+ }
188
+ this.#fail(error);
189
+ }
190
+ #failAndClose(error) {
191
+ const lifecycle = this.#lifecycle;
192
+ const transport = lifecycle.state === "disconnected" ? undefined : lifecycle.transport;
193
+ this.#fail(error);
194
+ transport?.close();
195
+ }
196
+ #fail(error) {
197
+ const lifecycle = this.#lifecycle;
198
+ if (lifecycle.state === "disconnected")
199
+ return;
200
+ this.#lifecycle = { state: "disconnected" };
201
+ lifecycle.handshake?.reject(error);
202
+ this.#options.onStateChange({ state: "disconnected", error });
203
+ }
204
+ #isCurrent(id) {
205
+ return this.#lifecycle.state !== "disconnected" && this.#lifecycle.id === id;
206
+ }
207
+ }
208
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,wBAAwB,EACxB,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,EAGvB,oBAAoB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,sBAAsB,EAAyB,MAAM,cAAc,CAAC;AAI7E,MAAM,UAAU,GAAG,WAAW,CAAC;AA0B/B,MAAM,OAAO,UAAU;IACb,QAAQ,CAAoB;IAC5B,eAAe,CAAS;IACjC,UAAU,GAAwB,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAC5D,SAAS,GAAG,CAAC,CAAC;IAEd,YAAY,OAA0B,EAAE;QACvC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,cAAc,IAAI,wBAAwB,CAAC;QAC1E,IACC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC;YAC3C,IAAI,CAAC,eAAe,IAAI,CAAC;YACzB,IAAI,CAAC,eAAe,GAAG,UAAU,EAChC,CAAC;YACF,MAAM,IAAI,SAAS,CAAC,0DAA0D,UAAU,EAAE,CAAC,CAAC;QAC7F,CAAC;IAAA,CACD;IAED,IAAI,KAAK,GAAoB;QAC5B,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;IAAA,CAC7B;IAED,IAAI,cAAc,GAAW;QAC5B,OAAO,IAAI,CAAC,eAAe,CAAC;IAAA,CAC5B;IAED,OAAO,GAAyB;QAC/B,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,cAAc,EAAE,CAAC;YAC9C,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,iBAAiB,CAAC,qBAAqB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC;QAC5B,MAAM,SAAS,GAAG,sBAAsB,EAAe,CAAC;QACxD,IAAI,CAAC,UAAU,GAAG;YACjB,KAAK,EAAE,YAAY;YACnB,EAAE;YACF,OAAO,EAAE,IAAI,oBAAoB,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3E,SAAS;SACT,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAG;YAChB,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,CAAC;YAC9C,OAAO,EAAE,GAAG,EAAE,CAAC;gBACd,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;oBAAE,IAAI,CAAC,YAAY,EAAE,CAAC;YAAA,CAC7C;YACD,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;gBACnB,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;oBAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;YAAA,CACxE;SAC+B,CAAC;QAClC,KAAK,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACvC,OAAO,SAAS,CAAC,OAAO,CAAC;IAAA,CACzB;IAED,UAAU,CAAC,MAAM,GAAmB,qBAAqB,EAAQ;QAChE,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,cAAc;YAAE,OAAO;QACrD,IAAI,CAAC,aAAa,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAAA,CACxF;IAED,IAAI,CAAC,KAAY,EAAQ;QACxB,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAAA,CAC1B;IAED,IAAI,CAAC,KAAiB,EAAQ;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,WAAW;YAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACnE,IAAI,OAAsB,CAAC;QAC3B,IAAI,CAAC;YACJ,OAAO,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;YAC/C,OAAO;QACR,CAAC;QACD,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC;YAChC,IAAI,OAAO,CAAC,KAAK,KAAK,cAAc,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,SAAS,EAAE,CAAC;gBACnF,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;YAChD,CAAC;QAAA,CACD,CAAC,CAAC;IAAA,CACH;IAED,KAAK,CAAC,cAAc,CAAC,EAAU,EAAE,QAA+B,EAAiB;QAChF,IAAI,SAAwB,CAAC;QAC7B,IAAI,CAAC;YACJ,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;YAChE,OAAO;QACR,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,YAAY,IAAI,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7D,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,OAAO;QACR,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,EAAE,GAAG,SAAS,EAAE,SAAS,EAAE,CAAC;QAC9C,IAAI,CAAC;YACJ,MAAM,SAAS,CAAC,IAAI,CACnB,mBAAmB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAC3G,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;QACzE,CAAC;IAAA,CACD;IAED,WAAW,CAAC,EAAU,EAAE,KAAiB,EAAQ;QAChD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,cAAc,IAAI,SAAS,CAAC,EAAE,KAAK,EAAE;YAAE,OAAO;QACtE,IAAI,SAAS,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;YAC9D,IAAI,CAAC,aAAa,CAAC,IAAI,uBAAuB,CAAC,uDAAuD,CAAC,CAAC,CAAC;YACzG,OAAO;QACR,CAAC;QACD,IAAI,QAAyB,CAAC;QAC9B,IAAI,CAAC;YACJ,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YACnC,OAAO;QACR,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,cAAc;gBAAE,OAAO;YACrD,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;IAAA,CACD;IAED,cAAc,CAAC,OAAsB,EAAQ;QAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;YACtC,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBACpC,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;gBACnD,OAAO;YACR,CAAC;YACD,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC9B,IAAI,CAAC,aAAa,CAAC,IAAI,uBAAuB,CAAC,wCAAwC,CAAC,CAAC,CAAC;gBAC1F,OAAO;YACR,CAAC;YACD,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACjD,IAAI,CAAC,aAAa,CACjB,IAAI,uBAAuB,CAC1B,oBAAoB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAC/G,CACD,CAAC;gBACF,OAAO;YACR,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;gBAC1B,IAAI,CAAC,aAAa,CAAC,IAAI,uBAAuB,CAAC,wDAAwD,CAAC,CAAC,CAAC;gBAC1G,OAAO;YACR,CAAC;YACD,MAAM,SAAS,GAAG;gBACjB,KAAK,EAAE,WAAW;gBAClB,EAAE,EAAE,SAAS,CAAC,EAAE;gBAChB,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,SAAS,EAAE,SAAS,CAAC,SAAS;gBAC9B,SAAS,EAAE,SAAS,CAAC,SAAS;aACiC,CAAC;YACjE,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;YAC5B,IAAI,CAAC;gBACJ,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;oBAAE,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;gBACtE,OAAO;YACR,CAAC;YACD,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;gBAAE,OAAO;YAC1C,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;YACpD,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;gBAAE,OAAO;YAC1C,IAAI,CAAC,UAAU,GAAG,EAAE,GAAG,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;YACzD,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACrC,OAAO;QACR,CAAC;QACD,IAAI,SAAS,CAAC,KAAK,KAAK,WAAW;YAAE,OAAO;QAC5C,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAChE,IAAI,CAAC,aAAa,CAAC,IAAI,uBAAuB,CAAC,8BAA8B,CAAC,CAAC,CAAC;YAChF,OAAO;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAAA,CACjC;IAED,YAAY,GAAS;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,cAAc;YAAE,OAAO;QAC/C,IAAI,KAAK,GAAU,IAAI,iBAAiB,CAAC,uBAAuB,CAAC,CAAC;QAClE,IAAI,CAAC;YACJ,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACvB,KAAK,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAAA,CAClB;IAED,aAAa,CAAC,KAAY,EAAQ;QACjC,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;QACvF,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAClB,SAAS,EAAE,KAAK,EAAE,CAAC;IAAA,CACnB;IAED,KAAK,CAAC,KAAY,EAAQ;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,SAAS,CAAC,KAAK,KAAK,cAAc;YAAE,OAAO;QAC/C,IAAI,CAAC,UAAU,GAAG,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;QAC5C,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IAAA,CAC9D;IAED,UAAU,CAAC,EAAU,EAAW;QAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,cAAc,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,CAAC;IAAA,CAC7E;CACD","sourcesContent":["import {\n\tDEFAULT_MAX_FRAME_LENGTH,\n\tencodeClientMessage,\n\tPROTOCOL_VERSION,\n\tProtocolValidationError,\n\ttype ServerHello,\n\ttype ServerMessage,\n\tServerMessageDecoder,\n} from \"@m4ike1/ion-protocol\";\nimport { DisconnectedError, ServerError, toDisconnectedError, toError } from \"./errors.ts\";\nimport { createPromiseResolvers, type PromiseResolvers } from \"./promise.ts\";\nimport type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from \"./transport.ts\";\nimport type { ConnectionState, ConnectionStateChange } from \"./types.ts\";\n\nconst MAX_UINT32 = 0xffff_ffff;\n\ntype ActiveConnection = {\n\tid: number;\n\tdecoder: ServerMessageDecoder;\n\ttransport?: ByteTransport;\n};\n\ntype ConnectionLifecycle =\n\t| { state: \"disconnected\" }\n\t| ({ state: \"connecting\"; handshake: PromiseResolvers<ServerHello> } & ActiveConnection)\n\t| ({\n\t\t\tstate: \"connected\";\n\t\t\ttransport: ByteTransport;\n\t\t\thandshake: PromiseResolvers<ServerHello> | undefined;\n\t } & ActiveConnection);\n\ninterface ConnectionOptions {\n\ttransportFactory: ByteTransportFactory;\n\tserverId: string;\n\tmaxFrameLength?: number;\n\tonHandshake(hello: ServerHello): void;\n\tonMessage(message: Exclude<ServerMessage, { type: \"hello\" | \"hello_error\" }>): void;\n\tonStateChange(change: ConnectionStateChange): void;\n}\n\nexport class Connection {\n\treadonly #options: ConnectionOptions;\n\treadonly #maxFrameLength: number;\n\t#lifecycle: ConnectionLifecycle = { state: \"disconnected\" };\n\t#sequence = 0;\n\n\tconstructor(options: ConnectionOptions) {\n\t\tthis.#options = options;\n\t\tthis.#maxFrameLength = options.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;\n\t\tif (\n\t\t\t!Number.isSafeInteger(this.#maxFrameLength) ||\n\t\t\tthis.#maxFrameLength <= 0 ||\n\t\t\tthis.#maxFrameLength > MAX_UINT32\n\t\t) {\n\t\t\tthrow new TypeError(`Client maxFrameLength must be an integer between 1 and ${MAX_UINT32}`);\n\t\t}\n\t}\n\n\tget state(): ConnectionState {\n\t\treturn this.#lifecycle.state;\n\t}\n\n\tget maxFrameLength(): number {\n\t\treturn this.#maxFrameLength;\n\t}\n\n\tconnect(): Promise<ServerHello> {\n\t\tif (this.#lifecycle.state !== \"disconnected\") {\n\t\t\treturn Promise.reject(new DisconnectedError(`Client is already ${this.#lifecycle.state}`));\n\t\t}\n\t\tconst id = ++this.#sequence;\n\t\tconst handshake = createPromiseResolvers<ServerHello>();\n\t\tthis.#lifecycle = {\n\t\t\tstate: \"connecting\",\n\t\t\tid,\n\t\t\tdecoder: new ServerMessageDecoder({ maxFrameLength: this.#maxFrameLength }),\n\t\t\thandshake,\n\t\t};\n\t\tthis.#options.onStateChange({ state: \"connecting\" });\n\t\tconst handlers = {\n\t\t\tonData: (chunk) => this.#handleData(id, chunk),\n\t\t\tonClose: () => {\n\t\t\t\tif (this.#isCurrent(id)) this.#handleClose();\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tif (this.#isCurrent(id)) this.#failAndClose(toDisconnectedError(error));\n\t\t\t},\n\t\t} satisfies ByteTransportHandlers;\n\t\tvoid this.#openTransport(id, handlers);\n\t\treturn handshake.promise;\n\t}\n\n\tdisconnect(reason: string | Error = \"Client disconnected\"): void {\n\t\tif (this.#lifecycle.state === \"disconnected\") return;\n\t\tthis.#failAndClose(typeof reason === \"string\" ? new DisconnectedError(reason) : reason);\n\t}\n\n\tfail(error: Error): void {\n\t\tthis.#failAndClose(error);\n\t}\n\n\tsend(frame: Uint8Array): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state !== \"connected\") throw new DisconnectedError();\n\t\tlet sending: Promise<void>;\n\t\ttry {\n\t\t\tsending = lifecycle.transport.send(frame);\n\t\t} catch (error) {\n\t\t\tthis.#failAndClose(toDisconnectedError(error));\n\t\t\treturn;\n\t\t}\n\t\tvoid sending.catch((error: unknown) => {\n\t\t\tconst current = this.#lifecycle;\n\t\t\tif (current.state !== \"disconnected\" && current.transport === lifecycle.transport) {\n\t\t\t\tthis.#failAndClose(toDisconnectedError(error));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync #openTransport(id: number, handlers: ByteTransportHandlers): Promise<void> {\n\t\tlet transport: ByteTransport;\n\t\ttry {\n\t\t\ttransport = await this.#options.transportFactory(handlers);\n\t\t} catch (error) {\n\t\t\tif (this.#isCurrent(id)) this.#fail(toDisconnectedError(error));\n\t\t\treturn;\n\t\t}\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state !== \"connecting\" || lifecycle.id !== id) {\n\t\t\ttransport.close();\n\t\t\treturn;\n\t\t}\n\t\tthis.#lifecycle = { ...lifecycle, transport };\n\t\ttry {\n\t\t\tawait transport.send(\n\t\t\t\tencodeClientMessage({ type: \"hello\", version: PROTOCOL_VERSION }, { maxFrameLength: this.#maxFrameLength }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (this.#isCurrent(id)) this.#failAndClose(toDisconnectedError(error));\n\t\t}\n\t}\n\n\t#handleData(id: number, chunk: Uint8Array): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\" || lifecycle.id !== id) return;\n\t\tif (lifecycle.state === \"connecting\" && !lifecycle.transport) {\n\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Received server data before the client hello was sent\"));\n\t\t\treturn;\n\t\t}\n\t\tlet messages: ServerMessage[];\n\t\ttry {\n\t\t\tmessages = lifecycle.decoder.push(chunk);\n\t\t} catch (error) {\n\t\t\tthis.#failAndClose(toError(error));\n\t\t\treturn;\n\t\t}\n\t\tfor (const message of messages) {\n\t\t\tif (this.#lifecycle.state === \"disconnected\") return;\n\t\t\tthis.#handleMessage(message);\n\t\t}\n\t}\n\n\t#handleMessage(message: ServerMessage): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"connecting\") {\n\t\t\tif (message.type === \"hello_error\") {\n\t\t\t\tthis.#failAndClose(new ServerError(message.error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== \"hello\") {\n\t\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Expected server hello as first message\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.serverId !== this.#options.serverId) {\n\t\t\t\tthis.#failAndClose(\n\t\t\t\t\tnew ProtocolValidationError(\n\t\t\t\t\t\t`Connected server ${JSON.stringify(message.serverId)} does not match ${JSON.stringify(this.#options.serverId)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!lifecycle.transport) {\n\t\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Received server hello before the client hello was sent\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connected = {\n\t\t\t\tstate: \"connected\",\n\t\t\t\tid: lifecycle.id,\n\t\t\t\tdecoder: lifecycle.decoder,\n\t\t\t\ttransport: lifecycle.transport,\n\t\t\t\thandshake: lifecycle.handshake,\n\t\t\t} satisfies Extract<ConnectionLifecycle, { state: \"connected\" }>;\n\t\t\tthis.#lifecycle = connected;\n\t\t\ttry {\n\t\t\t\tthis.#options.onHandshake(message);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.#lifecycle === connected) this.#failAndClose(toError(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.#lifecycle !== connected) return;\n\t\t\tthis.#options.onStateChange({ state: \"connected\" });\n\t\t\tif (this.#lifecycle !== connected) return;\n\t\t\tthis.#lifecycle = { ...connected, handshake: undefined };\n\t\t\tlifecycle.handshake.resolve(message);\n\t\t\treturn;\n\t\t}\n\t\tif (lifecycle.state !== \"connected\") return;\n\t\tif (message.type === \"hello\" || message.type === \"hello_error\") {\n\t\t\tthis.#failAndClose(new ProtocolValidationError(\"Unexpected handshake message\"));\n\t\t\treturn;\n\t\t}\n\t\tthis.#options.onMessage(message);\n\t}\n\n\t#handleClose(): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\") return;\n\t\tlet error: Error = new DisconnectedError(\"Byte transport closed\");\n\t\ttry {\n\t\t\tlifecycle.decoder.end();\n\t\t} catch (decoderError) {\n\t\t\terror = toError(decoderError);\n\t\t}\n\t\tthis.#fail(error);\n\t}\n\n\t#failAndClose(error: Error): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tconst transport = lifecycle.state === \"disconnected\" ? undefined : lifecycle.transport;\n\t\tthis.#fail(error);\n\t\ttransport?.close();\n\t}\n\n\t#fail(error: Error): void {\n\t\tconst lifecycle = this.#lifecycle;\n\t\tif (lifecycle.state === \"disconnected\") return;\n\t\tthis.#lifecycle = { state: \"disconnected\" };\n\t\tlifecycle.handshake?.reject(error);\n\t\tthis.#options.onStateChange({ state: \"disconnected\", error });\n\t}\n\n\t#isCurrent(id: number): boolean {\n\t\treturn this.#lifecycle.state !== \"disconnected\" && this.#lifecycle.id === id;\n\t}\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { ProtocolError, ProtocolErrorCode } from "@m4ike1/ion-protocol";
2
+ export declare class ServerError extends Error {
3
+ readonly code: ProtocolErrorCode;
4
+ constructor(error: ProtocolError);
5
+ }
6
+ export declare class DisconnectedError extends Error {
7
+ constructor(message?: string, cause?: Error);
8
+ }
9
+ export declare class ClientDisposedError extends Error {
10
+ constructor();
11
+ }
12
+ export declare function toError(error: unknown): Error;
13
+ export declare function toDisconnectedError(error: unknown): DisconnectedError;
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE7E,qBAAa,WAAY,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAEjC,YAAY,KAAK,EAAE,aAAa,EAI/B;CACD;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,YAAY,OAAO,SAA2B,EAAE,KAAK,CAAC,EAAE,KAAK,EAG5D;CACD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,cAGC;CACD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAE7C;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAGrE","sourcesContent":["import type { ProtocolError, ProtocolErrorCode } from \"@m4ike1/ion-protocol\";\n\nexport class ServerError extends Error {\n\treadonly code: ProtocolErrorCode;\n\n\tconstructor(error: ProtocolError) {\n\t\tsuper(error.message);\n\t\tthis.name = \"ServerError\";\n\t\tthis.code = error.code;\n\t}\n}\n\nexport class DisconnectedError extends Error {\n\tconstructor(message = \"Client is disconnected\", cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"DisconnectedError\";\n\t}\n}\n\nexport class ClientDisposedError extends Error {\n\tconstructor() {\n\t\tsuper(\"Client is disposed\");\n\t\tthis.name = \"ClientDisposedError\";\n\t}\n}\n\nexport function toError(error: unknown): Error {\n\treturn error instanceof Error ? error : new Error(String(error));\n}\n\nexport function toDisconnectedError(error: unknown): DisconnectedError {\n\tconst cause = toError(error);\n\treturn cause instanceof DisconnectedError ? cause : new DisconnectedError(cause.message, cause);\n}\n"]}
package/dist/errors.js ADDED
@@ -0,0 +1,28 @@
1
+ export class ServerError extends Error {
2
+ code;
3
+ constructor(error) {
4
+ super(error.message);
5
+ this.name = "ServerError";
6
+ this.code = error.code;
7
+ }
8
+ }
9
+ export class DisconnectedError extends Error {
10
+ constructor(message = "Client is disconnected", cause) {
11
+ super(message, cause === undefined ? undefined : { cause });
12
+ this.name = "DisconnectedError";
13
+ }
14
+ }
15
+ export class ClientDisposedError extends Error {
16
+ constructor() {
17
+ super("Client is disposed");
18
+ this.name = "ClientDisposedError";
19
+ }
20
+ }
21
+ export function toError(error) {
22
+ return error instanceof Error ? error : new Error(String(error));
23
+ }
24
+ export function toDisconnectedError(error) {
25
+ const cause = toError(error);
26
+ return cause instanceof DisconnectedError ? cause : new DisconnectedError(cause.message, cause);
27
+ }
28
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC5B,IAAI,CAAoB;IAEjC,YAAY,KAAoB,EAAE;QACjC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IAAA,CACvB;CACD;AAED,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC3C,YAAY,OAAO,GAAG,wBAAwB,EAAE,KAAa,EAAE;QAC9D,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAAA,CAChC;CACD;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC7C,cAAc;QACb,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IAAA,CAClC;CACD;AAED,MAAM,UAAU,OAAO,CAAC,KAAc,EAAS;IAC9C,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAAA,CACjE;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAc,EAAqB;IACtE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,KAAK,YAAY,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAAA,CAChG","sourcesContent":["import type { ProtocolError, ProtocolErrorCode } from \"@m4ike1/ion-protocol\";\n\nexport class ServerError extends Error {\n\treadonly code: ProtocolErrorCode;\n\n\tconstructor(error: ProtocolError) {\n\t\tsuper(error.message);\n\t\tthis.name = \"ServerError\";\n\t\tthis.code = error.code;\n\t}\n}\n\nexport class DisconnectedError extends Error {\n\tconstructor(message = \"Client is disconnected\", cause?: Error) {\n\t\tsuper(message, cause === undefined ? undefined : { cause });\n\t\tthis.name = \"DisconnectedError\";\n\t}\n}\n\nexport class ClientDisposedError extends Error {\n\tconstructor() {\n\t\tsuper(\"Client is disposed\");\n\t\tthis.name = \"ClientDisposedError\";\n\t}\n}\n\nexport function toError(error: unknown): Error {\n\treturn error instanceof Error ? error : new Error(String(error));\n}\n\nexport function toDisconnectedError(error: unknown): DisconnectedError {\n\tconst cause = toError(error);\n\treturn cause instanceof DisconnectedError ? cause : new DisconnectedError(cause.message, cause);\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export { Client, createClientServiceTransport } from "./client.ts";
2
+ export { ClientDisposedError, DisconnectedError, ServerError } from "./errors.ts";
3
+ export type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from "./transport.ts";
4
+ export type { AttachmentChangeListener, ClientOptions, ConnectionState, ConnectionStateChange, ListenerErrorHandler, ServiceSubscription, Unsubscribe, } from "./types.ts";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClF,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACjG,YAAY,EACX,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,GACX,MAAM,YAAY,CAAC","sourcesContent":["export { Client, createClientServiceTransport } from \"./client.ts\";\nexport { ClientDisposedError, DisconnectedError, ServerError } from \"./errors.ts\";\nexport type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from \"./transport.ts\";\nexport type {\n\tAttachmentChangeListener,\n\tClientOptions,\n\tConnectionState,\n\tConnectionStateChange,\n\tListenerErrorHandler,\n\tServiceSubscription,\n\tUnsubscribe,\n} from \"./types.ts\";\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Client, createClientServiceTransport } from "./client.js";
2
+ export { ClientDisposedError, DisconnectedError, ServerError } from "./errors.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { Client, createClientServiceTransport } from \"./client.ts\";\nexport { ClientDisposedError, DisconnectedError, ServerError } from \"./errors.ts\";\nexport type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from \"./transport.ts\";\nexport type {\n\tAttachmentChangeListener,\n\tClientOptions,\n\tConnectionState,\n\tConnectionStateChange,\n\tListenerErrorHandler,\n\tServiceSubscription,\n\tUnsubscribe,\n} from \"./types.ts\";\n"]}
@@ -0,0 +1,8 @@
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>;
8
+ //# sourceMappingURL=promise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../src/promise.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACzC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B;AAED,kHAAkH;AAClH,wBAAgB,sBAAsB,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,CAQ/D","sourcesContent":["export interface PromiseResolvers<T> {\n\tpromise: Promise<T>;\n\tresolve(value: T | PromiseLike<T>): void;\n\treject(reason?: unknown): void;\n}\n\n/** Remove in favor of `Promise.withResolvers()` when the repository's TypeScript lib baseline moves to ES2024. */\nexport function createPromiseResolvers<T>(): PromiseResolvers<T> {\n\tlet resolve!: PromiseResolvers<T>[\"resolve\"];\n\tlet reject!: PromiseResolvers<T>[\"reject\"];\n\tconst promise = new Promise<T>((resolvePromise, rejectPromise) => {\n\t\tresolve = resolvePromise;\n\t\treject = rejectPromise;\n\t});\n\treturn { promise, resolve, reject };\n}\n"]}
@@ -0,0 +1,11 @@
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
+ }
11
+ //# sourceMappingURL=promise.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promise.js","sourceRoot":"","sources":["../src/promise.ts"],"names":[],"mappings":"AAMA,kHAAkH;AAClH,MAAM,UAAU,sBAAsB,GAA2B;IAChE,IAAI,OAAwC,CAAC;IAC7C,IAAI,MAAsC,CAAC;IAC3C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,cAAc,EAAE,aAAa,EAAE,EAAE,CAAC;QACjE,OAAO,GAAG,cAAc,CAAC;QACzB,MAAM,GAAG,aAAa,CAAC;IAAA,CACvB,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAAA,CACpC","sourcesContent":["export interface PromiseResolvers<T> {\n\tpromise: Promise<T>;\n\tresolve(value: T | PromiseLike<T>): void;\n\treject(reason?: unknown): void;\n}\n\n/** Remove in favor of `Promise.withResolvers()` when the repository's TypeScript lib baseline moves to ES2024. */\nexport function createPromiseResolvers<T>(): PromiseResolvers<T> {\n\tlet resolve!: PromiseResolvers<T>[\"resolve\"];\n\tlet reject!: PromiseResolvers<T>[\"reject\"];\n\tconst promise = new Promise<T>((resolvePromise, rejectPromise) => {\n\t\tresolve = resolvePromise;\n\t\treject = rejectPromise;\n\t});\n\treturn { promise, resolve, reject };\n}\n"]}
@@ -0,0 +1,17 @@
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>;
17
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC7B,yEAAyE;IACzE,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,+EAA+E;IAC/E,KAAK,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACrC,gDAAgD;IAChD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,yCAAyC;IACzC,OAAO,IAAI,IAAI,CAAC;IAChB,4CAA4C;IAC5C,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAC5B;AAED,oGAAoG;AACpG,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC","sourcesContent":["export interface ByteTransport {\n\t/** Sends one byte chunk. Calls must be delivered in invocation order. */\n\tsend(chunk: Uint8Array): Promise<void>;\n\t/** Closes the transport. Implementations must make repeated calls harmless. */\n\tclose(): void;\n}\n\nexport interface ByteTransportHandlers {\n\t/** Delivers an arbitrary inbound byte chunk. */\n\tonData(chunk: Uint8Array): void;\n\t/** Reports an orderly terminal close. */\n\tonClose(): void;\n\t/** Reports a terminal transport failure. */\n\tonError(error: Error): void;\n}\n\n/** Creates a fresh connected, authenticated transport. Exactly one terminal handler is expected. */\nexport type ByteTransportFactory = (handlers: ByteTransportHandlers) => ByteTransport | Promise<ByteTransport>;\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"","sourcesContent":["export interface ByteTransport {\n\t/** Sends one byte chunk. Calls must be delivered in invocation order. */\n\tsend(chunk: Uint8Array): Promise<void>;\n\t/** Closes the transport. Implementations must make repeated calls harmless. */\n\tclose(): void;\n}\n\nexport interface ByteTransportHandlers {\n\t/** Delivers an arbitrary inbound byte chunk. */\n\tonData(chunk: Uint8Array): void;\n\t/** Reports an orderly terminal close. */\n\tonClose(): void;\n\t/** Reports a terminal transport failure. */\n\tonError(error: Error): void;\n}\n\n/** Creates a fresh connected, authenticated transport. Exactly one terminal handler is expected. */\nexport type ByteTransportFactory = (handlers: ByteTransportHandlers) => ByteTransport | Promise<ByteTransport>;\n"]}
@@ -0,0 +1,28 @@
1
+ import type { ServiceSubscriptionSnapshot } from "@m4ike1/chord";
2
+ import type { RpcTarget, SessionTarget } from "@m4ike1/ion-protocol";
3
+ import type { ByteTransportFactory } from "./transport.ts";
4
+ export type ConnectionState = "disconnected" | "connecting" | "connected";
5
+ export interface ConnectionStateChange {
6
+ state: ConnectionState;
7
+ error?: Error;
8
+ }
9
+ export type Unsubscribe = () => void;
10
+ export type ListenerErrorHandler = (error: Error) => void;
11
+ export type AttachmentChangeListener = (attachment: SessionTarget | undefined) => void;
12
+ export interface ServiceSubscription {
13
+ readonly id: string;
14
+ readonly target: RpcTarget;
15
+ readonly snapshot: ServiceSubscriptionSnapshot;
16
+ /** Begin ordered update delivery after the caller has installed the snapshot. */
17
+ start(): void;
18
+ dispose(): Promise<void>;
19
+ }
20
+ export interface ClientOptions {
21
+ transportFactory: ByteTransportFactory;
22
+ /** Logical server identity expected at the physical endpoint. */
23
+ serverId: string;
24
+ maxFrameLength?: number;
25
+ /** Reports subscriber failures without allowing them to corrupt client state. */
26
+ onListenerError?: ListenerErrorHandler;
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AACrC,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAC1D,MAAM,MAAM,wBAAwB,GAAG,CAAC,UAAU,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,CAAC;AAEvF,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,2BAA2B,CAAC;IAC/C,iFAAiF;IACjF,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC7B,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,oBAAoB,CAAC;CACvC","sourcesContent":["import type { ServiceSubscriptionSnapshot } from \"@m4ike1/chord\";\nimport type { RpcTarget, SessionTarget } from \"@m4ike1/ion-protocol\";\nimport type { ByteTransportFactory } from \"./transport.ts\";\n\nexport type ConnectionState = \"disconnected\" | \"connecting\" | \"connected\";\n\nexport interface ConnectionStateChange {\n\tstate: ConnectionState;\n\terror?: Error;\n}\n\nexport type Unsubscribe = () => void;\nexport type ListenerErrorHandler = (error: Error) => void;\nexport type AttachmentChangeListener = (attachment: SessionTarget | undefined) => void;\n\nexport interface ServiceSubscription {\n\treadonly id: string;\n\treadonly target: RpcTarget;\n\treadonly snapshot: ServiceSubscriptionSnapshot;\n\t/** Begin ordered update delivery after the caller has installed the snapshot. */\n\tstart(): void;\n\tdispose(): Promise<void>;\n}\n\nexport interface ClientOptions {\n\ttransportFactory: ByteTransportFactory;\n\t/** Logical server identity expected at the physical endpoint. */\n\tserverId: string;\n\tmaxFrameLength?: number;\n\t/** Reports subscriber failures without allowing them to corrupt client state. */\n\tonListenerError?: ListenerErrorHandler;\n}\n"]}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ServiceSubscriptionSnapshot } from \"@m4ike1/chord\";\nimport type { RpcTarget, SessionTarget } from \"@m4ike1/ion-protocol\";\nimport type { ByteTransportFactory } from \"./transport.ts\";\n\nexport type ConnectionState = \"disconnected\" | \"connecting\" | \"connected\";\n\nexport interface ConnectionStateChange {\n\tstate: ConnectionState;\n\terror?: Error;\n}\n\nexport type Unsubscribe = () => void;\nexport type ListenerErrorHandler = (error: Error) => void;\nexport type AttachmentChangeListener = (attachment: SessionTarget | undefined) => void;\n\nexport interface ServiceSubscription {\n\treadonly id: string;\n\treadonly target: RpcTarget;\n\treadonly snapshot: ServiceSubscriptionSnapshot;\n\t/** Begin ordered update delivery after the caller has installed the snapshot. */\n\tstart(): void;\n\tdispose(): Promise<void>;\n}\n\nexport interface ClientOptions {\n\ttransportFactory: ByteTransportFactory;\n\t/** Logical server identity expected at the physical endpoint. */\n\tserverId: string;\n\tmaxFrameLength?: number;\n\t/** Reports subscriber failures without allowing them to corrupt client state. */\n\tonListenerError?: ListenerErrorHandler;\n}\n"]}
package/dist/unix.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { type ServerId } from "@m4ike1/ion-protocol";
2
+ import type { ByteTransportFactory } from "./transport.ts";
3
+ export interface UnixTransportOptions {
4
+ path: string;
5
+ maxPendingBytes?: number;
6
+ }
7
+ export interface UnixServerRoute {
8
+ serverId: ServerId;
9
+ path: string;
10
+ }
11
+ export interface DiscoverUnixServersOptions {
12
+ /** Directory containing server-addressed Unix sockets. */
13
+ directory: string;
14
+ /** Maximum time for each connection and handshake. Defaults to 1,000 ms. */
15
+ timeoutMs?: number;
16
+ }
17
+ /** Discover reachable local servers by probing server-addressed Unix sockets. */
18
+ export declare function discoverUnixServers(options: DiscoverUnixServersOptions): Promise<UnixServerRoute[]>;
19
+ /** Creates fresh Unix-domain socket transports for Client connection attempts in Node-compatible runtimes. */
20
+ export declare function createUnixTransportFactory(options: UnixTransportOptions): ByteTransportFactory;
21
+ //# sourceMappingURL=unix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unix.d.ts","sourceRoot":"","sources":["../src/unix.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGpH,OAAO,KAAK,EAAiB,oBAAoB,EAAyB,MAAM,gBAAgB,CAAC;AAOjG,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,0BAA0B;IAC1C,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,iFAAiF;AACjF,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAgDzG;AAED,8GAA8G;AAC9G,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAG9F","sourcesContent":["import { lstat, readdir } from \"node:fs/promises\";\nimport { createConnection, type Socket } from \"node:net\";\nimport { join } from \"node:path\";\nimport { DEFAULT_MAX_FRAME_LENGTH, isServerId, ProtocolValidationError, type ServerId } from \"@m4ike1/ion-protocol\";\nimport { Client } from \"./client.ts\";\nimport { DisconnectedError, ServerError } from \"./errors.ts\";\nimport type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from \"./transport.ts\";\n\nconst DEFAULT_DISCOVERY_TIMEOUT_MS = 1_000;\nconst MAX_TIMER_DELAY_MS = 2_147_483_647;\nconst UNIX_SOCKET_SUFFIX = \".sock\";\nconst MAX_CONCURRENT_DISCOVERY_PROBES = 16;\n\nexport interface UnixTransportOptions {\n\tpath: string;\n\tmaxPendingBytes?: number;\n}\n\nexport interface UnixServerRoute {\n\tserverId: ServerId;\n\tpath: string;\n}\n\nexport interface DiscoverUnixServersOptions {\n\t/** Directory containing server-addressed Unix sockets. */\n\tdirectory: string;\n\t/** Maximum time for each connection and handshake. Defaults to 1,000 ms. */\n\ttimeoutMs?: number;\n}\n\n/** Discover reachable local servers by probing server-addressed Unix sockets. */\nexport async function discoverUnixServers(options: DiscoverUnixServersOptions): Promise<UnixServerRoute[]> {\n\tif (process.platform === \"win32\") throw new Error(\"Unix transport is not supported on Windows\");\n\tconst directory = options.directory;\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;\n\tif (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_DELAY_MS) {\n\t\tthrow new TypeError(`Unix discovery timeoutMs must be an integer between 1 and ${MAX_TIMER_DELAY_MS}`);\n\t}\n\n\tlet names: string[];\n\ttry {\n\t\tnames = await readdir(directory);\n\t} catch (error) {\n\t\tif (isErrorCode(error, \"ENOENT\")) return [];\n\t\tthrow error;\n\t}\n\n\tconst candidates = names.flatMap((name): UnixServerRoute[] => {\n\t\tif (!name.endsWith(UNIX_SOCKET_SUFFIX)) return [];\n\t\tconst serverId = name.slice(0, -UNIX_SOCKET_SUFFIX.length);\n\t\treturn isServerId(serverId) ? [{ serverId, path: join(directory, name) }] : [];\n\t});\n\tconst routes: UnixServerRoute[] = [];\n\tlet nextIndex = 0;\n\tlet failure: { error: unknown } | undefined;\n\tconst workerCount = Math.min(MAX_CONCURRENT_DISCOVERY_PROBES, candidates.length);\n\tawait Promise.all(\n\t\tArray.from({ length: workerCount }, async () => {\n\t\t\twhile (!failure) {\n\t\t\t\tconst candidate = candidates[nextIndex++];\n\t\t\t\tif (!candidate) return;\n\t\t\t\ttry {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (!(await lstat(candidate.path)).isSocket()) continue;\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// A socket can disappear between readdir and lstat during normal server shutdown.\n\t\t\t\t\t\tif (isErrorCode(error, \"ENOENT\")) continue;\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t\tconst route = await probeUnixServer(candidate, timeoutMs);\n\t\t\t\t\tif (route) routes.push(route);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailure ??= { error };\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\t);\n\tif (failure) throw failure.error;\n\treturn routes.sort((left, right) => left.serverId.localeCompare(right.serverId));\n}\n\n/** Creates fresh Unix-domain socket transports for Client connection attempts in Node-compatible runtimes. */\nexport function createUnixTransportFactory(options: UnixTransportOptions): ByteTransportFactory {\n\tconst maxPendingBytes = validateUnixTransportOptions(options);\n\treturn (handlers) => connectUnixSocket(options.path, maxPendingBytes, handlers);\n}\n\nfunction validateUnixTransportOptions(options: UnixTransportOptions): number {\n\tif (options.path.length === 0) throw new TypeError(\"Unix transport path must not be empty\");\n\tconst maxPendingBytes = options.maxPendingBytes ?? DEFAULT_MAX_FRAME_LENGTH * 4;\n\tif (!Number.isSafeInteger(maxPendingBytes) || maxPendingBytes <= 0) {\n\t\tthrow new TypeError(\"Unix transport maxPendingBytes must be a positive safe integer\");\n\t}\n\tif (process.platform === \"win32\") throw new Error(\"Unix transport is not supported on Windows\");\n\treturn maxPendingBytes;\n}\n\nfunction connectUnixSocket(\n\tpath: string,\n\tmaxPendingBytes: number,\n\thandlers: ByteTransportHandlers,\n\tonSocket?: (socket: Socket) => void,\n): Promise<ByteTransport> {\n\treturn new Promise<ByteTransport>((resolve, reject) => {\n\t\tconst socket = createConnection(path);\n\t\tonSocket?.(socket);\n\t\tlet connected = false;\n\t\tlet terminal = false;\n\n\t\tconst close = (): void => {\n\t\t\tif (terminal) return;\n\t\t\tterminal = true;\n\t\t\tsocket.destroy();\n\t\t\tif (connected) handlers.onClose();\n\t\t\telse reject(new Error(\"Unix transport closed before connecting\"));\n\t\t};\n\n\t\tsocket.once(\"connect\", () => {\n\t\t\tif (terminal) return;\n\t\t\tconnected = true;\n\t\t\tresolve(\n\t\t\t\tnew UnixByteTransport(socket, maxPendingBytes, () => {\n\t\t\t\t\tterminal = true;\n\t\t\t\t}),\n\t\t\t);\n\t\t});\n\t\tsocket.on(\"data\", (chunk) => {\n\t\t\tif (!terminal) handlers.onData(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));\n\t\t});\n\t\tsocket.once(\"end\", close);\n\t\tsocket.once(\"close\", close);\n\t\tsocket.once(\"error\", (error) => {\n\t\t\tif (terminal) return;\n\t\t\tterminal = true;\n\t\t\tsocket.destroy();\n\t\t\tif (connected) handlers.onError(error);\n\t\t\telse reject(error);\n\t\t});\n\t});\n}\n\nclass UnixByteTransport implements ByteTransport {\n\treadonly #socket: Socket;\n\treadonly #maxPendingBytes: number;\n\treadonly #markLocalClose: () => void;\n\t#closed = false;\n\t#pendingBytes = 0;\n\t#writeTail: Promise<void> = Promise.resolve();\n\n\tconstructor(socket: Socket, maxPendingBytes: number, markLocalClose: () => void) {\n\t\tthis.#socket = socket;\n\t\tthis.#maxPendingBytes = maxPendingBytes;\n\t\tthis.#markLocalClose = markLocalClose;\n\t}\n\n\tsend(chunk: Uint8Array): Promise<void> {\n\t\tif (!(chunk instanceof Uint8Array)) {\n\t\t\treturn Promise.reject(new TypeError(\"Unix transport chunks must be Uint8Array\"));\n\t\t}\n\t\tif (this.#closed) return Promise.reject(new Error(\"Unix transport is closed\"));\n\t\tif (this.#pendingBytes + chunk.byteLength > this.#maxPendingBytes) {\n\t\t\treturn Promise.reject(new Error(\"Unix transport exceeded its pending byte limit\"));\n\t\t}\n\t\tthis.#pendingBytes += chunk.byteLength;\n\t\tconst bytes = chunk.slice();\n\t\tconst write = this.#writeTail.then(() => this.#write(bytes));\n\t\tconst tracked = write.finally(() => {\n\t\t\tthis.#pendingBytes -= bytes.byteLength;\n\t\t});\n\t\tthis.#writeTail = tracked.catch(() => {});\n\t\treturn tracked;\n\t}\n\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#markLocalClose();\n\t\tthis.#socket.destroy();\n\t}\n\n\t#write(chunk: Uint8Array): Promise<void> {\n\t\tif (this.#closed || !this.#socket.writable) return Promise.reject(new Error(\"Unix transport is closed\"));\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tlet callbackComplete = false;\n\t\t\tlet drainComplete = false;\n\t\t\tlet requiresDrain: boolean | undefined;\n\t\t\tlet settled = false;\n\n\t\t\tconst onDrain = (): void => {\n\t\t\t\tdrainComplete = true;\n\t\t\t\tfinish();\n\t\t\t};\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tthis.#socket.off(\"drain\", onDrain);\n\t\t\t\tthis.#socket.off(\"close\", onClose);\n\t\t\t};\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst finish = (): void => {\n\t\t\t\tif (settled || !callbackComplete || requiresDrain === undefined) return;\n\t\t\t\tif (requiresDrain && !drainComplete) return;\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tconst onClose = (): void => fail(new Error(\"Unix transport closed during write\"));\n\n\t\t\ttry {\n\t\t\t\tthis.#socket.once(\"close\", onClose);\n\t\t\t\tconst accepted = this.#socket.write(chunk, (error) => {\n\t\t\t\t\tif (error) {\n\t\t\t\t\t\tfail(error);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcallbackComplete = true;\n\t\t\t\t\tfinish();\n\t\t\t\t});\n\t\t\t\trequiresDrain = !accepted;\n\t\t\t\tif (requiresDrain) this.#socket.once(\"drain\", onDrain);\n\t\t\t\tfinish();\n\t\t\t} catch (error) {\n\t\t\t\tfail(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n}\n\nasync function probeUnixServer(route: UnixServerRoute, timeoutMs: number): Promise<UnixServerRoute | undefined> {\n\tconst maxPendingBytes = validateUnixTransportOptions({ path: route.path });\n\tlet socket: Socket | undefined;\n\tconst client = new Client({\n\t\tserverId: route.serverId,\n\t\ttransportFactory: (handlers) =>\n\t\t\tconnectUnixSocket(route.path, maxPendingBytes, handlers, (created) => {\n\t\t\t\tsocket = created;\n\t\t\t}),\n\t});\n\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\ttry {\n\t\tawait Promise.race([\n\t\t\tclient.connect(),\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\ttimeout = setTimeout(() => {\n\t\t\t\t\tsocket?.destroy();\n\t\t\t\t\treject(new UnixDiscoveryTimeoutError());\n\t\t\t\t}, timeoutMs);\n\t\t\t\ttimeout.unref();\n\t\t\t}),\n\t\t]);\n\t\treturn route;\n\t} catch (error) {\n\t\t// Missing/refused sockets are stale or shutting down. Protocol failures mean\n\t\t// the endpoint is not the advertised server. Both are safe to omit.\n\t\tif (\n\t\t\terror instanceof UnixDiscoveryTimeoutError ||\n\t\t\terror instanceof ProtocolValidationError ||\n\t\t\t(error instanceof DisconnectedError && error.cause === undefined) ||\n\t\t\t(error instanceof ServerError && error.code === \"version\") ||\n\t\t\tisErrorCode(error, \"ENOENT\") ||\n\t\t\tisErrorCode(error, \"ECONNREFUSED\") ||\n\t\t\tisErrorCode(error, \"ECONNRESET\") ||\n\t\t\tisErrorCode(error, \"EPIPE\") ||\n\t\t\tisErrorCode(error, \"ETIMEDOUT\")\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\tthrow error;\n\t} finally {\n\t\tif (timeout) clearTimeout(timeout);\n\t\tawait client.dispose();\n\t\tconst activeSocket = socket;\n\t\tif (activeSocket && !activeSocket.destroyed) activeSocket.destroy();\n\t\tif (activeSocket && !activeSocket.closed) {\n\t\t\tawait new Promise<void>((resolve) => activeSocket.once(\"close\", resolve));\n\t\t}\n\t}\n}\n\nclass UnixDiscoveryTimeoutError extends Error {}\n\nfunction isErrorCode(error: unknown, code: string): boolean {\n\tlet current = error;\n\tconst seen = new Set<unknown>();\n\twhile (current instanceof Error && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tif (\"code\" in current && current.code === code) return true;\n\t\tcurrent = current.cause;\n\t}\n\treturn false;\n}\n"]}