@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.
@@ -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
  }
@@ -69,10 +185,12 @@ class BaseClient {
69
185
  baseUrl;
70
186
  apiKey;
71
187
  origin;
188
+ fetchImpl;
72
189
  constructor(options = {}) {
73
190
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
74
191
  this.apiKey = resolveApiKey(options.apiKey);
75
192
  this.origin = options.origin ?? "cli";
193
+ this.fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
76
194
  }
77
195
  static authHeaders(apiKey) {
78
196
  const headers = {
@@ -113,27 +231,27 @@ class BaseClient {
113
231
  serializedBody = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
114
232
  } catch (err) {
115
233
  const detail = err instanceof Error ? err.message : String(err);
116
- 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 });
117
235
  }
118
236
  }
119
237
  let response;
120
238
  try {
121
- response = await fetch(url, {
239
+ response = await this.fetchImpl(url, {
122
240
  method,
123
241
  headers,
124
242
  body: serializedBody
125
243
  });
126
- } catch {
127
- 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 });
128
246
  }
129
247
  if (!response.ok) {
130
248
  if (response.status === 401) {
131
- throw new ApiError(401, "API key invalid or expired.");
249
+ throw new AuthError;
132
250
  }
133
251
  if (response.status === 429) {
134
252
  const retryAfter = response.headers.get("Retry-After");
135
253
  const msg = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
136
- throw new ApiError(429, msg);
254
+ throw new RateLimitError(msg, retryAfter ?? undefined);
137
255
  }
138
256
  if (response.status >= 500) {
139
257
  throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
@@ -168,47 +286,14 @@ class BaseClient {
168
286
  }
169
287
 
170
288
  // src/streams/errors.ts
171
- class AuthError extends Error {
172
- status = 401;
173
- constructor(message = "API key invalid or expired.") {
174
- super(message);
175
- this.name = "AuthError";
176
- }
177
- }
178
-
179
- class RateLimitError extends Error {
180
- retryAfter;
181
- status = 429;
182
- constructor(message = "Rate limited. Try again later.", retryAfter) {
183
- super(message);
184
- this.retryAfter = retryAfter;
185
- this.name = "RateLimitError";
186
- }
187
- }
188
-
189
- class ValidationError extends Error {
190
- status;
191
- body;
192
- constructor(message, status, body) {
193
- super(message);
194
- this.status = status;
195
- this.body = body;
196
- this.name = "ValidationError";
197
- }
198
- }
199
-
200
- class StreamsServerError extends Error {
201
- status;
202
- body;
289
+ class StreamsServerError extends ApiError {
203
290
  constructor(message, status, body) {
204
- super(message);
205
- this.status = status;
206
- this.body = body;
291
+ super(status, message, body, undefined, { retryable: true });
207
292
  this.name = "StreamsServerError";
208
293
  }
209
294
  }
210
295
 
211
- class StreamsSignatureError extends Error {
296
+ class StreamsSignatureError extends SecondLayerError {
212
297
  constructor(message = "Streams response signature verification failed.") {
213
298
  super(message);
214
299
  this.name = "StreamsSignatureError";
@@ -238,6 +323,19 @@ var Cursor = {
238
323
  function reorgKey(reorg) {
239
324
  return `${reorg.detected_at}|${reorg.fork_point_height}|${reorg.new_canonical_tip}`;
240
325
  }
326
+ function assertFinalizedCheckpoint(returned, checkpoint) {
327
+ if (returned === null || returned === undefined)
328
+ return;
329
+ if (returned === checkpoint)
330
+ return;
331
+ const r = Cursor.parse(returned);
332
+ const c = checkpoint === null ? null : Cursor.parse(checkpoint);
333
+ const above = c === null || r.blockHeight > c.blockHeight || r.blockHeight === c.blockHeight && r.eventIndex > c.eventIndex;
334
+ if (above) {
335
+ const boundary = c ? ` ("${checkpoint}")` : " (none delivered yet)";
336
+ 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);
337
+ }
338
+ }
241
339
  function batchContext(cursor, height, tipHeight) {
242
340
  return {
243
341
  cursor,
@@ -246,6 +344,41 @@ function batchContext(cursor, height, tipHeight) {
246
344
  blocksBehind: height === null ? null : Math.max(0, tipHeight - height)
247
345
  };
248
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
+ }
249
382
  async function defaultSleep(ms, signal) {
250
383
  if (signal?.aborted)
251
384
  return;
@@ -272,7 +405,7 @@ async function consumeStreamsEvents(opts) {
272
405
  let emptyPolls = 0;
273
406
  let height = null;
274
407
  while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
275
- const envelope = await opts.fetchEvents({
408
+ const envelope = await fetchPageWithRetry(() => opts.fetchEvents({
276
409
  cursor,
277
410
  limit: opts.batchSize,
278
411
  types: opts.types,
@@ -281,6 +414,12 @@ async function consumeStreamsEvents(opts) {
281
414
  sender: opts.sender,
282
415
  recipient: opts.recipient,
283
416
  assetIdentifier: opts.assetIdentifier
417
+ }), {
418
+ retryCount: opts.retryCount,
419
+ retryDelay: opts.retryDelay,
420
+ onError: opts.onError,
421
+ sleep,
422
+ signal: opts.signal
284
423
  });
285
424
  pages++;
286
425
  if (!finalizedOnly && opts.onReorg) {
@@ -302,6 +441,8 @@ async function consumeStreamsEvents(opts) {
302
441
  const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
303
442
  height = emitted.at(-1)?.block_height ?? height;
304
443
  const returnedCursor = await opts.onBatch(emitted, envelope, batchContext(checkpoint, height, envelope.tip.block_height));
444
+ if (finalizedOnly)
445
+ assertFinalizedCheckpoint(returnedCursor, checkpoint);
305
446
  const nextCursor = returnedCursor ?? checkpoint;
306
447
  if (nextCursor && nextCursor !== cursor) {
307
448
  cursor = nextCursor;
@@ -785,7 +926,10 @@ function createStreamsClient(options) {
785
926
  emptyBackoffMs: params.emptyBackoffMs,
786
927
  maxPages: params.maxPages,
787
928
  maxEmptyPolls: params.maxEmptyPolls,
788
- signal: params.signal
929
+ signal: params.signal,
930
+ retryCount: params.retryCount,
931
+ retryDelay: params.retryDelay,
932
+ onError: params.onError
789
933
  });
790
934
  },
791
935
  stream(params = {}) {
@@ -1181,5 +1325,5 @@ export {
1181
1325
  AuthError
1182
1326
  };
1183
1327
 
1184
- //# debugId=2B52BCE7E3B3A40964756E2164756E21
1328
+ //# debugId=9793637197EFD6F364756E2164756E21
1185
1329
  //# sourceMappingURL=index.js.map