@org-quicko/silo-client 1.1.0 → 1.1.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.
package/README.md CHANGED
@@ -8,7 +8,8 @@ an environment holds collections, and a collection holds entries.
8
8
  npm install @org-quicko/silo-client
9
9
  ```
10
10
 
11
- Runs on Node 18+, Bun, Deno, browsers and workers. It has no dependencies.
11
+ Runs on Node 18+, Bun, Deno, browsers and workers. Its optional cache uses one
12
+ runtime dependency, `@isaacs/ttlcache`.
12
13
 
13
14
  The examples below build moviespace, a small film database.
14
15
 
@@ -319,6 +320,46 @@ await movies.get(id, { timeoutMilliseconds: 2_000 })
319
320
  Nothing is retried for you. A retried `POST` creates a second entry, and only
320
321
  the caller knows whether a call was safe to repeat.
321
322
 
323
+ ## Optional read cache
324
+
325
+ Reads go to the server unless you enable caching. Choose a positive finite
326
+ integer TTL in milliseconds. It starts when a response is stored and does
327
+ not reset on a hit. `maxEntries` is optional; set it to bound memory use in a
328
+ long-running client. When full, the soonest-expiring entry is removed first.
329
+
330
+ ```ts
331
+ const silo = new Silo({
332
+ url: "https://cms.moviespace.com",
333
+ cache: { ttlMilliseconds: 5_000, maxEntries: 100 },
334
+ })
335
+ const movies = silo.scope("moviespace", "prod").collection<Movie>("movies")
336
+
337
+ await movies.get(id) // may use a cached response
338
+ await movies.get(id, { cache: "bypass" }) // fetch without reading or storing
339
+ await movies.get(id, { cache: "refresh" }) // fetch and store on success
340
+ silo.cache.clear() // after a change made elsewhere
341
+ ```
342
+
343
+ Only successful GET JSON responses are cached, with separate entries for
344
+ different URLs, queries and headers, including API keys. Returned objects
345
+ are independent copies. A failed refresh leaves the existing cache unchanged.
346
+ Your writes clear the shared cache before dispatch and after settlement,
347
+ including failures. Pending reads cannot refill it after a clear or overwrite
348
+ a newer read. `health()` always bypasses; `MediaAsset.refresh()` refreshes by
349
+ default and accepts an explicit bypass. Modes do nothing when caching is off.
350
+
351
+ `withKey()` and `withUrl()` share the cache. To share it explicitly, pass
352
+ `cache: otherClient.cache`, or construct `new SiloCache({ ttlMilliseconds: 5000 })`.
353
+ The facade exposes only `enabled`, `size` and `clear()`. Sharing preserves
354
+ isolation by URL and headers. If a custom fetch adds authentication or changes
355
+ the destination, include that context in the client's URL/headers or bypass
356
+ caching.
357
+
358
+ The cache lives in the tab, worker or process. Changes made elsewhere can
359
+ remain hidden until expiry or a fresh read. Infinite TTLs are unsupported;
360
+ `maxEntries` defaults to unlimited and may be `Infinity`. On Deno, an active
361
+ expiry timer can keep the process alive; call `clear()` when finished.
362
+
322
363
  ## Anonymous reads
323
364
 
324
365
  A key is optional. Without one you reach the collections whose schema does not
@@ -0,0 +1,2 @@
1
+ /** Controls an enabled cache for one read. */
2
+ export type CacheMode = "bypass" | "refresh";
@@ -0,0 +1,2 @@
1
+ /** Controls an enabled cache for one read. */
2
+ export type CacheMode = "bypass" | "refresh";
@@ -0,0 +1,16 @@
1
+ import type { SiloCacheOptions } from "./silo-cache-options.cjs";
2
+ interface CachedResponse {
3
+ body: unknown;
4
+ }
5
+ /** The cache's values and pending-read tickets. */
6
+ export declare class ResponseCache {
7
+ #private;
8
+ constructor(options: SiloCacheOptions);
9
+ get size(): number;
10
+ get(key: string): CachedResponse | undefined;
11
+ set(key: string, ticket: symbol, body: unknown): void;
12
+ start(key: string): symbol;
13
+ finish(key: string, ticket: symbol): void;
14
+ clear(): void;
15
+ }
16
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { SiloCacheOptions } from "./silo-cache-options.js";
2
+ interface CachedResponse {
3
+ body: unknown;
4
+ }
5
+ /** The cache's values and pending-read tickets. */
6
+ export declare class ResponseCache {
7
+ #private;
8
+ constructor(options: SiloCacheOptions);
9
+ get size(): number;
10
+ get(key: string): CachedResponse | undefined;
11
+ set(key: string, ticket: symbol, body: unknown): void;
12
+ start(key: string): symbol;
13
+ finish(key: string, ticket: symbol): void;
14
+ clear(): void;
15
+ }
16
+ export {};
@@ -0,0 +1,7 @@
1
+ /** Bounds an opt-in in-memory read cache. */
2
+ export interface SiloCacheOptions {
3
+ /** Positive finite safe integer in milliseconds, counted from storage. */
4
+ ttlMilliseconds: number;
5
+ /** Positive safe integer or Infinity. Omit for no count limit. */
6
+ maxEntries?: number;
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Bounds an opt-in in-memory read cache. */
2
+ export interface SiloCacheOptions {
3
+ /** Positive finite safe integer in milliseconds, counted from storage. */
4
+ ttlMilliseconds: number;
5
+ /** Positive safe integer or Infinity. Omit for no count limit. */
6
+ maxEntries?: number;
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { SiloCacheOptions } from "./silo-cache-options.cjs";
2
+ /** An optional, in-memory cache shared by one or more `Silo` instances. */
3
+ export declare class SiloCache {
4
+ #private;
5
+ constructor(options?: SiloCacheOptions);
6
+ get enabled(): boolean;
7
+ get size(): number;
8
+ /** Clears responses and prevents pending reads from storing their results. */
9
+ clear(): void;
10
+ private static validate;
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { SiloCacheOptions } from "./silo-cache-options.js";
2
+ /** An optional, in-memory cache shared by one or more `Silo` instances. */
3
+ export declare class SiloCache {
4
+ #private;
5
+ constructor(options?: SiloCacheOptions);
6
+ get enabled(): boolean;
7
+ get size(): number;
8
+ /** Clears responses and prevents pending reads from storing their results. */
9
+ clear(): void;
10
+ private static validate;
11
+ }
package/dist/index.cjs CHANGED
@@ -47,6 +47,7 @@ __export(exports_src, {
47
47
  SortTerm: () => SortTerm,
48
48
  Sort: () => Sort,
49
49
  SiloError: () => SiloError,
50
+ SiloCache: () => SiloCache,
50
51
  Silo: () => Silo,
51
52
  SearchReach: () => SearchReach,
52
53
  SearchPage: () => SearchPage,
@@ -305,12 +306,14 @@ var DefaultUsageLimit = 50;
305
306
  class MediaUsagePage extends Page {
306
307
  transport;
307
308
  assetId;
309
+ requestOptions;
308
310
  visible;
309
311
  visibleCapped;
310
- constructor(rows, total, visible, visibleCapped, window, transport, assetId) {
312
+ constructor(rows, total, visible, visibleCapped, window, transport, assetId, requestOptions) {
311
313
  super(rows, total, window);
312
314
  this.transport = transport;
313
315
  this.assetId = assetId;
316
+ this.requestOptions = requestOptions;
314
317
  this.visible = visible;
315
318
  this.visibleCapped = visibleCapped;
316
319
  }
@@ -325,11 +328,11 @@ class MediaUsagePage extends Page {
325
328
  }
326
329
  async next(options) {
327
330
  const window = this.windowForNext();
328
- return window ? MediaUsagePage.loadWindow(this.transport, this.assetId, window, options) : null;
331
+ return window ? MediaUsagePage.loadWindow(this.transport, this.assetId, window, { ...this.requestOptions, ...options }) : null;
329
332
  }
330
333
  async previous(options) {
331
334
  const window = this.window.previous();
332
- return window ? MediaUsagePage.loadWindow(this.transport, this.assetId, window, options) : null;
335
+ return window ? MediaUsagePage.loadWindow(this.transport, this.assetId, window, { ...this.requestOptions, ...options }) : null;
333
336
  }
334
337
  static load(transport, assetId, query, options) {
335
338
  const window = new PageWindow(query.limit ?? DefaultUsageLimit, query.offset ?? 0);
@@ -341,10 +344,11 @@ class MediaUsagePage extends Page {
341
344
  path: ApiPath.mediaAssetUsages(assetId),
342
345
  query: { limit: window.limit, offset: window.offset },
343
346
  signal: options?.signal,
344
- timeoutMilliseconds: options?.timeoutMilliseconds
347
+ timeoutMilliseconds: options?.timeoutMilliseconds,
348
+ cache: options?.cache
345
349
  });
346
350
  const mapped = MediaAssetMapper.toUsagePage(body);
347
- return new MediaUsagePage(mapped.items, mapped.total, mapped.visible, mapped.visibleCapped, window, transport, assetId);
351
+ return new MediaUsagePage(mapped.items, mapped.total, mapped.visible, mapped.visibleCapped, window, transport, assetId, options);
348
352
  }
349
353
  }
350
354
 
@@ -440,7 +444,8 @@ class MediaAsset {
440
444
  method: "GET",
441
445
  path: ApiPath.mediaAsset(this.id),
442
446
  signal: options?.signal,
443
- timeoutMilliseconds: options?.timeoutMilliseconds
447
+ timeoutMilliseconds: options?.timeoutMilliseconds,
448
+ cache: options?.cache ?? "refresh"
444
449
  });
445
450
  this.record = MediaAssetMapper.toRecord(payload);
446
451
  return this;
@@ -502,7 +507,8 @@ class MediaFolders {
502
507
  method: "GET",
503
508
  path: ApiPath.mediaFolders(),
504
509
  signal: options?.signal,
505
- timeoutMilliseconds: options?.timeoutMilliseconds
510
+ timeoutMilliseconds: options?.timeoutMilliseconds,
511
+ cache: options?.cache
506
512
  });
507
513
  return body.items;
508
514
  }
@@ -544,21 +550,23 @@ class MediaFolders {
544
550
  class MediaPage extends Page {
545
551
  transport;
546
552
  wireQuery;
547
- constructor(rows, total, window, transport, wireQuery) {
553
+ requestOptions;
554
+ constructor(rows, total, window, transport, wireQuery, requestOptions) {
548
555
  super(rows, total, window);
549
556
  this.transport = transport;
550
557
  this.wireQuery = wireQuery;
558
+ this.requestOptions = requestOptions;
551
559
  }
552
560
  get files() {
553
561
  return this.rows;
554
562
  }
555
563
  async next(options) {
556
564
  const window = this.windowForNext();
557
- return window ? MediaPage.loadWindow(this.transport, this.wireQuery, window, options) : null;
565
+ return window ? MediaPage.loadWindow(this.transport, this.wireQuery, window, { ...this.requestOptions, ...options }) : null;
558
566
  }
559
567
  async previous(options) {
560
568
  const window = this.window.previous();
561
- return window ? MediaPage.loadWindow(this.transport, this.wireQuery, window, options) : null;
569
+ return window ? MediaPage.loadWindow(this.transport, this.wireQuery, window, { ...this.requestOptions, ...options }) : null;
562
570
  }
563
571
  static async loadWindow(transport, wireQuery, window, options) {
564
572
  const body = await transport.json({
@@ -566,10 +574,11 @@ class MediaPage extends Page {
566
574
  path: ApiPath.media(),
567
575
  query: { ...wireQuery, limit: window.limit, offset: window.offset },
568
576
  signal: options?.signal,
569
- timeoutMilliseconds: options?.timeoutMilliseconds
577
+ timeoutMilliseconds: options?.timeoutMilliseconds,
578
+ cache: options?.cache
570
579
  });
571
580
  const rows = body.items.map((payload) => new MediaAsset(transport, MediaAssetMapper.toRecord(payload)));
572
- return new MediaPage(rows, body.total, new PageWindow(body.limit, body.offset), transport, wireQuery);
581
+ return new MediaPage(rows, body.total, new PageWindow(body.limit, body.offset), transport, wireQuery, options);
573
582
  }
574
583
  }
575
584
 
@@ -689,7 +698,8 @@ class Media {
689
698
  method: "GET",
690
699
  path: ApiPath.mediaAsset(id),
691
700
  signal: options?.signal,
692
- timeoutMilliseconds: options?.timeoutMilliseconds
701
+ timeoutMilliseconds: options?.timeoutMilliseconds,
702
+ cache: options?.cache
693
703
  });
694
704
  return new MediaAsset(this.transport, MediaAssetMapper.toRecord(payload));
695
705
  }
@@ -698,7 +708,8 @@ class Media {
698
708
  method: "GET",
699
709
  path: ApiPath.mediaExtensions(),
700
710
  signal: options?.signal,
701
- timeoutMilliseconds: options?.timeoutMilliseconds
711
+ timeoutMilliseconds: options?.timeoutMilliseconds,
712
+ cache: options?.cache
702
713
  });
703
714
  return body.items;
704
715
  }
@@ -1411,6 +1422,132 @@ class Projects {
1411
1422
  }
1412
1423
  }
1413
1424
 
1425
+ // src/transport/caching-transport.ts
1426
+ class CachingTransport {
1427
+ #fetchTransport;
1428
+ #cache;
1429
+ constructor(fetchTransport, cache) {
1430
+ this.#fetchTransport = fetchTransport;
1431
+ this.#cache = cache;
1432
+ }
1433
+ async json(request) {
1434
+ if (request.method !== "GET")
1435
+ return this.write(() => this.#fetchTransport.json(request));
1436
+ if (request.cache === "bypass")
1437
+ return this.#fetchTransport.json(request);
1438
+ if (request.signal?.aborted)
1439
+ throw new RequestAbortedError(request.method, request.path);
1440
+ let prepared;
1441
+ try {
1442
+ prepared = this.#fetchTransport.prepare(request);
1443
+ } catch (caught) {
1444
+ throw this.#fetchTransport.preparationFailure(request, caught);
1445
+ }
1446
+ const key = JSON.stringify([prepared.url, [...prepared.headers]]);
1447
+ if (request.cache !== "refresh") {
1448
+ const cached = this.#cache.get(key);
1449
+ if (cached)
1450
+ return cached.body;
1451
+ }
1452
+ const ticket = this.#cache.start(key);
1453
+ try {
1454
+ const body = await this.#fetchTransport.jsonPrepared(request, prepared);
1455
+ this.#cache.set(key, ticket, body);
1456
+ return body;
1457
+ } finally {
1458
+ this.#cache.finish(key, ticket);
1459
+ }
1460
+ }
1461
+ empty(request) {
1462
+ return request.method === "GET" ? this.#fetchTransport.empty(request) : this.write(() => this.#fetchTransport.empty(request));
1463
+ }
1464
+ stream(request) {
1465
+ return request.method === "GET" ? this.#fetchTransport.stream(request) : this.write(() => this.#fetchTransport.stream(request));
1466
+ }
1467
+ upload(request, form) {
1468
+ return request.method === "GET" ? this.#fetchTransport.upload(request, form) : this.write(() => this.#fetchTransport.upload(request, form));
1469
+ }
1470
+ async write(dispatch) {
1471
+ this.#cache.clear();
1472
+ try {
1473
+ return await dispatch();
1474
+ } finally {
1475
+ this.#cache.clear();
1476
+ }
1477
+ }
1478
+ }
1479
+
1480
+ // src/cache/response-cache.ts
1481
+ var import_ttlcache = require("@isaacs/ttlcache");
1482
+
1483
+ class ResponseCache {
1484
+ #store;
1485
+ #pending = new Map;
1486
+ constructor(options) {
1487
+ this.#store = new import_ttlcache.TTLCache({
1488
+ ttl: options.ttlMilliseconds,
1489
+ ...options.maxEntries === undefined || options.maxEntries === Infinity ? {} : { max: options.maxEntries },
1490
+ checkAgeOnGet: true,
1491
+ updateAgeOnGet: false
1492
+ });
1493
+ }
1494
+ get size() {
1495
+ return this.#store.size;
1496
+ }
1497
+ get(key) {
1498
+ const cached = this.#store.get(key);
1499
+ return cached === undefined ? undefined : { body: structuredClone(cached.body) };
1500
+ }
1501
+ set(key, ticket, body) {
1502
+ if (this.#pending.get(key) !== ticket)
1503
+ return;
1504
+ this.#store.set(key, { body: structuredClone(body) });
1505
+ }
1506
+ start(key) {
1507
+ const ticket = Symbol();
1508
+ this.#pending.set(key, ticket);
1509
+ return ticket;
1510
+ }
1511
+ finish(key, ticket) {
1512
+ if (this.#pending.get(key) === ticket)
1513
+ this.#pending.delete(key);
1514
+ }
1515
+ clear() {
1516
+ this.#pending.clear();
1517
+ this.#store.clear();
1518
+ }
1519
+ }
1520
+
1521
+ // src/cache/silo-cache.ts
1522
+ class SiloCache {
1523
+ #responseCache;
1524
+ constructor(options) {
1525
+ if (options !== undefined)
1526
+ SiloCache.validate(options);
1527
+ this.#responseCache = options === undefined ? undefined : new ResponseCache(options);
1528
+ }
1529
+ get enabled() {
1530
+ return this.#responseCache !== undefined;
1531
+ }
1532
+ get size() {
1533
+ return this.#responseCache?.size ?? 0;
1534
+ }
1535
+ clear() {
1536
+ this.#responseCache?.clear();
1537
+ }
1538
+ wrap(fetchTransport) {
1539
+ return this.#responseCache ? new CachingTransport(fetchTransport, this.#responseCache) : fetchTransport;
1540
+ }
1541
+ static validate(options) {
1542
+ if (!Number.isSafeInteger(options.ttlMilliseconds) || options.ttlMilliseconds <= 0) {
1543
+ throw new TypeError("SiloOptions.cache.ttlMilliseconds must be a positive finite safe integer");
1544
+ }
1545
+ if (options.maxEntries !== undefined && options.maxEntries !== Infinity && (!Number.isSafeInteger(options.maxEntries) || options.maxEntries <= 0)) {
1546
+ throw new TypeError("SiloOptions.cache.maxEntries must be a positive safe integer or Infinity");
1547
+ }
1548
+ }
1549
+ }
1550
+
1414
1551
  // src/errors/silo-error.ts
1415
1552
  class SiloError extends Error {
1416
1553
  status;
@@ -1705,61 +1842,80 @@ class ResponseDecoder {
1705
1842
  }
1706
1843
  }
1707
1844
 
1708
- // src/transport/transport.ts
1709
- class Transport {
1710
- url;
1711
- key;
1712
- headers;
1713
- timeoutMilliseconds;
1714
- fetchFunction;
1845
+ // src/transport/fetch-transport.ts
1846
+ class FetchTransport {
1847
+ #url;
1848
+ #key;
1849
+ #headers;
1850
+ #timeoutMilliseconds;
1851
+ #fetchFunction;
1715
1852
  constructor(options) {
1716
- this.url = Transport.normalizeUrl(options.url);
1717
- this.key = options.key;
1718
- this.headers = options.headers ?? {};
1719
- this.timeoutMilliseconds = options.timeoutMilliseconds;
1720
- this.fetchFunction = Transport.resolveFetch(options.fetch);
1853
+ this.#url = FetchTransport.normalizeUrl(options.url);
1854
+ this.#key = options.key;
1855
+ this.#headers = options.headers ?? {};
1856
+ this.#timeoutMilliseconds = options.timeoutMilliseconds;
1857
+ this.#fetchFunction = FetchTransport.resolveFetch(options.fetch);
1721
1858
  }
1722
1859
  async json(request) {
1723
1860
  const response = await this.execute(request);
1724
1861
  return await ResponseDecoder.decode(response, request, true);
1725
1862
  }
1863
+ async jsonPrepared(request, prepared) {
1864
+ const response = await this.execute(request, prepared);
1865
+ return await ResponseDecoder.decode(response, request, true);
1866
+ }
1726
1867
  async empty(request) {
1727
1868
  const response = await this.execute(request);
1728
1869
  await ResponseDecoder.decode(response, request, false);
1729
1870
  }
1730
1871
  async stream(request) {
1731
- const response = await this.execute(request);
1732
- return response.body;
1872
+ return (await this.execute(request)).body;
1733
1873
  }
1734
1874
  async upload(request, form) {
1735
- const response = await this.execute(request, form);
1875
+ const response = await this.execute(request, undefined, form);
1736
1876
  return await ResponseDecoder.decode(response, request, true);
1737
1877
  }
1738
1878
  withKey(key) {
1739
- return new Transport({ ...this.snapshot(), key });
1879
+ return new FetchTransport({ ...this.snapshot(), key });
1740
1880
  }
1741
1881
  withUrl(url) {
1742
- return new Transport({ ...this.snapshot(), url });
1882
+ return new FetchTransport({ ...this.snapshot(), url });
1883
+ }
1884
+ prepare(request, form) {
1885
+ const rawHeaders = { ...this.#headers, ...request.headers };
1886
+ if (this.#key)
1887
+ rawHeaders.Authorization = `Bearer ${this.#key}`;
1888
+ if (!form && request.body !== undefined && !("Content-Type" in rawHeaders)) {
1889
+ rawHeaders["Content-Type"] = "application/json";
1890
+ }
1891
+ return {
1892
+ url: `${this.#url}${request.path}${QueryString.build(request.query)}`,
1893
+ headers: new Headers(rawHeaders),
1894
+ body: form ?? (request.body === undefined ? undefined : JSON.stringify(request.body))
1895
+ };
1896
+ }
1897
+ preparationFailure(request, caught) {
1898
+ return new NetworkError(request.method, request.path, caught);
1743
1899
  }
1744
1900
  snapshot() {
1745
1901
  return {
1746
- url: this.url,
1747
- key: this.key,
1748
- headers: this.headers,
1749
- timeoutMilliseconds: this.timeoutMilliseconds,
1750
- fetch: this.fetchFunction
1902
+ url: this.#url,
1903
+ key: this.#key,
1904
+ headers: this.#headers,
1905
+ timeoutMilliseconds: this.#timeoutMilliseconds,
1906
+ fetch: this.#fetchFunction
1751
1907
  };
1752
1908
  }
1753
- async execute(request, form) {
1754
- const abortSignals = new AbortSignals(request.signal, request.timeoutMilliseconds ?? this.timeoutMilliseconds);
1755
- const url = `${this.url}${request.path}${QueryString.build(request.query)}`;
1756
- const sendRequest = this.fetchFunction;
1909
+ async execute(request, prepared, form) {
1910
+ const abortSignals = new AbortSignals(request.signal, request.timeoutMilliseconds ?? this.#timeoutMilliseconds);
1911
+ const sendRequest = this.#fetchFunction;
1757
1912
  let response;
1758
1913
  try {
1759
- response = await sendRequest(url, {
1914
+ const outbound = prepared ?? this.prepare(request, form);
1915
+ response = await sendRequest(outbound.url, {
1760
1916
  method: request.method,
1761
- headers: this.buildHeaders(request, Boolean(form)),
1762
- body: form ?? (request.body === undefined ? undefined : JSON.stringify(request.body)),
1917
+ headers: outbound.headers,
1918
+ body: outbound.body,
1763
1919
  signal: abortSignals.signal
1764
1920
  });
1765
1921
  } catch (caught) {
@@ -1773,24 +1929,13 @@ class Transport {
1773
1929
  return response;
1774
1930
  }
1775
1931
  transportFailure(request, abortSignals, caught) {
1776
- const firedBy = abortSignals.firedBy();
1777
- if (firedBy === "timeout") {
1778
- return new TimeoutError(request.method, request.path, request.timeoutMilliseconds ?? this.timeoutMilliseconds ?? 0);
1932
+ if (abortSignals.firedBy() === "timeout") {
1933
+ return new TimeoutError(request.method, request.path, request.timeoutMilliseconds ?? this.#timeoutMilliseconds ?? 0);
1779
1934
  }
1780
- if (firedBy === "caller") {
1935
+ if (abortSignals.firedBy() === "caller")
1781
1936
  return new RequestAbortedError(request.method, request.path);
1782
- }
1783
1937
  return new NetworkError(request.method, request.path, caught);
1784
1938
  }
1785
- buildHeaders(request, isUpload) {
1786
- const headers = { ...this.headers, ...request.headers };
1787
- if (this.key)
1788
- headers["Authorization"] = `Bearer ${this.key}`;
1789
- if (!isUpload && request.body !== undefined && !("Content-Type" in headers)) {
1790
- headers["Content-Type"] = "application/json";
1791
- }
1792
- return headers;
1793
- }
1794
1939
  static normalizeUrl(url) {
1795
1940
  return url.replace(/\/+$/, "");
1796
1941
  }
@@ -1808,17 +1953,20 @@ class Transport {
1808
1953
  class Silo {
1809
1954
  projects;
1810
1955
  media;
1956
+ cache;
1811
1957
  options;
1812
1958
  transport;
1813
1959
  constructor(options) {
1814
- this.options = options;
1815
- this.transport = new Transport({
1960
+ this.cache = options.cache instanceof SiloCache ? options.cache : new SiloCache(options.cache);
1961
+ this.options = { ...options, cache: this.cache };
1962
+ const fetchTransport = new FetchTransport({
1816
1963
  url: options.url,
1817
1964
  key: options.key,
1818
1965
  headers: options.headers,
1819
1966
  timeoutMilliseconds: options.timeoutMilliseconds,
1820
1967
  fetch: options.fetch
1821
1968
  });
1969
+ this.transport = this.cache.wrap(fetchTransport);
1822
1970
  this.projects = new Projects(this.transport);
1823
1971
  this.media = new Media(this.transport);
1824
1972
  }
@@ -1836,7 +1984,8 @@ class Silo {
1836
1984
  method: "GET",
1837
1985
  path: ApiPath.health(),
1838
1986
  signal: options?.signal,
1839
- timeoutMilliseconds: options?.timeoutMilliseconds
1987
+ timeoutMilliseconds: options?.timeoutMilliseconds,
1988
+ cache: "bypass"
1840
1989
  });
1841
1990
  }
1842
1991
  withKey(key) {
package/dist/index.d.cts CHANGED
@@ -5,7 +5,10 @@
5
5
  * change rather than a hand-built request.
6
6
  */
7
7
  export { Silo } from "./silo.cjs";
8
+ export { SiloCache } from "./cache/silo-cache.cjs";
8
9
  export type { SiloOptions } from "./silo-options.cjs";
10
+ export type { CacheMode } from "./cache/cache-mode.cjs";
11
+ export type { SiloCacheOptions } from "./cache/silo-cache-options.cjs";
9
12
  export type { RequestOptions } from "./request-options.cjs";
10
13
  export type { FetchFunction } from "./transport/fetch-function.cjs";
11
14
  export { RouteInventory } from "./transport/route-inventory.cjs";
package/dist/index.d.ts CHANGED
@@ -5,7 +5,10 @@
5
5
  * change rather than a hand-built request.
6
6
  */
7
7
  export { Silo } from "./silo.js";
8
+ export { SiloCache } from "./cache/silo-cache.js";
8
9
  export type { SiloOptions } from "./silo-options.js";
10
+ export type { CacheMode } from "./cache/cache-mode.js";
11
+ export type { SiloCacheOptions } from "./cache/silo-cache-options.js";
9
12
  export type { RequestOptions } from "./request-options.js";
10
13
  export type { FetchFunction } from "./transport/fetch-function.js";
11
14
  export { RouteInventory } from "./transport/route-inventory.js";