@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.
- package/CHANGELOG.md +42 -0
- package/README.md +75 -0
- package/dist/client.d.ts +30 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +388 -0
- package/dist/client.js.map +1 -0
- package/dist/connection.d.ts +25 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +208 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/promise.d.ts +8 -0
- package/dist/promise.d.ts.map +1 -0
- package/dist/promise.js +11 -0
- package/dist/promise.js.map +1 -0
- package/dist/transport.d.ts +17 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unix.d.ts +21 -0
- package/dist/unix.d.ts.map +1 -0
- package/dist/unix.js +281 -0
- package/dist/unix.js.map +1 -0
- package/package.json +57 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Restarted the `@m4ike1` version line at 0.1.0. The versions inherited from upstream Pi (`0.85.1`, `0.86.1`) stay on npm and remain installable by exact version, but they are a different line: this release sorts below them, so an existing `^0.86.1` range will not pick it up, and inter-package ranges move to `^0.1.0`. The `latest` dist-tag follows ion's own line from this release onward.
|
|
10
|
+
|
|
11
|
+
## [0.86.1] - 2026-09-24
|
|
12
|
+
|
|
13
|
+
## [0.86.0] - 2026-09-24
|
|
14
|
+
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
|
|
17
|
+
- Renamed product `pi` → `ion` (breaking). Package names `@m4ike1/pi-*` → `@m4ike1/ion-*` (`ionConfig` instead of `piConfig`, `@m4ike1/ion-coding-agent`, `@m4ike1/ion-ai`, etc.), binary `pi` → `ion`, config directory `.pi` → `.ion` (XDG now immediate at `~/.config/ion`, `~/.local/share/ion`, `~/.local/state/ion`, `~/.cache/ion`), environment variables `PI_*` → `ION_*` (`PI_CODING_AGENT_DIR` → `ION_CODING_AGENT_DIR`, `PI_DISABLE_SESSION_INDEX` etc., with `PI_*` fallback for one release), and `pi.dev` → `ion.dev`. Legacy `~/.pi` is migrated on first run if `~/.config/ion` is missing. `APP_TITLE` `π` → `⛬`.
|
|
18
|
+
- Renamed this package from `@maikel/ion-client` to `@m4ike1/ion-client` (breaking). The `@maikel` npm scope belongs to another account, so the fork publishes under `@m4ike1`. Import specifiers and dependency ranges change accordingly. Nothing was ever published under the old scope, so there is no published version to migrate from.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [0.85.1] - 2026-09-05
|
|
23
|
+
|
|
24
|
+
## [0.85.0] - 2026-09-04
|
|
25
|
+
|
|
26
|
+
## [0.84.4] - 2026-08-28
|
|
27
|
+
|
|
28
|
+
## [0.84.3] - 2026-08-24
|
|
29
|
+
|
|
30
|
+
## [0.84.2] - 2026-08-14
|
|
31
|
+
|
|
32
|
+
## [0.84.1] - 2026-08-07
|
|
33
|
+
|
|
34
|
+
## [0.84.0] - 2026-08-06
|
|
35
|
+
|
|
36
|
+
### Breaking Changes
|
|
37
|
+
|
|
38
|
+
- Replaced `SessionSummary` with durable `SessionMetadata` for `PiClient.listSessions()` and server snapshots; runtime state is available only from acquired session snapshots ([#7708](https://github.com/maikel/pi/pull/7708)).
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Added the experimental transport-neutral `PiClient` and multi-session `PiSessionHandle` APIs with structured `PiServerError` responses.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @m4ike1/ion-client
|
|
2
|
+
|
|
3
|
+
Transport-neutral client for remote ion sessions over framed CBOR bytes. Bring your own ordered byte transport (WebSocket, Unix socket, or equivalent); the client handles the hello handshake, `serverId` verification, request correlation, service subscriptions, and attachment routing.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Client, type ByteTransportFactory } from "@m4ike1/ion-client";
|
|
7
|
+
|
|
8
|
+
const transportFactory: ByteTransportFactory = (handlers) => {
|
|
9
|
+
// Open a fresh authenticated connection; call
|
|
10
|
+
// handlers.onData(chunk) / onClose() / onError(err) as events arrive.
|
|
11
|
+
return { send: async (chunk) => {}, close: () => {} };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const client = await Client.connect({
|
|
15
|
+
serverId: "01234567-89ab-4def-8123-456789abcdef",
|
|
16
|
+
transportFactory,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await client.request(
|
|
20
|
+
{ serverId: client.hello!.serverId },
|
|
21
|
+
{ serviceId: "example.service", member: "read", args: [] },
|
|
22
|
+
);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What it does
|
|
26
|
+
|
|
27
|
+
- Verifies the physical endpoint reports the expected logical `serverId` during the hello handshake. A mismatch fails the connection.
|
|
28
|
+
- Sends low-level Chord service calls via `request()` and `subscribeService()` against an explicit routed target: `{ serverId }` for server-wide calls, or the full live `{ serverId, sessionId, attachmentId }` for session calls.
|
|
29
|
+
- Tracks the live attachment from out-of-band server messages (`client.attachment`, `onAttachmentChange()`). The server-generated `attachmentId` rejects delayed frames after switching or reattaching.
|
|
30
|
+
- Adapts to typed Chord service bindings via `createClientServiceTransport(client, getTarget)`. The client itself never constructs typed proxies or interprets application contracts.
|
|
31
|
+
|
|
32
|
+
## Lifecycle
|
|
33
|
+
|
|
34
|
+
- `Client.connect(options)` constructs and connects; `new Client(options)` + `connect()` defers the handshake. `connect()` rejects while already `connecting`/`connected`.
|
|
35
|
+
- On disconnect or `dispose()`, pending requests reject locally; accepted work may still complete remotely. The client clears the live attachment, drops service listeners, and never reconnects or replays automatically. Call `reconnect()`, re-attach through the application's management service, and repeat only operations known to be safe.
|
|
36
|
+
- `disconnect()` and `dispose()` are idempotent. `dispose()` also supports `await using`.
|
|
37
|
+
- `connectionState` is `disconnected | connecting | connected`; observe with `onConnectionStateChange()`. Listener errors go to `onListenerError` and never corrupt client state.
|
|
38
|
+
|
|
39
|
+
## Errors
|
|
40
|
+
|
|
41
|
+
- `ServerError` (with `.code`) for server-rejected calls, including handshake `hello_error` rejections.
|
|
42
|
+
- `DisconnectedError` when sending while not connected, or when the transport closes/fails.
|
|
43
|
+
- `ClientDisposedError` after `dispose()`.
|
|
44
|
+
- Abort a single call with an `AbortSignal`; the client sends a `cancel` frame when the request was already sent.
|
|
45
|
+
|
|
46
|
+
## Limits
|
|
47
|
+
|
|
48
|
+
- `ClientOptions.maxFrameLength` bounds inbound protocol payloads.
|
|
49
|
+
- Unix transports add `maxPendingBytes` for queued output. Configure matching limits on both peers.
|
|
50
|
+
|
|
51
|
+
## Unix-domain sockets
|
|
52
|
+
|
|
53
|
+
Node.js and Bun consumers can use the separate entrypoint:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { Client } from "@m4ike1/ion-client";
|
|
57
|
+
import { createUnixTransportFactory, discoverUnixServers } from "@m4ike1/ion-client/unix";
|
|
58
|
+
|
|
59
|
+
const client = new Client({
|
|
60
|
+
serverId: "01234567-89ab-4def-8123-456789abcdef",
|
|
61
|
+
transportFactory: createUnixTransportFactory({ path: "/tmp/ion.sock" }),
|
|
62
|
+
});
|
|
63
|
+
await client.connect();
|
|
64
|
+
|
|
65
|
+
const routes = await discoverUnixServers({ directory: "/run/user/1000/ion" });
|
|
66
|
+
// [{ serverId: "...", path: "/run/user/1000/ion/<serverId>.sock" }]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Discovery probes `<serverId>.sock` files (at most 16 concurrently, 1s default per-probe timeout via `timeoutMs`), ignores malformed/non-socket/stale/mismatched endpoints, and throws on unexpected filesystem or socket errors. Not supported on Windows.
|
|
70
|
+
|
|
71
|
+
## Further reading
|
|
72
|
+
|
|
73
|
+
- `docs/reference.md` — full API reference for `Client`, transports, subscriptions, and errors.
|
|
74
|
+
- `docs/how-to.md` — recipes: custom transports, Unix setup and discovery, subscriptions, cancellation, reconnect.
|
|
75
|
+
- `docs/concepts.md` — why transport neutrality, `serverId` verification, and attachment routing work the way they do.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type JsonValue, type RemoteServiceTransport, type ServiceCall, type ServiceCatalogueEntry, type ServiceMode, type ServiceProviderUpdate } from "@m4ike1/chord";
|
|
2
|
+
import { type RpcTarget, type ServerHello, type SessionTarget } from "@m4ike1/ion-protocol";
|
|
3
|
+
import type { AttachmentChangeListener, ClientOptions, ConnectionState, ConnectionStateChange, ServiceSubscription, Unsubscribe } from "./types.ts";
|
|
4
|
+
type ServiceResult = JsonValue | undefined;
|
|
5
|
+
export declare class Client {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(options: ClientOptions);
|
|
8
|
+
get disposed(): boolean;
|
|
9
|
+
get connectionState(): ConnectionState;
|
|
10
|
+
get connected(): boolean;
|
|
11
|
+
get serverId(): string;
|
|
12
|
+
get hello(): ServerHello | undefined;
|
|
13
|
+
get attachment(): SessionTarget | undefined;
|
|
14
|
+
static connect(options: ClientOptions): Promise<Client>;
|
|
15
|
+
connect(): Promise<ServerHello>;
|
|
16
|
+
reconnect(): Promise<ServerHello>;
|
|
17
|
+
disconnect(reason?: string): void;
|
|
18
|
+
onConnectionStateChange(listener: (change: ConnectionStateChange) => void): Unsubscribe;
|
|
19
|
+
onAttachmentChange(listener: AttachmentChangeListener): Unsubscribe;
|
|
20
|
+
/** Invoke one low-level protocol call against an explicit routed target. */
|
|
21
|
+
request(target: RpcTarget, call: ServiceCall, signal?: AbortSignal): Promise<ServiceResult>;
|
|
22
|
+
serviceCatalogue(target: RpcTarget, signal?: AbortSignal): Promise<readonly ServiceCatalogueEntry[]>;
|
|
23
|
+
subscribeService(target: RpcTarget, serviceId: string, mode: ServiceMode, listener: (update: ServiceProviderUpdate) => void | Promise<void>, signal?: AbortSignal): Promise<ServiceSubscription>;
|
|
24
|
+
dispose(): Promise<void>;
|
|
25
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** Adapts a lazily resolved routed client target to a Chord service transport. */
|
|
28
|
+
export declare function createClientServiceTransport(client: Client, getTarget: () => RpcTarget | undefined): RemoteServiceTransport;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,SAAS,EAKd,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAG1B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAMN,KAAK,SAAS,EACd,KAAK,WAAW,EAEhB,KAAK,aAAa,EAClB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,KAAK,EACX,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,MAAM,YAAY,CAAC;AAEpB,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAmB3C,qBAAa,MAAM;;IAclB,YAAY,OAAO,EAAE,aAAa,EAejC;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,eAAe,IAAI,eAAe,CAErC;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,KAAK,IAAI,WAAW,GAAG,SAAS,CAEnC;IAED,IAAI,UAAU,IAAI,aAAa,GAAG,SAAS,CAE1C;IAED,OAAa,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5D;IAED,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,CAI9B;IAED,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,CAEhC;IAED,UAAU,CAAC,MAAM,SAAwB,GAAG,IAAI,CAE/C;IAED,uBAAuB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,GAAG,WAAW,CAItF;IAED,kBAAkB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,WAAW,CAIlE;IAED,4EAA4E;IAC5E,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAE1F;IAEK,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,qBAAqB,EAAE,CAAC,CAWzG;IAEK,gBAAgB,CACrB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACjE,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,mBAAmB,CAAC,CA0D9B;IA+ID,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAavB;IAED,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAErC;CAiDD;AAED,kFAAkF;AAClF,wBAAgB,4BAA4B,CAC3C,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,SAAS,GAAG,SAAS,GACpC,sBAAsB,CAuBxB","sourcesContent":["import {\n\tcreateServiceCatalogueCall,\n\tcreateServiceStateDecoder,\n\tcreateServiceSubscribeCall,\n\tcreateServiceUnsubscribeCall,\n\ttype JsonValue,\n\tparseServiceCall,\n\tparseServiceCatalogue,\n\tparseWireServiceProviderUpdate,\n\tparseWireServiceSubscriptionSnapshot,\n\ttype RemoteServiceTransport,\n\ttype ServiceCall,\n\ttype ServiceCatalogueEntry,\n\ttype ServiceMode,\n\ttype ServiceProviderUpdate,\n\ttype ServiceStateDecoder,\n\ttype ServiceSubscriptionSnapshot,\n} from \"@m4ike1/chord\";\nimport { BACKGROUND_CONTEXT } from \"@m4ike1/chord/context\";\nimport {\n\ttype AttachmentEnvelope,\n\tencodeClientMessage,\n\tisServerId,\n\tProtocolValidationError,\n\ttype ResponseEnvelope,\n\ttype RpcTarget,\n\ttype ServerHello,\n\ttype ServiceEventEnvelope,\n\ttype SessionTarget,\n} from \"@m4ike1/ion-protocol\";\nimport { Connection } from \"./connection.ts\";\nimport { ClientDisposedError, DisconnectedError, ServerError, toError } from \"./errors.ts\";\nimport { createPromiseResolvers } from \"./promise.ts\";\nimport type {\n\tAttachmentChangeListener,\n\tClientOptions,\n\tConnectionState,\n\tConnectionStateChange,\n\tServiceSubscription,\n\tUnsubscribe,\n} from \"./types.ts\";\n\ntype ServiceResult = JsonValue | undefined;\n\ninterface PendingRequest {\n\tresolve(result: ServiceResult): void;\n\treject(error: Error): void;\n\tcleanup(): void;\n}\n\ninterface ActiveServiceListener {\n\treadonly target: RpcTarget;\n\treadonly listener: (update: ServiceProviderUpdate) => void | Promise<void>;\n\treadonly decoder: ServiceStateDecoder;\n\treadonly queuedWireUpdates: JsonValue[];\n\treadonly queued: ServiceProviderUpdate[];\n\tdeliveryTail: Promise<void>;\n\thydrated: boolean;\n\tready: boolean;\n}\n\nexport class Client {\n\treadonly #options: ClientOptions;\n\treadonly #connection: Connection;\n\treadonly #pendingRequests = new Map<string, PendingRequest>();\n\treadonly #connectionStateListeners = new Set<(change: ConnectionStateChange) => void>();\n\treadonly #attachmentListeners = new Set<AttachmentChangeListener>();\n\treadonly #serviceListeners = new Map<string, ActiveServiceListener>();\n\t#requestSequence = 0;\n\t#serviceSubscriptionSequence = 0;\n\t#hello: ServerHello | undefined;\n\t#attachment: SessionTarget | undefined;\n\t#disposed = false;\n\t#disposePromise: Promise<void> | undefined;\n\n\tconstructor(options: ClientOptions) {\n\t\tif (!isServerId(options.serverId)) {\n\t\t\tthrow new TypeError(\"serverId must be a canonical lowercase UUIDv4\");\n\t\t}\n\t\tthis.#options = options;\n\t\tthis.#connection = new Connection({\n\t\t\ttransportFactory: options.transportFactory,\n\t\t\tserverId: options.serverId,\n\t\t\tmaxFrameLength: options.maxFrameLength,\n\t\t\tonHandshake: (hello) => {\n\t\t\t\tthis.#hello = hello;\n\t\t\t},\n\t\t\tonMessage: (message) => this.#handleMessage(message),\n\t\t\tonStateChange: (change) => this.#handleConnectionStateChange(change),\n\t\t});\n\t}\n\n\tget disposed(): boolean {\n\t\treturn this.#disposed;\n\t}\n\n\tget connectionState(): ConnectionState {\n\t\treturn this.#connection.state;\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connection.state === \"connected\";\n\t}\n\n\tget serverId(): string {\n\t\treturn this.#options.serverId;\n\t}\n\n\tget hello(): ServerHello | undefined {\n\t\treturn this.#hello;\n\t}\n\n\tget attachment(): SessionTarget | undefined {\n\t\treturn this.#attachment;\n\t}\n\n\tstatic async connect(options: ClientOptions): Promise<Client> {\n\t\tconst client = new Client(options);\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\treturn client;\n\t\t} catch (error) {\n\t\t\tawait client.dispose();\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tconnect(): Promise<ServerHello> {\n\t\tif (this.#disposed) return Promise.reject(new ClientDisposedError());\n\t\tthis.#hello = undefined;\n\t\treturn this.#connection.connect();\n\t}\n\n\treconnect(): Promise<ServerHello> {\n\t\treturn this.connect();\n\t}\n\n\tdisconnect(reason = \"Client disconnected\"): void {\n\t\tthis.#connection.disconnect(reason);\n\t}\n\n\tonConnectionStateChange(listener: (change: ConnectionStateChange) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\tthis.#connectionStateListeners.add(listener);\n\t\treturn () => this.#connectionStateListeners.delete(listener);\n\t}\n\n\tonAttachmentChange(listener: AttachmentChangeListener): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\tthis.#attachmentListeners.add(listener);\n\t\treturn () => this.#attachmentListeners.delete(listener);\n\t}\n\n\t/** Invoke one low-level protocol call against an explicit routed target. */\n\trequest(target: RpcTarget, call: ServiceCall, signal?: AbortSignal): Promise<ServiceResult> {\n\t\treturn this.#request(target, call, signal);\n\t}\n\n\tasync serviceCatalogue(target: RpcTarget, signal?: AbortSignal): Promise<readonly ServiceCatalogueEntry[]> {\n\t\tconst result = await this.#request(target, createServiceCatalogueCall(), signal);\n\t\ttry {\n\t\t\treturn parseServiceCatalogue(result);\n\t\t} catch (error) {\n\t\t\tconst validationError = new ProtocolValidationError(\n\t\t\t\terror instanceof Error ? error.message : \"Invalid service catalogue\",\n\t\t\t);\n\t\t\tthis.#connection.fail(validationError);\n\t\t\tthrow validationError;\n\t\t}\n\t}\n\n\tasync subscribeService(\n\t\ttarget: RpcTarget,\n\t\tserviceId: string,\n\t\tmode: ServiceMode,\n\t\tlistener: (update: ServiceProviderUpdate) => void | Promise<void>,\n\t\tsignal?: AbortSignal,\n\t): Promise<ServiceSubscription> {\n\t\tconst subscriptionId = `service-${++this.#serviceSubscriptionSequence}`;\n\t\tconst active: ActiveServiceListener = {\n\t\t\ttarget,\n\t\t\tlistener,\n\t\t\tdecoder: createServiceStateDecoder(),\n\t\t\tqueuedWireUpdates: [],\n\t\t\tqueued: [],\n\t\t\tdeliveryTail: Promise.resolve(),\n\t\t\thydrated: false,\n\t\t\tready: false,\n\t\t};\n\t\tthis.#serviceListeners.set(subscriptionId, active);\n\t\tlet snapshot: ServiceSubscriptionSnapshot;\n\t\ttry {\n\t\t\tsnapshot = await this.#request(\n\t\t\t\ttarget,\n\t\t\t\tcreateServiceSubscribeCall(subscriptionId, serviceId, mode),\n\t\t\t\tsignal,\n\t\t\t\t(result) => {\n\t\t\t\t\tconst decoded = active.decoder.decodeSnapshot(parseWireServiceSubscriptionSnapshot(result));\n\t\t\t\t\tactive.hydrated = true;\n\t\t\t\t\tfor (const update of active.queuedWireUpdates.splice(0)) {\n\t\t\t\t\t\tactive.queued.push(active.decoder.decodeUpdate(parseWireServiceProviderUpdate(update)));\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded;\n\t\t\t\t},\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (this.#serviceListeners.get(subscriptionId) === active) this.#serviceListeners.delete(subscriptionId);\n\t\t\tthrow error;\n\t\t}\n\t\tif (this.#serviceListeners.get(subscriptionId) !== active) throw new DisconnectedError();\n\t\tlet disposed = false;\n\t\treturn {\n\t\t\tid: subscriptionId,\n\t\t\ttarget,\n\t\t\tsnapshot,\n\t\t\tstart: () => {\n\t\t\t\tif (disposed || active.ready) return;\n\t\t\t\tactive.ready = true;\n\t\t\t\tfor (const update of active.queued.splice(0)) this.#deliverServiceUpdate(active, update);\n\t\t\t},\n\t\t\tdispose: async () => {\n\t\t\t\tif (disposed) return;\n\t\t\t\tdisposed = true;\n\t\t\t\tif (this.#serviceListeners.get(subscriptionId) === active) this.#serviceListeners.delete(subscriptionId);\n\t\t\t\ttry {\n\t\t\t\t\tif (this.connected && this.#targetIsCurrent(target)) {\n\t\t\t\t\t\tawait this.#request(target, createServiceUnsubscribeCall(subscriptionId));\n\t\t\t\t\t}\n\t\t\t\t\tawait active.deliveryTail;\n\t\t\t\t} finally {\n\t\t\t\t\tactive.queuedWireUpdates.length = 0;\n\t\t\t\t\tactive.queued.length = 0;\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n\n\t#request<T = ServiceResult>(\n\t\ttarget: RpcTarget,\n\t\tcall: ServiceCall,\n\t\tsignal?: AbortSignal,\n\t\ttransform?: (result: ServiceResult) => T,\n\t): Promise<T> {\n\t\tif (this.#disposed) return Promise.reject(new ClientDisposedError());\n\t\tif (!this.connected) return Promise.reject(new DisconnectedError());\n\t\tif (signal?.aborted) return Promise.reject(abortError(signal));\n\t\tconst id = `request-${++this.#requestSequence}`;\n\t\tconst { promise, resolve, reject } = createPromiseResolvers<T>();\n\t\tlet sent = false;\n\t\tlet aborted = false;\n\t\tlet onAbort: (() => void) | undefined;\n\t\tconst sendCancel = (): void => {\n\t\t\tif (!sent || !this.connected) return;\n\t\t\ttry {\n\t\t\t\tthis.#connection.send(\n\t\t\t\t\tencodeClientMessage({ type: \"cancel\", id, target }, { maxFrameLength: this.#connection.maxFrameLength }),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#connection.fail(toError(error));\n\t\t\t}\n\t\t};\n\t\tif (signal !== undefined) {\n\t\t\tonAbort = () => {\n\t\t\t\tif (aborted) return;\n\t\t\t\taborted = true;\n\t\t\t\treject(abortError(signal));\n\t\t\t\tsendCancel();\n\t\t\t};\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tthis.#pendingRequests.set(id, {\n\t\t\tresolve: (result) => {\n\t\t\t\ttry {\n\t\t\t\t\tresolve(transform === undefined ? (result as T) : transform(result));\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst validationError = new ProtocolValidationError(\n\t\t\t\t\t\terror instanceof Error ? error.message : \"Invalid service operation stream\",\n\t\t\t\t\t);\n\t\t\t\t\tthis.#connection.fail(validationError);\n\t\t\t\t\treject(validationError);\n\t\t\t\t}\n\t\t\t},\n\t\t\treject,\n\t\t\tcleanup: () => {\n\t\t\t\tif (signal !== undefined && onAbort !== undefined) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t},\n\t\t});\n\t\tlet frame: Uint8Array;\n\t\ttry {\n\t\t\tframe = encodeClientMessage(\n\t\t\t\t{ type: \"request\", id, target, call: parseServiceCall(call) as unknown as JsonValue },\n\t\t\t\t{ maxFrameLength: this.#connection.maxFrameLength },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthis.#takePendingRequest(id)?.reject(toError(error));\n\t\t\treturn promise;\n\t\t}\n\t\tthis.#connection.send(frame);\n\t\tsent = true;\n\t\tif (aborted) sendCancel();\n\t\treturn promise;\n\t}\n\n\t#handleMessage(message: ResponseEnvelope | ServiceEventEnvelope | AttachmentEnvelope): void {\n\t\tif (message.type === \"attachment\") {\n\t\t\tif (message.attachment !== null && message.attachment.serverId !== this.#options.serverId) {\n\t\t\t\tthis.#connection.fail(new ProtocolValidationError(\"Attachment update belongs to another server\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.#setAttachment(message.attachment ?? undefined);\n\t\t\treturn;\n\t\t}\n\t\tif (message.type === \"service_update\") {\n\t\t\tconst active = this.#serviceListeners.get(message.subscriptionId);\n\t\t\tif (active === undefined) return;\n\t\t\tif (!active.hydrated) {\n\t\t\t\tactive.queuedWireUpdates.push(message.update);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet update: ServiceProviderUpdate;\n\t\t\ttry {\n\t\t\t\tupdate = active.decoder.decodeUpdate(parseWireServiceProviderUpdate(message.update));\n\t\t\t} catch (error) {\n\t\t\t\tthis.#connection.fail(\n\t\t\t\t\tnew ProtocolValidationError(error instanceof Error ? error.message : \"Invalid service operation stream\"),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (active.ready) this.#deliverServiceUpdate(active, update);\n\t\t\telse active.queued.push(update);\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.#takePendingRequest(message.id);\n\t\tif (!pending) {\n\t\t\tthis.#connection.fail(new ProtocolValidationError(\"Response has no matching request\"));\n\t\t\treturn;\n\t\t}\n\t\tif (!message.ok) {\n\t\t\tpending.reject(new ServerError(message.error));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(message.result);\n\t}\n\n\t#handleConnectionStateChange(change: ConnectionStateChange): void {\n\t\tif (change.state === \"disconnected\") {\n\t\t\tthis.#hello = undefined;\n\t\t\tthis.#setAttachment(undefined);\n\t\t\tthis.#rejectPendingRequests(change.error ?? new DisconnectedError());\n\t\t\tthis.#serviceListeners.clear();\n\t\t}\n\t\tfor (const listener of this.#connectionStateListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(change);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#reportListenerError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\t#takePendingRequest(id: string): PendingRequest | undefined {\n\t\tconst request = this.#pendingRequests.get(id);\n\t\tif (request) {\n\t\t\tthis.#pendingRequests.delete(id);\n\t\t\trequest.cleanup();\n\t\t}\n\t\treturn request;\n\t}\n\n\t#rejectPendingRequests(error: Error): void {\n\t\tconst requests = [...this.#pendingRequests.values()];\n\t\tthis.#pendingRequests.clear();\n\t\tfor (const request of requests) {\n\t\t\trequest.cleanup();\n\t\t\trequest.reject(error);\n\t\t}\n\t}\n\n\tdispose(): Promise<void> {\n\t\tif (this.#disposePromise) return this.#disposePromise;\n\t\tthis.#disposed = true;\n\t\tthis.#disposePromise = Promise.resolve();\n\t\tconst error = new ClientDisposedError();\n\t\tthis.#rejectPendingRequests(error);\n\t\tthis.#connection.disconnect(error);\n\t\tthis.#hello = undefined;\n\t\tthis.#setAttachment(undefined);\n\t\tthis.#connectionStateListeners.clear();\n\t\tthis.#attachmentListeners.clear();\n\t\tthis.#serviceListeners.clear();\n\t\treturn this.#disposePromise;\n\t}\n\n\t[Symbol.asyncDispose](): Promise<void> {\n\t\treturn this.dispose();\n\t}\n\n\t#setAttachment(attachment: SessionTarget | undefined): void {\n\t\tconst previous = this.#attachment;\n\t\tif (\n\t\t\tprevious?.serverId === attachment?.serverId &&\n\t\t\tprevious?.sessionId === attachment?.sessionId &&\n\t\t\tprevious?.attachmentId === attachment?.attachmentId\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#attachment = attachment;\n\t\tfor (const listener of this.#attachmentListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(attachment);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#reportListenerError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\t#deliverServiceUpdate(active: ActiveServiceListener, update: ServiceProviderUpdate): void {\n\t\tactive.deliveryTail = active.deliveryTail\n\t\t\t.then(() => active.listener(update))\n\t\t\t.catch((error: unknown) => this.#reportListenerError(error));\n\t}\n\n\t#targetIsCurrent(target: RpcTarget): boolean {\n\t\tif (!(\"sessionId\" in target)) return this.#hello?.serverId === target.serverId;\n\t\tconst attachment = this.#attachment;\n\t\treturn (\n\t\t\tattachment?.serverId === target.serverId &&\n\t\t\tattachment.sessionId === target.sessionId &&\n\t\t\tattachment.attachmentId === target.attachmentId\n\t\t);\n\t}\n\n\t#assertNotDisposed(): void {\n\t\tif (this.#disposed) throw new ClientDisposedError();\n\t}\n\n\t#reportListenerError(error: unknown): void {\n\t\tif (!this.#options.onListenerError) return;\n\t\ttry {\n\t\t\tthis.#options.onListenerError(toError(error));\n\t\t} catch {\n\t\t\t// Diagnostics cannot affect protocol or transport state.\n\t\t}\n\t}\n}\n\n/** Adapts a lazily resolved routed client target to a Chord service transport. */\nexport function createClientServiceTransport(\n\tclient: Client,\n\tgetTarget: () => RpcTarget | undefined,\n): RemoteServiceTransport {\n\tconst target = (): RpcTarget => {\n\t\tconst resolved = getTarget();\n\t\tif (resolved === undefined) throw new Error(\"Remote service target is unavailable\");\n\t\treturn resolved;\n\t};\n\treturn {\n\t\tinvoke: async (call, context) => client.request(target(), call, context.abortSignal),\n\t\tasync subscribe(serviceId, mode, listener, context) {\n\t\t\tconst subscription = await client.subscribeService(\n\t\t\t\ttarget(),\n\t\t\t\tserviceId,\n\t\t\t\tmode,\n\t\t\t\t(update) => listener(update, BACKGROUND_CONTEXT),\n\t\t\t\tcontext.abortSignal,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tsnapshot: subscription.snapshot,\n\t\t\t\tactivate: () => subscription.start(),\n\t\t\t\tclose: () => subscription.dispose(),\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction abortError(signal: AbortSignal): Error {\n\tconst reason: unknown = signal.reason;\n\treturn reason instanceof Error ? reason : new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"]}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { createServiceCatalogueCall, createServiceStateDecoder, createServiceSubscribeCall, createServiceUnsubscribeCall, parseServiceCall, parseServiceCatalogue, parseWireServiceProviderUpdate, parseWireServiceSubscriptionSnapshot, } from "@m4ike1/chord";
|
|
2
|
+
import { BACKGROUND_CONTEXT } from "@m4ike1/chord/context";
|
|
3
|
+
import { encodeClientMessage, isServerId, ProtocolValidationError, } from "@m4ike1/ion-protocol";
|
|
4
|
+
import { Connection } from "./connection.js";
|
|
5
|
+
import { ClientDisposedError, DisconnectedError, ServerError, toError } from "./errors.js";
|
|
6
|
+
import { createPromiseResolvers } from "./promise.js";
|
|
7
|
+
export class Client {
|
|
8
|
+
#options;
|
|
9
|
+
#connection;
|
|
10
|
+
#pendingRequests = new Map();
|
|
11
|
+
#connectionStateListeners = new Set();
|
|
12
|
+
#attachmentListeners = new Set();
|
|
13
|
+
#serviceListeners = new Map();
|
|
14
|
+
#requestSequence = 0;
|
|
15
|
+
#serviceSubscriptionSequence = 0;
|
|
16
|
+
#hello;
|
|
17
|
+
#attachment;
|
|
18
|
+
#disposed = false;
|
|
19
|
+
#disposePromise;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
if (!isServerId(options.serverId)) {
|
|
22
|
+
throw new TypeError("serverId must be a canonical lowercase UUIDv4");
|
|
23
|
+
}
|
|
24
|
+
this.#options = options;
|
|
25
|
+
this.#connection = new Connection({
|
|
26
|
+
transportFactory: options.transportFactory,
|
|
27
|
+
serverId: options.serverId,
|
|
28
|
+
maxFrameLength: options.maxFrameLength,
|
|
29
|
+
onHandshake: (hello) => {
|
|
30
|
+
this.#hello = hello;
|
|
31
|
+
},
|
|
32
|
+
onMessage: (message) => this.#handleMessage(message),
|
|
33
|
+
onStateChange: (change) => this.#handleConnectionStateChange(change),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
get disposed() {
|
|
37
|
+
return this.#disposed;
|
|
38
|
+
}
|
|
39
|
+
get connectionState() {
|
|
40
|
+
return this.#connection.state;
|
|
41
|
+
}
|
|
42
|
+
get connected() {
|
|
43
|
+
return this.#connection.state === "connected";
|
|
44
|
+
}
|
|
45
|
+
get serverId() {
|
|
46
|
+
return this.#options.serverId;
|
|
47
|
+
}
|
|
48
|
+
get hello() {
|
|
49
|
+
return this.#hello;
|
|
50
|
+
}
|
|
51
|
+
get attachment() {
|
|
52
|
+
return this.#attachment;
|
|
53
|
+
}
|
|
54
|
+
static async connect(options) {
|
|
55
|
+
const client = new Client(options);
|
|
56
|
+
try {
|
|
57
|
+
await client.connect();
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await client.dispose();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
connect() {
|
|
66
|
+
if (this.#disposed)
|
|
67
|
+
return Promise.reject(new ClientDisposedError());
|
|
68
|
+
this.#hello = undefined;
|
|
69
|
+
return this.#connection.connect();
|
|
70
|
+
}
|
|
71
|
+
reconnect() {
|
|
72
|
+
return this.connect();
|
|
73
|
+
}
|
|
74
|
+
disconnect(reason = "Client disconnected") {
|
|
75
|
+
this.#connection.disconnect(reason);
|
|
76
|
+
}
|
|
77
|
+
onConnectionStateChange(listener) {
|
|
78
|
+
this.#assertNotDisposed();
|
|
79
|
+
this.#connectionStateListeners.add(listener);
|
|
80
|
+
return () => this.#connectionStateListeners.delete(listener);
|
|
81
|
+
}
|
|
82
|
+
onAttachmentChange(listener) {
|
|
83
|
+
this.#assertNotDisposed();
|
|
84
|
+
this.#attachmentListeners.add(listener);
|
|
85
|
+
return () => this.#attachmentListeners.delete(listener);
|
|
86
|
+
}
|
|
87
|
+
/** Invoke one low-level protocol call against an explicit routed target. */
|
|
88
|
+
request(target, call, signal) {
|
|
89
|
+
return this.#request(target, call, signal);
|
|
90
|
+
}
|
|
91
|
+
async serviceCatalogue(target, signal) {
|
|
92
|
+
const result = await this.#request(target, createServiceCatalogueCall(), signal);
|
|
93
|
+
try {
|
|
94
|
+
return parseServiceCatalogue(result);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const validationError = new ProtocolValidationError(error instanceof Error ? error.message : "Invalid service catalogue");
|
|
98
|
+
this.#connection.fail(validationError);
|
|
99
|
+
throw validationError;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async subscribeService(target, serviceId, mode, listener, signal) {
|
|
103
|
+
const subscriptionId = `service-${++this.#serviceSubscriptionSequence}`;
|
|
104
|
+
const active = {
|
|
105
|
+
target,
|
|
106
|
+
listener,
|
|
107
|
+
decoder: createServiceStateDecoder(),
|
|
108
|
+
queuedWireUpdates: [],
|
|
109
|
+
queued: [],
|
|
110
|
+
deliveryTail: Promise.resolve(),
|
|
111
|
+
hydrated: false,
|
|
112
|
+
ready: false,
|
|
113
|
+
};
|
|
114
|
+
this.#serviceListeners.set(subscriptionId, active);
|
|
115
|
+
let snapshot;
|
|
116
|
+
try {
|
|
117
|
+
snapshot = await this.#request(target, createServiceSubscribeCall(subscriptionId, serviceId, mode), signal, (result) => {
|
|
118
|
+
const decoded = active.decoder.decodeSnapshot(parseWireServiceSubscriptionSnapshot(result));
|
|
119
|
+
active.hydrated = true;
|
|
120
|
+
for (const update of active.queuedWireUpdates.splice(0)) {
|
|
121
|
+
active.queued.push(active.decoder.decodeUpdate(parseWireServiceProviderUpdate(update)));
|
|
122
|
+
}
|
|
123
|
+
return decoded;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (this.#serviceListeners.get(subscriptionId) === active)
|
|
128
|
+
this.#serviceListeners.delete(subscriptionId);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (this.#serviceListeners.get(subscriptionId) !== active)
|
|
132
|
+
throw new DisconnectedError();
|
|
133
|
+
let disposed = false;
|
|
134
|
+
return {
|
|
135
|
+
id: subscriptionId,
|
|
136
|
+
target,
|
|
137
|
+
snapshot,
|
|
138
|
+
start: () => {
|
|
139
|
+
if (disposed || active.ready)
|
|
140
|
+
return;
|
|
141
|
+
active.ready = true;
|
|
142
|
+
for (const update of active.queued.splice(0))
|
|
143
|
+
this.#deliverServiceUpdate(active, update);
|
|
144
|
+
},
|
|
145
|
+
dispose: async () => {
|
|
146
|
+
if (disposed)
|
|
147
|
+
return;
|
|
148
|
+
disposed = true;
|
|
149
|
+
if (this.#serviceListeners.get(subscriptionId) === active)
|
|
150
|
+
this.#serviceListeners.delete(subscriptionId);
|
|
151
|
+
try {
|
|
152
|
+
if (this.connected && this.#targetIsCurrent(target)) {
|
|
153
|
+
await this.#request(target, createServiceUnsubscribeCall(subscriptionId));
|
|
154
|
+
}
|
|
155
|
+
await active.deliveryTail;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
active.queuedWireUpdates.length = 0;
|
|
159
|
+
active.queued.length = 0;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
#request(target, call, signal, transform) {
|
|
165
|
+
if (this.#disposed)
|
|
166
|
+
return Promise.reject(new ClientDisposedError());
|
|
167
|
+
if (!this.connected)
|
|
168
|
+
return Promise.reject(new DisconnectedError());
|
|
169
|
+
if (signal?.aborted)
|
|
170
|
+
return Promise.reject(abortError(signal));
|
|
171
|
+
const id = `request-${++this.#requestSequence}`;
|
|
172
|
+
const { promise, resolve, reject } = createPromiseResolvers();
|
|
173
|
+
let sent = false;
|
|
174
|
+
let aborted = false;
|
|
175
|
+
let onAbort;
|
|
176
|
+
const sendCancel = () => {
|
|
177
|
+
if (!sent || !this.connected)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
this.#connection.send(encodeClientMessage({ type: "cancel", id, target }, { maxFrameLength: this.#connection.maxFrameLength }));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.#connection.fail(toError(error));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
if (signal !== undefined) {
|
|
187
|
+
onAbort = () => {
|
|
188
|
+
if (aborted)
|
|
189
|
+
return;
|
|
190
|
+
aborted = true;
|
|
191
|
+
reject(abortError(signal));
|
|
192
|
+
sendCancel();
|
|
193
|
+
};
|
|
194
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
195
|
+
}
|
|
196
|
+
this.#pendingRequests.set(id, {
|
|
197
|
+
resolve: (result) => {
|
|
198
|
+
try {
|
|
199
|
+
resolve(transform === undefined ? result : transform(result));
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const validationError = new ProtocolValidationError(error instanceof Error ? error.message : "Invalid service operation stream");
|
|
203
|
+
this.#connection.fail(validationError);
|
|
204
|
+
reject(validationError);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
reject,
|
|
208
|
+
cleanup: () => {
|
|
209
|
+
if (signal !== undefined && onAbort !== undefined)
|
|
210
|
+
signal.removeEventListener("abort", onAbort);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
let frame;
|
|
214
|
+
try {
|
|
215
|
+
frame = encodeClientMessage({ type: "request", id, target, call: parseServiceCall(call) }, { maxFrameLength: this.#connection.maxFrameLength });
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.#takePendingRequest(id)?.reject(toError(error));
|
|
219
|
+
return promise;
|
|
220
|
+
}
|
|
221
|
+
this.#connection.send(frame);
|
|
222
|
+
sent = true;
|
|
223
|
+
if (aborted)
|
|
224
|
+
sendCancel();
|
|
225
|
+
return promise;
|
|
226
|
+
}
|
|
227
|
+
#handleMessage(message) {
|
|
228
|
+
if (message.type === "attachment") {
|
|
229
|
+
if (message.attachment !== null && message.attachment.serverId !== this.#options.serverId) {
|
|
230
|
+
this.#connection.fail(new ProtocolValidationError("Attachment update belongs to another server"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.#setAttachment(message.attachment ?? undefined);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (message.type === "service_update") {
|
|
237
|
+
const active = this.#serviceListeners.get(message.subscriptionId);
|
|
238
|
+
if (active === undefined)
|
|
239
|
+
return;
|
|
240
|
+
if (!active.hydrated) {
|
|
241
|
+
active.queuedWireUpdates.push(message.update);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let update;
|
|
245
|
+
try {
|
|
246
|
+
update = active.decoder.decodeUpdate(parseWireServiceProviderUpdate(message.update));
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
this.#connection.fail(new ProtocolValidationError(error instanceof Error ? error.message : "Invalid service operation stream"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (active.ready)
|
|
253
|
+
this.#deliverServiceUpdate(active, update);
|
|
254
|
+
else
|
|
255
|
+
active.queued.push(update);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const pending = this.#takePendingRequest(message.id);
|
|
259
|
+
if (!pending) {
|
|
260
|
+
this.#connection.fail(new ProtocolValidationError("Response has no matching request"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!message.ok) {
|
|
264
|
+
pending.reject(new ServerError(message.error));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
pending.resolve(message.result);
|
|
268
|
+
}
|
|
269
|
+
#handleConnectionStateChange(change) {
|
|
270
|
+
if (change.state === "disconnected") {
|
|
271
|
+
this.#hello = undefined;
|
|
272
|
+
this.#setAttachment(undefined);
|
|
273
|
+
this.#rejectPendingRequests(change.error ?? new DisconnectedError());
|
|
274
|
+
this.#serviceListeners.clear();
|
|
275
|
+
}
|
|
276
|
+
for (const listener of this.#connectionStateListeners) {
|
|
277
|
+
try {
|
|
278
|
+
listener(change);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
this.#reportListenerError(error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
#takePendingRequest(id) {
|
|
286
|
+
const request = this.#pendingRequests.get(id);
|
|
287
|
+
if (request) {
|
|
288
|
+
this.#pendingRequests.delete(id);
|
|
289
|
+
request.cleanup();
|
|
290
|
+
}
|
|
291
|
+
return request;
|
|
292
|
+
}
|
|
293
|
+
#rejectPendingRequests(error) {
|
|
294
|
+
const requests = [...this.#pendingRequests.values()];
|
|
295
|
+
this.#pendingRequests.clear();
|
|
296
|
+
for (const request of requests) {
|
|
297
|
+
request.cleanup();
|
|
298
|
+
request.reject(error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
dispose() {
|
|
302
|
+
if (this.#disposePromise)
|
|
303
|
+
return this.#disposePromise;
|
|
304
|
+
this.#disposed = true;
|
|
305
|
+
this.#disposePromise = Promise.resolve();
|
|
306
|
+
const error = new ClientDisposedError();
|
|
307
|
+
this.#rejectPendingRequests(error);
|
|
308
|
+
this.#connection.disconnect(error);
|
|
309
|
+
this.#hello = undefined;
|
|
310
|
+
this.#setAttachment(undefined);
|
|
311
|
+
this.#connectionStateListeners.clear();
|
|
312
|
+
this.#attachmentListeners.clear();
|
|
313
|
+
this.#serviceListeners.clear();
|
|
314
|
+
return this.#disposePromise;
|
|
315
|
+
}
|
|
316
|
+
[Symbol.asyncDispose]() {
|
|
317
|
+
return this.dispose();
|
|
318
|
+
}
|
|
319
|
+
#setAttachment(attachment) {
|
|
320
|
+
const previous = this.#attachment;
|
|
321
|
+
if (previous?.serverId === attachment?.serverId &&
|
|
322
|
+
previous?.sessionId === attachment?.sessionId &&
|
|
323
|
+
previous?.attachmentId === attachment?.attachmentId) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.#attachment = attachment;
|
|
327
|
+
for (const listener of this.#attachmentListeners) {
|
|
328
|
+
try {
|
|
329
|
+
listener(attachment);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
this.#reportListenerError(error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
#deliverServiceUpdate(active, update) {
|
|
337
|
+
active.deliveryTail = active.deliveryTail
|
|
338
|
+
.then(() => active.listener(update))
|
|
339
|
+
.catch((error) => this.#reportListenerError(error));
|
|
340
|
+
}
|
|
341
|
+
#targetIsCurrent(target) {
|
|
342
|
+
if (!("sessionId" in target))
|
|
343
|
+
return this.#hello?.serverId === target.serverId;
|
|
344
|
+
const attachment = this.#attachment;
|
|
345
|
+
return (attachment?.serverId === target.serverId &&
|
|
346
|
+
attachment.sessionId === target.sessionId &&
|
|
347
|
+
attachment.attachmentId === target.attachmentId);
|
|
348
|
+
}
|
|
349
|
+
#assertNotDisposed() {
|
|
350
|
+
if (this.#disposed)
|
|
351
|
+
throw new ClientDisposedError();
|
|
352
|
+
}
|
|
353
|
+
#reportListenerError(error) {
|
|
354
|
+
if (!this.#options.onListenerError)
|
|
355
|
+
return;
|
|
356
|
+
try {
|
|
357
|
+
this.#options.onListenerError(toError(error));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Diagnostics cannot affect protocol or transport state.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Adapts a lazily resolved routed client target to a Chord service transport. */
|
|
365
|
+
export function createClientServiceTransport(client, getTarget) {
|
|
366
|
+
const target = () => {
|
|
367
|
+
const resolved = getTarget();
|
|
368
|
+
if (resolved === undefined)
|
|
369
|
+
throw new Error("Remote service target is unavailable");
|
|
370
|
+
return resolved;
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
invoke: async (call, context) => client.request(target(), call, context.abortSignal),
|
|
374
|
+
async subscribe(serviceId, mode, listener, context) {
|
|
375
|
+
const subscription = await client.subscribeService(target(), serviceId, mode, (update) => listener(update, BACKGROUND_CONTEXT), context.abortSignal);
|
|
376
|
+
return {
|
|
377
|
+
snapshot: subscription.snapshot,
|
|
378
|
+
activate: () => subscription.start(),
|
|
379
|
+
close: () => subscription.dispose(),
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function abortError(signal) {
|
|
385
|
+
const reason = signal.reason;
|
|
386
|
+
return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
|
|
387
|
+
}
|
|
388
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tcreateServiceCatalogueCall,\n\tcreateServiceStateDecoder,\n\tcreateServiceSubscribeCall,\n\tcreateServiceUnsubscribeCall,\n\ttype JsonValue,\n\tparseServiceCall,\n\tparseServiceCatalogue,\n\tparseWireServiceProviderUpdate,\n\tparseWireServiceSubscriptionSnapshot,\n\ttype RemoteServiceTransport,\n\ttype ServiceCall,\n\ttype ServiceCatalogueEntry,\n\ttype ServiceMode,\n\ttype ServiceProviderUpdate,\n\ttype ServiceStateDecoder,\n\ttype ServiceSubscriptionSnapshot,\n} from \"@m4ike1/chord\";\nimport { BACKGROUND_CONTEXT } from \"@m4ike1/chord/context\";\nimport {\n\ttype AttachmentEnvelope,\n\tencodeClientMessage,\n\tisServerId,\n\tProtocolValidationError,\n\ttype ResponseEnvelope,\n\ttype RpcTarget,\n\ttype ServerHello,\n\ttype ServiceEventEnvelope,\n\ttype SessionTarget,\n} from \"@m4ike1/ion-protocol\";\nimport { Connection } from \"./connection.ts\";\nimport { ClientDisposedError, DisconnectedError, ServerError, toError } from \"./errors.ts\";\nimport { createPromiseResolvers } from \"./promise.ts\";\nimport type {\n\tAttachmentChangeListener,\n\tClientOptions,\n\tConnectionState,\n\tConnectionStateChange,\n\tServiceSubscription,\n\tUnsubscribe,\n} from \"./types.ts\";\n\ntype ServiceResult = JsonValue | undefined;\n\ninterface PendingRequest {\n\tresolve(result: ServiceResult): void;\n\treject(error: Error): void;\n\tcleanup(): void;\n}\n\ninterface ActiveServiceListener {\n\treadonly target: RpcTarget;\n\treadonly listener: (update: ServiceProviderUpdate) => void | Promise<void>;\n\treadonly decoder: ServiceStateDecoder;\n\treadonly queuedWireUpdates: JsonValue[];\n\treadonly queued: ServiceProviderUpdate[];\n\tdeliveryTail: Promise<void>;\n\thydrated: boolean;\n\tready: boolean;\n}\n\nexport class Client {\n\treadonly #options: ClientOptions;\n\treadonly #connection: Connection;\n\treadonly #pendingRequests = new Map<string, PendingRequest>();\n\treadonly #connectionStateListeners = new Set<(change: ConnectionStateChange) => void>();\n\treadonly #attachmentListeners = new Set<AttachmentChangeListener>();\n\treadonly #serviceListeners = new Map<string, ActiveServiceListener>();\n\t#requestSequence = 0;\n\t#serviceSubscriptionSequence = 0;\n\t#hello: ServerHello | undefined;\n\t#attachment: SessionTarget | undefined;\n\t#disposed = false;\n\t#disposePromise: Promise<void> | undefined;\n\n\tconstructor(options: ClientOptions) {\n\t\tif (!isServerId(options.serverId)) {\n\t\t\tthrow new TypeError(\"serverId must be a canonical lowercase UUIDv4\");\n\t\t}\n\t\tthis.#options = options;\n\t\tthis.#connection = new Connection({\n\t\t\ttransportFactory: options.transportFactory,\n\t\t\tserverId: options.serverId,\n\t\t\tmaxFrameLength: options.maxFrameLength,\n\t\t\tonHandshake: (hello) => {\n\t\t\t\tthis.#hello = hello;\n\t\t\t},\n\t\t\tonMessage: (message) => this.#handleMessage(message),\n\t\t\tonStateChange: (change) => this.#handleConnectionStateChange(change),\n\t\t});\n\t}\n\n\tget disposed(): boolean {\n\t\treturn this.#disposed;\n\t}\n\n\tget connectionState(): ConnectionState {\n\t\treturn this.#connection.state;\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connection.state === \"connected\";\n\t}\n\n\tget serverId(): string {\n\t\treturn this.#options.serverId;\n\t}\n\n\tget hello(): ServerHello | undefined {\n\t\treturn this.#hello;\n\t}\n\n\tget attachment(): SessionTarget | undefined {\n\t\treturn this.#attachment;\n\t}\n\n\tstatic async connect(options: ClientOptions): Promise<Client> {\n\t\tconst client = new Client(options);\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\treturn client;\n\t\t} catch (error) {\n\t\t\tawait client.dispose();\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tconnect(): Promise<ServerHello> {\n\t\tif (this.#disposed) return Promise.reject(new ClientDisposedError());\n\t\tthis.#hello = undefined;\n\t\treturn this.#connection.connect();\n\t}\n\n\treconnect(): Promise<ServerHello> {\n\t\treturn this.connect();\n\t}\n\n\tdisconnect(reason = \"Client disconnected\"): void {\n\t\tthis.#connection.disconnect(reason);\n\t}\n\n\tonConnectionStateChange(listener: (change: ConnectionStateChange) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\tthis.#connectionStateListeners.add(listener);\n\t\treturn () => this.#connectionStateListeners.delete(listener);\n\t}\n\n\tonAttachmentChange(listener: AttachmentChangeListener): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\tthis.#attachmentListeners.add(listener);\n\t\treturn () => this.#attachmentListeners.delete(listener);\n\t}\n\n\t/** Invoke one low-level protocol call against an explicit routed target. */\n\trequest(target: RpcTarget, call: ServiceCall, signal?: AbortSignal): Promise<ServiceResult> {\n\t\treturn this.#request(target, call, signal);\n\t}\n\n\tasync serviceCatalogue(target: RpcTarget, signal?: AbortSignal): Promise<readonly ServiceCatalogueEntry[]> {\n\t\tconst result = await this.#request(target, createServiceCatalogueCall(), signal);\n\t\ttry {\n\t\t\treturn parseServiceCatalogue(result);\n\t\t} catch (error) {\n\t\t\tconst validationError = new ProtocolValidationError(\n\t\t\t\terror instanceof Error ? error.message : \"Invalid service catalogue\",\n\t\t\t);\n\t\t\tthis.#connection.fail(validationError);\n\t\t\tthrow validationError;\n\t\t}\n\t}\n\n\tasync subscribeService(\n\t\ttarget: RpcTarget,\n\t\tserviceId: string,\n\t\tmode: ServiceMode,\n\t\tlistener: (update: ServiceProviderUpdate) => void | Promise<void>,\n\t\tsignal?: AbortSignal,\n\t): Promise<ServiceSubscription> {\n\t\tconst subscriptionId = `service-${++this.#serviceSubscriptionSequence}`;\n\t\tconst active: ActiveServiceListener = {\n\t\t\ttarget,\n\t\t\tlistener,\n\t\t\tdecoder: createServiceStateDecoder(),\n\t\t\tqueuedWireUpdates: [],\n\t\t\tqueued: [],\n\t\t\tdeliveryTail: Promise.resolve(),\n\t\t\thydrated: false,\n\t\t\tready: false,\n\t\t};\n\t\tthis.#serviceListeners.set(subscriptionId, active);\n\t\tlet snapshot: ServiceSubscriptionSnapshot;\n\t\ttry {\n\t\t\tsnapshot = await this.#request(\n\t\t\t\ttarget,\n\t\t\t\tcreateServiceSubscribeCall(subscriptionId, serviceId, mode),\n\t\t\t\tsignal,\n\t\t\t\t(result) => {\n\t\t\t\t\tconst decoded = active.decoder.decodeSnapshot(parseWireServiceSubscriptionSnapshot(result));\n\t\t\t\t\tactive.hydrated = true;\n\t\t\t\t\tfor (const update of active.queuedWireUpdates.splice(0)) {\n\t\t\t\t\t\tactive.queued.push(active.decoder.decodeUpdate(parseWireServiceProviderUpdate(update)));\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded;\n\t\t\t\t},\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (this.#serviceListeners.get(subscriptionId) === active) this.#serviceListeners.delete(subscriptionId);\n\t\t\tthrow error;\n\t\t}\n\t\tif (this.#serviceListeners.get(subscriptionId) !== active) throw new DisconnectedError();\n\t\tlet disposed = false;\n\t\treturn {\n\t\t\tid: subscriptionId,\n\t\t\ttarget,\n\t\t\tsnapshot,\n\t\t\tstart: () => {\n\t\t\t\tif (disposed || active.ready) return;\n\t\t\t\tactive.ready = true;\n\t\t\t\tfor (const update of active.queued.splice(0)) this.#deliverServiceUpdate(active, update);\n\t\t\t},\n\t\t\tdispose: async () => {\n\t\t\t\tif (disposed) return;\n\t\t\t\tdisposed = true;\n\t\t\t\tif (this.#serviceListeners.get(subscriptionId) === active) this.#serviceListeners.delete(subscriptionId);\n\t\t\t\ttry {\n\t\t\t\t\tif (this.connected && this.#targetIsCurrent(target)) {\n\t\t\t\t\t\tawait this.#request(target, createServiceUnsubscribeCall(subscriptionId));\n\t\t\t\t\t}\n\t\t\t\t\tawait active.deliveryTail;\n\t\t\t\t} finally {\n\t\t\t\t\tactive.queuedWireUpdates.length = 0;\n\t\t\t\t\tactive.queued.length = 0;\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n\n\t#request<T = ServiceResult>(\n\t\ttarget: RpcTarget,\n\t\tcall: ServiceCall,\n\t\tsignal?: AbortSignal,\n\t\ttransform?: (result: ServiceResult) => T,\n\t): Promise<T> {\n\t\tif (this.#disposed) return Promise.reject(new ClientDisposedError());\n\t\tif (!this.connected) return Promise.reject(new DisconnectedError());\n\t\tif (signal?.aborted) return Promise.reject(abortError(signal));\n\t\tconst id = `request-${++this.#requestSequence}`;\n\t\tconst { promise, resolve, reject } = createPromiseResolvers<T>();\n\t\tlet sent = false;\n\t\tlet aborted = false;\n\t\tlet onAbort: (() => void) | undefined;\n\t\tconst sendCancel = (): void => {\n\t\t\tif (!sent || !this.connected) return;\n\t\t\ttry {\n\t\t\t\tthis.#connection.send(\n\t\t\t\t\tencodeClientMessage({ type: \"cancel\", id, target }, { maxFrameLength: this.#connection.maxFrameLength }),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#connection.fail(toError(error));\n\t\t\t}\n\t\t};\n\t\tif (signal !== undefined) {\n\t\t\tonAbort = () => {\n\t\t\t\tif (aborted) return;\n\t\t\t\taborted = true;\n\t\t\t\treject(abortError(signal));\n\t\t\t\tsendCancel();\n\t\t\t};\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tthis.#pendingRequests.set(id, {\n\t\t\tresolve: (result) => {\n\t\t\t\ttry {\n\t\t\t\t\tresolve(transform === undefined ? (result as T) : transform(result));\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst validationError = new ProtocolValidationError(\n\t\t\t\t\t\terror instanceof Error ? error.message : \"Invalid service operation stream\",\n\t\t\t\t\t);\n\t\t\t\t\tthis.#connection.fail(validationError);\n\t\t\t\t\treject(validationError);\n\t\t\t\t}\n\t\t\t},\n\t\t\treject,\n\t\t\tcleanup: () => {\n\t\t\t\tif (signal !== undefined && onAbort !== undefined) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t},\n\t\t});\n\t\tlet frame: Uint8Array;\n\t\ttry {\n\t\t\tframe = encodeClientMessage(\n\t\t\t\t{ type: \"request\", id, target, call: parseServiceCall(call) as unknown as JsonValue },\n\t\t\t\t{ maxFrameLength: this.#connection.maxFrameLength },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthis.#takePendingRequest(id)?.reject(toError(error));\n\t\t\treturn promise;\n\t\t}\n\t\tthis.#connection.send(frame);\n\t\tsent = true;\n\t\tif (aborted) sendCancel();\n\t\treturn promise;\n\t}\n\n\t#handleMessage(message: ResponseEnvelope | ServiceEventEnvelope | AttachmentEnvelope): void {\n\t\tif (message.type === \"attachment\") {\n\t\t\tif (message.attachment !== null && message.attachment.serverId !== this.#options.serverId) {\n\t\t\t\tthis.#connection.fail(new ProtocolValidationError(\"Attachment update belongs to another server\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.#setAttachment(message.attachment ?? undefined);\n\t\t\treturn;\n\t\t}\n\t\tif (message.type === \"service_update\") {\n\t\t\tconst active = this.#serviceListeners.get(message.subscriptionId);\n\t\t\tif (active === undefined) return;\n\t\t\tif (!active.hydrated) {\n\t\t\t\tactive.queuedWireUpdates.push(message.update);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet update: ServiceProviderUpdate;\n\t\t\ttry {\n\t\t\t\tupdate = active.decoder.decodeUpdate(parseWireServiceProviderUpdate(message.update));\n\t\t\t} catch (error) {\n\t\t\t\tthis.#connection.fail(\n\t\t\t\t\tnew ProtocolValidationError(error instanceof Error ? error.message : \"Invalid service operation stream\"),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (active.ready) this.#deliverServiceUpdate(active, update);\n\t\t\telse active.queued.push(update);\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.#takePendingRequest(message.id);\n\t\tif (!pending) {\n\t\t\tthis.#connection.fail(new ProtocolValidationError(\"Response has no matching request\"));\n\t\t\treturn;\n\t\t}\n\t\tif (!message.ok) {\n\t\t\tpending.reject(new ServerError(message.error));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(message.result);\n\t}\n\n\t#handleConnectionStateChange(change: ConnectionStateChange): void {\n\t\tif (change.state === \"disconnected\") {\n\t\t\tthis.#hello = undefined;\n\t\t\tthis.#setAttachment(undefined);\n\t\t\tthis.#rejectPendingRequests(change.error ?? new DisconnectedError());\n\t\t\tthis.#serviceListeners.clear();\n\t\t}\n\t\tfor (const listener of this.#connectionStateListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(change);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#reportListenerError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\t#takePendingRequest(id: string): PendingRequest | undefined {\n\t\tconst request = this.#pendingRequests.get(id);\n\t\tif (request) {\n\t\t\tthis.#pendingRequests.delete(id);\n\t\t\trequest.cleanup();\n\t\t}\n\t\treturn request;\n\t}\n\n\t#rejectPendingRequests(error: Error): void {\n\t\tconst requests = [...this.#pendingRequests.values()];\n\t\tthis.#pendingRequests.clear();\n\t\tfor (const request of requests) {\n\t\t\trequest.cleanup();\n\t\t\trequest.reject(error);\n\t\t}\n\t}\n\n\tdispose(): Promise<void> {\n\t\tif (this.#disposePromise) return this.#disposePromise;\n\t\tthis.#disposed = true;\n\t\tthis.#disposePromise = Promise.resolve();\n\t\tconst error = new ClientDisposedError();\n\t\tthis.#rejectPendingRequests(error);\n\t\tthis.#connection.disconnect(error);\n\t\tthis.#hello = undefined;\n\t\tthis.#setAttachment(undefined);\n\t\tthis.#connectionStateListeners.clear();\n\t\tthis.#attachmentListeners.clear();\n\t\tthis.#serviceListeners.clear();\n\t\treturn this.#disposePromise;\n\t}\n\n\t[Symbol.asyncDispose](): Promise<void> {\n\t\treturn this.dispose();\n\t}\n\n\t#setAttachment(attachment: SessionTarget | undefined): void {\n\t\tconst previous = this.#attachment;\n\t\tif (\n\t\t\tprevious?.serverId === attachment?.serverId &&\n\t\t\tprevious?.sessionId === attachment?.sessionId &&\n\t\t\tprevious?.attachmentId === attachment?.attachmentId\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthis.#attachment = attachment;\n\t\tfor (const listener of this.#attachmentListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(attachment);\n\t\t\t} catch (error) {\n\t\t\t\tthis.#reportListenerError(error);\n\t\t\t}\n\t\t}\n\t}\n\n\t#deliverServiceUpdate(active: ActiveServiceListener, update: ServiceProviderUpdate): void {\n\t\tactive.deliveryTail = active.deliveryTail\n\t\t\t.then(() => active.listener(update))\n\t\t\t.catch((error: unknown) => this.#reportListenerError(error));\n\t}\n\n\t#targetIsCurrent(target: RpcTarget): boolean {\n\t\tif (!(\"sessionId\" in target)) return this.#hello?.serverId === target.serverId;\n\t\tconst attachment = this.#attachment;\n\t\treturn (\n\t\t\tattachment?.serverId === target.serverId &&\n\t\t\tattachment.sessionId === target.sessionId &&\n\t\t\tattachment.attachmentId === target.attachmentId\n\t\t);\n\t}\n\n\t#assertNotDisposed(): void {\n\t\tif (this.#disposed) throw new ClientDisposedError();\n\t}\n\n\t#reportListenerError(error: unknown): void {\n\t\tif (!this.#options.onListenerError) return;\n\t\ttry {\n\t\t\tthis.#options.onListenerError(toError(error));\n\t\t} catch {\n\t\t\t// Diagnostics cannot affect protocol or transport state.\n\t\t}\n\t}\n}\n\n/** Adapts a lazily resolved routed client target to a Chord service transport. */\nexport function createClientServiceTransport(\n\tclient: Client,\n\tgetTarget: () => RpcTarget | undefined,\n): RemoteServiceTransport {\n\tconst target = (): RpcTarget => {\n\t\tconst resolved = getTarget();\n\t\tif (resolved === undefined) throw new Error(\"Remote service target is unavailable\");\n\t\treturn resolved;\n\t};\n\treturn {\n\t\tinvoke: async (call, context) => client.request(target(), call, context.abortSignal),\n\t\tasync subscribe(serviceId, mode, listener, context) {\n\t\t\tconst subscription = await client.subscribeService(\n\t\t\t\ttarget(),\n\t\t\t\tserviceId,\n\t\t\t\tmode,\n\t\t\t\t(update) => listener(update, BACKGROUND_CONTEXT),\n\t\t\t\tcontext.abortSignal,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tsnapshot: subscription.snapshot,\n\t\t\t\tactivate: () => subscription.start(),\n\t\t\t\tclose: () => subscription.dispose(),\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction abortError(signal: AbortSignal): Error {\n\tconst reason: unknown = signal.reason;\n\treturn reason instanceof Error ? reason : new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"]}
|