@usearete/sdk 0.21.0 → 0.22.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/index.esm.js CHANGED
@@ -8,6 +8,17 @@ const DEFAULT_CONFIG = {
8
8
  maxReconnectAttempts: 5,
9
9
  maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
10
10
  };
11
+ const REPLAY_ERROR_CODES = {
12
+ 'cursor-expired': true,
13
+ 'cursor-epoch-changed': true,
14
+ 'cursor-unknown': true,
15
+ 'invalid-cursor': true,
16
+ 'replay-gap': true,
17
+ 'replay-lagged': true,
18
+ };
19
+ function isReplayErrorCode(code) {
20
+ return REPLAY_ERROR_CODES[code] === true;
21
+ }
11
22
  /**
12
23
  * Determines if the error indicates the client should fetch a new token
13
24
  */
@@ -30,7 +41,9 @@ class AreteError extends Error {
30
41
  }
31
42
  }
32
43
  /**
33
- * Parse a kebab-case error code string (from X-Error-Code header) to AuthErrorCode
44
+ * Parse a kebab-case error code string (from X-Error-Code header) to AuthErrorCode.
45
+ * Replay refusals keep their wire code: each needs a different recovery, so
46
+ * collapsing them into a generic failure would destroy the only distinction.
34
47
  */
35
48
  function parseErrorCode(errorCode) {
36
49
  const codeMap = {
@@ -63,7 +76,10 @@ function parseErrorCode(errorCode) {
63
76
  'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
64
77
  'quota-exceeded': 'QUOTA_EXCEEDED',
65
78
  };
66
- return codeMap[errorCode.toLowerCase()] || 'INTERNAL_ERROR';
79
+ const normalized = errorCode.toLowerCase();
80
+ if (isReplayErrorCode(normalized))
81
+ return normalized;
82
+ return codeMap[normalized] || 'INTERNAL_ERROR';
67
83
  }
68
84
 
69
85
  const GZIP_MAGIC_0 = 0x1f;
@@ -96,6 +112,17 @@ function isSubscriptionId(value) {
96
112
  function isMode(value) {
97
113
  return typeof value === 'string' && FRAME_MODES.has(value);
98
114
  }
115
+ function isOffset(value) {
116
+ return Number.isInteger(value) && value >= 0;
117
+ }
118
+ function isReplayWindow(value) {
119
+ return isRecord(value)
120
+ && typeof value['epoch'] === 'string'
121
+ && value['epoch'].length > 0
122
+ && isOffset(value['earliest'])
123
+ && isOffset(value['next'])
124
+ && (value['gapAfter'] === undefined || isOffset(value['gapAfter']));
125
+ }
99
126
  function isSort(value) {
100
127
  if (!isRecord(value))
101
128
  return false;
@@ -170,7 +197,9 @@ function isValidFrame(frame) {
170
197
  && typeof frame['fatal'] === 'boolean'
171
198
  && (frame['message'] === undefined || typeof frame['message'] === 'string')
172
199
  && (frame['error'] === undefined || typeof frame['error'] === 'string')
173
- && (frame['retryable'] === undefined || typeof frame['retryable'] === 'boolean');
200
+ && (frame['retryable'] === undefined || typeof frame['retryable'] === 'boolean')
201
+ && (frame['replayWindow'] === undefined || isReplayWindow(frame['replayWindow']))
202
+ && (frame['recoverFrom'] === undefined || typeof frame['recoverFrom'] === 'string');
174
203
  }
175
204
  if (!hasV2Identity(frame) || typeof frame['op'] !== 'string')
176
205
  return false;
@@ -179,7 +208,8 @@ function isValidFrame(frame) {
179
208
  if (frame['op'] === 'subscribed') {
180
209
  return isQuery(frame['query'])
181
210
  && isMode(frame['mode'])
182
- && (frame['sort'] === undefined || isSort(frame['sort']));
211
+ && (frame['sort'] === undefined || isSort(frame['sort']))
212
+ && (frame['replayWindow'] === undefined || isReplayWindow(frame['replayWindow']));
183
213
  }
184
214
  if (!isMode(frame['mode']) || typeof frame['entity'] !== 'string')
185
215
  return false;
@@ -196,10 +226,49 @@ function isValidFrame(frame) {
196
226
  && typeof frame['key'] === 'string'
197
227
  && 'data' in frame
198
228
  && (frame['seq'] === undefined || typeof frame['seq'] === 'string')
229
+ && (frame['offset'] === undefined || isOffset(frame['offset']))
199
230
  && (frame['append'] === undefined
200
231
  || (Array.isArray(frame['append'])
201
232
  && frame['append'].every((entry) => typeof entry === 'string')));
202
233
  }
234
+ /**
235
+ * Builds the `{epoch}:{offset}` cursor a consumer stores and sends back as
236
+ * `query.after`. An offset only means something inside the epoch the
237
+ * acknowledgement named, so the epoch has to be carried across frames.
238
+ */
239
+ class CursorTracker {
240
+ constructor() {
241
+ this.epochs = new Map();
242
+ this.cursors = new Map();
243
+ }
244
+ /** Records the frame and returns the cursor it carries, if any. */
245
+ observe(frame) {
246
+ if (isSubscribedFrame(frame)) {
247
+ const epoch = frame.replayWindow?.epoch;
248
+ if (epoch === undefined)
249
+ this.epochs.delete(frame.subscriptionId);
250
+ else
251
+ this.epochs.set(frame.subscriptionId, epoch);
252
+ return undefined;
253
+ }
254
+ if (!isEntityFrame(frame) || frame.offset === undefined)
255
+ return undefined;
256
+ const epoch = this.epochs.get(frame.subscriptionId);
257
+ if (epoch === undefined)
258
+ return undefined;
259
+ const cursor = `${epoch}:${frame.offset}`;
260
+ this.cursors.set(frame.subscriptionId, cursor);
261
+ return cursor;
262
+ }
263
+ /** The last cursor delivered on a subscription, for resuming after a drop. */
264
+ last(subscriptionId) {
265
+ return this.cursors.get(subscriptionId);
266
+ }
267
+ forget(subscriptionId) {
268
+ this.epochs.delete(subscriptionId);
269
+ this.cursors.delete(subscriptionId);
270
+ }
271
+ }
203
272
 
204
273
  let fallbackId = 0;
205
274
  const QUERY_FIELDS = new Set([
@@ -444,6 +513,7 @@ class SubscriptionRegistry {
444
513
  onChange: (callback) => this.queryStore.onChange(subscriptionId, callback),
445
514
  onUpdate: (callback) => this.queryStore.onUpdate(subscriptionId, callback),
446
515
  onRichUpdate: (callback) => this.queryStore.onRichUpdate(subscriptionId, callback),
516
+ getError: () => this.queryStore.getError(subscriptionId),
447
517
  refresh: () => this.refreshTracker(tracker),
448
518
  release: () => {
449
519
  if (released)
@@ -705,6 +775,12 @@ class ConnectionManager {
705
775
  this.currentState = 'disconnected';
706
776
  this.subscriptionQueue = new Map();
707
777
  this.activeSubscriptions = new Map();
778
+ /**
779
+ * Replay positions per subscription. Shared with the frame processor rather
780
+ * than duplicated: both observe the same frame stream, and this one is
781
+ * pruned on unsubscribe, so the processor gets that pruning for free.
782
+ */
783
+ this.cursors = new CursorTracker();
708
784
  this.socketGeneration = 0;
709
785
  this.pendingConnect = null;
710
786
  this.frameHandlers = new Set();
@@ -1516,6 +1592,7 @@ class ConnectionManager {
1516
1592
  }
1517
1593
  unsubscribe(subscriptionId) {
1518
1594
  this.subscriptionQueue.delete(subscriptionId);
1595
+ this.cursors.forget(subscriptionId);
1519
1596
  if (this.activeSubscriptions.has(subscriptionId)) {
1520
1597
  this.activeSubscriptions.delete(subscriptionId);
1521
1598
  if (this.ws?.readyState === WebSocket.OPEN) {
@@ -1560,9 +1637,14 @@ class ConnectionManager {
1560
1637
  }
1561
1638
  resubscribeActive() {
1562
1639
  for (const subscription of this.activeSubscriptions.values()) {
1563
- if (this.ws?.readyState === WebSocket.OPEN) {
1564
- this.ws.send(JSON.stringify(subscription));
1565
- }
1640
+ if (this.ws?.readyState !== WebSocket.OPEN)
1641
+ continue;
1642
+ // Re-sending the original `after` would replay what was already
1643
+ // delivered; sending none would skip whatever the drop cost us.
1644
+ const after = this.cursors.last(subscription.subscriptionId);
1645
+ this.ws.send(JSON.stringify(after === undefined
1646
+ ? subscription
1647
+ : { ...subscription, query: { ...subscription.query, after } }));
1566
1648
  }
1567
1649
  }
1568
1650
  addQueuedSubscription(subscription) {
@@ -1583,6 +1665,7 @@ class ConnectionManager {
1583
1665
  }
1584
1666
  }
1585
1667
  notifyFrameHandlers(frame) {
1668
+ this.cursors.observe(frame);
1586
1669
  for (const handler of this.frameHandlers) {
1587
1670
  handler(frame);
1588
1671
  }
@@ -1704,12 +1787,23 @@ function toCamelCaseSegment(value) {
1704
1787
  return pascal[0].toLowerCase() + pascal.slice(1);
1705
1788
  }
1706
1789
  class FrameProcessor {
1790
+ /**
1791
+ * Observe the connection's cursor tracker instead of a private one.
1792
+ *
1793
+ * The connection prunes on unsubscribe; this processor never sees an
1794
+ * `unsubscribed` frame, so its own tracker would grow for the life of the
1795
+ * page as a React app mounts and unmounts components with different queries.
1796
+ */
1797
+ useCursorTracker(cursors) {
1798
+ this.cursors = cursors;
1799
+ }
1707
1800
  constructor(storage, config = {}) {
1708
1801
  this.pendingUpdates = [];
1709
1802
  this.flushTimer = null;
1710
1803
  this.isProcessing = false;
1711
1804
  this.latestProcessedSlot = null;
1712
1805
  this.processedSlotWaiters = new Set();
1806
+ this.cursors = new CursorTracker();
1713
1807
  this.storage = storage;
1714
1808
  this.maxEntriesPerView = config.maxEntriesPerView === undefined
1715
1809
  ? DEFAULT_MAX_ENTRIES_PER_VIEW
@@ -2054,6 +2148,7 @@ class FrameProcessor {
2054
2148
  return null;
2055
2149
  }
2056
2150
  handleSubscribedFrame(frame) {
2151
+ this.cursors.observe(frame);
2057
2152
  const viewPath = frame.query.view;
2058
2153
  if (this.storage.setViewConfig && frame.sort) {
2059
2154
  this.storage.setViewConfig(viewPath, {
@@ -2099,7 +2194,11 @@ class FrameProcessor {
2099
2194
  const viewPath = frame.entity;
2100
2195
  const previousValue = this.storage.get(viewPath, frame.key);
2101
2196
  const previousSequence = this.getInternalSeq(previousValue);
2102
- const duplicateOrStaleSequence = frame.seq !== undefined
2197
+ const cursor = this.cursors.observe(frame);
2198
+ // On a tape the offset is the identity: two events decoded from one
2199
+ // transaction share a seq, so the seq guard would discard the second.
2200
+ const duplicateOrStaleSequence = frame.offset === undefined
2201
+ && frame.seq !== undefined
2103
2202
  && previousSequence !== undefined
2104
2203
  && this.compareSeq(frame.seq, previousSequence) <= 0;
2105
2204
  switch (frame.op) {
@@ -2136,9 +2235,10 @@ class FrameProcessor {
2136
2235
  type: 'upsert',
2137
2236
  key: frame.key,
2138
2237
  data: nextValue,
2238
+ cursor,
2139
2239
  };
2140
2240
  this.storage.notifyUpdate(viewPath, frame.key, update);
2141
- const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue);
2241
+ const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue, undefined, cursor);
2142
2242
  this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
2143
2243
  this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, richUpdate, frame.seq ?? this.extractSeq(frame.data));
2144
2244
  break;
@@ -2174,27 +2274,34 @@ class FrameProcessor {
2174
2274
  type: 'patch',
2175
2275
  key: frame.key,
2176
2276
  data: normalizedPatch,
2277
+ cursor,
2177
2278
  };
2178
2279
  this.storage.notifyUpdate(viewPath, frame.key, update);
2179
- const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue, normalizedPatch);
2280
+ const richUpdate = this.createRichUpdate(frame.key, previousValue, nextValue, normalizedPatch, cursor);
2180
2281
  this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
2181
2282
  this.queryStore?.applyLive(frame.subscriptionId, frame.key, update, richUpdate, frame.seq ?? this.extractSeq(frame.data));
2182
2283
  break;
2183
2284
  }
2184
2285
  case 'remove':
2185
- this.queryStore?.applyLive(frame.subscriptionId, frame.key, { type: 'remove', key: frame.key });
2286
+ this.queryStore?.applyLive(frame.subscriptionId, frame.key, { type: 'remove', key: frame.key, cursor });
2186
2287
  break;
2187
2288
  case 'delete':
2188
2289
  this.storage.delete(viewPath, frame.key);
2189
2290
  this.storage.notifyUpdate(viewPath, frame.key, {
2190
2291
  type: 'delete',
2191
2292
  key: frame.key,
2293
+ cursor,
2192
2294
  });
2193
2295
  if (previousValue !== null) {
2194
- const richUpdate = { type: 'deleted', key: frame.key, lastKnown: previousValue };
2296
+ const richUpdate = {
2297
+ type: 'deleted',
2298
+ key: frame.key,
2299
+ lastKnown: previousValue,
2300
+ cursor,
2301
+ };
2195
2302
  this.storage.notifyRichUpdate(viewPath, frame.key, richUpdate);
2196
2303
  }
2197
- this.queryStore?.deleteGlobal(viewPath, frame.key, previousValue ?? undefined);
2304
+ this.queryStore?.deleteGlobal(viewPath, frame.key, previousValue ?? undefined, frame.subscriptionId, cursor);
2198
2305
  break;
2199
2306
  }
2200
2307
  }
@@ -2204,10 +2311,10 @@ class FrameProcessor {
2204
2311
  : { type: 'updated', key, before, after, patch };
2205
2312
  this.storage.notifyRichUpdate(viewPath, key, richUpdate);
2206
2313
  }
2207
- createRichUpdate(key, before, after, patch) {
2314
+ createRichUpdate(key, before, after, patch, cursor) {
2208
2315
  return before === null
2209
- ? { type: 'created', key, data: after }
2210
- : { type: 'updated', key, before, after, patch };
2316
+ ? { type: 'created', key, data: after, cursor }
2317
+ : { type: 'updated', key, before, after, patch, cursor };
2211
2318
  }
2212
2319
  enforceMaxEntries(viewPath) {
2213
2320
  if (this.maxEntriesPerView === null)
@@ -2645,6 +2752,10 @@ class QueryStore {
2645
2752
  getQueryKey(subscriptionId) {
2646
2753
  return this.records.get(subscriptionId)?.queryKey;
2647
2754
  }
2755
+ /** The current failure without rebuilding the snapshot's data. */
2756
+ getError(subscriptionId) {
2757
+ return this.records.get(subscriptionId)?.error;
2758
+ }
2648
2759
  getSnapshot(subscriptionId) {
2649
2760
  const record = this.records.get(subscriptionId);
2650
2761
  if (!record)
@@ -2663,6 +2774,7 @@ class QueryStore {
2663
2774
  data,
2664
2775
  isLoading: record.isLoading,
2665
2776
  isRefreshing: record.isRefreshing,
2777
+ ...(record.cursor ? { cursor: record.cursor } : {}),
2666
2778
  ...(record.error ? { error: record.error } : {}),
2667
2779
  };
2668
2780
  record.cachedSnapshot = snapshot;
@@ -2758,13 +2870,15 @@ class QueryStore {
2758
2870
  const record = this.records.get(subscriptionId);
2759
2871
  if (!record)
2760
2872
  return;
2873
+ if (update.cursor !== undefined)
2874
+ record.cursor = update.cursor;
2761
2875
  if (update.type === 'remove') {
2762
2876
  const lastKnown = this.storage.get(record.subscription.query.view, key) ?? undefined;
2763
2877
  record.keys = record.keys.filter((entry) => entry !== key);
2764
2878
  record.sequences.delete(key);
2765
2879
  this.touch(record);
2766
2880
  this.emitUpdate(record, update);
2767
- this.emitRichUpdate(record, { type: 'removed', key, lastKnown });
2881
+ this.emitRichUpdate(record, { type: 'removed', key, lastKnown, cursor: update.cursor });
2768
2882
  return;
2769
2883
  }
2770
2884
  if (!record.keys.includes(key))
@@ -2783,15 +2897,22 @@ class QueryStore {
2783
2897
  if (richUpdate)
2784
2898
  this.emitRichUpdate(record, richUpdate);
2785
2899
  }
2786
- deleteGlobal(view, key, lastKnown) {
2900
+ deleteGlobal(view, key, lastKnown, subscriptionId, cursor) {
2787
2901
  for (const record of this.records.values()) {
2788
2902
  if (record.subscription.query.view !== view || !record.keys.includes(key))
2789
2903
  continue;
2790
2904
  record.keys = record.keys.filter((entry) => entry !== key);
2791
2905
  record.sequences.delete(key);
2906
+ // A delete fans out to every subscription on the view, but only the one
2907
+ // the frame named read it at this offset. Handing the position to the
2908
+ // others would let a consumer still replaying checkpoint past events it
2909
+ // has not been given.
2910
+ const owned = cursor !== undefined && record.subscription.subscriptionId === subscriptionId;
2911
+ if (owned)
2912
+ record.cursor = cursor;
2792
2913
  this.touch(record);
2793
- this.emitUpdate(record, { type: 'delete', key });
2794
- this.emitRichUpdate(record, { type: 'deleted', key, lastKnown });
2914
+ this.emitUpdate(record, owned ? { type: 'delete', key, cursor } : { type: 'delete', key });
2915
+ this.emitRichUpdate(record, owned ? { type: 'deleted', key, lastKnown, cursor } : { type: 'deleted', key, lastKnown });
2795
2916
  }
2796
2917
  }
2797
2918
  evict(view, key) {
@@ -2932,43 +3053,112 @@ class QueryStore {
2932
3053
  }
2933
3054
 
2934
3055
  const MAX_QUEUE_SIZE = 1000;
2935
- function enqueue(queue, value) {
2936
- if (queue.length >= MAX_QUEUE_SIZE)
2937
- queue.shift();
2938
- queue.push(value);
3056
+ /**
3057
+ * A replayable record was dropped locally before the consumer read it.
3058
+ * `cursor` is the last one delivered, so the stream can be resumed from it
3059
+ * with `after` rather than silently skipping the lost records.
3060
+ */
3061
+ class StreamGapError extends AreteError {
3062
+ constructor(cursor) {
3063
+ super(`Update stream dropped a replayable record; resume after ${cursor ?? 'the start of the window'}`, 'stream-gap', { cursor });
3064
+ this.cursor = cursor;
3065
+ this.name = 'StreamGapError';
3066
+ }
3067
+ }
3068
+ /** Terminate the stream when a replay refusal makes further delivery impossible. */
3069
+ function watchRefusals(lease, fail) {
3070
+ return lease.onChange(() => {
3071
+ const error = lease.getError();
3072
+ if (error && isReplayErrorCode(String(error.code)))
3073
+ fail(error);
3074
+ });
2939
3075
  }
2940
3076
  function createIterator(start) {
2941
3077
  const queue = [];
2942
- let waitingResolve = null;
3078
+ let waiting = null;
2943
3079
  let done = false;
2944
- const push = (value) => {
2945
- if (waitingResolve) {
2946
- const resolve = waitingResolve;
2947
- waitingResolve = null;
2948
- resolve({ value, done: false });
2949
- }
2950
- else {
2951
- enqueue(queue, value);
2952
- }
2953
- };
2954
- const stop = start(push);
3080
+ let failure = null;
3081
+ let lastCursor;
3082
+ let stop = () => { };
2955
3083
  const cleanup = () => {
2956
3084
  if (done)
2957
3085
  return;
2958
3086
  done = true;
2959
3087
  stop();
2960
- waitingResolve?.({ value: undefined, done: true });
2961
- waitingResolve = null;
3088
+ waiting?.resolve({ value: undefined, done: true });
3089
+ waiting = null;
3090
+ };
3091
+ const deliver = (item) => {
3092
+ if (item.cursor !== undefined)
3093
+ lastCursor = item.cursor;
3094
+ return { value: item.value, done: false };
3095
+ };
3096
+ /**
3097
+ * End the stream at `error`, after whatever is already queued.
3098
+ *
3099
+ * `discardQueued` is for a local overflow only: there the hole is at the
3100
+ * front of the queue, so everything behind it is no longer contiguous with
3101
+ * what the consumer has read. A server refusal is the opposite — it reports
3102
+ * records skipped *after* everything already sent, so the queue is real data
3103
+ * that arrived before the gap. Dropping it would lose records the consumer
3104
+ * was never told about and leave `recoverFrom` pointing past them.
3105
+ */
3106
+ const fail = (error, discardQueued = false) => {
3107
+ if (done || failure)
3108
+ return;
3109
+ failure = error;
3110
+ if (discardQueued)
3111
+ queue.length = 0;
3112
+ const pending = waiting;
3113
+ waiting = null;
3114
+ // A waiter means the queue was empty, so there is nothing to drain first.
3115
+ if (pending && queue.length === 0) {
3116
+ failure = null;
3117
+ cleanup();
3118
+ pending.reject(error);
3119
+ return;
3120
+ }
3121
+ if (pending) {
3122
+ const item = queue.shift();
3123
+ pending.resolve(deliver(item));
3124
+ }
2962
3125
  };
3126
+ const push = (value, cursor) => {
3127
+ if (done || failure)
3128
+ return;
3129
+ if (waiting) {
3130
+ const pending = waiting;
3131
+ waiting = null;
3132
+ pending.resolve(deliver({ value, cursor }));
3133
+ return;
3134
+ }
3135
+ if (queue.length >= MAX_QUEUE_SIZE) {
3136
+ const dropped = queue.shift();
3137
+ if (dropped?.cursor !== undefined) {
3138
+ fail(new StreamGapError(lastCursor), true);
3139
+ return;
3140
+ }
3141
+ }
3142
+ queue.push({ value, cursor });
3143
+ };
3144
+ stop = start(push, fail);
3145
+ if (done)
3146
+ stop();
2963
3147
  return {
2964
3148
  async next() {
3149
+ const item = queue.shift();
3150
+ if (item !== undefined)
3151
+ return deliver(item);
3152
+ if (failure) {
3153
+ const error = failure;
3154
+ failure = null;
3155
+ cleanup();
3156
+ throw error;
3157
+ }
2965
3158
  if (done)
2966
3159
  return { value: undefined, done: true };
2967
- const value = queue.shift();
2968
- if (value !== undefined)
2969
- return { value, done: false };
2970
- return new Promise((resolve) => {
2971
- waitingResolve = resolve;
3160
+ return new Promise((resolve, reject) => {
3161
+ waiting = { resolve, reject };
2972
3162
  });
2973
3163
  },
2974
3164
  async return() {
@@ -2987,14 +3177,16 @@ function acquire(registry, query, snapshotEnabled) {
2987
3177
  function createUpdateStream(_storage, subscriptionRegistry, query, keyFilter, snapshotEnabled = true) {
2988
3178
  return {
2989
3179
  [Symbol.asyncIterator]() {
2990
- return createIterator((push) => {
3180
+ return createIterator((push, fail) => {
2991
3181
  const lease = acquire(subscriptionRegistry, query, snapshotEnabled);
2992
3182
  const unsubscribe = lease.onUpdate((update) => {
2993
3183
  if (keyFilter === undefined || update.key === keyFilter)
2994
- push(update);
3184
+ push(update, update.cursor);
2995
3185
  });
3186
+ const stopRefusals = watchRefusals(lease, fail);
2996
3187
  return () => {
2997
3188
  unsubscribe();
3189
+ stopRefusals();
2998
3190
  lease.release();
2999
3191
  };
3000
3192
  });
@@ -3005,16 +3197,16 @@ function createEntityStream(_storage, subscriptionRegistry, query, options, keyF
3005
3197
  const schema = options?.schema;
3006
3198
  return {
3007
3199
  [Symbol.asyncIterator]() {
3008
- return createIterator((push) => {
3200
+ return createIterator((push, fail) => {
3009
3201
  const lease = acquire(subscriptionRegistry, query, options?.withSnapshot ?? true);
3010
- const emit = (entity) => {
3202
+ const emit = (entity, cursor) => {
3011
3203
  if (!schema) {
3012
- push(entity);
3204
+ push(entity, cursor);
3013
3205
  return;
3014
3206
  }
3015
3207
  const parsed = schema.safeParse(entity);
3016
3208
  if (parsed.success)
3017
- push(parsed.data);
3209
+ push(parsed.data, cursor);
3018
3210
  };
3019
3211
  for (const [index, entity] of lease.getSnapshot().data.entries()) {
3020
3212
  const key = lease.getSnapshot().keys[index];
@@ -3025,12 +3217,14 @@ function createEntityStream(_storage, subscriptionRegistry, query, options, keyF
3025
3217
  if (keyFilter !== undefined && update.key !== keyFilter)
3026
3218
  return;
3027
3219
  if (update.type === 'created')
3028
- emit(update.data);
3220
+ emit(update.data, update.cursor);
3029
3221
  if (update.type === 'updated')
3030
- emit(update.after);
3222
+ emit(update.after, update.cursor);
3031
3223
  });
3224
+ const stopRefusals = watchRefusals(lease, fail);
3032
3225
  return () => {
3033
3226
  unsubscribe();
3227
+ stopRefusals();
3034
3228
  lease.release();
3035
3229
  };
3036
3230
  });
@@ -3040,14 +3234,16 @@ function createEntityStream(_storage, subscriptionRegistry, query, options, keyF
3040
3234
  function createRichUpdateStream(_storage, subscriptionRegistry, query, keyFilter, snapshotEnabled = true) {
3041
3235
  return {
3042
3236
  [Symbol.asyncIterator]() {
3043
- return createIterator((push) => {
3237
+ return createIterator((push, fail) => {
3044
3238
  const lease = acquire(subscriptionRegistry, query, snapshotEnabled);
3045
3239
  const unsubscribe = lease.onRichUpdate((update) => {
3046
3240
  if (keyFilter === undefined || update.key === keyFilter)
3047
- push(update);
3241
+ push(update, update.cursor);
3048
3242
  });
3243
+ const stopRefusals = watchRefusals(lease, fail);
3049
3244
  return () => {
3050
3245
  unsubscribe();
3246
+ stopRefusals();
3051
3247
  lease.release();
3052
3248
  };
3053
3249
  });
@@ -6835,6 +7031,7 @@ let Arete$1 = class Arete {
6835
7031
  auth: options.auth,
6836
7032
  fetch: this.fetchImpl,
6837
7033
  });
7034
+ this.processor.useCursorTracker(this.connection.cursors);
6838
7035
  this.subscriptionRegistry = new SubscriptionRegistry(this.connection, this.queryStore);
6839
7036
  this.connection.onFrame((frame) => {
6840
7037
  this.processor.handleFrame(frame);
@@ -7726,5 +7923,5 @@ function stringifyBigints(value) {
7726
7923
 
7727
7924
  const Arete = Object.assign(Arete$1, { session: createSession });
7728
7925
 
7729
- export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OPERATION_KIND, OperationCallbackError, OperationExecutionError, PROGRAM_OPERATION_EXTENSIONS, ProcessedSlotTimeoutError, QueryStore, ReadRequestError, SPL_TOKEN_PROGRAM_ADDRESS, STACK_RUNTIME_EXTENSIONS, SYSTEM_PROGRAM_ADDRESS, SubscriptionRegistry, TOKEN_2022_PROGRAM_ADDRESS, TRANSACTION_RESOURCE_OPTION_KEYS, TransactionExecutionError, TransactionOptionsError, TransactionTransportError, account, appendFlowTransactions, appendTransactionInstructions, applyConnectedStackExtensions, arg, buildInstruction, bytes, canonicalQueryKey, chainAccountLoader, createChainClient, createEntityStream, createHostedSolanaGatewayTransports, createInstructionExecutor, createInstructionHandler, createPreparedFlow, createPreparedInstruction, createPreparedTransaction, createPreparedTransactionBody, createProgramPdas, createPublicKeySeed, createRichUpdateStream, createSeed, createSession, createSignerRegistry, createSubscriptionId, createTransactionTransport, createTypedListView, createTypedStateView, createTypedViews, createUpdateStream, decodeBase58, defineProgramExtensions, defineStackExtensions, deriveAssociatedTokenAccount, deriveHttpEndpoint, findProgramAddress as derivePda, describePreparedOperation, encodeBase58, executeInstruction, executePreparedOperation, extendProgram, extendPrograms, extendStack, findProgramAddress, findProgramAddressSync, flowOperation, formatPreparedOperation, formatProgramError, formatRawToUi, getMintDecimals, getOperationKind, getProgramRuntimeExtensions, getStackRuntimeExtensions, getTransactionFailureOutcome, hostedWebsocketSuffixes, inspectPreparedOperation, instructionOperation, isEntityFrame, isErrorFrame, isHostedAreteEndpoint, isSnapshotFrame, isSubscribedFrame, isUnsubscribedFrame, isValidFrame, literal, normalizeSubscription, normalizeSubscriptionQuery, normalizeSubscriptionRequest, normalizeTransactionError, parseFrame, parseFrameFromBlob, parseInstructionError, parseUiAmountToRaw, pda, prependFlowTransactionInstructions, prependTransactionInstructions, programAccountRead, programQuery, resolveAccounts, resolveAmount, resolveAmountToRaw, resolveAmountsToRaw, resolveTokenProgramAddress, resolveTransactionBuildOptions, safeToRawAmount, serializeInstructionData, serializeViewKey, setHostedWebsocketSuffixes, stackQuery, stringifyBigints, toJsonValue, toRawAmount, toWireResourceOptions, transactionOperation, unwrapOperationExecutionError, validateAccountResolution, validateProgramReadDescriptor, validateSubscriptionId, withProgramRead, withPrograms };
7926
+ export { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OPERATION_KIND, OperationCallbackError, OperationExecutionError, PROGRAM_OPERATION_EXTENSIONS, ProcessedSlotTimeoutError, QueryStore, ReadRequestError, SPL_TOKEN_PROGRAM_ADDRESS, STACK_RUNTIME_EXTENSIONS, SYSTEM_PROGRAM_ADDRESS, StreamGapError, SubscriptionRegistry, TOKEN_2022_PROGRAM_ADDRESS, TRANSACTION_RESOURCE_OPTION_KEYS, TransactionExecutionError, TransactionOptionsError, TransactionTransportError, account, appendFlowTransactions, appendTransactionInstructions, applyConnectedStackExtensions, arg, buildInstruction, bytes, canonicalQueryKey, chainAccountLoader, createChainClient, createEntityStream, createHostedSolanaGatewayTransports, createInstructionExecutor, createInstructionHandler, createPreparedFlow, createPreparedInstruction, createPreparedTransaction, createPreparedTransactionBody, createProgramPdas, createPublicKeySeed, createRichUpdateStream, createSeed, createSession, createSignerRegistry, createSubscriptionId, createTransactionTransport, createTypedListView, createTypedStateView, createTypedViews, createUpdateStream, decodeBase58, defineProgramExtensions, defineStackExtensions, deriveAssociatedTokenAccount, deriveHttpEndpoint, findProgramAddress as derivePda, describePreparedOperation, encodeBase58, executeInstruction, executePreparedOperation, extendProgram, extendPrograms, extendStack, findProgramAddress, findProgramAddressSync, flowOperation, formatPreparedOperation, formatProgramError, formatRawToUi, getMintDecimals, getOperationKind, getProgramRuntimeExtensions, getStackRuntimeExtensions, getTransactionFailureOutcome, hostedWebsocketSuffixes, inspectPreparedOperation, instructionOperation, isEntityFrame, isErrorFrame, isHostedAreteEndpoint, isReplayErrorCode, isSnapshotFrame, isSubscribedFrame, isUnsubscribedFrame, isValidFrame, literal, normalizeSubscription, normalizeSubscriptionQuery, normalizeSubscriptionRequest, normalizeTransactionError, parseFrame, parseFrameFromBlob, parseInstructionError, parseUiAmountToRaw, pda, prependFlowTransactionInstructions, prependTransactionInstructions, programAccountRead, programQuery, resolveAccounts, resolveAmount, resolveAmountToRaw, resolveAmountsToRaw, resolveTokenProgramAddress, resolveTransactionBuildOptions, safeToRawAmount, serializeInstructionData, serializeViewKey, setHostedWebsocketSuffixes, stackQuery, stringifyBigints, toJsonValue, toRawAmount, toWireResourceOptions, transactionOperation, unwrapOperationExecutionError, validateAccountResolution, validateProgramReadDescriptor, validateSubscriptionId, withProgramRead, withPrograms };
7730
7927
  //# sourceMappingURL=index.esm.js.map