@notion-headless-cms/core 0.3.7 → 0.3.9
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/dist/cache/memory.d.mts +2 -47
- package/dist/cache/memory.mjs +78 -38
- package/dist/cache/memory.mjs.map +1 -1
- package/dist/{content-WydAfQtk.d.mts → content-BIcYVt2y.d.mts} +28 -13
- package/dist/{hooks-D8Lgf-Co.d.mts → hooks-C0Pv0WYd.d.mts} +16 -8
- package/dist/hooks.d.mts +1 -1
- package/dist/hooks.mjs +18 -7
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.d.mts +172 -281
- package/dist/index.mjs +340 -324
- package/dist/index.mjs.map +1 -1
- package/dist/memory-V04Q09jC.d.mts +146 -0
- package/package.json +4 -8
- package/dist/cache/noop.d.mts +0 -11
- package/dist/cache/noop.mjs +0 -44
- package/dist/cache/noop.mjs.map +0 -1
- package/dist/cache-D051BP4G.d.mts +0 -148
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { noopDocumentCache, noopImageCache } from "./cache/noop.mjs";
|
|
1
|
+
import { memoryCache } from "./cache/memory.mjs";
|
|
3
2
|
import { CMSError, isCMSError, isCMSErrorInNamespace } from "./errors.mjs";
|
|
4
3
|
import { mergeHooks, mergeLoggers } from "./hooks.mjs";
|
|
5
4
|
//#region src/cache.ts
|
|
@@ -18,6 +17,47 @@ function isStale(cachedAt, ttlMs) {
|
|
|
18
17
|
return Date.now() - cachedAt > ttlMs;
|
|
19
18
|
}
|
|
20
19
|
//#endregion
|
|
20
|
+
//#region src/cache/noop.ts
|
|
21
|
+
/** 何もキャッシュしないドキュメントオペレーション。常に null を返す。 */
|
|
22
|
+
const noopDoc = {
|
|
23
|
+
getList(_collection) {
|
|
24
|
+
return Promise.resolve(null);
|
|
25
|
+
},
|
|
26
|
+
setList(_collection, _data) {
|
|
27
|
+
return Promise.resolve();
|
|
28
|
+
},
|
|
29
|
+
getMeta(_collection, _slug) {
|
|
30
|
+
return Promise.resolve(null);
|
|
31
|
+
},
|
|
32
|
+
setMeta(_collection, _slug, _data) {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
},
|
|
35
|
+
getContent(_collection, _slug) {
|
|
36
|
+
return Promise.resolve(null);
|
|
37
|
+
},
|
|
38
|
+
setContent(_collection, _slug, _data) {
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
},
|
|
41
|
+
invalidate() {
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/** 何もキャッシュしない画像オペレーション。 */
|
|
46
|
+
const noopImg = {
|
|
47
|
+
get(_hash) {
|
|
48
|
+
return Promise.resolve(null);
|
|
49
|
+
},
|
|
50
|
+
set() {
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 何もキャッシュしないアダプタ。`createCMS({ cache })` 未指定時の内部デフォルト。
|
|
56
|
+
* テストでも使える。
|
|
57
|
+
*/
|
|
58
|
+
const noopDocOps = noopDoc;
|
|
59
|
+
const noopImgOps = noopImg;
|
|
60
|
+
//#endregion
|
|
21
61
|
//#region src/image.ts
|
|
22
62
|
/** レスポンスヘッダまたはURLの拡張子からContent-Typeを推測する。 */
|
|
23
63
|
function inferContentType(url, responseContentType) {
|
|
@@ -28,23 +68,47 @@ function inferContentType(url, responseContentType) {
|
|
|
28
68
|
return "image/jpeg";
|
|
29
69
|
}
|
|
30
70
|
/**
|
|
31
|
-
*
|
|
71
|
+
* URL → SHA-256 hash のメモ化マップ。
|
|
72
|
+
* Notion の画像 URL は同じ画像でも署名が時刻ごとに変わるが、
|
|
73
|
+
* 1 リクエスト内では同一 URL が複数回現れることが多い (重複ハッシュ計算を回避)。
|
|
74
|
+
*
|
|
75
|
+
* メモリリーク防止に最大エントリ数を設けており、超過時は最古から削除する LRU。
|
|
76
|
+
*/
|
|
77
|
+
const HASH_MEMO_LIMIT = 1024;
|
|
78
|
+
const hashMemo = /* @__PURE__ */ new Map();
|
|
79
|
+
async function memoSha256(url) {
|
|
80
|
+
const cached = hashMemo.get(url);
|
|
81
|
+
if (cached !== void 0) {
|
|
82
|
+
hashMemo.delete(url);
|
|
83
|
+
hashMemo.set(url, cached);
|
|
84
|
+
return cached;
|
|
85
|
+
}
|
|
86
|
+
const hash = await sha256Hex(url);
|
|
87
|
+
hashMemo.set(url, hash);
|
|
88
|
+
if (hashMemo.size > HASH_MEMO_LIMIT) {
|
|
89
|
+
const firstKey = hashMemo.keys().next().value;
|
|
90
|
+
if (firstKey !== void 0) hashMemo.delete(firstKey);
|
|
91
|
+
}
|
|
92
|
+
return hash;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Notion画像URLをfetchして ImageCacheOps にキャッシュし、プロキシURL を返す。
|
|
32
96
|
* 既存キャッシュがあれば再fetchしない。
|
|
33
97
|
*/
|
|
34
|
-
async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
|
|
35
|
-
const hash = await
|
|
98
|
+
async function fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, logger) {
|
|
99
|
+
const hash = await memoSha256(notionUrl);
|
|
36
100
|
const proxyUrl = `${imageProxyBase}/${hash}`;
|
|
37
101
|
if (await cache.get(hash)) {
|
|
38
102
|
logger?.debug?.("画像キャッシュヒット", {
|
|
39
103
|
operation: "fetchAndCacheImage",
|
|
40
|
-
cacheAdapter:
|
|
104
|
+
cacheAdapter: cacheName,
|
|
41
105
|
imageHash: hash
|
|
42
106
|
});
|
|
43
107
|
return proxyUrl;
|
|
44
108
|
}
|
|
45
109
|
logger?.debug?.("画像キャッシュミス、Notion からフェッチ", {
|
|
46
110
|
operation: "fetchAndCacheImage",
|
|
47
|
-
cacheAdapter:
|
|
111
|
+
cacheAdapter: cacheName,
|
|
48
112
|
imageHash: hash
|
|
49
113
|
});
|
|
50
114
|
try {
|
|
@@ -63,7 +127,7 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
|
|
|
63
127
|
await cache.set(hash, data, contentType);
|
|
64
128
|
logger?.debug?.("画像をキャッシュに保存", {
|
|
65
129
|
operation: "fetchAndCacheImage",
|
|
66
|
-
cacheAdapter:
|
|
130
|
+
cacheAdapter: cacheName,
|
|
67
131
|
imageHash: hash
|
|
68
132
|
});
|
|
69
133
|
} catch (err) {
|
|
@@ -81,23 +145,30 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
|
|
|
81
145
|
return proxyUrl;
|
|
82
146
|
}
|
|
83
147
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* {@link ImageCacheAdapter} に保存後、フロントエンドへの配信用プロキシ URL を返す。
|
|
88
|
-
* Content-Type はレスポンスヘッダまたは URL の拡張子から推測する。
|
|
89
|
-
* タイムアウトは 10 秒固定。
|
|
148
|
+
* `ImageCacheOps` と `imageProxyBase` から `cacheImage` 関数を構築する。
|
|
149
|
+
* 返り値は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして
|
|
150
|
+
* {@link ImageCacheOps} に保存後、プロキシ URL を返す。
|
|
90
151
|
*/
|
|
91
|
-
function buildCacheImageFn(cache, imageProxyBase, logger) {
|
|
92
|
-
return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger);
|
|
152
|
+
function buildCacheImageFn(cache, cacheName, imageProxyBase, logger) {
|
|
153
|
+
return (notionUrl) => fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, logger);
|
|
93
154
|
}
|
|
94
155
|
//#endregion
|
|
95
156
|
//#region src/rendering.ts
|
|
96
157
|
/**
|
|
97
|
-
*
|
|
98
|
-
|
|
158
|
+
* メタデータキャッシュエントリを生成する。Notion API も renderer も呼ばない軽量関数。
|
|
159
|
+
*/
|
|
160
|
+
function buildCachedItemMeta(item, source) {
|
|
161
|
+
return {
|
|
162
|
+
item,
|
|
163
|
+
notionUpdatedAt: source.getLastModified(item),
|
|
164
|
+
cachedAt: Date.now()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* アイテム本文を Markdown ロード → blocks 生成 → HTML レンダリング → フック適用まで
|
|
169
|
+
* 実行し、本文キャッシュ用の `CachedItemContent` を返す。
|
|
99
170
|
*/
|
|
100
|
-
async function
|
|
171
|
+
async function buildCachedItemContent(item, ctx) {
|
|
101
172
|
const start = Date.now();
|
|
102
173
|
ctx.logger?.info?.("コンテンツのレンダリング開始", {
|
|
103
174
|
slug: item.slug,
|
|
@@ -114,7 +185,7 @@ async function buildCachedItem(item, ctx) {
|
|
|
114
185
|
message: "Failed to load markdown from source.",
|
|
115
186
|
cause: err,
|
|
116
187
|
context: {
|
|
117
|
-
operation: "
|
|
188
|
+
operation: "buildCachedItemContent:loadMarkdown",
|
|
118
189
|
pageId: item.id,
|
|
119
190
|
slug: item.slug
|
|
120
191
|
}
|
|
@@ -130,7 +201,7 @@ async function buildCachedItem(item, ctx) {
|
|
|
130
201
|
});
|
|
131
202
|
blocks = [];
|
|
132
203
|
}
|
|
133
|
-
const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imageProxyBase, ctx.logger) : void 0;
|
|
204
|
+
const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imgCacheName, ctx.imageProxyBase, ctx.logger) : void 0;
|
|
134
205
|
let html;
|
|
135
206
|
const rendererFn = ctx.rendererFn ?? await loadDefaultRenderer();
|
|
136
207
|
try {
|
|
@@ -147,7 +218,7 @@ async function buildCachedItem(item, ctx) {
|
|
|
147
218
|
message: "Failed to render markdown.",
|
|
148
219
|
cause: err,
|
|
149
220
|
context: {
|
|
150
|
-
operation: "
|
|
221
|
+
operation: "buildCachedItemContent:renderMarkdown",
|
|
151
222
|
pageId: item.id,
|
|
152
223
|
slug: item.slug
|
|
153
224
|
}
|
|
@@ -158,11 +229,10 @@ async function buildCachedItem(item, ctx) {
|
|
|
158
229
|
html,
|
|
159
230
|
blocks,
|
|
160
231
|
markdown,
|
|
161
|
-
item,
|
|
162
232
|
notionUpdatedAt: ctx.source.getLastModified(item),
|
|
163
233
|
cachedAt: Date.now()
|
|
164
234
|
};
|
|
165
|
-
if (ctx.hooks.
|
|
235
|
+
if (ctx.hooks.beforeCacheContent) result = await ctx.hooks.beforeCacheContent(result, item);
|
|
166
236
|
const durationMs = Date.now() - start;
|
|
167
237
|
ctx.logger?.info?.("コンテンツのレンダリング完了", {
|
|
168
238
|
slug: item.slug,
|
|
@@ -230,132 +300,101 @@ async function withRetry(fn, config) {
|
|
|
230
300
|
/**
|
|
231
301
|
* コレクション別キャッシュキーを生成する。
|
|
232
302
|
* item: `{collection}:{slug}` / list: `{collection}`
|
|
303
|
+
*
|
|
304
|
+
* (Cache adapter 内部のキー戦略はアダプタごとに異なるが、
|
|
305
|
+
* 表示や再計算用に core 側でも公開ヘルパーを提供する)
|
|
233
306
|
*/
|
|
234
307
|
function collectionKey(collection, slug) {
|
|
235
308
|
return slug ? `${collection}:${slug}` : collection;
|
|
236
309
|
}
|
|
237
310
|
/** CollectionClient の実装。ユーザーは `createCMS` 経由でインスタンスを受け取る。 */
|
|
238
311
|
var CollectionClientImpl = class {
|
|
312
|
+
cache;
|
|
239
313
|
constructor(ctx) {
|
|
240
314
|
this.ctx = ctx;
|
|
315
|
+
this.cache = {
|
|
316
|
+
invalidate: (slug) => this.invalidateImpl(slug),
|
|
317
|
+
warm: (opts) => this.warmImpl(opts),
|
|
318
|
+
adjacent: (slug, opts) => this.adjacentImpl(slug, opts)
|
|
319
|
+
};
|
|
241
320
|
}
|
|
242
|
-
async
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
321
|
+
async get(slug, opts = {}) {
|
|
322
|
+
if (opts.fresh) {
|
|
323
|
+
this.ctx.hooks.onCacheMiss?.(slug);
|
|
324
|
+
const item = await this.findRaw(slug);
|
|
325
|
+
if (!item) return null;
|
|
326
|
+
const meta = await this.persistMeta(slug, item);
|
|
327
|
+
await this.invalidateContent(slug);
|
|
328
|
+
return this.attachLazyContent(meta);
|
|
329
|
+
}
|
|
330
|
+
const cachedMeta = await this.ctx.docCache.getMeta(this.ctx.collection, slug);
|
|
331
|
+
if (cachedMeta) {
|
|
332
|
+
if (this.ctx.ttlMs !== void 0 && isStale(cachedMeta.cachedAt, this.ctx.ttlMs)) {
|
|
246
333
|
this.ctx.logger?.debug?.("キャッシュ期限切れ(TTL)、フェッチ", {
|
|
247
|
-
operation: "
|
|
334
|
+
operation: "get",
|
|
248
335
|
slug,
|
|
249
336
|
collection: this.ctx.collection,
|
|
250
|
-
cacheAdapter: this.ctx.
|
|
337
|
+
cacheAdapter: this.ctx.docCacheName
|
|
251
338
|
});
|
|
252
339
|
this.ctx.hooks.onCacheMiss?.(slug);
|
|
253
340
|
const item = await this.findRaw(slug);
|
|
254
341
|
if (!item) return null;
|
|
255
|
-
const
|
|
256
|
-
await this.
|
|
257
|
-
return this.
|
|
342
|
+
const meta = await this.persistMeta(slug, item);
|
|
343
|
+
await this.invalidateContent(slug);
|
|
344
|
+
return this.attachLazyContent(meta);
|
|
258
345
|
}
|
|
259
|
-
const bg = this.checkAndUpdateItemBg(slug,
|
|
346
|
+
const bg = this.checkAndUpdateItemBg(slug, cachedMeta);
|
|
260
347
|
if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
|
|
261
348
|
this.ctx.logger?.debug?.("キャッシュヒット", {
|
|
262
|
-
operation: "
|
|
349
|
+
operation: "get",
|
|
263
350
|
slug,
|
|
264
351
|
collection: this.ctx.collection,
|
|
265
|
-
cacheAdapter: this.ctx.
|
|
266
|
-
cachedAt:
|
|
352
|
+
cacheAdapter: this.ctx.docCacheName,
|
|
353
|
+
cachedAt: cachedMeta.cachedAt
|
|
267
354
|
});
|
|
268
|
-
this.ctx.hooks.onCacheHit?.(slug,
|
|
269
|
-
return this.
|
|
355
|
+
this.ctx.hooks.onCacheHit?.(slug, cachedMeta);
|
|
356
|
+
return this.attachLazyContent(cachedMeta);
|
|
270
357
|
}
|
|
271
358
|
this.ctx.logger?.debug?.("キャッシュミス、フェッチ", {
|
|
272
|
-
operation: "
|
|
359
|
+
operation: "get",
|
|
273
360
|
slug,
|
|
274
361
|
collection: this.ctx.collection,
|
|
275
|
-
cacheAdapter: this.ctx.
|
|
362
|
+
cacheAdapter: this.ctx.docCacheName
|
|
276
363
|
});
|
|
277
364
|
this.ctx.hooks.onCacheMiss?.(slug);
|
|
278
365
|
const item = await this.findRaw(slug);
|
|
279
|
-
if (!item)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
slug,
|
|
283
|
-
collection: this.ctx.collection
|
|
284
|
-
});
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
const entry = await buildCachedItem(item, this.ctx.render);
|
|
288
|
-
const save = this.ctx.docCache.setItem(slug, entry);
|
|
289
|
-
if (this.ctx.waitUntil) this.ctx.waitUntil(save);
|
|
290
|
-
else await save;
|
|
291
|
-
return this.attachContent(entry.item, entry);
|
|
366
|
+
if (!item) return null;
|
|
367
|
+
const meta = await this.persistMeta(slug, item, { background: true });
|
|
368
|
+
return this.attachLazyContent(meta);
|
|
292
369
|
}
|
|
293
|
-
async
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
items,
|
|
297
|
-
version: this.ctx.source.getListVersion(items)
|
|
298
|
-
};
|
|
370
|
+
async list(opts) {
|
|
371
|
+
return applyListOptions(await this.fetchList(), opts);
|
|
299
372
|
}
|
|
300
|
-
async
|
|
373
|
+
async params() {
|
|
301
374
|
return (await this.fetchList()).map((item) => ({ slug: item.slug }));
|
|
302
375
|
}
|
|
303
|
-
async
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
async revalidate(slug) {
|
|
319
|
-
this.ctx.logger?.debug?.("アイテムキャッシュを無効化", {
|
|
320
|
-
operation: "revalidate",
|
|
321
|
-
collection: this.ctx.collection,
|
|
322
|
-
cacheAdapter: this.ctx.docCache.name,
|
|
323
|
-
slug
|
|
324
|
-
});
|
|
325
|
-
if (!this.ctx.docCache.invalidate) return;
|
|
326
|
-
await this.ctx.docCache.invalidate({
|
|
327
|
-
collection: this.ctx.collection,
|
|
328
|
-
slug
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
async revalidateAll() {
|
|
376
|
+
async invalidateImpl(slug) {
|
|
377
|
+
if (slug !== void 0) {
|
|
378
|
+
this.ctx.logger?.debug?.("アイテムキャッシュを無効化", {
|
|
379
|
+
operation: "cache.invalidate",
|
|
380
|
+
collection: this.ctx.collection,
|
|
381
|
+
cacheAdapter: this.ctx.docCacheName,
|
|
382
|
+
slug
|
|
383
|
+
});
|
|
384
|
+
await this.ctx.docCache.invalidate({
|
|
385
|
+
collection: this.ctx.collection,
|
|
386
|
+
slug
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
332
390
|
this.ctx.logger?.debug?.("コレクション全体のキャッシュを無効化", {
|
|
333
|
-
operation: "
|
|
391
|
+
operation: "cache.invalidate",
|
|
334
392
|
collection: this.ctx.collection,
|
|
335
|
-
cacheAdapter: this.ctx.
|
|
393
|
+
cacheAdapter: this.ctx.docCacheName
|
|
336
394
|
});
|
|
337
|
-
if (!this.ctx.docCache.invalidate) return;
|
|
338
395
|
await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
|
|
339
396
|
}
|
|
340
|
-
async
|
|
341
|
-
await this.revalidate(slug);
|
|
342
|
-
const item = await this.getItem(slug);
|
|
343
|
-
if (!item) return { changed: false };
|
|
344
|
-
return item.updatedAt !== since ? {
|
|
345
|
-
changed: true,
|
|
346
|
-
item
|
|
347
|
-
} : { changed: false };
|
|
348
|
-
}
|
|
349
|
-
async checkListForUpdate({ since, filter }) {
|
|
350
|
-
await this.revalidateAll();
|
|
351
|
-
const { items, version } = await this.getList(filter);
|
|
352
|
-
return version !== since ? {
|
|
353
|
-
changed: true,
|
|
354
|
-
items,
|
|
355
|
-
version
|
|
356
|
-
} : { changed: false };
|
|
357
|
-
}
|
|
358
|
-
async prefetch(opts) {
|
|
397
|
+
async warmImpl(opts) {
|
|
359
398
|
const items = await this.fetchListRaw();
|
|
360
399
|
const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;
|
|
361
400
|
let ok = 0;
|
|
@@ -364,12 +403,13 @@ var CollectionClientImpl = class {
|
|
|
364
403
|
const chunk = items.slice(i, i + concurrency);
|
|
365
404
|
await Promise.all(chunk.map(async (item) => {
|
|
366
405
|
try {
|
|
367
|
-
|
|
368
|
-
await this.ctx.
|
|
406
|
+
await this.persistMeta(item.slug, item);
|
|
407
|
+
const content = await buildCachedItemContent(item, this.ctx.render);
|
|
408
|
+
await this.ctx.docCache.setContent(this.ctx.collection, item.slug, content);
|
|
369
409
|
ok++;
|
|
370
410
|
} catch (err) {
|
|
371
411
|
failed++;
|
|
372
|
-
this.ctx.logger?.warn?.("
|
|
412
|
+
this.ctx.logger?.warn?.("warm: アイテムの事前レンダリングに失敗", {
|
|
373
413
|
slug: item.slug,
|
|
374
414
|
pageId: item.id,
|
|
375
415
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -378,7 +418,7 @@ var CollectionClientImpl = class {
|
|
|
378
418
|
}));
|
|
379
419
|
opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);
|
|
380
420
|
}
|
|
381
|
-
await this.ctx.docCache.setList({
|
|
421
|
+
await this.ctx.docCache.setList(this.ctx.collection, {
|
|
382
422
|
items,
|
|
383
423
|
cachedAt: Date.now()
|
|
384
424
|
});
|
|
@@ -387,45 +427,86 @@ var CollectionClientImpl = class {
|
|
|
387
427
|
failed
|
|
388
428
|
};
|
|
389
429
|
}
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
430
|
+
async adjacentImpl(slug, opts) {
|
|
431
|
+
const items = applyListOptions(await this.fetchList(), { sort: opts?.sort });
|
|
432
|
+
const index = items.findIndex((it) => it.slug === slug);
|
|
433
|
+
if (index === -1) return {
|
|
434
|
+
prev: null,
|
|
435
|
+
next: null
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
prev: index > 0 ? items[index - 1] ?? null : null,
|
|
439
|
+
next: index < items.length - 1 ? items[index + 1] ?? null : null
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async persistMeta(slug, item, opts = {}) {
|
|
443
|
+
let meta = buildCachedItemMeta(item, this.ctx.source);
|
|
444
|
+
if (this.ctx.hooks.beforeCacheMeta) meta = await this.ctx.hooks.beforeCacheMeta(meta);
|
|
445
|
+
const save = this.ctx.docCache.setMeta(this.ctx.collection, slug, meta);
|
|
446
|
+
if (opts.background && this.ctx.waitUntil) this.ctx.waitUntil(save);
|
|
447
|
+
else await save;
|
|
448
|
+
return meta;
|
|
449
|
+
}
|
|
450
|
+
async invalidateContent(slug) {
|
|
451
|
+
await this.ctx.docCache.invalidate({
|
|
452
|
+
collection: this.ctx.collection,
|
|
453
|
+
slug,
|
|
454
|
+
kind: "content"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 本文キャッシュをロードする。キャッシュが無いか、メタとの整合性が取れない場合は
|
|
459
|
+
* 再生成して書き戻す。
|
|
460
|
+
*/
|
|
461
|
+
async loadOrBuildContent(slug, item) {
|
|
462
|
+
const expected = this.ctx.source.getLastModified(item);
|
|
463
|
+
const cached = await this.ctx.docCache.getContent(this.ctx.collection, slug);
|
|
464
|
+
if (cached && cached.notionUpdatedAt === expected) return cached;
|
|
465
|
+
const fresh = await buildCachedItemContent(item, this.ctx.render);
|
|
466
|
+
await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);
|
|
467
|
+
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
468
|
+
return fresh;
|
|
469
|
+
}
|
|
470
|
+
/** メタ既知の状態で本文だけバックグラウンド再生成する。エラーは握りつぶす。 */
|
|
471
|
+
async rebuildContentBg(slug, item) {
|
|
472
|
+
try {
|
|
473
|
+
const fresh = await buildCachedItemContent(item, this.ctx.render);
|
|
474
|
+
await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);
|
|
475
|
+
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
this.ctx.logger?.warn?.("本文のバックグラウンド再生成に失敗", {
|
|
478
|
+
slug,
|
|
479
|
+
collection: this.ctx.collection,
|
|
480
|
+
error: err instanceof Error ? err.message : String(err)
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
attachLazyContent(meta) {
|
|
485
|
+
const slug = meta.item.slug;
|
|
486
|
+
const item = meta.item;
|
|
487
|
+
let payloadPromise;
|
|
488
|
+
const loadPayload = () => {
|
|
489
|
+
if (!payloadPromise) payloadPromise = this.loadOrBuildContent(slug, item);
|
|
490
|
+
return payloadPromise;
|
|
491
|
+
};
|
|
492
|
+
const render = async (opts) => {
|
|
493
|
+
const payload = await loadPayload();
|
|
494
|
+
return opts?.format === "markdown" ? payload.markdown : payload.html;
|
|
413
495
|
};
|
|
414
|
-
|
|
415
|
-
return Object.assign(Object.create(null), item, { content });
|
|
496
|
+
return Object.assign(Object.create(null), item, { render });
|
|
416
497
|
}
|
|
417
498
|
async fetchList() {
|
|
418
|
-
const cached = await this.ctx.docCache.getList();
|
|
499
|
+
const cached = await this.ctx.docCache.getList(this.ctx.collection);
|
|
419
500
|
if (cached) {
|
|
420
501
|
if (this.ctx.ttlMs !== void 0 && isStale(cached.cachedAt, this.ctx.ttlMs)) {
|
|
421
502
|
this.ctx.logger?.debug?.("リストキャッシュ期限切れ(TTL)、フェッチ", {
|
|
422
|
-
operation: "
|
|
503
|
+
operation: "list",
|
|
423
504
|
collection: this.ctx.collection,
|
|
424
|
-
cacheAdapter: this.ctx.
|
|
505
|
+
cacheAdapter: this.ctx.docCacheName
|
|
425
506
|
});
|
|
426
507
|
this.ctx.hooks.onListCacheMiss?.();
|
|
427
508
|
const items = await this.fetchListRaw();
|
|
428
|
-
await this.ctx.docCache.setList({
|
|
509
|
+
await this.ctx.docCache.setList(this.ctx.collection, {
|
|
429
510
|
items,
|
|
430
511
|
cachedAt: Date.now()
|
|
431
512
|
});
|
|
@@ -434,22 +515,22 @@ var CollectionClientImpl = class {
|
|
|
434
515
|
const bg = this.checkAndUpdateListBg(cached);
|
|
435
516
|
if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
|
|
436
517
|
this.ctx.logger?.debug?.("リストキャッシュヒット", {
|
|
437
|
-
operation: "
|
|
518
|
+
operation: "list",
|
|
438
519
|
collection: this.ctx.collection,
|
|
439
|
-
cacheAdapter: this.ctx.
|
|
520
|
+
cacheAdapter: this.ctx.docCacheName
|
|
440
521
|
});
|
|
441
522
|
this.ctx.hooks.onListCacheHit?.(cached);
|
|
442
523
|
return cached.items;
|
|
443
524
|
}
|
|
444
525
|
this.ctx.logger?.debug?.("リストキャッシュミス、フェッチ", {
|
|
445
|
-
operation: "
|
|
526
|
+
operation: "list",
|
|
446
527
|
collection: this.ctx.collection,
|
|
447
|
-
cacheAdapter: this.ctx.
|
|
528
|
+
cacheAdapter: this.ctx.docCacheName
|
|
448
529
|
});
|
|
449
530
|
this.ctx.hooks.onListCacheMiss?.();
|
|
450
531
|
const items = await this.fetchListRaw();
|
|
451
532
|
const cachedAt = Date.now();
|
|
452
|
-
const save = this.ctx.docCache.setList({
|
|
533
|
+
const save = this.ctx.docCache.setList(this.ctx.collection, {
|
|
453
534
|
items,
|
|
454
535
|
cachedAt
|
|
455
536
|
});
|
|
@@ -462,22 +543,23 @@ var CollectionClientImpl = class {
|
|
|
462
543
|
const item = await this.findRaw(slug);
|
|
463
544
|
if (!item) return;
|
|
464
545
|
if (this.ctx.source.getLastModified(item) !== cached.notionUpdatedAt) {
|
|
465
|
-
const
|
|
466
|
-
await this.
|
|
467
|
-
this.ctx.logger?.debug?.("SWR:
|
|
468
|
-
operation: "
|
|
546
|
+
const meta = await this.persistMeta(slug, item);
|
|
547
|
+
await this.invalidateContent(slug);
|
|
548
|
+
this.ctx.logger?.debug?.("SWR: 差分を検出、メタを差し替え", {
|
|
549
|
+
operation: "get:bg",
|
|
469
550
|
slug,
|
|
470
551
|
collection: this.ctx.collection,
|
|
471
552
|
notionUpdatedAt: cached.notionUpdatedAt
|
|
472
553
|
});
|
|
473
|
-
this.ctx.hooks.onCacheRevalidated?.(slug,
|
|
554
|
+
this.ctx.hooks.onCacheRevalidated?.(slug, meta);
|
|
555
|
+
await this.rebuildContentBg(slug, item);
|
|
474
556
|
} else if (this.ctx.ttlMs !== void 0) {
|
|
475
|
-
await this.ctx.docCache.
|
|
557
|
+
await this.ctx.docCache.setMeta(this.ctx.collection, slug, {
|
|
476
558
|
...cached,
|
|
477
559
|
cachedAt: Date.now()
|
|
478
560
|
});
|
|
479
561
|
this.ctx.logger?.debug?.("SWR: 差分なし、TTL をリセット", {
|
|
480
|
-
operation: "
|
|
562
|
+
operation: "get:bg",
|
|
481
563
|
slug,
|
|
482
564
|
collection: this.ctx.collection
|
|
483
565
|
});
|
|
@@ -498,19 +580,19 @@ var CollectionClientImpl = class {
|
|
|
498
580
|
items,
|
|
499
581
|
cachedAt: Date.now()
|
|
500
582
|
};
|
|
501
|
-
await this.ctx.docCache.setList(listEntry);
|
|
583
|
+
await this.ctx.docCache.setList(this.ctx.collection, listEntry);
|
|
502
584
|
this.ctx.logger?.debug?.("SWR: リスト差分を検出、キャッシュを差し替え", {
|
|
503
|
-
operation: "
|
|
585
|
+
operation: "list:bg",
|
|
504
586
|
collection: this.ctx.collection
|
|
505
587
|
});
|
|
506
588
|
this.ctx.hooks.onListCacheRevalidated?.(listEntry);
|
|
507
589
|
} else if (this.ctx.ttlMs !== void 0) {
|
|
508
|
-
await this.ctx.docCache.setList({
|
|
590
|
+
await this.ctx.docCache.setList(this.ctx.collection, {
|
|
509
591
|
...cached,
|
|
510
592
|
cachedAt: Date.now()
|
|
511
593
|
});
|
|
512
594
|
this.ctx.logger?.debug?.("SWR: リスト差分なし、TTL をリセット", {
|
|
513
|
-
operation: "
|
|
595
|
+
operation: "list:bg",
|
|
514
596
|
collection: this.ctx.collection
|
|
515
597
|
});
|
|
516
598
|
}
|
|
@@ -525,7 +607,7 @@ var CollectionClientImpl = class {
|
|
|
525
607
|
return withRetry(() => this.ctx.source.list({ publishedStatuses: this.ctx.publishedStatuses.length > 0 ? this.ctx.publishedStatuses : void 0 }), {
|
|
526
608
|
...this.ctx.retryConfig,
|
|
527
609
|
onRetry: (attempt, status) => {
|
|
528
|
-
this.ctx.logger?.warn?.("
|
|
610
|
+
this.ctx.logger?.warn?.("list() リトライ中", {
|
|
529
611
|
attempt,
|
|
530
612
|
status
|
|
531
613
|
});
|
|
@@ -536,15 +618,14 @@ var CollectionClientImpl = class {
|
|
|
536
618
|
const retryOpts = {
|
|
537
619
|
...this.ctx.retryConfig,
|
|
538
620
|
onRetry: (attempt, status) => {
|
|
539
|
-
this.ctx.logger?.warn?.("
|
|
621
|
+
this.ctx.logger?.warn?.("get() リトライ中", {
|
|
540
622
|
attempt,
|
|
541
623
|
status,
|
|
542
624
|
slug
|
|
543
625
|
});
|
|
544
626
|
}
|
|
545
627
|
};
|
|
546
|
-
const
|
|
547
|
-
const notionPropName = slugField ? this.ctx.source.properties?.[slugField]?.notion : void 0;
|
|
628
|
+
const notionPropName = this.ctx.source.properties?.[this.ctx.slugField]?.notion;
|
|
548
629
|
let item;
|
|
549
630
|
const findByProp = this.ctx.source.findByProp?.bind(this.ctx.source);
|
|
550
631
|
if (notionPropName && findByProp) item = await withRetry(() => findByProp(notionPropName, slug), retryOpts);
|
|
@@ -554,11 +635,11 @@ var CollectionClientImpl = class {
|
|
|
554
635
|
return item;
|
|
555
636
|
}
|
|
556
637
|
};
|
|
557
|
-
function
|
|
638
|
+
function applyListOptions(items, opts) {
|
|
558
639
|
if (!opts) return items;
|
|
559
640
|
let result = items;
|
|
560
|
-
if (opts.
|
|
561
|
-
const allow = new Set(opts.
|
|
641
|
+
if (opts.status) {
|
|
642
|
+
const allow = new Set(Array.isArray(opts.status) ? opts.status : [opts.status]);
|
|
562
643
|
result = result.filter((it) => it.status !== void 0 && allow.has(it.status));
|
|
563
644
|
}
|
|
564
645
|
if (opts.tag) {
|
|
@@ -580,7 +661,7 @@ function applyGetListOptions(items, opts) {
|
|
|
580
661
|
}
|
|
581
662
|
function makeComparator(sort) {
|
|
582
663
|
const by = sort.by;
|
|
583
|
-
const dir = sort.
|
|
664
|
+
const dir = sort.dir === "asc" ? 1 : -1;
|
|
584
665
|
return (a, b) => {
|
|
585
666
|
const av = a[by];
|
|
586
667
|
const bv = b[by];
|
|
@@ -648,88 +729,41 @@ function trimTrailingSlash(s) {
|
|
|
648
729
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
649
730
|
}
|
|
650
731
|
//#endregion
|
|
651
|
-
//#region src/preset-node.ts
|
|
652
|
-
/**
|
|
653
|
-
* Node.js ランタイム向けの `createCMS` オプションプリセット。
|
|
654
|
-
* メモリキャッシュをデフォルト有効にした `{ cache, renderer }` を返す。
|
|
655
|
-
*
|
|
656
|
-
* @example
|
|
657
|
-
* import { createCMS, nodePreset } from "@notion-headless-cms/core";
|
|
658
|
-
* import { cmsDataSources } from "./generated/cms-schema";
|
|
659
|
-
*
|
|
660
|
-
* const cms = createCMS({
|
|
661
|
-
* ...nodePreset({ ttlMs: 5 * 60_000 }),
|
|
662
|
-
* dataSources: cmsDataSources,
|
|
663
|
-
* });
|
|
664
|
-
*/
|
|
665
|
-
function nodePreset(opts = {}) {
|
|
666
|
-
if (opts.cache === "disabled") return {
|
|
667
|
-
cache: void 0,
|
|
668
|
-
renderer: opts.renderer
|
|
669
|
-
};
|
|
670
|
-
return {
|
|
671
|
-
cache: opts.cache ?? {
|
|
672
|
-
document: memoryDocumentCache(),
|
|
673
|
-
image: memoryImageCache(),
|
|
674
|
-
ttlMs: opts.ttlMs
|
|
675
|
-
},
|
|
676
|
-
renderer: opts.renderer
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
//#endregion
|
|
680
732
|
//#region src/cms.ts
|
|
681
733
|
const DEFAULT_IMAGE_PROXY_BASE = "/api/images";
|
|
682
|
-
function resolveDocumentCache(cache) {
|
|
683
|
-
if (!cache || cache === "disabled" || !cache.document) return noopDocumentCache();
|
|
684
|
-
return cache.document;
|
|
685
|
-
}
|
|
686
|
-
function resolveImageCache(cache) {
|
|
687
|
-
if (!cache || cache === "disabled" || !cache.image) return noopImageCache();
|
|
688
|
-
return cache.image;
|
|
689
|
-
}
|
|
690
|
-
function resolveTtl(cache) {
|
|
691
|
-
if (!cache || cache === "disabled") return void 0;
|
|
692
|
-
return cache.ttlMs;
|
|
693
|
-
}
|
|
694
|
-
function hasImageCacheConfigured(cache) {
|
|
695
|
-
if (!cache || cache === "disabled") return false;
|
|
696
|
-
return !!cache.image;
|
|
697
|
-
}
|
|
698
734
|
/**
|
|
699
|
-
* `
|
|
700
|
-
*
|
|
735
|
+
* `cache` オプションから document / image オペレーションを解決する。
|
|
736
|
+
*
|
|
737
|
+
* - 配列で渡された場合は各 adapter の `handles` を見て先勝ち (最初に見つかったもの) で振り分ける
|
|
738
|
+
* - 単体で渡された場合は `handles` の領域だけ反映、片側は noop
|
|
739
|
+
* - 未指定なら両方 noop
|
|
701
740
|
*/
|
|
702
|
-
function
|
|
703
|
-
const
|
|
704
|
-
let
|
|
705
|
-
let
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
getItem: (slug) => base.getItem(itemKey(slug)),
|
|
721
|
-
setItem: (slug, data) => base.setItem(itemKey(slug), data),
|
|
722
|
-
async invalidate(scope) {
|
|
723
|
-
listSlot = null;
|
|
724
|
-
listInitialized = true;
|
|
725
|
-
if (!base.invalidate) return;
|
|
726
|
-
if (scope === "all") return base.invalidate({ collection });
|
|
727
|
-
if ("slug" in scope) return base.invalidate({
|
|
728
|
-
collection: scope.collection,
|
|
729
|
-
slug: itemKey(scope.slug)
|
|
730
|
-
});
|
|
731
|
-
return base.invalidate(scope);
|
|
741
|
+
function resolveCache(cache) {
|
|
742
|
+
const adapters = cache === void 0 ? [] : Array.isArray(cache) ? cache : [cache];
|
|
743
|
+
let doc = noopDocOps;
|
|
744
|
+
let docName = "noop-document";
|
|
745
|
+
let img = noopImgOps;
|
|
746
|
+
let imgName = "noop-image";
|
|
747
|
+
let docFound = false;
|
|
748
|
+
let imgFound = false;
|
|
749
|
+
for (const adapter of adapters) {
|
|
750
|
+
if (!docFound && adapter.handles.includes("document") && adapter.doc) {
|
|
751
|
+
doc = adapter.doc;
|
|
752
|
+
docName = adapter.name;
|
|
753
|
+
docFound = true;
|
|
754
|
+
}
|
|
755
|
+
if (!imgFound && adapter.handles.includes("image") && adapter.img) {
|
|
756
|
+
img = adapter.img;
|
|
757
|
+
imgName = adapter.name;
|
|
758
|
+
imgFound = true;
|
|
732
759
|
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
doc,
|
|
763
|
+
docName,
|
|
764
|
+
img,
|
|
765
|
+
imgName,
|
|
766
|
+
hasImg: imgFound
|
|
733
767
|
};
|
|
734
768
|
}
|
|
735
769
|
const LOG_LEVEL_ORDER = {
|
|
@@ -752,68 +786,53 @@ function applyLogLevel(logger, minLevel) {
|
|
|
752
786
|
return filtered;
|
|
753
787
|
}
|
|
754
788
|
/**
|
|
755
|
-
*
|
|
756
|
-
* 明示的な `cache` / `renderer` がある場合はそちらが優先される。
|
|
757
|
-
* `preset` 未指定時は opts をそのまま返す。
|
|
758
|
-
*/
|
|
759
|
-
function resolvePreset(opts) {
|
|
760
|
-
if (opts.preset === "disabled") return {
|
|
761
|
-
...opts,
|
|
762
|
-
cache: void 0
|
|
763
|
-
};
|
|
764
|
-
if (opts.preset === "node") {
|
|
765
|
-
const presetResult = nodePreset({ ttlMs: opts.ttlMs });
|
|
766
|
-
return {
|
|
767
|
-
...opts,
|
|
768
|
-
cache: opts.cache ?? presetResult.cache,
|
|
769
|
-
renderer: opts.renderer ?? presetResult.renderer
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
return opts;
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* 複数の DataSource を束ねた CMS クライアントを生成する。
|
|
776
|
-
*
|
|
777
|
-
* @example
|
|
778
|
-
* // Node.js(preset を使った簡潔な記法)
|
|
779
|
-
* const cms = createCMS({ dataSources: cmsDataSources, preset: "node", ttlMs: 5 * 60_000 });
|
|
789
|
+
* 複数の `CollectionDef` を束ねた CMS クライアントを生成する。
|
|
780
790
|
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* const cms = createCMS({ ...nodePreset({ ttlMs: 5 * 60_000 }), dataSources: cmsDataSources });
|
|
791
|
+
* 通常はユーザーが直接呼ぶことはなく、CLI 生成の `nhc.ts` の `createCMS`
|
|
792
|
+
* (低レベルのこの関数をラップしたもの) を経由する。
|
|
784
793
|
*
|
|
785
794
|
* @example
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
795
|
+
* createCMS({
|
|
796
|
+
* collections: {
|
|
797
|
+
* posts: {
|
|
798
|
+
* source: createNotionCollection({ token, dataSourceId, properties }),
|
|
799
|
+
* slugField: "slug",
|
|
800
|
+
* statusField: "status",
|
|
801
|
+
* publishedStatuses: ["公開済み"],
|
|
802
|
+
* }
|
|
803
|
+
* },
|
|
804
|
+
* cache: memoryCache({ ttlMs: 5 * 60_000 }),
|
|
790
805
|
* });
|
|
791
806
|
*/
|
|
792
807
|
function createCMS(opts) {
|
|
793
|
-
if (!opts.
|
|
808
|
+
if (!opts.collections || Object.keys(opts.collections).length === 0) throw new CMSError({
|
|
794
809
|
code: "core/config_invalid",
|
|
795
|
-
message: "createCMS:
|
|
810
|
+
message: "createCMS: collections に少なくとも 1 つのコレクションを指定してください。",
|
|
796
811
|
context: { operation: "createCMS" }
|
|
797
812
|
});
|
|
798
|
-
for (const [name,
|
|
799
|
-
|
|
800
|
-
|
|
813
|
+
for (const [name, def] of Object.entries(opts.collections)) {
|
|
814
|
+
if (!def.source) throw new CMSError({
|
|
815
|
+
code: "core/config_invalid",
|
|
816
|
+
message: `createCMS: コレクション "${name}" の source は必須です。`,
|
|
817
|
+
context: {
|
|
818
|
+
operation: "createCMS",
|
|
819
|
+
collection: name
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
if (!def.slugField) throw new CMSError({
|
|
801
823
|
code: "core/config_invalid",
|
|
802
|
-
message: `createCMS: コレクション "${name}" の
|
|
824
|
+
message: `createCMS: コレクション "${name}" の slugField は必須です。`,
|
|
803
825
|
context: {
|
|
804
826
|
operation: "createCMS",
|
|
805
827
|
collection: name
|
|
806
828
|
}
|
|
807
829
|
});
|
|
808
830
|
}
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
const hasImageCache = hasImageCacheConfigured(resolved.cache);
|
|
813
|
-
const ttlMs = resolveTtl(resolved.cache);
|
|
814
|
-
const imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
|
|
831
|
+
const cacheRes = resolveCache(opts.cache);
|
|
832
|
+
const ttlMs = opts.ttlMs;
|
|
833
|
+
const imageProxyBase = opts.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
|
|
815
834
|
const contentConfig = opts.content;
|
|
816
|
-
const rendererFn =
|
|
835
|
+
const rendererFn = opts.renderer;
|
|
817
836
|
const waitUntil = opts.waitUntil;
|
|
818
837
|
const baseLogger = mergeLoggers(opts.plugins ?? [], opts.logger);
|
|
819
838
|
const logger = opts.logLevel ? applyLogLevel(baseLogger, opts.logLevel) : baseLogger;
|
|
@@ -823,62 +842,59 @@ function createCMS(opts) {
|
|
|
823
842
|
...DEFAULT_RETRY_CONFIG,
|
|
824
843
|
...opts.rateLimiter ?? {}
|
|
825
844
|
};
|
|
826
|
-
const collectionNames = Object.keys(opts.
|
|
845
|
+
const collectionNames = Object.keys(opts.collections);
|
|
827
846
|
const collections = {};
|
|
828
|
-
const scopedCaches = [];
|
|
829
847
|
for (const name of collectionNames) {
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
const col = opts.collections?.[name];
|
|
834
|
-
const colHooks = col?.hooks;
|
|
848
|
+
const def = opts.collections[name];
|
|
849
|
+
const source = def.source;
|
|
850
|
+
const colHooks = def.hooks;
|
|
835
851
|
const collectionHooks = colHooks ? mergeHooks([{
|
|
836
852
|
name: `${name}:global`,
|
|
837
853
|
hooks
|
|
838
854
|
}], colHooks, logger) : hooks;
|
|
855
|
+
const renderCtx = {
|
|
856
|
+
source,
|
|
857
|
+
rendererFn,
|
|
858
|
+
imgCache: cacheRes.img,
|
|
859
|
+
imgCacheName: cacheRes.imgName,
|
|
860
|
+
hasImageCache: cacheRes.hasImg,
|
|
861
|
+
imageProxyBase,
|
|
862
|
+
contentConfig,
|
|
863
|
+
hooks: collectionHooks,
|
|
864
|
+
logger
|
|
865
|
+
};
|
|
839
866
|
collections[name] = new CollectionClientImpl({
|
|
840
867
|
collection: name,
|
|
841
868
|
source,
|
|
842
|
-
docCache:
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
rendererFn,
|
|
846
|
-
imgCache,
|
|
847
|
-
hasImageCache,
|
|
848
|
-
imageProxyBase,
|
|
849
|
-
contentConfig,
|
|
850
|
-
hooks: collectionHooks,
|
|
851
|
-
logger
|
|
852
|
-
},
|
|
869
|
+
docCache: cacheRes.doc,
|
|
870
|
+
docCacheName: cacheRes.docName,
|
|
871
|
+
render: renderCtx,
|
|
853
872
|
hooks: collectionHooks,
|
|
854
873
|
logger,
|
|
855
874
|
ttlMs,
|
|
856
|
-
publishedStatuses:
|
|
857
|
-
accessibleStatuses:
|
|
875
|
+
publishedStatuses: def.publishedStatuses ? [...def.publishedStatuses] : [],
|
|
876
|
+
accessibleStatuses: def.accessibleStatuses ? [...def.accessibleStatuses] : [],
|
|
858
877
|
retryConfig,
|
|
859
878
|
maxConcurrent,
|
|
860
879
|
waitUntil,
|
|
861
|
-
slugField:
|
|
880
|
+
slugField: def.slugField
|
|
862
881
|
});
|
|
863
882
|
}
|
|
864
883
|
const globalOps = {
|
|
865
884
|
$collections: collectionNames,
|
|
866
|
-
async $
|
|
885
|
+
async $invalidate(scope) {
|
|
867
886
|
logger?.debug?.("グローバルキャッシュを無効化", {
|
|
868
|
-
operation: "$
|
|
869
|
-
cacheAdapter:
|
|
887
|
+
operation: "$invalidate",
|
|
888
|
+
cacheAdapter: cacheRes.docName
|
|
870
889
|
});
|
|
871
|
-
|
|
872
|
-
if (!cache.invalidate) continue;
|
|
873
|
-
await cache.invalidate(scope ?? "all");
|
|
874
|
-
}
|
|
890
|
+
await cacheRes.doc.invalidate(scope ?? "all");
|
|
875
891
|
},
|
|
876
892
|
$handler(handlerOpts) {
|
|
877
893
|
return createHandler({
|
|
878
|
-
imageCache:
|
|
894
|
+
imageCache: cacheRes.img,
|
|
879
895
|
parseWebhook: async (req, webhookSecret) => {
|
|
880
896
|
for (const name of collectionNames) {
|
|
881
|
-
const ds = opts.
|
|
897
|
+
const ds = opts.collections[name].source;
|
|
882
898
|
if (ds.parseWebhook) try {
|
|
883
899
|
return await ds.parseWebhook(req.clone(), { secret: webhookSecret });
|
|
884
900
|
} catch (err) {
|
|
@@ -898,11 +914,11 @@ function createCMS(opts) {
|
|
|
898
914
|
} catch {}
|
|
899
915
|
return null;
|
|
900
916
|
},
|
|
901
|
-
revalidate: (scope) => globalOps.$
|
|
917
|
+
revalidate: (scope) => globalOps.$invalidate(scope)
|
|
902
918
|
}, handlerOpts);
|
|
903
919
|
},
|
|
904
920
|
$getCachedImage(hash) {
|
|
905
|
-
return
|
|
921
|
+
return cacheRes.img.get(hash);
|
|
906
922
|
}
|
|
907
923
|
};
|
|
908
924
|
return Object.assign(Object.create(null), collections, globalOps);
|
|
@@ -913,6 +929,6 @@ function definePlugin(plugin) {
|
|
|
913
929
|
return plugin;
|
|
914
930
|
}
|
|
915
931
|
//#endregion
|
|
916
|
-
export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale,
|
|
932
|
+
export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryCache, mergeHooks, mergeLoggers, noopDocOps, noopImgOps, sha256Hex, withRetry };
|
|
917
933
|
|
|
918
934
|
//# sourceMappingURL=index.mjs.map
|