@makerx/graphql-core 3.1.0 → 3.3.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.
@@ -8,30 +8,74 @@
8
8
  *
9
9
  * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.
10
10
  *
11
+ * ### Avoiding the subscribe-before-snapshot race
12
+ *
13
+ * `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the
14
+ * snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the
15
+ * snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that
16
+ * window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this
17
+ * helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory
18
+ * to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead
19
+ * of dropped, and are delivered immediately after the snapshot.
20
+ *
11
21
  * @param iterator The async iterator to wrap.
12
- * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
22
+ * @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an
23
+ * array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the
24
+ * "snapshot then stream" pattern so the subscription is established before the snapshot is read.
13
25
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
14
26
  * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
15
- * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
16
- * is checked.
27
+ * the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in
28
+ * the factory form the single eager pull used to establish the subscription is discarded on teardown. For events
29
+ * from the wrapped iterator, every event is checked.
17
30
  * @returns An async iterator for the event data.
18
31
  */
19
32
  function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFinal, }) {
20
33
  let done = false;
21
- const initialQueue = initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload];
34
+ const toQueue = (payload) => payload === undefined ? [] : Array.isArray(payload) ? [...payload] : [payload];
35
+ // Factory form: the caller wants the channel subscription established BEFORE the initial payload
36
+ // (typically a DB snapshot) is read. The wrapped iterator subscribes on its first `next()`, so pull it
37
+ // eagerly *now*. The resulting event is buffered and delivered after the initial payload drains; the
38
+ // factory (and therefore the snapshot read) runs lazily on the first consumer `next()`, after the
39
+ // subscription is already kicked.
40
+ const isFactory = typeof initialPayload === 'function';
41
+ let eagerPull = isFactory ? wrapped.next() : undefined;
42
+ // Value form: build the queue eagerly, exactly as before. Factory form: resolved lazily on first next().
43
+ let initialQueue = isFactory ? undefined : toQueue(initialPayload);
44
+ // Hand back the eagerly-pulled (= already-subscribed) event the first time, then defer to wrapped.next().
45
+ const consumeWrapped = () => {
46
+ if (eagerPull) {
47
+ const pull = eagerPull;
48
+ eagerPull = undefined;
49
+ return pull;
50
+ }
51
+ return wrapped.next();
52
+ };
53
+ // Abandon the eager pull without leaking an unhandled rejection. Only reached via early teardown
54
+ // (return/throw before the buffered event is consumed) or the eventIsFinal short-circuit on the
55
+ // snapshot — neither consumes the buffered event, and wrapped.return() tears down the kicked subscription.
56
+ const discardEagerPull = () => {
57
+ if (eagerPull) {
58
+ eagerPull.catch(() => { });
59
+ eagerPull = undefined;
60
+ }
61
+ };
22
62
  const iterator = {
23
63
  next: async () => {
24
64
  if (done)
25
65
  return { done: true, value: undefined };
66
+ if (initialQueue === undefined) {
67
+ initialQueue = toQueue(await initialPayload());
68
+ }
26
69
  if (initialQueue.length > 0) {
27
70
  const value = initialQueue.shift();
28
71
  if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {
29
72
  done = true;
73
+ discardEagerPull(); // tear down the redundant eager subscription, if any
30
74
  await wrapped.return?.();
31
75
  }
32
76
  return { done: false, value };
33
77
  }
34
- const next = await wrapped.next();
78
+ const next = await consumeWrapped();
35
79
  if (!next.done && eventIsFinal && eventIsFinal(next.value)) {
36
80
  done = true;
37
81
  await wrapped.return?.();
@@ -40,14 +84,18 @@ function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFi
40
84
  },
41
85
  return: async (value) => {
42
86
  done = true;
43
- initialQueue.length = 0;
87
+ if (initialQueue)
88
+ initialQueue.length = 0;
89
+ discardEagerPull();
44
90
  if (wrapped.return)
45
91
  return wrapped.return(value);
46
92
  return { done: true, value: value };
47
93
  },
48
94
  throw: async (err) => {
49
95
  done = true;
50
- initialQueue.length = 0;
96
+ if (initialQueue)
97
+ initialQueue.length = 0;
98
+ discardEagerPull();
51
99
  if (wrapped.throw)
52
100
  return wrapped.throw(err);
53
101
  throw err;
@@ -1 +1 @@
1
- {"version":3,"file":"subscription-iterator.mjs","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which\n * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event\n * is checked.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: TEventData | TEventData[]\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n const initialQueue: TEventData[] =\n initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload]\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {\n done = true\n await wrapped.return?.()\n }\n return { done: false, value }\n }\n\n const next = await wrapped.next()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n initialQueue.length = 0\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n initialQueue.length = 0\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;AAiBG;AACG,SAAU,wBAAwB,CAAa,EACnD,QAAQ,EAAE,OAAO,EACjB,cAAc,EACd,YAAY,GAKb,EAAA;IACC,IAAI,IAAI,GAAG,KAAK;AAChB,IAAA,MAAM,YAAY,GAChB,cAAc,KAAK,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;AAE5G,IAAA,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,YAAW;AACf,YAAA,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;AAEjD,YAAA,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,gBAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAgB;AAChD,gBAAA,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE;oBACpE,IAAI,GAAG,IAAI;AACX,oBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;gBAC1B;AACA,gBAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YAC/B;AAEA,YAAA,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE;AACjC,YAAA,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;gBAC1D,IAAI,GAAG,IAAI;AACX,gBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;YAC1B;AACA,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,MAAM,EAAE,OAAO,KAAK,KAAI;YACtB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,MAAM;AAAE,gBAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAmB,EAAE;QACnD,CAAC;AACD,QAAA,KAAK,EAAE,OAAO,GAAG,KAAI;YACnB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,KAAK;AAAE,gBAAA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,YAAA,MAAM,GAAG;QACX,CAAC;KACF;AAED,IAAA,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,MAAM,QAAQ,EAAE;AAChE;;;;"}
1
+ {"version":3,"file":"subscription-iterator.mjs","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * A function that lazily produces an initial payload. Provided in place of a plain value when the\n * caller needs the wrapped iterator's subscription to be established *before* the payload is read.\n *\n * May return `undefined` (or a promise of it) to indicate \"no snapshot\" — e.g. a repository\n * `findOne` that yields nothing — in which case iteration proceeds straight to the wrapped stream.\n */\nexport type InitialPayloadFactory<TEventData> = () => TEventData | TEventData[] | undefined | Promise<TEventData | TEventData[] | undefined>\n\n/**\n * An initial payload: a single event, an array of events, or a factory that produces either (or\n * `undefined` for \"no snapshot\").\n *\n * Note: the factory form is detected via `typeof initialPayload === 'function'`. If `TEventData` is\n * itself a callable type, a value-form payload would be misclassified as a factory — wrap such a value\n * in an array (`[value]`) to disambiguate. GraphQL event payloads are objects, so this is not a concern\n * in practice.\n */\nexport type InitialPayload<TEventData> = TEventData | TEventData[] | InitialPayloadFactory<TEventData>\n\n/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * ### Avoiding the subscribe-before-snapshot race\n *\n * `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the\n * snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the\n * snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that\n * window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this\n * helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory\n * to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead\n * of dropped, and are delivered immediately after the snapshot.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an\n * array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the\n * \"snapshot then stream\" pattern so the subscription is established before the snapshot is read.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which\n * the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in\n * the factory form the single eager pull used to establish the subscription is discarded on teardown. For events\n * from the wrapped iterator, every event is checked.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: InitialPayload<TEventData>\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n\n const toQueue = (payload: TEventData | TEventData[] | undefined): TEventData[] =>\n payload === undefined ? [] : Array.isArray(payload) ? [...payload] : [payload]\n\n // Factory form: the caller wants the channel subscription established BEFORE the initial payload\n // (typically a DB snapshot) is read. The wrapped iterator subscribes on its first `next()`, so pull it\n // eagerly *now*. The resulting event is buffered and delivered after the initial payload drains; the\n // factory (and therefore the snapshot read) runs lazily on the first consumer `next()`, after the\n // subscription is already kicked.\n const isFactory = typeof initialPayload === 'function'\n let eagerPull: Promise<IteratorResult<TEventData>> | undefined = isFactory ? wrapped.next() : undefined\n\n // Value form: build the queue eagerly, exactly as before. Factory form: resolved lazily on first next().\n let initialQueue: TEventData[] | undefined = isFactory ? undefined : toQueue(initialPayload as TEventData | TEventData[] | undefined)\n\n // Hand back the eagerly-pulled (= already-subscribed) event the first time, then defer to wrapped.next().\n const consumeWrapped = (): Promise<IteratorResult<TEventData>> => {\n if (eagerPull) {\n const pull = eagerPull\n eagerPull = undefined\n return pull\n }\n return wrapped.next()\n }\n\n // Abandon the eager pull without leaking an unhandled rejection. Only reached via early teardown\n // (return/throw before the buffered event is consumed) or the eventIsFinal short-circuit on the\n // snapshot — neither consumes the buffered event, and wrapped.return() tears down the kicked subscription.\n const discardEagerPull = () => {\n if (eagerPull) {\n eagerPull.catch(() => {})\n eagerPull = undefined\n }\n }\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue === undefined) {\n initialQueue = toQueue(await (initialPayload as InitialPayloadFactory<TEventData>)())\n }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {\n done = true\n discardEagerPull() // tear down the redundant eager subscription, if any\n await wrapped.return?.()\n }\n return { done: false, value }\n }\n\n const next = await consumeWrapped()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n if (initialQueue) initialQueue.length = 0\n discardEagerPull()\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n if (initialQueue) initialQueue.length = 0\n discardEagerPull()\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":"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"}