@sammorrowdrums/mcpi-client 0.85.0-bootstrap.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +385 -0
- package/dist/client.js.map +1 -0
- package/dist/connection.d.ts +24 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +204 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -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/session-handle.d.ts +54 -0
- package/dist/session-handle.d.ts.map +1 -0
- package/dist/session-handle.js +51 -0
- package/dist/session-handle.js.map +1 -0
- package/dist/state.d.ts +22 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +145 -0
- package/dist/state.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 +22 -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 +8 -0
- package/dist/unix.d.ts.map +1 -0
- package/dist/unix.js +151 -0
- package/dist/unix.js.map +1 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Renamed the public npm package from `@earendil-works/pi-client` to `@sammorrowdrums/mcpi-client`. The new package explicitly publishes to the public npm registry and requires Node.js 22.19 or newer.
|
|
8
|
+
|
|
9
|
+
## [0.84.2] - 2026-08-14
|
|
10
|
+
|
|
11
|
+
## [0.84.1] - 2026-08-07
|
|
12
|
+
|
|
13
|
+
## [0.84.0] - 2026-08-06
|
|
14
|
+
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
|
|
17
|
+
- Replaced `SessionSummary` with durable `SessionMetadata` for `PiClient.listSessions()` and server snapshots; runtime state is available only from acquired session snapshots ([#7708](https://github.com/earendil-works/pi/pull/7708)).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Added the experimental transport-neutral `PiClient` and multi-session `PiSessionHandle` APIs with structured `PiServerError` responses.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @sammorrowdrums/mcpi-client
|
|
2
|
+
|
|
3
|
+
Transport-neutral client for remote mcpi sessions. The compatibility-named `PiClient` API exchanges length-prefixed CBOR messages through a small `ByteTransport` interface. The package has no Node-specific imports.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { PiClient, type ByteTransportFactory } from "@sammorrowdrums/mcpi-client";
|
|
7
|
+
|
|
8
|
+
const transportFactory: ByteTransportFactory = async (handlers) => {
|
|
9
|
+
// Connect using WebSocket, Unix socket, or another ordered byte transport.
|
|
10
|
+
return {
|
|
11
|
+
async send(chunk) {
|
|
12
|
+
// Deliver chunks in invocation order and honor backpressure.
|
|
13
|
+
},
|
|
14
|
+
close() {},
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const client = new PiClient({ transportFactory });
|
|
19
|
+
await client.connect();
|
|
20
|
+
const session = await client.createSession({ cwd: "/workspace" });
|
|
21
|
+
const unsubscribe = session.subscribe((snapshot) => render(snapshot));
|
|
22
|
+
await session.prompt("Inspect this project");
|
|
23
|
+
unsubscribe();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Call `handlers.onData(chunk)` for inbound bytes, `handlers.onClose()` for an orderly terminal close, and `handlers.onError(error)` for transport failures. A factory must create a fresh transport for every connection attempt and complete any transport-specific authentication before resolving. For example, a WebSocket factory can provide credentials in its upgrade request.
|
|
27
|
+
|
|
28
|
+
`PiClient` does not reconnect automatically. Call `reconnect()` after disconnection. One connection can attach several sessions. Requests are correlated by ID. Server snapshots and successful response snapshots are authoritative, while progress events do not mutate snapshot state optimistically. Read cached session metadata from `client.snapshot?.sessions`; call `listSessions()` to request refreshed durable metadata from the server. Runtime state is available after acquiring a session.
|
|
29
|
+
|
|
30
|
+
`acquireSession()` returns an independent `SessionLease`; leases cannot be constructed directly. Use `{ mode: "exclusive" }` for a lifecycle or mutation coordinator and `{ mode: "shared" }` when multiple low-level consumers intentionally share the session. Exclusive acquisition fails with `PiSessionOwnershipError` while any lease exists, and shared acquisition fails while an exclusive lease exists. `attachSession()` is a shared-acquisition convenience method. `createSession()` returns an exclusive lease for the newly created session.
|
|
31
|
+
|
|
32
|
+
Calling `dispose()` or `detach()` releases only that lease. A lease rejects commands as soon as release begins. The client sends the protocol detach request after the final lease is released. If explicit `detach()` fails, the lease becomes active again for retry. If cleanup-oriented `dispose()` fails, it reports the protocol error but relinquishes local ownership; `PiClient` reconciles the failed protocol cleanup before the next acquisition. A released lease becomes unavailable without affecting other shared leases. Server removal or disconnection invalidates every lease for the affected attachment, and disposing an invalidated lease is a no-op. Commands fail with `PiDisconnectedError` while the client is disconnected and `PiSessionDetachedError` when the client is connected but a lease is releasing, released, or invalidated. Leases implement `AsyncDisposable`.
|
|
33
|
+
|
|
34
|
+
`subscribe()` observes authoritative snapshots. `onEvent()` observes protocol events. Both return an unsubscribe function. Structured errors returned by the server are exposed as `PiServerError`.
|
|
35
|
+
|
|
36
|
+
## Limits and security
|
|
37
|
+
|
|
38
|
+
`PiClientOptions.maxFrameLength` bounds inbound and outbound CBOR payloads. Configure matching limits on the client and server. Transports should separately bound queued outbound bytes and preserve send order.
|
|
39
|
+
|
|
40
|
+
Treat peers as untrusted. Use a secure transport with appropriate access controls and authenticate during transport establishment.
|
|
41
|
+
|
|
42
|
+
Subscriber exceptions are isolated from protocol state. Set `onListenerError` in `PiClientOptions` to report them to application logging or diagnostics.
|
|
43
|
+
|
|
44
|
+
## Unix-domain sockets
|
|
45
|
+
|
|
46
|
+
Node.js and Bun consumers can use the separately exported Unix-domain socket transport:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { PiClient } from "@sammorrowdrums/mcpi-client";
|
|
50
|
+
import { createUnixTransportFactory } from "@sammorrowdrums/mcpi-client/unix";
|
|
51
|
+
|
|
52
|
+
const client = new PiClient({
|
|
53
|
+
transportFactory: createUnixTransportFactory({
|
|
54
|
+
path: "/tmp/mcpi.sock",
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await client.connect();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`maxPendingBytes` bounds queued outbound data. It defaults to four times the protocol frame limit. The transport preserves send order and waits for socket backpressure before resolving each send.
|
|
62
|
+
|
|
63
|
+
The `@sammorrowdrums/mcpi-client` root remains transport- and runtime-neutral. Importing the Node-compatible transport requires the explicit `@sammorrowdrums/mcpi-client/unix` subpath.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ServerEvent, type ServerSnapshot, type SessionMetadata } from "@sammorrowdrums/mcpi-protocol";
|
|
2
|
+
import { type AcquireSessionOptions, type PiSessionHandle } from "./session-handle.ts";
|
|
3
|
+
import type { ConnectionState, ConnectionStateChange, CreateSessionOptions, PiClientOptions, Unsubscribe } from "./types.ts";
|
|
4
|
+
export declare class PiClient {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(options: PiClientOptions);
|
|
7
|
+
get disposed(): boolean;
|
|
8
|
+
get connectionState(): ConnectionState;
|
|
9
|
+
get connected(): boolean;
|
|
10
|
+
get snapshot(): ServerSnapshot | undefined;
|
|
11
|
+
static connect(options: PiClientOptions): Promise<PiClient>;
|
|
12
|
+
connect(): Promise<ServerSnapshot>;
|
|
13
|
+
reconnect(): Promise<ServerSnapshot>;
|
|
14
|
+
disconnect(reason?: string): void;
|
|
15
|
+
subscribe(listener: (snapshot: ServerSnapshot) => void): Unsubscribe;
|
|
16
|
+
onEvent(listener: (event: ServerEvent) => void): Unsubscribe;
|
|
17
|
+
onConnectionStateChange(listener: (change: ConnectionStateChange) => void): Unsubscribe;
|
|
18
|
+
listSessions(): Promise<readonly SessionMetadata[]>;
|
|
19
|
+
createSession(options?: CreateSessionOptions): Promise<PiSessionHandle>;
|
|
20
|
+
attachSession(sessionId: string): Promise<PiSessionHandle>;
|
|
21
|
+
acquireSession(sessionId: string, options: AcquireSessionOptions): Promise<PiSessionHandle>;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAQN,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,+BAA+B,CAAC;AAWvC,OAAO,EACN,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EAIpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACX,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,MAAM,YAAY,CAAC;AAcpB,qBAAa,QAAQ;;IAiBpB,YAAY,OAAO,EAAE,eAAe,EAUnC;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,eAAe,IAAI,eAAe,CAErC;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,QAAQ,IAAI,cAAc,GAAG,SAAS,CAEzC;IAED,OAAa,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAShE;IAED,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC,CAIjC;IAED,SAAS,IAAI,OAAO,CAAC,cAAc,CAAC,CAEnC;IAED,UAAU,CAAC,MAAM,SAAwB,GAAG,IAAI,CAE/C;IAED,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,WAAW,CAGnE;IAED,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,WAAW,CAG3D;IAED,uBAAuB,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,GAAG,WAAW,CAItF;IAEK,YAAY,IAAI,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAExD;IAEK,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,eAAe,CAAC,CAIhF;IAEK,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAE/D;IAEK,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA0BhG;IAqKD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAWvB;IAED,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAErC;CA2ED","sourcesContent":["import {\n\ttype Command,\n\ttype CommandResult,\n\ttype EventEnvelope,\n\tencodeClientMessage,\n\tProtocolValidationError,\n\ttype ResponseEnvelope,\n\ttype ResultForCommand,\n\ttype ServerEvent,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@sammorrowdrums/mcpi-protocol\";\nimport { Connection } from \"./connection.ts\";\nimport {\n\tPiClientDisposedError,\n\tPiDisconnectedError,\n\tPiServerError,\n\tPiSessionDetachedError,\n\tPiSessionOwnershipError,\n\ttoError,\n} from \"./errors.ts\";\nimport { createPromiseResolvers } from \"./promise.ts\";\nimport {\n\ttype AcquireSessionOptions,\n\ttype PiSessionHandle,\n\tSessionHandle,\n\ttype SessionHandleCallbacks,\n\ttype SessionLeaseMode,\n} from \"./session-handle.ts\";\nimport { ClientState } from \"./state.ts\";\nimport type {\n\tConnectionState,\n\tConnectionStateChange,\n\tCreateSessionOptions,\n\tPiClientOptions,\n\tUnsubscribe,\n} from \"./types.ts\";\n\ntype SessionLeaseState = \"active\" | \"releasing\" | \"released\" | \"invalidated\";\n\ninterface SessionLeaseToken {\n\treadonly mode: SessionLeaseMode;\n}\n\ninterface PendingRequest {\n\tcommand: Command;\n\tresolve(result: CommandResult): void;\n\treject(error: Error): void;\n}\n\nexport class PiClient {\n\treadonly #options: PiClientOptions;\n\treadonly #connection: Connection;\n\treadonly #state: ClientState;\n\treadonly #pendingRequests = new Map<string, PendingRequest>();\n\treadonly #sessionLeaseCounts = new Map<string, number>();\n\treadonly #exclusiveSessionLeases = new Map<string, SessionLeaseToken>();\n\treadonly #sessionLeaseGenerations = new Map<string, number>();\n\treadonly #sessionAttachments = new Map<string, Promise<void>>();\n\treadonly #sessionDetachments = new Map<string, Promise<void>>();\n\treadonly #sessionCleanupRequired = new Set<string>();\n\treadonly #sessionReconciliations = new Map<string, Promise<void>>();\n\treadonly #connectionStateListeners = new Set<(change: ConnectionStateChange) => void>();\n\t#requestSequence = 0;\n\t#disposed = false;\n\t#disposePromise: Promise<void> | undefined;\n\n\tconstructor(options: PiClientOptions) {\n\t\tthis.#options = options;\n\t\tthis.#state = new ClientState(options.onListenerError);\n\t\tthis.#connection = new Connection({\n\t\t\ttransportFactory: options.transportFactory,\n\t\t\tmaxFrameLength: options.maxFrameLength,\n\t\t\tonHandshake: (snapshot) => this.#state.applyServerSnapshot(snapshot),\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 snapshot(): ServerSnapshot | undefined {\n\t\treturn this.#state.snapshot;\n\t}\n\n\tstatic async connect(options: PiClientOptions): Promise<PiClient> {\n\t\tconst client = new PiClient(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<ServerSnapshot> {\n\t\tif (this.#disposed) return Promise.reject(new PiClientDisposedError());\n\t\tif (this.#connection.state === \"disconnected\") this.#state.reset();\n\t\treturn this.#connection.connect();\n\t}\n\n\treconnect(): Promise<ServerSnapshot> {\n\t\treturn this.connect();\n\t}\n\n\tdisconnect(reason = \"Client disconnected\"): void {\n\t\tthis.#connection.disconnect(reason);\n\t}\n\n\tsubscribe(listener: (snapshot: ServerSnapshot) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\treturn this.#state.subscribe(listener);\n\t}\n\n\tonEvent(listener: (event: ServerEvent) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\treturn this.#state.onEvent(listener);\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\tasync listSessions(): Promise<readonly SessionMetadata[]> {\n\t\treturn (await this.#request({ command: \"list\" })).sessions;\n\t}\n\n\tasync createSession(options: CreateSessionOptions = {}): Promise<PiSessionHandle> {\n\t\tconst result = await this.#request({ command: \"create\", ...options });\n\t\tconst token = this.#reserveSessionLease(result.session.id, \"exclusive\");\n\t\treturn this.#createSessionLease(result.session.id, token);\n\t}\n\n\tasync attachSession(sessionId: string): Promise<PiSessionHandle> {\n\t\treturn this.acquireSession(sessionId, { mode: \"shared\" });\n\t}\n\n\tasync acquireSession(sessionId: string, options: AcquireSessionOptions): Promise<PiSessionHandle> {\n\t\tthis.#assertNotDisposed();\n\t\tconst token = this.#reserveSessionLease(sessionId, options.mode);\n\t\ttry {\n\t\t\tconst detachment = this.#sessionDetachments.get(sessionId);\n\t\t\tif (detachment) await detachment.catch(() => {});\n\t\t\tconst reconciled = this.#sessionCleanupRequired.has(sessionId)\n\t\t\t\t? await this.#reconcileSessionCleanup(sessionId)\n\t\t\t\t: false;\n\t\t\tif (reconciled || !this.#state.isSessionAttached(sessionId)) {\n\t\t\t\tlet attachment = this.#sessionAttachments.get(sessionId);\n\t\t\t\tif (!attachment) {\n\t\t\t\t\tattachment = this.#attachSession(sessionId);\n\t\t\t\t\tthis.#sessionAttachments.set(sessionId, attachment);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait attachment;\n\t\t\t\t} finally {\n\t\t\t\t\tif (this.#sessionAttachments.get(sessionId) === attachment) this.#sessionAttachments.delete(sessionId);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.#createSessionLease(sessionId, token);\n\t\t} catch (error) {\n\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync #attachSession(sessionId: string): Promise<void> {\n\t\tconst previous = this.#state.forgetSessionSnapshot(sessionId);\n\t\ttry {\n\t\t\tawait this.#request({ command: \"attach\", sessionId });\n\t\t} catch (error) {\n\t\t\tif (previous) this.#state.restoreSessionSnapshot(previous);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t#request<const TCommand extends Command>(command: TCommand): Promise<ResultForCommand<TCommand>> {\n\t\tif (this.#disposed) return Promise.reject(new PiClientDisposedError());\n\t\tif (!this.connected) return Promise.reject(new PiDisconnectedError());\n\t\tconst id = `request-${++this.#requestSequence}`;\n\t\tconst { promise, resolve, reject } = createPromiseResolvers<CommandResult>();\n\t\tthis.#pendingRequests.set(id, { command, resolve, reject });\n\t\tlet frame: Uint8Array;\n\t\ttry {\n\t\t\tframe = encodeClientMessage(\n\t\t\t\t{ type: \"request\", id, request: command },\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 as Promise<ResultForCommand<TCommand>>;\n\t\t}\n\t\tthis.#connection.send(frame);\n\t\treturn promise as Promise<ResultForCommand<TCommand>>;\n\t}\n\n\t#createSessionLease(sessionId: string, token: SessionLeaseToken): PiSessionHandle {\n\t\tconst generation = this.#sessionLeaseGenerations.get(sessionId) ?? 0;\n\t\tthis.#sessionLeaseGenerations.set(sessionId, generation);\n\t\tlet state: SessionLeaseState = \"active\";\n\t\tlet releasePromise: Promise<void> | undefined;\n\t\tconst refreshState = () => {\n\t\t\tif (\n\t\t\t\t(state === \"active\" || state === \"releasing\") &&\n\t\t\t\tthis.#sessionLeaseGenerations.get(sessionId) !== generation\n\t\t\t) {\n\t\t\t\tstate = \"invalidated\";\n\t\t\t}\n\t\t};\n\t\tconst isActive = () => {\n\t\t\trefreshState();\n\t\t\treturn state === \"active\" && this.#state.isSessionAttached(sessionId);\n\t\t};\n\t\tconst assertActive = () => {\n\t\t\tthis.#assertNotDisposed();\n\t\t\tif (!this.connected) throw new PiDisconnectedError();\n\t\t\tif (!isActive()) throw new PiSessionDetachedError(sessionId);\n\t\t};\n\t\tconst release = (relinquishOnFailure: boolean): Promise<void> => {\n\t\t\trefreshState();\n\t\t\tif (state === \"released\" || state === \"invalidated\") return Promise.resolve();\n\t\t\tif (releasePromise) return releasePromise;\n\t\t\tassertActive();\n\t\t\tstate = \"releasing\";\n\t\t\treleasePromise = (async () => {\n\t\t\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\t\t\tif (count <= 1) {\n\t\t\t\t\tconst detachment = this.#request({ command: \"detach\", sessionId }).then(() => undefined);\n\t\t\t\t\tthis.#sessionDetachments.set(sessionId, detachment);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait detachment;\n\t\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (this.#sessionDetachments.get(sessionId) === detachment) {\n\t\t\t\t\t\t\tthis.#sessionDetachments.delete(sessionId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t}\n\t\t\t\tstate = \"released\";\n\t\t\t})().catch((error: unknown) => {\n\t\t\t\trefreshState();\n\t\t\t\tif (state === \"invalidated\") return;\n\t\t\t\tif (relinquishOnFailure) {\n\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t\tthis.#sessionCleanupRequired.add(sessionId);\n\t\t\t\t\tstate = \"released\";\n\t\t\t\t} else {\n\t\t\t\t\tstate = \"active\";\n\t\t\t\t\treleasePromise = undefined;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\treturn releasePromise;\n\t\t};\n\t\tconst callbacks: SessionHandleCallbacks = {\n\t\t\tisAttached: isActive,\n\t\t\tgetSnapshot: () => (isActive() ? this.#state.getSessionSnapshot(sessionId) : undefined),\n\t\t\tsubscribe: (listener) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#state.subscribeSession(sessionId, (snapshot) => {\n\t\t\t\t\tif (isActive()) listener(snapshot);\n\t\t\t\t});\n\t\t\t},\n\t\t\tonEvent: (listener) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#state.onSessionEvent(sessionId, (event) => {\n\t\t\t\t\tif (isActive() || event.type === \"session_removed\") listener(event);\n\t\t\t\t});\n\t\t\t},\n\t\t\tdetach: () => release(false),\n\t\t\tdispose: () => release(true),\n\t\t\trequest: (command) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#request(command);\n\t\t\t},\n\t\t};\n\t\treturn new SessionHandle(sessionId, callbacks);\n\t}\n\n\t#handleMessage(message: ResponseEnvelope | EventEnvelope): void {\n\t\tif (message.type === \"event\") {\n\t\t\tif (message.event.type === \"session_removed\") this.#invalidateSessionLeases(message.event.sessionId);\n\t\t\tthis.#state.applyEvent(message.event);\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 PiServerError(message.error));\n\t\t\treturn;\n\t\t}\n\t\tif (message.result.command !== pending.command.command) {\n\t\t\tconst error = new ProtocolValidationError(\n\t\t\t\t`Response command ${message.result.command} does not match ${pending.command.command}`,\n\t\t\t);\n\t\t\tpending.reject(error);\n\t\t\tthis.#connection.fail(error);\n\t\t\treturn;\n\t\t}\n\t\tthis.#state.applyResult(message.result);\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.#state.clearAttachments();\n\t\t\tthis.#invalidateAllSessionLeases();\n\t\t\tthis.#rejectPendingRequests(change.error ?? new PiDisconnectedError());\n\t\t}\n\t\tthis.#notifyConnectionStateListeners(change);\n\t}\n\n\t#takePendingRequest(id: string): PendingRequest | undefined {\n\t\tconst request = this.#pendingRequests.get(id);\n\t\tif (request) this.#pendingRequests.delete(id);\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) request.reject(error);\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 PiClientDisposedError();\n\t\tthis.#rejectPendingRequests(error);\n\t\tthis.#connection.disconnect(error);\n\t\tthis.#state.dispose();\n\t\tthis.#invalidateAllSessionLeases();\n\t\tthis.#connectionStateListeners.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#assertNotDisposed(): void {\n\t\tif (this.#disposed) throw new PiClientDisposedError();\n\t}\n\n\tasync #reconcileSessionCleanup(sessionId: string): Promise<boolean> {\n\t\tif (!this.#sessionCleanupRequired.has(sessionId)) return false;\n\t\tlet reconciliation = this.#sessionReconciliations.get(sessionId);\n\t\tif (!reconciliation) {\n\t\t\treconciliation = this.#request({ command: \"detach\", sessionId })\n\t\t\t\t.then(() => undefined)\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.#sessionCleanupRequired.delete(sessionId);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.#sessionReconciliations.delete(sessionId);\n\t\t\t\t});\n\t\t\tthis.#sessionReconciliations.set(sessionId, reconciliation);\n\t\t}\n\t\tawait reconciliation;\n\t\treturn true;\n\t}\n\n\t#reserveSessionLease(sessionId: string, mode: SessionLeaseMode): SessionLeaseToken {\n\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\tif (mode === \"exclusive\" && count > 0) {\n\t\t\tthrow new PiSessionOwnershipError(sessionId, `Session ${sessionId} already has an active lease`);\n\t\t}\n\t\tif (mode === \"shared\" && this.#exclusiveSessionLeases.has(sessionId)) {\n\t\t\tthrow new PiSessionOwnershipError(sessionId, `Session ${sessionId} has an exclusive lease`);\n\t\t}\n\t\tconst token: SessionLeaseToken = { mode };\n\t\tthis.#sessionLeaseCounts.set(sessionId, count + 1);\n\t\tif (mode === \"exclusive\") this.#exclusiveSessionLeases.set(sessionId, token);\n\t\treturn token;\n\t}\n\n\t#releaseSessionLease(sessionId: string, token: SessionLeaseToken): void {\n\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\tif (count <= 1) this.#sessionLeaseCounts.delete(sessionId);\n\t\telse this.#sessionLeaseCounts.set(sessionId, count - 1);\n\t\tif (this.#exclusiveSessionLeases.get(sessionId) === token) this.#exclusiveSessionLeases.delete(sessionId);\n\t}\n\n\t#invalidateSessionLeases(sessionId: string): void {\n\t\tthis.#sessionLeaseCounts.delete(sessionId);\n\t\tthis.#exclusiveSessionLeases.delete(sessionId);\n\t\tthis.#sessionCleanupRequired.delete(sessionId);\n\t\tthis.#sessionLeaseGenerations.set(sessionId, (this.#sessionLeaseGenerations.get(sessionId) ?? 0) + 1);\n\t}\n\n\t#invalidateAllSessionLeases(): void {\n\t\tfor (const sessionId of this.#sessionLeaseCounts.keys()) this.#invalidateSessionLeases(sessionId);\n\t\tthis.#sessionCleanupRequired.clear();\n\t}\n\n\t#notifyConnectionStateListeners(change: ConnectionStateChange): void {\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#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"]}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { encodeClientMessage, ProtocolValidationError, } from "@sammorrowdrums/mcpi-protocol";
|
|
2
|
+
import { Connection } from "./connection.js";
|
|
3
|
+
import { PiClientDisposedError, PiDisconnectedError, PiServerError, PiSessionDetachedError, PiSessionOwnershipError, toError, } from "./errors.js";
|
|
4
|
+
import { createPromiseResolvers } from "./promise.js";
|
|
5
|
+
import { SessionHandle, } from "./session-handle.js";
|
|
6
|
+
import { ClientState } from "./state.js";
|
|
7
|
+
export class PiClient {
|
|
8
|
+
#options;
|
|
9
|
+
#connection;
|
|
10
|
+
#state;
|
|
11
|
+
#pendingRequests = new Map();
|
|
12
|
+
#sessionLeaseCounts = new Map();
|
|
13
|
+
#exclusiveSessionLeases = new Map();
|
|
14
|
+
#sessionLeaseGenerations = new Map();
|
|
15
|
+
#sessionAttachments = new Map();
|
|
16
|
+
#sessionDetachments = new Map();
|
|
17
|
+
#sessionCleanupRequired = new Set();
|
|
18
|
+
#sessionReconciliations = new Map();
|
|
19
|
+
#connectionStateListeners = new Set();
|
|
20
|
+
#requestSequence = 0;
|
|
21
|
+
#disposed = false;
|
|
22
|
+
#disposePromise;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#options = options;
|
|
25
|
+
this.#state = new ClientState(options.onListenerError);
|
|
26
|
+
this.#connection = new Connection({
|
|
27
|
+
transportFactory: options.transportFactory,
|
|
28
|
+
maxFrameLength: options.maxFrameLength,
|
|
29
|
+
onHandshake: (snapshot) => this.#state.applyServerSnapshot(snapshot),
|
|
30
|
+
onMessage: (message) => this.#handleMessage(message),
|
|
31
|
+
onStateChange: (change) => this.#handleConnectionStateChange(change),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
get disposed() {
|
|
35
|
+
return this.#disposed;
|
|
36
|
+
}
|
|
37
|
+
get connectionState() {
|
|
38
|
+
return this.#connection.state;
|
|
39
|
+
}
|
|
40
|
+
get connected() {
|
|
41
|
+
return this.#connection.state === "connected";
|
|
42
|
+
}
|
|
43
|
+
get snapshot() {
|
|
44
|
+
return this.#state.snapshot;
|
|
45
|
+
}
|
|
46
|
+
static async connect(options) {
|
|
47
|
+
const client = new PiClient(options);
|
|
48
|
+
try {
|
|
49
|
+
await client.connect();
|
|
50
|
+
return client;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await client.dispose();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
connect() {
|
|
58
|
+
if (this.#disposed)
|
|
59
|
+
return Promise.reject(new PiClientDisposedError());
|
|
60
|
+
if (this.#connection.state === "disconnected")
|
|
61
|
+
this.#state.reset();
|
|
62
|
+
return this.#connection.connect();
|
|
63
|
+
}
|
|
64
|
+
reconnect() {
|
|
65
|
+
return this.connect();
|
|
66
|
+
}
|
|
67
|
+
disconnect(reason = "Client disconnected") {
|
|
68
|
+
this.#connection.disconnect(reason);
|
|
69
|
+
}
|
|
70
|
+
subscribe(listener) {
|
|
71
|
+
this.#assertNotDisposed();
|
|
72
|
+
return this.#state.subscribe(listener);
|
|
73
|
+
}
|
|
74
|
+
onEvent(listener) {
|
|
75
|
+
this.#assertNotDisposed();
|
|
76
|
+
return this.#state.onEvent(listener);
|
|
77
|
+
}
|
|
78
|
+
onConnectionStateChange(listener) {
|
|
79
|
+
this.#assertNotDisposed();
|
|
80
|
+
this.#connectionStateListeners.add(listener);
|
|
81
|
+
return () => this.#connectionStateListeners.delete(listener);
|
|
82
|
+
}
|
|
83
|
+
async listSessions() {
|
|
84
|
+
return (await this.#request({ command: "list" })).sessions;
|
|
85
|
+
}
|
|
86
|
+
async createSession(options = {}) {
|
|
87
|
+
const result = await this.#request({ command: "create", ...options });
|
|
88
|
+
const token = this.#reserveSessionLease(result.session.id, "exclusive");
|
|
89
|
+
return this.#createSessionLease(result.session.id, token);
|
|
90
|
+
}
|
|
91
|
+
async attachSession(sessionId) {
|
|
92
|
+
return this.acquireSession(sessionId, { mode: "shared" });
|
|
93
|
+
}
|
|
94
|
+
async acquireSession(sessionId, options) {
|
|
95
|
+
this.#assertNotDisposed();
|
|
96
|
+
const token = this.#reserveSessionLease(sessionId, options.mode);
|
|
97
|
+
try {
|
|
98
|
+
const detachment = this.#sessionDetachments.get(sessionId);
|
|
99
|
+
if (detachment)
|
|
100
|
+
await detachment.catch(() => { });
|
|
101
|
+
const reconciled = this.#sessionCleanupRequired.has(sessionId)
|
|
102
|
+
? await this.#reconcileSessionCleanup(sessionId)
|
|
103
|
+
: false;
|
|
104
|
+
if (reconciled || !this.#state.isSessionAttached(sessionId)) {
|
|
105
|
+
let attachment = this.#sessionAttachments.get(sessionId);
|
|
106
|
+
if (!attachment) {
|
|
107
|
+
attachment = this.#attachSession(sessionId);
|
|
108
|
+
this.#sessionAttachments.set(sessionId, attachment);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await attachment;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (this.#sessionAttachments.get(sessionId) === attachment)
|
|
115
|
+
this.#sessionAttachments.delete(sessionId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return this.#createSessionLease(sessionId, token);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.#releaseSessionLease(sessionId, token);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async #attachSession(sessionId) {
|
|
126
|
+
const previous = this.#state.forgetSessionSnapshot(sessionId);
|
|
127
|
+
try {
|
|
128
|
+
await this.#request({ command: "attach", sessionId });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (previous)
|
|
132
|
+
this.#state.restoreSessionSnapshot(previous);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
#request(command) {
|
|
137
|
+
if (this.#disposed)
|
|
138
|
+
return Promise.reject(new PiClientDisposedError());
|
|
139
|
+
if (!this.connected)
|
|
140
|
+
return Promise.reject(new PiDisconnectedError());
|
|
141
|
+
const id = `request-${++this.#requestSequence}`;
|
|
142
|
+
const { promise, resolve, reject } = createPromiseResolvers();
|
|
143
|
+
this.#pendingRequests.set(id, { command, resolve, reject });
|
|
144
|
+
let frame;
|
|
145
|
+
try {
|
|
146
|
+
frame = encodeClientMessage({ type: "request", id, request: command }, { maxFrameLength: this.#connection.maxFrameLength });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.#takePendingRequest(id)?.reject(toError(error));
|
|
150
|
+
return promise;
|
|
151
|
+
}
|
|
152
|
+
this.#connection.send(frame);
|
|
153
|
+
return promise;
|
|
154
|
+
}
|
|
155
|
+
#createSessionLease(sessionId, token) {
|
|
156
|
+
const generation = this.#sessionLeaseGenerations.get(sessionId) ?? 0;
|
|
157
|
+
this.#sessionLeaseGenerations.set(sessionId, generation);
|
|
158
|
+
let state = "active";
|
|
159
|
+
let releasePromise;
|
|
160
|
+
const refreshState = () => {
|
|
161
|
+
if ((state === "active" || state === "releasing") &&
|
|
162
|
+
this.#sessionLeaseGenerations.get(sessionId) !== generation) {
|
|
163
|
+
state = "invalidated";
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const isActive = () => {
|
|
167
|
+
refreshState();
|
|
168
|
+
return state === "active" && this.#state.isSessionAttached(sessionId);
|
|
169
|
+
};
|
|
170
|
+
const assertActive = () => {
|
|
171
|
+
this.#assertNotDisposed();
|
|
172
|
+
if (!this.connected)
|
|
173
|
+
throw new PiDisconnectedError();
|
|
174
|
+
if (!isActive())
|
|
175
|
+
throw new PiSessionDetachedError(sessionId);
|
|
176
|
+
};
|
|
177
|
+
const release = (relinquishOnFailure) => {
|
|
178
|
+
refreshState();
|
|
179
|
+
if (state === "released" || state === "invalidated")
|
|
180
|
+
return Promise.resolve();
|
|
181
|
+
if (releasePromise)
|
|
182
|
+
return releasePromise;
|
|
183
|
+
assertActive();
|
|
184
|
+
state = "releasing";
|
|
185
|
+
releasePromise = (async () => {
|
|
186
|
+
const count = this.#sessionLeaseCounts.get(sessionId) ?? 0;
|
|
187
|
+
if (count <= 1) {
|
|
188
|
+
const detachment = this.#request({ command: "detach", sessionId }).then(() => undefined);
|
|
189
|
+
this.#sessionDetachments.set(sessionId, detachment);
|
|
190
|
+
try {
|
|
191
|
+
await detachment;
|
|
192
|
+
this.#releaseSessionLease(sessionId, token);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
if (this.#sessionDetachments.get(sessionId) === detachment) {
|
|
196
|
+
this.#sessionDetachments.delete(sessionId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
this.#releaseSessionLease(sessionId, token);
|
|
202
|
+
}
|
|
203
|
+
state = "released";
|
|
204
|
+
})().catch((error) => {
|
|
205
|
+
refreshState();
|
|
206
|
+
if (state === "invalidated")
|
|
207
|
+
return;
|
|
208
|
+
if (relinquishOnFailure) {
|
|
209
|
+
this.#releaseSessionLease(sessionId, token);
|
|
210
|
+
this.#sessionCleanupRequired.add(sessionId);
|
|
211
|
+
state = "released";
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
state = "active";
|
|
215
|
+
releasePromise = undefined;
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
});
|
|
219
|
+
return releasePromise;
|
|
220
|
+
};
|
|
221
|
+
const callbacks = {
|
|
222
|
+
isAttached: isActive,
|
|
223
|
+
getSnapshot: () => (isActive() ? this.#state.getSessionSnapshot(sessionId) : undefined),
|
|
224
|
+
subscribe: (listener) => {
|
|
225
|
+
assertActive();
|
|
226
|
+
return this.#state.subscribeSession(sessionId, (snapshot) => {
|
|
227
|
+
if (isActive())
|
|
228
|
+
listener(snapshot);
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
onEvent: (listener) => {
|
|
232
|
+
assertActive();
|
|
233
|
+
return this.#state.onSessionEvent(sessionId, (event) => {
|
|
234
|
+
if (isActive() || event.type === "session_removed")
|
|
235
|
+
listener(event);
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
detach: () => release(false),
|
|
239
|
+
dispose: () => release(true),
|
|
240
|
+
request: (command) => {
|
|
241
|
+
assertActive();
|
|
242
|
+
return this.#request(command);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
return new SessionHandle(sessionId, callbacks);
|
|
246
|
+
}
|
|
247
|
+
#handleMessage(message) {
|
|
248
|
+
if (message.type === "event") {
|
|
249
|
+
if (message.event.type === "session_removed")
|
|
250
|
+
this.#invalidateSessionLeases(message.event.sessionId);
|
|
251
|
+
this.#state.applyEvent(message.event);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const pending = this.#takePendingRequest(message.id);
|
|
255
|
+
if (!pending) {
|
|
256
|
+
this.#connection.fail(new ProtocolValidationError("Response has no matching request"));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!message.ok) {
|
|
260
|
+
pending.reject(new PiServerError(message.error));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (message.result.command !== pending.command.command) {
|
|
264
|
+
const error = new ProtocolValidationError(`Response command ${message.result.command} does not match ${pending.command.command}`);
|
|
265
|
+
pending.reject(error);
|
|
266
|
+
this.#connection.fail(error);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.#state.applyResult(message.result);
|
|
270
|
+
pending.resolve(message.result);
|
|
271
|
+
}
|
|
272
|
+
#handleConnectionStateChange(change) {
|
|
273
|
+
if (change.state === "disconnected") {
|
|
274
|
+
this.#state.clearAttachments();
|
|
275
|
+
this.#invalidateAllSessionLeases();
|
|
276
|
+
this.#rejectPendingRequests(change.error ?? new PiDisconnectedError());
|
|
277
|
+
}
|
|
278
|
+
this.#notifyConnectionStateListeners(change);
|
|
279
|
+
}
|
|
280
|
+
#takePendingRequest(id) {
|
|
281
|
+
const request = this.#pendingRequests.get(id);
|
|
282
|
+
if (request)
|
|
283
|
+
this.#pendingRequests.delete(id);
|
|
284
|
+
return request;
|
|
285
|
+
}
|
|
286
|
+
#rejectPendingRequests(error) {
|
|
287
|
+
const requests = [...this.#pendingRequests.values()];
|
|
288
|
+
this.#pendingRequests.clear();
|
|
289
|
+
for (const request of requests)
|
|
290
|
+
request.reject(error);
|
|
291
|
+
}
|
|
292
|
+
dispose() {
|
|
293
|
+
if (this.#disposePromise)
|
|
294
|
+
return this.#disposePromise;
|
|
295
|
+
this.#disposed = true;
|
|
296
|
+
this.#disposePromise = Promise.resolve();
|
|
297
|
+
const error = new PiClientDisposedError();
|
|
298
|
+
this.#rejectPendingRequests(error);
|
|
299
|
+
this.#connection.disconnect(error);
|
|
300
|
+
this.#state.dispose();
|
|
301
|
+
this.#invalidateAllSessionLeases();
|
|
302
|
+
this.#connectionStateListeners.clear();
|
|
303
|
+
return this.#disposePromise;
|
|
304
|
+
}
|
|
305
|
+
[Symbol.asyncDispose]() {
|
|
306
|
+
return this.dispose();
|
|
307
|
+
}
|
|
308
|
+
#assertNotDisposed() {
|
|
309
|
+
if (this.#disposed)
|
|
310
|
+
throw new PiClientDisposedError();
|
|
311
|
+
}
|
|
312
|
+
async #reconcileSessionCleanup(sessionId) {
|
|
313
|
+
if (!this.#sessionCleanupRequired.has(sessionId))
|
|
314
|
+
return false;
|
|
315
|
+
let reconciliation = this.#sessionReconciliations.get(sessionId);
|
|
316
|
+
if (!reconciliation) {
|
|
317
|
+
reconciliation = this.#request({ command: "detach", sessionId })
|
|
318
|
+
.then(() => undefined)
|
|
319
|
+
.then(() => {
|
|
320
|
+
this.#sessionCleanupRequired.delete(sessionId);
|
|
321
|
+
})
|
|
322
|
+
.finally(() => {
|
|
323
|
+
this.#sessionReconciliations.delete(sessionId);
|
|
324
|
+
});
|
|
325
|
+
this.#sessionReconciliations.set(sessionId, reconciliation);
|
|
326
|
+
}
|
|
327
|
+
await reconciliation;
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
#reserveSessionLease(sessionId, mode) {
|
|
331
|
+
const count = this.#sessionLeaseCounts.get(sessionId) ?? 0;
|
|
332
|
+
if (mode === "exclusive" && count > 0) {
|
|
333
|
+
throw new PiSessionOwnershipError(sessionId, `Session ${sessionId} already has an active lease`);
|
|
334
|
+
}
|
|
335
|
+
if (mode === "shared" && this.#exclusiveSessionLeases.has(sessionId)) {
|
|
336
|
+
throw new PiSessionOwnershipError(sessionId, `Session ${sessionId} has an exclusive lease`);
|
|
337
|
+
}
|
|
338
|
+
const token = { mode };
|
|
339
|
+
this.#sessionLeaseCounts.set(sessionId, count + 1);
|
|
340
|
+
if (mode === "exclusive")
|
|
341
|
+
this.#exclusiveSessionLeases.set(sessionId, token);
|
|
342
|
+
return token;
|
|
343
|
+
}
|
|
344
|
+
#releaseSessionLease(sessionId, token) {
|
|
345
|
+
const count = this.#sessionLeaseCounts.get(sessionId) ?? 0;
|
|
346
|
+
if (count <= 1)
|
|
347
|
+
this.#sessionLeaseCounts.delete(sessionId);
|
|
348
|
+
else
|
|
349
|
+
this.#sessionLeaseCounts.set(sessionId, count - 1);
|
|
350
|
+
if (this.#exclusiveSessionLeases.get(sessionId) === token)
|
|
351
|
+
this.#exclusiveSessionLeases.delete(sessionId);
|
|
352
|
+
}
|
|
353
|
+
#invalidateSessionLeases(sessionId) {
|
|
354
|
+
this.#sessionLeaseCounts.delete(sessionId);
|
|
355
|
+
this.#exclusiveSessionLeases.delete(sessionId);
|
|
356
|
+
this.#sessionCleanupRequired.delete(sessionId);
|
|
357
|
+
this.#sessionLeaseGenerations.set(sessionId, (this.#sessionLeaseGenerations.get(sessionId) ?? 0) + 1);
|
|
358
|
+
}
|
|
359
|
+
#invalidateAllSessionLeases() {
|
|
360
|
+
for (const sessionId of this.#sessionLeaseCounts.keys())
|
|
361
|
+
this.#invalidateSessionLeases(sessionId);
|
|
362
|
+
this.#sessionCleanupRequired.clear();
|
|
363
|
+
}
|
|
364
|
+
#notifyConnectionStateListeners(change) {
|
|
365
|
+
for (const listener of this.#connectionStateListeners) {
|
|
366
|
+
try {
|
|
367
|
+
listener(change);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.#reportListenerError(error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
#reportListenerError(error) {
|
|
375
|
+
if (!this.#options.onListenerError)
|
|
376
|
+
return;
|
|
377
|
+
try {
|
|
378
|
+
this.#options.onListenerError(toError(error));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Diagnostics cannot affect protocol or transport state.
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
//# 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\ttype Command,\n\ttype CommandResult,\n\ttype EventEnvelope,\n\tencodeClientMessage,\n\tProtocolValidationError,\n\ttype ResponseEnvelope,\n\ttype ResultForCommand,\n\ttype ServerEvent,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@sammorrowdrums/mcpi-protocol\";\nimport { Connection } from \"./connection.ts\";\nimport {\n\tPiClientDisposedError,\n\tPiDisconnectedError,\n\tPiServerError,\n\tPiSessionDetachedError,\n\tPiSessionOwnershipError,\n\ttoError,\n} from \"./errors.ts\";\nimport { createPromiseResolvers } from \"./promise.ts\";\nimport {\n\ttype AcquireSessionOptions,\n\ttype PiSessionHandle,\n\tSessionHandle,\n\ttype SessionHandleCallbacks,\n\ttype SessionLeaseMode,\n} from \"./session-handle.ts\";\nimport { ClientState } from \"./state.ts\";\nimport type {\n\tConnectionState,\n\tConnectionStateChange,\n\tCreateSessionOptions,\n\tPiClientOptions,\n\tUnsubscribe,\n} from \"./types.ts\";\n\ntype SessionLeaseState = \"active\" | \"releasing\" | \"released\" | \"invalidated\";\n\ninterface SessionLeaseToken {\n\treadonly mode: SessionLeaseMode;\n}\n\ninterface PendingRequest {\n\tcommand: Command;\n\tresolve(result: CommandResult): void;\n\treject(error: Error): void;\n}\n\nexport class PiClient {\n\treadonly #options: PiClientOptions;\n\treadonly #connection: Connection;\n\treadonly #state: ClientState;\n\treadonly #pendingRequests = new Map<string, PendingRequest>();\n\treadonly #sessionLeaseCounts = new Map<string, number>();\n\treadonly #exclusiveSessionLeases = new Map<string, SessionLeaseToken>();\n\treadonly #sessionLeaseGenerations = new Map<string, number>();\n\treadonly #sessionAttachments = new Map<string, Promise<void>>();\n\treadonly #sessionDetachments = new Map<string, Promise<void>>();\n\treadonly #sessionCleanupRequired = new Set<string>();\n\treadonly #sessionReconciliations = new Map<string, Promise<void>>();\n\treadonly #connectionStateListeners = new Set<(change: ConnectionStateChange) => void>();\n\t#requestSequence = 0;\n\t#disposed = false;\n\t#disposePromise: Promise<void> | undefined;\n\n\tconstructor(options: PiClientOptions) {\n\t\tthis.#options = options;\n\t\tthis.#state = new ClientState(options.onListenerError);\n\t\tthis.#connection = new Connection({\n\t\t\ttransportFactory: options.transportFactory,\n\t\t\tmaxFrameLength: options.maxFrameLength,\n\t\t\tonHandshake: (snapshot) => this.#state.applyServerSnapshot(snapshot),\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 snapshot(): ServerSnapshot | undefined {\n\t\treturn this.#state.snapshot;\n\t}\n\n\tstatic async connect(options: PiClientOptions): Promise<PiClient> {\n\t\tconst client = new PiClient(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<ServerSnapshot> {\n\t\tif (this.#disposed) return Promise.reject(new PiClientDisposedError());\n\t\tif (this.#connection.state === \"disconnected\") this.#state.reset();\n\t\treturn this.#connection.connect();\n\t}\n\n\treconnect(): Promise<ServerSnapshot> {\n\t\treturn this.connect();\n\t}\n\n\tdisconnect(reason = \"Client disconnected\"): void {\n\t\tthis.#connection.disconnect(reason);\n\t}\n\n\tsubscribe(listener: (snapshot: ServerSnapshot) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\treturn this.#state.subscribe(listener);\n\t}\n\n\tonEvent(listener: (event: ServerEvent) => void): Unsubscribe {\n\t\tthis.#assertNotDisposed();\n\t\treturn this.#state.onEvent(listener);\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\tasync listSessions(): Promise<readonly SessionMetadata[]> {\n\t\treturn (await this.#request({ command: \"list\" })).sessions;\n\t}\n\n\tasync createSession(options: CreateSessionOptions = {}): Promise<PiSessionHandle> {\n\t\tconst result = await this.#request({ command: \"create\", ...options });\n\t\tconst token = this.#reserveSessionLease(result.session.id, \"exclusive\");\n\t\treturn this.#createSessionLease(result.session.id, token);\n\t}\n\n\tasync attachSession(sessionId: string): Promise<PiSessionHandle> {\n\t\treturn this.acquireSession(sessionId, { mode: \"shared\" });\n\t}\n\n\tasync acquireSession(sessionId: string, options: AcquireSessionOptions): Promise<PiSessionHandle> {\n\t\tthis.#assertNotDisposed();\n\t\tconst token = this.#reserveSessionLease(sessionId, options.mode);\n\t\ttry {\n\t\t\tconst detachment = this.#sessionDetachments.get(sessionId);\n\t\t\tif (detachment) await detachment.catch(() => {});\n\t\t\tconst reconciled = this.#sessionCleanupRequired.has(sessionId)\n\t\t\t\t? await this.#reconcileSessionCleanup(sessionId)\n\t\t\t\t: false;\n\t\t\tif (reconciled || !this.#state.isSessionAttached(sessionId)) {\n\t\t\t\tlet attachment = this.#sessionAttachments.get(sessionId);\n\t\t\t\tif (!attachment) {\n\t\t\t\t\tattachment = this.#attachSession(sessionId);\n\t\t\t\t\tthis.#sessionAttachments.set(sessionId, attachment);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait attachment;\n\t\t\t\t} finally {\n\t\t\t\t\tif (this.#sessionAttachments.get(sessionId) === attachment) this.#sessionAttachments.delete(sessionId);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.#createSessionLease(sessionId, token);\n\t\t} catch (error) {\n\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync #attachSession(sessionId: string): Promise<void> {\n\t\tconst previous = this.#state.forgetSessionSnapshot(sessionId);\n\t\ttry {\n\t\t\tawait this.#request({ command: \"attach\", sessionId });\n\t\t} catch (error) {\n\t\t\tif (previous) this.#state.restoreSessionSnapshot(previous);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t#request<const TCommand extends Command>(command: TCommand): Promise<ResultForCommand<TCommand>> {\n\t\tif (this.#disposed) return Promise.reject(new PiClientDisposedError());\n\t\tif (!this.connected) return Promise.reject(new PiDisconnectedError());\n\t\tconst id = `request-${++this.#requestSequence}`;\n\t\tconst { promise, resolve, reject } = createPromiseResolvers<CommandResult>();\n\t\tthis.#pendingRequests.set(id, { command, resolve, reject });\n\t\tlet frame: Uint8Array;\n\t\ttry {\n\t\t\tframe = encodeClientMessage(\n\t\t\t\t{ type: \"request\", id, request: command },\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 as Promise<ResultForCommand<TCommand>>;\n\t\t}\n\t\tthis.#connection.send(frame);\n\t\treturn promise as Promise<ResultForCommand<TCommand>>;\n\t}\n\n\t#createSessionLease(sessionId: string, token: SessionLeaseToken): PiSessionHandle {\n\t\tconst generation = this.#sessionLeaseGenerations.get(sessionId) ?? 0;\n\t\tthis.#sessionLeaseGenerations.set(sessionId, generation);\n\t\tlet state: SessionLeaseState = \"active\";\n\t\tlet releasePromise: Promise<void> | undefined;\n\t\tconst refreshState = () => {\n\t\t\tif (\n\t\t\t\t(state === \"active\" || state === \"releasing\") &&\n\t\t\t\tthis.#sessionLeaseGenerations.get(sessionId) !== generation\n\t\t\t) {\n\t\t\t\tstate = \"invalidated\";\n\t\t\t}\n\t\t};\n\t\tconst isActive = () => {\n\t\t\trefreshState();\n\t\t\treturn state === \"active\" && this.#state.isSessionAttached(sessionId);\n\t\t};\n\t\tconst assertActive = () => {\n\t\t\tthis.#assertNotDisposed();\n\t\t\tif (!this.connected) throw new PiDisconnectedError();\n\t\t\tif (!isActive()) throw new PiSessionDetachedError(sessionId);\n\t\t};\n\t\tconst release = (relinquishOnFailure: boolean): Promise<void> => {\n\t\t\trefreshState();\n\t\t\tif (state === \"released\" || state === \"invalidated\") return Promise.resolve();\n\t\t\tif (releasePromise) return releasePromise;\n\t\t\tassertActive();\n\t\t\tstate = \"releasing\";\n\t\t\treleasePromise = (async () => {\n\t\t\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\t\t\tif (count <= 1) {\n\t\t\t\t\tconst detachment = this.#request({ command: \"detach\", sessionId }).then(() => undefined);\n\t\t\t\t\tthis.#sessionDetachments.set(sessionId, detachment);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait detachment;\n\t\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (this.#sessionDetachments.get(sessionId) === detachment) {\n\t\t\t\t\t\t\tthis.#sessionDetachments.delete(sessionId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t}\n\t\t\t\tstate = \"released\";\n\t\t\t})().catch((error: unknown) => {\n\t\t\t\trefreshState();\n\t\t\t\tif (state === \"invalidated\") return;\n\t\t\t\tif (relinquishOnFailure) {\n\t\t\t\t\tthis.#releaseSessionLease(sessionId, token);\n\t\t\t\t\tthis.#sessionCleanupRequired.add(sessionId);\n\t\t\t\t\tstate = \"released\";\n\t\t\t\t} else {\n\t\t\t\t\tstate = \"active\";\n\t\t\t\t\treleasePromise = undefined;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\treturn releasePromise;\n\t\t};\n\t\tconst callbacks: SessionHandleCallbacks = {\n\t\t\tisAttached: isActive,\n\t\t\tgetSnapshot: () => (isActive() ? this.#state.getSessionSnapshot(sessionId) : undefined),\n\t\t\tsubscribe: (listener) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#state.subscribeSession(sessionId, (snapshot) => {\n\t\t\t\t\tif (isActive()) listener(snapshot);\n\t\t\t\t});\n\t\t\t},\n\t\t\tonEvent: (listener) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#state.onSessionEvent(sessionId, (event) => {\n\t\t\t\t\tif (isActive() || event.type === \"session_removed\") listener(event);\n\t\t\t\t});\n\t\t\t},\n\t\t\tdetach: () => release(false),\n\t\t\tdispose: () => release(true),\n\t\t\trequest: (command) => {\n\t\t\t\tassertActive();\n\t\t\t\treturn this.#request(command);\n\t\t\t},\n\t\t};\n\t\treturn new SessionHandle(sessionId, callbacks);\n\t}\n\n\t#handleMessage(message: ResponseEnvelope | EventEnvelope): void {\n\t\tif (message.type === \"event\") {\n\t\t\tif (message.event.type === \"session_removed\") this.#invalidateSessionLeases(message.event.sessionId);\n\t\t\tthis.#state.applyEvent(message.event);\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 PiServerError(message.error));\n\t\t\treturn;\n\t\t}\n\t\tif (message.result.command !== pending.command.command) {\n\t\t\tconst error = new ProtocolValidationError(\n\t\t\t\t`Response command ${message.result.command} does not match ${pending.command.command}`,\n\t\t\t);\n\t\t\tpending.reject(error);\n\t\t\tthis.#connection.fail(error);\n\t\t\treturn;\n\t\t}\n\t\tthis.#state.applyResult(message.result);\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.#state.clearAttachments();\n\t\t\tthis.#invalidateAllSessionLeases();\n\t\t\tthis.#rejectPendingRequests(change.error ?? new PiDisconnectedError());\n\t\t}\n\t\tthis.#notifyConnectionStateListeners(change);\n\t}\n\n\t#takePendingRequest(id: string): PendingRequest | undefined {\n\t\tconst request = this.#pendingRequests.get(id);\n\t\tif (request) this.#pendingRequests.delete(id);\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) request.reject(error);\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 PiClientDisposedError();\n\t\tthis.#rejectPendingRequests(error);\n\t\tthis.#connection.disconnect(error);\n\t\tthis.#state.dispose();\n\t\tthis.#invalidateAllSessionLeases();\n\t\tthis.#connectionStateListeners.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#assertNotDisposed(): void {\n\t\tif (this.#disposed) throw new PiClientDisposedError();\n\t}\n\n\tasync #reconcileSessionCleanup(sessionId: string): Promise<boolean> {\n\t\tif (!this.#sessionCleanupRequired.has(sessionId)) return false;\n\t\tlet reconciliation = this.#sessionReconciliations.get(sessionId);\n\t\tif (!reconciliation) {\n\t\t\treconciliation = this.#request({ command: \"detach\", sessionId })\n\t\t\t\t.then(() => undefined)\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.#sessionCleanupRequired.delete(sessionId);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.#sessionReconciliations.delete(sessionId);\n\t\t\t\t});\n\t\t\tthis.#sessionReconciliations.set(sessionId, reconciliation);\n\t\t}\n\t\tawait reconciliation;\n\t\treturn true;\n\t}\n\n\t#reserveSessionLease(sessionId: string, mode: SessionLeaseMode): SessionLeaseToken {\n\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\tif (mode === \"exclusive\" && count > 0) {\n\t\t\tthrow new PiSessionOwnershipError(sessionId, `Session ${sessionId} already has an active lease`);\n\t\t}\n\t\tif (mode === \"shared\" && this.#exclusiveSessionLeases.has(sessionId)) {\n\t\t\tthrow new PiSessionOwnershipError(sessionId, `Session ${sessionId} has an exclusive lease`);\n\t\t}\n\t\tconst token: SessionLeaseToken = { mode };\n\t\tthis.#sessionLeaseCounts.set(sessionId, count + 1);\n\t\tif (mode === \"exclusive\") this.#exclusiveSessionLeases.set(sessionId, token);\n\t\treturn token;\n\t}\n\n\t#releaseSessionLease(sessionId: string, token: SessionLeaseToken): void {\n\t\tconst count = this.#sessionLeaseCounts.get(sessionId) ?? 0;\n\t\tif (count <= 1) this.#sessionLeaseCounts.delete(sessionId);\n\t\telse this.#sessionLeaseCounts.set(sessionId, count - 1);\n\t\tif (this.#exclusiveSessionLeases.get(sessionId) === token) this.#exclusiveSessionLeases.delete(sessionId);\n\t}\n\n\t#invalidateSessionLeases(sessionId: string): void {\n\t\tthis.#sessionLeaseCounts.delete(sessionId);\n\t\tthis.#exclusiveSessionLeases.delete(sessionId);\n\t\tthis.#sessionCleanupRequired.delete(sessionId);\n\t\tthis.#sessionLeaseGenerations.set(sessionId, (this.#sessionLeaseGenerations.get(sessionId) ?? 0) + 1);\n\t}\n\n\t#invalidateAllSessionLeases(): void {\n\t\tfor (const sessionId of this.#sessionLeaseCounts.keys()) this.#invalidateSessionLeases(sessionId);\n\t\tthis.#sessionCleanupRequired.clear();\n\t}\n\n\t#notifyConnectionStateListeners(change: ConnectionStateChange): void {\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#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"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ServerMessage, type ServerSnapshot } from "@sammorrowdrums/mcpi-protocol";
|
|
2
|
+
import type { ByteTransportFactory } from "./transport.ts";
|
|
3
|
+
import type { ConnectionState, ConnectionStateChange } from "./types.ts";
|
|
4
|
+
interface ConnectionOptions {
|
|
5
|
+
transportFactory: ByteTransportFactory;
|
|
6
|
+
maxFrameLength?: number;
|
|
7
|
+
onHandshake(snapshot: ServerSnapshot): void;
|
|
8
|
+
onMessage(message: Exclude<ServerMessage, {
|
|
9
|
+
type: "hello" | "hello_error";
|
|
10
|
+
}>): void;
|
|
11
|
+
onStateChange(change: ConnectionStateChange): void;
|
|
12
|
+
}
|
|
13
|
+
export declare class Connection {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(options: ConnectionOptions);
|
|
16
|
+
get state(): ConnectionState;
|
|
17
|
+
get maxFrameLength(): number;
|
|
18
|
+
connect(): Promise<ServerSnapshot>;
|
|
19
|
+
disconnect(reason?: string | Error): void;
|
|
20
|
+
fail(error: Error): void;
|
|
21
|
+
send(frame: Uint8Array): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=connection.d.ts.map
|