@secondlayer/sdk 6.35.0 → 6.37.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.
@@ -1,17 +1,127 @@
1
1
  // src/errors.ts
2
- class ApiError extends Error {
2
+ var DOCS_BASE = "https://www.secondlayer.tools";
3
+
4
+ class SecondLayerError extends Error {
5
+ name = "SecondLayerError";
6
+ shortMessage;
7
+ code;
8
+ docsUrl;
9
+ metaMessages;
10
+ retryable;
11
+ retryAfterSeconds;
12
+ constructor(shortMessage, options = {}) {
13
+ const detailBlocks = [
14
+ ...options.metaMessages ?? [],
15
+ ...options.details ? [options.details] : [],
16
+ ...options.docsUrl ? [`Docs: ${options.docsUrl}`] : []
17
+ ];
18
+ const message = [
19
+ shortMessage,
20
+ detailBlocks.length > 0 ? `
21
+ ${detailBlocks.join(`
22
+ `)}` : ""
23
+ ].join("");
24
+ super(message, options.cause ? { cause: options.cause } : undefined);
25
+ this.shortMessage = shortMessage;
26
+ this.code = options.code;
27
+ this.docsUrl = options.docsUrl;
28
+ this.metaMessages = options.metaMessages;
29
+ this.retryable = options.retryable ?? false;
30
+ this.retryAfterSeconds = options.retryAfterSeconds;
31
+ }
32
+ walk(fn) {
33
+ let err = this;
34
+ let last = this;
35
+ while (err != null) {
36
+ if (fn?.(err))
37
+ return err;
38
+ last = err;
39
+ err = err.cause;
40
+ }
41
+ return fn ? null : last;
42
+ }
43
+ toJSON() {
44
+ return {
45
+ name: this.name,
46
+ message: this.message,
47
+ shortMessage: this.shortMessage,
48
+ cause: this.cause instanceof Error ? this.cause.message : undefined,
49
+ code: this.code,
50
+ docsUrl: this.docsUrl,
51
+ retryable: this.retryable,
52
+ retryAfterSeconds: this.retryAfterSeconds
53
+ };
54
+ }
55
+ }
56
+ function statusRetryable(status) {
57
+ return status === 429 || status >= 500;
58
+ }
59
+ function docsFor(status, code) {
60
+ if (code === "UPGRADE_REQUIRED" || code === "PLAN_REQUIRED" || status === 402)
61
+ return `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;
62
+ if (status === 429 || status === 401)
63
+ return `${DOCS_BASE}/docs/authentication`;
64
+ return;
65
+ }
66
+
67
+ class ApiError extends SecondLayerError {
3
68
  status;
4
69
  body;
5
- code;
6
- constructor(status, message, body, code) {
7
- super(message);
70
+ constructor(status, message, body, code, options = {}) {
71
+ super(message, {
72
+ code,
73
+ retryable: options.retryable ?? statusRetryable(status),
74
+ docsUrl: options.docsUrl ?? docsFor(status, code),
75
+ ...options.cause ? { cause: options.cause } : {},
76
+ ...options.details ? { details: options.details } : {},
77
+ ...options.metaMessages ? { metaMessages: options.metaMessages } : {},
78
+ ...options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}
79
+ });
8
80
  this.status = status;
9
81
  this.body = body;
10
- this.code = code;
11
82
  this.name = "ApiError";
12
83
  }
13
84
  }
14
85
 
86
+ class AuthError extends ApiError {
87
+ status = 401;
88
+ constructor(message = "API key invalid or expired.") {
89
+ super(401, message);
90
+ this.name = "AuthError";
91
+ }
92
+ }
93
+
94
+ class RateLimitError extends ApiError {
95
+ retryAfter;
96
+ status = 429;
97
+ constructor(message = "Rate limited. Try again later.", retryAfter) {
98
+ super(429, message, undefined, undefined, {
99
+ retryable: true,
100
+ retryAfterSeconds: parseRetryAfter(retryAfter)
101
+ });
102
+ this.retryAfter = retryAfter;
103
+ this.name = "RateLimitError";
104
+ }
105
+ }
106
+
107
+ class ValidationError extends ApiError {
108
+ constructor(message, status, body) {
109
+ super(status, message, body, undefined, { retryable: false });
110
+ this.name = "ValidationError";
111
+ }
112
+ }
113
+ function parseRetryAfter(value) {
114
+ if (!value)
115
+ return;
116
+ const seconds = Number(value);
117
+ if (Number.isFinite(seconds) && seconds >= 0)
118
+ return seconds;
119
+ const date = Date.parse(value);
120
+ if (!Number.isNaN(date))
121
+ return Math.max(0, (date - Date.now()) / 1000);
122
+ return;
123
+ }
124
+
15
125
  class VersionConflictError extends ApiError {
16
126
  currentVersion;
17
127
  expectedVersion;
@@ -26,7 +136,13 @@ class VersionConflictError extends ApiError {
26
136
  class ByoBreakingChangeError extends ApiError {
27
137
  details;
28
138
  constructor(message, details) {
29
- super(422, message, details, "BYO_BREAKING_CHANGE");
139
+ super(422, message, details, "BYO_BREAKING_CHANGE", {
140
+ metaMessages: [
141
+ "Review and run manually on your database:",
142
+ details.plan.dropStatement,
143
+ ...details.plan.statements
144
+ ]
145
+ });
30
146
  this.name = "ByoBreakingChangeError";
31
147
  this.details = details;
32
148
  }
@@ -66,10 +182,12 @@ class BaseClient {
66
182
  baseUrl;
67
183
  apiKey;
68
184
  origin;
185
+ fetchImpl;
69
186
  constructor(options = {}) {
70
187
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
71
188
  this.apiKey = resolveApiKey(options.apiKey);
72
189
  this.origin = options.origin ?? "cli";
190
+ this.fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
73
191
  }
74
192
  static authHeaders(apiKey) {
75
193
  const headers = {
@@ -110,27 +228,27 @@ class BaseClient {
110
228
  serializedBody = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
111
229
  } catch (err) {
112
230
  const detail = err instanceof Error ? err.message : String(err);
113
- throw new ApiError(0, `Failed to serialize request body: ${detail}`);
231
+ throw new ApiError(0, `Failed to serialize request body: ${detail}`, undefined, undefined, { retryable: false });
114
232
  }
115
233
  }
116
234
  let response;
117
235
  try {
118
- response = await fetch(url, {
236
+ response = await this.fetchImpl(url, {
119
237
  method,
120
238
  headers,
121
239
  body: serializedBody
122
240
  });
123
- } catch {
124
- throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`);
241
+ } catch (err) {
242
+ throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`, undefined, undefined, { retryable: true, cause: err instanceof Error ? err : undefined });
125
243
  }
126
244
  if (!response.ok) {
127
245
  if (response.status === 401) {
128
- throw new ApiError(401, "API key invalid or expired.");
246
+ throw new AuthError;
129
247
  }
130
248
  if (response.status === 429) {
131
249
  const retryAfter = response.headers.get("Retry-After");
132
250
  const msg = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
133
- throw new ApiError(429, msg);
251
+ throw new RateLimitError(msg, retryAfter ?? undefined);
134
252
  }
135
253
  if (response.status >= 500) {
136
254
  throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
@@ -424,47 +542,14 @@ class Contracts extends BaseClient {
424
542
  }
425
543
 
426
544
  // src/streams/errors.ts
427
- class AuthError extends Error {
428
- status = 401;
429
- constructor(message = "API key invalid or expired.") {
430
- super(message);
431
- this.name = "AuthError";
432
- }
433
- }
434
-
435
- class RateLimitError extends Error {
436
- retryAfter;
437
- status = 429;
438
- constructor(message = "Rate limited. Try again later.", retryAfter) {
439
- super(message);
440
- this.retryAfter = retryAfter;
441
- this.name = "RateLimitError";
442
- }
443
- }
444
-
445
- class ValidationError extends Error {
446
- status;
447
- body;
448
- constructor(message, status, body) {
449
- super(message);
450
- this.status = status;
451
- this.body = body;
452
- this.name = "ValidationError";
453
- }
454
- }
455
-
456
- class StreamsServerError extends Error {
457
- status;
458
- body;
545
+ class StreamsServerError extends ApiError {
459
546
  constructor(message, status, body) {
460
- super(message);
461
- this.status = status;
462
- this.body = body;
547
+ super(status, message, body, undefined, { retryable: true });
463
548
  this.name = "StreamsServerError";
464
549
  }
465
550
  }
466
551
 
467
- class StreamsSignatureError extends Error {
552
+ class StreamsSignatureError extends SecondLayerError {
468
553
  constructor(message = "Streams response signature verification failed.") {
469
554
  super(message);
470
555
  this.name = "StreamsSignatureError";
@@ -494,6 +579,19 @@ var Cursor = {
494
579
  function reorgKey(reorg) {
495
580
  return `${reorg.detected_at}|${reorg.fork_point_height}|${reorg.new_canonical_tip}`;
496
581
  }
582
+ function assertFinalizedCheckpoint(returned, checkpoint) {
583
+ if (returned === null || returned === undefined)
584
+ return;
585
+ if (returned === checkpoint)
586
+ return;
587
+ const r = Cursor.parse(returned);
588
+ const c = checkpoint === null ? null : Cursor.parse(checkpoint);
589
+ const above = c === null || r.blockHeight > c.blockHeight || r.blockHeight === c.blockHeight && r.eventIndex > c.eventIndex;
590
+ if (above) {
591
+ const boundary = c ? ` ("${checkpoint}")` : " (none delivered yet)";
592
+ throw new ValidationError(`onBatch returned cursor "${returned}", above the last delivered finalized event${boundary}. With finalizedOnly, events past that point were filtered out and will be re-read once finalized — committing beyond it skips them permanently. Return ctx.cursor (or nothing); never envelope.next_cursor.`, 400);
593
+ }
594
+ }
497
595
  function batchContext(cursor, height, tipHeight) {
498
596
  return {
499
597
  cursor,
@@ -502,6 +600,41 @@ function batchContext(cursor, height, tipHeight) {
502
600
  blocksBehind: height === null ? null : Math.max(0, tipHeight - height)
503
601
  };
504
602
  }
603
+ var MAX_RETRY_AFTER_MS = 300000;
604
+ function errRetryable(err) {
605
+ if (err && typeof err === "object" && "retryable" in err) {
606
+ return err.retryable === true;
607
+ }
608
+ return err instanceof TypeError;
609
+ }
610
+ function errRetryAfterMs(err) {
611
+ if (err && typeof err === "object" && "retryAfterSeconds" in err) {
612
+ const seconds = err.retryAfterSeconds;
613
+ if (typeof seconds === "number" && Number.isFinite(seconds)) {
614
+ return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);
615
+ }
616
+ }
617
+ return;
618
+ }
619
+ async function fetchPageWithRetry(fetchPage, opts) {
620
+ const retryCount = opts.retryCount ?? 3;
621
+ const retryDelay = opts.retryDelay ?? 1000;
622
+ for (let attempt = 0;; attempt++) {
623
+ try {
624
+ return await fetchPage();
625
+ } catch (err) {
626
+ const retriesLeft = retryCount - attempt;
627
+ if (retriesLeft <= 0 || !errRetryable(err) || opts.signal?.aborted) {
628
+ throw err;
629
+ }
630
+ const delayMs = errRetryAfterMs(err) ?? retryDelay * (attempt + 1);
631
+ opts.onError?.(err, { attempt, retriesLeft, delayMs });
632
+ await opts.sleep(delayMs, opts.signal);
633
+ if (opts.signal?.aborted)
634
+ throw err;
635
+ }
636
+ }
637
+ }
505
638
  async function defaultSleep(ms, signal) {
506
639
  if (signal?.aborted)
507
640
  return;
@@ -528,7 +661,7 @@ async function consumeStreamsEvents(opts) {
528
661
  let emptyPolls = 0;
529
662
  let height = null;
530
663
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
531
- const envelope = await opts.fetchEvents({
664
+ const envelope = await fetchPageWithRetry(() => opts.fetchEvents({
532
665
  cursor,
533
666
  limit: opts.batchSize,
534
667
  types: opts.types,
@@ -537,6 +670,12 @@ async function consumeStreamsEvents(opts) {
537
670
  sender: opts.sender,
538
671
  recipient: opts.recipient,
539
672
  assetIdentifier: opts.assetIdentifier
673
+ }), {
674
+ retryCount: opts.retryCount,
675
+ retryDelay: opts.retryDelay,
676
+ onError: opts.onError,
677
+ sleep,
678
+ signal: opts.signal
540
679
  });
541
680
  pages++;
542
681
  if (!finalizedOnly && opts.onReorg) {
@@ -558,6 +697,8 @@ async function consumeStreamsEvents(opts) {
558
697
  const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
559
698
  height = emitted.at(-1)?.block_height ?? height;
560
699
  const returnedCursor = await opts.onBatch(emitted, envelope, batchContext(checkpoint, height, envelope.tip.block_height));
700
+ if (finalizedOnly)
701
+ assertFinalizedCheckpoint(returnedCursor, checkpoint);
561
702
  const nextCursor = returnedCursor ?? checkpoint;
562
703
  if (nextCursor && nextCursor !== cursor) {
563
704
  cursor = nextCursor;
@@ -665,10 +806,16 @@ async function consumeIndexFeed(opts) {
665
806
  let emptyPolls = 0;
666
807
  let height = null;
667
808
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
668
- const envelope = await opts.fetchPage({
809
+ const envelope = await fetchPageWithRetry(() => opts.fetchPage({
669
810
  cursor,
670
811
  fromHeight: cursor === null ? opts.fromHeight : undefined,
671
812
  limit: batchSize
813
+ }), {
814
+ retryCount: opts.retryCount,
815
+ retryDelay: opts.retryDelay,
816
+ onError: opts.onError,
817
+ sleep,
818
+ signal: opts.signal
672
819
  });
673
820
  pages++;
674
821
  if (!finalizedOnly && opts.onReorg) {
@@ -691,6 +838,8 @@ async function consumeIndexFeed(opts) {
691
838
  const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
692
839
  height = emitted.at(-1)?.block_height ?? height;
693
840
  const returnedCursor = await opts.onBatch(emitted, envelope, batchContext(checkpoint, height, envelope.tip.block_height));
841
+ if (finalizedOnly)
842
+ assertFinalizedCheckpoint(returnedCursor, checkpoint);
694
843
  const nextCursor = returnedCursor ?? checkpoint;
695
844
  if (nextCursor && nextCursor !== cursor) {
696
845
  cursor = nextCursor;
@@ -1555,7 +1704,10 @@ function createStreamsClient(options) {
1555
1704
  emptyBackoffMs: params.emptyBackoffMs,
1556
1705
  maxPages: params.maxPages,
1557
1706
  maxEmptyPolls: params.maxEmptyPolls,
1558
- signal: params.signal
1707
+ signal: params.signal,
1708
+ retryCount: params.retryCount,
1709
+ retryDelay: params.retryDelay,
1710
+ onError: params.onError
1559
1711
  });
1560
1712
  },
1561
1713
  stream(params = {}) {
@@ -1790,5 +1942,5 @@ export {
1790
1942
  Subgraphs
1791
1943
  };
1792
1944
 
1793
- //# debugId=C01B0960A474ED8C64756E2164756E21
1945
+ //# debugId=A36A74FD0514129964756E2164756E21
1794
1946
  //# sourceMappingURL=index.js.map