@ryusenkei/maezae 0.12.0 → 0.13.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.
package/README.md CHANGED
@@ -31,6 +31,34 @@ const client = createDeliveryClient((path, init) =>
31
31
  );
32
32
  ```
33
33
 
34
+ ## 回数の制限
35
+
36
+ - 対象は公開のエンドポイント(`https://api.maezae.app/<媒体>/content/v1/…`)だけ。上限は媒体と API の鍵ごとに1分で600回で、データセンターごとの近似で数える
37
+ - 数えるのは、CMS がキャッシュから返せずに DB で計算する要求だけ。キャッシュから返す応答と、`If-None-Match` に対する304は数えない
38
+ - 上限を超えると429(`code: "rate_limited"`)を返す。`Retry-After` の秒数だけ待ってから再試行する
39
+ - 呼び出しを減らす方法: 1ページの取得を `prefetch`(`/bundle`)で1回にまとめ、接続を `cachedDelivery` で包む
40
+
41
+ ## 接続のキャッシュ(0.13.0 から)
42
+
43
+ `cachedDelivery` は同じパスとクエリの取得を保持し、配信 API への呼び出しを減らす
44
+
45
+ - 保持から `freshSec` 秒(既定は30)は呼び出さずに返す。過ぎたら前回の ETag で再検証し、304は回数の制限に数えない
46
+ - 429・5xx・接続の失敗のときは、保持した値を `x-stale: 1` つきで返す
47
+ - 下書き(`drafts`)・GET 以外・`prefetch` の `ifNoneMatch` を付けた取得は保持しない
48
+ - 保持の場所: Workers は `caches.default`(データセンターごとに共有)、ほかの実行環境は `memoryDeliveryCache()`
49
+ - `origin` はキーの前半で、媒体ごとに別の値にする
50
+
51
+ ```ts
52
+ import { cachedDelivery, createDeliveryClient } from '@ryusenkei/maezae/delivery';
53
+
54
+ const origin = 'https://<媒体>.maezae.app';
55
+ const delivery = (path: string, init?: RequestInit) =>
56
+ fetch(`https://api.maezae.app/<媒体>${path}`, { ...init, headers: { ...init?.headers, authorization: `Bearer ${env.CMS_API_KEY}` } });
57
+ const client = createDeliveryClient(cachedDelivery(delivery, { cache: caches.default, origin }));
58
+ ```
59
+
60
+ 公開の操作の反映は、CMS の側の遅れに加えて最大で `freshSec` 秒遅れる
61
+
34
62
  ## プレビュー
35
63
 
36
64
  - CMS は媒体のサイトのオリジンへ、公開のページと同じパスで署名つきのリクエストを送信
package/dist/delivery.js CHANGED
@@ -1,4 +1,96 @@
1
1
  import { a as optionsOf, l as cropRect, o as typeOf, p as createClock, t as pagePathOf, u as croppedHotspot } from "./page-BnwHL-LC.js";
2
+ // src/delivery/cache.ts
3
+ /** 保持した日時(ミリ秒)を置く内部のヘッダー */
4
+ const CACHED_AT = "x-maezae-cached-at";
5
+ /** 失敗のときに返せるように保持する期間(秒) */
6
+ const KEEP_SEC = 86400;
7
+ const withHeaders = (res, body, set, drop = []) => {
8
+ const headers = new Headers(res.headers);
9
+ for (const k of drop) headers.delete(k);
10
+ for (const [k, v] of Object.entries(set)) headers.set(k, v);
11
+ return new Response(body, {
12
+ status: res.status,
13
+ statusText: res.statusText,
14
+ headers
15
+ });
16
+ };
17
+ /**
18
+ * 保持つきの配信 API の接続(createDeliveryClient に渡す)
19
+ * - origin: キーの前半(媒体ごとに別の値、例: https://<媒体>.maezae.app)。同じ保持の場所を複数の媒体で使っても混ざらない
20
+ * - freshSec: 呼び出さずに返す秒数(既定は30、CMS のキャッシュの区切りと同じ)
21
+ */
22
+ function cachedDelivery(delivery, { cache, origin, freshSec = 30 }) {
23
+ const store = async (key, res, body) => {
24
+ try {
25
+ await cache.put(key, withHeaders(res, body, {
26
+ [CACHED_AT]: String(Date.now()),
27
+ "cache-control": `max-age=${KEEP_SEC}`
28
+ }));
29
+ } catch {}
30
+ };
31
+ const fromCache = (res, body, extra = {}) => withHeaders(res, body, extra, [CACHED_AT, "cache-control"]);
32
+ return async (path, init = {}) => {
33
+ const headers = new Headers(init.headers);
34
+ const key = `${origin}${path}`;
35
+ if ((init.method ?? "GET").toUpperCase() !== "GET" || headers.has("if-none-match") || new URL(key).searchParams.get("drafts") === "1") return delivery(path, init);
36
+ const cached = await cache.match(key).catch(() => null) ?? null;
37
+ if (cached && Date.now() - Number(cached.headers.get(CACHED_AT)) < freshSec * 1e3) return fromCache(cached, cached.body);
38
+ const etag = cached?.headers.get("etag");
39
+ if (etag) headers.set("if-none-match", etag);
40
+ let res;
41
+ try {
42
+ res = await delivery(path, {
43
+ ...init,
44
+ headers
45
+ });
46
+ } catch (e) {
47
+ if (cached) return fromCache(cached, cached.body, { "x-stale": "1" });
48
+ throw e;
49
+ }
50
+ if (res.status === 304 && cached) {
51
+ const body = await cached.text();
52
+ await store(key, cached, body);
53
+ return fromCache(cached, body);
54
+ }
55
+ if (res.status === 200 && !res.headers.has("x-partial") && !res.headers.has("x-stale")) {
56
+ const body = await res.text();
57
+ await store(key, res, body);
58
+ return fromCache(res, body);
59
+ }
60
+ if (cached && (res.status === 429 || res.status >= 500)) {
61
+ await res.body?.cancel();
62
+ return fromCache(cached, cached.body, { "x-stale": "1" });
63
+ }
64
+ return res;
65
+ };
66
+ }
67
+ /** メモリの保持の場所(Workers 以外の実行環境向け、maxEntries を超えたら最も古く使用したものから破棄) */
68
+ function memoryDeliveryCache(maxEntries = 500) {
69
+ const entries = /* @__PURE__ */ new Map();
70
+ return {
71
+ match: async (key) => {
72
+ const v = entries.get(key);
73
+ if (!v) return null;
74
+ entries.delete(key);
75
+ entries.set(key, v);
76
+ return new Response(v.body, {
77
+ status: v.status,
78
+ headers: v.headers
79
+ });
80
+ },
81
+ put: async (key, res) => {
82
+ entries.delete(key);
83
+ const headers = [];
84
+ res.headers.forEach((v, k) => headers.push([k, v]));
85
+ entries.set(key, {
86
+ status: res.status,
87
+ headers,
88
+ body: await res.text()
89
+ });
90
+ if (entries.size > maxEntries) entries.delete(entries.keys().next().value);
91
+ }
92
+ };
93
+ }
2
94
  // src/delivery/query.ts
3
95
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
96
  /** in の値の検査(コンマを含む値はクエリで区切れない) */
@@ -306,4 +398,4 @@ function createDeliveryClock(timeZone) {
306
398
  })
307
399
  };
308
400
  }
309
- export { DELIVERY_PATH, DeliveryError, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
401
+ export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
@@ -0,0 +1,18 @@
1
+ import type { Delivery } from './client.js';
2
+ /** Cache API と同じ形の保持の場所(key は URL) */
3
+ export type DeliveryCache = Readonly<{
4
+ match(key: string): Promise<Response | null | undefined>;
5
+ put(key: string, response: Response): Promise<void>;
6
+ }>;
7
+ /**
8
+ * 保持つきの配信 API の接続(createDeliveryClient に渡す)
9
+ * - origin: キーの前半(媒体ごとに別の値、例: https://<媒体>.maezae.app)。同じ保持の場所を複数の媒体で使っても混ざらない
10
+ * - freshSec: 呼び出さずに返す秒数(既定は30、CMS のキャッシュの区切りと同じ)
11
+ */
12
+ export declare function cachedDelivery(delivery: Delivery, { cache, origin, freshSec }: {
13
+ cache: DeliveryCache;
14
+ origin: string;
15
+ freshSec?: number;
16
+ }): Delivery;
17
+ /** メモリの保持の場所(Workers 以外の実行環境向け、maxEntries を超えたら最も古く使用したものから破棄) */
18
+ export declare function memoryDeliveryCache(maxEntries?: number): DeliveryCache;
@@ -1,3 +1,4 @@
1
+ export { cachedDelivery, memoryDeliveryCache, type DeliveryCache } from './cache.js';
1
2
  export { createDeliveryClient, DELIVERY_PATH, DeliveryError, subPath, type BundleResult, type Delivery, type DeliveryClient, type Prefetched } from './client.js';
2
3
  export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media.js';
3
4
  export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../../model/src/index.js';
@@ -1,2 +1,2 @@
1
- export { createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from './delivery/index.js';
2
- export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from './delivery/index.js';
1
+ export { cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from './delivery/index.js';
2
+ export type { BundleResult, DeliveredAsset, Delivery, DeliveryCache, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from './delivery/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",