@notion-headless-cms/core 0.5.2 → 0.5.3
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 +3 -2
- package/dist/cache/memory.d.mts +1 -1
- package/dist/cache/memory.mjs +0 -5
- package/dist/cache/memory.mjs.map +1 -1
- package/dist/{cache-v9jTMnYd.d.mts → cache-BnC6kxoU.d.mts} +7 -1
- package/dist/{config-Ccs0RxoN.d.mts → config-RnU3jrFb.d.mts} +9 -2
- package/dist/index.d.mts +43 -5
- package/dist/index.mjs +219 -74
- package/dist/index.mjs.map +1 -1
- package/dist/preset/node.d.mts +2 -2
- package/dist/source-author.d.mts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,13 +75,14 @@ declare module "@notion-headless-cms/core" {
|
|
|
75
75
|
- `list(opts?)` — 公開済み一覧(本文なし)
|
|
76
76
|
- `params()` — SSG 用 slug 一覧
|
|
77
77
|
- `cache.adjacent(slug, opts?)` — 前後記事ナビゲーション
|
|
78
|
-
- `cache.warm(opts?)` / `cache.invalidate(scope?)` / `cache.checkItem(...)` —
|
|
78
|
+
- `cache.warm(opts?)` / `cache.prime(slug)` / `cache.invalidate(scope?)` / `cache.checkItem(...)` — キャッシュ管理(`prime` は単件ウォーム)
|
|
79
79
|
|
|
80
80
|
### グローバル操作
|
|
81
81
|
|
|
82
82
|
- `cms.collections` — 登録コレクション名一覧
|
|
83
83
|
- `cms.invalidate(scope?)` — `"all" | { collection } | { collection, slug }`
|
|
84
|
-
- `cms.
|
|
84
|
+
- `cms.warmByPageId(pageId)` — Notion ページ ID から該当 1 件を単件ウォーム(公式 webhook 用)
|
|
85
|
+
- `cms.handler(opts?)` — Web Standard `(req) => Response`(画像プロキシ + webhook + `notion-webhook`)
|
|
85
86
|
- `cms.getCachedImage(hash)` — 画像キャッシュ直アクセス
|
|
86
87
|
- `cms.cacheImage(url)` — Notion 画像 URL を `{imageProxyBase}/{sha256}` 形式に変換し、永続キャッシュへ書き込む(画像キャッシュ未設定なら `undefined`)
|
|
87
88
|
- `cms.imageProxyBase` — 画像プロキシのベース URL
|
package/dist/cache/memory.d.mts
CHANGED
package/dist/cache/memory.mjs
CHANGED
|
@@ -16,13 +16,11 @@ var MemoryDocumentOps = class {
|
|
|
16
16
|
metas = /* @__PURE__ */ new Map();
|
|
17
17
|
contents = /* @__PURE__ */ new Map();
|
|
18
18
|
maxItems;
|
|
19
|
-
/** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */
|
|
20
19
|
hits = 0;
|
|
21
20
|
misses = 0;
|
|
22
21
|
constructor(options) {
|
|
23
22
|
this.maxItems = options?.maxItems;
|
|
24
23
|
}
|
|
25
|
-
/** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */
|
|
26
24
|
totalEntries() {
|
|
27
25
|
return this.lists.size + this.metas.size + this.contents.size;
|
|
28
26
|
}
|
|
@@ -108,13 +106,11 @@ var MemoryDocumentOps = class {
|
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
};
|
|
111
|
-
/** インメモリの画像オペレーション実装。 */
|
|
112
109
|
var MemoryImageOps = class {
|
|
113
110
|
store = /* @__PURE__ */ new Map();
|
|
114
111
|
totalBytes = 0;
|
|
115
112
|
maxItems;
|
|
116
113
|
maxSizeBytes;
|
|
117
|
-
/** `cms.stats()` 用のヒット/ミス集計。 */
|
|
118
114
|
hits = 0;
|
|
119
115
|
misses = 0;
|
|
120
116
|
constructor(options) {
|
|
@@ -129,7 +125,6 @@ var MemoryImageOps = class {
|
|
|
129
125
|
} else this.misses++;
|
|
130
126
|
return Promise.resolve(entry ?? null);
|
|
131
127
|
}
|
|
132
|
-
/** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */
|
|
133
128
|
snapshot() {
|
|
134
129
|
return {
|
|
135
130
|
entries: this.store.size,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CacheAreaStats,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n /** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */\n totalEntries(): number {\n return this.lists.size + this.metas.size + this.contents.size;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n const entry = this.lists.get(collection) as CachedItemList<T> | undefined;\n if (entry) this.hits++;\n else this.misses++;\n return Promise.resolve(entry ?? null);\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) {\n touch(this.metas, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) {\n touch(this.contents, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n // コレクション全体\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\n/** インメモリの画像オペレーション実装。 */\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) {\n touch(this.store, hash);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n /** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */\n snapshot(): { entries: number; sizeBytes: number } {\n return { entries: this.store.size, sizeBytes: this.totalBytes };\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n const doc = new MemoryDocumentOps(options);\n const img = new MemoryImageOps(options);\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc,\n img,\n async stats(): Promise<CacheAdapterStats> {\n const imgSnap = img.snapshot();\n const docArea: CacheAreaStats = {\n hits: doc.hits,\n misses: doc.misses,\n entries: doc.totalEntries(),\n };\n const imgArea: CacheAreaStats = {\n hits: img.hits,\n misses: img.misses,\n entries: imgSnap.entries,\n sizeBytes: imgSnap.sizeBytes,\n };\n return { name: \"memory\", doc: docArea, img: imgArea };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CacheAreaStats,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n totalEntries(): number {\n return this.lists.size + this.metas.size + this.contents.size;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n const entry = this.lists.get(collection) as CachedItemList<T> | undefined;\n if (entry) this.hits++;\n else this.misses++;\n return Promise.resolve(entry ?? null);\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) {\n touch(this.metas, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) {\n touch(this.contents, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) {\n touch(this.store, hash);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n snapshot(): { entries: number; sizeBytes: number } {\n return { entries: this.store.size, sizeBytes: this.totalBytes };\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n const doc = new MemoryDocumentOps(options);\n const img = new MemoryImageOps(options);\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc,\n img,\n async stats(): Promise<CacheAdapterStats> {\n const imgSnap = img.snapshot();\n const docArea: CacheAreaStats = {\n hits: doc.hits,\n misses: doc.misses,\n entries: doc.totalEntries(),\n };\n const imgArea: CacheAreaStats = {\n hits: img.hits,\n misses: img.misses,\n entries: imgSnap.entries,\n sizeBytes: imgSnap.sizeBytes,\n };\n return { name: \"memory\", doc: docArea, img: imgArea };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -69,6 +69,12 @@ interface DataSource<T extends BaseContentItem = BaseContentItem> {
|
|
|
69
69
|
* Core が slug フィールドのルックアップに使用する。
|
|
70
70
|
*/
|
|
71
71
|
findByProp?(notionPropName: string, value: string): Promise<T | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Notion ページ ID で1件取得する(公式 webhook の sparse payload は page id のみを送るため、
|
|
74
|
+
* `warmByPageId` がこれで slug を解決する)。
|
|
75
|
+
* 自身の data source に属さないページを渡された場合は `null` を返す実装とする。
|
|
76
|
+
*/
|
|
77
|
+
findById?(pageId: string): Promise<T | null>;
|
|
72
78
|
/** アイテム本文を ContentBlock 配列で返す。 */
|
|
73
79
|
loadBlocks(item: T): Promise<ContentBlock[]>;
|
|
74
80
|
/** アイテム本文を Markdown 文字列で返す (html() 生成の元ソース)。 */
|
|
@@ -195,4 +201,4 @@ interface CacheAdapter {
|
|
|
195
201
|
}
|
|
196
202
|
//#endregion
|
|
197
203
|
export { ImageCacheOps as a, InvalidateScope as c, WebhookConfig as d, DocumentCacheOps as i, PropertyDef as l, CacheAdapterStats as n, DataSource as o, CacheAreaStats as r, InvalidateKind as s, CacheAdapter as t, PropertyMap as u };
|
|
198
|
-
//# sourceMappingURL=cache-
|
|
204
|
+
//# sourceMappingURL=cache-BnC6kxoU.d.mts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
|
|
2
|
-
import { o as DataSource, t as CacheAdapter } from "./cache-
|
|
2
|
+
import { o as DataSource, t as CacheAdapter } from "./cache-BnC6kxoU.mjs";
|
|
3
3
|
import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BPhaVO6-.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/types/sources.d.ts
|
|
@@ -167,6 +167,13 @@ interface CreateClientOptions<S extends CMSSources = CMSSources> {
|
|
|
167
167
|
imageProxyBase?: string;
|
|
168
168
|
/** Cloudflare Workers の `waitUntil` に相当する非同期処理の登録関数。 */
|
|
169
169
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
170
|
+
/**
|
|
171
|
+
* Notion 公式 webhook(integration の Webhooks)の検証トークン(HMAC 署名キー)。
|
|
172
|
+
* 設定すると `cms.handler()` の `POST {basePath}/notion-webhook` が署名検証つきで有効になり、
|
|
173
|
+
* 受信イベントの対象ページを `warmByPageId` で再ウォームする。
|
|
174
|
+
* `cms.handler({ notionWebhook: { secret } })` で個別上書きも可能。
|
|
175
|
+
*/
|
|
176
|
+
notionWebhookSecret?: string;
|
|
170
177
|
/** ライフサイクルフック (全コレクション共通)。 */
|
|
171
178
|
hooks?: CMSHooks<BaseContentItem>;
|
|
172
179
|
/** プラグイン配列。 */
|
|
@@ -182,4 +189,4 @@ interface CreateClientOptions<S extends CMSSources = CMSSources> {
|
|
|
182
189
|
}
|
|
183
190
|
//#endregion
|
|
184
191
|
export { MergeSourceCollections as _, DEFAULT_RATE_LIMITER as a, RateLimiterConfig as c, RendererPluginList as d, SWRConfig as f, CMSSources as g, CMSAdapter as h, CreateClientOptions as i, RenderOptions as l, defineCollection as m, CollectionsConfig as n, InferCollectionItem as o, StrictCollectionDef as p, ContentConfig as r, LogLevel as s, CollectionDef as t, RendererFn as u };
|
|
185
|
-
//# sourceMappingURL=config-
|
|
192
|
+
//# sourceMappingURL=config-RnU3jrFb.d.mts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as CachedItemMeta, c as ContentResult, i as CachedItemList, l as ImageRef, n as CMSSchemaProperties, o as StorageBinary, r as CachedItemContent, s as ContentBlock, t as BaseContentItem, u as InlineNode } from "./content-DwsfWZao.mjs";
|
|
2
|
-
import { a as ImageCacheOps, c as InvalidateScope, d as WebhookConfig, i as DocumentCacheOps, l as PropertyDef, n as CacheAdapterStats, o as DataSource, r as CacheAreaStats, s as InvalidateKind, t as CacheAdapter, u as PropertyMap } from "./cache-
|
|
2
|
+
import { a as ImageCacheOps, c as InvalidateScope, d as WebhookConfig, i as DocumentCacheOps, l as PropertyDef, n as CacheAdapterStats, o as DataSource, r as CacheAreaStats, s as InvalidateKind, t as CacheAdapter, u as PropertyMap } from "./cache-BnC6kxoU.mjs";
|
|
3
3
|
import { a as isCMSError, i as CMSErrorContext, n as CMSError, o as isCMSErrorInNamespace, r as CMSErrorCode, s as matchCMSError, t as BuiltInCMSErrorCode } from "./errors-DPD5yq9S.mjs";
|
|
4
4
|
import { a as CMSHooks, i as Logger, n as definePlugin, o as MaybePromise, r as LogContext, t as CMSPlugin } from "./plugin-BPhaVO6-.mjs";
|
|
5
|
-
import { _ as MergeSourceCollections, a as DEFAULT_RATE_LIMITER, c as RateLimiterConfig, d as RendererPluginList, f as SWRConfig, g as CMSSources, i as CreateClientOptions, l as RenderOptions, m as defineCollection, n as CollectionsConfig, o as InferCollectionItem, p as StrictCollectionDef, r as ContentConfig, s as LogLevel, u as RendererFn } from "./config-
|
|
5
|
+
import { _ as MergeSourceCollections, a as DEFAULT_RATE_LIMITER, c as RateLimiterConfig, d as RendererPluginList, f as SWRConfig, g as CMSSources, i as CreateClientOptions, l as RenderOptions, m as defineCollection, n as CollectionsConfig, o as InferCollectionItem, p as StrictCollectionDef, r as ContentConfig, s as LogLevel, u as RendererFn } from "./config-RnU3jrFb.mjs";
|
|
6
6
|
import { MemoryCacheOptions, memoryCache } from "./cache/memory.mjs";
|
|
7
7
|
import { mergeHooks, mergeLoggers } from "./hooks.mjs";
|
|
8
8
|
import { NodePresetOptions, nodePreset } from "./preset/node.mjs";
|
|
@@ -96,6 +96,11 @@ interface CollectionCacheOps<T extends BaseContentItem> {
|
|
|
96
96
|
* SSG ビルド前のウォームアップに利用する。
|
|
97
97
|
*/
|
|
98
98
|
warm(opts?: WarmOptions): Promise<WarmResult>;
|
|
99
|
+
/**
|
|
100
|
+
* 指定 slug の1件だけを Notion から再取得し、メタ・本文キャッシュを作り直す(単件ウォーム)。
|
|
101
|
+
* webhook 受信時など、更新された1ページだけを温め直すのに使う。アイテムが存在しなければ何もしない。
|
|
102
|
+
*/
|
|
103
|
+
prime(slug: string): Promise<void>;
|
|
99
104
|
}
|
|
100
105
|
/** `check()` の戻り値。差分なしか、差分ありの場合はアイテムを含む。 */
|
|
101
106
|
type CheckResult<T extends BaseContentItem> = {
|
|
@@ -167,7 +172,6 @@ declare const noopDocOps: DocumentCacheOps;
|
|
|
167
172
|
declare const noopImgOps: ImageCacheOps;
|
|
168
173
|
//#endregion
|
|
169
174
|
//#region src/handler.d.ts
|
|
170
|
-
/** `$handler()` の挙動設定。 */
|
|
171
175
|
interface HandlerOptions {
|
|
172
176
|
/** マウントするベースパス。デフォルト `/api/cms`。 */
|
|
173
177
|
basePath?: string;
|
|
@@ -179,12 +183,26 @@ interface HandlerOptions {
|
|
|
179
183
|
versionsPath?: string;
|
|
180
184
|
/** 更新チェック (check) のパス (basePath 相対)。デフォルト `/check`。 */
|
|
181
185
|
checkPath?: string;
|
|
186
|
+
/** Notion 公式 webhook 受信のパス (basePath 相対)。デフォルト `/notion-webhook`。 */
|
|
187
|
+
notionWebhookPath?: string;
|
|
182
188
|
/** Webhook 署名検証用シークレット (未指定なら検証スキップ)。 */
|
|
183
189
|
webhookSecret?: string;
|
|
190
|
+
/** Notion 公式 webhook(integration の Webhooks)の受信設定。 */
|
|
191
|
+
notionWebhook?: {
|
|
192
|
+
/**
|
|
193
|
+
* 検証トークン(HMAC-SHA256 署名キー)。未指定時は createCMS の
|
|
194
|
+
* `notion.webhookSecret`(= `CreateClientOptions.notionWebhookSecret`)を既定で使う。
|
|
195
|
+
*/
|
|
196
|
+
secret?: string;
|
|
197
|
+
/**
|
|
198
|
+
* サブスク登録時に Notion が送る `verification_token` を受け取るコールバック。
|
|
199
|
+
* 値を控えて `notion.webhookSecret` に設定する用途(既定ではレスポンス本文にも echo する)。
|
|
200
|
+
*/
|
|
201
|
+
onVerificationToken?: (token: string) => void;
|
|
202
|
+
};
|
|
184
203
|
/** デフォルト実装を無効化する場合 true。 */
|
|
185
204
|
disabled?: boolean;
|
|
186
205
|
}
|
|
187
|
-
/** `$handler()` が内部で依存する CMS 機能の最小セット。 */
|
|
188
206
|
interface HandlerAdapter {
|
|
189
207
|
imageCache: ImageCacheOps;
|
|
190
208
|
/**
|
|
@@ -210,6 +228,18 @@ interface HandlerAdapter {
|
|
|
210
228
|
checkFor(collection: string, slug: string, currentVersion: string): Promise<{
|
|
211
229
|
stale: boolean;
|
|
212
230
|
} | null>;
|
|
231
|
+
/**
|
|
232
|
+
* Notion ページ ID を全コレクション横断で解決し単件ウォームする(公式 webhook 用)。
|
|
233
|
+
* 一致したコレクション / slug、無ければ `null`。
|
|
234
|
+
*/
|
|
235
|
+
warmByPageId(pageId: string): Promise<{
|
|
236
|
+
collection: string;
|
|
237
|
+
slug: string;
|
|
238
|
+
} | null>;
|
|
239
|
+
/** createCMS で設定された Notion webhook 検証トークンの既定値。 */
|
|
240
|
+
notionWebhookSecret?: string;
|
|
241
|
+
/** 応答送信後もウォームを完走させる実行フック (Cloudflare の `waitUntil` 相当)。 */
|
|
242
|
+
scheduleBackground?: (p: Promise<unknown>) => void;
|
|
213
243
|
}
|
|
214
244
|
/**
|
|
215
245
|
* Web Standard な Request → Response ルーター。
|
|
@@ -224,7 +254,6 @@ interface HandlerAdapter {
|
|
|
224
254
|
declare function createHandler(adapter: HandlerAdapter, opts?: HandlerOptions): (req: Request) => Promise<Response>;
|
|
225
255
|
//#endregion
|
|
226
256
|
//#region src/cms.d.ts
|
|
227
|
-
/** コレクション別アクセス + グローバル操作の合成型。 */
|
|
228
257
|
type CMSClient<C extends CollectionsConfig> = { [K in keyof C]: CollectionClient<InferCollectionItem<C[K]>> } & CMSGlobalOps;
|
|
229
258
|
/**
|
|
230
259
|
* `cms.stats()` が返す集約済みキャッシュ統計。
|
|
@@ -257,6 +286,15 @@ interface CMSGlobalOps {
|
|
|
257
286
|
/** クライアント単位の trace ID (`createClient` で発行)。 */
|
|
258
287
|
readonly traceId: string;
|
|
259
288
|
invalidate(scope?: InvalidateScope): Promise<void>;
|
|
289
|
+
/**
|
|
290
|
+
* Notion ページ ID を全コレクション横断で解決し、該当アイテムを単件ウォームする。
|
|
291
|
+
* 公式 webhook の sparse payload(page id のみ)からミラーを再生成するために使う。
|
|
292
|
+
* 一致したコレクションと slug を返す。どのコレクションにも属さなければ `null`。
|
|
293
|
+
*/
|
|
294
|
+
warmByPageId(pageId: string): Promise<{
|
|
295
|
+
collection: string;
|
|
296
|
+
slug: string;
|
|
297
|
+
} | null>;
|
|
260
298
|
/** Web Standard な Request/Response ベースのルートハンドラ (画像プロキシ + webhook)。 */
|
|
261
299
|
handler(opts?: HandlerOptions): (req: Request) => Promise<Response>;
|
|
262
300
|
getCachedImage(hash: string): Promise<StorageBinary | null>;
|
package/dist/index.mjs
CHANGED
|
@@ -19,7 +19,6 @@ function isStale(cachedAt, ttlMs) {
|
|
|
19
19
|
}
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/cache/noop.ts
|
|
22
|
-
/** 何もキャッシュしないドキュメントオペレーション。常に null を返す。 */
|
|
23
22
|
const noopDoc = {
|
|
24
23
|
getList(_collection) {
|
|
25
24
|
return Promise.resolve(null);
|
|
@@ -43,7 +42,6 @@ const noopDoc = {
|
|
|
43
42
|
return Promise.resolve();
|
|
44
43
|
}
|
|
45
44
|
};
|
|
46
|
-
/** 何もキャッシュしない画像オペレーション。 */
|
|
47
45
|
const noopImg = {
|
|
48
46
|
get(_hash) {
|
|
49
47
|
return Promise.resolve(null);
|
|
@@ -59,6 +57,69 @@ const noopImg = {
|
|
|
59
57
|
const noopDocOps = noopDoc;
|
|
60
58
|
const noopImgOps = noopImg;
|
|
61
59
|
//#endregion
|
|
60
|
+
//#region src/page-index.ts
|
|
61
|
+
/**
|
|
62
|
+
* Notion ページ ID を比較用に正規化する。
|
|
63
|
+
* Notion の ID は文脈によりダッシュの有無・大文字小文字が揺れるため、
|
|
64
|
+
* ダッシュ除去 + 小文字化して突き合わせる。
|
|
65
|
+
*
|
|
66
|
+
* react-renderer 側も同一実装で `pageLinks` を引くため、変更時は両方を揃えること。
|
|
67
|
+
*/
|
|
68
|
+
function normalizePageId(id) {
|
|
69
|
+
return id.replace(/-/g, "").toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
function asCollectionClient(source, name) {
|
|
72
|
+
const client = source[name];
|
|
73
|
+
if (!client || typeof client.list !== "function") return void 0;
|
|
74
|
+
return client;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 全コレクションを `list()` で走査し、pageId → {collection, slug, title} の逆引きマップを構築する。
|
|
78
|
+
* Notion 内部リンク(link_to_page / page mention など)を自サイト URL へ解決するための材料。
|
|
79
|
+
*
|
|
80
|
+
* `list()` は SWR ドキュメントキャッシュ経由のためウォーム後は安価。
|
|
81
|
+
*/
|
|
82
|
+
async function buildPageIndex(source, opts) {
|
|
83
|
+
const names = opts?.collections ?? source.collections;
|
|
84
|
+
const index = /* @__PURE__ */ new Map();
|
|
85
|
+
if (!names || typeof names[Symbol.iterator] !== "function") return index;
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
const client = asCollectionClient(source, name);
|
|
88
|
+
if (!client) continue;
|
|
89
|
+
const items = await client.list();
|
|
90
|
+
if (!Array.isArray(items)) continue;
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
const key = normalizePageId(item.id);
|
|
93
|
+
if (!index.has(key)) index.set(key, {
|
|
94
|
+
collection: name,
|
|
95
|
+
slug: item.slug,
|
|
96
|
+
title: item.title
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return index;
|
|
101
|
+
}
|
|
102
|
+
const defaultUrl = (entry) => `/${entry.collection}/${entry.slug}`;
|
|
103
|
+
/**
|
|
104
|
+
* Notion 内部リンクを「正規化 pageId → {href, title}」のプレーンマップに解決する。
|
|
105
|
+
* サーバ側(loader / RSC / route handler)で 1 回構築し、`<NotionRenderer pageLinks={...} />`
|
|
106
|
+
* に渡す。プレーンオブジェクトなのでシリアライズ境界(RSC / loader)を越えられる。
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const pageLinks = await buildPageLinkMap(cms);
|
|
110
|
+
* <NotionRenderer blocks={blocks} pageLinks={pageLinks} />;
|
|
111
|
+
*/
|
|
112
|
+
async function buildPageLinkMap(source, opts) {
|
|
113
|
+
const index = opts?.index ?? await buildPageIndex(source, opts);
|
|
114
|
+
const toUrl = opts?.url ?? defaultUrl;
|
|
115
|
+
const map = {};
|
|
116
|
+
for (const [key, entry] of index) map[key] = {
|
|
117
|
+
href: toUrl(entry, key),
|
|
118
|
+
title: entry.title
|
|
119
|
+
};
|
|
120
|
+
return map;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
62
123
|
//#region src/image.ts
|
|
63
124
|
/**
|
|
64
125
|
* レスポンスの Content-Type ヘッダから画像の MIME タイプを取り出す。
|
|
@@ -332,9 +393,22 @@ var CollectionClientImpl = class {
|
|
|
332
393
|
this.cache = {
|
|
333
394
|
invalidate: () => this.invalidateImpl(),
|
|
334
395
|
invalidateItem: (slug) => this.invalidateItemImpl(slug),
|
|
335
|
-
warm: (opts) => this.warmImpl(opts)
|
|
396
|
+
warm: (opts) => this.warmImpl(opts),
|
|
397
|
+
prime: (slug) => this.primeImpl(slug)
|
|
336
398
|
};
|
|
337
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Notion ページ ID で該当アイテムを解決し、単件ウォーム + リストキャッシュを更新する。
|
|
402
|
+
* このコレクションに属さない page id の場合は何もせず `null` を返す。
|
|
403
|
+
* 一致した場合は温めた slug を返す(公式 webhook から `cms.warmByPageId` 経由で呼ばれる)。
|
|
404
|
+
*/
|
|
405
|
+
async warmByPageId(pageId) {
|
|
406
|
+
const item = await this.resolveByPageId(pageId);
|
|
407
|
+
if (!item) return null;
|
|
408
|
+
await this.primeItem(item);
|
|
409
|
+
await this.refreshList();
|
|
410
|
+
return item.slug;
|
|
411
|
+
}
|
|
338
412
|
async find(slug, opts = {}) {
|
|
339
413
|
if (opts.bypassCache) {
|
|
340
414
|
this.ctx.hooks.onCacheMiss?.(slug);
|
|
@@ -450,9 +524,7 @@ var CollectionClientImpl = class {
|
|
|
450
524
|
const chunk = items.slice(i, i + concurrency);
|
|
451
525
|
await Promise.all(chunk.map(async (item) => {
|
|
452
526
|
try {
|
|
453
|
-
await this.
|
|
454
|
-
const content = await buildCachedItemContent(item, this.ctx.render);
|
|
455
|
-
await this.ctx.docCache.setContent(this.ctx.collection, item.slug, content);
|
|
527
|
+
await this.primeItem(item);
|
|
456
528
|
ok++;
|
|
457
529
|
} catch (err) {
|
|
458
530
|
failed.push({
|
|
@@ -477,6 +549,47 @@ var CollectionClientImpl = class {
|
|
|
477
549
|
failed
|
|
478
550
|
};
|
|
479
551
|
}
|
|
552
|
+
async primeImpl(slug) {
|
|
553
|
+
const item = await this.fetchRaw(slug);
|
|
554
|
+
if (!item) return;
|
|
555
|
+
await this.primeItem(item);
|
|
556
|
+
}
|
|
557
|
+
/** 取得済みアイテムからメタ・本文キャッシュを作り直す(warm / prime / warmByPageId 共通)。 */
|
|
558
|
+
async primeItem(item) {
|
|
559
|
+
await this.persistMeta(item.slug, item);
|
|
560
|
+
const content = await buildCachedItemContent(item, this.ctx.render);
|
|
561
|
+
await this.ctx.docCache.setContent(this.ctx.collection, item.slug, content);
|
|
562
|
+
}
|
|
563
|
+
/** リストキャッシュを最新の取得結果で作り直す。 */
|
|
564
|
+
async refreshList() {
|
|
565
|
+
const items = await this.fetchListRaw();
|
|
566
|
+
await this.ctx.docCache.setList(this.ctx.collection, {
|
|
567
|
+
items,
|
|
568
|
+
cachedAt: Date.now()
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** Notion page id からアクセス可能なアイテムを解決する。`findById` 優先、無ければ list を走査。 */
|
|
572
|
+
async resolveByPageId(pageId) {
|
|
573
|
+
const target = normalizePageId(pageId);
|
|
574
|
+
const findById = this.ctx.source.findById?.bind(this.ctx.source);
|
|
575
|
+
let item;
|
|
576
|
+
if (findById) item = await withRetry(() => findById(pageId), {
|
|
577
|
+
...this.ctx.retryConfig,
|
|
578
|
+
onRetry: (attempt, status, delayMs) => {
|
|
579
|
+
this.ctx.logger?.warn?.("findById() リトライ中", {
|
|
580
|
+
attempt,
|
|
581
|
+
status,
|
|
582
|
+
pageId,
|
|
583
|
+
backoffMs: delayMs
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
else item = (await this.fetchListRaw()).find((i) => normalizePageId(i.id) === target) ?? null;
|
|
588
|
+
if (!item) return null;
|
|
589
|
+
if (item.isArchived || item.isInTrash) return null;
|
|
590
|
+
if (this.ctx.accessibleStatuses.length > 0 && (!item.status || !this.ctx.accessibleStatuses.includes(item.status))) return null;
|
|
591
|
+
return item;
|
|
592
|
+
}
|
|
480
593
|
async persistMeta(slug, item, opts = {}) {
|
|
481
594
|
let meta = buildCachedItemMeta(item, this.ctx.source);
|
|
482
595
|
if (this.ctx.hooks.beforeCacheMeta) meta = await this.ctx.hooks.beforeCacheMeta(meta);
|
|
@@ -807,10 +920,35 @@ const DEFAULT_OPTS = {
|
|
|
807
920
|
imagesPath: "/images",
|
|
808
921
|
revalidatePath: "/revalidate",
|
|
809
922
|
versionsPath: "/versions",
|
|
810
|
-
checkPath: "/check"
|
|
923
|
+
checkPath: "/check",
|
|
924
|
+
notionWebhookPath: "/notion-webhook"
|
|
811
925
|
};
|
|
812
926
|
const JSON_HEADERS = { "content-type": "application/json" };
|
|
813
|
-
/**
|
|
927
|
+
/** HMAC-SHA256 を hex で返す。core はゼロ依存のため import せずグローバル `crypto.subtle` を使う。 */
|
|
928
|
+
async function hmacSha256Hex(secret, message) {
|
|
929
|
+
const enc = new TextEncoder();
|
|
930
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
931
|
+
name: "HMAC",
|
|
932
|
+
hash: "SHA-256"
|
|
933
|
+
}, false, ["sign"]);
|
|
934
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
935
|
+
let hex = "";
|
|
936
|
+
for (const b of new Uint8Array(sig)) hex += b.toString(16).padStart(2, "0");
|
|
937
|
+
return hex;
|
|
938
|
+
}
|
|
939
|
+
/** タイミング攻撃を避ける定数時間文字列比較。 */
|
|
940
|
+
function timingSafeEqual(a, b) {
|
|
941
|
+
if (a.length !== b.length) return false;
|
|
942
|
+
let diff = 0;
|
|
943
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
944
|
+
return diff === 0;
|
|
945
|
+
}
|
|
946
|
+
function jsonResponse(body, status) {
|
|
947
|
+
return new Response(JSON.stringify(body), {
|
|
948
|
+
status,
|
|
949
|
+
headers: JSON_HEADERS
|
|
950
|
+
});
|
|
951
|
+
}
|
|
814
952
|
function httpStatusForError(code) {
|
|
815
953
|
if (code === "webhook/signature_invalid") return 401;
|
|
816
954
|
if (code === "webhook/not_implemented") return 501;
|
|
@@ -819,7 +957,6 @@ function httpStatusForError(code) {
|
|
|
819
957
|
if (code === "handler/unknown_collection") return 404;
|
|
820
958
|
return null;
|
|
821
959
|
}
|
|
822
|
-
/** CMSError を HTTP ステータスへ写像できれば JSON レスポンスにする。未対応なら null。 */
|
|
823
960
|
function errorResponse(err) {
|
|
824
961
|
if (!isCMSError(err)) return null;
|
|
825
962
|
const status = httpStatusForError(err.code);
|
|
@@ -832,7 +969,6 @@ function errorResponse(err) {
|
|
|
832
969
|
headers: JSON_HEADERS
|
|
833
970
|
});
|
|
834
971
|
}
|
|
835
|
-
/** `:collection/:slug` を分解する。どちらか欠ける場合は null。 */
|
|
836
972
|
function splitCollectionSlug(sub) {
|
|
837
973
|
const slashIndex = sub.indexOf("/");
|
|
838
974
|
if (slashIndex <= 0 || slashIndex === sub.length - 1) return null;
|
|
@@ -857,6 +993,7 @@ function createHandler(adapter, opts = {}) {
|
|
|
857
993
|
const revalidatePath = opts.revalidatePath ?? DEFAULT_OPTS.revalidatePath;
|
|
858
994
|
const versionsPath = opts.versionsPath ?? DEFAULT_OPTS.versionsPath;
|
|
859
995
|
const checkPath = opts.checkPath ?? DEFAULT_OPTS.checkPath;
|
|
996
|
+
const notionWebhookPath = opts.notionWebhookPath ?? DEFAULT_OPTS.notionWebhookPath;
|
|
860
997
|
return async (req) => {
|
|
861
998
|
const url = new URL(req.url);
|
|
862
999
|
const path = url.pathname;
|
|
@@ -922,6 +1059,53 @@ function createHandler(adapter, opts = {}) {
|
|
|
922
1059
|
throw err;
|
|
923
1060
|
}
|
|
924
1061
|
}
|
|
1062
|
+
if (req.method === "POST" && rel === notionWebhookPath) {
|
|
1063
|
+
const raw = await req.text();
|
|
1064
|
+
let payload;
|
|
1065
|
+
try {
|
|
1066
|
+
payload = JSON.parse(raw);
|
|
1067
|
+
} catch {
|
|
1068
|
+
return jsonResponse({
|
|
1069
|
+
ok: false,
|
|
1070
|
+
reason: "invalid json"
|
|
1071
|
+
}, 400);
|
|
1072
|
+
}
|
|
1073
|
+
if (payload && typeof payload === "object" && "verification_token" in payload) {
|
|
1074
|
+
const token = String(payload.verification_token);
|
|
1075
|
+
opts.notionWebhook?.onVerificationToken?.(token);
|
|
1076
|
+
return jsonResponse({
|
|
1077
|
+
ok: true,
|
|
1078
|
+
verification_token: token
|
|
1079
|
+
}, 200);
|
|
1080
|
+
}
|
|
1081
|
+
const secret = opts.notionWebhook?.secret ?? adapter.notionWebhookSecret;
|
|
1082
|
+
if (!secret) return jsonResponse({
|
|
1083
|
+
ok: false,
|
|
1084
|
+
reason: "notion webhook secret not configured"
|
|
1085
|
+
}, 503);
|
|
1086
|
+
if (!timingSafeEqual(req.headers.get("X-Notion-Signature") ?? "", `sha256=${await hmacSha256Hex(secret, raw)}`)) return jsonResponse({
|
|
1087
|
+
ok: false,
|
|
1088
|
+
code: "webhook/signature_invalid"
|
|
1089
|
+
}, 401);
|
|
1090
|
+
const entity = payload.entity;
|
|
1091
|
+
const pageId = entity?.type === "page" ? entity.id : void 0;
|
|
1092
|
+
if (!pageId) return jsonResponse({
|
|
1093
|
+
ok: true,
|
|
1094
|
+
skipped: "no page entity"
|
|
1095
|
+
}, 200);
|
|
1096
|
+
if (adapter.scheduleBackground) {
|
|
1097
|
+
adapter.scheduleBackground(adapter.warmByPageId(pageId));
|
|
1098
|
+
return jsonResponse({
|
|
1099
|
+
ok: true,
|
|
1100
|
+
pageId
|
|
1101
|
+
}, 200);
|
|
1102
|
+
}
|
|
1103
|
+
return jsonResponse({
|
|
1104
|
+
ok: true,
|
|
1105
|
+
pageId,
|
|
1106
|
+
result: await adapter.warmByPageId(pageId)
|
|
1107
|
+
}, 200);
|
|
1108
|
+
}
|
|
925
1109
|
if (req.method === "POST" && rel.startsWith(`${revalidatePath}/`)) {
|
|
926
1110
|
const collection = rel.slice(revalidatePath.length + 1);
|
|
927
1111
|
if (!collection || collection.includes("/")) return new Response(JSON.stringify({
|
|
@@ -1084,6 +1268,7 @@ function createClient(opts) {
|
|
|
1084
1268
|
};
|
|
1085
1269
|
const collectionNames = [];
|
|
1086
1270
|
const collections = {};
|
|
1271
|
+
const collectionImpls = {};
|
|
1087
1272
|
for (const [name, def] of Object.entries(collectionsInput)) {
|
|
1088
1273
|
collectionNames.push(name);
|
|
1089
1274
|
const source = def.source;
|
|
@@ -1103,7 +1288,7 @@ function createClient(opts) {
|
|
|
1103
1288
|
hooks: collectionHooks,
|
|
1104
1289
|
logger
|
|
1105
1290
|
};
|
|
1106
|
-
|
|
1291
|
+
const impl = new CollectionClientImpl({
|
|
1107
1292
|
collection: name,
|
|
1108
1293
|
source,
|
|
1109
1294
|
docCache: cacheRes.doc,
|
|
@@ -1119,6 +1304,8 @@ function createClient(opts) {
|
|
|
1119
1304
|
waitUntil,
|
|
1120
1305
|
slugField: def.slugField
|
|
1121
1306
|
});
|
|
1307
|
+
collections[name] = impl;
|
|
1308
|
+
collectionImpls[name] = impl;
|
|
1122
1309
|
}
|
|
1123
1310
|
const globalOps = {
|
|
1124
1311
|
collections: collectionNames,
|
|
@@ -1170,9 +1357,30 @@ function createClient(opts) {
|
|
|
1170
1357
|
});
|
|
1171
1358
|
await cacheRes.doc.invalidate(scope ?? "all");
|
|
1172
1359
|
},
|
|
1360
|
+
async warmByPageId(pageId) {
|
|
1361
|
+
for (const name of collectionNames) {
|
|
1362
|
+
const slug = await collectionImpls[name]?.warmByPageId(pageId);
|
|
1363
|
+
if (slug) {
|
|
1364
|
+
logger?.debug?.("warmByPageId: ページを再ウォーム", {
|
|
1365
|
+
operation: "warmByPageId",
|
|
1366
|
+
collection: name,
|
|
1367
|
+
slug,
|
|
1368
|
+
pageId
|
|
1369
|
+
});
|
|
1370
|
+
return {
|
|
1371
|
+
collection: name,
|
|
1372
|
+
slug
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return null;
|
|
1377
|
+
},
|
|
1173
1378
|
handler(handlerOpts) {
|
|
1174
1379
|
return createHandler({
|
|
1175
1380
|
imageCache: cacheRes.img,
|
|
1381
|
+
warmByPageId: (pageId) => globalOps.warmByPageId(pageId),
|
|
1382
|
+
notionWebhookSecret: opts.notionWebhookSecret,
|
|
1383
|
+
scheduleBackground: waitUntil,
|
|
1176
1384
|
async parseWebhookFor(collection, req, webhookSecret) {
|
|
1177
1385
|
const def = collectionsInput[collection];
|
|
1178
1386
|
if (!def) throw new CMSError({
|
|
@@ -1231,69 +1439,6 @@ function createClient(opts) {
|
|
|
1231
1439
|
return Object.assign(Object.create(null), collections, globalOps);
|
|
1232
1440
|
}
|
|
1233
1441
|
//#endregion
|
|
1234
|
-
//#region src/page-index.ts
|
|
1235
|
-
/**
|
|
1236
|
-
* Notion ページ ID を比較用に正規化する。
|
|
1237
|
-
* Notion の ID は文脈によりダッシュの有無・大文字小文字が揺れるため、
|
|
1238
|
-
* ダッシュ除去 + 小文字化して突き合わせる。
|
|
1239
|
-
*
|
|
1240
|
-
* react-renderer 側も同一実装で `pageLinks` を引くため、変更時は両方を揃えること。
|
|
1241
|
-
*/
|
|
1242
|
-
function normalizePageId(id) {
|
|
1243
|
-
return id.replace(/-/g, "").toLowerCase();
|
|
1244
|
-
}
|
|
1245
|
-
function asCollectionClient(source, name) {
|
|
1246
|
-
const client = source[name];
|
|
1247
|
-
if (!client || typeof client.list !== "function") return void 0;
|
|
1248
|
-
return client;
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* 全コレクションを `list()` で走査し、pageId → {collection, slug, title} の逆引きマップを構築する。
|
|
1252
|
-
* Notion 内部リンク(link_to_page / page mention など)を自サイト URL へ解決するための材料。
|
|
1253
|
-
*
|
|
1254
|
-
* `list()` は SWR ドキュメントキャッシュ経由のためウォーム後は安価。
|
|
1255
|
-
*/
|
|
1256
|
-
async function buildPageIndex(source, opts) {
|
|
1257
|
-
const names = opts?.collections ?? source.collections;
|
|
1258
|
-
const index = /* @__PURE__ */ new Map();
|
|
1259
|
-
if (!names || typeof names[Symbol.iterator] !== "function") return index;
|
|
1260
|
-
for (const name of names) {
|
|
1261
|
-
const client = asCollectionClient(source, name);
|
|
1262
|
-
if (!client) continue;
|
|
1263
|
-
const items = await client.list();
|
|
1264
|
-
if (!Array.isArray(items)) continue;
|
|
1265
|
-
for (const item of items) {
|
|
1266
|
-
const key = normalizePageId(item.id);
|
|
1267
|
-
if (!index.has(key)) index.set(key, {
|
|
1268
|
-
collection: name,
|
|
1269
|
-
slug: item.slug,
|
|
1270
|
-
title: item.title
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return index;
|
|
1275
|
-
}
|
|
1276
|
-
const defaultUrl = (entry) => `/${entry.collection}/${entry.slug}`;
|
|
1277
|
-
/**
|
|
1278
|
-
* Notion 内部リンクを「正規化 pageId → {href, title}」のプレーンマップに解決する。
|
|
1279
|
-
* サーバ側(loader / RSC / route handler)で 1 回構築し、`<NotionRenderer pageLinks={...} />`
|
|
1280
|
-
* に渡す。プレーンオブジェクトなのでシリアライズ境界(RSC / loader)を越えられる。
|
|
1281
|
-
*
|
|
1282
|
-
* @example
|
|
1283
|
-
* const pageLinks = await buildPageLinkMap(cms);
|
|
1284
|
-
* <NotionRenderer blocks={blocks} pageLinks={pageLinks} />;
|
|
1285
|
-
*/
|
|
1286
|
-
async function buildPageLinkMap(source, opts) {
|
|
1287
|
-
const index = opts?.index ?? await buildPageIndex(source, opts);
|
|
1288
|
-
const toUrl = opts?.url ?? defaultUrl;
|
|
1289
|
-
const map = {};
|
|
1290
|
-
for (const [key, entry] of index) map[key] = {
|
|
1291
|
-
href: toUrl(entry, key),
|
|
1292
|
-
title: entry.title
|
|
1293
|
-
};
|
|
1294
|
-
return map;
|
|
1295
|
-
}
|
|
1296
|
-
//#endregion
|
|
1297
1442
|
//#region src/types/config.ts
|
|
1298
1443
|
/**
|
|
1299
1444
|
* `RateLimiterConfig` のデフォルト値 (Issue #313 / M2)。
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/cache.ts","../src/cache/noop.ts","../src/image.ts","../src/rendering.ts","../src/retry.ts","../src/collection.ts","../src/handler.ts","../src/cms.ts","../src/page-index.ts","../src/types/config.ts","../src/types/plugin.ts"],"sourcesContent":["/** 文字列をSHA-256でハッシュ化し、16進数文字列として返す。画像キーの生成に使用。 */\nexport async function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * キャッシュが有効期限切れかどうかを判定する。\n * ttlMs が未指定の場合は常に false(無期限有効)を返す。\n */\nexport function isStale(cachedAt: number, ttlMs?: number): boolean {\n if (ttlMs === undefined) return false;\n return Date.now() - cachedAt > ttlMs;\n}\n","import type {\n BaseContentItem,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n StorageBinary,\n} from \"../types/index\";\n\n/** 何もキャッシュしないドキュメントオペレーション。常に null を返す。 */\nconst noopDoc: DocumentCacheOps = {\n getList<T extends BaseContentItem>(\n _collection: string,\n ): Promise<CachedItemList<T> | null> {\n return Promise.resolve(null);\n },\n setList<T extends BaseContentItem>(\n _collection: string,\n _data: CachedItemList<T>,\n ): Promise<void> {\n return Promise.resolve();\n },\n getMeta<T extends BaseContentItem>(\n _collection: string,\n _slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n return Promise.resolve(null);\n },\n setMeta<T extends BaseContentItem>(\n _collection: string,\n _slug: string,\n _data: CachedItemMeta<T>,\n ): Promise<void> {\n return Promise.resolve();\n },\n getContent(\n _collection: string,\n _slug: string,\n ): Promise<CachedItemContent | null> {\n return Promise.resolve(null);\n },\n setContent(\n _collection: string,\n _slug: string,\n _data: CachedItemContent,\n ): Promise<void> {\n return Promise.resolve();\n },\n invalidate(): Promise<void> {\n return Promise.resolve();\n },\n};\n\n/** 何もキャッシュしない画像オペレーション。 */\nconst noopImg: ImageCacheOps = {\n get(_hash: string): Promise<StorageBinary | null> {\n return Promise.resolve(null);\n },\n set(): Promise<void> {\n return Promise.resolve();\n },\n};\n\n/**\n * 何もキャッシュしないアダプタ。`createClient({ cache })` 未指定時の内部デフォルト。\n * テストでも使える。\n */\nexport const noopDocOps: DocumentCacheOps = noopDoc;\nexport const noopImgOps: ImageCacheOps = noopImg;\n","import { sha256Hex } from \"./cache\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport type { ImageCacheOps, Logger, StorageBinary } from \"./types/index\";\n\n/**\n * レスポンスの Content-Type ヘッダから画像の MIME タイプを取り出す。\n * ヘッダがない、または image/* でない場合は CMSError を投げる。\n * URL 拡張子からの推測や jpeg デフォルトは行わない。\n */\nfunction pickImageContentType(\n headerValue: string | null,\n notionUrl: string,\n): string {\n if (!headerValue) {\n throw new CMSError({\n code: \"cache/image_invalid_content_type\",\n message: \"Image response missing Content-Type header.\",\n context: { operation: \"fetchAndCacheImage:contentType\", notionUrl },\n });\n }\n const value = (headerValue.split(\";\")[0] ?? headerValue).trim().toLowerCase();\n if (!value.startsWith(\"image/\")) {\n throw new CMSError({\n code: \"cache/image_invalid_content_type\",\n message: `Image response has non-image Content-Type: ${value}`,\n context: {\n operation: \"fetchAndCacheImage:contentType\",\n notionUrl,\n contentType: value,\n },\n });\n }\n return value;\n}\n\n/**\n * Notion画像URLをfetchして ImageCacheOps にキャッシュし、プロキシURL を返す。\n * 既存キャッシュがあれば再fetchしない。\n */\nasync function fetchAndCacheImage(\n cache: ImageCacheOps,\n cacheName: string,\n notionUrl: string,\n hash: string,\n imageProxyBase: string,\n logger?: Logger,\n): Promise<string> {\n const proxyUrl = `${imageProxyBase}/${hash}`;\n\n const existing = await cache.get(hash);\n if (existing) {\n logger?.debug?.(\"画像キャッシュヒット\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n return proxyUrl;\n }\n\n logger?.debug?.(\"画像キャッシュミス、Notion からフェッチ\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n\n try {\n const response = await fetch(notionUrl, {\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n throw new CMSError({\n code: \"cache/image_fetch_failed\",\n message: `Failed to fetch Notion image: HTTP ${response.status}`,\n context: {\n operation: \"fetchAndCacheImage\",\n notionUrl,\n httpStatus: response.status,\n },\n });\n }\n\n const data = await response.arrayBuffer();\n const contentType = pickImageContentType(\n response.headers.get(\"content-type\"),\n notionUrl,\n );\n await cache.set(hash, data, contentType);\n logger?.debug?.(\"画像をキャッシュに保存\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"cache/io_failed\",\n message: \"Failed to fetch or cache Notion image.\",\n cause: err,\n context: { operation: \"fetchAndCacheImage\", notionUrl },\n });\n }\n\n return proxyUrl;\n}\n\n/**\n * `ImageCacheOps` と `imageProxyBase` から `cacheImage` 関数を構築する。\n * 返り値は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして\n * {@link ImageCacheOps} に保存後、プロキシ URL を返す。\n *\n * ハッシュのメモ化はファクトリ呼び出し単位でスコープ化されており、\n * インスタンス間でキャッシュを共有しない。\n */\nexport function buildCacheImageFn(\n cache: ImageCacheOps,\n cacheName: string,\n imageProxyBase: string,\n logger?: Logger,\n): (notionUrl: string) => Promise<string> {\n const hashMemo = new Map<string, string>();\n return async (notionUrl) => {\n let hash = hashMemo.get(notionUrl);\n if (hash === undefined) {\n hash = await sha256Hex(notionUrl);\n hashMemo.set(notionUrl, hash);\n }\n return fetchAndCacheImage(\n cache,\n cacheName,\n notionUrl,\n hash,\n imageProxyBase,\n logger,\n );\n };\n}\n\nexport type { StorageBinary };\n","import type { ContentBlock } from \"./content/blocks\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport { buildCacheImageFn } from \"./image\";\nimport type {\n BaseContentItem,\n CachedItemContent,\n CachedItemMeta,\n CMSHooks,\n ContentConfig,\n DataSource,\n ImageCacheOps,\n Logger,\n RendererFn,\n} from \"./types/index\";\n\n/**\n * `@notion-headless-cms/markdown-html` を動的 import してデフォルトレンダラーを返す。\n * core のゼロ依存ルールを守るため静的 import は禁止。\n */\nasync function loadDefaultRenderer(): Promise<RendererFn> {\n try {\n const mod = await import(\"@notion-headless-cms/markdown-html\");\n return (mod as { renderMarkdown: RendererFn }).renderMarkdown;\n } catch {\n throw new CMSError({\n code: \"core/config_invalid\",\n message:\n \"renderer が未指定で、@notion-headless-cms/markdown-html のロードにも失敗しました。\" +\n \" createClient の renderer オプションを指定するか、@notion-headless-cms/markdown-html をインストールしてください。\",\n context: { operation: \"loadDefaultRenderer\" },\n });\n }\n}\n\n/** 本文レンダリングに必要な依存を束ねたコンテキスト。 */\nexport interface RenderContext<T extends BaseContentItem> {\n source: DataSource<T>;\n rendererFn: RendererFn | undefined;\n imgCache: ImageCacheOps;\n imgCacheName: string;\n hasImageCache: boolean;\n imageProxyBase: string;\n contentConfig: ContentConfig | undefined;\n hooks: CMSHooks<T>;\n logger: Logger | undefined;\n}\n\n/**\n * メタデータキャッシュエントリを生成する。Notion API も renderer も呼ばない軽量関数。\n */\nexport function buildCachedItemMeta<T extends BaseContentItem>(\n item: T,\n source: DataSource<T>,\n): CachedItemMeta<T> {\n return {\n item,\n notionUpdatedAt: source.getLastModified(item),\n cachedAt: Date.now(),\n };\n}\n\n/**\n * アイテム本文を Markdown ロード → blocks 生成 → HTML レンダリング → フック適用まで\n * 実行し、本文キャッシュ用の `CachedItemContent` を返す。\n */\nexport async function buildCachedItemContent<T extends BaseContentItem>(\n item: T,\n ctx: RenderContext<T>,\n): Promise<CachedItemContent> {\n const start = Date.now();\n ctx.logger?.info?.(\"コンテンツのレンダリング開始\", {\n slug: item.slug,\n pageId: item.id,\n });\n ctx.hooks.onRenderStart?.(item.slug);\n\n let markdown: string;\n try {\n markdown = await ctx.source.loadMarkdown(item);\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"source/load_markdown_failed\",\n message: \"Failed to load markdown from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadMarkdown\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n let blocks: ContentBlock[];\n try {\n blocks = await ctx.source.loadBlocks(item);\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"source/load_blocks_failed\",\n message: \"Failed to load blocks from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadBlocks\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n // react-renderer など Notion 形式を直接消費する利用側のため、\n // DataSource が対応していれば BlockObjectResponse ツリーも取得・キャッシュする。\n // markdown 戦略のように loadNotionBlocks 未対応の場合は `source/blocks_unsupported`\n // を吸収して undefined にする (ページ描画は markdown 経路で代替する想定)。\n let notionBlocks: unknown[] | undefined;\n if (ctx.source.loadNotionBlocks) {\n try {\n notionBlocks = await ctx.source.loadNotionBlocks(item);\n } catch (err) {\n if (isCMSError(err) && err.is(\"source/blocks_unsupported\")) {\n notionBlocks = undefined;\n } else if (isCMSError(err)) {\n throw err;\n } else {\n throw new CMSError({\n code: \"source/load_blocks_failed\",\n message: \"Failed to load Notion blocks from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadNotionBlocks\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n }\n }\n\n const cacheImage = ctx.hasImageCache\n ? buildCacheImageFn(\n ctx.imgCache,\n ctx.imgCacheName,\n ctx.imageProxyBase,\n ctx.logger,\n )\n : undefined;\n\n const rendererFn = ctx.rendererFn ?? (await loadDefaultRenderer());\n\n let html: string;\n try {\n html = await rendererFn(markdown, {\n imageProxyBase: ctx.imageProxyBase,\n cacheImage,\n remarkPlugins: ctx.contentConfig?.remarkPlugins,\n rehypePlugins: ctx.contentConfig?.rehypePlugins,\n });\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"renderer/failed\",\n message: \"Failed to render markdown.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:renderMarkdown\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n if (ctx.hooks.afterRender) {\n html = await ctx.hooks.afterRender(html, item);\n }\n\n let result: CachedItemContent = {\n html,\n blocks,\n markdown,\n notionBlocks,\n notionUpdatedAt: ctx.source.getLastModified(item),\n cachedAt: Date.now(),\n };\n\n if (ctx.hooks.beforeCacheContent) {\n result = await ctx.hooks.beforeCacheContent(result, item);\n }\n\n const durationMs = Date.now() - start;\n ctx.logger?.info?.(\"コンテンツのレンダリング完了\", {\n slug: item.slug,\n durationMs,\n });\n ctx.hooks.onRenderEnd?.(item.slug, durationMs);\n\n return result;\n}\n","export interface RetryConfig {\n retryOn: number[];\n maxRetries: number;\n baseDelayMs: number;\n /** true のとき指数バックオフにランダムジッターを加える(Thundering Herd 対策)。デフォルト: true */\n jitter?: boolean;\n /**\n * リトライ前に呼ばれるフック。`attempt` は 1 始まり、`status` はリトライ対象の HTTP ステータス、\n * `delayMs` は次回試行までの実際の待機時間 (ジッター反映後)。\n * 既存呼び出しは `attempt` / `status` だけで動くよう `delayMs` は省略可。\n */\n onRetry?: (attempt: number, status: number, delayMs?: number) => void;\n}\n\nexport const DEFAULT_RETRY_CONFIG: RetryConfig = {\n retryOn: [429, 502, 503],\n maxRetries: 4,\n baseDelayMs: 1000,\n jitter: true,\n};\n\n/**\n * 指数バックオフ(オプションでジッター付き)でリトライする。\n *\n * `config.retryOn` に含まれるステータスコードを持つエラーのみリトライ対象。\n * 遅延は `baseDelayMs * 2^attempt` の指数バックオフ。\n * `jitter` が `true`(デフォルト)の場合、0.5〜1.0 の乱数係数を乗算して\n * Thundering Herd を防ぐ。`false` にすると確定的な遅延になる。\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n config: RetryConfig,\n): Promise<T> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n const status = (err as { status?: number }).status;\n if (status === undefined || !config.retryOn.includes(status)) {\n throw err;\n }\n lastError = err;\n if (attempt < config.maxRetries) {\n const jitterFactor =\n config.jitter !== false ? 0.5 + Math.random() * 0.5 : 1;\n const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;\n // onRetry には attempt / status と次回待機時間 (ms) を渡す。\n // 呼び出し側のロガーが `LogContext.backoffMs` として記録できる。\n config.onRetry?.(attempt + 1, status, delay);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n throw lastError;\n}\n","import { isStale } from \"./cache\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport type { RenderContext } from \"./rendering\";\nimport { buildCachedItemContent, buildCachedItemMeta } from \"./rendering\";\nimport type { RetryConfig } from \"./retry\";\nimport { withRetry } from \"./retry\";\nimport type {\n AdjacencyOptions,\n BaseContentItem,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n CheckResult,\n CMSHooks,\n CollectionCacheOps,\n CollectionClient,\n DataSource,\n DocumentCacheOps,\n FindOptions,\n ItemWithContent,\n ListOptions,\n Logger,\n SortOption,\n WarmOptions,\n WarmResult,\n WhereClause,\n} from \"./types/index\";\n\n/**\n * コレクション別キャッシュキーを生成する (item: `{collection}:{slug}` / list: `{collection}`)。\n *\n * 各 cache adapter は内部で独自のキー戦略を持つが、ログ出力や差分再計算で\n * 同一表現が必要になるため core 側にも公開する。\n */\nexport function collectionKey(collection: string, slug?: string): string {\n return slug ? `${collection}:${slug}` : collection;\n}\n\nexport interface CollectionContext<T extends BaseContentItem> {\n collection: string;\n source: DataSource<T>;\n docCache: DocumentCacheOps;\n docCacheName: string;\n render: RenderContext<T>;\n hooks: CMSHooks<T>;\n logger: Logger | undefined;\n ttlMs: number | undefined;\n publishedStatuses: string[];\n accessibleStatuses: string[];\n retryConfig: RetryConfig;\n maxConcurrent: number;\n waitUntil: ((p: Promise<unknown>) => void) | undefined;\n /**\n * slug として使うフィールド名。`source.properties[slugField].notion` を\n * Notion プロパティ名として `findByProp` を呼び出す。\n */\n slugField: string;\n}\n\nexport class CollectionClientImpl<T extends BaseContentItem>\n implements CollectionClient<T>\n{\n readonly cache: CollectionCacheOps<T>;\n\n constructor(private readonly ctx: CollectionContext<T>) {\n this.cache = {\n invalidate: () => this.invalidateImpl(),\n invalidateItem: (slug: string) => this.invalidateItemImpl(slug),\n warm: (opts?: WarmOptions) => this.warmImpl(opts),\n };\n }\n\n // ── 基本取得 ──────────────────────────────────────────────────────────\n\n async find(\n slug: string,\n opts: FindOptions = {},\n ): Promise<ItemWithContent<T> | null> {\n if (opts.bypassCache) {\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n return this.attachLazyContent(meta);\n }\n\n const cachedMeta = await this.ctx.docCache.getMeta<T>(\n this.ctx.collection,\n slug,\n );\n if (cachedMeta) {\n // TTL 切れはブロッキングで再取得する (stale を返さない要件)\n if (\n this.ctx.ttlMs !== undefined &&\n isStale(cachedMeta.cachedAt, this.ctx.ttlMs)\n ) {\n this.ctx.logger?.debug?.(\"キャッシュ期限切れ(TTL)、フェッチ\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n return this.attachLazyContent(meta);\n }\n // SWR: 即時返却しつつバックグラウンドで差分チェック\n const bg = this.checkAndUpdateItemBg(slug, cachedMeta);\n if (this.ctx.waitUntil) this.ctx.waitUntil(bg);\n this.ctx.logger?.debug?.(\"キャッシュヒット\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n cachedAt: cachedMeta.cachedAt,\n });\n this.ctx.hooks.onCacheHit?.(slug, cachedMeta);\n return this.attachLazyContent(cachedMeta);\n }\n\n this.ctx.logger?.debug?.(\"キャッシュミス、フェッチ\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n // 保存だけはバックグラウンド可: ユーザー向けレスポンスを早めに返す\n const meta = await this.persistMeta(slug, item, { background: true });\n return this.attachLazyContent(meta);\n }\n\n async list(opts?: ListOptions<T>): Promise<T[]> {\n const allItems = await this.fetchList();\n return applyListOptions(allItems, opts);\n }\n\n async params(): Promise<string[]> {\n const items = await this.fetchList();\n return items.map((item) => item.slug);\n }\n\n async peekVersion(\n slug: string,\n ): Promise<{ notionUpdatedAt: string; cachedAt: number } | null> {\n const meta = await this.ctx.docCache.getMeta<T>(this.ctx.collection, slug);\n if (!meta) return null;\n return { notionUpdatedAt: meta.notionUpdatedAt, cachedAt: meta.cachedAt };\n }\n\n async check(\n slug: string,\n currentVersion: string,\n ): Promise<CheckResult<T> | null> {\n const raw = await this.fetchRaw(slug);\n if (!raw) return null;\n if (raw.lastEditedTime === currentVersion) return { stale: false };\n const meta = await this.persistMeta(slug, raw);\n await this.invalidateContentEntry(slug);\n return { stale: true, item: this.attachLazyContent(meta) };\n }\n\n async adjacent(\n slug: string,\n opts?: AdjacencyOptions<T>,\n ): Promise<{ prev: T | null; next: T | null }> {\n const items = applyListOptions(await this.fetchList(), {\n sort: opts?.sort,\n });\n const index = items.findIndex((it) => it.slug === slug);\n if (index === -1) return { prev: null, next: null };\n return {\n prev: index > 0 ? (items[index - 1] ?? null) : null,\n next: index < items.length - 1 ? (items[index + 1] ?? null) : null,\n };\n }\n\n // ── キャッシュ操作 ────────────────────────────────────────────────────\n\n private async invalidateImpl(): Promise<void> {\n this.ctx.logger?.debug?.(\"コレクション全体のキャッシュを無効化\", {\n operation: \"cache.invalidate\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n await this.ctx.docCache.invalidate({ collection: this.ctx.collection });\n }\n\n private async invalidateItemImpl(slug: string): Promise<void> {\n this.ctx.logger?.debug?.(\"アイテムキャッシュを無効化\", {\n operation: \"cache.invalidateItem\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n slug,\n });\n await this.ctx.docCache.invalidate({\n collection: this.ctx.collection,\n slug,\n });\n }\n\n private async warmImpl(opts?: WarmOptions): Promise<WarmResult> {\n const items = await this.fetchListRaw();\n const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;\n let ok = 0;\n const failed: Array<{ slug: string; error: unknown }> = [];\n\n for (let i = 0; i < items.length; i += concurrency) {\n const chunk = items.slice(i, i + concurrency);\n await Promise.all(\n chunk.map(async (item) => {\n try {\n await this.persistMeta(item.slug, item);\n const content = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(\n this.ctx.collection,\n item.slug,\n content,\n );\n ok++;\n } catch (err) {\n failed.push({ slug: item.slug, error: err });\n this.ctx.logger?.warn?.(\"warm: アイテムの事前レンダリングに失敗\", {\n slug: item.slug,\n pageId: item.id,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }),\n );\n opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);\n }\n\n await this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt: Date.now(),\n });\n return { ok, failed };\n }\n\n // ── 内部 ──────────────────────────────────────────────────────────────\n\n private async persistMeta(\n slug: string,\n item: T,\n opts: { background?: boolean } = {},\n ): Promise<CachedItemMeta<T>> {\n let meta = buildCachedItemMeta(item, this.ctx.source);\n if (this.ctx.hooks.beforeCacheMeta) {\n meta = await this.ctx.hooks.beforeCacheMeta(meta);\n }\n const save = this.ctx.docCache.setMeta(this.ctx.collection, slug, meta);\n if (opts.background && this.ctx.waitUntil) {\n this.ctx.waitUntil(save);\n } else {\n await save;\n }\n return meta;\n }\n\n private async invalidateContentEntry(slug: string): Promise<void> {\n await this.ctx.docCache.invalidate({\n collection: this.ctx.collection,\n slug,\n kind: \"content\",\n });\n }\n\n /** 本文キャッシュ。メタとの整合 (`notionUpdatedAt`) が崩れていれば再生成して書き戻す。 */\n private async loadOrBuildContent(\n slug: string,\n item: T,\n ): Promise<CachedItemContent> {\n const expected = this.ctx.source.getLastModified(item);\n const cached = await this.ctx.docCache.getContent(\n this.ctx.collection,\n slug,\n );\n if (cached && cached.notionUpdatedAt === expected) {\n return cached;\n }\n\n const fresh = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);\n this.ctx.hooks.onContentRevalidated?.(slug, fresh);\n return fresh;\n }\n\n /** メタ既知の状態で本文だけ再生成する。エラーは onSwrError フックに通知して握り潰す。 */\n private async rebuildContentBg(slug: string, item: T): Promise<void> {\n try {\n const fresh = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);\n this.ctx.hooks.onContentRevalidated?.(slug, fresh);\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/content_rebuild_failed\",\n message: \"SWR background content rebuild failed.\",\n cause: err,\n context: {\n operation: \"swr.rebuildContentBg\",\n collection: this.ctx.collection,\n slug,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"item-content\", slug });\n this.ctx.logger?.warn?.(\"本文のバックグラウンド再生成に失敗\", {\n slug,\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private attachLazyContent(meta: CachedItemMeta<T>): ItemWithContent<T> {\n const slug = meta.item.slug;\n const item = meta.item;\n // html() / markdown() / blocks() を同じアイテムから複数回呼んでも I/O は 1 回に集約する\n let payloadPromise: Promise<CachedItemContent> | undefined;\n const loadPayload = (): Promise<CachedItemContent> => {\n if (!payloadPromise) {\n payloadPromise = this.loadOrBuildContent(slug, item);\n }\n return payloadPromise;\n };\n\n return Object.assign(Object.create(null) as object, item, {\n html: async () => (await loadPayload()).html,\n markdown: async () => (await loadPayload()).markdown,\n blocks: async () => (await loadPayload()).blocks,\n notionBlocks: async () => {\n const notionBlocks = (await loadPayload()).notionBlocks;\n // markdown 戦略やフェッチャ未設定だと常に undefined になり、\n // React レンダリング側で原因が分からない無言失敗になるため一度だけ案内する。\n if (notionBlocks === undefined) this.warnMissingNotionBlocks();\n return notionBlocks;\n },\n }) as ItemWithContent<T>;\n }\n\n // notionBlocks() が undefined を返したときの案内を CollectionClient 単位で一度だけ出す。\n private notionBlocksWarned = false;\n\n private warnMissingNotionBlocks(): void {\n if (this.notionBlocksWarned) return;\n this.notionBlocksWarned = true;\n this.ctx.logger?.warn?.(\n \"notionBlocks() が undefined を返しました。BlockObjectResponse ツリーは blocks 戦略でのみ得られます (notionSource の fetch 未指定の既定でも有効)。markdownFetcher を使用中の場合は markdown→React の Renderer を使うか、blocksFetcher() に切り替えてください。\",\n { collection: this.ctx.collection, operation: \"notionBlocks\" },\n );\n }\n\n private async fetchList(): Promise<T[]> {\n const cached = await this.ctx.docCache.getList<T>(this.ctx.collection);\n if (cached) {\n // TTL 切れはブロッキングで再取得する (find と同じ理由)\n if (\n this.ctx.ttlMs !== undefined &&\n isStale(cached.cachedAt, this.ctx.ttlMs)\n ) {\n this.ctx.logger?.debug?.(\"リストキャッシュ期限切れ(TTL)、フェッチ\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheMiss?.();\n const items = await this.fetchListRaw();\n await this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt: Date.now(),\n });\n return items;\n }\n const bg = this.checkAndUpdateListBg(cached);\n if (this.ctx.waitUntil) this.ctx.waitUntil(bg);\n this.ctx.logger?.debug?.(\"リストキャッシュヒット\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheHit?.(cached);\n return cached.items;\n }\n\n this.ctx.logger?.debug?.(\"リストキャッシュミス、フェッチ\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheMiss?.();\n const items = await this.fetchListRaw();\n const cachedAt = Date.now();\n const save = this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt,\n });\n if (this.ctx.waitUntil) {\n this.ctx.waitUntil(save);\n } else {\n await save;\n }\n return items;\n }\n\n private async checkAndUpdateItemBg(\n slug: string,\n cached: CachedItemMeta<T>,\n ): Promise<void> {\n try {\n const item = await this.fetchRaw(slug);\n if (!item) return;\n const lm = this.ctx.source.getLastModified(item);\n if (lm !== cached.notionUpdatedAt) {\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n this.ctx.logger?.debug?.(\"SWR: 差分を検出、メタを差し替え\", {\n operation: \"find:bg\",\n slug,\n collection: this.ctx.collection,\n notionUpdatedAt: cached.notionUpdatedAt,\n });\n this.ctx.hooks.onCacheRevalidated?.(slug, meta);\n await this.rebuildContentBg(slug, item);\n } else {\n await this.ctx.docCache.setMeta(this.ctx.collection, slug, {\n ...cached,\n cachedAt: Date.now(),\n });\n this.ctx.logger?.debug?.(\"SWR: 差分なし、cachedAt を更新\", {\n operation: \"find:bg\",\n slug,\n collection: this.ctx.collection,\n });\n }\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/item_check_failed\",\n message: \"SWR background item check failed.\",\n cause: err,\n context: {\n operation: \"swr.checkAndUpdateItemBg\",\n collection: this.ctx.collection,\n slug,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"item-meta\", slug });\n this.ctx.logger?.warn?.(\n \"SWR: アイテムのバックグラウンド差分チェックに失敗\",\n {\n slug,\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n }\n }\n\n private async checkAndUpdateListBg(cached: CachedItemList<T>): Promise<void> {\n try {\n const items = await this.fetchListRaw();\n if (\n this.ctx.source.getListVersion(items) !==\n this.ctx.source.getListVersion(cached.items)\n ) {\n const listEntry = { items, cachedAt: Date.now() };\n await this.ctx.docCache.setList(this.ctx.collection, listEntry);\n this.ctx.logger?.debug?.(\n \"SWR: リスト差分を検出、キャッシュを差し替え\",\n {\n operation: \"list:bg\",\n collection: this.ctx.collection,\n },\n );\n this.ctx.hooks.onListCacheRevalidated?.(listEntry);\n } else if (this.ctx.ttlMs !== undefined) {\n await this.ctx.docCache.setList(this.ctx.collection, {\n ...cached,\n cachedAt: Date.now(),\n });\n this.ctx.logger?.debug?.(\"SWR: リスト差分なし、TTL をリセット\", {\n operation: \"list:bg\",\n collection: this.ctx.collection,\n });\n }\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/list_check_failed\",\n message: \"SWR background list check failed.\",\n cause: err,\n context: {\n operation: \"swr.checkAndUpdateListBg\",\n collection: this.ctx.collection,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"list\" });\n this.ctx.logger?.warn?.(\n \"SWR: リストのバックグラウンド差分チェックに失敗\",\n {\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n }\n }\n\n private async fetchListRaw(): Promise<T[]> {\n const items = await withRetry(\n () =>\n this.ctx.source.list({\n publishedStatuses:\n this.ctx.publishedStatuses.length > 0\n ? this.ctx.publishedStatuses\n : undefined,\n }),\n {\n ...this.ctx.retryConfig,\n onRetry: (attempt, status, delayMs) => {\n this.ctx.logger?.warn?.(\"list() リトライ中\", {\n attempt,\n status,\n backoffMs: delayMs,\n });\n },\n },\n );\n return items.filter((item) => {\n if (item.isArchived || item.isInTrash) return false;\n if (\n this.ctx.accessibleStatuses.length > 0 &&\n (!item.status || !this.ctx.accessibleStatuses.includes(item.status))\n )\n return false;\n return true;\n });\n }\n\n private async fetchRaw(slug: string): Promise<T | null> {\n const retryOpts = {\n ...this.ctx.retryConfig,\n onRetry: (attempt: number, status: number, delayMs?: number) => {\n this.ctx.logger?.warn?.(\"find() リトライ中\", {\n attempt,\n status,\n slug,\n backoffMs: delayMs,\n });\n },\n };\n\n // PropertyMap が解決できる場合は単一プロパティ filter で 1 ページだけ取得する (高速)\n const notionPropName =\n this.ctx.source.properties?.[this.ctx.slugField]?.notion;\n\n let item: T | null;\n const findByProp = this.ctx.source.findByProp?.bind(this.ctx.source);\n if (notionPropName && findByProp) {\n item = await withRetry(() => findByProp(notionPropName, slug), retryOpts);\n } else {\n // PropertyMap 未提供 / findByProp 未実装の DataSource 向けフォールバック\n const all = await withRetry(() => this.ctx.source.list(), retryOpts);\n item = all.find((i) => i.slug === slug) ?? null;\n }\n\n if (!item) return null;\n if (item.isArchived || item.isInTrash) return null;\n if (\n this.ctx.accessibleStatuses.length > 0 &&\n (!item.status || !this.ctx.accessibleStatuses.includes(item.status))\n ) {\n return null;\n }\n return item;\n }\n}\n\nfunction matchesWhere<T extends BaseContentItem>(\n item: T,\n where: WhereClause<T>,\n): boolean {\n for (const key of Object.keys(where) as (keyof T & string)[]) {\n const expected = where[key];\n const actual = item[key];\n if (Array.isArray(expected)) {\n if (!(expected as readonly unknown[]).includes(actual)) return false;\n } else {\n if (actual !== expected) return false;\n }\n }\n return true;\n}\n\nfunction applyListOptions<T extends BaseContentItem>(\n items: T[],\n opts?: ListOptions<T>,\n): T[] {\n if (!opts) return sortByPublishedAtDesc(items);\n let result = items;\n\n if (opts.statuses) {\n const allow = new Set(\n Array.isArray(opts.statuses) ? opts.statuses : [opts.statuses],\n );\n result = result.filter((it) => it.status != null && allow.has(it.status));\n }\n\n if (opts.tag) {\n const tag = opts.tag;\n result = result.filter((it) => {\n const tags = (it as { tags?: string[] }).tags;\n return Array.isArray(tags) && tags.includes(tag);\n });\n }\n\n if (opts.where) {\n const where = opts.where;\n result = result.filter((it) => matchesWhere(it, where));\n }\n\n if (opts.filter) {\n result = result.filter(opts.filter);\n }\n\n if (opts.sort) {\n result = [...result].sort(makeComparator(opts.sort));\n } else {\n result = sortByPublishedAtDesc(result);\n }\n\n const skip = opts.skip ?? 0;\n const limit = opts.limit;\n if (skip > 0 || limit !== undefined) {\n result = result.slice(skip, limit !== undefined ? skip + limit : undefined);\n }\n\n return result;\n}\n\n/** publishedAt 降順 (未設定なら lastEditedTime 降順) でソートする。 */\nfunction sortByPublishedAtDesc<T extends BaseContentItem>(items: T[]): T[] {\n return [...items].sort((a, b) => {\n const av = a.publishedAt ?? a.lastEditedTime;\n const bv = b.publishedAt ?? b.lastEditedTime;\n if (av === bv) return 0;\n return av > bv ? -1 : 1;\n });\n}\n\nfunction makeComparator<T extends BaseContentItem>(\n sort: SortOption<T>,\n): (a: T, b: T) => number {\n if (sort.compare) return sort.compare;\n const by = sort.by as keyof T;\n const dir = sort.dir === \"asc\" ? 1 : -1;\n return (a, b) => {\n const av = a[by];\n const bv = b[by];\n if (av === bv) return 0;\n if (av === undefined || av === null) return 1;\n if (bv === undefined || bv === null) return -1;\n if (typeof av === \"string\" && typeof bv === \"string\") {\n return av > bv ? dir : -dir;\n }\n if (typeof av === \"number\" && typeof bv === \"number\") {\n return av > bv ? dir : -dir;\n }\n throw new CMSError({\n code: \"core/sort_unsupported_type\",\n message: `\"${String(by)}\" フィールドの型 \"${typeof av}\" はソート非対応です。compare 関数を指定してください。`,\n context: {\n operation: \"makeComparator\",\n field: String(by),\n type: typeof av,\n },\n });\n };\n}\n","import { isCMSError } from \"./errors\";\nimport type { ImageCacheOps, InvalidateScope } from \"./types/index\";\n\n/** `$handler()` の挙動設定。 */\nexport interface HandlerOptions {\n /** マウントするベースパス。デフォルト `/api/cms`。 */\n basePath?: string;\n /** 画像プロキシのパス (basePath 相対)。デフォルト `/images/:hash`。 */\n imagesPath?: string;\n /** revalidate webhook のパス (basePath 相対)。デフォルト `/revalidate`。 */\n revalidatePath?: string;\n /** バージョン照会 (peekVersion) のパス (basePath 相対)。デフォルト `/versions`。 */\n versionsPath?: string;\n /** 更新チェック (check) のパス (basePath 相対)。デフォルト `/check`。 */\n checkPath?: string;\n /** Webhook 署名検証用シークレット (未指定なら検証スキップ)。 */\n webhookSecret?: string;\n /** デフォルト実装を無効化する場合 true。 */\n disabled?: boolean;\n}\n\n/** `$handler()` が内部で依存する CMS 機能の最小セット。 */\nexport interface HandlerAdapter {\n imageCache: ImageCacheOps;\n /**\n * 指定コレクションの DataSource.parseWebhook を呼ぶ。\n * 未知コレクション → `handler/unknown_collection` CMSError\n * parseWebhook 未実装 → `webhook/not_implemented` CMSError\n */\n parseWebhookFor(\n collection: string,\n req: Request,\n webhookSecret: string | undefined,\n ): Promise<InvalidateScope>;\n revalidate(scope: InvalidateScope): Promise<void>;\n /**\n * 指定コレクション / slug の `peekVersion`(KV メタのみ、Notion API 非呼び出し)を返す。\n * キャッシュ未登録なら `null`。未知コレクション → `handler/unknown_collection` CMSError。\n */\n peekVersionFor(\n collection: string,\n slug: string,\n ): Promise<{ notionUpdatedAt: string; cachedAt: number } | null>;\n /**\n * 指定コレクション / slug を `currentVersion` と比較し(Notion を実照会)、\n * 差分があればキャッシュを更新して `stale` を返す。\n * アイテムが存在しない場合は `null`。未知コレクション → `handler/unknown_collection` CMSError。\n */\n checkFor(\n collection: string,\n slug: string,\n currentVersion: string,\n ): Promise<{ stale: boolean } | null>;\n}\n\nconst DEFAULT_OPTS = {\n basePath: \"/api/cms\",\n imagesPath: \"/images\",\n revalidatePath: \"/revalidate\",\n versionsPath: \"/versions\",\n checkPath: \"/check\",\n} as const;\n\nconst JSON_HEADERS = { \"content-type\": \"application/json\" } as const;\n\n/** CMSError コードを HTTP ステータスへ写像する。未対応コードは null。 */\nfunction httpStatusForError(code: string): number | null {\n if (code === \"webhook/signature_invalid\") return 401;\n if (code === \"webhook/not_implemented\") return 501;\n if (code === \"webhook/unknown_collection\") return 404;\n if (code === \"webhook/payload_invalid\") return 400;\n if (code === \"handler/unknown_collection\") return 404;\n return null;\n}\n\n/** CMSError を HTTP ステータスへ写像できれば JSON レスポンスにする。未対応なら null。 */\nfunction errorResponse(err: unknown): Response | null {\n if (!isCMSError(err)) return null;\n const status = httpStatusForError(err.code);\n if (status === null) return null;\n return new Response(JSON.stringify({ ok: false, code: err.code }), {\n status,\n headers: JSON_HEADERS,\n });\n}\n\n/** `:collection/:slug` を分解する。どちらか欠ける場合は null。 */\nfunction splitCollectionSlug(\n sub: string,\n): { collection: string; slug: string } | null {\n const slashIndex = sub.indexOf(\"/\");\n if (slashIndex <= 0 || slashIndex === sub.length - 1) return null;\n return {\n collection: sub.slice(0, slashIndex),\n slug: sub.slice(slashIndex + 1),\n };\n}\n\n/**\n * Web Standard な Request → Response ルーター。\n * Next.js / React Router / Hono / Cloudflare Workers いずれでも使える。\n *\n * ルート:\n * - GET `{basePath}/images/:hash` — 画像プロキシ\n * - GET `{basePath}/versions/:collection/:slug` — peekVersion(更新検知ポーリング)\n * - GET/POST `{basePath}/check/:collection/:slug?v=` — check(更新を実照会してキャッシュ更新)\n * - POST `{basePath}/revalidate/:collection` — Webhook 受信 + $revalidate()\n */\nexport function createHandler(\n adapter: HandlerAdapter,\n opts: HandlerOptions = {},\n): (req: Request) => Promise<Response> {\n const basePath = trimTrailingSlash(opts.basePath ?? DEFAULT_OPTS.basePath);\n const imagesPath = opts.imagesPath ?? DEFAULT_OPTS.imagesPath;\n const revalidatePath = opts.revalidatePath ?? DEFAULT_OPTS.revalidatePath;\n const versionsPath = opts.versionsPath ?? DEFAULT_OPTS.versionsPath;\n const checkPath = opts.checkPath ?? DEFAULT_OPTS.checkPath;\n\n return async (req: Request): Promise<Response> => {\n const url = new URL(req.url);\n const path = url.pathname;\n\n if (!path.startsWith(basePath)) {\n return new Response(\"Not Found\", { status: 404 });\n }\n const rel = path.slice(basePath.length) || \"/\";\n\n if (req.method === \"GET\" && rel.startsWith(`${imagesPath}/`)) {\n const hash = rel.slice(imagesPath.length + 1);\n if (!hash) return new Response(\"Bad Request\", { status: 400 });\n const object = await adapter.imageCache.get(hash);\n if (!object) return new Response(\"Not Found\", { status: 404 });\n const headers = new Headers();\n if (object.contentType) headers.set(\"content-type\", object.contentType);\n headers.set(\"cache-control\", \"public, max-age=31536000, immutable\");\n return new Response(object.data, { headers });\n }\n\n if (req.method === \"GET\" && rel.startsWith(`${versionsPath}/`)) {\n const target = splitCollectionSlug(rel.slice(versionsPath.length + 1));\n if (!target) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"collection and slug required\" }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const version = await adapter.peekVersionFor(\n target.collection,\n target.slug,\n );\n // 値が無い場合も 200 + null を返す(ポーリング側は null を「未確定」として継続する)。\n return new Response(JSON.stringify(version), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n if (\n (req.method === \"GET\" || req.method === \"POST\") &&\n rel.startsWith(`${checkPath}/`)\n ) {\n const target = splitCollectionSlug(rel.slice(checkPath.length + 1));\n // 比較基準のバージョンは `?v=` クエリで受け取る。\n const currentVersion = url.searchParams.get(\"v\");\n if (!target || !currentVersion) {\n return new Response(\n JSON.stringify({\n ok: false,\n reason: \"collection, slug and ?v= are required\",\n }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const result = await adapter.checkFor(\n target.collection,\n target.slug,\n currentVersion,\n );\n // アイテム未存在は 404。差分判定 (stale) は 200 で返す。\n if (result === null) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"not found\" }),\n {\n status: 404,\n headers: JSON_HEADERS,\n },\n );\n }\n return new Response(JSON.stringify({ stale: result.stale }), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n if (req.method === \"POST\" && rel.startsWith(`${revalidatePath}/`)) {\n const collection = rel.slice(revalidatePath.length + 1);\n if (!collection || collection.includes(\"/\")) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"collection required\" }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const scope = await adapter.parseWebhookFor(\n collection,\n req,\n opts.webhookSecret,\n );\n await adapter.revalidate(scope);\n return new Response(JSON.stringify({ ok: true, scope }), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n return new Response(\"Not Found\", { status: 404 });\n };\n}\n\nfunction trimTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n","import { noopDocOps, noopImgOps } from \"./cache/noop\";\nimport { CollectionClientImpl, type CollectionContext } from \"./collection\";\nimport { CMSError } from \"./errors\";\nimport { createHandler, type HandlerOptions } from \"./handler\";\nimport { mergeHooks, mergeLoggers, withTraceId } from \"./hooks\";\nimport { buildCacheImageFn } from \"./image\";\nimport type { RenderContext } from \"./rendering\";\nimport type { RetryConfig } from \"./retry\";\nimport { DEFAULT_RETRY_CONFIG } from \"./retry\";\nimport type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CMSHooks,\n CollectionClient,\n CollectionsConfig,\n CreateClientOptions,\n DataSource,\n DocumentCacheOps,\n ImageCacheOps,\n InferCollectionItem,\n InvalidateScope,\n Logger,\n LogLevel,\n RendererFn,\n StorageBinary,\n} from \"./types/index\";\nimport type {\n CMSAdapter,\n CMSSources,\n MergeSourceCollections,\n} from \"./types/sources\";\n\nconst DEFAULT_IMAGE_PROXY_BASE = \"/api/images\";\n\n/** コレクション別アクセス + グローバル操作の合成型。 */\nexport type CMSClient<C extends CollectionsConfig> = {\n [K in keyof C]: CollectionClient<InferCollectionItem<C[K]>>;\n} & CMSGlobalOps;\n\n/**\n * `cms.stats()` が返す集約済みキャッシュ統計。\n * 各 adapter の `stats()` 戻り値をそのまま配列で保持しつつ、ヒット率を算出する。\n */\nexport interface CMSStats {\n /** クライアント単位の trace ID (`createClient` で発行)。 */\n traceId: string;\n /** ドキュメントキャッシュの集計 (`handles: [\"document\"]` の adapter の `stats()` 戻り値)。 */\n document?: {\n adapter: string;\n hits: number;\n misses: number;\n entries?: number;\n sizeBytes?: number;\n /** 0〜1。`hits + misses === 0` のときは 0。 */\n hitRate: number;\n };\n /** 画像キャッシュの集計 (`handles: [\"image\"]` の adapter の `stats()` 戻り値)。 */\n image?: {\n adapter: string;\n hits: number;\n misses: number;\n entries?: number;\n sizeBytes?: number;\n hitRate: number;\n };\n}\n\nexport interface CMSGlobalOps {\n readonly collections: readonly string[];\n /** クライアント単位の trace ID (`createClient` で発行)。 */\n readonly traceId: string;\n invalidate(scope?: InvalidateScope): Promise<void>;\n /** Web Standard な Request/Response ベースのルートハンドラ (画像プロキシ + webhook)。 */\n handler(opts?: HandlerOptions): (req: Request) => Promise<Response>;\n getCachedImage(hash: string): Promise<StorageBinary | null>;\n /**\n * Notion 画像 URL を `{imageProxyBase}/{sha256}` 形式へ変換しキャッシュへ書き込む。\n * 画像キャッシュが未設定 (noop) の場合は `undefined`。\n */\n readonly cacheImage: ((url: string) => Promise<string>) | undefined;\n readonly imageProxyBase: string;\n /**\n * ドキュメント / 画像キャッシュのヒット・ミス・サイズを集約して返す。\n * 各キャッシュアダプタの `stats()` を呼ぶだけで副作用はない。\n * `stats()` を実装していない adapter は集計から除外される (noop など)。\n */\n stats(): Promise<CMSStats>;\n}\n\ninterface ResolvedCache {\n doc: DocumentCacheOps;\n docName: string;\n docAdapter: CacheAdapter | undefined;\n img: ImageCacheOps;\n imgName: string;\n imgAdapter: CacheAdapter | undefined;\n hasImg: boolean;\n}\n\n/**\n * adapter の `handles` を見て先勝ちで document / image を割り当てる。未指定は両方 noop。\n * `cms.stats()` から元 adapter の `stats()` を呼びたいので、解決元 adapter も保持する。\n */\nfunction resolveCache(\n cache: readonly CacheAdapter[] | undefined,\n): ResolvedCache {\n const adapters = cache ?? [];\n\n let doc: DocumentCacheOps = noopDocOps;\n let docName = \"noop-document\";\n let docAdapter: CacheAdapter | undefined;\n let img: ImageCacheOps = noopImgOps;\n let imgName = \"noop-image\";\n let imgAdapter: CacheAdapter | undefined;\n let docFound = false;\n let imgFound = false;\n\n for (const adapter of adapters) {\n if (!docFound && adapter.handles.includes(\"document\") && adapter.doc) {\n doc = adapter.doc;\n docName = adapter.name;\n docAdapter = adapter;\n docFound = true;\n }\n if (!imgFound && adapter.handles.includes(\"image\") && adapter.img) {\n img = adapter.img;\n imgName = adapter.name;\n imgAdapter = adapter;\n imgFound = true;\n }\n }\n\n return {\n doc,\n docName,\n docAdapter,\n img,\n imgName,\n imgAdapter,\n hasImg: imgFound,\n };\n}\n\n/**\n * 衝突しにくい短い trace ID を生成する。`{epoch36}-{rand36}` の 10〜12 文字程度。\n * core はゼロ依存ルールに従い node:crypto を静的 import しないため、Math.random ベースで十分。\n */\nfunction generateTraceId(): string {\n const t = Date.now().toString(36);\n const r = Math.floor(Math.random() * 36 ** 6)\n .toString(36)\n .padStart(6, \"0\");\n return `${t}-${r}`;\n}\n\nconst LOG_LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nfunction applyLogLevel(\n logger: Logger | undefined,\n minLevel: LogLevel,\n): Logger | undefined {\n if (!logger) return undefined;\n const minOrder = LOG_LEVEL_ORDER[minLevel];\n const filtered: Logger = {};\n for (const level of [\"debug\", \"info\", \"warn\", \"error\"] as const) {\n if (LOG_LEVEL_ORDER[level] >= minOrder) {\n filtered[level] = logger[level];\n }\n }\n return filtered;\n}\n\n/**\n * CMS クライアントを生成する。\n *\n * @example\n * import { createClient, nodePreset } from \"@notion-headless-cms/core\";\n * import { notionSource } from \"@notion-headless-cms/notion-source\";\n * import { schema } from \"./generated/nhc.schema\";\n *\n * const cms = createClient({\n * sources: { notion: notionSource({ schema, token: process.env.NOTION_TOKEN! }) },\n * ...nodePreset(),\n * });\n *\n * const posts = await cms.posts.list();\n */\nexport function createClient<S extends CMSSources = CMSSources>(\n opts: CreateClientOptions<S>,\n): CMSClient<\n MergeSourceCollections<S> extends CollectionsConfig\n ? MergeSourceCollections<S>\n : CollectionsConfig\n> {\n // sources の各アダプタが持つ collections をマージする\n const collectionsInput: CollectionsConfig = {};\n if (opts.sources) {\n for (const adapter of Object.values(\n opts.sources as unknown as Record<string, CMSAdapter | undefined>,\n )) {\n if (adapter) Object.assign(collectionsInput, adapter.collections);\n }\n }\n\n if (Object.keys(collectionsInput).length === 0) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message:\n \"createClient: sources に少なくとも 1 つのコレクションを指定してください。\",\n context: { operation: \"createClient\" },\n nextSteps: [\n \"notionSource({ schema, token }) を sources.notion に渡す\",\n \"`nhc generate` でスキーマを生成してから import する\",\n ],\n docsUrl:\n \"https://github.com/kjfsm/notion-headless-cms/blob/main/docs/quickstart.md\",\n });\n }\n\n for (const [name, def] of Object.entries(collectionsInput)) {\n if (!def.source) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message: `createClient: コレクション \"${name}\" の source は必須です。`,\n context: { operation: \"createClient\", collection: name },\n nextSteps: [\"notionSource(...) を sources に渡しているか確認する\"],\n });\n }\n if (!def.slugField) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message: `createClient: コレクション \"${name}\" の slugField は必須です。`,\n context: { operation: \"createClient\", collection: name },\n nextSteps: [\n `nhc.config.ts の ${name} コレクションに slugField を設定する`,\n ],\n });\n }\n }\n\n const cacheRes = resolveCache(opts.cache);\n const ttlMs = opts.swr?.ttlMs;\n const imageProxyBase = opts.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;\n const contentConfig = opts.content;\n const rendererFn: RendererFn | undefined = opts.renderer;\n const waitUntil = opts.waitUntil;\n // plugin logger と createClient 引数の logger を合成し、その上でクライアント単位の\n // traceId をログコンテキストに自動付与する。\n const baseLogger: Logger | undefined = mergeLoggers(\n opts.plugins ?? [],\n opts.logger,\n );\n const traceId = generateTraceId();\n const tracedLogger = withTraceId(baseLogger, traceId);\n const logger = opts.logLevel\n ? applyLogLevel(tracedLogger, opts.logLevel)\n : tracedLogger;\n const hooks: CMSHooks<BaseContentItem> = mergeHooks(\n opts.plugins ?? [],\n opts.hooks,\n logger,\n );\n const maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;\n const retryConfig: RetryConfig = {\n ...DEFAULT_RETRY_CONFIG,\n ...(opts.rateLimiter ?? {}),\n };\n\n const collectionNames: string[] = [];\n const collections: Record<string, CollectionClient<BaseContentItem>> = {};\n for (const [name, def] of Object.entries(collectionsInput)) {\n collectionNames.push(name);\n const source = def.source as DataSource<BaseContentItem>;\n const colHooks = def.hooks as CMSHooks<BaseContentItem> | undefined;\n const collectionHooks: CMSHooks<BaseContentItem> = colHooks\n ? mergeHooks([{ name: `${name}:global`, hooks }], colHooks, logger)\n : hooks;\n const renderCtx: RenderContext<BaseContentItem> = {\n source,\n rendererFn,\n imgCache: cacheRes.img,\n imgCacheName: cacheRes.imgName,\n hasImageCache: cacheRes.hasImg,\n imageProxyBase,\n contentConfig,\n hooks: collectionHooks,\n logger,\n };\n const ctx: CollectionContext<BaseContentItem> = {\n collection: name,\n source,\n docCache: cacheRes.doc,\n docCacheName: cacheRes.docName,\n render: renderCtx,\n hooks: collectionHooks,\n logger,\n ttlMs,\n publishedStatuses: def.publishedStatuses\n ? [...def.publishedStatuses]\n : [],\n accessibleStatuses: def.accessibleStatuses\n ? [...def.accessibleStatuses]\n : [],\n retryConfig,\n maxConcurrent,\n waitUntil,\n slugField: def.slugField,\n };\n collections[name] = new CollectionClientImpl(ctx);\n }\n\n const cacheImage = cacheRes.hasImg\n ? buildCacheImageFn(cacheRes.img, cacheRes.imgName, imageProxyBase, logger)\n : undefined;\n\n const globalOps: CMSGlobalOps = {\n collections: collectionNames,\n cacheImage,\n imageProxyBase,\n traceId,\n async stats(): Promise<CMSStats> {\n const stats: CMSStats = { traceId };\n // doc / img それぞれ、resolveCache が選んだ adapter にだけ stats() を要求する。\n // 同じ adapter が両方を担当している場合 (memoryCache など) は 1 回呼ぶだけで足りる。\n const adapterCache = new Map<CacheAdapter, Promise<CacheAdapterStats>>();\n const ensure = (\n adapter: CacheAdapter | undefined,\n ): Promise<CacheAdapterStats> | undefined => {\n if (!adapter?.stats) return undefined;\n const cached = adapterCache.get(adapter);\n if (cached) return cached;\n const fresh = adapter.stats();\n adapterCache.set(adapter, fresh);\n return fresh;\n };\n const docPromise = ensure(cacheRes.docAdapter);\n const imgPromise = ensure(cacheRes.imgAdapter);\n const computeHitRate = (h: number, m: number): number =>\n h + m === 0 ? 0 : h / (h + m);\n if (docPromise) {\n const docStats = await docPromise;\n if (docStats.doc) {\n stats.document = {\n adapter: docStats.name ?? cacheRes.docName,\n hits: docStats.doc.hits,\n misses: docStats.doc.misses,\n entries: docStats.doc.entries,\n sizeBytes: docStats.doc.sizeBytes,\n hitRate: computeHitRate(docStats.doc.hits, docStats.doc.misses),\n };\n }\n }\n if (imgPromise) {\n const imgStats = await imgPromise;\n if (imgStats.img) {\n stats.image = {\n adapter: imgStats.name ?? cacheRes.imgName,\n hits: imgStats.img.hits,\n misses: imgStats.img.misses,\n entries: imgStats.img.entries,\n sizeBytes: imgStats.img.sizeBytes,\n hitRate: computeHitRate(imgStats.img.hits, imgStats.img.misses),\n };\n }\n }\n return stats;\n },\n async invalidate(scope?: InvalidateScope): Promise<void> {\n logger?.debug?.(\"グローバルキャッシュを無効化\", {\n operation: \"invalidate\",\n cacheAdapter: cacheRes.docName,\n });\n await cacheRes.doc.invalidate(scope ?? \"all\");\n },\n handler(handlerOpts?: HandlerOptions) {\n return createHandler(\n {\n imageCache: cacheRes.img,\n async parseWebhookFor(collection, req, webhookSecret) {\n const def = collectionsInput[collection];\n if (!def) {\n throw new CMSError({\n code: \"webhook/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"parseWebhookFor\", collection },\n });\n }\n const ds = def.source as DataSource<BaseContentItem>;\n if (!ds.parseWebhook) {\n throw new CMSError({\n code: \"webhook/not_implemented\",\n message: `Collection \"${collection}\" does not support webhooks.`,\n context: { operation: \"parseWebhookFor\", collection },\n });\n }\n return ds.parseWebhook(req, { secret: webhookSecret });\n },\n revalidate: (scope) => globalOps.invalidate(scope),\n peekVersionFor(collection, slug) {\n const client = collections[collection];\n if (!client) {\n throw new CMSError({\n code: \"handler/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"peekVersionFor\", collection, slug },\n });\n }\n return client.peekVersion(slug);\n },\n async checkFor(collection, slug, currentVersion) {\n const client = collections[collection];\n if (!client) {\n throw new CMSError({\n code: \"handler/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"checkFor\", collection, slug },\n });\n }\n const result = await client.check(slug, currentVersion);\n // ItemWithContent は lazy 関数を含むため、HTTP には stale 判定のみ返す\n // (差分ありの場合 check() が副作用でキャッシュ更新済み。利用側は loader 再実行で本文取得)。\n return result === null ? null : { stale: result.stale };\n },\n },\n handlerOpts,\n );\n },\n getCachedImage(hash) {\n return cacheRes.img.get(hash);\n },\n };\n\n return Object.assign(\n Object.create(null) as object,\n collections,\n globalOps,\n ) as CMSClient<\n MergeSourceCollections<S> extends CollectionsConfig\n ? MergeSourceCollections<S>\n : CollectionsConfig\n >;\n}\n","import type { CollectionClient } from \"./types/collection\";\nimport type { BaseContentItem } from \"./types/content\";\n\n/** 逆引きの解決先。Notion ページ ID から自サイトの位置を引く。 */\nexport interface PageIndexEntry {\n /** 所属コレクション名。 */\n collection: string;\n /** URL キー。 */\n slug: string;\n /** ページ名(表示テキスト用)。 */\n title?: string | null;\n}\n\n/** `buildPageIndex` が返す逆引きマップ。キーは `normalizePageId` 済みの pageId。 */\nexport type PageIndex = Map<string, PageIndexEntry>;\n\n/**\n * `buildPageIndex` / `buildPageLinkMap` が必要とする最小の CMS 形状。\n * `collections` でコレクション名を列挙し、各名でコレクションクライアントへアクセスする。\n * `CMSClient` はコレクションを自身に spread しているため構造的に満たす。\n */\nexport interface PageIndexSource {\n readonly collections: readonly string[];\n}\n\nexport interface BuildPageIndexOptions {\n /** 走査対象のコレクション名。未指定なら `source.collections` 全件。 */\n collections?: readonly string[];\n}\n\n/**\n * Notion ページ ID を比較用に正規化する。\n * Notion の ID は文脈によりダッシュの有無・大文字小文字が揺れるため、\n * ダッシュ除去 + 小文字化して突き合わせる。\n *\n * react-renderer 側も同一実装で `pageLinks` を引くため、変更時は両方を揃えること。\n */\nexport function normalizePageId(id: string): string {\n return id.replace(/-/g, \"\").toLowerCase();\n}\n\nfunction asCollectionClient(\n source: PageIndexSource,\n name: string,\n): CollectionClient<BaseContentItem> | undefined {\n const client = (source as unknown as Record<string, unknown>)[name] as\n | CollectionClient<BaseContentItem>\n | undefined;\n // collections に名前はあっても spread されていない / list を持たない値は無視する。\n if (!client || typeof client.list !== \"function\") return undefined;\n return client;\n}\n\n/**\n * 全コレクションを `list()` で走査し、pageId → {collection, slug, title} の逆引きマップを構築する。\n * Notion 内部リンク(link_to_page / page mention など)を自サイト URL へ解決するための材料。\n *\n * `list()` は SWR ドキュメントキャッシュ経由のためウォーム後は安価。\n */\nexport async function buildPageIndex(\n source: PageIndexSource,\n opts?: BuildPageIndexOptions,\n): Promise<PageIndex> {\n // collections を持たない / 異常な source でもクラッシュさせず空マップを返す。\n const names = opts?.collections ?? source.collections;\n const index: PageIndex = new Map();\n if (\n !names ||\n typeof (names as Iterable<string>)[Symbol.iterator] !== \"function\"\n ) {\n return index;\n }\n for (const name of names) {\n const client = asCollectionClient(source, name);\n if (!client) continue;\n const items = await client.list();\n if (!Array.isArray(items)) continue;\n for (const item of items) {\n // pageId は一意なので衝突しない前提。万一重複しても先勝ちで保持する。\n const key = normalizePageId(item.id);\n if (!index.has(key)) {\n index.set(key, {\n collection: name,\n slug: item.slug,\n title: item.title,\n });\n }\n }\n }\n return index;\n}\n\n/** 解決済みの内部リンク。`href` は構築時に決まり、`title` は表示テキスト用。 */\nexport interface ResolvedPageLink {\n href: string;\n title?: string | null;\n}\n\n/**\n * 正規化済み pageId → 解決済みリンクのプレーンマップ。\n * 関数ではなくプレーンオブジェクトなので、loader / RSC(Server Component → Client\n * Component)境界を越えて `<NotionRenderer pageLinks={...} />` にそのまま渡せる。\n */\nexport type PageLinkMap = Record<string, ResolvedPageLink>;\n\nexport interface BuildPageLinkMapOptions extends BuildPageIndexOptions {\n /**\n * エントリから URL を組み立てる関数。既定は `/${collection}/${slug}`。\n * 単一コレクションで `/${slug}` にしたい場合などに上書きする。\n */\n url?: (entry: PageIndexEntry, pageId: string) => string;\n /** 事前構築済みインデックス。指定するとリクエストごとの再構築を省ける。 */\n index?: PageIndex;\n}\n\nconst defaultUrl = (entry: PageIndexEntry): string =>\n `/${entry.collection}/${entry.slug}`;\n\n/**\n * Notion 内部リンクを「正規化 pageId → {href, title}」のプレーンマップに解決する。\n * サーバ側(loader / RSC / route handler)で 1 回構築し、`<NotionRenderer pageLinks={...} />`\n * に渡す。プレーンオブジェクトなのでシリアライズ境界(RSC / loader)を越えられる。\n *\n * @example\n * const pageLinks = await buildPageLinkMap(cms);\n * <NotionRenderer blocks={blocks} pageLinks={pageLinks} />;\n */\nexport async function buildPageLinkMap(\n source: PageIndexSource,\n opts?: BuildPageLinkMapOptions,\n): Promise<PageLinkMap> {\n const index = opts?.index ?? (await buildPageIndex(source, opts));\n const toUrl = opts?.url ?? defaultUrl;\n const map: PageLinkMap = {};\n for (const [key, entry] of index) {\n map[key] = { href: toUrl(entry, key), title: entry.title };\n }\n return map;\n}\n","import type { CacheAdapter } from \"./cache\";\nimport type { BaseContentItem } from \"./content\";\nimport type { DataSource } from \"./data-source\";\nimport type { CMSHooks } from \"./hooks\";\nimport type { Logger } from \"./logger\";\nimport type { CMSPlugin } from \"./plugin\";\nimport type { CMSSources } from \"./sources\";\n\n/** `Logger` の出力を絞り込むログレベル。指定したレベル未満のログを抑制する。 */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/**\n * renderer プラグインの不透明型。\n * core は unified / remark / rehype に依存せず、このリストをそのまま renderer に渡すだけ。\n */\nexport type RendererPluginList = unknown[];\n\n/**\n * render() オプション。core は renderer の実装を知らず、この型だけを扱う。\n * @notion-headless-cms/markdown-html の renderMarkdown() はこのシグネチャと構造的に互換。\n */\nexport interface RenderOptions {\n imageProxyBase?: string;\n cacheImage?: (url: string) => Promise<string>;\n remarkPlugins?: RendererPluginList;\n rehypePlugins?: RendererPluginList;\n}\n\n/** カスタムレンダラー関数の型。デフォルトは @notion-headless-cms/markdown-html の renderMarkdown。 */\nexport type RendererFn = (\n markdown: string,\n opts?: RenderOptions,\n) => Promise<string>;\n\n/** レンダリング・コンテンツ処理設定。 */\nexport interface ContentConfig {\n /** 追加する remark プラグイン。 */\n remarkPlugins?: RendererPluginList;\n /** 追加する rehype プラグイン。 */\n rehypePlugins?: RendererPluginList;\n}\n\n/** SWR(Stale-While-Revalidate)設定。 */\nexport interface SWRConfig {\n /** SWR の有効期間 (ミリ秒)。未設定時は TTL なし(失効まで stale を返す)。 */\n ttlMs?: number;\n}\n\n/**\n * `RateLimiterConfig` のデフォルト値 (Issue #313 / M2)。\n * 型からだけでは見えない既定値を表面化することで、IDE 補完と\n * preset の対称化 (`nodePreset` / `cloudflarePreset` / `nextPreset`) で\n * 同じ既定が適用されることを保証する。\n */\nexport const DEFAULT_RATE_LIMITER: Required<RateLimiterConfig> = {\n maxConcurrent: 3,\n retryOn: [429, 502, 503],\n maxRetries: 4,\n baseDelayMs: 1000,\n};\n\n/** レートリミット・リトライ設定。既定値は {@link DEFAULT_RATE_LIMITER}。 */\nexport interface RateLimiterConfig {\n /** 同時実行数の上限。デフォルト: 3 */\n maxConcurrent?: number;\n /** リトライ対象の HTTP ステータスコード。デフォルト: [429, 502, 503] */\n retryOn?: number[];\n /** 最大リトライ回数。デフォルト: 4 */\n maxRetries?: number;\n /** リトライ時の基準待機時間(ミリ秒)。デフォルト: 1000 */\n baseDelayMs?: number;\n}\n\n/**\n * コレクション 1 件の定義。CLI が生成する `nhc.ts` から `createClient` に渡される。\n *\n * `source` は notion-orm 等の DataSource 実装。\n * `slugField` / `statusField` は TS フィールド名 (DataSource の `properties` キーと一致)。\n */\nexport interface CollectionDef<T extends BaseContentItem = BaseContentItem> {\n /** Notion etc. のデータソース実装。 */\n source: DataSource<T>;\n /** slug として使う TS フィールド名 (必須)。`source.properties[slugField]` で Notion プロパティ名を解決する。 */\n slugField: string;\n /** ステータスとして使う TS フィールド名。 */\n statusField?: string;\n /** 公開扱いするステータス値。`list()` のデフォルト絞り込みに使う。 */\n publishedStatuses?: readonly string[];\n /** アクセス許可するステータス値。`get()` の閲覧可否判定に使う。 */\n accessibleStatuses?: readonly string[];\n /** コレクション固有のライフサイクルフック。グローバル hooks の後に実行される。 */\n hooks?: CMSHooks<T>;\n}\n\n/**\n * `CollectionDef` の strict 版。`slugField` / `statusField` が `keyof T & string` で\n * 型ガードされており、誤フィールド名は型エラーになる (Issue #314 / M3)。\n * CLI 生成スキーマや `defineCollection<T>()` 経由で利用される。\n */\nexport interface StrictCollectionDef<T extends BaseContentItem>\n extends Omit<CollectionDef<T>, \"slugField\" | \"statusField\"> {\n /** slug として使う TS フィールド名。`keyof T` で型ガードされる。 */\n slugField: keyof T & string;\n /** ステータスとして使う TS フィールド名。`keyof T` で型ガードされる。 */\n statusField?: keyof T & string;\n}\n\n/**\n * 型推論ヘルパー: `T` を明示してコレクション定義を作る。`slugField` / `statusField` は\n * `keyof T & string` で補完・型ガードされ、誤フィールド名 (例: `\"slag\"`) で型エラーになる\n * (Issue #314 / M3)。CLI 生成 `nhc.schema.ts` で利用される。\n *\n * @example\n * ```ts\n * type PostItem = BaseContentItem & { authorName?: string };\n * const posts = defineCollection<PostItem>({\n * source: notionSource(...),\n * slugField: \"slug\", // OK\n * statusField: \"status\", // OK\n * // statusField: \"stat\", // 型エラー\n * });\n * ```\n */\nexport function defineCollection<T extends BaseContentItem>(\n def: StrictCollectionDef<T>,\n): CollectionDef<T> {\n // StrictCollectionDef は CollectionDef の slugField/statusField を絞ったサブ型なので\n // 構造的に CollectionDef<T> へ代入可能。型システム上は再構成のため as 経由。\n return def as unknown as CollectionDef<T>;\n}\n\n/**\n * `createClient({ collections })` の map 型。\n * キーがコレクション名、値が `CollectionDef<T>`。\n */\nexport type CollectionsConfig = Record<string, CollectionDef<BaseContentItem>>;\n\n/** `CollectionsConfig` から各 T を抽出するユーティリティ型。 */\nexport type InferCollectionItem<C> =\n C extends CollectionDef<infer T> ? T : BaseContentItem;\n\n/**\n * `createClient()` の入力。\n *\n * @example\n * import { createClient, nodePreset } from \"@notion-headless-cms/core\";\n * import { notionSource } from \"@notion-headless-cms/notion-source\";\n * import { schema } from \"./generated/nhc.schema\";\n *\n * const cms = createClient({\n * sources: { notion: notionSource({ schema, token: process.env.NOTION_TOKEN! }) },\n * ...nodePreset(),\n * });\n */\nexport interface CreateClientOptions<S extends CMSSources = CMSSources> {\n /** データソースアダプター (`@notion-headless-cms/notion-source` 等) のマップ。 */\n sources?: S;\n /**\n * キャッシュアダプタ (配列)。未指定時はキャッシュなし。\n * - `memoryCache()` のように doc + image 両方を担当するもの\n * - `r2Cache()` (image のみ)、`kvCache()` (doc のみ) のように片側のみ担当するもの\n * - 複数 adapter を配列で組み合わせると、各 adapter の `handles` で振り分けられる\n */\n cache?: readonly CacheAdapter[];\n /** SWR(Stale-While-Revalidate)設定。 */\n swr?: SWRConfig;\n /**\n * Markdown→HTML レンダラー。\n * 省略時は `@notion-headless-cms/markdown-html` の `renderMarkdown` を動的 import で使用する。\n * カスタム実装も `RendererFn` 型を満たせば使用可能。\n */\n renderer?: RendererFn;\n /** 画像プロキシのベース URL。デフォルト `/api/images`。 */\n imageProxyBase?: string;\n /** Cloudflare Workers の `waitUntil` に相当する非同期処理の登録関数。 */\n waitUntil?: (p: Promise<unknown>) => void;\n /** ライフサイクルフック (全コレクション共通)。 */\n hooks?: CMSHooks<BaseContentItem>;\n /** プラグイン配列。 */\n plugins?: CMSPlugin<BaseContentItem>[];\n /** ロガー。 */\n logger?: Logger;\n /** ログレベルの下限。指定したレベル未満のログを内部で抑制する。 */\n logLevel?: LogLevel;\n /** レートリミット・リトライ設定。 */\n rateLimiter?: RateLimiterConfig;\n /** レンダリング・コンテンツ処理設定。 */\n content?: ContentConfig;\n}\n","import type { BaseContentItem } from \"./content\";\nimport type { CMSHooks } from \"./hooks\";\nimport type { Logger } from \"./logger\";\n\nexport interface CMSPlugin<T extends BaseContentItem = BaseContentItem> {\n name: string;\n hooks?: CMSHooks<T>;\n logger?: Partial<Logger>;\n}\n\nexport function definePlugin<T extends BaseContentItem>(\n plugin: CMSPlugin<T>,\n): CMSPlugin<T> {\n return plugin;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/cache.ts","../src/cache/noop.ts","../src/page-index.ts","../src/image.ts","../src/rendering.ts","../src/retry.ts","../src/collection.ts","../src/handler.ts","../src/cms.ts","../src/types/config.ts","../src/types/plugin.ts"],"sourcesContent":["/** 文字列をSHA-256でハッシュ化し、16進数文字列として返す。画像キーの生成に使用。 */\nexport async function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * キャッシュが有効期限切れかどうかを判定する。\n * ttlMs が未指定の場合は常に false(無期限有効)を返す。\n */\nexport function isStale(cachedAt: number, ttlMs?: number): boolean {\n if (ttlMs === undefined) return false;\n return Date.now() - cachedAt > ttlMs;\n}\n","import type {\n BaseContentItem,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n StorageBinary,\n} from \"../types/index\";\n\nconst noopDoc: DocumentCacheOps = {\n getList<T extends BaseContentItem>(\n _collection: string,\n ): Promise<CachedItemList<T> | null> {\n return Promise.resolve(null);\n },\n setList<T extends BaseContentItem>(\n _collection: string,\n _data: CachedItemList<T>,\n ): Promise<void> {\n return Promise.resolve();\n },\n getMeta<T extends BaseContentItem>(\n _collection: string,\n _slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n return Promise.resolve(null);\n },\n setMeta<T extends BaseContentItem>(\n _collection: string,\n _slug: string,\n _data: CachedItemMeta<T>,\n ): Promise<void> {\n return Promise.resolve();\n },\n getContent(\n _collection: string,\n _slug: string,\n ): Promise<CachedItemContent | null> {\n return Promise.resolve(null);\n },\n setContent(\n _collection: string,\n _slug: string,\n _data: CachedItemContent,\n ): Promise<void> {\n return Promise.resolve();\n },\n invalidate(): Promise<void> {\n return Promise.resolve();\n },\n};\n\nconst noopImg: ImageCacheOps = {\n get(_hash: string): Promise<StorageBinary | null> {\n return Promise.resolve(null);\n },\n set(): Promise<void> {\n return Promise.resolve();\n },\n};\n\n/**\n * 何もキャッシュしないアダプタ。`createClient({ cache })` 未指定時の内部デフォルト。\n * テストでも使える。\n */\nexport const noopDocOps: DocumentCacheOps = noopDoc;\nexport const noopImgOps: ImageCacheOps = noopImg;\n","import type { CollectionClient } from \"./types/collection\";\nimport type { BaseContentItem } from \"./types/content\";\n\n/** 逆引きの解決先。Notion ページ ID から自サイトの位置を引く。 */\nexport interface PageIndexEntry {\n /** 所属コレクション名。 */\n collection: string;\n /** URL キー。 */\n slug: string;\n /** ページ名(表示テキスト用)。 */\n title?: string | null;\n}\n\n/** `buildPageIndex` が返す逆引きマップ。キーは `normalizePageId` 済みの pageId。 */\nexport type PageIndex = Map<string, PageIndexEntry>;\n\n/**\n * `buildPageIndex` / `buildPageLinkMap` が必要とする最小の CMS 形状。\n * `collections` でコレクション名を列挙し、各名でコレクションクライアントへアクセスする。\n * `CMSClient` はコレクションを自身に spread しているため構造的に満たす。\n */\nexport interface PageIndexSource {\n readonly collections: readonly string[];\n}\n\nexport interface BuildPageIndexOptions {\n /** 走査対象のコレクション名。未指定なら `source.collections` 全件。 */\n collections?: readonly string[];\n}\n\n/**\n * Notion ページ ID を比較用に正規化する。\n * Notion の ID は文脈によりダッシュの有無・大文字小文字が揺れるため、\n * ダッシュ除去 + 小文字化して突き合わせる。\n *\n * react-renderer 側も同一実装で `pageLinks` を引くため、変更時は両方を揃えること。\n */\nexport function normalizePageId(id: string): string {\n return id.replace(/-/g, \"\").toLowerCase();\n}\n\nfunction asCollectionClient(\n source: PageIndexSource,\n name: string,\n): CollectionClient<BaseContentItem> | undefined {\n const client = (source as unknown as Record<string, unknown>)[name] as\n | CollectionClient<BaseContentItem>\n | undefined;\n // collections に名前はあっても spread されていない / list を持たない値は無視する。\n if (!client || typeof client.list !== \"function\") return undefined;\n return client;\n}\n\n/**\n * 全コレクションを `list()` で走査し、pageId → {collection, slug, title} の逆引きマップを構築する。\n * Notion 内部リンク(link_to_page / page mention など)を自サイト URL へ解決するための材料。\n *\n * `list()` は SWR ドキュメントキャッシュ経由のためウォーム後は安価。\n */\nexport async function buildPageIndex(\n source: PageIndexSource,\n opts?: BuildPageIndexOptions,\n): Promise<PageIndex> {\n // collections を持たない / 異常な source でもクラッシュさせず空マップを返す。\n const names = opts?.collections ?? source.collections;\n const index: PageIndex = new Map();\n if (\n !names ||\n typeof (names as Iterable<string>)[Symbol.iterator] !== \"function\"\n ) {\n return index;\n }\n for (const name of names) {\n const client = asCollectionClient(source, name);\n if (!client) continue;\n const items = await client.list();\n if (!Array.isArray(items)) continue;\n for (const item of items) {\n // pageId は一意なので衝突しない前提。万一重複しても先勝ちで保持する。\n const key = normalizePageId(item.id);\n if (!index.has(key)) {\n index.set(key, {\n collection: name,\n slug: item.slug,\n title: item.title,\n });\n }\n }\n }\n return index;\n}\n\n/** 解決済みの内部リンク。`href` は構築時に決まり、`title` は表示テキスト用。 */\nexport interface ResolvedPageLink {\n href: string;\n title?: string | null;\n}\n\n/**\n * 正規化済み pageId → 解決済みリンクのプレーンマップ。\n * 関数ではなくプレーンオブジェクトなので、loader / RSC(Server Component → Client\n * Component)境界を越えて `<NotionRenderer pageLinks={...} />` にそのまま渡せる。\n */\nexport type PageLinkMap = Record<string, ResolvedPageLink>;\n\nexport interface BuildPageLinkMapOptions extends BuildPageIndexOptions {\n /**\n * エントリから URL を組み立てる関数。既定は `/${collection}/${slug}`。\n * 単一コレクションで `/${slug}` にしたい場合などに上書きする。\n */\n url?: (entry: PageIndexEntry, pageId: string) => string;\n /** 事前構築済みインデックス。指定するとリクエストごとの再構築を省ける。 */\n index?: PageIndex;\n}\n\nconst defaultUrl = (entry: PageIndexEntry): string =>\n `/${entry.collection}/${entry.slug}`;\n\n/**\n * Notion 内部リンクを「正規化 pageId → {href, title}」のプレーンマップに解決する。\n * サーバ側(loader / RSC / route handler)で 1 回構築し、`<NotionRenderer pageLinks={...} />`\n * に渡す。プレーンオブジェクトなのでシリアライズ境界(RSC / loader)を越えられる。\n *\n * @example\n * const pageLinks = await buildPageLinkMap(cms);\n * <NotionRenderer blocks={blocks} pageLinks={pageLinks} />;\n */\nexport async function buildPageLinkMap(\n source: PageIndexSource,\n opts?: BuildPageLinkMapOptions,\n): Promise<PageLinkMap> {\n const index = opts?.index ?? (await buildPageIndex(source, opts));\n const toUrl = opts?.url ?? defaultUrl;\n const map: PageLinkMap = {};\n for (const [key, entry] of index) {\n map[key] = { href: toUrl(entry, key), title: entry.title };\n }\n return map;\n}\n","import { sha256Hex } from \"./cache\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport type { ImageCacheOps, Logger, StorageBinary } from \"./types/index\";\n\n/**\n * レスポンスの Content-Type ヘッダから画像の MIME タイプを取り出す。\n * ヘッダがない、または image/* でない場合は CMSError を投げる。\n * URL 拡張子からの推測や jpeg デフォルトは行わない。\n */\nfunction pickImageContentType(\n headerValue: string | null,\n notionUrl: string,\n): string {\n if (!headerValue) {\n throw new CMSError({\n code: \"cache/image_invalid_content_type\",\n message: \"Image response missing Content-Type header.\",\n context: { operation: \"fetchAndCacheImage:contentType\", notionUrl },\n });\n }\n const value = (headerValue.split(\";\")[0] ?? headerValue).trim().toLowerCase();\n if (!value.startsWith(\"image/\")) {\n throw new CMSError({\n code: \"cache/image_invalid_content_type\",\n message: `Image response has non-image Content-Type: ${value}`,\n context: {\n operation: \"fetchAndCacheImage:contentType\",\n notionUrl,\n contentType: value,\n },\n });\n }\n return value;\n}\n\n/**\n * Notion画像URLをfetchして ImageCacheOps にキャッシュし、プロキシURL を返す。\n * 既存キャッシュがあれば再fetchしない。\n */\nasync function fetchAndCacheImage(\n cache: ImageCacheOps,\n cacheName: string,\n notionUrl: string,\n hash: string,\n imageProxyBase: string,\n logger?: Logger,\n): Promise<string> {\n const proxyUrl = `${imageProxyBase}/${hash}`;\n\n const existing = await cache.get(hash);\n if (existing) {\n logger?.debug?.(\"画像キャッシュヒット\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n return proxyUrl;\n }\n\n logger?.debug?.(\"画像キャッシュミス、Notion からフェッチ\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n\n try {\n const response = await fetch(notionUrl, {\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n throw new CMSError({\n code: \"cache/image_fetch_failed\",\n message: `Failed to fetch Notion image: HTTP ${response.status}`,\n context: {\n operation: \"fetchAndCacheImage\",\n notionUrl,\n httpStatus: response.status,\n },\n });\n }\n\n const data = await response.arrayBuffer();\n const contentType = pickImageContentType(\n response.headers.get(\"content-type\"),\n notionUrl,\n );\n await cache.set(hash, data, contentType);\n logger?.debug?.(\"画像をキャッシュに保存\", {\n operation: \"fetchAndCacheImage\",\n cacheAdapter: cacheName,\n imageHash: hash,\n });\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"cache/io_failed\",\n message: \"Failed to fetch or cache Notion image.\",\n cause: err,\n context: { operation: \"fetchAndCacheImage\", notionUrl },\n });\n }\n\n return proxyUrl;\n}\n\n/**\n * `ImageCacheOps` と `imageProxyBase` から `cacheImage` 関数を構築する。\n * 返り値は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして\n * {@link ImageCacheOps} に保存後、プロキシ URL を返す。\n *\n * ハッシュのメモ化はファクトリ呼び出し単位でスコープ化されており、\n * インスタンス間でキャッシュを共有しない。\n */\nexport function buildCacheImageFn(\n cache: ImageCacheOps,\n cacheName: string,\n imageProxyBase: string,\n logger?: Logger,\n): (notionUrl: string) => Promise<string> {\n const hashMemo = new Map<string, string>();\n return async (notionUrl) => {\n let hash = hashMemo.get(notionUrl);\n if (hash === undefined) {\n hash = await sha256Hex(notionUrl);\n hashMemo.set(notionUrl, hash);\n }\n return fetchAndCacheImage(\n cache,\n cacheName,\n notionUrl,\n hash,\n imageProxyBase,\n logger,\n );\n };\n}\n\nexport type { StorageBinary };\n","import type { ContentBlock } from \"./content/blocks\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport { buildCacheImageFn } from \"./image\";\nimport type {\n BaseContentItem,\n CachedItemContent,\n CachedItemMeta,\n CMSHooks,\n ContentConfig,\n DataSource,\n ImageCacheOps,\n Logger,\n RendererFn,\n} from \"./types/index\";\n\n/**\n * `@notion-headless-cms/markdown-html` を動的 import してデフォルトレンダラーを返す。\n * core のゼロ依存ルールを守るため静的 import は禁止。\n */\nasync function loadDefaultRenderer(): Promise<RendererFn> {\n try {\n const mod = await import(\"@notion-headless-cms/markdown-html\");\n return (mod as { renderMarkdown: RendererFn }).renderMarkdown;\n } catch {\n throw new CMSError({\n code: \"core/config_invalid\",\n message:\n \"renderer が未指定で、@notion-headless-cms/markdown-html のロードにも失敗しました。\" +\n \" createClient の renderer オプションを指定するか、@notion-headless-cms/markdown-html をインストールしてください。\",\n context: { operation: \"loadDefaultRenderer\" },\n });\n }\n}\n\nexport interface RenderContext<T extends BaseContentItem> {\n source: DataSource<T>;\n rendererFn: RendererFn | undefined;\n imgCache: ImageCacheOps;\n imgCacheName: string;\n hasImageCache: boolean;\n imageProxyBase: string;\n contentConfig: ContentConfig | undefined;\n hooks: CMSHooks<T>;\n logger: Logger | undefined;\n}\n\n/**\n * メタデータキャッシュエントリを生成する。Notion API も renderer も呼ばない軽量関数。\n */\nexport function buildCachedItemMeta<T extends BaseContentItem>(\n item: T,\n source: DataSource<T>,\n): CachedItemMeta<T> {\n return {\n item,\n notionUpdatedAt: source.getLastModified(item),\n cachedAt: Date.now(),\n };\n}\n\n/**\n * アイテム本文を Markdown ロード → blocks 生成 → HTML レンダリング → フック適用まで\n * 実行し、本文キャッシュ用の `CachedItemContent` を返す。\n */\nexport async function buildCachedItemContent<T extends BaseContentItem>(\n item: T,\n ctx: RenderContext<T>,\n): Promise<CachedItemContent> {\n const start = Date.now();\n ctx.logger?.info?.(\"コンテンツのレンダリング開始\", {\n slug: item.slug,\n pageId: item.id,\n });\n ctx.hooks.onRenderStart?.(item.slug);\n\n let markdown: string;\n try {\n markdown = await ctx.source.loadMarkdown(item);\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"source/load_markdown_failed\",\n message: \"Failed to load markdown from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadMarkdown\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n let blocks: ContentBlock[];\n try {\n blocks = await ctx.source.loadBlocks(item);\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"source/load_blocks_failed\",\n message: \"Failed to load blocks from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadBlocks\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n // react-renderer など Notion 形式を直接消費する利用側のため、\n // DataSource が対応していれば BlockObjectResponse ツリーも取得・キャッシュする。\n // markdown 戦略のように loadNotionBlocks 未対応の場合は `source/blocks_unsupported`\n // を吸収して undefined にする (ページ描画は markdown 経路で代替する想定)。\n let notionBlocks: unknown[] | undefined;\n if (ctx.source.loadNotionBlocks) {\n try {\n notionBlocks = await ctx.source.loadNotionBlocks(item);\n } catch (err) {\n if (isCMSError(err) && err.is(\"source/blocks_unsupported\")) {\n notionBlocks = undefined;\n } else if (isCMSError(err)) {\n throw err;\n } else {\n throw new CMSError({\n code: \"source/load_blocks_failed\",\n message: \"Failed to load Notion blocks from source.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:loadNotionBlocks\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n }\n }\n\n const cacheImage = ctx.hasImageCache\n ? buildCacheImageFn(\n ctx.imgCache,\n ctx.imgCacheName,\n ctx.imageProxyBase,\n ctx.logger,\n )\n : undefined;\n\n const rendererFn = ctx.rendererFn ?? (await loadDefaultRenderer());\n\n let html: string;\n try {\n html = await rendererFn(markdown, {\n imageProxyBase: ctx.imageProxyBase,\n cacheImage,\n remarkPlugins: ctx.contentConfig?.remarkPlugins,\n rehypePlugins: ctx.contentConfig?.rehypePlugins,\n });\n } catch (err) {\n if (isCMSError(err)) throw err;\n throw new CMSError({\n code: \"renderer/failed\",\n message: \"Failed to render markdown.\",\n cause: err,\n context: {\n operation: \"buildCachedItemContent:renderMarkdown\",\n pageId: item.id,\n slug: item.slug,\n },\n });\n }\n\n if (ctx.hooks.afterRender) {\n html = await ctx.hooks.afterRender(html, item);\n }\n\n let result: CachedItemContent = {\n html,\n blocks,\n markdown,\n notionBlocks,\n notionUpdatedAt: ctx.source.getLastModified(item),\n cachedAt: Date.now(),\n };\n\n if (ctx.hooks.beforeCacheContent) {\n result = await ctx.hooks.beforeCacheContent(result, item);\n }\n\n const durationMs = Date.now() - start;\n ctx.logger?.info?.(\"コンテンツのレンダリング完了\", {\n slug: item.slug,\n durationMs,\n });\n ctx.hooks.onRenderEnd?.(item.slug, durationMs);\n\n return result;\n}\n","export interface RetryConfig {\n retryOn: number[];\n maxRetries: number;\n baseDelayMs: number;\n /** true のとき指数バックオフにランダムジッターを加える(Thundering Herd 対策)。デフォルト: true */\n jitter?: boolean;\n /**\n * リトライ前に呼ばれるフック。`attempt` は 1 始まり、`status` はリトライ対象の HTTP ステータス、\n * `delayMs` は次回試行までの実際の待機時間 (ジッター反映後)。\n * 既存呼び出しは `attempt` / `status` だけで動くよう `delayMs` は省略可。\n */\n onRetry?: (attempt: number, status: number, delayMs?: number) => void;\n}\n\nexport const DEFAULT_RETRY_CONFIG: RetryConfig = {\n retryOn: [429, 502, 503],\n maxRetries: 4,\n baseDelayMs: 1000,\n jitter: true,\n};\n\n/**\n * 指数バックオフ(オプションでジッター付き)でリトライする。\n *\n * `config.retryOn` に含まれるステータスコードを持つエラーのみリトライ対象。\n * 遅延は `baseDelayMs * 2^attempt` の指数バックオフ。\n * `jitter` が `true`(デフォルト)の場合、0.5〜1.0 の乱数係数を乗算して\n * Thundering Herd を防ぐ。`false` にすると確定的な遅延になる。\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n config: RetryConfig,\n): Promise<T> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n const status = (err as { status?: number }).status;\n if (status === undefined || !config.retryOn.includes(status)) {\n throw err;\n }\n lastError = err;\n if (attempt < config.maxRetries) {\n const jitterFactor =\n config.jitter !== false ? 0.5 + Math.random() * 0.5 : 1;\n const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;\n config.onRetry?.(attempt + 1, status, delay);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n throw lastError;\n}\n","import { isStale } from \"./cache\";\nimport { CMSError, isCMSError } from \"./errors\";\nimport { normalizePageId } from \"./page-index\";\nimport type { RenderContext } from \"./rendering\";\nimport { buildCachedItemContent, buildCachedItemMeta } from \"./rendering\";\nimport type { RetryConfig } from \"./retry\";\nimport { withRetry } from \"./retry\";\nimport type {\n AdjacencyOptions,\n BaseContentItem,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n CheckResult,\n CMSHooks,\n CollectionCacheOps,\n CollectionClient,\n DataSource,\n DocumentCacheOps,\n FindOptions,\n ItemWithContent,\n ListOptions,\n Logger,\n SortOption,\n WarmOptions,\n WarmResult,\n WhereClause,\n} from \"./types/index\";\n\n/**\n * コレクション別キャッシュキーを生成する (item: `{collection}:{slug}` / list: `{collection}`)。\n *\n * 各 cache adapter は内部で独自のキー戦略を持つが、ログ出力や差分再計算で\n * 同一表現が必要になるため core 側にも公開する。\n */\nexport function collectionKey(collection: string, slug?: string): string {\n return slug ? `${collection}:${slug}` : collection;\n}\n\nexport interface CollectionContext<T extends BaseContentItem> {\n collection: string;\n source: DataSource<T>;\n docCache: DocumentCacheOps;\n docCacheName: string;\n render: RenderContext<T>;\n hooks: CMSHooks<T>;\n logger: Logger | undefined;\n ttlMs: number | undefined;\n publishedStatuses: string[];\n accessibleStatuses: string[];\n retryConfig: RetryConfig;\n maxConcurrent: number;\n waitUntil: ((p: Promise<unknown>) => void) | undefined;\n /**\n * slug として使うフィールド名。`source.properties[slugField].notion` を\n * Notion プロパティ名として `findByProp` を呼び出す。\n */\n slugField: string;\n}\n\nexport class CollectionClientImpl<T extends BaseContentItem>\n implements CollectionClient<T>\n{\n readonly cache: CollectionCacheOps<T>;\n\n constructor(private readonly ctx: CollectionContext<T>) {\n this.cache = {\n invalidate: () => this.invalidateImpl(),\n invalidateItem: (slug: string) => this.invalidateItemImpl(slug),\n warm: (opts?: WarmOptions) => this.warmImpl(opts),\n prime: (slug: string) => this.primeImpl(slug),\n };\n }\n\n /**\n * Notion ページ ID で該当アイテムを解決し、単件ウォーム + リストキャッシュを更新する。\n * このコレクションに属さない page id の場合は何もせず `null` を返す。\n * 一致した場合は温めた slug を返す(公式 webhook から `cms.warmByPageId` 経由で呼ばれる)。\n */\n async warmByPageId(pageId: string): Promise<string | null> {\n const item = await this.resolveByPageId(pageId);\n if (!item) return null;\n await this.primeItem(item);\n // 一覧の見出し・新規公開・並び順の変化を反映するためリストキャッシュも作り直す。\n await this.refreshList();\n return item.slug;\n }\n\n async find(\n slug: string,\n opts: FindOptions = {},\n ): Promise<ItemWithContent<T> | null> {\n if (opts.bypassCache) {\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n return this.attachLazyContent(meta);\n }\n\n const cachedMeta = await this.ctx.docCache.getMeta<T>(\n this.ctx.collection,\n slug,\n );\n if (cachedMeta) {\n // TTL 切れはブロッキングで再取得する (stale を返さない要件)\n if (\n this.ctx.ttlMs !== undefined &&\n isStale(cachedMeta.cachedAt, this.ctx.ttlMs)\n ) {\n this.ctx.logger?.debug?.(\"キャッシュ期限切れ(TTL)、フェッチ\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n return this.attachLazyContent(meta);\n }\n const bg = this.checkAndUpdateItemBg(slug, cachedMeta);\n if (this.ctx.waitUntil) this.ctx.waitUntil(bg);\n this.ctx.logger?.debug?.(\"キャッシュヒット\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n cachedAt: cachedMeta.cachedAt,\n });\n this.ctx.hooks.onCacheHit?.(slug, cachedMeta);\n return this.attachLazyContent(cachedMeta);\n }\n\n this.ctx.logger?.debug?.(\"キャッシュミス、フェッチ\", {\n operation: \"find\",\n slug,\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onCacheMiss?.(slug);\n const item = await this.fetchRaw(slug);\n if (!item) return null;\n // 保存だけはバックグラウンド可: ユーザー向けレスポンスを早めに返す\n const meta = await this.persistMeta(slug, item, { background: true });\n return this.attachLazyContent(meta);\n }\n\n async list(opts?: ListOptions<T>): Promise<T[]> {\n const allItems = await this.fetchList();\n return applyListOptions(allItems, opts);\n }\n\n async params(): Promise<string[]> {\n const items = await this.fetchList();\n return items.map((item) => item.slug);\n }\n\n async peekVersion(\n slug: string,\n ): Promise<{ notionUpdatedAt: string; cachedAt: number } | null> {\n const meta = await this.ctx.docCache.getMeta<T>(this.ctx.collection, slug);\n if (!meta) return null;\n return { notionUpdatedAt: meta.notionUpdatedAt, cachedAt: meta.cachedAt };\n }\n\n async check(\n slug: string,\n currentVersion: string,\n ): Promise<CheckResult<T> | null> {\n const raw = await this.fetchRaw(slug);\n if (!raw) return null;\n if (raw.lastEditedTime === currentVersion) return { stale: false };\n const meta = await this.persistMeta(slug, raw);\n await this.invalidateContentEntry(slug);\n return { stale: true, item: this.attachLazyContent(meta) };\n }\n\n async adjacent(\n slug: string,\n opts?: AdjacencyOptions<T>,\n ): Promise<{ prev: T | null; next: T | null }> {\n const items = applyListOptions(await this.fetchList(), {\n sort: opts?.sort,\n });\n const index = items.findIndex((it) => it.slug === slug);\n if (index === -1) return { prev: null, next: null };\n return {\n prev: index > 0 ? (items[index - 1] ?? null) : null,\n next: index < items.length - 1 ? (items[index + 1] ?? null) : null,\n };\n }\n\n private async invalidateImpl(): Promise<void> {\n this.ctx.logger?.debug?.(\"コレクション全体のキャッシュを無効化\", {\n operation: \"cache.invalidate\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n await this.ctx.docCache.invalidate({ collection: this.ctx.collection });\n }\n\n private async invalidateItemImpl(slug: string): Promise<void> {\n this.ctx.logger?.debug?.(\"アイテムキャッシュを無効化\", {\n operation: \"cache.invalidateItem\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n slug,\n });\n await this.ctx.docCache.invalidate({\n collection: this.ctx.collection,\n slug,\n });\n }\n\n private async warmImpl(opts?: WarmOptions): Promise<WarmResult> {\n const items = await this.fetchListRaw();\n const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;\n let ok = 0;\n const failed: Array<{ slug: string; error: unknown }> = [];\n\n for (let i = 0; i < items.length; i += concurrency) {\n const chunk = items.slice(i, i + concurrency);\n await Promise.all(\n chunk.map(async (item) => {\n try {\n await this.primeItem(item);\n ok++;\n } catch (err) {\n failed.push({ slug: item.slug, error: err });\n this.ctx.logger?.warn?.(\"warm: アイテムの事前レンダリングに失敗\", {\n slug: item.slug,\n pageId: item.id,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }),\n );\n opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);\n }\n\n await this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt: Date.now(),\n });\n return { ok, failed };\n }\n\n private async primeImpl(slug: string): Promise<void> {\n const item = await this.fetchRaw(slug);\n if (!item) return;\n await this.primeItem(item);\n }\n\n /** 取得済みアイテムからメタ・本文キャッシュを作り直す(warm / prime / warmByPageId 共通)。 */\n private async primeItem(item: T): Promise<void> {\n await this.persistMeta(item.slug, item);\n const content = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(this.ctx.collection, item.slug, content);\n }\n\n /** リストキャッシュを最新の取得結果で作り直す。 */\n private async refreshList(): Promise<void> {\n const items = await this.fetchListRaw();\n await this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt: Date.now(),\n });\n }\n\n /** Notion page id からアクセス可能なアイテムを解決する。`findById` 優先、無ければ list を走査。 */\n private async resolveByPageId(pageId: string): Promise<T | null> {\n const target = normalizePageId(pageId);\n const findById = this.ctx.source.findById?.bind(this.ctx.source);\n let item: T | null;\n if (findById) {\n item = await withRetry(() => findById(pageId), {\n ...this.ctx.retryConfig,\n onRetry: (attempt, status, delayMs) => {\n this.ctx.logger?.warn?.(\"findById() リトライ中\", {\n attempt,\n status,\n pageId,\n backoffMs: delayMs,\n });\n },\n });\n } else {\n const all = await this.fetchListRaw();\n item = all.find((i) => normalizePageId(i.id) === target) ?? null;\n }\n\n if (!item) return null;\n if (item.isArchived || item.isInTrash) return null;\n if (\n this.ctx.accessibleStatuses.length > 0 &&\n (!item.status || !this.ctx.accessibleStatuses.includes(item.status))\n ) {\n return null;\n }\n return item;\n }\n\n private async persistMeta(\n slug: string,\n item: T,\n opts: { background?: boolean } = {},\n ): Promise<CachedItemMeta<T>> {\n let meta = buildCachedItemMeta(item, this.ctx.source);\n if (this.ctx.hooks.beforeCacheMeta) {\n meta = await this.ctx.hooks.beforeCacheMeta(meta);\n }\n const save = this.ctx.docCache.setMeta(this.ctx.collection, slug, meta);\n if (opts.background && this.ctx.waitUntil) {\n this.ctx.waitUntil(save);\n } else {\n await save;\n }\n return meta;\n }\n\n private async invalidateContentEntry(slug: string): Promise<void> {\n await this.ctx.docCache.invalidate({\n collection: this.ctx.collection,\n slug,\n kind: \"content\",\n });\n }\n\n /** 本文キャッシュ。メタとの整合 (`notionUpdatedAt`) が崩れていれば再生成して書き戻す。 */\n private async loadOrBuildContent(\n slug: string,\n item: T,\n ): Promise<CachedItemContent> {\n const expected = this.ctx.source.getLastModified(item);\n const cached = await this.ctx.docCache.getContent(\n this.ctx.collection,\n slug,\n );\n if (cached && cached.notionUpdatedAt === expected) {\n return cached;\n }\n\n const fresh = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);\n this.ctx.hooks.onContentRevalidated?.(slug, fresh);\n return fresh;\n }\n\n /** メタ既知の状態で本文だけ再生成する。エラーは onSwrError フックに通知して握り潰す。 */\n private async rebuildContentBg(slug: string, item: T): Promise<void> {\n try {\n const fresh = await buildCachedItemContent(item, this.ctx.render);\n await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);\n this.ctx.hooks.onContentRevalidated?.(slug, fresh);\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/content_rebuild_failed\",\n message: \"SWR background content rebuild failed.\",\n cause: err,\n context: {\n operation: \"swr.rebuildContentBg\",\n collection: this.ctx.collection,\n slug,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"item-content\", slug });\n this.ctx.logger?.warn?.(\"本文のバックグラウンド再生成に失敗\", {\n slug,\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private attachLazyContent(meta: CachedItemMeta<T>): ItemWithContent<T> {\n const slug = meta.item.slug;\n const item = meta.item;\n // html() / markdown() / blocks() を同じアイテムから複数回呼んでも I/O は 1 回に集約する\n let payloadPromise: Promise<CachedItemContent> | undefined;\n const loadPayload = (): Promise<CachedItemContent> => {\n if (!payloadPromise) {\n payloadPromise = this.loadOrBuildContent(slug, item);\n }\n return payloadPromise;\n };\n\n return Object.assign(Object.create(null) as object, item, {\n html: async () => (await loadPayload()).html,\n markdown: async () => (await loadPayload()).markdown,\n blocks: async () => (await loadPayload()).blocks,\n notionBlocks: async () => {\n const notionBlocks = (await loadPayload()).notionBlocks;\n // markdown 戦略やフェッチャ未設定だと常に undefined になり、\n // React レンダリング側で原因が分からない無言失敗になるため一度だけ案内する。\n if (notionBlocks === undefined) this.warnMissingNotionBlocks();\n return notionBlocks;\n },\n }) as ItemWithContent<T>;\n }\n\n private notionBlocksWarned = false;\n\n private warnMissingNotionBlocks(): void {\n if (this.notionBlocksWarned) return;\n this.notionBlocksWarned = true;\n this.ctx.logger?.warn?.(\n \"notionBlocks() が undefined を返しました。BlockObjectResponse ツリーは blocks 戦略でのみ得られます (notionSource の fetch 未指定の既定でも有効)。markdownFetcher を使用中の場合は markdown→React の Renderer を使うか、blocksFetcher() に切り替えてください。\",\n { collection: this.ctx.collection, operation: \"notionBlocks\" },\n );\n }\n\n private async fetchList(): Promise<T[]> {\n const cached = await this.ctx.docCache.getList<T>(this.ctx.collection);\n if (cached) {\n if (\n this.ctx.ttlMs !== undefined &&\n isStale(cached.cachedAt, this.ctx.ttlMs)\n ) {\n this.ctx.logger?.debug?.(\"リストキャッシュ期限切れ(TTL)、フェッチ\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheMiss?.();\n const items = await this.fetchListRaw();\n await this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt: Date.now(),\n });\n return items;\n }\n const bg = this.checkAndUpdateListBg(cached);\n if (this.ctx.waitUntil) this.ctx.waitUntil(bg);\n this.ctx.logger?.debug?.(\"リストキャッシュヒット\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheHit?.(cached);\n return cached.items;\n }\n\n this.ctx.logger?.debug?.(\"リストキャッシュミス、フェッチ\", {\n operation: \"list\",\n collection: this.ctx.collection,\n cacheAdapter: this.ctx.docCacheName,\n });\n this.ctx.hooks.onListCacheMiss?.();\n const items = await this.fetchListRaw();\n const cachedAt = Date.now();\n const save = this.ctx.docCache.setList(this.ctx.collection, {\n items,\n cachedAt,\n });\n if (this.ctx.waitUntil) {\n this.ctx.waitUntil(save);\n } else {\n await save;\n }\n return items;\n }\n\n private async checkAndUpdateItemBg(\n slug: string,\n cached: CachedItemMeta<T>,\n ): Promise<void> {\n try {\n const item = await this.fetchRaw(slug);\n if (!item) return;\n const lm = this.ctx.source.getLastModified(item);\n if (lm !== cached.notionUpdatedAt) {\n const meta = await this.persistMeta(slug, item);\n await this.invalidateContentEntry(slug);\n this.ctx.logger?.debug?.(\"SWR: 差分を検出、メタを差し替え\", {\n operation: \"find:bg\",\n slug,\n collection: this.ctx.collection,\n notionUpdatedAt: cached.notionUpdatedAt,\n });\n this.ctx.hooks.onCacheRevalidated?.(slug, meta);\n await this.rebuildContentBg(slug, item);\n } else {\n await this.ctx.docCache.setMeta(this.ctx.collection, slug, {\n ...cached,\n cachedAt: Date.now(),\n });\n this.ctx.logger?.debug?.(\"SWR: 差分なし、cachedAt を更新\", {\n operation: \"find:bg\",\n slug,\n collection: this.ctx.collection,\n });\n }\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/item_check_failed\",\n message: \"SWR background item check failed.\",\n cause: err,\n context: {\n operation: \"swr.checkAndUpdateItemBg\",\n collection: this.ctx.collection,\n slug,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"item-meta\", slug });\n this.ctx.logger?.warn?.(\n \"SWR: アイテムのバックグラウンド差分チェックに失敗\",\n {\n slug,\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n }\n }\n\n private async checkAndUpdateListBg(cached: CachedItemList<T>): Promise<void> {\n try {\n const items = await this.fetchListRaw();\n if (\n this.ctx.source.getListVersion(items) !==\n this.ctx.source.getListVersion(cached.items)\n ) {\n const listEntry = { items, cachedAt: Date.now() };\n await this.ctx.docCache.setList(this.ctx.collection, listEntry);\n this.ctx.logger?.debug?.(\n \"SWR: リスト差分を検出、キャッシュを差し替え\",\n {\n operation: \"list:bg\",\n collection: this.ctx.collection,\n },\n );\n this.ctx.hooks.onListCacheRevalidated?.(listEntry);\n } else if (this.ctx.ttlMs !== undefined) {\n await this.ctx.docCache.setList(this.ctx.collection, {\n ...cached,\n cachedAt: Date.now(),\n });\n this.ctx.logger?.debug?.(\"SWR: リスト差分なし、TTL をリセット\", {\n operation: \"list:bg\",\n collection: this.ctx.collection,\n });\n }\n } catch (err) {\n const cmsErr = isCMSError(err)\n ? err\n : new CMSError({\n code: \"swr/list_check_failed\",\n message: \"SWR background list check failed.\",\n cause: err,\n context: {\n operation: \"swr.checkAndUpdateListBg\",\n collection: this.ctx.collection,\n },\n });\n this.ctx.hooks.onSwrError?.(cmsErr, { phase: \"list\" });\n this.ctx.logger?.warn?.(\n \"SWR: リストのバックグラウンド差分チェックに失敗\",\n {\n collection: this.ctx.collection,\n code: cmsErr.code,\n error: err instanceof Error ? err.message : String(err),\n },\n );\n }\n }\n\n private async fetchListRaw(): Promise<T[]> {\n const items = await withRetry(\n () =>\n this.ctx.source.list({\n publishedStatuses:\n this.ctx.publishedStatuses.length > 0\n ? this.ctx.publishedStatuses\n : undefined,\n }),\n {\n ...this.ctx.retryConfig,\n onRetry: (attempt, status, delayMs) => {\n this.ctx.logger?.warn?.(\"list() リトライ中\", {\n attempt,\n status,\n backoffMs: delayMs,\n });\n },\n },\n );\n return items.filter((item) => {\n if (item.isArchived || item.isInTrash) return false;\n if (\n this.ctx.accessibleStatuses.length > 0 &&\n (!item.status || !this.ctx.accessibleStatuses.includes(item.status))\n )\n return false;\n return true;\n });\n }\n\n private async fetchRaw(slug: string): Promise<T | null> {\n const retryOpts = {\n ...this.ctx.retryConfig,\n onRetry: (attempt: number, status: number, delayMs?: number) => {\n this.ctx.logger?.warn?.(\"find() リトライ中\", {\n attempt,\n status,\n slug,\n backoffMs: delayMs,\n });\n },\n };\n\n // PropertyMap が解決できる場合は単一プロパティ filter で 1 ページだけ取得する (高速)\n const notionPropName =\n this.ctx.source.properties?.[this.ctx.slugField]?.notion;\n\n let item: T | null;\n const findByProp = this.ctx.source.findByProp?.bind(this.ctx.source);\n if (notionPropName && findByProp) {\n item = await withRetry(() => findByProp(notionPropName, slug), retryOpts);\n } else {\n const all = await withRetry(() => this.ctx.source.list(), retryOpts);\n item = all.find((i) => i.slug === slug) ?? null;\n }\n\n if (!item) return null;\n if (item.isArchived || item.isInTrash) return null;\n if (\n this.ctx.accessibleStatuses.length > 0 &&\n (!item.status || !this.ctx.accessibleStatuses.includes(item.status))\n ) {\n return null;\n }\n return item;\n }\n}\n\nfunction matchesWhere<T extends BaseContentItem>(\n item: T,\n where: WhereClause<T>,\n): boolean {\n for (const key of Object.keys(where) as (keyof T & string)[]) {\n const expected = where[key];\n const actual = item[key];\n if (Array.isArray(expected)) {\n if (!(expected as readonly unknown[]).includes(actual)) return false;\n } else {\n if (actual !== expected) return false;\n }\n }\n return true;\n}\n\nfunction applyListOptions<T extends BaseContentItem>(\n items: T[],\n opts?: ListOptions<T>,\n): T[] {\n if (!opts) return sortByPublishedAtDesc(items);\n let result = items;\n\n if (opts.statuses) {\n const allow = new Set(\n Array.isArray(opts.statuses) ? opts.statuses : [opts.statuses],\n );\n result = result.filter((it) => it.status != null && allow.has(it.status));\n }\n\n if (opts.tag) {\n const tag = opts.tag;\n result = result.filter((it) => {\n const tags = (it as { tags?: string[] }).tags;\n return Array.isArray(tags) && tags.includes(tag);\n });\n }\n\n if (opts.where) {\n const where = opts.where;\n result = result.filter((it) => matchesWhere(it, where));\n }\n\n if (opts.filter) {\n result = result.filter(opts.filter);\n }\n\n if (opts.sort) {\n result = [...result].sort(makeComparator(opts.sort));\n } else {\n result = sortByPublishedAtDesc(result);\n }\n\n const skip = opts.skip ?? 0;\n const limit = opts.limit;\n if (skip > 0 || limit !== undefined) {\n result = result.slice(skip, limit !== undefined ? skip + limit : undefined);\n }\n\n return result;\n}\n\n/** publishedAt 降順 (未設定なら lastEditedTime 降順) でソートする。 */\nfunction sortByPublishedAtDesc<T extends BaseContentItem>(items: T[]): T[] {\n return [...items].sort((a, b) => {\n const av = a.publishedAt ?? a.lastEditedTime;\n const bv = b.publishedAt ?? b.lastEditedTime;\n if (av === bv) return 0;\n return av > bv ? -1 : 1;\n });\n}\n\nfunction makeComparator<T extends BaseContentItem>(\n sort: SortOption<T>,\n): (a: T, b: T) => number {\n if (sort.compare) return sort.compare;\n const by = sort.by as keyof T;\n const dir = sort.dir === \"asc\" ? 1 : -1;\n return (a, b) => {\n const av = a[by];\n const bv = b[by];\n if (av === bv) return 0;\n if (av === undefined || av === null) return 1;\n if (bv === undefined || bv === null) return -1;\n if (typeof av === \"string\" && typeof bv === \"string\") {\n return av > bv ? dir : -dir;\n }\n if (typeof av === \"number\" && typeof bv === \"number\") {\n return av > bv ? dir : -dir;\n }\n throw new CMSError({\n code: \"core/sort_unsupported_type\",\n message: `\"${String(by)}\" フィールドの型 \"${typeof av}\" はソート非対応です。compare 関数を指定してください。`,\n context: {\n operation: \"makeComparator\",\n field: String(by),\n type: typeof av,\n },\n });\n };\n}\n","import { isCMSError } from \"./errors\";\nimport type { ImageCacheOps, InvalidateScope } from \"./types/index\";\n\nexport interface HandlerOptions {\n /** マウントするベースパス。デフォルト `/api/cms`。 */\n basePath?: string;\n /** 画像プロキシのパス (basePath 相対)。デフォルト `/images/:hash`。 */\n imagesPath?: string;\n /** revalidate webhook のパス (basePath 相対)。デフォルト `/revalidate`。 */\n revalidatePath?: string;\n /** バージョン照会 (peekVersion) のパス (basePath 相対)。デフォルト `/versions`。 */\n versionsPath?: string;\n /** 更新チェック (check) のパス (basePath 相対)。デフォルト `/check`。 */\n checkPath?: string;\n /** Notion 公式 webhook 受信のパス (basePath 相対)。デフォルト `/notion-webhook`。 */\n notionWebhookPath?: string;\n /** Webhook 署名検証用シークレット (未指定なら検証スキップ)。 */\n webhookSecret?: string;\n /** Notion 公式 webhook(integration の Webhooks)の受信設定。 */\n notionWebhook?: {\n /**\n * 検証トークン(HMAC-SHA256 署名キー)。未指定時は createCMS の\n * `notion.webhookSecret`(= `CreateClientOptions.notionWebhookSecret`)を既定で使う。\n */\n secret?: string;\n /**\n * サブスク登録時に Notion が送る `verification_token` を受け取るコールバック。\n * 値を控えて `notion.webhookSecret` に設定する用途(既定ではレスポンス本文にも echo する)。\n */\n onVerificationToken?: (token: string) => void;\n };\n /** デフォルト実装を無効化する場合 true。 */\n disabled?: boolean;\n}\n\nexport interface HandlerAdapter {\n imageCache: ImageCacheOps;\n /**\n * 指定コレクションの DataSource.parseWebhook を呼ぶ。\n * 未知コレクション → `handler/unknown_collection` CMSError\n * parseWebhook 未実装 → `webhook/not_implemented` CMSError\n */\n parseWebhookFor(\n collection: string,\n req: Request,\n webhookSecret: string | undefined,\n ): Promise<InvalidateScope>;\n revalidate(scope: InvalidateScope): Promise<void>;\n /**\n * 指定コレクション / slug の `peekVersion`(KV メタのみ、Notion API 非呼び出し)を返す。\n * キャッシュ未登録なら `null`。未知コレクション → `handler/unknown_collection` CMSError。\n */\n peekVersionFor(\n collection: string,\n slug: string,\n ): Promise<{ notionUpdatedAt: string; cachedAt: number } | null>;\n /**\n * 指定コレクション / slug を `currentVersion` と比較し(Notion を実照会)、\n * 差分があればキャッシュを更新して `stale` を返す。\n * アイテムが存在しない場合は `null`。未知コレクション → `handler/unknown_collection` CMSError。\n */\n checkFor(\n collection: string,\n slug: string,\n currentVersion: string,\n ): Promise<{ stale: boolean } | null>;\n /**\n * Notion ページ ID を全コレクション横断で解決し単件ウォームする(公式 webhook 用)。\n * 一致したコレクション / slug、無ければ `null`。\n */\n warmByPageId(\n pageId: string,\n ): Promise<{ collection: string; slug: string } | null>;\n /** createCMS で設定された Notion webhook 検証トークンの既定値。 */\n notionWebhookSecret?: string;\n /** 応答送信後もウォームを完走させる実行フック (Cloudflare の `waitUntil` 相当)。 */\n scheduleBackground?: (p: Promise<unknown>) => void;\n}\n\nconst DEFAULT_OPTS = {\n basePath: \"/api/cms\",\n imagesPath: \"/images\",\n revalidatePath: \"/revalidate\",\n versionsPath: \"/versions\",\n checkPath: \"/check\",\n notionWebhookPath: \"/notion-webhook\",\n} as const;\n\nconst JSON_HEADERS = { \"content-type\": \"application/json\" } as const;\n\n/** HMAC-SHA256 を hex で返す。core はゼロ依存のため import せずグローバル `crypto.subtle` を使う。 */\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const enc = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", key, enc.encode(message));\n let hex = \"\";\n for (const b of new Uint8Array(sig)) hex += b.toString(16).padStart(2, \"0\");\n return hex;\n}\n\n/** タイミング攻撃を避ける定数時間文字列比較。 */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });\n}\n\nfunction httpStatusForError(code: string): number | null {\n if (code === \"webhook/signature_invalid\") return 401;\n if (code === \"webhook/not_implemented\") return 501;\n if (code === \"webhook/unknown_collection\") return 404;\n if (code === \"webhook/payload_invalid\") return 400;\n if (code === \"handler/unknown_collection\") return 404;\n return null;\n}\n\nfunction errorResponse(err: unknown): Response | null {\n if (!isCMSError(err)) return null;\n const status = httpStatusForError(err.code);\n if (status === null) return null;\n return new Response(JSON.stringify({ ok: false, code: err.code }), {\n status,\n headers: JSON_HEADERS,\n });\n}\n\nfunction splitCollectionSlug(\n sub: string,\n): { collection: string; slug: string } | null {\n const slashIndex = sub.indexOf(\"/\");\n if (slashIndex <= 0 || slashIndex === sub.length - 1) return null;\n return {\n collection: sub.slice(0, slashIndex),\n slug: sub.slice(slashIndex + 1),\n };\n}\n\n/**\n * Web Standard な Request → Response ルーター。\n * Next.js / React Router / Hono / Cloudflare Workers いずれでも使える。\n *\n * ルート:\n * - GET `{basePath}/images/:hash` — 画像プロキシ\n * - GET `{basePath}/versions/:collection/:slug` — peekVersion(更新検知ポーリング)\n * - GET/POST `{basePath}/check/:collection/:slug?v=` — check(更新を実照会してキャッシュ更新)\n * - POST `{basePath}/revalidate/:collection` — Webhook 受信 + $revalidate()\n */\nexport function createHandler(\n adapter: HandlerAdapter,\n opts: HandlerOptions = {},\n): (req: Request) => Promise<Response> {\n const basePath = trimTrailingSlash(opts.basePath ?? DEFAULT_OPTS.basePath);\n const imagesPath = opts.imagesPath ?? DEFAULT_OPTS.imagesPath;\n const revalidatePath = opts.revalidatePath ?? DEFAULT_OPTS.revalidatePath;\n const versionsPath = opts.versionsPath ?? DEFAULT_OPTS.versionsPath;\n const checkPath = opts.checkPath ?? DEFAULT_OPTS.checkPath;\n const notionWebhookPath =\n opts.notionWebhookPath ?? DEFAULT_OPTS.notionWebhookPath;\n\n return async (req: Request): Promise<Response> => {\n const url = new URL(req.url);\n const path = url.pathname;\n\n if (!path.startsWith(basePath)) {\n return new Response(\"Not Found\", { status: 404 });\n }\n const rel = path.slice(basePath.length) || \"/\";\n\n if (req.method === \"GET\" && rel.startsWith(`${imagesPath}/`)) {\n const hash = rel.slice(imagesPath.length + 1);\n if (!hash) return new Response(\"Bad Request\", { status: 400 });\n const object = await adapter.imageCache.get(hash);\n if (!object) return new Response(\"Not Found\", { status: 404 });\n const headers = new Headers();\n if (object.contentType) headers.set(\"content-type\", object.contentType);\n headers.set(\"cache-control\", \"public, max-age=31536000, immutable\");\n return new Response(object.data, { headers });\n }\n\n if (req.method === \"GET\" && rel.startsWith(`${versionsPath}/`)) {\n const target = splitCollectionSlug(rel.slice(versionsPath.length + 1));\n if (!target) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"collection and slug required\" }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const version = await adapter.peekVersionFor(\n target.collection,\n target.slug,\n );\n // 値が無い場合も 200 + null を返す(ポーリング側は null を「未確定」として継続する)。\n return new Response(JSON.stringify(version), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n if (\n (req.method === \"GET\" || req.method === \"POST\") &&\n rel.startsWith(`${checkPath}/`)\n ) {\n const target = splitCollectionSlug(rel.slice(checkPath.length + 1));\n const currentVersion = url.searchParams.get(\"v\");\n if (!target || !currentVersion) {\n return new Response(\n JSON.stringify({\n ok: false,\n reason: \"collection, slug and ?v= are required\",\n }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const result = await adapter.checkFor(\n target.collection,\n target.slug,\n currentVersion,\n );\n if (result === null) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"not found\" }),\n {\n status: 404,\n headers: JSON_HEADERS,\n },\n );\n }\n return new Response(JSON.stringify({ stale: result.stale }), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n if (req.method === \"POST\" && rel === notionWebhookPath) {\n const raw = await req.text();\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n return jsonResponse({ ok: false, reason: \"invalid json\" }, 400);\n }\n\n // サブスク登録時の検証: verification_token を控えられるよう echo + コールバック。\n if (\n payload &&\n typeof payload === \"object\" &&\n \"verification_token\" in payload\n ) {\n const token = String(\n (payload as Record<string, unknown>).verification_token,\n );\n opts.notionWebhook?.onVerificationToken?.(token);\n return jsonResponse({ ok: true, verification_token: token }, 200);\n }\n\n const secret = opts.notionWebhook?.secret ?? adapter.notionWebhookSecret;\n if (!secret) {\n return jsonResponse(\n { ok: false, reason: \"notion webhook secret not configured\" },\n 503,\n );\n }\n const signature = req.headers.get(\"X-Notion-Signature\") ?? \"\";\n const expected = `sha256=${await hmacSha256Hex(secret, raw)}`;\n if (!timingSafeEqual(signature, expected)) {\n return jsonResponse(\n { ok: false, code: \"webhook/signature_invalid\" },\n 401,\n );\n }\n\n const entity = (payload as { entity?: { id?: string; type?: string } })\n .entity;\n const pageId = entity?.type === \"page\" ? entity.id : undefined;\n if (!pageId) {\n return jsonResponse({ ok: true, skipped: \"no page entity\" }, 200);\n }\n\n // 応答は即返し、ウォームは可能なら waitUntil でバックグラウンド完走させる。\n if (adapter.scheduleBackground) {\n adapter.scheduleBackground(adapter.warmByPageId(pageId));\n return jsonResponse({ ok: true, pageId }, 200);\n }\n const result = await adapter.warmByPageId(pageId);\n return jsonResponse({ ok: true, pageId, result }, 200);\n }\n\n if (req.method === \"POST\" && rel.startsWith(`${revalidatePath}/`)) {\n const collection = rel.slice(revalidatePath.length + 1);\n if (!collection || collection.includes(\"/\")) {\n return new Response(\n JSON.stringify({ ok: false, reason: \"collection required\" }),\n { status: 400, headers: JSON_HEADERS },\n );\n }\n try {\n const scope = await adapter.parseWebhookFor(\n collection,\n req,\n opts.webhookSecret,\n );\n await adapter.revalidate(scope);\n return new Response(JSON.stringify({ ok: true, scope }), {\n status: 200,\n headers: JSON_HEADERS,\n });\n } catch (err) {\n const res = errorResponse(err);\n if (res) return res;\n throw err;\n }\n }\n\n return new Response(\"Not Found\", { status: 404 });\n };\n}\n\nfunction trimTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n","import { noopDocOps, noopImgOps } from \"./cache/noop\";\nimport { CollectionClientImpl, type CollectionContext } from \"./collection\";\nimport { CMSError } from \"./errors\";\nimport { createHandler, type HandlerOptions } from \"./handler\";\nimport { mergeHooks, mergeLoggers, withTraceId } from \"./hooks\";\nimport { buildCacheImageFn } from \"./image\";\nimport type { RenderContext } from \"./rendering\";\nimport type { RetryConfig } from \"./retry\";\nimport { DEFAULT_RETRY_CONFIG } from \"./retry\";\nimport type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CMSHooks,\n CollectionClient,\n CollectionsConfig,\n CreateClientOptions,\n DataSource,\n DocumentCacheOps,\n ImageCacheOps,\n InferCollectionItem,\n InvalidateScope,\n Logger,\n LogLevel,\n RendererFn,\n StorageBinary,\n} from \"./types/index\";\nimport type {\n CMSAdapter,\n CMSSources,\n MergeSourceCollections,\n} from \"./types/sources\";\n\nconst DEFAULT_IMAGE_PROXY_BASE = \"/api/images\";\n\nexport type CMSClient<C extends CollectionsConfig> = {\n [K in keyof C]: CollectionClient<InferCollectionItem<C[K]>>;\n} & CMSGlobalOps;\n\n/**\n * `cms.stats()` が返す集約済みキャッシュ統計。\n * 各 adapter の `stats()` 戻り値をそのまま配列で保持しつつ、ヒット率を算出する。\n */\nexport interface CMSStats {\n /** クライアント単位の trace ID (`createClient` で発行)。 */\n traceId: string;\n /** ドキュメントキャッシュの集計 (`handles: [\"document\"]` の adapter の `stats()` 戻り値)。 */\n document?: {\n adapter: string;\n hits: number;\n misses: number;\n entries?: number;\n sizeBytes?: number;\n /** 0〜1。`hits + misses === 0` のときは 0。 */\n hitRate: number;\n };\n /** 画像キャッシュの集計 (`handles: [\"image\"]` の adapter の `stats()` 戻り値)。 */\n image?: {\n adapter: string;\n hits: number;\n misses: number;\n entries?: number;\n sizeBytes?: number;\n hitRate: number;\n };\n}\n\nexport interface CMSGlobalOps {\n readonly collections: readonly string[];\n /** クライアント単位の trace ID (`createClient` で発行)。 */\n readonly traceId: string;\n invalidate(scope?: InvalidateScope): Promise<void>;\n /**\n * Notion ページ ID を全コレクション横断で解決し、該当アイテムを単件ウォームする。\n * 公式 webhook の sparse payload(page id のみ)からミラーを再生成するために使う。\n * 一致したコレクションと slug を返す。どのコレクションにも属さなければ `null`。\n */\n warmByPageId(\n pageId: string,\n ): Promise<{ collection: string; slug: string } | null>;\n /** Web Standard な Request/Response ベースのルートハンドラ (画像プロキシ + webhook)。 */\n handler(opts?: HandlerOptions): (req: Request) => Promise<Response>;\n getCachedImage(hash: string): Promise<StorageBinary | null>;\n /**\n * Notion 画像 URL を `{imageProxyBase}/{sha256}` 形式へ変換しキャッシュへ書き込む。\n * 画像キャッシュが未設定 (noop) の場合は `undefined`。\n */\n readonly cacheImage: ((url: string) => Promise<string>) | undefined;\n readonly imageProxyBase: string;\n /**\n * ドキュメント / 画像キャッシュのヒット・ミス・サイズを集約して返す。\n * 各キャッシュアダプタの `stats()` を呼ぶだけで副作用はない。\n * `stats()` を実装していない adapter は集計から除外される (noop など)。\n */\n stats(): Promise<CMSStats>;\n}\n\ninterface ResolvedCache {\n doc: DocumentCacheOps;\n docName: string;\n docAdapter: CacheAdapter | undefined;\n img: ImageCacheOps;\n imgName: string;\n imgAdapter: CacheAdapter | undefined;\n hasImg: boolean;\n}\n\n/**\n * adapter の `handles` を見て先勝ちで document / image を割り当てる。未指定は両方 noop。\n * `cms.stats()` から元 adapter の `stats()` を呼びたいので、解決元 adapter も保持する。\n */\nfunction resolveCache(\n cache: readonly CacheAdapter[] | undefined,\n): ResolvedCache {\n const adapters = cache ?? [];\n\n let doc: DocumentCacheOps = noopDocOps;\n let docName = \"noop-document\";\n let docAdapter: CacheAdapter | undefined;\n let img: ImageCacheOps = noopImgOps;\n let imgName = \"noop-image\";\n let imgAdapter: CacheAdapter | undefined;\n let docFound = false;\n let imgFound = false;\n\n for (const adapter of adapters) {\n if (!docFound && adapter.handles.includes(\"document\") && adapter.doc) {\n doc = adapter.doc;\n docName = adapter.name;\n docAdapter = adapter;\n docFound = true;\n }\n if (!imgFound && adapter.handles.includes(\"image\") && adapter.img) {\n img = adapter.img;\n imgName = adapter.name;\n imgAdapter = adapter;\n imgFound = true;\n }\n }\n\n return {\n doc,\n docName,\n docAdapter,\n img,\n imgName,\n imgAdapter,\n hasImg: imgFound,\n };\n}\n\n/**\n * 衝突しにくい短い trace ID を生成する。`{epoch36}-{rand36}` の 10〜12 文字程度。\n * core はゼロ依存ルールに従い node:crypto を静的 import しないため、Math.random ベースで十分。\n */\nfunction generateTraceId(): string {\n const t = Date.now().toString(36);\n const r = Math.floor(Math.random() * 36 ** 6)\n .toString(36)\n .padStart(6, \"0\");\n return `${t}-${r}`;\n}\n\nconst LOG_LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nfunction applyLogLevel(\n logger: Logger | undefined,\n minLevel: LogLevel,\n): Logger | undefined {\n if (!logger) return undefined;\n const minOrder = LOG_LEVEL_ORDER[minLevel];\n const filtered: Logger = {};\n for (const level of [\"debug\", \"info\", \"warn\", \"error\"] as const) {\n if (LOG_LEVEL_ORDER[level] >= minOrder) {\n filtered[level] = logger[level];\n }\n }\n return filtered;\n}\n\n/**\n * CMS クライアントを生成する。\n *\n * @example\n * import { createClient, nodePreset } from \"@notion-headless-cms/core\";\n * import { notionSource } from \"@notion-headless-cms/notion-source\";\n * import { schema } from \"./generated/nhc.schema\";\n *\n * const cms = createClient({\n * sources: { notion: notionSource({ schema, token: process.env.NOTION_TOKEN! }) },\n * ...nodePreset(),\n * });\n *\n * const posts = await cms.posts.list();\n */\nexport function createClient<S extends CMSSources = CMSSources>(\n opts: CreateClientOptions<S>,\n): CMSClient<\n MergeSourceCollections<S> extends CollectionsConfig\n ? MergeSourceCollections<S>\n : CollectionsConfig\n> {\n const collectionsInput: CollectionsConfig = {};\n if (opts.sources) {\n for (const adapter of Object.values(\n opts.sources as unknown as Record<string, CMSAdapter | undefined>,\n )) {\n if (adapter) Object.assign(collectionsInput, adapter.collections);\n }\n }\n\n if (Object.keys(collectionsInput).length === 0) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message:\n \"createClient: sources に少なくとも 1 つのコレクションを指定してください。\",\n context: { operation: \"createClient\" },\n nextSteps: [\n \"notionSource({ schema, token }) を sources.notion に渡す\",\n \"`nhc generate` でスキーマを生成してから import する\",\n ],\n docsUrl:\n \"https://github.com/kjfsm/notion-headless-cms/blob/main/docs/quickstart.md\",\n });\n }\n\n for (const [name, def] of Object.entries(collectionsInput)) {\n if (!def.source) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message: `createClient: コレクション \"${name}\" の source は必須です。`,\n context: { operation: \"createClient\", collection: name },\n nextSteps: [\"notionSource(...) を sources に渡しているか確認する\"],\n });\n }\n if (!def.slugField) {\n throw new CMSError({\n code: \"core/config_invalid\",\n message: `createClient: コレクション \"${name}\" の slugField は必須です。`,\n context: { operation: \"createClient\", collection: name },\n nextSteps: [\n `nhc.config.ts の ${name} コレクションに slugField を設定する`,\n ],\n });\n }\n }\n\n const cacheRes = resolveCache(opts.cache);\n const ttlMs = opts.swr?.ttlMs;\n const imageProxyBase = opts.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;\n const contentConfig = opts.content;\n const rendererFn: RendererFn | undefined = opts.renderer;\n const waitUntil = opts.waitUntil;\n const baseLogger: Logger | undefined = mergeLoggers(\n opts.plugins ?? [],\n opts.logger,\n );\n const traceId = generateTraceId();\n const tracedLogger = withTraceId(baseLogger, traceId);\n const logger = opts.logLevel\n ? applyLogLevel(tracedLogger, opts.logLevel)\n : tracedLogger;\n const hooks: CMSHooks<BaseContentItem> = mergeHooks(\n opts.plugins ?? [],\n opts.hooks,\n logger,\n );\n const maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;\n const retryConfig: RetryConfig = {\n ...DEFAULT_RETRY_CONFIG,\n ...(opts.rateLimiter ?? {}),\n };\n\n const collectionNames: string[] = [];\n const collections: Record<string, CollectionClient<BaseContentItem>> = {};\n // warmByPageId は CollectionClient I/F に無い impl 専用メソッドを呼ぶため、具象も保持する。\n const collectionImpls: Record<\n string,\n CollectionClientImpl<BaseContentItem>\n > = {};\n for (const [name, def] of Object.entries(collectionsInput)) {\n collectionNames.push(name);\n const source = def.source as DataSource<BaseContentItem>;\n const colHooks = def.hooks as CMSHooks<BaseContentItem> | undefined;\n const collectionHooks: CMSHooks<BaseContentItem> = colHooks\n ? mergeHooks([{ name: `${name}:global`, hooks }], colHooks, logger)\n : hooks;\n const renderCtx: RenderContext<BaseContentItem> = {\n source,\n rendererFn,\n imgCache: cacheRes.img,\n imgCacheName: cacheRes.imgName,\n hasImageCache: cacheRes.hasImg,\n imageProxyBase,\n contentConfig,\n hooks: collectionHooks,\n logger,\n };\n const ctx: CollectionContext<BaseContentItem> = {\n collection: name,\n source,\n docCache: cacheRes.doc,\n docCacheName: cacheRes.docName,\n render: renderCtx,\n hooks: collectionHooks,\n logger,\n ttlMs,\n publishedStatuses: def.publishedStatuses\n ? [...def.publishedStatuses]\n : [],\n accessibleStatuses: def.accessibleStatuses\n ? [...def.accessibleStatuses]\n : [],\n retryConfig,\n maxConcurrent,\n waitUntil,\n slugField: def.slugField,\n };\n const impl = new CollectionClientImpl(ctx);\n collections[name] = impl;\n collectionImpls[name] = impl;\n }\n\n const cacheImage = cacheRes.hasImg\n ? buildCacheImageFn(cacheRes.img, cacheRes.imgName, imageProxyBase, logger)\n : undefined;\n\n const globalOps: CMSGlobalOps = {\n collections: collectionNames,\n cacheImage,\n imageProxyBase,\n traceId,\n async stats(): Promise<CMSStats> {\n const stats: CMSStats = { traceId };\n // doc / img それぞれ、resolveCache が選んだ adapter にだけ stats() を要求する。\n // 同じ adapter が両方を担当している場合 (memoryCache など) は 1 回呼ぶだけで足りる。\n const adapterCache = new Map<CacheAdapter, Promise<CacheAdapterStats>>();\n const ensure = (\n adapter: CacheAdapter | undefined,\n ): Promise<CacheAdapterStats> | undefined => {\n if (!adapter?.stats) return undefined;\n const cached = adapterCache.get(adapter);\n if (cached) return cached;\n const fresh = adapter.stats();\n adapterCache.set(adapter, fresh);\n return fresh;\n };\n const docPromise = ensure(cacheRes.docAdapter);\n const imgPromise = ensure(cacheRes.imgAdapter);\n const computeHitRate = (h: number, m: number): number =>\n h + m === 0 ? 0 : h / (h + m);\n if (docPromise) {\n const docStats = await docPromise;\n if (docStats.doc) {\n stats.document = {\n adapter: docStats.name ?? cacheRes.docName,\n hits: docStats.doc.hits,\n misses: docStats.doc.misses,\n entries: docStats.doc.entries,\n sizeBytes: docStats.doc.sizeBytes,\n hitRate: computeHitRate(docStats.doc.hits, docStats.doc.misses),\n };\n }\n }\n if (imgPromise) {\n const imgStats = await imgPromise;\n if (imgStats.img) {\n stats.image = {\n adapter: imgStats.name ?? cacheRes.imgName,\n hits: imgStats.img.hits,\n misses: imgStats.img.misses,\n entries: imgStats.img.entries,\n sizeBytes: imgStats.img.sizeBytes,\n hitRate: computeHitRate(imgStats.img.hits, imgStats.img.misses),\n };\n }\n }\n return stats;\n },\n async invalidate(scope?: InvalidateScope): Promise<void> {\n logger?.debug?.(\"グローバルキャッシュを無効化\", {\n operation: \"invalidate\",\n cacheAdapter: cacheRes.docName,\n });\n await cacheRes.doc.invalidate(scope ?? \"all\");\n },\n async warmByPageId(pageId) {\n for (const name of collectionNames) {\n const slug = await collectionImpls[name]?.warmByPageId(pageId);\n if (slug) {\n logger?.debug?.(\"warmByPageId: ページを再ウォーム\", {\n operation: \"warmByPageId\",\n collection: name,\n slug,\n pageId,\n });\n return { collection: name, slug };\n }\n }\n return null;\n },\n handler(handlerOpts?: HandlerOptions) {\n return createHandler(\n {\n imageCache: cacheRes.img,\n warmByPageId: (pageId) => globalOps.warmByPageId(pageId),\n notionWebhookSecret: opts.notionWebhookSecret,\n scheduleBackground: waitUntil,\n async parseWebhookFor(collection, req, webhookSecret) {\n const def = collectionsInput[collection];\n if (!def) {\n throw new CMSError({\n code: \"webhook/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"parseWebhookFor\", collection },\n });\n }\n const ds = def.source as DataSource<BaseContentItem>;\n if (!ds.parseWebhook) {\n throw new CMSError({\n code: \"webhook/not_implemented\",\n message: `Collection \"${collection}\" does not support webhooks.`,\n context: { operation: \"parseWebhookFor\", collection },\n });\n }\n return ds.parseWebhook(req, { secret: webhookSecret });\n },\n revalidate: (scope) => globalOps.invalidate(scope),\n peekVersionFor(collection, slug) {\n const client = collections[collection];\n if (!client) {\n throw new CMSError({\n code: \"handler/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"peekVersionFor\", collection, slug },\n });\n }\n return client.peekVersion(slug);\n },\n async checkFor(collection, slug, currentVersion) {\n const client = collections[collection];\n if (!client) {\n throw new CMSError({\n code: \"handler/unknown_collection\",\n message: `Unknown collection: ${collection}`,\n context: { operation: \"checkFor\", collection, slug },\n });\n }\n const result = await client.check(slug, currentVersion);\n // ItemWithContent は lazy 関数を含むため、HTTP には stale 判定のみ返す\n // (差分ありの場合 check() が副作用でキャッシュ更新済み。利用側は loader 再実行で本文取得)。\n return result === null ? null : { stale: result.stale };\n },\n },\n handlerOpts,\n );\n },\n getCachedImage(hash) {\n return cacheRes.img.get(hash);\n },\n };\n\n return Object.assign(\n Object.create(null) as object,\n collections,\n globalOps,\n ) as CMSClient<\n MergeSourceCollections<S> extends CollectionsConfig\n ? MergeSourceCollections<S>\n : CollectionsConfig\n >;\n}\n","import type { CacheAdapter } from \"./cache\";\nimport type { BaseContentItem } from \"./content\";\nimport type { DataSource } from \"./data-source\";\nimport type { CMSHooks } from \"./hooks\";\nimport type { Logger } from \"./logger\";\nimport type { CMSPlugin } from \"./plugin\";\nimport type { CMSSources } from \"./sources\";\n\n/** `Logger` の出力を絞り込むログレベル。指定したレベル未満のログを抑制する。 */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/**\n * renderer プラグインの不透明型。\n * core は unified / remark / rehype に依存せず、このリストをそのまま renderer に渡すだけ。\n */\nexport type RendererPluginList = unknown[];\n\n/**\n * render() オプション。core は renderer の実装を知らず、この型だけを扱う。\n * @notion-headless-cms/markdown-html の renderMarkdown() はこのシグネチャと構造的に互換。\n */\nexport interface RenderOptions {\n imageProxyBase?: string;\n cacheImage?: (url: string) => Promise<string>;\n remarkPlugins?: RendererPluginList;\n rehypePlugins?: RendererPluginList;\n}\n\n/** カスタムレンダラー関数の型。デフォルトは @notion-headless-cms/markdown-html の renderMarkdown。 */\nexport type RendererFn = (\n markdown: string,\n opts?: RenderOptions,\n) => Promise<string>;\n\n/** レンダリング・コンテンツ処理設定。 */\nexport interface ContentConfig {\n /** 追加する remark プラグイン。 */\n remarkPlugins?: RendererPluginList;\n /** 追加する rehype プラグイン。 */\n rehypePlugins?: RendererPluginList;\n}\n\n/** SWR(Stale-While-Revalidate)設定。 */\nexport interface SWRConfig {\n /** SWR の有効期間 (ミリ秒)。未設定時は TTL なし(失効まで stale を返す)。 */\n ttlMs?: number;\n}\n\n/**\n * `RateLimiterConfig` のデフォルト値 (Issue #313 / M2)。\n * 型からだけでは見えない既定値を表面化することで、IDE 補完と\n * preset の対称化 (`nodePreset` / `cloudflarePreset` / `nextPreset`) で\n * 同じ既定が適用されることを保証する。\n */\nexport const DEFAULT_RATE_LIMITER: Required<RateLimiterConfig> = {\n maxConcurrent: 3,\n retryOn: [429, 502, 503],\n maxRetries: 4,\n baseDelayMs: 1000,\n};\n\n/** レートリミット・リトライ設定。既定値は {@link DEFAULT_RATE_LIMITER}。 */\nexport interface RateLimiterConfig {\n /** 同時実行数の上限。デフォルト: 3 */\n maxConcurrent?: number;\n /** リトライ対象の HTTP ステータスコード。デフォルト: [429, 502, 503] */\n retryOn?: number[];\n /** 最大リトライ回数。デフォルト: 4 */\n maxRetries?: number;\n /** リトライ時の基準待機時間(ミリ秒)。デフォルト: 1000 */\n baseDelayMs?: number;\n}\n\n/**\n * コレクション 1 件の定義。CLI が生成する `nhc.ts` から `createClient` に渡される。\n *\n * `source` は notion-orm 等の DataSource 実装。\n * `slugField` / `statusField` は TS フィールド名 (DataSource の `properties` キーと一致)。\n */\nexport interface CollectionDef<T extends BaseContentItem = BaseContentItem> {\n /** Notion etc. のデータソース実装。 */\n source: DataSource<T>;\n /** slug として使う TS フィールド名 (必須)。`source.properties[slugField]` で Notion プロパティ名を解決する。 */\n slugField: string;\n /** ステータスとして使う TS フィールド名。 */\n statusField?: string;\n /** 公開扱いするステータス値。`list()` のデフォルト絞り込みに使う。 */\n publishedStatuses?: readonly string[];\n /** アクセス許可するステータス値。`get()` の閲覧可否判定に使う。 */\n accessibleStatuses?: readonly string[];\n /** コレクション固有のライフサイクルフック。グローバル hooks の後に実行される。 */\n hooks?: CMSHooks<T>;\n}\n\n/**\n * `CollectionDef` の strict 版。`slugField` / `statusField` が `keyof T & string` で\n * 型ガードされており、誤フィールド名は型エラーになる (Issue #314 / M3)。\n * CLI 生成スキーマや `defineCollection<T>()` 経由で利用される。\n */\nexport interface StrictCollectionDef<T extends BaseContentItem>\n extends Omit<CollectionDef<T>, \"slugField\" | \"statusField\"> {\n /** slug として使う TS フィールド名。`keyof T` で型ガードされる。 */\n slugField: keyof T & string;\n /** ステータスとして使う TS フィールド名。`keyof T` で型ガードされる。 */\n statusField?: keyof T & string;\n}\n\n/**\n * 型推論ヘルパー: `T` を明示してコレクション定義を作る。`slugField` / `statusField` は\n * `keyof T & string` で補完・型ガードされ、誤フィールド名 (例: `\"slag\"`) で型エラーになる\n * (Issue #314 / M3)。CLI 生成 `nhc.schema.ts` で利用される。\n *\n * @example\n * ```ts\n * type PostItem = BaseContentItem & { authorName?: string };\n * const posts = defineCollection<PostItem>({\n * source: notionSource(...),\n * slugField: \"slug\", // OK\n * statusField: \"status\", // OK\n * // statusField: \"stat\", // 型エラー\n * });\n * ```\n */\nexport function defineCollection<T extends BaseContentItem>(\n def: StrictCollectionDef<T>,\n): CollectionDef<T> {\n // StrictCollectionDef は CollectionDef の slugField/statusField を絞ったサブ型なので\n // 構造的に CollectionDef<T> へ代入可能。型システム上は再構成のため as 経由。\n return def as unknown as CollectionDef<T>;\n}\n\n/**\n * `createClient({ collections })` の map 型。\n * キーがコレクション名、値が `CollectionDef<T>`。\n */\nexport type CollectionsConfig = Record<string, CollectionDef<BaseContentItem>>;\n\n/** `CollectionsConfig` から各 T を抽出するユーティリティ型。 */\nexport type InferCollectionItem<C> =\n C extends CollectionDef<infer T> ? T : BaseContentItem;\n\n/**\n * `createClient()` の入力。\n *\n * @example\n * import { createClient, nodePreset } from \"@notion-headless-cms/core\";\n * import { notionSource } from \"@notion-headless-cms/notion-source\";\n * import { schema } from \"./generated/nhc.schema\";\n *\n * const cms = createClient({\n * sources: { notion: notionSource({ schema, token: process.env.NOTION_TOKEN! }) },\n * ...nodePreset(),\n * });\n */\nexport interface CreateClientOptions<S extends CMSSources = CMSSources> {\n /** データソースアダプター (`@notion-headless-cms/notion-source` 等) のマップ。 */\n sources?: S;\n /**\n * キャッシュアダプタ (配列)。未指定時はキャッシュなし。\n * - `memoryCache()` のように doc + image 両方を担当するもの\n * - `r2Cache()` (image のみ)、`kvCache()` (doc のみ) のように片側のみ担当するもの\n * - 複数 adapter を配列で組み合わせると、各 adapter の `handles` で振り分けられる\n */\n cache?: readonly CacheAdapter[];\n /** SWR(Stale-While-Revalidate)設定。 */\n swr?: SWRConfig;\n /**\n * Markdown→HTML レンダラー。\n * 省略時は `@notion-headless-cms/markdown-html` の `renderMarkdown` を動的 import で使用する。\n * カスタム実装も `RendererFn` 型を満たせば使用可能。\n */\n renderer?: RendererFn;\n /** 画像プロキシのベース URL。デフォルト `/api/images`。 */\n imageProxyBase?: string;\n /** Cloudflare Workers の `waitUntil` に相当する非同期処理の登録関数。 */\n waitUntil?: (p: Promise<unknown>) => void;\n /**\n * Notion 公式 webhook(integration の Webhooks)の検証トークン(HMAC 署名キー)。\n * 設定すると `cms.handler()` の `POST {basePath}/notion-webhook` が署名検証つきで有効になり、\n * 受信イベントの対象ページを `warmByPageId` で再ウォームする。\n * `cms.handler({ notionWebhook: { secret } })` で個別上書きも可能。\n */\n notionWebhookSecret?: string;\n /** ライフサイクルフック (全コレクション共通)。 */\n hooks?: CMSHooks<BaseContentItem>;\n /** プラグイン配列。 */\n plugins?: CMSPlugin<BaseContentItem>[];\n /** ロガー。 */\n logger?: Logger;\n /** ログレベルの下限。指定したレベル未満のログを内部で抑制する。 */\n logLevel?: LogLevel;\n /** レートリミット・リトライ設定。 */\n rateLimiter?: RateLimiterConfig;\n /** レンダリング・コンテンツ処理設定。 */\n content?: ContentConfig;\n}\n","import type { BaseContentItem } from \"./content\";\nimport type { CMSHooks } from \"./hooks\";\nimport type { Logger } from \"./logger\";\n\nexport interface CMSPlugin<T extends BaseContentItem = BaseContentItem> {\n name: string;\n hooks?: CMSHooks<T>;\n logger?: Partial<Logger>;\n}\n\nexport function definePlugin<T extends BaseContentItem>(\n plugin: CMSPlugin<T>,\n): CMSPlugin<T> {\n return plugin;\n}\n"],"mappings":"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"}
|
package/dist/preset/node.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CacheAdapter } from "../cache-
|
|
2
|
-
import { f as SWRConfig } from "../config-
|
|
1
|
+
import { t as CacheAdapter } from "../cache-BnC6kxoU.mjs";
|
|
2
|
+
import { f as SWRConfig } from "../config-RnU3jrFb.mjs";
|
|
3
3
|
import { MemoryCacheOptions } from "../cache/memory.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/preset/node.d.ts
|
package/dist/source-author.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
|
|
2
|
-
import { _ as MergeSourceCollections, g as CMSSources, h as CMSAdapter, n as CollectionsConfig, o as InferCollectionItem, t as CollectionDef } from "./config-
|
|
2
|
+
import { _ as MergeSourceCollections, g as CMSSources, h as CMSAdapter, n as CollectionsConfig, o as InferCollectionItem, t as CollectionDef } from "./config-RnU3jrFb.mjs";
|
|
3
3
|
export type { BaseContentItem, CMSAdapter, CMSSources, CollectionDef, CollectionsConfig, InferCollectionItem, MergeSourceCollections };
|