@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 +42 -1
- package/dist/cache/cache-mode.d.cts +2 -0
- package/dist/cache/cache-mode.d.ts +2 -0
- package/dist/cache/response-cache.d.cts +16 -0
- package/dist/cache/response-cache.d.ts +16 -0
- package/dist/cache/silo-cache-options.d.cts +7 -0
- package/dist/cache/silo-cache-options.d.ts +7 -0
- package/dist/cache/silo-cache.d.cts +11 -0
- package/dist/cache/silo-cache.d.ts +11 -0
- package/dist/index.cjs +209 -60
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +209 -60
- package/dist/media/media-page.d.cts +1 -0
- package/dist/media/media-page.d.ts +1 -0
- package/dist/media/media-usage-page.d.cts +1 -0
- package/dist/media/media-usage-page.d.ts +1 -0
- package/dist/request-options.d.cts +3 -0
- package/dist/request-options.d.ts +3 -0
- package/dist/silo-options.d.cts +4 -0
- package/dist/silo-options.d.ts +4 -0
- package/dist/silo.d.cts +3 -0
- package/dist/silo.d.ts +3 -0
- package/dist/transport/caching-transport.d.cts +14 -0
- package/dist/transport/caching-transport.d.ts +14 -0
- package/dist/transport/fetch-transport.d.cts +31 -0
- package/dist/transport/fetch-transport.d.ts +31 -0
- package/dist/transport/prepared-transport-request.d.cts +6 -0
- package/dist/transport/prepared-transport-request.d.ts +6 -0
- package/dist/transport/transport-request.d.cts +2 -0
- package/dist/transport/transport-request.d.ts +2 -0
- package/dist/transport/transport.d.cts +2 -43
- package/dist/transport/transport.d.ts +2 -43
- package/package.json +6 -3
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.
|
|
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,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
|
-
|
|
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
|
|
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
|
|
1717
|
-
this
|
|
1718
|
-
this
|
|
1719
|
-
this
|
|
1720
|
-
this
|
|
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
|
-
|
|
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
|
|
1879
|
+
return new FetchTransport({ ...this.snapshot(), key });
|
|
1740
1880
|
}
|
|
1741
1881
|
withUrl(url) {
|
|
1742
|
-
return new
|
|
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
|
|
1747
|
-
key: this
|
|
1748
|
-
headers: this
|
|
1749
|
-
timeoutMilliseconds: this
|
|
1750
|
-
fetch: this
|
|
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
|
|
1755
|
-
const
|
|
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
|
-
|
|
1914
|
+
const outbound = prepared ?? this.prepare(request, form);
|
|
1915
|
+
response = await sendRequest(outbound.url, {
|
|
1760
1916
|
method: request.method,
|
|
1761
|
-
headers:
|
|
1762
|
-
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
|
-
|
|
1777
|
-
|
|
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.
|
|
1815
|
-
this.
|
|
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";
|