@secondlayer/sdk 16.2.0 → 16.3.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.
@@ -482,6 +482,8 @@ type StreamsEventsStreamBitcoinParams = StreamsEventsStreamCommon & {
482
482
  address?: string;
483
483
  };
484
484
  type StreamsEventsStreamParams = StreamsEventsStreamStacksParams | StreamsEventsStreamBitcoinParams;
485
+ /** Where a `subscribe` connection is in its lifecycle. */
486
+ type StreamsSubscriptionState = "connecting" | "open" | "reconnecting" | "closed";
485
487
  type StreamsEventsSubscribeCommon = {
486
488
  /** Resume strictly after this cursor; omit to live-tail from the tip. */
487
489
  fromCursor?: string | null;
@@ -495,6 +497,13 @@ type StreamsEventsSubscribeCommon = {
495
497
  */
496
498
  onError?: (err: unknown) => void;
497
499
  /**
500
+ * Lifecycle observer. `connecting` before each connection attempt, `open`
501
+ * once the server answers, `reconnecting` while backing off after a drop
502
+ * or error, `closed` once, when the subscription ends (unsubscribed,
503
+ * aborted, or stopped by an error a retry cannot fix).
504
+ */
505
+ onState?: (state: StreamsSubscriptionState) => void;
506
+ /**
498
507
  * First reconnect pause in ms (default 1000). Doubles per consecutive
499
508
  * failure up to 30 s with jitter, resets once a frame arrives, and never
500
509
  * undercuts a `Retry-After`.
@@ -1111,6 +1120,13 @@ type CreateStreamsClientOptions = {
1111
1120
  origin?: "cli" | "mcp" | "session";
1112
1121
  fetchImpl?: FetchLike2;
1113
1122
  /**
1123
+ * Give up on a request that has not connected and returned response headers
1124
+ * within this many ms (default 30000; `0` disables). Retryable: the consume
1125
+ * loops retry it and `subscribe` reconnects. Body reads are not covered, so
1126
+ * a healthy long-lived stream is never cut off.
1127
+ */
1128
+ connectTimeoutMs?: number;
1129
+ /**
1114
1130
  * Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required
1115
1131
  * to use `client.dumps`. See `GET /public/streams/dumps/manifest`.
1116
1132
  */
@@ -1310,4 +1326,4 @@ declare const Cursor: {
1310
1326
  eventIndex: number;
1311
1327
  };
1312
1328
  };
1313
- export { AuthError, ConsumerBatchContext, Cursor, DecodedEventColumns, DecodedEventRow, DecodedFtBurn, DecodedFtBurnPayload, DecodedFtMint, DecodedFtMintPayload, DecodedFtTransfer, DecodedFtTransferPayload, DecodedNftBurn, DecodedNftBurnPayload, DecodedNftMint, DecodedNftMintPayload, DecodedNftTransfer, DecodedNftTransferPayload, DecodedPrint, DecodedPrintPayload, DecodedPrintValue, DecodedStxBurn, DecodedStxBurnPayload, DecodedStxLock, DecodedStxLockPayload, DecodedStxMint, DecodedStxMintPayload, DecodedStxTransfer, DecodedStxTransferPayload, FetchLike2 as FetchLike, FtTransferEvent, FtTransferPayload2 as FtTransferPayload, NftTransferEvent, NftTransferPayload2 as NftTransferPayload, RateLimitError, RuneEtchEntry, RuneEventPayload, RuneEventType, RuneStreamsEvent, STREAMS_EVENT_TYPES, StreamsBatch, StreamsBitcoinCanonicalBlock, StreamsBitcoinTip, StreamsCanonicalBlock, StreamsClient, StreamsConsumeBitcoinParams, StreamsConsumeParams, StreamsConsumeStacksParams, StreamsDumpFile, StreamsDumps, StreamsDumpsManifest, StreamsEvent, StreamsEventPayload, StreamsEventType, StreamsEventsBitcoinListParams, StreamsEventsConsumeBitcoinParams, StreamsEventsConsumeParams, StreamsEventsConsumeResult, StreamsEventsEnvelope, StreamsEventsListEnvelope, StreamsEventsListParams, StreamsEventsStacksListParams, StreamsEventsStreamBitcoinParams, StreamsEventsStreamParams, StreamsEventsStreamStacksParams, StreamsEventsSubscribeBitcoinParams, StreamsEventsSubscribeParams, StreamsEventsSubscribeStacksParams, StreamsReorg, StreamsReorgContext, StreamsReorgsListEnvelope, StreamsReorgsListParams, StreamsServerError, StreamsSignatureError, StreamsSubscription, StreamsTip, StreamsWireEvent2 as StreamsWireEvent, ValidationError, VmStreamsEvent, createStreamsClient, decode };
1329
+ export { AuthError, ConsumerBatchContext, Cursor, DecodedEventColumns, DecodedEventRow, DecodedFtBurn, DecodedFtBurnPayload, DecodedFtMint, DecodedFtMintPayload, DecodedFtTransfer, DecodedFtTransferPayload, DecodedNftBurn, DecodedNftBurnPayload, DecodedNftMint, DecodedNftMintPayload, DecodedNftTransfer, DecodedNftTransferPayload, DecodedPrint, DecodedPrintPayload, DecodedPrintValue, DecodedStxBurn, DecodedStxBurnPayload, DecodedStxLock, DecodedStxLockPayload, DecodedStxMint, DecodedStxMintPayload, DecodedStxTransfer, DecodedStxTransferPayload, FetchLike2 as FetchLike, FtTransferEvent, FtTransferPayload2 as FtTransferPayload, NftTransferEvent, NftTransferPayload2 as NftTransferPayload, RateLimitError, RuneEtchEntry, RuneEventPayload, RuneEventType, RuneStreamsEvent, STREAMS_EVENT_TYPES, StreamsBatch, StreamsBitcoinCanonicalBlock, StreamsBitcoinTip, StreamsCanonicalBlock, StreamsClient, StreamsConsumeBitcoinParams, StreamsConsumeParams, StreamsConsumeStacksParams, StreamsDumpFile, StreamsDumps, StreamsDumpsManifest, StreamsEvent, StreamsEventPayload, StreamsEventType, StreamsEventsBitcoinListParams, StreamsEventsConsumeBitcoinParams, StreamsEventsConsumeParams, StreamsEventsConsumeResult, StreamsEventsEnvelope, StreamsEventsListEnvelope, StreamsEventsListParams, StreamsEventsStacksListParams, StreamsEventsStreamBitcoinParams, StreamsEventsStreamParams, StreamsEventsStreamStacksParams, StreamsEventsSubscribeBitcoinParams, StreamsEventsSubscribeParams, StreamsEventsSubscribeStacksParams, StreamsReorg, StreamsReorgContext, StreamsReorgsListEnvelope, StreamsReorgsListParams, StreamsServerError, StreamsSignatureError, StreamsSubscription, StreamsSubscriptionState, StreamsTip, StreamsWireEvent2 as StreamsWireEvent, ValidationError, VmStreamsEvent, createStreamsClient, decode };
@@ -247,6 +247,22 @@ function resolveAccountKey(accountKey) {
247
247
  return;
248
248
  return process.env?.SECONDLAYER_API_KEY || undefined;
249
249
  }
250
+ function resolveAccountUrl() {
251
+ const env = typeof process !== "undefined" ? process.env : undefined;
252
+ return (env?.SL_CREDITS_API_URL || env?.ARCHIVE_OPS_API_URL || HOSTED_API_URL).replace(/\/+$/, "");
253
+ }
254
+ var ACCOUNT_CREDENTIAL_RE = /^s[ks]-sl_/;
255
+ function resolveIdentity(target, options = {}) {
256
+ const url = target === "account" ? resolveAccountUrl() : resolveBaseUrl();
257
+ const envKey = target === "account" ? resolveAccountKey() : typeof process !== "undefined" ? process.env?.[INSTANCE_TOKEN_ENV] || undefined : undefined;
258
+ if (envKey && (target === "instance" || ACCOUNT_CREDENTIAL_RE.test(envKey))) {
259
+ return { source: "env", token: envKey, url };
260
+ }
261
+ if (options.session) {
262
+ return { source: "session", token: options.session, url };
263
+ }
264
+ return { source: "none", token: undefined, url };
265
+ }
250
266
  function seg(value) {
251
267
  return encodeURIComponent(String(value));
252
268
  }
@@ -749,13 +765,70 @@ async function consumeStreamsEvents(opts) {
749
765
  }
750
766
  return { cursor, pages, emptyPolls };
751
767
  }
768
+ async function* pullFromLoop(start, external) {
769
+ const controller = new AbortController;
770
+ if (external) {
771
+ if (external.aborted)
772
+ controller.abort();
773
+ else
774
+ external.addEventListener("abort", () => controller.abort(), {
775
+ once: true
776
+ });
777
+ }
778
+ const state = {
779
+ delivered: null,
780
+ held: null,
781
+ finished: false,
782
+ failure: null,
783
+ wake: null
784
+ };
785
+ const notify = () => {
786
+ const resolve = state.wake;
787
+ state.wake = null;
788
+ resolve?.();
789
+ };
790
+ const loop = start((value) => new Promise((release) => {
791
+ state.delivered = { value, release };
792
+ notify();
793
+ }), controller.signal).then(() => {
794
+ state.finished = true;
795
+ }, (error) => {
796
+ state.failure = { error };
797
+ state.finished = true;
798
+ }).finally(notify);
799
+ try {
800
+ while (true) {
801
+ if (state.delivered) {
802
+ const { value, release } = state.delivered;
803
+ state.delivered = null;
804
+ state.held = release;
805
+ yield value;
806
+ state.held = null;
807
+ release();
808
+ continue;
809
+ }
810
+ if (state.finished)
811
+ break;
812
+ await new Promise((resolve) => {
813
+ state.wake = resolve;
814
+ if (state.delivered || state.finished)
815
+ notify();
816
+ });
817
+ }
818
+ if (state.failure)
819
+ throw state.failure.error;
820
+ } finally {
821
+ controller.abort();
822
+ state.held?.();
823
+ state.delivered?.release();
824
+ await loop;
825
+ }
826
+ }
752
827
  async function* iterateStreamsBatches(opts) {
753
- const sleep = opts.sleep ?? defaultSleep;
754
- let cursor = opts.fromCursor ?? null;
755
- while (!opts.signal?.aborted) {
756
- const envelope = await opts.fetchEvents({
757
- cursor,
758
- limit: opts.batchSize,
828
+ yield* pullFromLoop(async (deliver, signal) => {
829
+ const result = await consumeStreamsEvents({
830
+ fromCursor: opts.fromCursor,
831
+ batchSize: opts.batchSize,
759
832
  chain: opts.chain,
760
833
  types: opts.types,
761
834
  notTypes: opts.notTypes,
@@ -765,38 +838,41 @@ async function* iterateStreamsBatches(opts) {
765
838
  assetIdentifier: opts.assetIdentifier,
766
839
  rune: opts.rune,
767
840
  address: opts.address,
768
- filters: opts.filters
841
+ filters: opts.filters,
842
+ fetchEvents: opts.fetchEvents,
843
+ sleep: opts.sleep,
844
+ emptyBackoffMs: opts.intervalMs,
845
+ signal,
846
+ onBatch: async (events, ctx) => {
847
+ if (events.length === 0 && ctx.reorgs.length === 0)
848
+ return;
849
+ await deliver({
850
+ events,
851
+ cursor: ctx.cursor,
852
+ tip: ctx.tip,
853
+ reorgs: [...ctx.reorgs]
854
+ });
855
+ }
769
856
  });
770
- const checkpoint = envelope.next_cursor ?? cursor;
771
- if (envelope.events.length > 0 || envelope.reorgs.length > 0) {
772
- yield {
773
- events: envelope.events,
774
- cursor: checkpoint,
775
- tip: envelope.tip,
776
- reorgs: envelope.reorgs
777
- };
778
- }
779
- const advanced = checkpoint !== null && checkpoint !== cursor;
780
- cursor = checkpoint;
781
- if (!advanced && envelope.events.length === 0) {
782
- if (opts.signal?.aborted)
783
- return;
784
- await sleep(opts.intervalMs, opts.signal);
785
- }
857
+ assertNotStalled(result, signal);
858
+ }, opts.signal);
859
+ }
860
+ function assertNotStalled(result, signal, bounds = {}) {
861
+ if (signal.aborted)
862
+ return;
863
+ if (bounds.maxPages !== undefined && (bounds.pages ?? 0) >= bounds.maxPages) {
864
+ return;
786
865
  }
866
+ if (bounds.maxEmptyPolls !== undefined && result.emptyPolls >= bounds.maxEmptyPolls) {
867
+ return;
868
+ }
869
+ throw new StreamsServerError(`Streams returned events at cursor ${result.cursor ?? "(start)"} but the cursor did not advance, so the stream stopped instead of re-reading the same page.`, 0, undefined, "CURSOR_STALLED");
787
870
  }
788
871
  async function* streamStreamsEvents(opts) {
789
- const sleep = opts.sleep ?? defaultSleep;
790
- const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
791
- const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
792
- const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
793
- let cursor = opts.fromCursor ?? null;
794
- let pages = 0;
795
- let emptyPolls = 0;
796
- while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
797
- const envelope = await opts.fetchEvents({
798
- cursor,
799
- limit: opts.batchSize,
872
+ yield* pullFromLoop(async (deliver, signal) => {
873
+ const result = await consumeStreamsEvents({
874
+ fromCursor: opts.fromCursor,
875
+ batchSize: opts.batchSize,
800
876
  chain: opts.chain,
801
877
  types: opts.types,
802
878
  notTypes: opts.notTypes,
@@ -806,29 +882,27 @@ async function* streamStreamsEvents(opts) {
806
882
  assetIdentifier: opts.assetIdentifier,
807
883
  rune: opts.rune,
808
884
  address: opts.address,
809
- filters: opts.filters
885
+ filters: opts.filters,
886
+ fetchEvents: opts.fetchEvents,
887
+ sleep: opts.sleep,
888
+ emptyBackoffMs: opts.emptyBackoffMs,
889
+ maxPages: opts.maxPages,
890
+ maxEmptyPolls: opts.maxEmptyPolls,
891
+ signal,
892
+ onBatch: async (events) => {
893
+ for (const event of events) {
894
+ if (signal.aborted)
895
+ return;
896
+ await deliver(event);
897
+ }
898
+ }
810
899
  });
811
- pages++;
812
- for (const event of envelope.events) {
813
- if (opts.signal?.aborted)
814
- return;
815
- yield event;
816
- }
817
- const nextCursor = envelope.next_cursor;
818
- if (nextCursor && nextCursor !== cursor) {
819
- cursor = nextCursor;
820
- emptyPolls = 0;
821
- continue;
822
- }
823
- if (envelope.events.length === 0) {
824
- emptyPolls++;
825
- if (emptyPolls >= maxEmptyPolls || pages >= maxPages)
826
- return;
827
- await sleep(emptyBackoffMs, opts.signal);
828
- continue;
829
- }
830
- return;
831
- }
900
+ assertNotStalled(result, signal, {
901
+ maxPages: opts.maxPages,
902
+ maxEmptyPolls: opts.maxEmptyPolls,
903
+ pages: result.pages
904
+ });
905
+ }, opts.signal);
832
906
  }
833
907
 
834
908
  // src/streams/decode.ts
@@ -1061,8 +1135,14 @@ function subscribeStreamsEvents(opts) {
1061
1135
  const baseDelayMs = opts.reconnectDelayMs ?? params.reconnectDelayMs ?? 1000;
1062
1136
  const staleAfterMs = opts.staleAfterMs ?? params.staleAfterMs ?? SUBSCRIBE_DEFAULT_STALE_AFTER_MS;
1063
1137
  let attempt = 0;
1138
+ const emit = (state) => {
1139
+ try {
1140
+ params.onState?.(state);
1141
+ } catch {}
1142
+ };
1064
1143
  const run = async () => {
1065
1144
  while (!controller.signal.aborted) {
1145
+ emit("connecting");
1066
1146
  try {
1067
1147
  const url = `${opts.baseUrl}/v1/streams/events/stream${buildQuery({
1068
1148
  chain: params.chain === "bitcoin" ? "bitcoin" : undefined,
@@ -1084,6 +1164,7 @@ function subscribeStreamsEvents(opts) {
1084
1164
  fetchImpl: opts.fetchImpl,
1085
1165
  staleAfterMs,
1086
1166
  mapError: mapStreamsError,
1167
+ onOpen: () => emit("open"),
1087
1168
  onFrame: async (frame) => {
1088
1169
  attempt = 0;
1089
1170
  if (frame.event === "ping" || !frame.data)
@@ -1112,6 +1193,8 @@ function subscribeStreamsEvents(opts) {
1112
1193
  cursor = parsed.event.cursor ?? cursor;
1113
1194
  }
1114
1195
  });
1196
+ if (!controller.signal.aborted)
1197
+ emit("reconnecting");
1115
1198
  await sleep(reconnectDelay(attempt, baseDelayMs, undefined, opts.random), controller.signal);
1116
1199
  attempt += 1;
1117
1200
  } catch (err) {
@@ -1120,13 +1203,14 @@ function subscribeStreamsEvents(opts) {
1120
1203
  params.onError?.(err);
1121
1204
  if (isTerminal(err))
1122
1205
  throw err;
1206
+ emit("reconnecting");
1123
1207
  const retryAfterSeconds = err.retryAfterSeconds;
1124
1208
  await sleep(reconnectDelay(attempt, baseDelayMs, retryAfterSeconds, opts.random), controller.signal);
1125
1209
  attempt += 1;
1126
1210
  }
1127
1211
  }
1128
1212
  };
1129
- const done = run();
1213
+ const done = run().finally(() => emit("closed"));
1130
1214
  done.catch(() => {});
1131
1215
  const unsubscribe = () => controller.abort();
1132
1216
  return Object.assign(unsubscribe, { done });
@@ -1144,6 +1228,7 @@ async function readSse(opts) {
1144
1228
  if (!res.body) {
1145
1229
  throw new StreamsServerError("Streams SSE response has no body.", 0);
1146
1230
  }
1231
+ opts.onOpen?.();
1147
1232
  for await (const frame of parseSseFrames(res.body, opts.signal, opts.staleAfterMs)) {
1148
1233
  await opts.onFrame(frame);
1149
1234
  }
@@ -1219,6 +1304,33 @@ function parseFrame(raw) {
1219
1304
  `) : undefined };
1220
1305
  }
1221
1306
 
1307
+ // src/streams/timeout-fetch.ts
1308
+ var STREAMS_CONNECT_TIMEOUT_MS = 30000;
1309
+ function withConnectTimeout(fetchImpl, timeoutMs) {
1310
+ if (!(timeoutMs > 0) || !Number.isFinite(timeoutMs))
1311
+ return fetchImpl;
1312
+ return async (input, init) => {
1313
+ const callerSignal = init?.signal ?? undefined;
1314
+ const timeout = new AbortController;
1315
+ let timedOut = false;
1316
+ const timer = setTimeout(() => {
1317
+ timedOut = true;
1318
+ timeout.abort();
1319
+ }, timeoutMs);
1320
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
1321
+ try {
1322
+ return await fetchImpl(input, { ...init, signal });
1323
+ } catch (err) {
1324
+ if (timedOut && !callerSignal?.aborted) {
1325
+ throw new StreamsServerError(`No response headers within ${timeoutMs}ms. Retryable.`, 0, undefined, "REQUEST_TIMEOUT");
1326
+ }
1327
+ throw err;
1328
+ } finally {
1329
+ clearTimeout(timer);
1330
+ }
1331
+ };
1332
+ }
1333
+
1222
1334
  // src/streams/client.ts
1223
1335
  function cursorTuple(cursor) {
1224
1336
  if (!cursor)
@@ -1264,7 +1376,7 @@ function createStreamsClient(options) {
1264
1376
  "x-sl-origin": origin,
1265
1377
  ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
1266
1378
  });
1267
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
1379
+ const fetchImpl = withConnectTimeout(options.fetchImpl ?? ((input, init) => fetch(input, init)), options.connectTimeoutMs ?? STREAMS_CONNECT_TIMEOUT_MS);
1268
1380
  const verify = options.verify;
1269
1381
  const verifyMode = verify === false ? "off" : verify === undefined ? "lenient" : "strict";
1270
1382
  if (verify === true && baseUrl.startsWith("http:") && !isLoopbackUrl(baseUrl)) {
@@ -1575,5 +1687,5 @@ export {
1575
1687
  decode
1576
1688
  };
1577
1689
 
1578
- //# debugId=C74383F8F8E34A3564756E2164756E21
1690
+ //# debugId=8BCAE1BCFFCE4BBA64756E2164756E21
1579
1691
  //# sourceMappingURL=index.js.map