@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/dist/client.js CHANGED
@@ -1,32 +1,34 @@
1
- import { encodeClientMessage, ProtocolValidationError, } from "@earendil-works/pi-protocol";
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 { PiClientDisposedError, PiDisconnectedError, PiServerError, PiSessionDetachedError, PiSessionOwnershipError, toError, } from "./errors.js";
5
+ import { ClientDisposedError, DisconnectedError, ServerError, toError } from "./errors.js";
4
6
  import { createPromiseResolvers } from "./promise.js";
5
- import { SessionHandle, } from "./session-handle.js";
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: (snapshot) => this.#state.applyServerSnapshot(snapshot),
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 snapshot() {
44
- return this.#state.snapshot;
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 PiClient(options);
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 PiClientDisposedError());
60
- if (this.#connection.state === "disconnected")
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
- 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);
82
+ onAttachmentChange(listener) {
83
+ this.#assertNotDisposed();
84
+ this.#attachmentListeners.add(listener);
85
+ return () => this.#attachmentListeners.delete(listener);
90
86
  }
91
- async attachSession(sessionId) {
92
- return this.acquireSession(sessionId, { mode: "shared" });
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 acquireSession(sessionId, options) {
95
- this.#assertNotDisposed();
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
- 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);
94
+ return parseServiceCatalogue(result);
119
95
  }
120
96
  catch (error) {
121
- this.#releaseSessionLease(sessionId, token);
122
- throw error;
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 #attachSession(sessionId) {
126
- const previous = this.#state.forgetSessionSnapshot(sessionId);
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({ command: "attach", sessionId });
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 (previous)
132
- this.#state.restoreSessionSnapshot(previous);
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(command) {
164
+ #request(target, call, signal, transform) {
137
165
  if (this.#disposed)
138
- return Promise.reject(new PiClientDisposedError());
166
+ return Promise.reject(new ClientDisposedError());
139
167
  if (!this.connected)
140
- return Promise.reject(new PiDisconnectedError());
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
- this.#pendingRequests.set(id, { command, resolve, reject });
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, request: command }, { maxFrameLength: this.#connection.maxFrameLength });
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 === "event") {
249
- if (message.event.type === "session_removed")
250
- this.#invalidateSessionLeases(message.event.sessionId);
251
- this.#state.applyEvent(message.event);
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 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);
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.#state.clearAttachments();
275
- this.#invalidateAllSessionLeases();
276
- this.#rejectPendingRequests(change.error ?? new PiDisconnectedError());
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 PiClientDisposedError();
306
+ const error = new ClientDisposedError();
298
307
  this.#rejectPendingRequests(error);
299
308
  this.#connection.disconnect(error);
300
- this.#state.dispose();
301
- this.#invalidateAllSessionLeases();
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
- #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`);
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
- 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) {
326
+ this.#attachment = attachment;
327
+ for (const listener of this.#attachmentListeners) {
366
328
  try {
367
- listener(change);
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