@secondlayer/sdk 10.4.3 → 10.6.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.
@@ -39,6 +39,22 @@ interface SecondLayerOptions {
39
39
  * retry policy instead of stalling a walk or consume loop forever. `0`
40
40
  * disables the timeout. */
41
41
  requestTimeoutMs?: number;
42
+ /** Where GET responses carrying an `ETag` are kept, so a repeat read sends
43
+ * `If-None-Match` and a finalized page comes back as a free `304`.
44
+ * Defaults to an in-memory store of the last 64 pages. Pass your own to
45
+ * keep it across processes (a file, a KV), or `false` to turn it off. */
46
+ etagCache?: EtagCache | false;
47
+ }
48
+ /** One cached GET response: its validator and the body it validated. */
49
+ type EtagEntry = {
50
+ etag: string;
51
+ body: string;
52
+ };
53
+ /** Storage for `etagCache`. Keys are full request URLs. Either method may be
54
+ * async, so a disk or KV store works as well as a Map. */
55
+ interface EtagCache {
56
+ get(key: string): EtagEntry | undefined | Promise<EtagEntry | undefined>;
57
+ set(key: string, entry: EtagEntry): void | Promise<void>;
42
58
  }
43
59
  /** Per-call options every `request*` method accepts. */
44
60
  type RequestOptions = {
@@ -52,6 +68,7 @@ declare abstract class BaseClient {
52
68
  protected origin: "cli" | "mcp" | "session";
53
69
  protected fetchImpl: FetchLike;
54
70
  protected requestTimeoutMs: number;
71
+ protected etagCache: EtagCache | null;
55
72
  constructor(options?: Partial<SecondLayerOptions>);
56
73
  static authHeaders(apiKey?: string): Record<string, string>;
57
74
  protected request<T>(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise<T>;
@@ -68,7 +85,7 @@ declare abstract class BaseClient {
68
85
  /** Issue the HTTP request and map non-2xx statuses onto the error family.
69
86
  * `signal` is the combined caller-plus-timeout signal from `request*`;
70
87
  * subclasses that stream a body call this directly and read it themselves. */
71
- protected fetchResponse(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<Response>;
88
+ protected fetchResponse(method: string, path: string, body?: unknown, signal?: AbortSignal, extraHeaders?: Record<string, string>): Promise<Response>;
72
89
  }
73
90
  interface SubgraphSource {
74
91
  name: string;
@@ -108,7 +125,6 @@ interface SubgraphRowsEnvelope<T = unknown> {
108
125
  interface BundleSubgraphResponse {
109
126
  ok: true;
110
127
  name: string;
111
- version: string | null;
112
128
  description: string | null;
113
129
  sources: Record<string, Record<string, unknown>>;
114
130
  schema: Record<string, unknown>;
@@ -2651,4 +2667,4 @@ declare function getSubgraph<T extends {
2651
2667
  schema: Record<string, unknown>;
2652
2668
  }>(def: T, options?: Partial<SecondLayerOptions> | SecondLayer | Subgraphs): InferSubgraphClient2<T>;
2653
2669
  import { SubgraphAgentSchema as SubgraphAgentSchema2, SubgraphSpecFormat, SubgraphSpecOptions as SubgraphSpecOptions2 } from "@secondlayer/shared/subgraphs/spec";
2654
- export { getSubgraph, Subgraphs, SubgraphSpecOptions2 as SubgraphSpecOptions, SubgraphSpecFormat, SubgraphAgentSchema2 as SubgraphAgentSchema, SecondLayerOptions };
2670
+ export { SecondLayerOptions, SubgraphAgentSchema2 as SubgraphAgentSchema, SubgraphSpecFormat, SubgraphSpecOptions2 as SubgraphSpecOptions, Subgraphs, getSubgraph };
@@ -156,6 +156,30 @@ function parseRetryAfter(value) {
156
156
  }
157
157
 
158
158
  // src/base.ts
159
+ class MemoryEtagCache {
160
+ limit;
161
+ entries = new Map;
162
+ constructor(limit = 64) {
163
+ this.limit = limit;
164
+ }
165
+ get(key) {
166
+ const entry = this.entries.get(key);
167
+ if (entry) {
168
+ this.entries.delete(key);
169
+ this.entries.set(key, entry);
170
+ }
171
+ return entry;
172
+ }
173
+ set(key, entry) {
174
+ this.entries.delete(key);
175
+ this.entries.set(key, entry);
176
+ if (this.entries.size > this.limit) {
177
+ const oldest = this.entries.keys().next().value;
178
+ if (oldest !== undefined)
179
+ this.entries.delete(oldest);
180
+ }
181
+ }
182
+ }
159
183
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
160
184
  function abortError() {
161
185
  return new DOMException("This operation was aborted", "AbortError");
@@ -266,12 +290,14 @@ class BaseClient {
266
290
  origin;
267
291
  fetchImpl;
268
292
  requestTimeoutMs;
293
+ etagCache;
269
294
  constructor(options = {}) {
270
295
  this.baseUrl = resolveBaseUrl(options.baseUrl);
271
296
  this.apiKey = resolveApiKey(options.apiKey);
272
297
  this.origin = options.origin ?? "cli";
273
298
  this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
274
299
  this.fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
300
+ this.etagCache = options.etagCache === false ? null : options.etagCache ?? new MemoryEtagCache;
275
301
  }
276
302
  static authHeaders(apiKey) {
277
303
  const headers = {
@@ -284,11 +310,25 @@ class BaseClient {
284
310
  }
285
311
  async request(method, path, body, opts = {}) {
286
312
  return this.withRequestBudget(opts.signal, async (signal) => {
287
- const response = await this.fetchResponse(method, path, body, signal);
313
+ const cacheKey = method === "GET" && this.etagCache ? `${this.baseUrl}${path}` : null;
314
+ const cached = cacheKey ? await this.etagCache?.get(cacheKey) : undefined;
315
+ const response = await this.fetchResponse(method, path, body, signal, cached ? { "If-None-Match": cached.etag } : undefined);
288
316
  if (response.status === 204) {
289
317
  return;
290
318
  }
291
- return await raceAbort(response.json(), signal);
319
+ if (response.status === 304) {
320
+ if (!cached) {
321
+ throw new ApiError(304, "Not Modified without a cached copy");
322
+ }
323
+ return JSON.parse(cached.body);
324
+ }
325
+ const etag = cacheKey ? response.headers.get("ETag") : null;
326
+ if (!cacheKey || !etag) {
327
+ return await raceAbort(response.json(), signal);
328
+ }
329
+ const text = await raceAbort(response.text(), signal);
330
+ await this.etagCache?.set(cacheKey, { etag, body: text });
331
+ return JSON.parse(text);
292
332
  });
293
333
  }
294
334
  async requestOrNull(method, path, body, opts = {}) {
@@ -331,9 +371,12 @@ class BaseClient {
331
371
  callerSignal?.removeEventListener("abort", forward);
332
372
  }
333
373
  }
334
- async fetchResponse(method, path, body, signal) {
374
+ async fetchResponse(method, path, body, signal, extraHeaders) {
335
375
  const url = `${this.baseUrl}${path}`;
336
- const headers = BaseClient.authHeaders(this.apiKey);
376
+ const headers = {
377
+ ...BaseClient.authHeaders(this.apiKey),
378
+ ...extraHeaders
379
+ };
337
380
  headers["x-sl-origin"] = this.origin;
338
381
  let serializedBody;
339
382
  if (body !== undefined && body !== null) {
@@ -358,6 +401,8 @@ class BaseClient {
358
401
  throw err;
359
402
  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 });
360
403
  }
404
+ if (response.status === 304)
405
+ return response;
361
406
  if (!response.ok) {
362
407
  const errorBody = signal ? await raceAbort(response.text(), signal) : await response.text();
363
408
  const envelope = parseErrorEnvelope(errorBody);
@@ -1280,7 +1325,7 @@ async function defaultSleep(ms, signal) {
1280
1325
  });
1281
1326
  }
1282
1327
  async function consumeStreamsEvents(opts) {
1283
- const sleep3 = opts.sleep ?? defaultSleep;
1328
+ const sleep = opts.sleep ?? defaultSleep;
1284
1329
  const mode = opts.mode ?? "tail";
1285
1330
  const finalizedOnly = opts.finalizedOnly ?? false;
1286
1331
  const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
@@ -1310,7 +1355,7 @@ async function consumeStreamsEvents(opts) {
1310
1355
  retryCount: opts.retryCount,
1311
1356
  retryDelay: opts.retryDelay,
1312
1357
  onError: opts.onError,
1313
- sleep: sleep3,
1358
+ sleep,
1314
1359
  signal: opts.signal
1315
1360
  });
1316
1361
  pages++;
@@ -1342,7 +1387,7 @@ async function consumeStreamsEvents(opts) {
1342
1387
  retryCount: opts.retryCount,
1343
1388
  retryDelay: opts.retryDelay,
1344
1389
  onError: opts.onError,
1345
- sleep: sleep3,
1390
+ sleep,
1346
1391
  signal: opts.signal
1347
1392
  });
1348
1393
  if (listed.next_since !== null)
@@ -1405,7 +1450,7 @@ async function consumeStreamsEvents(opts) {
1405
1450
  if (mode === "bounded") {
1406
1451
  return { cursor, pages, emptyPolls };
1407
1452
  }
1408
- await sleep3(emptyBackoffMs, opts.signal);
1453
+ await sleep(emptyBackoffMs, opts.signal);
1409
1454
  continue;
1410
1455
  }
1411
1456
  return { cursor, pages, emptyPolls };
@@ -1413,7 +1458,7 @@ async function consumeStreamsEvents(opts) {
1413
1458
  return { cursor, pages, emptyPolls };
1414
1459
  }
1415
1460
  async function* iterateStreamsBatches(opts) {
1416
- const sleep3 = opts.sleep ?? defaultSleep;
1461
+ const sleep = opts.sleep ?? defaultSleep;
1417
1462
  let cursor = opts.fromCursor ?? null;
1418
1463
  while (!opts.signal?.aborted) {
1419
1464
  const envelope = await opts.fetchEvents({
@@ -1441,12 +1486,12 @@ async function* iterateStreamsBatches(opts) {
1441
1486
  if (!advanced && envelope.events.length === 0) {
1442
1487
  if (opts.signal?.aborted)
1443
1488
  return;
1444
- await sleep3(opts.intervalMs, opts.signal);
1489
+ await sleep(opts.intervalMs, opts.signal);
1445
1490
  }
1446
1491
  }
1447
1492
  }
1448
1493
  async function* streamStreamsEvents(opts) {
1449
- const sleep3 = opts.sleep ?? defaultSleep;
1494
+ const sleep = opts.sleep ?? defaultSleep;
1450
1495
  const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
1451
1496
  const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
1452
1497
  const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
@@ -1481,7 +1526,7 @@ async function* streamStreamsEvents(opts) {
1481
1526
  emptyPolls++;
1482
1527
  if (emptyPolls >= maxEmptyPolls || pages >= maxPages)
1483
1528
  return;
1484
- await sleep3(emptyBackoffMs, opts.signal);
1529
+ await sleep(emptyBackoffMs, opts.signal);
1485
1530
  continue;
1486
1531
  }
1487
1532
  return;
@@ -1490,7 +1535,7 @@ async function* streamStreamsEvents(opts) {
1490
1535
 
1491
1536
  // src/index-api/consumer.ts
1492
1537
  async function consumeIndexFeed(opts) {
1493
- const sleep3 = opts.sleep ?? defaultSleep;
1538
+ const sleep = opts.sleep ?? defaultSleep;
1494
1539
  const mode = opts.mode ?? "tail";
1495
1540
  const finalizedOnly = opts.finalizedOnly ?? false;
1496
1541
  const batchSize = opts.batchSize ?? 200;
@@ -1514,7 +1559,7 @@ async function consumeIndexFeed(opts) {
1514
1559
  retryCount: opts.retryCount,
1515
1560
  retryDelay: opts.retryDelay,
1516
1561
  onError: opts.onError,
1517
- sleep: sleep3,
1562
+ sleep,
1518
1563
  signal: opts.signal
1519
1564
  });
1520
1565
  pages++;
@@ -1577,7 +1622,7 @@ async function consumeIndexFeed(opts) {
1577
1622
  if (mode === "bounded") {
1578
1623
  return { cursor, pages, emptyPolls };
1579
1624
  }
1580
- await sleep3(emptyBackoffMs, opts.signal);
1625
+ await sleep(emptyBackoffMs, opts.signal);
1581
1626
  continue;
1582
1627
  }
1583
1628
  return { cursor, pages, emptyPolls };
@@ -2639,9 +2684,9 @@ function getSubgraph(def, options = {}) {
2639
2684
  return new Subgraphs(options).typed(def);
2640
2685
  }
2641
2686
  export {
2642
- getSubgraph,
2643
- Subgraphs
2687
+ Subgraphs,
2688
+ getSubgraph
2644
2689
  };
2645
2690
 
2646
- //# debugId=B66CAEBD81B3C1B764756E2164756E21
2691
+ //# debugId=8E4B27D4BE8A894F64756E2164756E21
2647
2692
  //# sourceMappingURL=index.js.map