@cortexkit/subc-client 0.3.4 → 0.4.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/README.md +27 -13
- package/dist/client.d.ts +49 -54
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +303 -262
- package/dist/client.js.map +1 -1
- package/dist/envelope.d.ts +24 -17
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +91 -72
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +35 -8
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +221 -71
- package/dist/provider.js.map +1 -1
- package/dist/route-handle.d.ts +25 -0
- package/dist/route-handle.d.ts.map +1 -0
- package/dist/route-handle.js +52 -0
- package/dist/route-handle.js.map +1 -0
- package/dist/socket.d.ts +7 -0
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +23 -0
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +374 -277
- package/src/envelope.ts +136 -74
- package/src/index.ts +7 -0
- package/src/provider.ts +361 -94
- package/src/route-handle.ts +60 -0
- package/src/socket.ts +37 -0
package/dist/client.js
CHANGED
|
@@ -12,7 +12,8 @@ import { promises as fs } from "node:fs";
|
|
|
12
12
|
import { debuglog } from "node:util";
|
|
13
13
|
import { AuthError, authenticateClient } from "./auth.js";
|
|
14
14
|
import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
|
|
15
|
-
import { buildFrame, buildFlags,
|
|
15
|
+
import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, Priority, } from "./envelope.js";
|
|
16
|
+
import { belongsToConnection, createRouteHandle, newConnectionToken, sameRouteHandle, StaleRouteHandleError, } from "./route-handle.js";
|
|
16
17
|
import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, } from "./socket.js";
|
|
17
18
|
const debug = debuglog("subc-client");
|
|
18
19
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
@@ -101,7 +102,11 @@ export class SubcClient {
|
|
|
101
102
|
opts;
|
|
102
103
|
nextCorr = 1n;
|
|
103
104
|
pending = new Map();
|
|
105
|
+
lateResponses = new Map();
|
|
104
106
|
routes = new Map();
|
|
107
|
+
liveRoutes = new Map();
|
|
108
|
+
connectionToken = newConnectionToken();
|
|
109
|
+
ingressEpochDropCount = 0;
|
|
105
110
|
closedErr = null;
|
|
106
111
|
closeStarted = false;
|
|
107
112
|
reconnecting = null;
|
|
@@ -133,27 +138,56 @@ export class SubcClient {
|
|
|
133
138
|
const parsed = this.parseJson(reply);
|
|
134
139
|
return parsed.modules ?? [];
|
|
135
140
|
}
|
|
136
|
-
/** Open a route
|
|
141
|
+
/** Open a route and return its connection-bound immutable handle. */
|
|
137
142
|
async routeOpen(target, identity, opts = {}) {
|
|
138
143
|
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
144
|
+
const consumerCapabilities = opts.consumerCapabilities;
|
|
139
145
|
const body = this.encode({
|
|
140
146
|
op: "route.open",
|
|
141
147
|
target,
|
|
142
148
|
identity,
|
|
143
149
|
...(consumerIdentity ? { consumer_identity: consumerIdentity } : {}),
|
|
150
|
+
...(consumerCapabilities !== undefined ? { consumer_capabilities: consumerCapabilities } : {}),
|
|
144
151
|
});
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
let installed = null;
|
|
153
|
+
const install = (frame) => {
|
|
154
|
+
if (frame.header.ty !== FrameType.Response)
|
|
155
|
+
return true;
|
|
156
|
+
const parsed = this.parseJson(frame);
|
|
157
|
+
if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number") {
|
|
158
|
+
throw new SubcError(`route.open returned no route handle: ${JSON.stringify(parsed)}`);
|
|
159
|
+
}
|
|
160
|
+
installed = this.installRoute(parsed.route_channel, parsed.route_epoch);
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
const closeLateRoute = (frame) => {
|
|
164
|
+
if (frame.header.ty !== FrameType.Response)
|
|
165
|
+
return;
|
|
166
|
+
try {
|
|
167
|
+
const parsed = this.parseJson(frame);
|
|
168
|
+
if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number")
|
|
169
|
+
return;
|
|
170
|
+
const lateHandle = this.installRoute(parsed.route_channel, parsed.route_epoch);
|
|
171
|
+
this.failHandle(lateHandle, new SubcError("late route.open was closed", "route_closed"));
|
|
172
|
+
this.liveRoutes.delete(lateHandle.channel);
|
|
173
|
+
this.sendRouteGoodbye(lateHandle, true);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
this.closeConnectionAfterCleanupFailure();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
await this.controlRpc(body, install, closeLateRoute);
|
|
180
|
+
if (!installed)
|
|
181
|
+
throw new SubcError("route.open response was not installed");
|
|
182
|
+
return installed;
|
|
183
|
+
}
|
|
184
|
+
/** Send a data-plane request on exactly the supplied route generation. */
|
|
185
|
+
async request(handle, body, opts = {}) {
|
|
186
|
+
this.assertLiveHandle(handle);
|
|
154
187
|
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
155
188
|
const priority = opts.priority ?? Priority.Interactive;
|
|
156
|
-
const
|
|
189
|
+
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
190
|
+
const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
|
|
157
191
|
return this.parseJson(reply);
|
|
158
192
|
}
|
|
159
193
|
/**
|
|
@@ -164,9 +198,9 @@ export class SubcClient {
|
|
|
164
198
|
const body = params === undefined ? { method } : { method, params };
|
|
165
199
|
let retriedUnknownChannel = false;
|
|
166
200
|
for (;;) {
|
|
167
|
-
const
|
|
201
|
+
const routeHandle = await this.cachedRouteHandle(moduleId, opts);
|
|
168
202
|
try {
|
|
169
|
-
return (await this.managedRequest(
|
|
203
|
+
return (await this.managedRequest(routeHandle, body, opts));
|
|
170
204
|
}
|
|
171
205
|
catch (err) {
|
|
172
206
|
if (!(err instanceof SubcCallError))
|
|
@@ -178,7 +212,7 @@ export class SubcClient {
|
|
|
178
212
|
// re-opens the route instead of resending into the same dead channel.
|
|
179
213
|
if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
|
|
180
214
|
retriedUnknownChannel = true;
|
|
181
|
-
this.
|
|
215
|
+
this.evictRouteHandle(routeHandle);
|
|
182
216
|
continue;
|
|
183
217
|
}
|
|
184
218
|
if (err.kind === "not_sent") {
|
|
@@ -201,38 +235,32 @@ export class SubcClient {
|
|
|
201
235
|
}
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
* returned `closed` settles on the StreamEnd terminal (resolve) or an Error / route
|
|
208
|
-
* GOODBYE (reject). Events ride this held-open request's correlation id — they are
|
|
209
|
-
* never unsolicited, so they are not dropped. Call `unsubscribe()` to cancel.
|
|
210
|
-
*/
|
|
211
|
-
subscribe(routeChannel, body, onEvent, opts = {}) {
|
|
238
|
+
/** Open a held request on exactly one route generation. */
|
|
239
|
+
subscribe(handle, body, onEvent, opts = {}) {
|
|
240
|
+
this.assertLiveHandle(handle);
|
|
212
241
|
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
213
242
|
const priority = opts.priority ?? Priority.Interactive;
|
|
214
|
-
const
|
|
215
|
-
const
|
|
243
|
+
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
244
|
+
const corr = this.allocateCorr();
|
|
245
|
+
const key = pendingKey(handle, corr);
|
|
216
246
|
const closed = new Promise((resolve, reject) => {
|
|
217
247
|
if (this.closedErr) {
|
|
218
248
|
reject(this.closedErr);
|
|
219
249
|
return;
|
|
220
250
|
}
|
|
221
|
-
// No timeout: a subscription stays open indefinitely until StreamEnd, Error,
|
|
222
|
-
// route GOODBYE, or unsubscribe.
|
|
223
251
|
this.pending.set(key, {
|
|
224
|
-
|
|
252
|
+
handle,
|
|
225
253
|
resolve: () => resolve(),
|
|
226
254
|
reject,
|
|
227
255
|
onProgress: onEvent,
|
|
228
256
|
timer: null,
|
|
229
257
|
subscription: true,
|
|
230
258
|
});
|
|
231
|
-
const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false),
|
|
259
|
+
const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, bytes);
|
|
232
260
|
this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
this.rejectPending(key,
|
|
261
|
+
const pending = this.pending.get(key);
|
|
262
|
+
if (pending)
|
|
263
|
+
this.rejectPending(key, pending, err instanceof Error ? err : new SubcError(String(err)));
|
|
236
264
|
});
|
|
237
265
|
});
|
|
238
266
|
let cancelled = false;
|
|
@@ -240,89 +268,80 @@ export class SubcClient {
|
|
|
240
268
|
if (cancelled)
|
|
241
269
|
return;
|
|
242
270
|
cancelled = true;
|
|
243
|
-
|
|
244
|
-
// handler and ends with StreamEnd, which settles `closed`.
|
|
245
|
-
const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
|
|
246
|
-
this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {
|
|
247
|
-
// Best-effort: if the socket is already gone, the read loop fails the
|
|
248
|
-
// pending waiter and `closed` rejects on its own.
|
|
249
|
-
});
|
|
271
|
+
this.cancel(handle, corr, priority);
|
|
250
272
|
};
|
|
251
273
|
return { unsubscribe, closed };
|
|
252
274
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
/** Send a pure-header cancellation for an in-flight request. */
|
|
276
|
+
cancel(handle, corr, priority = Priority.Interactive) {
|
|
277
|
+
this.assertLiveHandle(handle);
|
|
278
|
+
const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), handle.channel, handle.epoch, corr, EMPTY_BODY);
|
|
279
|
+
this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => undefined);
|
|
280
|
+
}
|
|
281
|
+
/** Poll status or liveness for exactly the supplied route generation. */
|
|
282
|
+
async routePoll(handle, kind) {
|
|
283
|
+
this.assertLiveHandle(handle);
|
|
284
|
+
const body = this.encode({
|
|
285
|
+
op: "route.poll",
|
|
286
|
+
route_channel: handle.channel,
|
|
287
|
+
route_epoch: handle.epoch,
|
|
288
|
+
kind,
|
|
289
|
+
});
|
|
290
|
+
const reply = await this.controlRpc(body, (frame) => {
|
|
291
|
+
if (frame.header.ty !== FrameType.Response)
|
|
292
|
+
return true;
|
|
293
|
+
const parsed = this.parseJson(frame);
|
|
294
|
+
return parsed.route_channel === handle.channel && parsed.route_epoch === handle.epoch;
|
|
295
|
+
});
|
|
296
|
+
return this.parseJson(reply);
|
|
297
|
+
}
|
|
298
|
+
/** Tear down exactly the supplied route generation. */
|
|
299
|
+
async closeRoute(handle, opts = {}) {
|
|
300
|
+
this.assertLiveHandle(handle);
|
|
301
|
+
for (const [key, cached] of this.routes) {
|
|
302
|
+
if (cached.handle && sameRouteHandle(cached.handle, handle)) {
|
|
303
|
+
cached.closed = true;
|
|
304
|
+
cached.handle = null;
|
|
305
|
+
this.routes.delete(key);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (opts.drain)
|
|
309
|
+
await this.drainUnaryOnHandle(handle);
|
|
310
|
+
this.failHandle(handle, new SubcError("route closed by closeRoute", "route_closed"));
|
|
311
|
+
if (this.liveRoutes.get(handle.channel) === handle)
|
|
312
|
+
this.liveRoutes.delete(handle.channel);
|
|
313
|
+
this.sendRouteGoodbye(handle);
|
|
314
|
+
}
|
|
315
|
+
/** Close a cached managed route by its route-open identity tuple. */
|
|
316
|
+
async closeManagedRoute(target, identity, opts = {}) {
|
|
269
317
|
const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
|
|
270
318
|
const cached = this.routes.get(key);
|
|
271
319
|
if (!cached)
|
|
272
|
-
return;
|
|
273
|
-
// Generation guard: an in-flight openCachedRoute holds this same object and
|
|
274
|
-
// re-checks `closed` before installing its channel, so flipping it here makes
|
|
275
|
-
// close win over a racing reopen. Removing the map entry lets a later call()
|
|
276
|
-
// create a fresh route for the key (not a permanent tombstone).
|
|
320
|
+
return;
|
|
277
321
|
cached.closed = true;
|
|
278
322
|
this.routes.delete(key);
|
|
279
|
-
const
|
|
280
|
-
cached.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// nothing local to tear down here.
|
|
284
|
-
if (channel !== null)
|
|
285
|
-
await this.closeRouteChannel(channel, opts);
|
|
323
|
+
const handle = cached.handle;
|
|
324
|
+
cached.handle = null;
|
|
325
|
+
if (handle)
|
|
326
|
+
await this.closeRoute(handle, opts);
|
|
286
327
|
}
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
* {name, arguments}) and hold the channel themselves. Idempotent, never throws.
|
|
291
|
-
* Settles in-flight requests on the channel as RouteClosed and sends a best-effort
|
|
292
|
-
* route GOODBYE. `opts.drain` awaits in-flight UNARY requests first; subscriptions
|
|
293
|
-
* are always aborted (a held-open stream cannot be drained).
|
|
294
|
-
*/
|
|
295
|
-
async closeRouteChannel(channel, opts = {}) {
|
|
296
|
-
if (channel === 0)
|
|
297
|
-
return; // channel 0 is the control plane, never a route.
|
|
298
|
-
if (opts.drain) {
|
|
299
|
-
// Wait only for in-flight UNARY requests on this channel; subscriptions are
|
|
300
|
-
// aborted below (a held-open stream has no natural completion to drain to).
|
|
301
|
-
await this.drainUnaryOnChannel(channel);
|
|
302
|
-
}
|
|
303
|
-
// Settle anything still in flight on the channel (all of it in abort mode; only
|
|
304
|
-
// subscriptions + late stragglers after a drain). Managed requests are classified
|
|
305
|
-
// at-most-once via their classifyFailure; raw requests/subscriptions get a plain
|
|
306
|
-
// RouteClosed error.
|
|
307
|
-
this.failChannel(channel, new SubcError("route closed by closeRoute", "route_closed"));
|
|
308
|
-
// Best-effort GOODBYE: releases the route on the daemon and notifies the module.
|
|
309
|
-
this.sendRouteGoodbye(channel);
|
|
328
|
+
/** Alias retained for callers that name the operation by protocol channel; it still requires a full handle. */
|
|
329
|
+
async closeRouteChannel(handle, opts = {}) {
|
|
330
|
+
await this.closeRoute(handle, opts);
|
|
310
331
|
}
|
|
311
332
|
close() {
|
|
312
333
|
this.closeStarted = true;
|
|
313
334
|
this.fail(new SubcError("client closed"));
|
|
314
335
|
this.sock.close();
|
|
315
336
|
}
|
|
316
|
-
|
|
317
|
-
* time) has settled. Subscriptions are excluded — they are aborted, not drained. */
|
|
318
|
-
drainUnaryOnChannel(channel) {
|
|
337
|
+
drainUnaryOnHandle(handle) {
|
|
319
338
|
const waiters = [];
|
|
320
339
|
for (const pending of this.pending.values()) {
|
|
321
|
-
if (pending.
|
|
340
|
+
if (pending.handle === handle && !pending.subscription) {
|
|
322
341
|
waiters.push(new Promise((resolve) => {
|
|
323
|
-
const
|
|
342
|
+
const previous = pending.onSettle;
|
|
324
343
|
pending.onSettle = () => {
|
|
325
|
-
|
|
344
|
+
previous?.();
|
|
326
345
|
resolve();
|
|
327
346
|
};
|
|
328
347
|
}));
|
|
@@ -330,14 +349,20 @@ export class SubcClient {
|
|
|
330
349
|
}
|
|
331
350
|
return Promise.all(waiters).then(() => undefined);
|
|
332
351
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
352
|
+
sendRouteGoodbye(handle, closeOnQueueFailure = false) {
|
|
353
|
+
this.assertLiveConnection(handle);
|
|
354
|
+
if (this.closedErr) {
|
|
355
|
+
if (closeOnQueueFailure)
|
|
356
|
+
this.closeConnectionAfterCleanupFailure();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), handle.channel, handle.epoch, 0n, EMPTY_BODY);
|
|
360
|
+
const write = this.sock.writeTracked(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
|
|
361
|
+
if (!write.queued && closeOnQueueFailure)
|
|
362
|
+
this.closeConnectionAfterCleanupFailure();
|
|
363
|
+
write.completed.catch(() => {
|
|
364
|
+
if (closeOnQueueFailure && !write.queued)
|
|
365
|
+
this.closeConnectionAfterCleanupFailure();
|
|
341
366
|
});
|
|
342
367
|
}
|
|
343
368
|
static async openConnection(opts) {
|
|
@@ -354,33 +379,42 @@ export class SubcClient {
|
|
|
354
379
|
}
|
|
355
380
|
return { sock, conn };
|
|
356
381
|
}
|
|
357
|
-
async controlRpc(body) {
|
|
358
|
-
|
|
359
|
-
return this.send(0, body, Priority.Interactive, undefined, undefined);
|
|
382
|
+
async controlRpc(body, acceptFrame, onLateResponse) {
|
|
383
|
+
return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
|
|
360
384
|
}
|
|
361
|
-
send(
|
|
385
|
+
send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
|
|
386
|
+
if (handle)
|
|
387
|
+
this.assertLiveHandle(handle);
|
|
362
388
|
if (this.closedErr)
|
|
363
389
|
return Promise.reject(this.closedErr);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
390
|
+
let corr;
|
|
391
|
+
try {
|
|
392
|
+
corr = this.allocateCorr();
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return Promise.reject(error);
|
|
396
|
+
}
|
|
397
|
+
const key = pendingKey(handle, corr);
|
|
398
|
+
const channel = handle?.channel ?? 0;
|
|
399
|
+
const epoch = handle?.epoch ?? 0;
|
|
400
|
+
const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), channel, epoch, corr, body);
|
|
367
401
|
return new Promise((resolve, reject) => {
|
|
368
402
|
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
369
403
|
const pending = {
|
|
370
|
-
|
|
404
|
+
handle,
|
|
371
405
|
resolve,
|
|
372
406
|
reject,
|
|
373
407
|
onProgress,
|
|
374
408
|
timer: null,
|
|
409
|
+
acceptFrame,
|
|
410
|
+
onLateResponse,
|
|
375
411
|
};
|
|
376
|
-
pending.timer = setTimeout(() =>
|
|
377
|
-
this.arbitrateTimeout(key, pending, channel, corr, ms);
|
|
378
|
-
}, ms);
|
|
412
|
+
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
|
|
379
413
|
this.pending.set(key, pending);
|
|
380
|
-
this.sock.write(encodeFrame(frame), Date.now() + ms).catch((
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
this.rejectPending(key,
|
|
414
|
+
this.sock.write(encodeFrame(frame), Date.now() + ms).catch((error) => {
|
|
415
|
+
const current = this.pending.get(key);
|
|
416
|
+
if (current)
|
|
417
|
+
this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
|
|
384
418
|
});
|
|
385
419
|
});
|
|
386
420
|
}
|
|
@@ -396,6 +430,8 @@ export class SubcClient {
|
|
|
396
430
|
*/
|
|
397
431
|
arbitrateTimeout(key, pending, channel, corr, ms) {
|
|
398
432
|
const settleAsTimeout = () => {
|
|
433
|
+
if (pending.onLateResponse)
|
|
434
|
+
this.lateResponses.set(key, pending.onLateResponse);
|
|
399
435
|
this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
|
|
400
436
|
};
|
|
401
437
|
const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
|
|
@@ -416,69 +452,70 @@ export class SubcClient {
|
|
|
416
452
|
};
|
|
417
453
|
setImmediate(arbitrate);
|
|
418
454
|
}
|
|
419
|
-
async managedRequest(
|
|
455
|
+
async managedRequest(handle, body, opts) {
|
|
420
456
|
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
421
457
|
const priority = opts.priority ?? Priority.Interactive;
|
|
458
|
+
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
422
459
|
try {
|
|
423
|
-
const reply = await this.sendManaged(
|
|
460
|
+
const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
|
|
424
461
|
return this.parseJson(reply);
|
|
425
462
|
}
|
|
426
|
-
catch (
|
|
427
|
-
if (
|
|
428
|
-
throw
|
|
429
|
-
throw this.terminalCallError("managed call failed",
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (error instanceof SubcCallError)
|
|
465
|
+
throw error;
|
|
466
|
+
throw this.terminalCallError("managed call failed", error);
|
|
430
467
|
}
|
|
431
468
|
}
|
|
432
|
-
sendManaged(
|
|
469
|
+
sendManaged(handle, body, priority, admission, timeoutMs, onProgress) {
|
|
470
|
+
try {
|
|
471
|
+
this.assertLiveHandle(handle);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
return Promise.reject(this.notSentCallError("request used a stale route handle", error));
|
|
475
|
+
}
|
|
433
476
|
if (this.closedErr) {
|
|
434
477
|
return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
|
|
435
478
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
479
|
+
let corr;
|
|
480
|
+
try {
|
|
481
|
+
corr = this.allocateCorr();
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
return Promise.reject(this.notSentCallError("request correlation allocator was exhausted", error));
|
|
485
|
+
}
|
|
486
|
+
const key = pendingKey(handle, corr);
|
|
487
|
+
const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, body);
|
|
439
488
|
let handedToSocket = false;
|
|
440
|
-
const classifyFailure = (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// therefore OutcomeUnknown to avoid an unsafe double-mutation retry.
|
|
446
|
-
if (!handedToSocket) {
|
|
447
|
-
return this.notSentCallError("request bytes were not queued to the subc socket", err);
|
|
448
|
-
}
|
|
449
|
-
// A request-deadline timeout (arbitration expired without observing a drop)
|
|
450
|
-
// is refined from a real connection drop: the socket was NOT seen to fail, so
|
|
451
|
-
// the caller can skip a was-it-even-sent recovery path. Still outcome_unknown
|
|
452
|
-
// — queued-to-local-socket is not proof the daemon received or ran it.
|
|
453
|
-
if (err instanceof SubcError && err.code === REQUEST_DEADLINE_MARKER) {
|
|
454
|
-
return new SubcCallError("outcome_unknown", `managed call deadline exceeded after request bytes were queued to the local socket; no terminal response was observed; outcome unknown${causeMessage(err)}`, DEADLINE_NO_DROP_CODE, err);
|
|
489
|
+
const classifyFailure = (error) => {
|
|
490
|
+
if (!handedToSocket)
|
|
491
|
+
return this.notSentCallError("request bytes were not queued to the subc socket", error);
|
|
492
|
+
if (error instanceof SubcError && error.code === REQUEST_DEADLINE_MARKER) {
|
|
493
|
+
return new SubcCallError("outcome_unknown", `managed call deadline exceeded after request bytes were queued to the local socket; no terminal response was observed; outcome unknown${causeMessage(error)}`, DEADLINE_NO_DROP_CODE, error);
|
|
455
494
|
}
|
|
456
|
-
return this.outcomeUnknownCallError("connection dropped before the managed call returned a response",
|
|
495
|
+
return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", error);
|
|
457
496
|
};
|
|
458
497
|
return new Promise((resolve, reject) => {
|
|
459
498
|
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
460
499
|
const pending = {
|
|
461
|
-
|
|
500
|
+
handle,
|
|
462
501
|
resolve,
|
|
463
502
|
reject,
|
|
464
503
|
onProgress,
|
|
465
504
|
timer: null,
|
|
466
505
|
classifyFailure,
|
|
467
506
|
};
|
|
468
|
-
pending.timer = setTimeout(() =>
|
|
469
|
-
this.arbitrateTimeout(key, pending, channel, corr, ms);
|
|
470
|
-
}, ms);
|
|
507
|
+
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
|
|
471
508
|
this.pending.set(key, pending);
|
|
472
509
|
const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
|
|
473
510
|
handedToSocket = write.queued;
|
|
474
|
-
write.completed.catch((
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
this.rejectPending(key,
|
|
511
|
+
write.completed.catch((error) => {
|
|
512
|
+
const current = this.pending.get(key);
|
|
513
|
+
if (current)
|
|
514
|
+
this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
|
|
478
515
|
});
|
|
479
516
|
});
|
|
480
517
|
}
|
|
481
|
-
async
|
|
518
|
+
async cachedRouteHandle(moduleId, opts) {
|
|
482
519
|
const identity = opts.identity ?? this.opts.identity;
|
|
483
520
|
if (!identity) {
|
|
484
521
|
throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
|
|
@@ -494,15 +531,13 @@ export class SubcClient {
|
|
|
494
531
|
target,
|
|
495
532
|
identity,
|
|
496
533
|
consumerIdentity,
|
|
497
|
-
|
|
498
|
-
generation: 0,
|
|
534
|
+
handle: null,
|
|
499
535
|
opening: null,
|
|
500
536
|
};
|
|
501
537
|
this.routes.set(key, cached);
|
|
502
538
|
}
|
|
503
|
-
if (cached.
|
|
504
|
-
return cached.
|
|
505
|
-
}
|
|
539
|
+
if (cached.handle && this.isLiveHandle(cached.handle))
|
|
540
|
+
return cached.handle;
|
|
506
541
|
if (!cached.opening) {
|
|
507
542
|
cached.opening = this.openCachedRoute(cached).finally(() => {
|
|
508
543
|
cached.opening = null;
|
|
@@ -520,61 +555,45 @@ export class SubcClient {
|
|
|
520
555
|
try {
|
|
521
556
|
await this.ensureConnectedForManaged();
|
|
522
557
|
}
|
|
523
|
-
catch (
|
|
524
|
-
throw this.notSentRecoveryError("route.open could not run because reconnect failed",
|
|
525
|
-
}
|
|
526
|
-
if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
|
|
527
|
-
return cached.channel;
|
|
558
|
+
catch (error) {
|
|
559
|
+
throw this.notSentRecoveryError("route.open could not run because reconnect failed", error);
|
|
528
560
|
}
|
|
561
|
+
if (cached.handle && this.isLiveHandle(cached.handle))
|
|
562
|
+
return cached.handle;
|
|
529
563
|
try {
|
|
530
|
-
const
|
|
564
|
+
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
531
565
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
532
566
|
});
|
|
533
|
-
// Generation guard: a closeRoute may have flipped the tombstone WHILE this
|
|
534
|
-
// route.open was in flight. If so, close wins — do NOT install the channel
|
|
535
|
-
// into the (already-removed) cache entry; GOODBYE the channel we just opened
|
|
536
|
-
// so the daemon/module don't leak it, and fail as RouteClosed.
|
|
537
567
|
if (cached.closed) {
|
|
538
|
-
this.
|
|
568
|
+
this.liveRoutes.delete(handle.channel);
|
|
569
|
+
this.sendRouteGoodbye(handle);
|
|
539
570
|
throw this.routeClosedDuringOpen();
|
|
540
571
|
}
|
|
541
|
-
cached.
|
|
542
|
-
|
|
543
|
-
return channel;
|
|
572
|
+
cached.handle = handle;
|
|
573
|
+
return handle;
|
|
544
574
|
}
|
|
545
|
-
catch (
|
|
546
|
-
if (
|
|
547
|
-
throw
|
|
548
|
-
if (!this.closeStarted && isConsumerReconnectTransient(
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (error instanceof SubcCallError && error.code === "route_closed")
|
|
577
|
+
throw error;
|
|
578
|
+
if (!this.closeStarted && isConsumerReconnectTransient(error)) {
|
|
549
579
|
try {
|
|
550
|
-
await this.reconnectAfterDrop(
|
|
580
|
+
await this.reconnectAfterDrop(error);
|
|
551
581
|
}
|
|
552
|
-
catch (
|
|
553
|
-
throw this.notSentRecoveryError("route.open was not sent and reconnect failed",
|
|
582
|
+
catch (reconnectError) {
|
|
583
|
+
throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectError);
|
|
554
584
|
}
|
|
555
585
|
continue;
|
|
556
586
|
}
|
|
557
|
-
|
|
558
|
-
// reloading / momentarily absent) is retried IN-PLACE against the same live
|
|
559
|
-
// connection — never a socket reconnect, which would needlessly disrupt this
|
|
560
|
-
// connection's other routes — until the route-retry deadline. Past the
|
|
561
|
-
// deadline it surfaces as not_sent: provably pre-send (no data frame ever
|
|
562
|
-
// left the client) AND still transient, so the caller's own retry policy may
|
|
563
|
-
// safely re-attempt later. Reason and set kept in parity with subc-client-rs.
|
|
564
|
-
if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
|
|
587
|
+
if (!this.closeStarted && error instanceof SubcError && isRetryableRouteOpenCode(error.code)) {
|
|
565
588
|
routeRetryAttempt += 1;
|
|
566
589
|
if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
|
|
567
590
|
await this.opts.sleep(routeRetryDelay);
|
|
568
591
|
routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
|
|
569
592
|
continue;
|
|
570
593
|
}
|
|
571
|
-
throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${
|
|
594
|
+
throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error.code} (retry budget exhausted)`, error);
|
|
572
595
|
}
|
|
573
|
-
|
|
574
|
-
// unknown_target, ...) is pre-send but would never succeed on retry, so it
|
|
575
|
-
// stays terminal — a not_sent class here would invite a retry storm against a
|
|
576
|
-
// request the daemon will always reject.
|
|
577
|
-
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
|
|
596
|
+
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error);
|
|
578
597
|
}
|
|
579
598
|
}
|
|
580
599
|
}
|
|
@@ -649,33 +668,27 @@ export class SubcClient {
|
|
|
649
668
|
this.currentConn = opened.conn;
|
|
650
669
|
this.closedErr = null;
|
|
651
670
|
this.generation += 1;
|
|
671
|
+
this.connectionToken = newConnectionToken();
|
|
672
|
+
this.liveRoutes.clear();
|
|
673
|
+
this.lateResponses.clear();
|
|
674
|
+
this.nextCorr = 1n;
|
|
652
675
|
void this.readLoop(opened.sock, this.generation);
|
|
653
676
|
}
|
|
654
677
|
async reopenCachedRoutes() {
|
|
655
|
-
for (const cached of this.routes.values())
|
|
656
|
-
cached.
|
|
657
|
-
cached.generation = 0;
|
|
658
|
-
}
|
|
678
|
+
for (const cached of this.routes.values())
|
|
679
|
+
cached.handle = null;
|
|
659
680
|
for (const cached of this.routes.values()) {
|
|
660
681
|
if (cached.closed)
|
|
661
|
-
continue;
|
|
662
|
-
|
|
663
|
-
// lazy per-call path (openCachedRoute) does. Dropping it here would make a
|
|
664
|
-
// route reopened after a reconnect send route.open with no consumer_identity,
|
|
665
|
-
// so the daemon would re-stamp it with a different (weaker) principal than the
|
|
666
|
-
// one it was originally bound under — a silent post-reconnect trust downgrade.
|
|
667
|
-
const channel = await this.routeOpen(cached.target, cached.identity, {
|
|
682
|
+
continue;
|
|
683
|
+
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
668
684
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
669
685
|
});
|
|
670
|
-
// A closeRoute may have raced this reopen (flipping the tombstone during the
|
|
671
|
-
// route.open await). If so, GOODBYE the channel instead of installing it, so the
|
|
672
|
-
// closed route isn't silently re-established on the new connection.
|
|
673
686
|
if (cached.closed) {
|
|
674
|
-
this.
|
|
687
|
+
this.liveRoutes.delete(handle.channel);
|
|
688
|
+
this.sendRouteGoodbye(handle);
|
|
675
689
|
continue;
|
|
676
690
|
}
|
|
677
|
-
cached.
|
|
678
|
-
cached.generation = this.generation;
|
|
691
|
+
cached.handle = handle;
|
|
679
692
|
}
|
|
680
693
|
}
|
|
681
694
|
// A request timeout carries the local socket port and (channel, corr) so a
|
|
@@ -693,41 +706,39 @@ export class SubcClient {
|
|
|
693
706
|
async readLoop(sock, generation) {
|
|
694
707
|
try {
|
|
695
708
|
for (;;) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// A frame is now arriving. Mark the reader active THROUGH dispatch so a
|
|
701
|
-
// timeout that fires mid-arrival grants the reply its bounded grace window
|
|
702
|
-
// instead of settling as a spurious timeout.
|
|
703
|
-
this.readerActive = true;
|
|
709
|
+
this.readerActive = false;
|
|
710
|
+
const frame = await sock.readFrame(Number.POSITIVE_INFINITY, Date.now() + BODY_READ_TIMEOUT_MS, () => {
|
|
711
|
+
this.readerActive = true;
|
|
712
|
+
});
|
|
704
713
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
? new Uint8Array(0)
|
|
708
|
-
: await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
|
|
709
|
-
// Drop a frame read off a socket this client has already replaced
|
|
710
|
-
// (reconnect): its pendings were settled by fail(), and dispatching it
|
|
711
|
-
// against the current pending map could match a re-used (channel, corr).
|
|
712
|
-
if (this.sock === sock && this.generation === generation) {
|
|
713
|
-
this.dispatch({ header, body });
|
|
714
|
-
}
|
|
714
|
+
if (this.sock === sock && this.generation === generation)
|
|
715
|
+
this.dispatch(frame);
|
|
715
716
|
}
|
|
716
717
|
finally {
|
|
717
718
|
this.readerActive = false;
|
|
718
719
|
}
|
|
719
720
|
}
|
|
720
721
|
}
|
|
721
|
-
catch (
|
|
722
|
+
catch (error) {
|
|
722
723
|
if (this.sock === sock && this.generation === generation) {
|
|
723
|
-
this.fail(
|
|
724
|
+
this.fail(error instanceof Error ? error : new SubcError(String(error)));
|
|
724
725
|
}
|
|
725
726
|
}
|
|
726
727
|
}
|
|
727
728
|
dispatch(frame) {
|
|
728
|
-
|
|
729
|
+
let handle = null;
|
|
730
|
+
if (frame.header.channel !== 0) {
|
|
731
|
+
handle = this.liveRoutes.get(frame.header.channel) ?? null;
|
|
732
|
+
if (!handle || handle.epoch !== frame.header.epoch) {
|
|
733
|
+
this.ingressEpochDropCount += 1;
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const key = pendingKey(handle, frame.header.corr);
|
|
729
738
|
const pending = this.pending.get(key);
|
|
730
739
|
if (pending) {
|
|
740
|
+
if (pending.acceptFrame && !pending.acceptFrame(frame))
|
|
741
|
+
return;
|
|
731
742
|
switch (frame.header.ty) {
|
|
732
743
|
case FrameType.Push:
|
|
733
744
|
case FrameType.StreamData:
|
|
@@ -744,28 +755,24 @@ export class SubcClient {
|
|
|
744
755
|
return;
|
|
745
756
|
}
|
|
746
757
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
758
|
+
const late = this.lateResponses.get(key);
|
|
759
|
+
if (late && (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error)) {
|
|
760
|
+
this.lateResponses.delete(key);
|
|
761
|
+
late(frame);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (frame.header.ty === FrameType.Goodbye && handle) {
|
|
765
|
+
this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)"));
|
|
766
|
+
if (this.liveRoutes.get(handle.channel) === handle)
|
|
767
|
+
this.liveRoutes.delete(handle.channel);
|
|
768
|
+
this.evictRouteHandle(handle);
|
|
753
769
|
return;
|
|
754
770
|
}
|
|
755
|
-
// A terminal frame (Response/Error/StreamEnd) with no waiter is almost always
|
|
756
|
-
// a reply that arrived AFTER its request already settled — the fingerprint of a
|
|
757
|
-
// premature timeout under event-loop starvation (the reply raced the deadline
|
|
758
|
-
// and lost). Metadata-only debug log (never the body) so every future
|
|
759
|
-
// occurrence is a one-line diagnosis instead of an invisible drop. Enable with
|
|
760
|
-
// NODE_DEBUG=subc-client.
|
|
761
771
|
if (frame.header.ty === FrameType.Response ||
|
|
762
772
|
frame.header.ty === FrameType.Error ||
|
|
763
773
|
frame.header.ty === FrameType.StreamEnd) {
|
|
764
|
-
debug("dropped terminal frame with no waiter: type=%d channel=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.corr, this.sock.localPort() ?? "?");
|
|
765
|
-
return;
|
|
774
|
+
debug("dropped terminal frame with no waiter: type=%d channel=%d epoch=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.epoch, frame.header.corr, this.sock.localPort() ?? "?");
|
|
766
775
|
}
|
|
767
|
-
// Unmatched Push or stray frame: no registered waiter. Drop it — v1 has no
|
|
768
|
-
// unsolicited-push consumers.
|
|
769
776
|
}
|
|
770
777
|
/**
|
|
771
778
|
* Settle a pending exactly once. The object-identity guard (the map still
|
|
@@ -798,24 +805,16 @@ export class SubcClient {
|
|
|
798
805
|
return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
799
806
|
}
|
|
800
807
|
}
|
|
801
|
-
|
|
802
|
-
* Drop a dead channel from the managed-route cache so the next call re-opens.
|
|
803
|
-
* Only clears entries still pointing at THIS channel in the CURRENT socket
|
|
804
|
-
* generation; a route already re-opened (new channel) or re-created after a
|
|
805
|
-
* reconnect (new generation) is left alone.
|
|
806
|
-
*/
|
|
807
|
-
evictRouteChannel(channel) {
|
|
808
|
+
evictRouteHandle(handle) {
|
|
808
809
|
for (const cached of this.routes.values()) {
|
|
809
|
-
if (cached.
|
|
810
|
-
cached.
|
|
811
|
-
}
|
|
810
|
+
if (cached.handle && sameRouteHandle(cached.handle, handle))
|
|
811
|
+
cached.handle = null;
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
|
-
|
|
814
|
+
failHandle(handle, error) {
|
|
815
815
|
for (const [key, pending] of this.pending) {
|
|
816
|
-
if (pending.
|
|
817
|
-
this.rejectPending(key, pending,
|
|
818
|
-
}
|
|
816
|
+
if (pending.handle && sameRouteHandle(pending.handle, handle))
|
|
817
|
+
this.rejectPending(key, pending, error);
|
|
819
818
|
}
|
|
820
819
|
}
|
|
821
820
|
fail(err) {
|
|
@@ -843,6 +842,45 @@ export class SubcClient {
|
|
|
843
842
|
return this.notSentCallError(message, cause);
|
|
844
843
|
return this.terminalCallError(message, cause);
|
|
845
844
|
}
|
|
845
|
+
/** Number of nonzero-channel ingress frames dropped by endpoint epoch validation. */
|
|
846
|
+
get droppedIngressFrames() {
|
|
847
|
+
return this.ingressEpochDropCount;
|
|
848
|
+
}
|
|
849
|
+
installRoute(channel, epoch) {
|
|
850
|
+
const handle = createRouteHandle(channel, epoch, this.connectionToken);
|
|
851
|
+
this.liveRoutes.set(channel, handle);
|
|
852
|
+
return handle;
|
|
853
|
+
}
|
|
854
|
+
isLiveHandle(handle) {
|
|
855
|
+
return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
|
|
856
|
+
}
|
|
857
|
+
assertLiveConnection(handle) {
|
|
858
|
+
if (!belongsToConnection(handle, this.connectionToken))
|
|
859
|
+
throw new StaleRouteHandleError(handle);
|
|
860
|
+
}
|
|
861
|
+
assertLiveHandle(handle) {
|
|
862
|
+
if (!this.isLiveHandle(handle))
|
|
863
|
+
throw new StaleRouteHandleError(handle);
|
|
864
|
+
}
|
|
865
|
+
allocateCorr() {
|
|
866
|
+
const maximum = 0xffffffffffffffffn;
|
|
867
|
+
if (this.nextCorr > maximum) {
|
|
868
|
+
const error = new SubcError("channel-0 correlation id allocator exhausted", "corr_exhausted");
|
|
869
|
+
this.fail(error);
|
|
870
|
+
this.sock.close();
|
|
871
|
+
this.scheduleReconnectAfterDrop(error);
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
const corr = this.nextCorr;
|
|
875
|
+
this.nextCorr += 1n;
|
|
876
|
+
return corr;
|
|
877
|
+
}
|
|
878
|
+
closeConnectionAfterCleanupFailure() {
|
|
879
|
+
const error = new SubcError("late route cleanup could not be queued", "late_route_cleanup_failed");
|
|
880
|
+
this.fail(error);
|
|
881
|
+
this.sock.close();
|
|
882
|
+
this.scheduleReconnectAfterDrop(error);
|
|
883
|
+
}
|
|
846
884
|
encode(value) {
|
|
847
885
|
return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
|
|
848
886
|
}
|
|
@@ -939,4 +977,7 @@ function causeMessage(cause) {
|
|
|
939
977
|
return "";
|
|
940
978
|
return `: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
941
979
|
}
|
|
980
|
+
function pendingKey(handle, corr) {
|
|
981
|
+
return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
|
|
982
|
+
}
|
|
942
983
|
//# sourceMappingURL=client.js.map
|