@forgecart/sdk 1.202607281310.0 → 1.202608041455.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.
@@ -230,6 +230,16 @@ class BaseGraphQLClient {
230
230
  return new QueryObservable((subscriber) => {
231
231
  const client = this.getWsClient();
232
232
  let disposed = false;
233
+ // Delivery latch: once ANY sink callback has fired, the wire has
234
+ // spoken and a socket close can no longer mean "delivery unknown".
235
+ // Without it, a close event dispatched inside the delivery window
236
+ // (observed: between the error-result notify and the subscriber
237
+ // settling — CD runs 30845909288/30852033370/30855605921, where the
238
+ // server's NOT_VERIFIED answer was on the wire yet the consumer
239
+ // received WS_MUTATION_DELIVERY_INTERRUPTED, 3/3 deterministic)
240
+ // retro-fails an ALREADY-ANSWERED mutation. `settled` flips before
241
+ // any handler logic so every interleave is covered.
242
+ let settled = false;
233
243
  const offConnectionEvents = [];
234
244
  const cleanup = client.subscribe({
235
245
  query: document,
@@ -237,6 +247,7 @@ class BaseGraphQLClient {
237
247
  extensions: this.getPerOperationExtensions(),
238
248
  }, {
239
249
  next: (result) => {
250
+ settled = true;
240
251
  if (disposed)
241
252
  return;
242
253
  if (result.errors && result.errors.length > 0) {
@@ -247,8 +258,12 @@ class BaseGraphQLClient {
247
258
  })),
248
259
  };
249
260
  const error = new ClientError(response, result.errors[0].message);
250
- this.notifyErrorHandler(error);
261
+ // Terminal delivery FIRST, side-channel notification after:
262
+ // the notify hook runs arbitrary app code (toasts, logging,
263
+ // change detection), and anything it lets re-enter must find
264
+ // this operation already settled, never still interruptible.
251
265
  subscriber.error(error);
266
+ this.notifyErrorHandler(error);
252
267
  return;
253
268
  }
254
269
  if (result.data) {
@@ -257,11 +272,13 @@ class BaseGraphQLClient {
257
272
  }
258
273
  },
259
274
  error: (err) => {
275
+ settled = true;
260
276
  if (disposed)
261
277
  return;
262
278
  subscriber.error(this.toStreamError(err));
263
279
  },
264
280
  complete: () => {
281
+ settled = true;
265
282
  if (disposed)
266
283
  return;
267
284
  subscriber.complete();
@@ -271,7 +288,7 @@ class BaseGraphQLClient {
271
288
  let armed = WS_CONNECTION_STATE.get(client)?.connected ?? false;
272
289
  offConnectionEvents.push(client.on("connected", () => { armed = true; }));
273
290
  offConnectionEvents.push(client.on("closed", () => {
274
- if (disposed || !armed)
291
+ if (disposed || settled || !armed)
275
292
  return;
276
293
  const message = "WS_MUTATION_DELIVERY_INTERRUPTED";
277
294
  // subscriber.error tears the subscription down synchronously,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgecart/sdk",
3
- "version": "1.202607281310.0",
3
+ "version": "1.202608041455.0",
4
4
  "description": "ForgeCart Client SDK",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -334,6 +334,16 @@ export class BaseGraphQLClient {
334
334
  return new QueryObservable<T>((subscriber) => {
335
335
  const client = this.getWsClient();
336
336
  let disposed = false;
337
+ // Delivery latch: once ANY sink callback has fired, the wire has
338
+ // spoken and a socket close can no longer mean "delivery unknown".
339
+ // Without it, a close event dispatched inside the delivery window
340
+ // (observed: between the error-result notify and the subscriber
341
+ // settling — CD runs 30845909288/30852033370/30855605921, where the
342
+ // server's NOT_VERIFIED answer was on the wire yet the consumer
343
+ // received WS_MUTATION_DELIVERY_INTERRUPTED, 3/3 deterministic)
344
+ // retro-fails an ALREADY-ANSWERED mutation. `settled` flips before
345
+ // any handler logic so every interleave is covered.
346
+ let settled = false;
337
347
  const offConnectionEvents: (() => void)[] = [];
338
348
 
339
349
  const cleanup = client.subscribe<Record<string, unknown>>(
@@ -344,6 +354,7 @@ export class BaseGraphQLClient {
344
354
  },
345
355
  {
346
356
  next: (result) => {
357
+ settled = true;
347
358
  if (disposed) return;
348
359
  if (result.errors && result.errors.length > 0) {
349
360
  const response: GraphQLErrorResponse = {
@@ -353,8 +364,12 @@ export class BaseGraphQLClient {
353
364
  })),
354
365
  };
355
366
  const error = new ClientError(response, result.errors[0].message);
356
- this.notifyErrorHandler(error);
367
+ // Terminal delivery FIRST, side-channel notification after:
368
+ // the notify hook runs arbitrary app code (toasts, logging,
369
+ // change detection), and anything it lets re-enter must find
370
+ // this operation already settled, never still interruptible.
357
371
  subscriber.error(error);
372
+ this.notifyErrorHandler(error);
358
373
  return;
359
374
  }
360
375
  if (result.data) {
@@ -363,10 +378,12 @@ export class BaseGraphQLClient {
363
378
  }
364
379
  },
365
380
  error: (err) => {
381
+ settled = true;
366
382
  if (disposed) return;
367
383
  subscriber.error(this.toStreamError(err));
368
384
  },
369
385
  complete: () => {
386
+ settled = true;
370
387
  if (disposed) return;
371
388
  subscriber.complete();
372
389
  },
@@ -377,7 +394,7 @@ export class BaseGraphQLClient {
377
394
  let armed = WS_CONNECTION_STATE.get(client)?.connected ?? false;
378
395
  offConnectionEvents.push(client.on("connected", () => { armed = true; }));
379
396
  offConnectionEvents.push(client.on("closed", () => {
380
- if (disposed || !armed) return;
397
+ if (disposed || settled || !armed) return;
381
398
  const message = "WS_MUTATION_DELIVERY_INTERRUPTED";
382
399
  // subscriber.error tears the subscription down synchronously,
383
400
  // disposing the pending graphql-ws operation BEFORE its retry