@usearete/sdk 0.21.0 → 0.22.1
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/README.md +31 -8
- package/dist/index.cjs +252 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +104 -4
- package/dist/index.esm.js +251 -54
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
1564
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
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
|
|
3078
|
+
let waiting = null;
|
|
2943
3079
|
let done = false;
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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
|
-
|
|
2961
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
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
|