@earendil-works/pi-client 0.84.4 → 0.85.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 +2 -0
- package/README.md +37 -28
- package/dist/client.d.ts +20 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +248 -245
- package/dist/client.js.map +1 -1
- package/dist/connection.d.ts +4 -3
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +13 -9
- package/dist/connection.js.map +1 -1
- package/dist/errors.d.ts +6 -15
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -28
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +14 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/unix.d.ts +14 -1
- package/dist/unix.d.ts.map +1 -1
- package/dist/unix.js +138 -8
- package/dist/unix.js.map +1 -1
- package/package.json +3 -2
- package/dist/session-handle.d.ts +0 -54
- package/dist/session-handle.d.ts.map +0 -1
- package/dist/session-handle.js +0 -51
- package/dist/session-handle.js.map +0 -1
- package/dist/state.d.ts +0 -22
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -145
- package/dist/state.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServiceCatalogueCall, createServiceStateDecoder, createServiceSubscribeCall, createServiceUnsubscribeCall, parseServiceCall, parseServiceCatalogue, parseWireServiceProviderUpdate, parseWireServiceSubscriptionSnapshot, } from "@earendil-works/chord";
|
|
2
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
3
|
+
import { encodeClientMessage, isServerId, ProtocolValidationError, } from "@earendil-works/pi-protocol";
|
|
2
4
|
import { Connection } from "./connection.js";
|
|
3
|
-
import {
|
|
5
|
+
import { ClientDisposedError, DisconnectedError, ServerError, toError } from "./errors.js";
|
|
4
6
|
import { createPromiseResolvers } from "./promise.js";
|
|
5
|
-
|
|
6
|
-
import { ClientState } from "./state.js";
|
|
7
|
-
export class PiClient {
|
|
7
|
+
export class Client {
|
|
8
8
|
#options;
|
|
9
9
|
#connection;
|
|
10
|
-
#state;
|
|
11
10
|
#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
11
|
#connectionStateListeners = new Set();
|
|
12
|
+
#attachmentListeners = new Set();
|
|
13
|
+
#serviceListeners = new Map();
|
|
20
14
|
#requestSequence = 0;
|
|
15
|
+
#serviceSubscriptionSequence = 0;
|
|
16
|
+
#hello;
|
|
17
|
+
#attachment;
|
|
21
18
|
#disposed = false;
|
|
22
19
|
#disposePromise;
|
|
23
20
|
constructor(options) {
|
|
21
|
+
if (!isServerId(options.serverId)) {
|
|
22
|
+
throw new TypeError("serverId must be a canonical lowercase UUIDv4");
|
|
23
|
+
}
|
|
24
24
|
this.#options = options;
|
|
25
|
-
this.#state = new ClientState(options.onListenerError);
|
|
26
25
|
this.#connection = new Connection({
|
|
27
26
|
transportFactory: options.transportFactory,
|
|
27
|
+
serverId: options.serverId,
|
|
28
28
|
maxFrameLength: options.maxFrameLength,
|
|
29
|
-
onHandshake: (
|
|
29
|
+
onHandshake: (hello) => {
|
|
30
|
+
this.#hello = hello;
|
|
31
|
+
},
|
|
30
32
|
onMessage: (message) => this.#handleMessage(message),
|
|
31
33
|
onStateChange: (change) => this.#handleConnectionStateChange(change),
|
|
32
34
|
});
|
|
@@ -40,11 +42,17 @@ export class PiClient {
|
|
|
40
42
|
get connected() {
|
|
41
43
|
return this.#connection.state === "connected";
|
|
42
44
|
}
|
|
43
|
-
get
|
|
44
|
-
return this.#
|
|
45
|
+
get serverId() {
|
|
46
|
+
return this.#options.serverId;
|
|
47
|
+
}
|
|
48
|
+
get hello() {
|
|
49
|
+
return this.#hello;
|
|
50
|
+
}
|
|
51
|
+
get attachment() {
|
|
52
|
+
return this.#attachment;
|
|
45
53
|
}
|
|
46
54
|
static async connect(options) {
|
|
47
|
-
const client = new
|
|
55
|
+
const client = new Client(options);
|
|
48
56
|
try {
|
|
49
57
|
await client.connect();
|
|
50
58
|
return client;
|
|
@@ -56,9 +64,8 @@ export class PiClient {
|
|
|
56
64
|
}
|
|
57
65
|
connect() {
|
|
58
66
|
if (this.#disposed)
|
|
59
|
-
return Promise.reject(new
|
|
60
|
-
|
|
61
|
-
this.#state.reset();
|
|
67
|
+
return Promise.reject(new ClientDisposedError());
|
|
68
|
+
this.#hello = undefined;
|
|
62
69
|
return this.#connection.connect();
|
|
63
70
|
}
|
|
64
71
|
reconnect() {
|
|
@@ -67,188 +74,185 @@ export class PiClient {
|
|
|
67
74
|
disconnect(reason = "Client disconnected") {
|
|
68
75
|
this.#connection.disconnect(reason);
|
|
69
76
|
}
|
|
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
77
|
onConnectionStateChange(listener) {
|
|
79
78
|
this.#assertNotDisposed();
|
|
80
79
|
this.#connectionStateListeners.add(listener);
|
|
81
80
|
return () => this.#connectionStateListeners.delete(listener);
|
|
82
81
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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);
|
|
82
|
+
onAttachmentChange(listener) {
|
|
83
|
+
this.#assertNotDisposed();
|
|
84
|
+
this.#attachmentListeners.add(listener);
|
|
85
|
+
return () => this.#attachmentListeners.delete(listener);
|
|
90
86
|
}
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
/** Invoke one low-level protocol call against an explicit routed target. */
|
|
88
|
+
request(target, call, signal) {
|
|
89
|
+
return this.#request(target, call, signal);
|
|
93
90
|
}
|
|
94
|
-
async
|
|
95
|
-
this.#
|
|
96
|
-
const token = this.#reserveSessionLease(sessionId, options.mode);
|
|
91
|
+
async serviceCatalogue(target, signal) {
|
|
92
|
+
const result = await this.#request(target, createServiceCatalogueCall(), signal);
|
|
97
93
|
try {
|
|
98
|
-
|
|
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);
|
|
94
|
+
return parseServiceCatalogue(result);
|
|
119
95
|
}
|
|
120
96
|
catch (error) {
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
const validationError = new ProtocolValidationError(error instanceof Error ? error.message : "Invalid service catalogue");
|
|
98
|
+
this.#connection.fail(validationError);
|
|
99
|
+
throw validationError;
|
|
123
100
|
}
|
|
124
101
|
}
|
|
125
|
-
async
|
|
126
|
-
const
|
|
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;
|
|
127
116
|
try {
|
|
128
|
-
await this.#request(
|
|
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
|
+
});
|
|
129
125
|
}
|
|
130
126
|
catch (error) {
|
|
131
|
-
if (
|
|
132
|
-
this.#
|
|
127
|
+
if (this.#serviceListeners.get(subscriptionId) === active)
|
|
128
|
+
this.#serviceListeners.delete(subscriptionId);
|
|
133
129
|
throw error;
|
|
134
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
|
+
};
|
|
135
163
|
}
|
|
136
|
-
#request(
|
|
164
|
+
#request(target, call, signal, transform) {
|
|
137
165
|
if (this.#disposed)
|
|
138
|
-
return Promise.reject(new
|
|
166
|
+
return Promise.reject(new ClientDisposedError());
|
|
139
167
|
if (!this.connected)
|
|
140
|
-
return Promise.reject(new
|
|
168
|
+
return Promise.reject(new DisconnectedError());
|
|
169
|
+
if (signal?.aborted)
|
|
170
|
+
return Promise.reject(abortError(signal));
|
|
141
171
|
const id = `request-${++this.#requestSequence}`;
|
|
142
172
|
const { promise, resolve, reject } = createPromiseResolvers();
|
|
143
|
-
|
|
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
|
+
});
|
|
144
213
|
let frame;
|
|
145
214
|
try {
|
|
146
|
-
frame = encodeClientMessage({ type: "request", id,
|
|
215
|
+
frame = encodeClientMessage({ type: "request", id, target, call: parseServiceCall(call) }, { maxFrameLength: this.#connection.maxFrameLength });
|
|
147
216
|
}
|
|
148
217
|
catch (error) {
|
|
149
218
|
this.#takePendingRequest(id)?.reject(toError(error));
|
|
150
219
|
return promise;
|
|
151
220
|
}
|
|
152
221
|
this.#connection.send(frame);
|
|
222
|
+
sent = true;
|
|
223
|
+
if (aborted)
|
|
224
|
+
sendCancel();
|
|
153
225
|
return promise;
|
|
154
226
|
}
|
|
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
227
|
#handleMessage(message) {
|
|
248
|
-
if (message.type === "
|
|
249
|
-
if (message.
|
|
250
|
-
this.#
|
|
251
|
-
|
|
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);
|
|
252
256
|
return;
|
|
253
257
|
}
|
|
254
258
|
const pending = this.#takePendingRequest(message.id);
|
|
@@ -257,120 +261,95 @@ export class PiClient {
|
|
|
257
261
|
return;
|
|
258
262
|
}
|
|
259
263
|
if (!message.ok) {
|
|
260
|
-
pending.reject(new
|
|
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);
|
|
264
|
+
pending.reject(new ServerError(message.error));
|
|
267
265
|
return;
|
|
268
266
|
}
|
|
269
|
-
this.#state.applyResult(message.result);
|
|
270
267
|
pending.resolve(message.result);
|
|
271
268
|
}
|
|
272
269
|
#handleConnectionStateChange(change) {
|
|
273
270
|
if (change.state === "disconnected") {
|
|
274
|
-
this.#
|
|
275
|
-
this.#
|
|
276
|
-
this.#rejectPendingRequests(change.error ?? new
|
|
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
|
+
}
|
|
277
283
|
}
|
|
278
|
-
this.#notifyConnectionStateListeners(change);
|
|
279
284
|
}
|
|
280
285
|
#takePendingRequest(id) {
|
|
281
286
|
const request = this.#pendingRequests.get(id);
|
|
282
|
-
if (request)
|
|
287
|
+
if (request) {
|
|
283
288
|
this.#pendingRequests.delete(id);
|
|
289
|
+
request.cleanup();
|
|
290
|
+
}
|
|
284
291
|
return request;
|
|
285
292
|
}
|
|
286
293
|
#rejectPendingRequests(error) {
|
|
287
294
|
const requests = [...this.#pendingRequests.values()];
|
|
288
295
|
this.#pendingRequests.clear();
|
|
289
|
-
for (const request of requests)
|
|
296
|
+
for (const request of requests) {
|
|
297
|
+
request.cleanup();
|
|
290
298
|
request.reject(error);
|
|
299
|
+
}
|
|
291
300
|
}
|
|
292
301
|
dispose() {
|
|
293
302
|
if (this.#disposePromise)
|
|
294
303
|
return this.#disposePromise;
|
|
295
304
|
this.#disposed = true;
|
|
296
305
|
this.#disposePromise = Promise.resolve();
|
|
297
|
-
const error = new
|
|
306
|
+
const error = new ClientDisposedError();
|
|
298
307
|
this.#rejectPendingRequests(error);
|
|
299
308
|
this.#connection.disconnect(error);
|
|
300
|
-
this.#
|
|
301
|
-
this.#
|
|
309
|
+
this.#hello = undefined;
|
|
310
|
+
this.#setAttachment(undefined);
|
|
302
311
|
this.#connectionStateListeners.clear();
|
|
312
|
+
this.#attachmentListeners.clear();
|
|
313
|
+
this.#serviceListeners.clear();
|
|
303
314
|
return this.#disposePromise;
|
|
304
315
|
}
|
|
305
316
|
[Symbol.asyncDispose]() {
|
|
306
317
|
return this.dispose();
|
|
307
318
|
}
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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`);
|
|
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;
|
|
337
325
|
}
|
|
338
|
-
|
|
339
|
-
|
|
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) {
|
|
326
|
+
this.#attachment = attachment;
|
|
327
|
+
for (const listener of this.#attachmentListeners) {
|
|
366
328
|
try {
|
|
367
|
-
listener(
|
|
329
|
+
listener(attachment);
|
|
368
330
|
}
|
|
369
331
|
catch (error) {
|
|
370
332
|
this.#reportListenerError(error);
|
|
371
333
|
}
|
|
372
334
|
}
|
|
373
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
|
+
}
|
|
374
353
|
#reportListenerError(error) {
|
|
375
354
|
if (!this.#options.onListenerError)
|
|
376
355
|
return;
|
|
@@ -382,4 +361,28 @@ export class PiClient {
|
|
|
382
361
|
}
|
|
383
362
|
}
|
|
384
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
|
+
}
|
|
385
388
|
//# sourceMappingURL=client.js.map
|