@forgecart/sdk 1.202610251643.0 → 1.202610251830.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.
@@ -200,6 +200,25 @@ export declare class BaseGraphQLClient {
200
200
  * error.
201
201
  */
202
202
  private getFirstValueBudgetMs;
203
+ /**
204
+ * The longest graphql-ws waits before any re-dial (#3452): half the shorter
205
+ * of the handshake bound and a read's first-value deadline, so 5_000 ms at
206
+ * the defaults and 1_000 ms at a connectionTimeout of 2_000. A wait that
207
+ * starts with an operation therefore ends inside that operation's own
208
+ * bound, and the dial it leads to keeps the other half to be answered.
209
+ * A get*, not a find*: derived configuration, never a lookup.
210
+ */
211
+ private getRetryWaitCeilingMs;
212
+ /**
213
+ * graphql-ws's retryWait (#3452): the library's own randomised exponential
214
+ * delay, never longer than getRetryWaitCeilingMs(). graphql-ws resets its
215
+ * retry count only on an ack, so uncapped the delay doubles past its ladder
216
+ * for as long as an outage lasts; capped, the next operation after the
217
+ * outage re-dials within seconds. The timer stays a plain, reffed
218
+ * setTimeout: it replaces graphql-ws's own one for one and changes only
219
+ * how long it runs.
220
+ */
221
+ private waitBeforeRedial;
203
222
  /**
204
223
  * The name the document declares: `query ChannelPlugins` gives
205
224
  * "ChannelPlugins". Every generated document is a named operation, so the
@@ -89,6 +89,18 @@ var WsTimeoutBudgetMs;
89
89
  WsTimeoutBudgetMs[WsTimeoutBudgetMs["QUERY_FIRST_VALUE"] = 10000] = "QUERY_FIRST_VALUE";
90
90
  WsTimeoutBudgetMs[WsTimeoutBudgetMs["MUTATION_FIRST_VALUE"] = 60000] = "MUTATION_FIRST_VALUE";
91
91
  })(WsTimeoutBudgetMs || (WsTimeoutBudgetMs = {}));
92
+ /**
93
+ * graphql-ws's own randomised exponential backoff, which the library does
94
+ * not export: it waits BASE * 2 ** retries plus JITTER_MIN..JITTER_MAX of
95
+ * jitter before every re-dial. Restated so waitBeforeRedial can keep that
96
+ * delay and only cap it (#3452).
97
+ */
98
+ var WsRetryBackoffMs;
99
+ (function (WsRetryBackoffMs) {
100
+ WsRetryBackoffMs[WsRetryBackoffMs["BASE"] = 1000] = "BASE";
101
+ WsRetryBackoffMs[WsRetryBackoffMs["JITTER_MIN"] = 300] = "JITTER_MIN";
102
+ WsRetryBackoffMs[WsRetryBackoffMs["JITTER_MAX"] = 3000] = "JITTER_MAX";
103
+ })(WsRetryBackoffMs || (WsRetryBackoffMs = {}));
92
104
  /**
93
105
  * The per-OPERATION first-value budgets (#2245). A kind default sizes the
94
106
  * average operation of that kind; an operation that holds the socket for a
@@ -160,7 +172,8 @@ class BaseGraphQLClient {
160
172
  // A healthy handshake is milliseconds, and graphql-ws waits ~1-4 s before
161
173
  // its first re-dial — so 10 s still lets ONE transient non-101 refusal heal
162
174
  // (the retry-safety in getWsClient) while cutting the SILENT window from the
163
- // measured 32.5-46 s ladder to a bounded, logged failure (#2102).
175
+ // retry ladder (18.6-24 s at the default bounds, every wait capped by
176
+ // getRetryWaitCeilingMs, #3452) to a bounded, logged failure (#2102).
164
177
  this.connectionTimeout = options?.connectionTimeout ?? 10_000;
165
178
  this.tokenStorageKey = `forgecart:auth:${this.endpoint}:${this.baseHeaders["forgecart-token"] ?? ""}`;
166
179
  if (this.persistSession) {
@@ -206,8 +219,17 @@ class BaseGraphQLClient {
206
219
  // exponential backoff. A DISPOSED client answers false (#1994):
207
220
  // graphql-ws consults its own disposed flag before asking, so a
208
221
  // pending operation completes at dispose() instead of re-dialing
209
- // a dead endpoint for 32-46 s and erroring onto whoever runs then.
222
+ // a dead endpoint through the rest of the ladder and erroring onto
223
+ // whoever runs then.
210
224
  shouldRetry: () => !this.disposed,
225
+ // graphql-ws waits BEFORE it checks that an operation still holds
226
+ // the socket, and resets its count only on an ack: past the ladder
227
+ // every refused dial adds one, so its own waits double without end
228
+ // (32, 64, 128, 256 s) while every operation gives up at its bound
229
+ // first — after a long outage a quiet client never dialled again.
230
+ // The cap keeps every wait inside the bound of the operation that
231
+ // waits on it, so the shared socket heals on the next call (#3452).
232
+ retryWait: (retries) => this.waitBeforeRedial(retries),
211
233
  });
212
234
  const connectionState = { connected: false, disposed: false, pending: new Set() };
213
235
  WS_CONNECTION_STATE.set(this.wsClient, connectionState);
@@ -539,7 +561,7 @@ class BaseGraphQLClient {
539
561
  },
540
562
  });
541
563
  // Armed only while the socket is not ACK-live. An unanswered handshake
542
- // otherwise rides the whole graphql-ws retry ladder (32.5-46 s) with
564
+ // otherwise rides the whole graphql-ws retry ladder (18.6-24 s) with
543
565
  // nothing logged anywhere — the shape a pathless endpoint produces, and
544
566
  // a Suspense hole with no line to explain it. The shared socket is NOT
545
567
  // disposed and shouldRetry is untouched: graphql-ws keeps healing for the
@@ -692,6 +714,32 @@ class BaseGraphQLClient {
692
714
  return WsTimeoutBudgetMs.QUERY_FIRST_VALUE;
693
715
  return WsTimeoutBudgetMs.MUTATION_FIRST_VALUE;
694
716
  }
717
+ /**
718
+ * The longest graphql-ws waits before any re-dial (#3452): half the shorter
719
+ * of the handshake bound and a read's first-value deadline, so 5_000 ms at
720
+ * the defaults and 1_000 ms at a connectionTimeout of 2_000. A wait that
721
+ * starts with an operation therefore ends inside that operation's own
722
+ * bound, and the dial it leads to keeps the other half to be answered.
723
+ * A get*, not a find*: derived configuration, never a lookup.
724
+ */
725
+ getRetryWaitCeilingMs() {
726
+ return Math.min(this.connectionTimeout, WsTimeoutBudgetMs.QUERY_FIRST_VALUE) / 2;
727
+ }
728
+ /**
729
+ * graphql-ws's retryWait (#3452): the library's own randomised exponential
730
+ * delay, never longer than getRetryWaitCeilingMs(). graphql-ws resets its
731
+ * retry count only on an ack, so uncapped the delay doubles past its ladder
732
+ * for as long as an outage lasts; capped, the next operation after the
733
+ * outage re-dials within seconds. The timer stays a plain, reffed
734
+ * setTimeout: it replaces graphql-ws's own one for one and changes only
735
+ * how long it runs.
736
+ */
737
+ async waitBeforeRedial(retries) {
738
+ const jitterMs = Math.floor(Math.random() * (WsRetryBackoffMs.JITTER_MAX - WsRetryBackoffMs.JITTER_MIN) + WsRetryBackoffMs.JITTER_MIN);
739
+ const backoffMs = WsRetryBackoffMs.BASE * 2 ** retries + jitterMs;
740
+ const waitMs = Math.min(backoffMs, this.getRetryWaitCeilingMs());
741
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
742
+ }
695
743
  /**
696
744
  * The name the document declares: `query ChannelPlugins` gives
697
745
  * "ChannelPlugins". Every generated document is a named operation, so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgecart/sdk",
3
- "version": "1.202610251643.0",
3
+ "version": "1.202610251830.0",
4
4
  "description": "ForgeCart Client SDK",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,9 +21,12 @@ export interface ForgeCartAdminClientConfig {
21
21
  // Upper bound (ms) on an UNANSWERED graphql-ws handshake, per operation.
22
22
  // Defaults to 10_000. A healthy handshake is milliseconds and graphql-ws
23
23
  // waits ~1-4 s before its first re-dial, so the default still lets one
24
- // transient refusal heal while the caller stops waiting out the 32.5-46 s
25
- // retry ladder in silence (#2102). Never bounds a connected operation: a
26
- // live subscription runs for as long as the server streams.
24
+ // transient refusal heal while the caller stops waiting out the 18.6-24 s
25
+ // retry ladder in silence (#2102). Half the shorter of this and a read's
26
+ // 10 s deadline caps every wait before a re-dial — 5 s at the default —
27
+ // so after an outage of any length the next operation re-dials within
28
+ // that cap (#3452). Never bounds a connected operation: a live
29
+ // subscription runs for as long as the server streams.
27
30
  connectionTimeout?: number;
28
31
  channelToken: string;
29
32
  adminSecret?: string;
@@ -51,9 +54,12 @@ export interface ForgeCartShopClientConfig {
51
54
  // Upper bound (ms) on an UNANSWERED graphql-ws handshake, per operation.
52
55
  // Defaults to 10_000. A healthy handshake is milliseconds and graphql-ws
53
56
  // waits ~1-4 s before its first re-dial, so the default still lets one
54
- // transient refusal heal while the caller stops waiting out the 32.5-46 s
55
- // retry ladder in silence (#2102). Never bounds a connected operation: a
56
- // live subscription runs for as long as the server streams.
57
+ // transient refusal heal while the caller stops waiting out the 18.6-24 s
58
+ // retry ladder in silence (#2102). Half the shorter of this and a read's
59
+ // 10 s deadline caps every wait before a re-dial — 5 s at the default —
60
+ // so after an outage of any length the next operation re-dials within
61
+ // that cap (#3452). Never bounds a connected operation: a live
62
+ // subscription runs for as long as the server streams.
57
63
  connectionTimeout?: number;
58
64
  channelToken: string;
59
65
  uploadEndpoint?: string;
@@ -77,9 +83,12 @@ export interface ForgeCartClientConfig {
77
83
  // Upper bound (ms) on an UNANSWERED graphql-ws handshake, per operation.
78
84
  // Defaults to 10_000. A healthy handshake is milliseconds and graphql-ws
79
85
  // waits ~1-4 s before its first re-dial, so the default still lets one
80
- // transient refusal heal while the caller stops waiting out the 32.5-46 s
81
- // retry ladder in silence (#2102). Never bounds a connected operation: a
82
- // live subscription runs for as long as the server streams.
86
+ // transient refusal heal while the caller stops waiting out the 18.6-24 s
87
+ // retry ladder in silence (#2102). Half the shorter of this and a read's
88
+ // 10 s deadline caps every wait before a re-dial — 5 s at the default —
89
+ // so after an outage of any length the next operation re-dials within
90
+ // that cap (#3452). Never bounds a connected operation: a live
91
+ // subscription runs for as long as the server streams.
83
92
  connectionTimeout?: number;
84
93
  channelToken: string;
85
94
  adminSecret?: string;
@@ -214,6 +223,18 @@ enum WsTimeoutBudgetMs {
214
223
  MUTATION_FIRST_VALUE = 60_000,
215
224
  }
216
225
 
226
+ /**
227
+ * graphql-ws's own randomised exponential backoff, which the library does
228
+ * not export: it waits BASE * 2 ** retries plus JITTER_MIN..JITTER_MAX of
229
+ * jitter before every re-dial. Restated so waitBeforeRedial can keep that
230
+ * delay and only cap it (#3452).
231
+ */
232
+ enum WsRetryBackoffMs {
233
+ BASE = 1_000,
234
+ JITTER_MIN = 300,
235
+ JITTER_MAX = 3_000,
236
+ }
237
+
217
238
  /**
218
239
  * The per-OPERATION first-value budgets (#2245). A kind default sizes the
219
240
  * average operation of that kind; an operation that holds the socket for a
@@ -302,7 +323,8 @@ export class BaseGraphQLClient {
302
323
  // A healthy handshake is milliseconds, and graphql-ws waits ~1-4 s before
303
324
  // its first re-dial — so 10 s still lets ONE transient non-101 refusal heal
304
325
  // (the retry-safety in getWsClient) while cutting the SILENT window from the
305
- // measured 32.5-46 s ladder to a bounded, logged failure (#2102).
326
+ // retry ladder (18.6-24 s at the default bounds, every wait capped by
327
+ // getRetryWaitCeilingMs, #3452) to a bounded, logged failure (#2102).
306
328
  this.connectionTimeout = options?.connectionTimeout ?? 10_000;
307
329
  this.tokenStorageKey = `forgecart:auth:${this.endpoint}:${this.baseHeaders["forgecart-token"] ?? ""}`;
308
330
  if (this.persistSession) {
@@ -349,8 +371,17 @@ export class BaseGraphQLClient {
349
371
  // exponential backoff. A DISPOSED client answers false (#1994):
350
372
  // graphql-ws consults its own disposed flag before asking, so a
351
373
  // pending operation completes at dispose() instead of re-dialing
352
- // a dead endpoint for 32-46 s and erroring onto whoever runs then.
374
+ // a dead endpoint through the rest of the ladder and erroring onto
375
+ // whoever runs then.
353
376
  shouldRetry: () => !this.disposed,
377
+ // graphql-ws waits BEFORE it checks that an operation still holds
378
+ // the socket, and resets its count only on an ack: past the ladder
379
+ // every refused dial adds one, so its own waits double without end
380
+ // (32, 64, 128, 256 s) while every operation gives up at its bound
381
+ // first — after a long outage a quiet client never dialled again.
382
+ // The cap keeps every wait inside the bound of the operation that
383
+ // waits on it, so the shared socket heals on the next call (#3452).
384
+ retryWait: (retries) => this.waitBeforeRedial(retries),
354
385
  });
355
386
  const connectionState = { connected: false, disposed: false, pending: new Set<PendingOperation>() };
356
387
  WS_CONNECTION_STATE.set(this.wsClient, connectionState);
@@ -700,7 +731,7 @@ export class BaseGraphQLClient {
700
731
  );
701
732
 
702
733
  // Armed only while the socket is not ACK-live. An unanswered handshake
703
- // otherwise rides the whole graphql-ws retry ladder (32.5-46 s) with
734
+ // otherwise rides the whole graphql-ws retry ladder (18.6-24 s) with
704
735
  // nothing logged anywhere — the shape a pathless endpoint produces, and
705
736
  // a Suspense hole with no line to explain it. The shared socket is NOT
706
737
  // disposed and shouldRetry is untouched: graphql-ws keeps healing for the
@@ -851,6 +882,34 @@ export class BaseGraphQLClient {
851
882
  return WsTimeoutBudgetMs.MUTATION_FIRST_VALUE;
852
883
  }
853
884
 
885
+ /**
886
+ * The longest graphql-ws waits before any re-dial (#3452): half the shorter
887
+ * of the handshake bound and a read's first-value deadline, so 5_000 ms at
888
+ * the defaults and 1_000 ms at a connectionTimeout of 2_000. A wait that
889
+ * starts with an operation therefore ends inside that operation's own
890
+ * bound, and the dial it leads to keeps the other half to be answered.
891
+ * A get*, not a find*: derived configuration, never a lookup.
892
+ */
893
+ private getRetryWaitCeilingMs(): number {
894
+ return Math.min(this.connectionTimeout, WsTimeoutBudgetMs.QUERY_FIRST_VALUE) / 2;
895
+ }
896
+
897
+ /**
898
+ * graphql-ws's retryWait (#3452): the library's own randomised exponential
899
+ * delay, never longer than getRetryWaitCeilingMs(). graphql-ws resets its
900
+ * retry count only on an ack, so uncapped the delay doubles past its ladder
901
+ * for as long as an outage lasts; capped, the next operation after the
902
+ * outage re-dials within seconds. The timer stays a plain, reffed
903
+ * setTimeout: it replaces graphql-ws's own one for one and changes only
904
+ * how long it runs.
905
+ */
906
+ private async waitBeforeRedial(retries: number): Promise<void> {
907
+ const jitterMs = Math.floor(Math.random() * (WsRetryBackoffMs.JITTER_MAX - WsRetryBackoffMs.JITTER_MIN) + WsRetryBackoffMs.JITTER_MIN);
908
+ const backoffMs = WsRetryBackoffMs.BASE * 2 ** retries + jitterMs;
909
+ const waitMs = Math.min(backoffMs, this.getRetryWaitCeilingMs());
910
+ await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
911
+ }
912
+
854
913
  /**
855
914
  * The name the document declares: `query ChannelPlugins` gives
856
915
  * "ChannelPlugins". Every generated document is a named operation, so the