soroban-events 0.1.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soroban-events",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Resilient, windowed event streamer and XDR decoder for Soroban RPC",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/decoder.js CHANGED
@@ -2,6 +2,13 @@ import { scValToNative } from '@stellar/stellar-sdk';
2
2
 
3
3
  const DEFAULT_MAX_DEPTH = 50;
4
4
 
5
+ class ScValDepthError extends Error {
6
+ constructor(maxDepth) {
7
+ super(`ScVal nesting exceeds maximum depth of ${maxDepth}`);
8
+ this.name = "ScValDepthError";
9
+ }
10
+ }
11
+
5
12
  export function unwrapScVal(scVal, options = {}) {
6
13
  if (scVal == null) return null;
7
14
 
@@ -9,14 +16,15 @@ export function unwrapScVal(scVal, options = {}) {
9
16
 
10
17
  try {
11
18
  return normalizeNative(scValToNative(scVal), 0, maxDepth);
12
- } catch {
19
+ } catch (error) {
20
+ if (error instanceof ScValDepthError) throw error;
13
21
  return parseScValDirect(scVal);
14
22
  }
15
23
  }
16
24
 
17
25
  function normalizeNative(value, depth, maxDepth) {
18
26
  if (depth > maxDepth) {
19
- throw new Error(`ScVal nesting exceeds maximum depth of ${maxDepth}`);
27
+ throw new ScValDepthError(maxDepth);
20
28
  }
21
29
 
22
30
  if (value === null || value === undefined) return null;
package/src/streamer.js CHANGED
@@ -45,15 +45,20 @@ export class SorobanEventStreamer {
45
45
  );
46
46
 
47
47
  this.pollInterval = options.pollInterval ?? 3000;
48
- this.windowSize = Math.min(
49
- options.windowSize ?? MAX_SAFE_LEDGER_SPAN,
50
- MAX_SAFE_LEDGER_SPAN
51
- );
52
48
 
53
- this.pageSize = Math.min(
54
- options.pageSize ?? DEFAULT_PAGE_SIZE,
55
- 10000
56
- );
49
+ const windowSize = options.windowSize ?? MAX_SAFE_LEDGER_SPAN;
50
+ if (!Number.isInteger(windowSize) || windowSize < 1) {
51
+ throw new TypeError("windowSize must be a positive integer");
52
+ }
53
+
54
+ this.windowSize = Math.min(windowSize, MAX_SAFE_LEDGER_SPAN);
55
+
56
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
57
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 10000) {
58
+ throw new TypeError("pageSize must be an integer between 1 and 10000");
59
+ }
60
+
61
+ this.pageSize = pageSize;
57
62
 
58
63
  this.maxRetries = options.maxRetries ?? 3;
59
64
  this.retryBaseMs = options.retryBaseMs ?? 500;
@@ -75,6 +80,10 @@ export class SorobanEventStreamer {
75
80
  throw new TypeError('startLedger must be a positive integer');
76
81
  }
77
82
 
83
+ if (!Number.isInteger(limit) || limit < 1) {
84
+ throw new TypeError('limit must be a positive integer');
85
+ }
86
+
78
87
  if (endLedger != null && (!Number.isInteger(endLedger) || endLedger < startLedger)) {
79
88
  throw new TypeError('endLedger must be >= startLedger');
80
89
  }
@@ -182,6 +191,70 @@ export class SorobanEventStreamer {
182
191
  return rawEvents.slice(0, limit);
183
192
  }
184
193
 
194
+ async fetchWindowNewest(
195
+ startLedger,
196
+ endLedgerExclusive,
197
+ filters,
198
+ limit,
199
+ signal
200
+ ) {
201
+ const newest = [];
202
+ let cursor;
203
+
204
+ while (true) {
205
+ if (signal?.aborted) {
206
+ throw new DOMException("Operation aborted", "AbortError");
207
+ }
208
+
209
+ const pagination = {
210
+ limit: this.pageSize
211
+ };
212
+
213
+ if (cursor) {
214
+ pagination.cursor = cursor;
215
+ }
216
+
217
+ const params = {
218
+ filters,
219
+ pagination
220
+ };
221
+
222
+ if (!cursor) {
223
+ params.startLedger = startLedger;
224
+ params.endLedger = endLedgerExclusive;
225
+ }
226
+
227
+ const response = await this.requestWithRetry(params, signal);
228
+ const events = response?.events ?? [];
229
+
230
+ for (const event of events) {
231
+ if (!event?.id) continue;
232
+
233
+ newest.push(event);
234
+
235
+ if (newest.length > limit) {
236
+ newest.shift();
237
+ }
238
+ }
239
+
240
+ const nextCursor = response?.cursor;
241
+
242
+ if (!nextCursor || events.length === 0) {
243
+ break;
244
+ }
245
+
246
+ if (nextCursor === cursor) {
247
+ throw new Error(
248
+ `RPC pagination cursor did not advance for ledger range ${startLedger}-${endLedgerExclusive}`
249
+ );
250
+ }
251
+
252
+ cursor = nextCursor;
253
+ }
254
+
255
+ return newest;
256
+ }
257
+
185
258
  async requestWithRetry(params, signal) {
186
259
  let attempt = 0;
187
260
 
@@ -217,7 +290,8 @@ export class SorobanEventStreamer {
217
290
  contractId,
218
291
  limit = 10,
219
292
  maxLookbackLedgers = 50000,
220
- filters
293
+ filters,
294
+ signal
221
295
  } = {}) {
222
296
  if (limit <= 0) return [];
223
297
 
@@ -247,11 +321,12 @@ export class SorobanEventStreamer {
247
321
  endExclusive - this.windowSize
248
322
  );
249
323
 
250
- const events = await this.fetchWindow(
324
+ const events = await this.fetchWindowNewest(
251
325
  start,
252
326
  endExclusive,
253
327
  effectiveFilters,
254
- this.pageSize
328
+ limit,
329
+ signal
255
330
  );
256
331
 
257
332
  for (let i = events.length - 1; i >= 0; i--) {