@secondlayer/sdk 6.36.0 → 6.37.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.
@@ -274,6 +274,18 @@ type StreamsEventsConsumeParams = {
274
274
  * stale rows from an orphaned fork are left in place).
275
275
  */
276
276
  onReorg?: (reorg: StreamsReorg, ctx: StreamsReorgContext) => Promise<void> | void
277
+ /** Page-fetch retries after the first failure (429/5xx/network only —
278
+ * 4xx and handler throws always propagate). Default 3; `0` disables. */
279
+ retryCount?: number
280
+ /** Base retry delay in ms; the n-th retry waits `retryDelay * n`. A server
281
+ * `Retry-After` overrides it. Default 1000. */
282
+ retryDelay?: number
283
+ /** Void observer, called before each retry sleep (metrics/logging). */
284
+ onError?: (err: unknown, ctx: {
285
+ attempt: number
286
+ retriesLeft: number
287
+ delayMs: number
288
+ }) => void
277
289
  emptyBackoffMs?: number
278
290
  maxPages?: number
279
291
  maxEmptyPolls?: number
@@ -500,27 +512,105 @@ type CreateStreamsClientOptions = {
500
512
  verifyDumpsManifest?: boolean
501
513
  };
502
514
  declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
503
- declare class AuthError extends Error {
504
- readonly status = 401;
515
+ /** Options accepted by every error in the family. */
516
+ interface SecondLayerErrorOptions {
517
+ cause?: Error;
518
+ details?: string;
519
+ /** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`, `BYO_BREAKING_CHANGE`). */
520
+ code?: string;
521
+ /** Docs page that explains this failure and its fix. */
522
+ docsUrl?: string;
523
+ /** Extra context lines appended to the message (DDL to run, next steps). */
524
+ metaMessages?: string[];
525
+ /** Whether retrying the SAME request can succeed (429/5xx/network). */
526
+ retryable?: boolean;
527
+ /** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */
528
+ retryAfterSeconds?: number;
529
+ }
530
+ /**
531
+ * Root of the SDK error family. Implements the same protocol as `BaseError`
532
+ * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed
533
+ * chain read and a failed platform read serialize and introspect identically.
534
+ * It intentionally does NOT extend that class: `BaseError.details` is a
535
+ * `string`, while {@link ByoBreakingChangeError.details} is the documented
536
+ * structured migration plan — and cross-package `instanceof` is unreliable
537
+ * under per-package bundling anyway (match on `code`/`name` across packages).
538
+ *
539
+ * `retryable` is the signal the consume loops act on: 429/5xx/network are
540
+ * retryable, 4xx and body-serialization failures are not.
541
+ */
542
+ declare class SecondLayerError extends Error {
543
+ name: string;
544
+ /** The one-line failure, without the appended context blocks. */
545
+ shortMessage: string;
546
+ code?: string;
547
+ docsUrl?: string;
548
+ metaMessages?: string[];
549
+ retryable: boolean;
550
+ retryAfterSeconds?: number;
551
+ constructor(shortMessage: string, options?: SecondLayerErrorOptions);
552
+ /**
553
+ * Walk the cause chain. With a predicate, returns the first error matching
554
+ * it (or `null`); without one, returns the deepest cause. Mirrors viem's
555
+ * `error.walk()` so cross-library error handling reads the same.
556
+ */
557
+ walk(fn?: (err: unknown) => boolean): unknown;
558
+ toJSON(): {
559
+ name: string
560
+ message: string
561
+ shortMessage: string
562
+ cause: string | undefined
563
+ code: string | undefined
564
+ docsUrl: string | undefined
565
+ retryable: boolean
566
+ retryAfterSeconds: number | undefined
567
+ };
568
+ }
569
+ /**
570
+ * Error thrown by {@link SecondLayer} when an API request fails.
571
+ * Includes the HTTP status code for programmatic error handling.
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * try {
576
+ * await client.subgraphs.get("my-subgraph");
577
+ * } catch (err) {
578
+ * if (err instanceof ApiError && err.status === 404) {
579
+ * console.log("Subgraph not found");
580
+ * }
581
+ * }
582
+ * ```
583
+ */
584
+ declare class ApiError extends SecondLayerError {
585
+ /** HTTP status code (0 for network errors). */
586
+ status: number;
587
+ /** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */
588
+ body?: unknown;
589
+ constructor(status: number, message: string, body?: unknown, code?: string, options?: SecondLayerErrorOptions);
590
+ }
591
+ /** Thrown on a 401 — by both the platform clients and Streams. */
592
+ declare class AuthError extends ApiError {
593
+ readonly status: 401;
505
594
  constructor(message?: string);
506
595
  }
507
- declare class RateLimitError extends Error {
596
+ /** Thrown on a 429 — by both the platform clients and Streams. `retryable`,
597
+ * with `retryAfterSeconds` parsed from the `Retry-After` header when sent. */
598
+ declare class RateLimitError extends ApiError {
599
+ /** Raw `Retry-After` header value (seconds or HTTP-date). */
508
600
  readonly retryAfter?: string;
509
- readonly status = 429;
601
+ readonly status: 429;
510
602
  constructor(message?: string, retryAfter?: string);
511
603
  }
512
- declare class ValidationError extends Error {
513
- readonly status: number;
514
- readonly body?: unknown;
604
+ /** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried. */
605
+ declare class ValidationError extends ApiError {
515
606
  constructor(message: string, status: number, body?: unknown);
516
607
  }
517
- declare class StreamsServerError extends Error {
518
- readonly status: number;
519
- readonly body?: unknown;
608
+ /** Thrown on a 5xx from the Streams API. `retryable`. */
609
+ declare class StreamsServerError extends ApiError {
520
610
  constructor(message: string, status: number, body?: unknown);
521
611
  }
522
612
  /** Thrown when response signature verification is enabled and fails. */
523
- declare class StreamsSignatureError extends Error {
613
+ declare class StreamsSignatureError extends SecondLayerError {
524
614
  constructor(message?: string);
525
615
  }
526
616
  type FtTransferEvent = Extract<StreamsEvent, {
@@ -2,19 +2,129 @@
2
2
  import { ed25519 as ed255192 } from "@secondlayer/shared";
3
3
 
4
4
  // src/errors.ts
5
- class ApiError extends Error {
5
+ var DOCS_BASE = "https://www.secondlayer.tools";
6
+
7
+ class SecondLayerError extends Error {
8
+ name = "SecondLayerError";
9
+ shortMessage;
10
+ code;
11
+ docsUrl;
12
+ metaMessages;
13
+ retryable;
14
+ retryAfterSeconds;
15
+ constructor(shortMessage, options = {}) {
16
+ const detailBlocks = [
17
+ ...options.metaMessages ?? [],
18
+ ...options.details ? [options.details] : [],
19
+ ...options.docsUrl ? [`Docs: ${options.docsUrl}`] : []
20
+ ];
21
+ const message = [
22
+ shortMessage,
23
+ detailBlocks.length > 0 ? `
24
+ ${detailBlocks.join(`
25
+ `)}` : ""
26
+ ].join("");
27
+ super(message, options.cause ? { cause: options.cause } : undefined);
28
+ this.shortMessage = shortMessage;
29
+ this.code = options.code;
30
+ this.docsUrl = options.docsUrl;
31
+ this.metaMessages = options.metaMessages;
32
+ this.retryable = options.retryable ?? false;
33
+ this.retryAfterSeconds = options.retryAfterSeconds;
34
+ }
35
+ walk(fn) {
36
+ let err = this;
37
+ let last = this;
38
+ while (err != null) {
39
+ if (fn?.(err))
40
+ return err;
41
+ last = err;
42
+ err = err.cause;
43
+ }
44
+ return fn ? null : last;
45
+ }
46
+ toJSON() {
47
+ return {
48
+ name: this.name,
49
+ message: this.message,
50
+ shortMessage: this.shortMessage,
51
+ cause: this.cause instanceof Error ? this.cause.message : undefined,
52
+ code: this.code,
53
+ docsUrl: this.docsUrl,
54
+ retryable: this.retryable,
55
+ retryAfterSeconds: this.retryAfterSeconds
56
+ };
57
+ }
58
+ }
59
+ function statusRetryable(status) {
60
+ return status === 429 || status >= 500;
61
+ }
62
+ function docsFor(status, code) {
63
+ if (code === "UPGRADE_REQUIRED" || code === "PLAN_REQUIRED" || status === 402)
64
+ return `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;
65
+ if (status === 429 || status === 401)
66
+ return `${DOCS_BASE}/docs/authentication`;
67
+ return;
68
+ }
69
+
70
+ class ApiError extends SecondLayerError {
6
71
  status;
7
72
  body;
8
- code;
9
- constructor(status, message, body, code) {
10
- super(message);
73
+ constructor(status, message, body, code, options = {}) {
74
+ super(message, {
75
+ code,
76
+ retryable: options.retryable ?? statusRetryable(status),
77
+ docsUrl: options.docsUrl ?? docsFor(status, code),
78
+ ...options.cause ? { cause: options.cause } : {},
79
+ ...options.details ? { details: options.details } : {},
80
+ ...options.metaMessages ? { metaMessages: options.metaMessages } : {},
81
+ ...options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}
82
+ });
11
83
  this.status = status;
12
84
  this.body = body;
13
- this.code = code;
14
85
  this.name = "ApiError";
15
86
  }
16
87
  }
17
88
 
89
+ class AuthError extends ApiError {
90
+ status = 401;
91
+ constructor(message = "API key invalid or expired.") {
92
+ super(401, message);
93
+ this.name = "AuthError";
94
+ }
95
+ }
96
+
97
+ class RateLimitError extends ApiError {
98
+ retryAfter;
99
+ status = 429;
100
+ constructor(message = "Rate limited. Try again later.", retryAfter) {
101
+ super(429, message, undefined, undefined, {
102
+ retryable: true,
103
+ retryAfterSeconds: parseRetryAfter(retryAfter)
104
+ });
105
+ this.retryAfter = retryAfter;
106
+ this.name = "RateLimitError";
107
+ }
108
+ }
109
+
110
+ class ValidationError extends ApiError {
111
+ constructor(message, status, body) {
112
+ super(status, message, body, undefined, { retryable: false });
113
+ this.name = "ValidationError";
114
+ }
115
+ }
116
+ function parseRetryAfter(value) {
117
+ if (!value)
118
+ return;
119
+ const seconds = Number(value);
120
+ if (Number.isFinite(seconds) && seconds >= 0)
121
+ return seconds;
122
+ const date = Date.parse(value);
123
+ if (!Number.isNaN(date))
124
+ return Math.max(0, (date - Date.now()) / 1000);
125
+ return;
126
+ }
127
+
18
128
  class VersionConflictError extends ApiError {
19
129
  currentVersion;
20
130
  expectedVersion;
@@ -29,7 +139,13 @@ class VersionConflictError extends ApiError {
29
139
  class ByoBreakingChangeError extends ApiError {
30
140
  details;
31
141
  constructor(message, details) {
32
- super(422, message, details, "BYO_BREAKING_CHANGE");
142
+ super(422, message, details, "BYO_BREAKING_CHANGE", {
143
+ metaMessages: [
144
+ "Review and run manually on your database:",
145
+ details.plan.dropStatement,
146
+ ...details.plan.statements
147
+ ]
148
+ });
33
149
  this.name = "ByoBreakingChangeError";
34
150
  this.details = details;
35
151
  }
@@ -115,7 +231,7 @@ class BaseClient {
115
231
  serializedBody = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
116
232
  } catch (err) {
117
233
  const detail = err instanceof Error ? err.message : String(err);
118
- throw new ApiError(0, `Failed to serialize request body: ${detail}`);
234
+ throw new ApiError(0, `Failed to serialize request body: ${detail}`, undefined, undefined, { retryable: false });
119
235
  }
120
236
  }
121
237
  let response;
@@ -125,17 +241,17 @@ class BaseClient {
125
241
  headers,
126
242
  body: serializedBody
127
243
  });
128
- } catch {
129
- throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`);
244
+ } catch (err) {
245
+ 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 });
130
246
  }
131
247
  if (!response.ok) {
132
248
  if (response.status === 401) {
133
- throw new ApiError(401, "API key invalid or expired.");
249
+ throw new AuthError;
134
250
  }
135
251
  if (response.status === 429) {
136
252
  const retryAfter = response.headers.get("Retry-After");
137
253
  const msg = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
138
- throw new ApiError(429, msg);
254
+ throw new RateLimitError(msg, retryAfter ?? undefined);
139
255
  }
140
256
  if (response.status >= 500) {
141
257
  throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
@@ -170,47 +286,14 @@ class BaseClient {
170
286
  }
171
287
 
172
288
  // src/streams/errors.ts
173
- class AuthError extends Error {
174
- status = 401;
175
- constructor(message = "API key invalid or expired.") {
176
- super(message);
177
- this.name = "AuthError";
178
- }
179
- }
180
-
181
- class RateLimitError extends Error {
182
- retryAfter;
183
- status = 429;
184
- constructor(message = "Rate limited. Try again later.", retryAfter) {
185
- super(message);
186
- this.retryAfter = retryAfter;
187
- this.name = "RateLimitError";
188
- }
189
- }
190
-
191
- class ValidationError extends Error {
192
- status;
193
- body;
194
- constructor(message, status, body) {
195
- super(message);
196
- this.status = status;
197
- this.body = body;
198
- this.name = "ValidationError";
199
- }
200
- }
201
-
202
- class StreamsServerError extends Error {
203
- status;
204
- body;
289
+ class StreamsServerError extends ApiError {
205
290
  constructor(message, status, body) {
206
- super(message);
207
- this.status = status;
208
- this.body = body;
291
+ super(status, message, body, undefined, { retryable: true });
209
292
  this.name = "StreamsServerError";
210
293
  }
211
294
  }
212
295
 
213
- class StreamsSignatureError extends Error {
296
+ class StreamsSignatureError extends SecondLayerError {
214
297
  constructor(message = "Streams response signature verification failed.") {
215
298
  super(message);
216
299
  this.name = "StreamsSignatureError";
@@ -261,6 +344,41 @@ function batchContext(cursor, height, tipHeight) {
261
344
  blocksBehind: height === null ? null : Math.max(0, tipHeight - height)
262
345
  };
263
346
  }
347
+ var MAX_RETRY_AFTER_MS = 300000;
348
+ function errRetryable(err) {
349
+ if (err && typeof err === "object" && "retryable" in err) {
350
+ return err.retryable === true;
351
+ }
352
+ return err instanceof TypeError;
353
+ }
354
+ function errRetryAfterMs(err) {
355
+ if (err && typeof err === "object" && "retryAfterSeconds" in err) {
356
+ const seconds = err.retryAfterSeconds;
357
+ if (typeof seconds === "number" && Number.isFinite(seconds)) {
358
+ return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);
359
+ }
360
+ }
361
+ return;
362
+ }
363
+ async function fetchPageWithRetry(fetchPage, opts) {
364
+ const retryCount = opts.retryCount ?? 3;
365
+ const retryDelay = opts.retryDelay ?? 1000;
366
+ for (let attempt = 0;; attempt++) {
367
+ try {
368
+ return await fetchPage();
369
+ } catch (err) {
370
+ const retriesLeft = retryCount - attempt;
371
+ if (retriesLeft <= 0 || !errRetryable(err) || opts.signal?.aborted) {
372
+ throw err;
373
+ }
374
+ const delayMs = errRetryAfterMs(err) ?? retryDelay * (attempt + 1);
375
+ opts.onError?.(err, { attempt, retriesLeft, delayMs });
376
+ await opts.sleep(delayMs, opts.signal);
377
+ if (opts.signal?.aborted)
378
+ throw err;
379
+ }
380
+ }
381
+ }
264
382
  async function defaultSleep(ms, signal) {
265
383
  if (signal?.aborted)
266
384
  return;
@@ -287,7 +405,7 @@ async function consumeStreamsEvents(opts) {
287
405
  let emptyPolls = 0;
288
406
  let height = null;
289
407
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
290
- const envelope = await opts.fetchEvents({
408
+ const envelope = await fetchPageWithRetry(() => opts.fetchEvents({
291
409
  cursor,
292
410
  limit: opts.batchSize,
293
411
  types: opts.types,
@@ -296,6 +414,12 @@ async function consumeStreamsEvents(opts) {
296
414
  sender: opts.sender,
297
415
  recipient: opts.recipient,
298
416
  assetIdentifier: opts.assetIdentifier
417
+ }), {
418
+ retryCount: opts.retryCount,
419
+ retryDelay: opts.retryDelay,
420
+ onError: opts.onError,
421
+ sleep,
422
+ signal: opts.signal
299
423
  });
300
424
  pages++;
301
425
  if (!finalizedOnly && opts.onReorg) {
@@ -802,7 +926,10 @@ function createStreamsClient(options) {
802
926
  emptyBackoffMs: params.emptyBackoffMs,
803
927
  maxPages: params.maxPages,
804
928
  maxEmptyPolls: params.maxEmptyPolls,
805
- signal: params.signal
929
+ signal: params.signal,
930
+ retryCount: params.retryCount,
931
+ retryDelay: params.retryDelay,
932
+ onError: params.onError
806
933
  });
807
934
  },
808
935
  stream(params = {}) {
@@ -1198,5 +1325,5 @@ export {
1198
1325
  AuthError
1199
1326
  };
1200
1327
 
1201
- //# debugId=8F516D58E206D64764756E2164756E21
1328
+ //# debugId=9793637197EFD6F364756E2164756E21
1202
1329
  //# sourceMappingURL=index.js.map