@cortexkit/subc-client 0.3.3 → 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 -47
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +316 -247
- 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 +388 -262
- 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
|
/**
|
|
@@ -162,14 +196,25 @@ export class SubcClient {
|
|
|
162
196
|
*/
|
|
163
197
|
async call(moduleId, method, params, opts = {}) {
|
|
164
198
|
const body = params === undefined ? { method } : { method, params };
|
|
199
|
+
let retriedUnknownChannel = false;
|
|
165
200
|
for (;;) {
|
|
166
|
-
const
|
|
201
|
+
const routeHandle = await this.cachedRouteHandle(moduleId, opts);
|
|
167
202
|
try {
|
|
168
|
-
return (await this.managedRequest(
|
|
203
|
+
return (await this.managedRequest(routeHandle, body, opts));
|
|
169
204
|
}
|
|
170
205
|
catch (err) {
|
|
171
206
|
if (!(err instanceof SubcCallError))
|
|
172
207
|
throw this.terminalCallError("managed call failed", err);
|
|
208
|
+
// unknown_channel is the daemon ROUTER refusing an unrouted channel — the
|
|
209
|
+
// request provably never reached a module, so one in-place retry cannot
|
|
210
|
+
// double-execute anything. The cached bind is dead (module restarted and
|
|
211
|
+
// its route-gone GOODBYE raced or was missed); evict it so the retry
|
|
212
|
+
// re-opens the route instead of resending into the same dead channel.
|
|
213
|
+
if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
|
|
214
|
+
retriedUnknownChannel = true;
|
|
215
|
+
this.evictRouteHandle(routeHandle);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
173
218
|
if (err.kind === "not_sent") {
|
|
174
219
|
try {
|
|
175
220
|
await this.reconnectAfterDrop(err);
|
|
@@ -190,38 +235,32 @@ export class SubcClient {
|
|
|
190
235
|
}
|
|
191
236
|
}
|
|
192
237
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* returned `closed` settles on the StreamEnd terminal (resolve) or an Error / route
|
|
197
|
-
* GOODBYE (reject). Events ride this held-open request's correlation id — they are
|
|
198
|
-
* never unsolicited, so they are not dropped. Call `unsubscribe()` to cancel.
|
|
199
|
-
*/
|
|
200
|
-
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);
|
|
201
241
|
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
202
242
|
const priority = opts.priority ?? Priority.Interactive;
|
|
203
|
-
const
|
|
204
|
-
const
|
|
243
|
+
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
244
|
+
const corr = this.allocateCorr();
|
|
245
|
+
const key = pendingKey(handle, corr);
|
|
205
246
|
const closed = new Promise((resolve, reject) => {
|
|
206
247
|
if (this.closedErr) {
|
|
207
248
|
reject(this.closedErr);
|
|
208
249
|
return;
|
|
209
250
|
}
|
|
210
|
-
// No timeout: a subscription stays open indefinitely until StreamEnd, Error,
|
|
211
|
-
// route GOODBYE, or unsubscribe.
|
|
212
251
|
this.pending.set(key, {
|
|
213
|
-
|
|
252
|
+
handle,
|
|
214
253
|
resolve: () => resolve(),
|
|
215
254
|
reject,
|
|
216
255
|
onProgress: onEvent,
|
|
217
256
|
timer: null,
|
|
218
257
|
subscription: true,
|
|
219
258
|
});
|
|
220
|
-
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);
|
|
221
260
|
this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
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)));
|
|
225
264
|
});
|
|
226
265
|
});
|
|
227
266
|
let cancelled = false;
|
|
@@ -229,89 +268,80 @@ export class SubcClient {
|
|
|
229
268
|
if (cancelled)
|
|
230
269
|
return;
|
|
231
270
|
cancelled = true;
|
|
232
|
-
|
|
233
|
-
// handler and ends with StreamEnd, which settles `closed`.
|
|
234
|
-
const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
|
|
235
|
-
this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {
|
|
236
|
-
// Best-effort: if the socket is already gone, the read loop fails the
|
|
237
|
-
// pending waiter and `closed` rejects on its own.
|
|
238
|
-
});
|
|
271
|
+
this.cancel(handle, corr, priority);
|
|
239
272
|
};
|
|
240
273
|
return { unsubscribe, closed };
|
|
241
274
|
}
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 = {}) {
|
|
258
317
|
const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
|
|
259
318
|
const cached = this.routes.get(key);
|
|
260
319
|
if (!cached)
|
|
261
|
-
return;
|
|
262
|
-
// Generation guard: an in-flight openCachedRoute holds this same object and
|
|
263
|
-
// re-checks `closed` before installing its channel, so flipping it here makes
|
|
264
|
-
// close win over a racing reopen. Removing the map entry lets a later call()
|
|
265
|
-
// create a fresh route for the key (not a permanent tombstone).
|
|
320
|
+
return;
|
|
266
321
|
cached.closed = true;
|
|
267
322
|
this.routes.delete(key);
|
|
268
|
-
const
|
|
269
|
-
cached.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// nothing local to tear down here.
|
|
273
|
-
if (channel !== null)
|
|
274
|
-
await this.closeRouteChannel(channel, opts);
|
|
323
|
+
const handle = cached.handle;
|
|
324
|
+
cached.handle = null;
|
|
325
|
+
if (handle)
|
|
326
|
+
await this.closeRoute(handle, opts);
|
|
275
327
|
}
|
|
276
|
-
/**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* {name, arguments}) and hold the channel themselves. Idempotent, never throws.
|
|
280
|
-
* Settles in-flight requests on the channel as RouteClosed and sends a best-effort
|
|
281
|
-
* route GOODBYE. `opts.drain` awaits in-flight UNARY requests first; subscriptions
|
|
282
|
-
* are always aborted (a held-open stream cannot be drained).
|
|
283
|
-
*/
|
|
284
|
-
async closeRouteChannel(channel, opts = {}) {
|
|
285
|
-
if (channel === 0)
|
|
286
|
-
return; // channel 0 is the control plane, never a route.
|
|
287
|
-
if (opts.drain) {
|
|
288
|
-
// Wait only for in-flight UNARY requests on this channel; subscriptions are
|
|
289
|
-
// aborted below (a held-open stream has no natural completion to drain to).
|
|
290
|
-
await this.drainUnaryOnChannel(channel);
|
|
291
|
-
}
|
|
292
|
-
// Settle anything still in flight on the channel (all of it in abort mode; only
|
|
293
|
-
// subscriptions + late stragglers after a drain). Managed requests are classified
|
|
294
|
-
// at-most-once via their classifyFailure; raw requests/subscriptions get a plain
|
|
295
|
-
// RouteClosed error.
|
|
296
|
-
this.failChannel(channel, new SubcError("route closed by closeRoute", "route_closed"));
|
|
297
|
-
// Best-effort GOODBYE: releases the route on the daemon and notifies the module.
|
|
298
|
-
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);
|
|
299
331
|
}
|
|
300
332
|
close() {
|
|
301
333
|
this.closeStarted = true;
|
|
302
334
|
this.fail(new SubcError("client closed"));
|
|
303
335
|
this.sock.close();
|
|
304
336
|
}
|
|
305
|
-
|
|
306
|
-
* time) has settled. Subscriptions are excluded — they are aborted, not drained. */
|
|
307
|
-
drainUnaryOnChannel(channel) {
|
|
337
|
+
drainUnaryOnHandle(handle) {
|
|
308
338
|
const waiters = [];
|
|
309
339
|
for (const pending of this.pending.values()) {
|
|
310
|
-
if (pending.
|
|
340
|
+
if (pending.handle === handle && !pending.subscription) {
|
|
311
341
|
waiters.push(new Promise((resolve) => {
|
|
312
|
-
const
|
|
342
|
+
const previous = pending.onSettle;
|
|
313
343
|
pending.onSettle = () => {
|
|
314
|
-
|
|
344
|
+
previous?.();
|
|
315
345
|
resolve();
|
|
316
346
|
};
|
|
317
347
|
}));
|
|
@@ -319,14 +349,20 @@ export class SubcClient {
|
|
|
319
349
|
}
|
|
320
350
|
return Promise.all(waiters).then(() => undefined);
|
|
321
351
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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();
|
|
330
366
|
});
|
|
331
367
|
}
|
|
332
368
|
static async openConnection(opts) {
|
|
@@ -343,33 +379,42 @@ export class SubcClient {
|
|
|
343
379
|
}
|
|
344
380
|
return { sock, conn };
|
|
345
381
|
}
|
|
346
|
-
async controlRpc(body) {
|
|
347
|
-
|
|
348
|
-
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);
|
|
349
384
|
}
|
|
350
|
-
send(
|
|
385
|
+
send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
|
|
386
|
+
if (handle)
|
|
387
|
+
this.assertLiveHandle(handle);
|
|
351
388
|
if (this.closedErr)
|
|
352
389
|
return Promise.reject(this.closedErr);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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);
|
|
356
401
|
return new Promise((resolve, reject) => {
|
|
357
402
|
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
358
403
|
const pending = {
|
|
359
|
-
|
|
404
|
+
handle,
|
|
360
405
|
resolve,
|
|
361
406
|
reject,
|
|
362
407
|
onProgress,
|
|
363
408
|
timer: null,
|
|
409
|
+
acceptFrame,
|
|
410
|
+
onLateResponse,
|
|
364
411
|
};
|
|
365
|
-
pending.timer = setTimeout(() =>
|
|
366
|
-
this.arbitrateTimeout(key, pending, channel, corr, ms);
|
|
367
|
-
}, ms);
|
|
412
|
+
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
|
|
368
413
|
this.pending.set(key, pending);
|
|
369
|
-
this.sock.write(encodeFrame(frame), Date.now() + ms).catch((
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
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)));
|
|
373
418
|
});
|
|
374
419
|
});
|
|
375
420
|
}
|
|
@@ -385,6 +430,8 @@ export class SubcClient {
|
|
|
385
430
|
*/
|
|
386
431
|
arbitrateTimeout(key, pending, channel, corr, ms) {
|
|
387
432
|
const settleAsTimeout = () => {
|
|
433
|
+
if (pending.onLateResponse)
|
|
434
|
+
this.lateResponses.set(key, pending.onLateResponse);
|
|
388
435
|
this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
|
|
389
436
|
};
|
|
390
437
|
const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
|
|
@@ -405,69 +452,70 @@ export class SubcClient {
|
|
|
405
452
|
};
|
|
406
453
|
setImmediate(arbitrate);
|
|
407
454
|
}
|
|
408
|
-
async managedRequest(
|
|
455
|
+
async managedRequest(handle, body, opts) {
|
|
409
456
|
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
410
457
|
const priority = opts.priority ?? Priority.Interactive;
|
|
458
|
+
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
411
459
|
try {
|
|
412
|
-
const reply = await this.sendManaged(
|
|
460
|
+
const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
|
|
413
461
|
return this.parseJson(reply);
|
|
414
462
|
}
|
|
415
|
-
catch (
|
|
416
|
-
if (
|
|
417
|
-
throw
|
|
418
|
-
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);
|
|
419
467
|
}
|
|
420
468
|
}
|
|
421
|
-
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
|
+
}
|
|
422
476
|
if (this.closedErr) {
|
|
423
477
|
return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
|
|
424
478
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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);
|
|
428
488
|
let handedToSocket = false;
|
|
429
|
-
const classifyFailure = (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// therefore OutcomeUnknown to avoid an unsafe double-mutation retry.
|
|
435
|
-
if (!handedToSocket) {
|
|
436
|
-
return this.notSentCallError("request bytes were not queued to the subc socket", err);
|
|
437
|
-
}
|
|
438
|
-
// A request-deadline timeout (arbitration expired without observing a drop)
|
|
439
|
-
// is refined from a real connection drop: the socket was NOT seen to fail, so
|
|
440
|
-
// the caller can skip a was-it-even-sent recovery path. Still outcome_unknown
|
|
441
|
-
// — queued-to-local-socket is not proof the daemon received or ran it.
|
|
442
|
-
if (err instanceof SubcError && err.code === REQUEST_DEADLINE_MARKER) {
|
|
443
|
-
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);
|
|
444
494
|
}
|
|
445
|
-
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);
|
|
446
496
|
};
|
|
447
497
|
return new Promise((resolve, reject) => {
|
|
448
498
|
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
449
499
|
const pending = {
|
|
450
|
-
|
|
500
|
+
handle,
|
|
451
501
|
resolve,
|
|
452
502
|
reject,
|
|
453
503
|
onProgress,
|
|
454
504
|
timer: null,
|
|
455
505
|
classifyFailure,
|
|
456
506
|
};
|
|
457
|
-
pending.timer = setTimeout(() =>
|
|
458
|
-
this.arbitrateTimeout(key, pending, channel, corr, ms);
|
|
459
|
-
}, ms);
|
|
507
|
+
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
|
|
460
508
|
this.pending.set(key, pending);
|
|
461
509
|
const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
|
|
462
510
|
handedToSocket = write.queued;
|
|
463
|
-
write.completed.catch((
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
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)));
|
|
467
515
|
});
|
|
468
516
|
});
|
|
469
517
|
}
|
|
470
|
-
async
|
|
518
|
+
async cachedRouteHandle(moduleId, opts) {
|
|
471
519
|
const identity = opts.identity ?? this.opts.identity;
|
|
472
520
|
if (!identity) {
|
|
473
521
|
throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
|
|
@@ -483,15 +531,13 @@ export class SubcClient {
|
|
|
483
531
|
target,
|
|
484
532
|
identity,
|
|
485
533
|
consumerIdentity,
|
|
486
|
-
|
|
487
|
-
generation: 0,
|
|
534
|
+
handle: null,
|
|
488
535
|
opening: null,
|
|
489
536
|
};
|
|
490
537
|
this.routes.set(key, cached);
|
|
491
538
|
}
|
|
492
|
-
if (cached.
|
|
493
|
-
return cached.
|
|
494
|
-
}
|
|
539
|
+
if (cached.handle && this.isLiveHandle(cached.handle))
|
|
540
|
+
return cached.handle;
|
|
495
541
|
if (!cached.opening) {
|
|
496
542
|
cached.opening = this.openCachedRoute(cached).finally(() => {
|
|
497
543
|
cached.opening = null;
|
|
@@ -509,61 +555,45 @@ export class SubcClient {
|
|
|
509
555
|
try {
|
|
510
556
|
await this.ensureConnectedForManaged();
|
|
511
557
|
}
|
|
512
|
-
catch (
|
|
513
|
-
throw this.notSentRecoveryError("route.open could not run because reconnect failed",
|
|
514
|
-
}
|
|
515
|
-
if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
|
|
516
|
-
return cached.channel;
|
|
558
|
+
catch (error) {
|
|
559
|
+
throw this.notSentRecoveryError("route.open could not run because reconnect failed", error);
|
|
517
560
|
}
|
|
561
|
+
if (cached.handle && this.isLiveHandle(cached.handle))
|
|
562
|
+
return cached.handle;
|
|
518
563
|
try {
|
|
519
|
-
const
|
|
564
|
+
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
520
565
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
521
566
|
});
|
|
522
|
-
// Generation guard: a closeRoute may have flipped the tombstone WHILE this
|
|
523
|
-
// route.open was in flight. If so, close wins — do NOT install the channel
|
|
524
|
-
// into the (already-removed) cache entry; GOODBYE the channel we just opened
|
|
525
|
-
// so the daemon/module don't leak it, and fail as RouteClosed.
|
|
526
567
|
if (cached.closed) {
|
|
527
|
-
this.
|
|
568
|
+
this.liveRoutes.delete(handle.channel);
|
|
569
|
+
this.sendRouteGoodbye(handle);
|
|
528
570
|
throw this.routeClosedDuringOpen();
|
|
529
571
|
}
|
|
530
|
-
cached.
|
|
531
|
-
|
|
532
|
-
return channel;
|
|
572
|
+
cached.handle = handle;
|
|
573
|
+
return handle;
|
|
533
574
|
}
|
|
534
|
-
catch (
|
|
535
|
-
if (
|
|
536
|
-
throw
|
|
537
|
-
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)) {
|
|
538
579
|
try {
|
|
539
|
-
await this.reconnectAfterDrop(
|
|
580
|
+
await this.reconnectAfterDrop(error);
|
|
540
581
|
}
|
|
541
|
-
catch (
|
|
542
|
-
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);
|
|
543
584
|
}
|
|
544
585
|
continue;
|
|
545
586
|
}
|
|
546
|
-
|
|
547
|
-
// reloading / momentarily absent) is retried IN-PLACE against the same live
|
|
548
|
-
// connection — never a socket reconnect, which would needlessly disrupt this
|
|
549
|
-
// connection's other routes — until the route-retry deadline. Past the
|
|
550
|
-
// deadline it surfaces as not_sent: provably pre-send (no data frame ever
|
|
551
|
-
// left the client) AND still transient, so the caller's own retry policy may
|
|
552
|
-
// safely re-attempt later. Reason and set kept in parity with subc-client-rs.
|
|
553
|
-
if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
|
|
587
|
+
if (!this.closeStarted && error instanceof SubcError && isRetryableRouteOpenCode(error.code)) {
|
|
554
588
|
routeRetryAttempt += 1;
|
|
555
589
|
if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
|
|
556
590
|
await this.opts.sleep(routeRetryDelay);
|
|
557
591
|
routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
|
|
558
592
|
continue;
|
|
559
593
|
}
|
|
560
|
-
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);
|
|
561
595
|
}
|
|
562
|
-
|
|
563
|
-
// unknown_target, ...) is pre-send but would never succeed on retry, so it
|
|
564
|
-
// stays terminal — a not_sent class here would invite a retry storm against a
|
|
565
|
-
// request the daemon will always reject.
|
|
566
|
-
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
|
|
596
|
+
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error);
|
|
567
597
|
}
|
|
568
598
|
}
|
|
569
599
|
}
|
|
@@ -638,33 +668,27 @@ export class SubcClient {
|
|
|
638
668
|
this.currentConn = opened.conn;
|
|
639
669
|
this.closedErr = null;
|
|
640
670
|
this.generation += 1;
|
|
671
|
+
this.connectionToken = newConnectionToken();
|
|
672
|
+
this.liveRoutes.clear();
|
|
673
|
+
this.lateResponses.clear();
|
|
674
|
+
this.nextCorr = 1n;
|
|
641
675
|
void this.readLoop(opened.sock, this.generation);
|
|
642
676
|
}
|
|
643
677
|
async reopenCachedRoutes() {
|
|
644
|
-
for (const cached of this.routes.values())
|
|
645
|
-
cached.
|
|
646
|
-
cached.generation = 0;
|
|
647
|
-
}
|
|
678
|
+
for (const cached of this.routes.values())
|
|
679
|
+
cached.handle = null;
|
|
648
680
|
for (const cached of this.routes.values()) {
|
|
649
681
|
if (cached.closed)
|
|
650
|
-
continue;
|
|
651
|
-
|
|
652
|
-
// lazy per-call path (openCachedRoute) does. Dropping it here would make a
|
|
653
|
-
// route reopened after a reconnect send route.open with no consumer_identity,
|
|
654
|
-
// so the daemon would re-stamp it with a different (weaker) principal than the
|
|
655
|
-
// one it was originally bound under — a silent post-reconnect trust downgrade.
|
|
656
|
-
const channel = await this.routeOpen(cached.target, cached.identity, {
|
|
682
|
+
continue;
|
|
683
|
+
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
657
684
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
658
685
|
});
|
|
659
|
-
// A closeRoute may have raced this reopen (flipping the tombstone during the
|
|
660
|
-
// route.open await). If so, GOODBYE the channel instead of installing it, so the
|
|
661
|
-
// closed route isn't silently re-established on the new connection.
|
|
662
686
|
if (cached.closed) {
|
|
663
|
-
this.
|
|
687
|
+
this.liveRoutes.delete(handle.channel);
|
|
688
|
+
this.sendRouteGoodbye(handle);
|
|
664
689
|
continue;
|
|
665
690
|
}
|
|
666
|
-
cached.
|
|
667
|
-
cached.generation = this.generation;
|
|
691
|
+
cached.handle = handle;
|
|
668
692
|
}
|
|
669
693
|
}
|
|
670
694
|
// A request timeout carries the local socket port and (channel, corr) so a
|
|
@@ -682,41 +706,39 @@ export class SubcClient {
|
|
|
682
706
|
async readLoop(sock, generation) {
|
|
683
707
|
try {
|
|
684
708
|
for (;;) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// A frame is now arriving. Mark the reader active THROUGH dispatch so a
|
|
690
|
-
// timeout that fires mid-arrival grants the reply its bounded grace window
|
|
691
|
-
// instead of settling as a spurious timeout.
|
|
692
|
-
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
|
+
});
|
|
693
713
|
try {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
? new Uint8Array(0)
|
|
697
|
-
: await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
|
|
698
|
-
// Drop a frame read off a socket this client has already replaced
|
|
699
|
-
// (reconnect): its pendings were settled by fail(), and dispatching it
|
|
700
|
-
// against the current pending map could match a re-used (channel, corr).
|
|
701
|
-
if (this.sock === sock && this.generation === generation) {
|
|
702
|
-
this.dispatch({ header, body });
|
|
703
|
-
}
|
|
714
|
+
if (this.sock === sock && this.generation === generation)
|
|
715
|
+
this.dispatch(frame);
|
|
704
716
|
}
|
|
705
717
|
finally {
|
|
706
718
|
this.readerActive = false;
|
|
707
719
|
}
|
|
708
720
|
}
|
|
709
721
|
}
|
|
710
|
-
catch (
|
|
722
|
+
catch (error) {
|
|
711
723
|
if (this.sock === sock && this.generation === generation) {
|
|
712
|
-
this.fail(
|
|
724
|
+
this.fail(error instanceof Error ? error : new SubcError(String(error)));
|
|
713
725
|
}
|
|
714
726
|
}
|
|
715
727
|
}
|
|
716
728
|
dispatch(frame) {
|
|
717
|
-
|
|
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);
|
|
718
738
|
const pending = this.pending.get(key);
|
|
719
739
|
if (pending) {
|
|
740
|
+
if (pending.acceptFrame && !pending.acceptFrame(frame))
|
|
741
|
+
return;
|
|
720
742
|
switch (frame.header.ty) {
|
|
721
743
|
case FrameType.Push:
|
|
722
744
|
case FrameType.StreamData:
|
|
@@ -733,24 +755,24 @@ export class SubcClient {
|
|
|
733
755
|
return;
|
|
734
756
|
}
|
|
735
757
|
}
|
|
736
|
-
|
|
737
|
-
|
|
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);
|
|
738
769
|
return;
|
|
739
770
|
}
|
|
740
|
-
// A terminal frame (Response/Error/StreamEnd) with no waiter is almost always
|
|
741
|
-
// a reply that arrived AFTER its request already settled — the fingerprint of a
|
|
742
|
-
// premature timeout under event-loop starvation (the reply raced the deadline
|
|
743
|
-
// and lost). Metadata-only debug log (never the body) so every future
|
|
744
|
-
// occurrence is a one-line diagnosis instead of an invisible drop. Enable with
|
|
745
|
-
// NODE_DEBUG=subc-client.
|
|
746
771
|
if (frame.header.ty === FrameType.Response ||
|
|
747
772
|
frame.header.ty === FrameType.Error ||
|
|
748
773
|
frame.header.ty === FrameType.StreamEnd) {
|
|
749
|
-
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() ?? "?");
|
|
750
|
-
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() ?? "?");
|
|
751
775
|
}
|
|
752
|
-
// Unmatched Push or stray frame: no registered waiter. Drop it — v1 has no
|
|
753
|
-
// unsolicited-push consumers.
|
|
754
776
|
}
|
|
755
777
|
/**
|
|
756
778
|
* Settle a pending exactly once. The object-identity guard (the map still
|
|
@@ -783,11 +805,16 @@ export class SubcClient {
|
|
|
783
805
|
return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
784
806
|
}
|
|
785
807
|
}
|
|
786
|
-
|
|
808
|
+
evictRouteHandle(handle) {
|
|
809
|
+
for (const cached of this.routes.values()) {
|
|
810
|
+
if (cached.handle && sameRouteHandle(cached.handle, handle))
|
|
811
|
+
cached.handle = null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
failHandle(handle, error) {
|
|
787
815
|
for (const [key, pending] of this.pending) {
|
|
788
|
-
if (pending.
|
|
789
|
-
this.rejectPending(key, pending,
|
|
790
|
-
}
|
|
816
|
+
if (pending.handle && sameRouteHandle(pending.handle, handle))
|
|
817
|
+
this.rejectPending(key, pending, error);
|
|
791
818
|
}
|
|
792
819
|
}
|
|
793
820
|
fail(err) {
|
|
@@ -815,6 +842,45 @@ export class SubcClient {
|
|
|
815
842
|
return this.notSentCallError(message, cause);
|
|
816
843
|
return this.terminalCallError(message, cause);
|
|
817
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
|
+
}
|
|
818
884
|
encode(value) {
|
|
819
885
|
return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
|
|
820
886
|
}
|
|
@@ -911,4 +977,7 @@ function causeMessage(cause) {
|
|
|
911
977
|
return "";
|
|
912
978
|
return `: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
913
979
|
}
|
|
980
|
+
function pendingKey(handle, corr) {
|
|
981
|
+
return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
|
|
982
|
+
}
|
|
914
983
|
//# sourceMappingURL=client.js.map
|