@secondlayer/sdk 16.2.1 → 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.
@@ -963,6 +963,8 @@ type StreamsEventsStreamBitcoinParams = StreamsEventsStreamCommon & {
963
963
  address?: string;
964
964
  };
965
965
  type StreamsEventsStreamParams = StreamsEventsStreamStacksParams | StreamsEventsStreamBitcoinParams;
966
+ /** Where a `subscribe` connection is in its lifecycle. */
967
+ type StreamsSubscriptionState = "connecting" | "open" | "reconnecting" | "closed";
966
968
  type StreamsEventsSubscribeCommon = {
967
969
  /** Resume strictly after this cursor; omit to live-tail from the tip. */
968
970
  fromCursor?: string | null;
@@ -976,6 +978,13 @@ type StreamsEventsSubscribeCommon = {
976
978
  */
977
979
  onError?: (err: unknown) => void;
978
980
  /**
981
+ * Lifecycle observer. `connecting` before each connection attempt, `open`
982
+ * once the server answers, `reconnecting` while backing off after a drop
983
+ * or error, `closed` once, when the subscription ends (unsubscribed,
984
+ * aborted, or stopped by an error a retry cannot fix).
985
+ */
986
+ onState?: (state: StreamsSubscriptionState) => void;
987
+ /**
979
988
  * First reconnect pause in ms (default 1000). Doubles per consecutive
980
989
  * failure up to 30 s with jitter, resets once a frame arrives, and never
981
990
  * undercuts a `Retry-After`.
@@ -2472,6 +2481,9 @@ type IndexSbtcWithdrawal = {
2472
2481
  btc_block_height: number | null;
2473
2482
  /** When the sweep crossed the confirmation threshold (ISO), null otherwise. */
2474
2483
  confirmed_at: string | null;
2484
+ /** Monotonic stamp taken when the sweep became confirmed; resume a scan
2485
+ * from it with `confirmed_after_seq`. Null while unsettled. */
2486
+ settlement_seq: number | null;
2475
2487
  requested_at?: string | null;
2476
2488
  resolved_at?: string | null;
2477
2489
  };
@@ -244,6 +244,22 @@ function resolveAccountKey(accountKey) {
244
244
  return;
245
245
  return process.env?.SECONDLAYER_API_KEY || undefined;
246
246
  }
247
+ function resolveAccountUrl() {
248
+ const env = typeof process !== "undefined" ? process.env : undefined;
249
+ return (env?.SL_CREDITS_API_URL || env?.ARCHIVE_OPS_API_URL || HOSTED_API_URL).replace(/\/+$/, "");
250
+ }
251
+ var ACCOUNT_CREDENTIAL_RE = /^s[ks]-sl_/;
252
+ function resolveIdentity(target, options = {}) {
253
+ const url = target === "account" ? resolveAccountUrl() : resolveBaseUrl();
254
+ const envKey = target === "account" ? resolveAccountKey() : typeof process !== "undefined" ? process.env?.[INSTANCE_TOKEN_ENV] || undefined : undefined;
255
+ if (envKey && (target === "instance" || ACCOUNT_CREDENTIAL_RE.test(envKey))) {
256
+ return { source: "env", token: envKey, url };
257
+ }
258
+ if (options.session) {
259
+ return { source: "session", token: options.session, url };
260
+ }
261
+ return { source: "none", token: undefined, url };
262
+ }
247
263
  function seg(value) {
248
264
  return encodeURIComponent(String(value));
249
265
  }
@@ -486,8 +502,14 @@ function subscribeStreamsEvents(opts) {
486
502
  const baseDelayMs = opts.reconnectDelayMs ?? params.reconnectDelayMs ?? 1000;
487
503
  const staleAfterMs = opts.staleAfterMs ?? params.staleAfterMs ?? SUBSCRIBE_DEFAULT_STALE_AFTER_MS;
488
504
  let attempt = 0;
505
+ const emit = (state) => {
506
+ try {
507
+ params.onState?.(state);
508
+ } catch {}
509
+ };
489
510
  const run = async () => {
490
511
  while (!controller.signal.aborted) {
512
+ emit("connecting");
491
513
  try {
492
514
  const url = `${opts.baseUrl}/v1/streams/events/stream${buildQuery({
493
515
  chain: params.chain === "bitcoin" ? "bitcoin" : undefined,
@@ -509,6 +531,7 @@ function subscribeStreamsEvents(opts) {
509
531
  fetchImpl: opts.fetchImpl,
510
532
  staleAfterMs,
511
533
  mapError: mapStreamsError,
534
+ onOpen: () => emit("open"),
512
535
  onFrame: async (frame) => {
513
536
  attempt = 0;
514
537
  if (frame.event === "ping" || !frame.data)
@@ -537,6 +560,8 @@ function subscribeStreamsEvents(opts) {
537
560
  cursor = parsed.event.cursor ?? cursor;
538
561
  }
539
562
  });
563
+ if (!controller.signal.aborted)
564
+ emit("reconnecting");
540
565
  await sleep(reconnectDelay(attempt, baseDelayMs, undefined, opts.random), controller.signal);
541
566
  attempt += 1;
542
567
  } catch (err) {
@@ -545,13 +570,14 @@ function subscribeStreamsEvents(opts) {
545
570
  params.onError?.(err);
546
571
  if (isTerminal(err))
547
572
  throw err;
573
+ emit("reconnecting");
548
574
  const retryAfterSeconds = err.retryAfterSeconds;
549
575
  await sleep(reconnectDelay(attempt, baseDelayMs, retryAfterSeconds, opts.random), controller.signal);
550
576
  attempt += 1;
551
577
  }
552
578
  }
553
579
  };
554
- const done = run();
580
+ const done = run().finally(() => emit("closed"));
555
581
  done.catch(() => {});
556
582
  const unsubscribe = () => controller.abort();
557
583
  return Object.assign(unsubscribe, { done });
@@ -569,6 +595,7 @@ async function readSse(opts) {
569
595
  if (!res.body) {
570
596
  throw new StreamsServerError("Streams SSE response has no body.", 0);
571
597
  }
598
+ opts.onOpen?.();
572
599
  for await (const frame of parseSseFrames(res.body, opts.signal, opts.staleAfterMs)) {
573
600
  await opts.onFrame(frame);
574
601
  }
@@ -1470,13 +1497,70 @@ async function consumeStreamsEvents(opts) {
1470
1497
  }
1471
1498
  return { cursor, pages, emptyPolls };
1472
1499
  }
1500
+ async function* pullFromLoop(start, external) {
1501
+ const controller = new AbortController;
1502
+ if (external) {
1503
+ if (external.aborted)
1504
+ controller.abort();
1505
+ else
1506
+ external.addEventListener("abort", () => controller.abort(), {
1507
+ once: true
1508
+ });
1509
+ }
1510
+ const state = {
1511
+ delivered: null,
1512
+ held: null,
1513
+ finished: false,
1514
+ failure: null,
1515
+ wake: null
1516
+ };
1517
+ const notify = () => {
1518
+ const resolve = state.wake;
1519
+ state.wake = null;
1520
+ resolve?.();
1521
+ };
1522
+ const loop = start((value) => new Promise((release) => {
1523
+ state.delivered = { value, release };
1524
+ notify();
1525
+ }), controller.signal).then(() => {
1526
+ state.finished = true;
1527
+ }, (error) => {
1528
+ state.failure = { error };
1529
+ state.finished = true;
1530
+ }).finally(notify);
1531
+ try {
1532
+ while (true) {
1533
+ if (state.delivered) {
1534
+ const { value, release } = state.delivered;
1535
+ state.delivered = null;
1536
+ state.held = release;
1537
+ yield value;
1538
+ state.held = null;
1539
+ release();
1540
+ continue;
1541
+ }
1542
+ if (state.finished)
1543
+ break;
1544
+ await new Promise((resolve) => {
1545
+ state.wake = resolve;
1546
+ if (state.delivered || state.finished)
1547
+ notify();
1548
+ });
1549
+ }
1550
+ if (state.failure)
1551
+ throw state.failure.error;
1552
+ } finally {
1553
+ controller.abort();
1554
+ state.held?.();
1555
+ state.delivered?.release();
1556
+ await loop;
1557
+ }
1558
+ }
1473
1559
  async function* iterateStreamsBatches(opts) {
1474
- const sleep = opts.sleep ?? defaultSleep;
1475
- let cursor = opts.fromCursor ?? null;
1476
- while (!opts.signal?.aborted) {
1477
- const envelope = await opts.fetchEvents({
1478
- cursor,
1479
- limit: opts.batchSize,
1560
+ yield* pullFromLoop(async (deliver, signal) => {
1561
+ const result = await consumeStreamsEvents({
1562
+ fromCursor: opts.fromCursor,
1563
+ batchSize: opts.batchSize,
1480
1564
  chain: opts.chain,
1481
1565
  types: opts.types,
1482
1566
  notTypes: opts.notTypes,
@@ -1486,38 +1570,41 @@ async function* iterateStreamsBatches(opts) {
1486
1570
  assetIdentifier: opts.assetIdentifier,
1487
1571
  rune: opts.rune,
1488
1572
  address: opts.address,
1489
- filters: opts.filters
1573
+ filters: opts.filters,
1574
+ fetchEvents: opts.fetchEvents,
1575
+ sleep: opts.sleep,
1576
+ emptyBackoffMs: opts.intervalMs,
1577
+ signal,
1578
+ onBatch: async (events, ctx) => {
1579
+ if (events.length === 0 && ctx.reorgs.length === 0)
1580
+ return;
1581
+ await deliver({
1582
+ events,
1583
+ cursor: ctx.cursor,
1584
+ tip: ctx.tip,
1585
+ reorgs: [...ctx.reorgs]
1586
+ });
1587
+ }
1490
1588
  });
1491
- const checkpoint = envelope.next_cursor ?? cursor;
1492
- if (envelope.events.length > 0 || envelope.reorgs.length > 0) {
1493
- yield {
1494
- events: envelope.events,
1495
- cursor: checkpoint,
1496
- tip: envelope.tip,
1497
- reorgs: envelope.reorgs
1498
- };
1499
- }
1500
- const advanced = checkpoint !== null && checkpoint !== cursor;
1501
- cursor = checkpoint;
1502
- if (!advanced && envelope.events.length === 0) {
1503
- if (opts.signal?.aborted)
1504
- return;
1505
- await sleep(opts.intervalMs, opts.signal);
1506
- }
1589
+ assertNotStalled(result, signal);
1590
+ }, opts.signal);
1591
+ }
1592
+ function assertNotStalled(result, signal, bounds = {}) {
1593
+ if (signal.aborted)
1594
+ return;
1595
+ if (bounds.maxPages !== undefined && (bounds.pages ?? 0) >= bounds.maxPages) {
1596
+ return;
1597
+ }
1598
+ if (bounds.maxEmptyPolls !== undefined && result.emptyPolls >= bounds.maxEmptyPolls) {
1599
+ return;
1507
1600
  }
1601
+ 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");
1508
1602
  }
1509
1603
  async function* streamStreamsEvents(opts) {
1510
- const sleep = opts.sleep ?? defaultSleep;
1511
- const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
1512
- const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
1513
- const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
1514
- let cursor = opts.fromCursor ?? null;
1515
- let pages = 0;
1516
- let emptyPolls = 0;
1517
- while (pages < maxPages && emptyPolls < maxEmptyPolls && !opts.signal?.aborted) {
1518
- const envelope = await opts.fetchEvents({
1519
- cursor,
1520
- limit: opts.batchSize,
1604
+ yield* pullFromLoop(async (deliver, signal) => {
1605
+ const result = await consumeStreamsEvents({
1606
+ fromCursor: opts.fromCursor,
1607
+ batchSize: opts.batchSize,
1521
1608
  chain: opts.chain,
1522
1609
  types: opts.types,
1523
1610
  notTypes: opts.notTypes,
@@ -1527,29 +1614,27 @@ async function* streamStreamsEvents(opts) {
1527
1614
  assetIdentifier: opts.assetIdentifier,
1528
1615
  rune: opts.rune,
1529
1616
  address: opts.address,
1530
- filters: opts.filters
1617
+ filters: opts.filters,
1618
+ fetchEvents: opts.fetchEvents,
1619
+ sleep: opts.sleep,
1620
+ emptyBackoffMs: opts.emptyBackoffMs,
1621
+ maxPages: opts.maxPages,
1622
+ maxEmptyPolls: opts.maxEmptyPolls,
1623
+ signal,
1624
+ onBatch: async (events) => {
1625
+ for (const event of events) {
1626
+ if (signal.aborted)
1627
+ return;
1628
+ await deliver(event);
1629
+ }
1630
+ }
1531
1631
  });
1532
- pages++;
1533
- for (const event of envelope.events) {
1534
- if (opts.signal?.aborted)
1535
- return;
1536
- yield event;
1537
- }
1538
- const nextCursor = envelope.next_cursor;
1539
- if (nextCursor && nextCursor !== cursor) {
1540
- cursor = nextCursor;
1541
- emptyPolls = 0;
1542
- continue;
1543
- }
1544
- if (envelope.events.length === 0) {
1545
- emptyPolls++;
1546
- if (emptyPolls >= maxEmptyPolls || pages >= maxPages)
1547
- return;
1548
- await sleep(emptyBackoffMs, opts.signal);
1549
- continue;
1550
- }
1551
- return;
1552
- }
1632
+ assertNotStalled(result, signal, {
1633
+ maxPages: opts.maxPages,
1634
+ maxEmptyPolls: opts.maxEmptyPolls,
1635
+ pages: result.pages
1636
+ });
1637
+ }, opts.signal);
1553
1638
  }
1554
1639
 
1555
1640
  // src/index-api/consumer.ts
@@ -2311,6 +2396,33 @@ function createStreamsDumps(opts) {
2311
2396
  return { list, fileUrl, download };
2312
2397
  }
2313
2398
 
2399
+ // src/streams/timeout-fetch.ts
2400
+ var STREAMS_CONNECT_TIMEOUT_MS = 30000;
2401
+ function withConnectTimeout(fetchImpl, timeoutMs) {
2402
+ if (!(timeoutMs > 0) || !Number.isFinite(timeoutMs))
2403
+ return fetchImpl;
2404
+ return async (input, init) => {
2405
+ const callerSignal = init?.signal ?? undefined;
2406
+ const timeout = new AbortController;
2407
+ let timedOut = false;
2408
+ const timer = setTimeout(() => {
2409
+ timedOut = true;
2410
+ timeout.abort();
2411
+ }, timeoutMs);
2412
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
2413
+ try {
2414
+ return await fetchImpl(input, { ...init, signal });
2415
+ } catch (err) {
2416
+ if (timedOut && !callerSignal?.aborted) {
2417
+ throw new StreamsServerError(`No response headers within ${timeoutMs}ms. Retryable.`, 0, undefined, "REQUEST_TIMEOUT");
2418
+ }
2419
+ throw err;
2420
+ } finally {
2421
+ clearTimeout(timer);
2422
+ }
2423
+ };
2424
+ }
2425
+
2314
2426
  // src/streams/client.ts
2315
2427
  function cursorTuple(cursor) {
2316
2428
  if (!cursor)
@@ -2356,7 +2468,7 @@ function createStreamsClient(options) {
2356
2468
  "x-sl-origin": origin,
2357
2469
  ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
2358
2470
  });
2359
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
2471
+ const fetchImpl = withConnectTimeout(options.fetchImpl ?? ((input, init) => fetch(input, init)), options.connectTimeoutMs ?? STREAMS_CONNECT_TIMEOUT_MS);
2360
2472
  const verify = options.verify;
2361
2473
  const verifyMode = verify === false ? "off" : verify === undefined ? "lenient" : "strict";
2362
2474
  if (verify === true && baseUrl.startsWith("http:") && !isLoopbackUrl(baseUrl)) {
@@ -2846,5 +2958,5 @@ export {
2846
2958
  getSubgraph
2847
2959
  };
2848
2960
 
2849
- //# debugId=4BBCD55406927B9364756E2164756E21
2961
+ //# debugId=82C9947AF210E6E964756E2164756E21
2850
2962
  //# sourceMappingURL=index.js.map