@forgecart/sdk 1.202610241216.0 → 1.202610241437.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.generated.d.ts +36 -13
- package/dist/client.generated.js +91 -41
- package/package.json +1 -1
- package/src/client.generated.ts +102 -51
|
@@ -173,6 +173,11 @@ export declare class BaseGraphQLClient {
|
|
|
173
173
|
* sink callback, never the operation's lifetime, so a connected stream runs
|
|
174
174
|
* for as long as the server streams it. Both bounds raise the same typed
|
|
175
175
|
* code, so one consumer branch answers for both.
|
|
176
|
+
*
|
|
177
|
+
* Disposing the client ends the operation with WS_CLIENT_DISPOSED while it
|
|
178
|
+
* still waits on a handshake (#2102) or, as a query or a mutation, on its
|
|
179
|
+
* answer (#1572): graphql-ws would leave the first waiting forever and
|
|
180
|
+
* COMPLETE the second with no value — see settlePendingOperations.
|
|
176
181
|
*/
|
|
177
182
|
private executeWsOperation;
|
|
178
183
|
/**
|
|
@@ -212,6 +217,16 @@ export declare class BaseGraphQLClient {
|
|
|
212
217
|
* is specific to the bound that raised it, such as #2102's endpoint.
|
|
213
218
|
*/
|
|
214
219
|
private wsTimeoutError;
|
|
220
|
+
/**
|
|
221
|
+
* The ONE error a disposed client raises (#1994, #2102, #1572): for an
|
|
222
|
+
* operation started after dispose(), for one still waiting on a handshake,
|
|
223
|
+
* and for a query or a mutation still waiting on its answer. Named, so a
|
|
224
|
+
* consumer's error path tells an operation its own client cut off from one
|
|
225
|
+
* the server failed; carrying the endpoint, so a late one stays attributable.
|
|
226
|
+
* Never routed through the error handler: the disposal is the caller's own
|
|
227
|
+
* act, not a fault worth a notification.
|
|
228
|
+
*/
|
|
229
|
+
private clientDisposedError;
|
|
215
230
|
/**
|
|
216
231
|
* Uniform error contract for the sink-level error path. graphql-ws
|
|
217
232
|
* delivers a server ErrorMessage payload as a raw GraphQLError[] —
|
|
@@ -252,27 +267,35 @@ export declare class BaseGraphQLClient {
|
|
|
252
267
|
private settleWsDisposal;
|
|
253
268
|
private describeFailure;
|
|
254
269
|
/**
|
|
255
|
-
* Ends
|
|
256
|
-
*
|
|
257
|
-
* of operations its socket CARRIED
|
|
258
|
-
* its disposal says nothing to them and they wait
|
|
259
|
-
* will never connect again — and the caller
|
|
260
|
-
* (measured from the far end as backends
|
|
261
|
-
* the client had already abandoned).
|
|
262
|
-
*
|
|
263
|
-
*
|
|
270
|
+
* Ends, where its owner stands, every operation graphql-ws's disposal would
|
|
271
|
+
* leave without an ending it can use, with WS_CLIENT_DISPOSED. graphql-ws
|
|
272
|
+
* speaks to the sinks of operations its socket CARRIED. An unanswered
|
|
273
|
+
* handshake carried none, so its disposal says nothing to them and they wait
|
|
274
|
+
* forever on a client that will never connect again — and the caller
|
|
275
|
+
* awaiting one waits with it (#2102; measured from the far end as backends
|
|
276
|
+
* left idle in transaction on a wait the client had already abandoned). A
|
|
277
|
+
* query or a mutation it carried unanswered it COMPLETES with no value,
|
|
278
|
+
* which no consumer can tell from a request never made (#1572). Erroring
|
|
279
|
+
* from the BOUND is the thing this must not do: that fires 10 s later, in
|
|
280
|
+
* whatever context is alive then. dispose() is the owner's own stack, so a
|
|
281
|
+
* rejection lands there. A subscription past its handshake is not here: a
|
|
282
|
+
* stream its client ended completes truthfully, and an operation whose
|
|
283
|
+
* answer arrived left the registry with it.
|
|
264
284
|
*
|
|
265
285
|
* The socket's owner ends every operation on it, forks included — the same
|
|
266
286
|
* reason the socket rather than the client carries the disposed flag. A fork
|
|
267
287
|
* owns no socket and ends only what it started, leaving its owner on the
|
|
268
288
|
* wire. Idempotent: an entry settles once and deregisters itself doing so.
|
|
269
289
|
*/
|
|
270
|
-
private
|
|
290
|
+
private settlePendingOperations;
|
|
271
291
|
/**
|
|
272
292
|
* Terminal (#1994). Ends the socket, ends any retry in flight, ends every
|
|
273
|
-
* operation still waiting on a handshake (#2102)
|
|
274
|
-
*
|
|
275
|
-
*
|
|
293
|
+
* operation still waiting on a handshake (#2102) and every query or
|
|
294
|
+
* mutation still waiting on its answer (#1572) with WS_CLIENT_DISPOSED, and
|
|
295
|
+
* refuses every later operation with the same error — a disposed client
|
|
296
|
+
* never mints another socket. An answer that already arrived is kept, and a
|
|
297
|
+
* subscription the socket carried completes. Idempotent and synchronous;
|
|
298
|
+
* never throws.
|
|
276
299
|
*/
|
|
277
300
|
dispose(): void;
|
|
278
301
|
fork(transactionToken: string): BaseGraphQLClient;
|
package/dist/client.generated.js
CHANGED
|
@@ -116,8 +116,9 @@ const WS_FIRST_VALUE_BUDGET_MS_BY_OPERATION = {
|
|
|
116
116
|
// `disposed` is that socket's terminal end, and it lives here rather than
|
|
117
117
|
// on the client because every client still holding the socket — the forks
|
|
118
118
|
// included — is off the wire the moment its owner ends it. `pending` is
|
|
119
|
-
// socket-level for exactly that reason too (#2102):
|
|
120
|
-
// the socket, so ending the socket must end
|
|
119
|
+
// socket-level for exactly that reason too (#2102, #1572): an operation
|
|
120
|
+
// waiting on the wire belongs to the socket, so ending the socket must end
|
|
121
|
+
// everything still waiting on it.
|
|
121
122
|
const WS_CONNECTION_STATE = new WeakMap();
|
|
122
123
|
class BaseGraphQLClient {
|
|
123
124
|
baseHeaders;
|
|
@@ -170,7 +171,7 @@ class BaseGraphQLClient {
|
|
|
170
171
|
if (this.disposed) {
|
|
171
172
|
// A fresh socket here would start the retry ladder its owner just ended;
|
|
172
173
|
// the operation fails fast and typed instead, on the caller that asked.
|
|
173
|
-
throw
|
|
174
|
+
throw this.clientDisposedError();
|
|
174
175
|
}
|
|
175
176
|
if (this.wsClient && this.wsClientStale) {
|
|
176
177
|
this.disposeWsClient();
|
|
@@ -382,6 +383,11 @@ class BaseGraphQLClient {
|
|
|
382
383
|
* sink callback, never the operation's lifetime, so a connected stream runs
|
|
383
384
|
* for as long as the server streams it. Both bounds raise the same typed
|
|
384
385
|
* code, so one consumer branch answers for both.
|
|
386
|
+
*
|
|
387
|
+
* Disposing the client ends the operation with WS_CLIENT_DISPOSED while it
|
|
388
|
+
* still waits on a handshake (#2102) or, as a query or a mutation, on its
|
|
389
|
+
* answer (#1572): graphql-ws would leave the first waiting forever and
|
|
390
|
+
* COMPLETE the second with no value — see settlePendingOperations.
|
|
385
391
|
*/
|
|
386
392
|
executeWsOperation(document, variables, kind) {
|
|
387
393
|
return new QueryObservable((subscriber) => {
|
|
@@ -390,6 +396,10 @@ class BaseGraphQLClient {
|
|
|
390
396
|
// that replaced the boolean this method used to take.
|
|
391
397
|
const singleDelivery = kind === WsOperationKind.MUTATION;
|
|
392
398
|
const firstValueBudgetMs = this.getFirstValueBudgetMs(kind, this.operationNameOf(document));
|
|
399
|
+
// A query or a mutation is a REQUEST: its completion means something only
|
|
400
|
+
// once an answer arrived. A subscription is a stream its client may end at
|
|
401
|
+
// any point, so a disposal that completes it reports the truth (#1572).
|
|
402
|
+
const awaitsAnswer = kind !== WsOperationKind.SUBSCRIPTION;
|
|
393
403
|
let disposed = false;
|
|
394
404
|
// Delivery latch: once ANY sink callback has fired, the wire has
|
|
395
405
|
// spoken and a socket close can no longer mean "delivery unknown".
|
|
@@ -407,8 +417,10 @@ class BaseGraphQLClient {
|
|
|
407
417
|
// object and a browser a number, and the generated client must typecheck
|
|
408
418
|
// in both without an @types/node-only name.
|
|
409
419
|
let operationDeadline;
|
|
410
|
-
// This operation's entry in the socket's pending
|
|
411
|
-
//
|
|
420
|
+
// This operation's entry in the socket's pending registry, present for
|
|
421
|
+
// exactly as long as dispose() would otherwise leave it without an ending:
|
|
422
|
+
// while it waits on a handshake (#2102) and, for a request, until the
|
|
423
|
+
// wire answers it (#1572).
|
|
412
424
|
let pendingEntry;
|
|
413
425
|
const offConnectionEvents = [];
|
|
414
426
|
// Disarms the first-value deadline. Reached on the first sink callback
|
|
@@ -419,27 +431,33 @@ class BaseGraphQLClient {
|
|
|
419
431
|
clearTimeout(operationDeadline);
|
|
420
432
|
operationDeadline = undefined;
|
|
421
433
|
};
|
|
422
|
-
// Disarms the bound
|
|
423
|
-
//
|
|
424
|
-
// longer has to settle. Reached on the ACK, on the delivery latch and on
|
|
425
|
-
// teardown — every way this operation stops waiting on a handshake.
|
|
434
|
+
// Disarms the handshake bound. Reached on the ACK, on the delivery latch
|
|
435
|
+
// and on teardown — every way this operation stops waiting on a handshake.
|
|
426
436
|
const clearConnectionTimeout = () => {
|
|
427
|
-
if (pendingEntry !== undefined) {
|
|
428
|
-
WS_CONNECTION_STATE.get(client)?.pending.delete(pendingEntry);
|
|
429
|
-
pendingEntry = undefined;
|
|
430
|
-
}
|
|
431
437
|
if (connectionTimer === undefined)
|
|
432
438
|
return;
|
|
433
439
|
clearTimeout(connectionTimer);
|
|
434
440
|
connectionTimer = undefined;
|
|
435
441
|
};
|
|
442
|
+
// Deregisters the operation: dispose() no longer has to end it. Reached on
|
|
443
|
+
// the delivery latch and on teardown and, for a subscription, which waits
|
|
444
|
+
// on nothing past its handshake, on the ACK. A request stays registered
|
|
445
|
+
// across the ACK — the handshake is over, its answer is still owed.
|
|
446
|
+
const leavePending = () => {
|
|
447
|
+
if (pendingEntry === undefined)
|
|
448
|
+
return;
|
|
449
|
+
WS_CONNECTION_STATE.get(client)?.pending.delete(pendingEntry);
|
|
450
|
+
pendingEntry = undefined;
|
|
451
|
+
};
|
|
436
452
|
// The wire answering IS proof the handshake completed AND the first value
|
|
437
|
-
// arrived, so the latch
|
|
438
|
-
// outlived its operation would hold the event loop open
|
|
453
|
+
// arrived, so the latch, BOTH bounds and the registration fall together —
|
|
454
|
+
// and a timer that outlived its operation would hold the event loop open
|
|
455
|
+
// (a hung worker).
|
|
439
456
|
const settle = () => {
|
|
440
457
|
settled = true;
|
|
441
458
|
clearOperationDeadline();
|
|
442
459
|
clearConnectionTimeout();
|
|
460
|
+
leavePending();
|
|
443
461
|
};
|
|
444
462
|
// Armed BEFORE the operation is sent, so the budget measures the caller's
|
|
445
463
|
// own wait rather than a window that opens once graphql-ws gets round to
|
|
@@ -527,7 +545,11 @@ class BaseGraphQLClient {
|
|
|
527
545
|
// disposed and shouldRetry is untouched: graphql-ws keeps healing for the
|
|
528
546
|
// next operation; only THIS one gives up (#2102).
|
|
529
547
|
if (!(WS_CONNECTION_STATE.get(client)?.connected ?? false)) {
|
|
530
|
-
offConnectionEvents.push(client.on("connected", () => {
|
|
548
|
+
offConnectionEvents.push(client.on("connected", () => {
|
|
549
|
+
clearConnectionTimeout();
|
|
550
|
+
if (!awaitsAnswer)
|
|
551
|
+
leavePending();
|
|
552
|
+
}));
|
|
531
553
|
connectionTimer = setTimeout(() => {
|
|
532
554
|
connectionTimer = undefined;
|
|
533
555
|
// A residual guard, no longer the settlement: dispose() has already
|
|
@@ -571,23 +593,29 @@ class BaseGraphQLClient {
|
|
|
571
593
|
const timerHandle = connectionTimer;
|
|
572
594
|
if (typeof timerHandle === "object" && typeof timerHandle.unref === "function")
|
|
573
595
|
timerHandle.unref();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
596
|
+
}
|
|
597
|
+
// Reporting nothing when the bound elapses on a dead connection leaves
|
|
598
|
+
// something else to end this operation when its client ends — and
|
|
599
|
+
// graphql-ws is not it. Its dispose() speaks to the sinks of operations
|
|
600
|
+
// its socket CARRIED, and a handshake that never completed carried none,
|
|
601
|
+
// so that one would wait forever and whoever awaited it with it (#2102).
|
|
602
|
+
// A request it did carry hears a bare COMPLETE — no value and no error —
|
|
603
|
+
// so map never runs, catchError never fires, an await reports an
|
|
604
|
+
// anonymous EmptyError, and the request is indistinguishable from one
|
|
605
|
+
// never made (#1572: the sidebar of #1571 kept the previous tenant's
|
|
606
|
+
// entries across a channel switch that way). Registered while either can
|
|
607
|
+
// still happen, and settled by settlePendingOperations on the disposing
|
|
608
|
+
// owner's own stack, with the error a later operation already receives.
|
|
609
|
+
if (awaitsAnswer || connectionTimer !== undefined) {
|
|
581
610
|
pendingEntry = {
|
|
582
611
|
owner: this,
|
|
583
612
|
abandon: () => {
|
|
584
613
|
if (disposed || settled)
|
|
585
614
|
return;
|
|
586
615
|
// The same disarm a wire answer performs: the latch closes and the
|
|
587
|
-
//
|
|
616
|
+
// bounds are cleared, so nothing speaks twice for this operation.
|
|
588
617
|
settle();
|
|
589
|
-
|
|
590
|
-
subscriber.error(new ClientError({ errors: [{ message, extensions: { endpoint: this.endpoint } }] }, message));
|
|
618
|
+
subscriber.error(this.clientDisposedError());
|
|
591
619
|
},
|
|
592
620
|
};
|
|
593
621
|
WS_CONNECTION_STATE.get(client)?.pending.add(pendingEntry);
|
|
@@ -609,6 +637,7 @@ class BaseGraphQLClient {
|
|
|
609
637
|
disposed = true;
|
|
610
638
|
clearOperationDeadline();
|
|
611
639
|
clearConnectionTimeout();
|
|
640
|
+
leavePending();
|
|
612
641
|
for (const off of offConnectionEvents)
|
|
613
642
|
off();
|
|
614
643
|
cleanup();
|
|
@@ -691,6 +720,19 @@ class BaseGraphQLClient {
|
|
|
691
720
|
};
|
|
692
721
|
return new ClientError({ errors: [{ message, extensions }] }, message);
|
|
693
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* The ONE error a disposed client raises (#1994, #2102, #1572): for an
|
|
725
|
+
* operation started after dispose(), for one still waiting on a handshake,
|
|
726
|
+
* and for a query or a mutation still waiting on its answer. Named, so a
|
|
727
|
+
* consumer's error path tells an operation its own client cut off from one
|
|
728
|
+
* the server failed; carrying the endpoint, so a late one stays attributable.
|
|
729
|
+
* Never routed through the error handler: the disposal is the caller's own
|
|
730
|
+
* act, not a fault worth a notification.
|
|
731
|
+
*/
|
|
732
|
+
clientDisposedError() {
|
|
733
|
+
const message = "WS_CLIENT_DISPOSED";
|
|
734
|
+
return new ClientError({ errors: [{ message, extensions: { endpoint: this.endpoint } }] }, message);
|
|
735
|
+
}
|
|
694
736
|
/**
|
|
695
737
|
* Uniform error contract for the sink-level error path. graphql-ws
|
|
696
738
|
* delivers a server ErrorMessage payload as a raw GraphQLError[] —
|
|
@@ -857,22 +899,27 @@ class BaseGraphQLClient {
|
|
|
857
899
|
return String(value);
|
|
858
900
|
}
|
|
859
901
|
/**
|
|
860
|
-
* Ends
|
|
861
|
-
*
|
|
862
|
-
* of operations its socket CARRIED
|
|
863
|
-
* its disposal says nothing to them and they wait
|
|
864
|
-
* will never connect again — and the caller
|
|
865
|
-
* (measured from the far end as backends
|
|
866
|
-
* the client had already abandoned).
|
|
867
|
-
*
|
|
868
|
-
*
|
|
902
|
+
* Ends, where its owner stands, every operation graphql-ws's disposal would
|
|
903
|
+
* leave without an ending it can use, with WS_CLIENT_DISPOSED. graphql-ws
|
|
904
|
+
* speaks to the sinks of operations its socket CARRIED. An unanswered
|
|
905
|
+
* handshake carried none, so its disposal says nothing to them and they wait
|
|
906
|
+
* forever on a client that will never connect again — and the caller
|
|
907
|
+
* awaiting one waits with it (#2102; measured from the far end as backends
|
|
908
|
+
* left idle in transaction on a wait the client had already abandoned). A
|
|
909
|
+
* query or a mutation it carried unanswered it COMPLETES with no value,
|
|
910
|
+
* which no consumer can tell from a request never made (#1572). Erroring
|
|
911
|
+
* from the BOUND is the thing this must not do: that fires 10 s later, in
|
|
912
|
+
* whatever context is alive then. dispose() is the owner's own stack, so a
|
|
913
|
+
* rejection lands there. A subscription past its handshake is not here: a
|
|
914
|
+
* stream its client ended completes truthfully, and an operation whose
|
|
915
|
+
* answer arrived left the registry with it.
|
|
869
916
|
*
|
|
870
917
|
* The socket's owner ends every operation on it, forks included — the same
|
|
871
918
|
* reason the socket rather than the client carries the disposed flag. A fork
|
|
872
919
|
* owns no socket and ends only what it started, leaving its owner on the
|
|
873
920
|
* wire. Idempotent: an entry settles once and deregisters itself doing so.
|
|
874
921
|
*/
|
|
875
|
-
|
|
922
|
+
settlePendingOperations() {
|
|
876
923
|
const wsClient = this.wsClient;
|
|
877
924
|
if (!wsClient)
|
|
878
925
|
return;
|
|
@@ -888,15 +935,18 @@ class BaseGraphQLClient {
|
|
|
888
935
|
}
|
|
889
936
|
/**
|
|
890
937
|
* Terminal (#1994). Ends the socket, ends any retry in flight, ends every
|
|
891
|
-
* operation still waiting on a handshake (#2102)
|
|
892
|
-
*
|
|
893
|
-
*
|
|
938
|
+
* operation still waiting on a handshake (#2102) and every query or
|
|
939
|
+
* mutation still waiting on its answer (#1572) with WS_CLIENT_DISPOSED, and
|
|
940
|
+
* refuses every later operation with the same error — a disposed client
|
|
941
|
+
* never mints another socket. An answer that already arrived is kept, and a
|
|
942
|
+
* subscription the socket carried completes. Idempotent and synchronous;
|
|
943
|
+
* never throws.
|
|
894
944
|
*/
|
|
895
945
|
dispose() {
|
|
896
946
|
this.disposed = true;
|
|
897
947
|
// Before the socket goes: erroring a subscriber tears its graphql-ws
|
|
898
|
-
// subscription down, so the disposal that follows finds
|
|
899
|
-
this.
|
|
948
|
+
// subscription down, so the disposal that follows finds none of them.
|
|
949
|
+
this.settlePendingOperations();
|
|
900
950
|
this.disposeWsClient();
|
|
901
951
|
}
|
|
902
952
|
fork(transactionToken) {
|
package/package.json
CHANGED
package/src/client.generated.ts
CHANGED
|
@@ -236,11 +236,14 @@ const WS_FIRST_VALUE_BUDGET_MS_BY_OPERATION: Readonly<Record<string, number>> =
|
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* One operation still waiting on
|
|
240
|
-
*
|
|
241
|
-
*
|
|
239
|
+
* One operation still waiting on the wire, which graphql-ws's own disposal
|
|
240
|
+
* would leave without an ending it can use: one waiting on a handshake its
|
|
241
|
+
* socket has not answered (#2102), or a query or a mutation waiting on its
|
|
242
|
+
* answer (#1572). `owner` is the client that started it: the socket's owner
|
|
243
|
+
* ends every pending operation on that socket, a fork() only the ones it
|
|
244
|
+
* started.
|
|
242
245
|
*/
|
|
243
|
-
interface
|
|
246
|
+
interface PendingOperation {
|
|
244
247
|
owner: BaseGraphQLClient;
|
|
245
248
|
abandon: () => void;
|
|
246
249
|
}
|
|
@@ -252,9 +255,10 @@ interface PendingHandshake {
|
|
|
252
255
|
// `disposed` is that socket's terminal end, and it lives here rather than
|
|
253
256
|
// on the client because every client still holding the socket — the forks
|
|
254
257
|
// included — is off the wire the moment its owner ends it. `pending` is
|
|
255
|
-
// socket-level for exactly that reason too (#2102):
|
|
256
|
-
// the socket, so ending the socket must end
|
|
257
|
-
|
|
258
|
+
// socket-level for exactly that reason too (#2102, #1572): an operation
|
|
259
|
+
// waiting on the wire belongs to the socket, so ending the socket must end
|
|
260
|
+
// everything still waiting on it.
|
|
261
|
+
const WS_CONNECTION_STATE = new WeakMap<WsClient, { connected: boolean; disposed: boolean; pending: Set<PendingOperation> }>();
|
|
258
262
|
|
|
259
263
|
export class BaseGraphQLClient {
|
|
260
264
|
private readonly logger = new SDKLogger();
|
|
@@ -310,10 +314,7 @@ export class BaseGraphQLClient {
|
|
|
310
314
|
if (this.disposed) {
|
|
311
315
|
// A fresh socket here would start the retry ladder its owner just ended;
|
|
312
316
|
// the operation fails fast and typed instead, on the caller that asked.
|
|
313
|
-
throw
|
|
314
|
-
{ errors: [{ message: "WS_CLIENT_DISPOSED", extensions: { endpoint: this.endpoint } }] },
|
|
315
|
-
"WS_CLIENT_DISPOSED",
|
|
316
|
-
);
|
|
317
|
+
throw this.clientDisposedError();
|
|
317
318
|
}
|
|
318
319
|
if (this.wsClient && this.wsClientStale) {
|
|
319
320
|
this.disposeWsClient();
|
|
@@ -351,7 +352,7 @@ export class BaseGraphQLClient {
|
|
|
351
352
|
// a dead endpoint for 32-46 s and erroring onto whoever runs then.
|
|
352
353
|
shouldRetry: () => !this.disposed,
|
|
353
354
|
});
|
|
354
|
-
const connectionState = { connected: false, disposed: false, pending: new Set<
|
|
355
|
+
const connectionState = { connected: false, disposed: false, pending: new Set<PendingOperation>() };
|
|
355
356
|
WS_CONNECTION_STATE.set(this.wsClient, connectionState);
|
|
356
357
|
this.wsClient.on("connected", () => {
|
|
357
358
|
connectionState.connected = true;
|
|
@@ -538,6 +539,11 @@ export class BaseGraphQLClient {
|
|
|
538
539
|
* sink callback, never the operation's lifetime, so a connected stream runs
|
|
539
540
|
* for as long as the server streams it. Both bounds raise the same typed
|
|
540
541
|
* code, so one consumer branch answers for both.
|
|
542
|
+
*
|
|
543
|
+
* Disposing the client ends the operation with WS_CLIENT_DISPOSED while it
|
|
544
|
+
* still waits on a handshake (#2102) or, as a query or a mutation, on its
|
|
545
|
+
* answer (#1572): graphql-ws would leave the first waiting forever and
|
|
546
|
+
* COMPLETE the second with no value — see settlePendingOperations.
|
|
541
547
|
*/
|
|
542
548
|
private executeWsOperation<T>(
|
|
543
549
|
document: string,
|
|
@@ -550,6 +556,10 @@ export class BaseGraphQLClient {
|
|
|
550
556
|
// that replaced the boolean this method used to take.
|
|
551
557
|
const singleDelivery = kind === WsOperationKind.MUTATION;
|
|
552
558
|
const firstValueBudgetMs = this.getFirstValueBudgetMs(kind, this.operationNameOf(document));
|
|
559
|
+
// A query or a mutation is a REQUEST: its completion means something only
|
|
560
|
+
// once an answer arrived. A subscription is a stream its client may end at
|
|
561
|
+
// any point, so a disposal that completes it reports the truth (#1572).
|
|
562
|
+
const awaitsAnswer = kind !== WsOperationKind.SUBSCRIPTION;
|
|
553
563
|
let disposed = false;
|
|
554
564
|
// Delivery latch: once ANY sink callback has fired, the wire has
|
|
555
565
|
// spoken and a socket close can no longer mean "delivery unknown".
|
|
@@ -567,9 +577,11 @@ export class BaseGraphQLClient {
|
|
|
567
577
|
// object and a browser a number, and the generated client must typecheck
|
|
568
578
|
// in both without an @types/node-only name.
|
|
569
579
|
let operationDeadline: ReturnType<typeof setTimeout> | undefined;
|
|
570
|
-
// This operation's entry in the socket's pending
|
|
571
|
-
//
|
|
572
|
-
|
|
580
|
+
// This operation's entry in the socket's pending registry, present for
|
|
581
|
+
// exactly as long as dispose() would otherwise leave it without an ending:
|
|
582
|
+
// while it waits on a handshake (#2102) and, for a request, until the
|
|
583
|
+
// wire answers it (#1572).
|
|
584
|
+
let pendingEntry: PendingOperation | undefined;
|
|
573
585
|
const offConnectionEvents: (() => void)[] = [];
|
|
574
586
|
|
|
575
587
|
// Disarms the first-value deadline. Reached on the first sink callback
|
|
@@ -580,27 +592,33 @@ export class BaseGraphQLClient {
|
|
|
580
592
|
operationDeadline = undefined;
|
|
581
593
|
};
|
|
582
594
|
|
|
583
|
-
// Disarms the bound
|
|
584
|
-
//
|
|
585
|
-
// longer has to settle. Reached on the ACK, on the delivery latch and on
|
|
586
|
-
// teardown — every way this operation stops waiting on a handshake.
|
|
595
|
+
// Disarms the handshake bound. Reached on the ACK, on the delivery latch
|
|
596
|
+
// and on teardown — every way this operation stops waiting on a handshake.
|
|
587
597
|
const clearConnectionTimeout = (): void => {
|
|
588
|
-
if (pendingEntry !== undefined) {
|
|
589
|
-
WS_CONNECTION_STATE.get(client)?.pending.delete(pendingEntry);
|
|
590
|
-
pendingEntry = undefined;
|
|
591
|
-
}
|
|
592
598
|
if (connectionTimer === undefined) return;
|
|
593
599
|
clearTimeout(connectionTimer);
|
|
594
600
|
connectionTimer = undefined;
|
|
595
601
|
};
|
|
596
602
|
|
|
603
|
+
// Deregisters the operation: dispose() no longer has to end it. Reached on
|
|
604
|
+
// the delivery latch and on teardown and, for a subscription, which waits
|
|
605
|
+
// on nothing past its handshake, on the ACK. A request stays registered
|
|
606
|
+
// across the ACK — the handshake is over, its answer is still owed.
|
|
607
|
+
const leavePending = (): void => {
|
|
608
|
+
if (pendingEntry === undefined) return;
|
|
609
|
+
WS_CONNECTION_STATE.get(client)?.pending.delete(pendingEntry);
|
|
610
|
+
pendingEntry = undefined;
|
|
611
|
+
};
|
|
612
|
+
|
|
597
613
|
// The wire answering IS proof the handshake completed AND the first value
|
|
598
|
-
// arrived, so the latch
|
|
599
|
-
// outlived its operation would hold the event loop open
|
|
614
|
+
// arrived, so the latch, BOTH bounds and the registration fall together —
|
|
615
|
+
// and a timer that outlived its operation would hold the event loop open
|
|
616
|
+
// (a hung worker).
|
|
600
617
|
const settle = (): void => {
|
|
601
618
|
settled = true;
|
|
602
619
|
clearOperationDeadline();
|
|
603
620
|
clearConnectionTimeout();
|
|
621
|
+
leavePending();
|
|
604
622
|
};
|
|
605
623
|
|
|
606
624
|
// Armed BEFORE the operation is sent, so the budget measures the caller's
|
|
@@ -688,7 +706,10 @@ export class BaseGraphQLClient {
|
|
|
688
706
|
// disposed and shouldRetry is untouched: graphql-ws keeps healing for the
|
|
689
707
|
// next operation; only THIS one gives up (#2102).
|
|
690
708
|
if (!(WS_CONNECTION_STATE.get(client)?.connected ?? false)) {
|
|
691
|
-
offConnectionEvents.push(client.on("connected", () => {
|
|
709
|
+
offConnectionEvents.push(client.on("connected", () => {
|
|
710
|
+
clearConnectionTimeout();
|
|
711
|
+
if (!awaitsAnswer) leavePending();
|
|
712
|
+
}));
|
|
692
713
|
connectionTimer = setTimeout(() => {
|
|
693
714
|
connectionTimer = undefined;
|
|
694
715
|
// A residual guard, no longer the settlement: dispose() has already
|
|
@@ -730,22 +751,29 @@ export class BaseGraphQLClient {
|
|
|
730
751
|
// client stays correct in both runtimes with no @types/node-only typing.
|
|
731
752
|
const timerHandle = connectionTimer as unknown as { unref?: () => void } | number;
|
|
732
753
|
if (typeof timerHandle === "object" && typeof timerHandle.unref === "function") timerHandle.unref();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Reporting nothing when the bound elapses on a dead connection leaves
|
|
757
|
+
// something else to end this operation when its client ends — and
|
|
758
|
+
// graphql-ws is not it. Its dispose() speaks to the sinks of operations
|
|
759
|
+
// its socket CARRIED, and a handshake that never completed carried none,
|
|
760
|
+
// so that one would wait forever and whoever awaited it with it (#2102).
|
|
761
|
+
// A request it did carry hears a bare COMPLETE — no value and no error —
|
|
762
|
+
// so map never runs, catchError never fires, an await reports an
|
|
763
|
+
// anonymous EmptyError, and the request is indistinguishable from one
|
|
764
|
+
// never made (#1572: the sidebar of #1571 kept the previous tenant's
|
|
765
|
+
// entries across a channel switch that way). Registered while either can
|
|
766
|
+
// still happen, and settled by settlePendingOperations on the disposing
|
|
767
|
+
// owner's own stack, with the error a later operation already receives.
|
|
768
|
+
if (awaitsAnswer || connectionTimer !== undefined) {
|
|
740
769
|
pendingEntry = {
|
|
741
770
|
owner: this,
|
|
742
771
|
abandon: () => {
|
|
743
772
|
if (disposed || settled) return;
|
|
744
773
|
// The same disarm a wire answer performs: the latch closes and the
|
|
745
|
-
//
|
|
774
|
+
// bounds are cleared, so nothing speaks twice for this operation.
|
|
746
775
|
settle();
|
|
747
|
-
|
|
748
|
-
subscriber.error(new ClientError({ errors: [{ message, extensions: { endpoint: this.endpoint } }] }, message));
|
|
776
|
+
subscriber.error(this.clientDisposedError());
|
|
749
777
|
},
|
|
750
778
|
};
|
|
751
779
|
WS_CONNECTION_STATE.get(client)?.pending.add(pendingEntry);
|
|
@@ -768,6 +796,7 @@ export class BaseGraphQLClient {
|
|
|
768
796
|
disposed = true;
|
|
769
797
|
clearOperationDeadline();
|
|
770
798
|
clearConnectionTimeout();
|
|
799
|
+
leavePending();
|
|
771
800
|
for (const off of offConnectionEvents) off();
|
|
772
801
|
cleanup();
|
|
773
802
|
};
|
|
@@ -858,6 +887,20 @@ export class BaseGraphQLClient {
|
|
|
858
887
|
return new ClientError({ errors: [{ message, extensions }] }, message);
|
|
859
888
|
}
|
|
860
889
|
|
|
890
|
+
/**
|
|
891
|
+
* The ONE error a disposed client raises (#1994, #2102, #1572): for an
|
|
892
|
+
* operation started after dispose(), for one still waiting on a handshake,
|
|
893
|
+
* and for a query or a mutation still waiting on its answer. Named, so a
|
|
894
|
+
* consumer's error path tells an operation its own client cut off from one
|
|
895
|
+
* the server failed; carrying the endpoint, so a late one stays attributable.
|
|
896
|
+
* Never routed through the error handler: the disposal is the caller's own
|
|
897
|
+
* act, not a fault worth a notification.
|
|
898
|
+
*/
|
|
899
|
+
private clientDisposedError(): ClientError {
|
|
900
|
+
const message = "WS_CLIENT_DISPOSED";
|
|
901
|
+
return new ClientError({ errors: [{ message, extensions: { endpoint: this.endpoint } }] }, message);
|
|
902
|
+
}
|
|
903
|
+
|
|
861
904
|
/**
|
|
862
905
|
* Uniform error contract for the sink-level error path. graphql-ws
|
|
863
906
|
* delivers a server ErrorMessage payload as a raw GraphQLError[] —
|
|
@@ -1012,22 +1055,27 @@ export class BaseGraphQLClient {
|
|
|
1012
1055
|
}
|
|
1013
1056
|
|
|
1014
1057
|
/**
|
|
1015
|
-
* Ends
|
|
1016
|
-
*
|
|
1017
|
-
* of operations its socket CARRIED
|
|
1018
|
-
* its disposal says nothing to them and they wait
|
|
1019
|
-
* will never connect again — and the caller
|
|
1020
|
-
* (measured from the far end as backends
|
|
1021
|
-
* the client had already abandoned).
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1058
|
+
* Ends, where its owner stands, every operation graphql-ws's disposal would
|
|
1059
|
+
* leave without an ending it can use, with WS_CLIENT_DISPOSED. graphql-ws
|
|
1060
|
+
* speaks to the sinks of operations its socket CARRIED. An unanswered
|
|
1061
|
+
* handshake carried none, so its disposal says nothing to them and they wait
|
|
1062
|
+
* forever on a client that will never connect again — and the caller
|
|
1063
|
+
* awaiting one waits with it (#2102; measured from the far end as backends
|
|
1064
|
+
* left idle in transaction on a wait the client had already abandoned). A
|
|
1065
|
+
* query or a mutation it carried unanswered it COMPLETES with no value,
|
|
1066
|
+
* which no consumer can tell from a request never made (#1572). Erroring
|
|
1067
|
+
* from the BOUND is the thing this must not do: that fires 10 s later, in
|
|
1068
|
+
* whatever context is alive then. dispose() is the owner's own stack, so a
|
|
1069
|
+
* rejection lands there. A subscription past its handshake is not here: a
|
|
1070
|
+
* stream its client ended completes truthfully, and an operation whose
|
|
1071
|
+
* answer arrived left the registry with it.
|
|
1024
1072
|
*
|
|
1025
1073
|
* The socket's owner ends every operation on it, forks included — the same
|
|
1026
1074
|
* reason the socket rather than the client carries the disposed flag. A fork
|
|
1027
1075
|
* owns no socket and ends only what it started, leaving its owner on the
|
|
1028
1076
|
* wire. Idempotent: an entry settles once and deregisters itself doing so.
|
|
1029
1077
|
*/
|
|
1030
|
-
private
|
|
1078
|
+
private settlePendingOperations(): void {
|
|
1031
1079
|
const wsClient = this.wsClient;
|
|
1032
1080
|
if (!wsClient) return;
|
|
1033
1081
|
const pending = WS_CONNECTION_STATE.get(wsClient)?.pending;
|
|
@@ -1041,15 +1089,18 @@ export class BaseGraphQLClient {
|
|
|
1041
1089
|
|
|
1042
1090
|
/**
|
|
1043
1091
|
* Terminal (#1994). Ends the socket, ends any retry in flight, ends every
|
|
1044
|
-
* operation still waiting on a handshake (#2102)
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1092
|
+
* operation still waiting on a handshake (#2102) and every query or
|
|
1093
|
+
* mutation still waiting on its answer (#1572) with WS_CLIENT_DISPOSED, and
|
|
1094
|
+
* refuses every later operation with the same error — a disposed client
|
|
1095
|
+
* never mints another socket. An answer that already arrived is kept, and a
|
|
1096
|
+
* subscription the socket carried completes. Idempotent and synchronous;
|
|
1097
|
+
* never throws.
|
|
1047
1098
|
*/
|
|
1048
1099
|
dispose(): void {
|
|
1049
1100
|
this.disposed = true;
|
|
1050
1101
|
// Before the socket goes: erroring a subscriber tears its graphql-ws
|
|
1051
|
-
// subscription down, so the disposal that follows finds
|
|
1052
|
-
this.
|
|
1102
|
+
// subscription down, so the disposal that follows finds none of them.
|
|
1103
|
+
this.settlePendingOperations();
|
|
1053
1104
|
this.disposeWsClient();
|
|
1054
1105
|
}
|
|
1055
1106
|
|