@notion-headless-cms/core 0.3.6 → 0.3.8
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 +8 -5
- package/dist/cache/memory.mjs +41 -15
- package/dist/cache/memory.mjs.map +1 -1
- package/dist/cache/noop.d.mts +2 -2
- package/dist/cache/noop.mjs +8 -2
- package/dist/cache/noop.mjs.map +1 -1
- package/dist/{cache-D051BP4G.d.mts → cache-QrXdXYMs.d.mts} +28 -6
- package/dist/{content-WydAfQtk.d.mts → content-DyrOwjbA.d.mts} +38 -13
- package/dist/{hooks-D8Lgf-Co.d.mts → hooks-CPRRo9IN.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 +129 -17
- package/dist/index.mjs +215 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as CollectionConfig, c as InferCollectionItem, d as
|
|
3
|
-
import { a as Logger, i as definePlugin, n as mergeLoggers, o as CMSHooks, r as CMSPlugin, s as MaybePromise, t as mergeHooks } from "./hooks-
|
|
1
|
+
import { a as CachedItemMeta, c as ContentBlock, d as InlineNode, i as CachedItemList, l as ContentResult, n as CMSSchemaProperties, o as ItemContentPayload, r as CachedItemContent, s as StorageBinary, t as BaseContentItem, u as ImageRef } from "./content-DyrOwjbA.mjs";
|
|
2
|
+
import { a as CollectionConfig, c as InferCollectionItem, d as PropertyDef, f as PropertyMap, i as CMSSchema, l as InvalidateKind, n as DocumentCacheAdapter, o as DataSource, p as WebhookConfig, r as ImageCacheAdapter, s as DataSourceFactory, t as CacheConfig, u as InvalidateScope } from "./cache-QrXdXYMs.mjs";
|
|
3
|
+
import { a as Logger, i as definePlugin, n as mergeLoggers, o as CMSHooks, r as CMSPlugin, s as MaybePromise, t as mergeHooks } from "./hooks-CPRRo9IN.mjs";
|
|
4
4
|
import { MemoryDocumentCacheOptions, MemoryImageCacheOptions, memoryDocumentCache, memoryImageCache } from "./cache/memory.mjs";
|
|
5
5
|
import { noopDocumentCache, noopImageCache } from "./cache/noop.mjs";
|
|
6
6
|
import { BuiltInCMSErrorCode, CMSError, CMSErrorCode, CMSErrorContext, isCMSError, isCMSErrorInNamespace } from "./errors.mjs";
|
|
@@ -32,19 +32,79 @@ interface GetListOptions<T extends BaseContentItem = BaseContentItem> {
|
|
|
32
32
|
interface AdjacencyOptions<T extends BaseContentItem = BaseContentItem> {
|
|
33
33
|
sort?: SortOption<T>;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* `getItem` の返り値(メタデータ + lazy 本文アクセサ)。
|
|
37
|
+
* `content.html()` / `content.markdown()` / `content.blocks` を呼んだ時点で
|
|
38
|
+
* 初めてキャッシュ層から本文をロードする(または再生成する)。
|
|
39
|
+
*/
|
|
36
40
|
type ItemWithContent<T extends BaseContentItem> = T & {
|
|
37
41
|
content: ContentResult;
|
|
38
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* `getList` の戻り値。アイテム配列とバージョン文字列を含む。
|
|
45
|
+
* version は DataSource.getListVersion() が生成するフィルタ済みアイテムの識別子。
|
|
46
|
+
*/
|
|
47
|
+
interface GetListResult<T extends BaseContentItem = BaseContentItem> {
|
|
48
|
+
items: T[];
|
|
49
|
+
/** フィルタ適用後のアイテム群を識別するバージョン文字列。 */
|
|
50
|
+
version: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `checkForUpdate` の戻り値。
|
|
54
|
+
*
|
|
55
|
+
* **本文(HTML 等)は含めない**。差分判定はメタデータのみで完結し、
|
|
56
|
+
* 本文は `invalidate({ kind: "content" })` で失効 + バックグラウンド再生成される。
|
|
57
|
+
* クライアント側 (useSWR) は `mutate(metaKey, result.meta)` と `mutate(contentKey)` を
|
|
58
|
+
* 順に呼ぶことで透過的に最新化できる。
|
|
59
|
+
*/
|
|
60
|
+
type CheckForUpdateResult<T extends BaseContentItem = BaseContentItem> = {
|
|
61
|
+
changed: false;
|
|
62
|
+
} | {
|
|
63
|
+
changed: true;
|
|
64
|
+
meta: T;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* `checkListForUpdate` の戻り値。
|
|
68
|
+
* changed: true の場合は最新のアイテム配列とバージョンを含む。
|
|
69
|
+
*/
|
|
70
|
+
type CheckListForUpdateResult<T extends BaseContentItem = BaseContentItem> = {
|
|
71
|
+
changed: false;
|
|
72
|
+
} | {
|
|
73
|
+
changed: true;
|
|
74
|
+
items: T[];
|
|
75
|
+
version: string;
|
|
76
|
+
};
|
|
39
77
|
/**
|
|
40
78
|
* コレクション別の CMS クライアント。
|
|
41
79
|
* `cms.posts.getItem(slug)` のようにアクセスする。
|
|
42
80
|
*/
|
|
43
81
|
interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
44
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* スラッグで単件取得 (本文 lazy アクセサ付き)。
|
|
84
|
+
*
|
|
85
|
+
* メタデータはキャッシュ or Notion から即時取得し、
|
|
86
|
+
* 本文(html/markdown/blocks)は `result.content.*()` を呼んだ時点で初めてロードする。
|
|
87
|
+
*
|
|
88
|
+
* SWR: TTL 未設定 or 期限内ならキャッシュ即時返却 + バックグラウンド差分チェック。
|
|
89
|
+
* TTL 期限切れならブロッキングフェッチ。
|
|
90
|
+
*
|
|
91
|
+
* @returns キャッシュまたは Notion から取得したアイテム。存在しない場合は null。
|
|
92
|
+
*/
|
|
45
93
|
getItem(slug: string): Promise<ItemWithContent<T> | null>;
|
|
46
|
-
/**
|
|
47
|
-
|
|
94
|
+
/**
|
|
95
|
+
* メタデータのみを取得する軽量 API。
|
|
96
|
+
* `useSWR("/api/.../meta", () => cms.posts.getItemMeta(slug))` のような形で
|
|
97
|
+
* クライアントから fetcher として直接呼べる。本文は含まれない。
|
|
98
|
+
*/
|
|
99
|
+
getItemMeta(slug: string): Promise<T | null>;
|
|
100
|
+
/**
|
|
101
|
+
* 本文ペイロード(html/markdown/blocks)を取得する。
|
|
102
|
+
* `useSWR("/api/.../content", () => cms.posts.getItemContent(slug))` で利用。
|
|
103
|
+
* 関数を含まない pure JSON を返す。
|
|
104
|
+
*/
|
|
105
|
+
getItemContent(slug: string): Promise<ItemContentPayload | null>;
|
|
106
|
+
/** 公開済みアイテム一覧 (本文なし、一覧ページ向け)。items とバージョン文字列を返す。 */
|
|
107
|
+
getList(opts?: GetListOptions<T>): Promise<GetListResult<T>>;
|
|
48
108
|
/** Next App Router の `generateStaticParams` 向け。 */
|
|
49
109
|
getStaticParams(): Promise<{
|
|
50
110
|
slug: string;
|
|
@@ -56,11 +116,36 @@ interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
|
56
116
|
prev: T | null;
|
|
57
117
|
next: T | null;
|
|
58
118
|
}>;
|
|
59
|
-
/**
|
|
60
|
-
|
|
119
|
+
/**
|
|
120
|
+
* 指定スラッグのアイテムキャッシュを無効化する(メタ + 本文両方)。
|
|
121
|
+
* 次回の getItem 呼び出しで Notion から再取得される。
|
|
122
|
+
*/
|
|
123
|
+
revalidate(slug: string): Promise<void>;
|
|
124
|
+
/** コレクション全体のキャッシュを無効化する。 */
|
|
125
|
+
revalidateAll(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* 指定アイテムが since 以降に更新されたか確認する。
|
|
128
|
+
*
|
|
129
|
+
* メタデータのみを比較する軽量 API(本文 cache は破棄しない)。
|
|
130
|
+
* 差分があれば本文 cache を `kind: "content"` で失効させ、
|
|
131
|
+
* バックグラウンドで再生成を発火する(`waitUntil` あり時)。
|
|
132
|
+
*
|
|
133
|
+
* クライアント側 (useSWR) は戻り値の meta で `mutate(metaKey, meta)` し、
|
|
134
|
+
* `mutate(contentKey)` を呼べば透過的に最新化される。
|
|
135
|
+
*/
|
|
136
|
+
checkForUpdate(args: {
|
|
61
137
|
slug: string;
|
|
62
|
-
|
|
63
|
-
|
|
138
|
+
since: string;
|
|
139
|
+
}): Promise<CheckForUpdateResult<T>>;
|
|
140
|
+
/**
|
|
141
|
+
* リスト全体が since 以降に更新されたか確認する。
|
|
142
|
+
* 差分があった場合のみリストキャッシュを書き換える(本文 cache は触らない)。
|
|
143
|
+
*/
|
|
144
|
+
checkListForUpdate(args: {
|
|
145
|
+
since: string;
|
|
146
|
+
filter?: GetListOptions<T>;
|
|
147
|
+
}): Promise<CheckListForUpdateResult<T>>;
|
|
148
|
+
/** 全コンテンツをプリフェッチしてキャッシュ(メタ + 本文)に保存。 */
|
|
64
149
|
prefetch(opts?: {
|
|
65
150
|
concurrency?: number;
|
|
66
151
|
onProgress?: (done: number, total: number) => void;
|
|
@@ -308,7 +393,7 @@ interface CMSGlobalOps<D extends DataSourceMap> {
|
|
|
308
393
|
declare function createCMS<D extends DataSourceMap>(opts: CreateCMSOptions<D>): CMSClient<D>;
|
|
309
394
|
//#endregion
|
|
310
395
|
//#region src/rendering.d.ts
|
|
311
|
-
/**
|
|
396
|
+
/** 本文レンダリングに必要な依存を束ねたコンテキスト。 */
|
|
312
397
|
interface RenderContext<T extends BaseContentItem> {
|
|
313
398
|
source: DataSource<T>;
|
|
314
399
|
rendererFn: RendererFn | undefined;
|
|
@@ -373,7 +458,9 @@ declare class CollectionClientImpl<T extends BaseContentItem> implements Collect
|
|
|
373
458
|
private readonly ctx;
|
|
374
459
|
constructor(ctx: CollectionContext<T>);
|
|
375
460
|
getItem(slug: string): Promise<ItemWithContent<T> | null>;
|
|
376
|
-
|
|
461
|
+
getItemMeta(slug: string): Promise<T | null>;
|
|
462
|
+
getItemContent(slug: string): Promise<ItemContentPayload | null>;
|
|
463
|
+
getList(opts?: GetListOptions<T>): Promise<GetListResult<T>>;
|
|
377
464
|
getStaticParams(): Promise<{
|
|
378
465
|
slug: string;
|
|
379
466
|
}[]>;
|
|
@@ -382,9 +469,22 @@ declare class CollectionClientImpl<T extends BaseContentItem> implements Collect
|
|
|
382
469
|
prev: T | null;
|
|
383
470
|
next: T | null;
|
|
384
471
|
}>;
|
|
385
|
-
revalidate(
|
|
472
|
+
revalidate(slug: string): Promise<void>;
|
|
473
|
+
revalidateAll(): Promise<void>;
|
|
474
|
+
checkForUpdate({
|
|
475
|
+
slug,
|
|
476
|
+
since
|
|
477
|
+
}: {
|
|
386
478
|
slug: string;
|
|
387
|
-
|
|
479
|
+
since: string;
|
|
480
|
+
}): Promise<CheckForUpdateResult<T>>;
|
|
481
|
+
checkListForUpdate({
|
|
482
|
+
since,
|
|
483
|
+
filter
|
|
484
|
+
}: {
|
|
485
|
+
since: string;
|
|
486
|
+
filter?: GetListOptions<T>;
|
|
487
|
+
}): Promise<CheckListForUpdateResult<T>>;
|
|
388
488
|
prefetch(opts?: {
|
|
389
489
|
concurrency?: number;
|
|
390
490
|
onProgress?: (done: number, total: number) => void;
|
|
@@ -392,7 +492,19 @@ declare class CollectionClientImpl<T extends BaseContentItem> implements Collect
|
|
|
392
492
|
ok: number;
|
|
393
493
|
failed: number;
|
|
394
494
|
}>;
|
|
395
|
-
private
|
|
495
|
+
private persistMeta;
|
|
496
|
+
private invalidateContent;
|
|
497
|
+
/**
|
|
498
|
+
* 本文キャッシュをロードする。キャッシュが無いか、メタとの整合性が取れない場合は
|
|
499
|
+
* 再生成して書き戻す。
|
|
500
|
+
*/
|
|
501
|
+
private loadOrBuildContent;
|
|
502
|
+
/**
|
|
503
|
+
* メタは既知(差分検出済み or 直前にフェッチ済み)の状態で本文だけ
|
|
504
|
+
* バックグラウンド再生成する。エラーは握りつぶしてログのみ。
|
|
505
|
+
*/
|
|
506
|
+
private rebuildContentBg;
|
|
507
|
+
private attachLazyContent;
|
|
396
508
|
private fetchList;
|
|
397
509
|
private checkAndUpdateItemBg;
|
|
398
510
|
private checkAndUpdateListBg;
|
|
@@ -430,5 +542,5 @@ interface NodePresetOptions {
|
|
|
430
542
|
*/
|
|
431
543
|
declare function nodePreset(opts?: NodePresetOptions): Pick<CreateCMSOptions, "cache" | "renderer">;
|
|
432
544
|
//#endregion
|
|
433
|
-
export { type AdjacencyOptions, type BaseContentItem, type BuiltInCMSErrorCode, type CMSClient, CMSError, type CMSErrorCode, type CMSErrorContext, type CMSGlobalOps, type CMSHooks, type CMSPlugin, type CMSSchema, type CMSSchemaProperties, type CacheConfig, type
|
|
545
|
+
export { type AdjacencyOptions, type BaseContentItem, type BuiltInCMSErrorCode, type CMSClient, CMSError, type CMSErrorCode, type CMSErrorContext, type CMSGlobalOps, type CMSHooks, type CMSPlugin, type CMSSchema, type CMSSchemaProperties, type CacheConfig, type CachedItemContent, type CachedItemList, type CachedItemMeta, type CheckForUpdateResult, type CheckListForUpdateResult, type CollectionClient, CollectionClientImpl, type CollectionConfig, type CollectionContext, type CollectionSemantics, type ContentBlock, type ContentConfig, type ContentResult, type CreateCMSOptions, DEFAULT_RETRY_CONFIG, type DataSource, type DataSourceFactory, type DataSourceMap, type DocumentCacheAdapter, type GetListOptions, type GetListResult, type HandlerAdapter, type HandlerOptions, type ImageCacheAdapter, type ImageRef, type InferCollectionItem, type InferDataSourceItem, type InlineNode, type InvalidateKind, type InvalidateScope, type ItemContentPayload, type ItemWithContent, type Logger, type MaybePromise, type MemoryDocumentCacheOptions, type MemoryImageCacheOptions, type NodePresetOptions, type PropertyDef, type PropertyMap, type RateLimiterConfig, type RenderOptions, type RendererFn, type RendererPluginList, type RetryConfig, type SortOption, type StorageBinary, type WebhookConfig, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryDocumentCache, memoryImageCache, mergeHooks, mergeLoggers, nodePreset, noopDocumentCache, noopImageCache, sha256Hex, withRetry };
|
|
434
546
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -94,10 +94,20 @@ function buildCacheImageFn(cache, imageProxyBase, logger) {
|
|
|
94
94
|
//#endregion
|
|
95
95
|
//#region src/rendering.ts
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
98
|
-
* → フック適用まで実行し、キャッシュ保存用の `CachedItem` を返す。
|
|
97
|
+
* メタデータキャッシュエントリを生成する。Notion API も renderer も呼ばない軽量関数。
|
|
99
98
|
*/
|
|
100
|
-
|
|
99
|
+
function buildCachedItemMeta(item, source) {
|
|
100
|
+
return {
|
|
101
|
+
item,
|
|
102
|
+
notionUpdatedAt: source.getLastModified(item),
|
|
103
|
+
cachedAt: Date.now()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* アイテム本文を Markdown ロード → blocks 生成 → HTML レンダリング → フック適用まで
|
|
108
|
+
* 実行し、本文キャッシュ用の `CachedItemContent` を返す。
|
|
109
|
+
*/
|
|
110
|
+
async function buildCachedItemContent(item, ctx) {
|
|
101
111
|
const start = Date.now();
|
|
102
112
|
ctx.logger?.info?.("コンテンツのレンダリング開始", {
|
|
103
113
|
slug: item.slug,
|
|
@@ -114,7 +124,7 @@ async function buildCachedItem(item, ctx) {
|
|
|
114
124
|
message: "Failed to load markdown from source.",
|
|
115
125
|
cause: err,
|
|
116
126
|
context: {
|
|
117
|
-
operation: "
|
|
127
|
+
operation: "buildCachedItemContent:loadMarkdown",
|
|
118
128
|
pageId: item.id,
|
|
119
129
|
slug: item.slug
|
|
120
130
|
}
|
|
@@ -147,7 +157,7 @@ async function buildCachedItem(item, ctx) {
|
|
|
147
157
|
message: "Failed to render markdown.",
|
|
148
158
|
cause: err,
|
|
149
159
|
context: {
|
|
150
|
-
operation: "
|
|
160
|
+
operation: "buildCachedItemContent:renderMarkdown",
|
|
151
161
|
pageId: item.id,
|
|
152
162
|
slug: item.slug
|
|
153
163
|
}
|
|
@@ -158,11 +168,10 @@ async function buildCachedItem(item, ctx) {
|
|
|
158
168
|
html,
|
|
159
169
|
blocks,
|
|
160
170
|
markdown,
|
|
161
|
-
item,
|
|
162
171
|
notionUpdatedAt: ctx.source.getLastModified(item),
|
|
163
172
|
cachedAt: Date.now()
|
|
164
173
|
};
|
|
165
|
-
if (ctx.hooks.
|
|
174
|
+
if (ctx.hooks.beforeCacheContent) result = await ctx.hooks.beforeCacheContent(result, item);
|
|
166
175
|
const durationMs = Date.now() - start;
|
|
167
176
|
ctx.logger?.info?.("コンテンツのレンダリング完了", {
|
|
168
177
|
slug: item.slug,
|
|
@@ -240,9 +249,9 @@ var CollectionClientImpl = class {
|
|
|
240
249
|
this.ctx = ctx;
|
|
241
250
|
}
|
|
242
251
|
async getItem(slug) {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
if (this.ctx.ttlMs !== void 0 && isStale(
|
|
252
|
+
const cachedMeta = await this.ctx.docCache.getItemMeta(slug);
|
|
253
|
+
if (cachedMeta) {
|
|
254
|
+
if (this.ctx.ttlMs !== void 0 && isStale(cachedMeta.cachedAt, this.ctx.ttlMs)) {
|
|
246
255
|
this.ctx.logger?.debug?.("キャッシュ期限切れ(TTL)、フェッチ", {
|
|
247
256
|
operation: "getItem",
|
|
248
257
|
slug,
|
|
@@ -252,21 +261,21 @@ var CollectionClientImpl = class {
|
|
|
252
261
|
this.ctx.hooks.onCacheMiss?.(slug);
|
|
253
262
|
const item = await this.findRaw(slug);
|
|
254
263
|
if (!item) return null;
|
|
255
|
-
const
|
|
256
|
-
await this.
|
|
257
|
-
return this.
|
|
264
|
+
const meta = await this.persistMeta(slug, item);
|
|
265
|
+
await this.invalidateContent(slug);
|
|
266
|
+
return this.attachLazyContent(meta);
|
|
258
267
|
}
|
|
259
|
-
const bg = this.checkAndUpdateItemBg(slug,
|
|
268
|
+
const bg = this.checkAndUpdateItemBg(slug, cachedMeta);
|
|
260
269
|
if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
|
|
261
270
|
this.ctx.logger?.debug?.("キャッシュヒット", {
|
|
262
271
|
operation: "getItem",
|
|
263
272
|
slug,
|
|
264
273
|
collection: this.ctx.collection,
|
|
265
274
|
cacheAdapter: this.ctx.docCache.name,
|
|
266
|
-
cachedAt:
|
|
275
|
+
cachedAt: cachedMeta.cachedAt
|
|
267
276
|
});
|
|
268
|
-
this.ctx.hooks.onCacheHit?.(slug,
|
|
269
|
-
return this.
|
|
277
|
+
this.ctx.hooks.onCacheHit?.(slug, cachedMeta);
|
|
278
|
+
return this.attachLazyContent(cachedMeta);
|
|
270
279
|
}
|
|
271
280
|
this.ctx.logger?.debug?.("キャッシュミス、フェッチ", {
|
|
272
281
|
operation: "getItem",
|
|
@@ -284,14 +293,42 @@ var CollectionClientImpl = class {
|
|
|
284
293
|
});
|
|
285
294
|
return null;
|
|
286
295
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
const meta = await this.persistMeta(slug, item, { background: true });
|
|
297
|
+
return this.attachLazyContent(meta);
|
|
298
|
+
}
|
|
299
|
+
async getItemMeta(slug) {
|
|
300
|
+
const cachedMeta = await this.ctx.docCache.getItemMeta(slug);
|
|
301
|
+
if (cachedMeta) {
|
|
302
|
+
if (this.ctx.ttlMs !== void 0 && isStale(cachedMeta.cachedAt, this.ctx.ttlMs)) {
|
|
303
|
+
const item = await this.findRaw(slug);
|
|
304
|
+
if (!item) return null;
|
|
305
|
+
const meta = await this.persistMeta(slug, item);
|
|
306
|
+
await this.invalidateContent(slug);
|
|
307
|
+
return meta.item;
|
|
308
|
+
}
|
|
309
|
+
const bg = this.checkAndUpdateItemBg(slug, cachedMeta);
|
|
310
|
+
if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
|
|
311
|
+
this.ctx.hooks.onCacheHit?.(slug, cachedMeta);
|
|
312
|
+
return cachedMeta.item;
|
|
313
|
+
}
|
|
314
|
+
this.ctx.hooks.onCacheMiss?.(slug);
|
|
315
|
+
const item = await this.findRaw(slug);
|
|
316
|
+
if (!item) return null;
|
|
317
|
+
return (await this.persistMeta(slug, item, { background: true })).item;
|
|
318
|
+
}
|
|
319
|
+
async getItemContent(slug) {
|
|
320
|
+
const meta = await this.ctx.docCache.getItemMeta(slug);
|
|
321
|
+
const item = meta?.item ?? await this.findRaw(slug);
|
|
322
|
+
if (!item) return null;
|
|
323
|
+
if (!meta) await this.persistMeta(slug, item);
|
|
324
|
+
return toContentPayload(await this.loadOrBuildContent(slug, item));
|
|
292
325
|
}
|
|
293
326
|
async getList(opts) {
|
|
294
|
-
|
|
327
|
+
const items = applyGetListOptions(await this.fetchList(), opts);
|
|
328
|
+
return {
|
|
329
|
+
items,
|
|
330
|
+
version: this.ctx.source.getListVersion(items)
|
|
331
|
+
};
|
|
295
332
|
}
|
|
296
333
|
async getStaticParams() {
|
|
297
334
|
return (await this.fetchList()).map((item) => ({ slug: item.slug }));
|
|
@@ -311,20 +348,73 @@ var CollectionClientImpl = class {
|
|
|
311
348
|
next: index < items.length - 1 ? items[index + 1] ?? null : null
|
|
312
349
|
};
|
|
313
350
|
}
|
|
314
|
-
async revalidate(
|
|
315
|
-
this.ctx.logger?.debug?.("
|
|
351
|
+
async revalidate(slug) {
|
|
352
|
+
this.ctx.logger?.debug?.("アイテムキャッシュを無効化", {
|
|
316
353
|
operation: "revalidate",
|
|
317
354
|
collection: this.ctx.collection,
|
|
318
355
|
cacheAdapter: this.ctx.docCache.name,
|
|
319
|
-
slug
|
|
356
|
+
slug
|
|
320
357
|
});
|
|
321
358
|
if (!this.ctx.docCache.invalidate) return;
|
|
322
|
-
|
|
323
|
-
else await this.ctx.docCache.invalidate({
|
|
359
|
+
await this.ctx.docCache.invalidate({
|
|
324
360
|
collection: this.ctx.collection,
|
|
325
|
-
slug
|
|
361
|
+
slug
|
|
326
362
|
});
|
|
327
363
|
}
|
|
364
|
+
async revalidateAll() {
|
|
365
|
+
this.ctx.logger?.debug?.("コレクション全体のキャッシュを無効化", {
|
|
366
|
+
operation: "revalidateAll",
|
|
367
|
+
collection: this.ctx.collection,
|
|
368
|
+
cacheAdapter: this.ctx.docCache.name
|
|
369
|
+
});
|
|
370
|
+
if (!this.ctx.docCache.invalidate) return;
|
|
371
|
+
await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
|
|
372
|
+
}
|
|
373
|
+
async checkForUpdate({ slug, since }) {
|
|
374
|
+
const fresh = await this.findRaw(slug);
|
|
375
|
+
if (!fresh) return { changed: false };
|
|
376
|
+
const lm = this.ctx.source.getLastModified(fresh);
|
|
377
|
+
if (lm === since) {
|
|
378
|
+
this.ctx.logger?.debug?.("checkForUpdate: 差分なし", {
|
|
379
|
+
operation: "checkForUpdate",
|
|
380
|
+
slug,
|
|
381
|
+
collection: this.ctx.collection,
|
|
382
|
+
since
|
|
383
|
+
});
|
|
384
|
+
return { changed: false };
|
|
385
|
+
}
|
|
386
|
+
const meta = await this.persistMeta(slug, fresh);
|
|
387
|
+
await this.invalidateContent(slug);
|
|
388
|
+
this.ctx.hooks.onCacheRevalidated?.(slug, meta);
|
|
389
|
+
this.ctx.logger?.debug?.("checkForUpdate: 差分を検出", {
|
|
390
|
+
operation: "checkForUpdate",
|
|
391
|
+
slug,
|
|
392
|
+
collection: this.ctx.collection,
|
|
393
|
+
since,
|
|
394
|
+
notionUpdatedAt: lm
|
|
395
|
+
});
|
|
396
|
+
const bg = this.rebuildContentBg(slug, fresh);
|
|
397
|
+
if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
|
|
398
|
+
return {
|
|
399
|
+
changed: true,
|
|
400
|
+
meta: fresh
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async checkListForUpdate({ since, filter }) {
|
|
404
|
+
const allItems = await this.fetchListRaw();
|
|
405
|
+
const items = applyGetListOptions(allItems, filter);
|
|
406
|
+
const version = this.ctx.source.getListVersion(items);
|
|
407
|
+
if (version === since) return { changed: false };
|
|
408
|
+
await this.ctx.docCache.setList({
|
|
409
|
+
items: allItems,
|
|
410
|
+
cachedAt: Date.now()
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
changed: true,
|
|
414
|
+
items,
|
|
415
|
+
version
|
|
416
|
+
};
|
|
417
|
+
}
|
|
328
418
|
async prefetch(opts) {
|
|
329
419
|
const items = await this.fetchListRaw();
|
|
330
420
|
const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;
|
|
@@ -334,8 +424,9 @@ var CollectionClientImpl = class {
|
|
|
334
424
|
const chunk = items.slice(i, i + concurrency);
|
|
335
425
|
await Promise.all(chunk.map(async (item) => {
|
|
336
426
|
try {
|
|
337
|
-
|
|
338
|
-
await this.ctx.
|
|
427
|
+
await this.persistMeta(item.slug, item);
|
|
428
|
+
const content = await buildCachedItemContent(item, this.ctx.render);
|
|
429
|
+
await this.ctx.docCache.setItemContent(item.slug, content);
|
|
339
430
|
ok++;
|
|
340
431
|
} catch (err) {
|
|
341
432
|
failed++;
|
|
@@ -357,32 +448,71 @@ var CollectionClientImpl = class {
|
|
|
357
448
|
failed
|
|
358
449
|
};
|
|
359
450
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
451
|
+
async persistMeta(slug, item, opts = {}) {
|
|
452
|
+
let meta = buildCachedItemMeta(item, this.ctx.source);
|
|
453
|
+
if (this.ctx.hooks.beforeCacheMeta) meta = await this.ctx.hooks.beforeCacheMeta(meta);
|
|
454
|
+
const save = this.ctx.docCache.setItemMeta(slug, meta);
|
|
455
|
+
if (opts.background && this.ctx.waitUntil) this.ctx.waitUntil(save);
|
|
456
|
+
else await save;
|
|
457
|
+
return meta;
|
|
458
|
+
}
|
|
459
|
+
async invalidateContent(slug) {
|
|
460
|
+
if (!this.ctx.docCache.invalidate) return;
|
|
461
|
+
await this.ctx.docCache.invalidate({
|
|
462
|
+
collection: this.ctx.collection,
|
|
463
|
+
slug,
|
|
464
|
+
kind: "content"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 本文キャッシュをロードする。キャッシュが無いか、メタとの整合性が取れない場合は
|
|
469
|
+
* 再生成して書き戻す。
|
|
470
|
+
*/
|
|
471
|
+
async loadOrBuildContent(slug, item) {
|
|
472
|
+
const expected = this.ctx.source.getLastModified(item);
|
|
473
|
+
const cached = await this.ctx.docCache.getItemContent(slug);
|
|
474
|
+
if (cached && cached.notionUpdatedAt === expected) return cached;
|
|
475
|
+
const fresh = await buildCachedItemContent(item, this.ctx.render);
|
|
476
|
+
await this.ctx.docCache.setItemContent(slug, fresh);
|
|
477
|
+
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
478
|
+
return fresh;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* メタは既知(差分検出済み or 直前にフェッチ済み)の状態で本文だけ
|
|
482
|
+
* バックグラウンド再生成する。エラーは握りつぶしてログのみ。
|
|
483
|
+
*/
|
|
484
|
+
async rebuildContentBg(slug, item) {
|
|
485
|
+
try {
|
|
486
|
+
const fresh = await buildCachedItemContent(item, this.ctx.render);
|
|
487
|
+
await this.ctx.docCache.setItemContent(slug, fresh);
|
|
488
|
+
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
this.ctx.logger?.warn?.("本文のバックグラウンド再生成に失敗", {
|
|
491
|
+
slug,
|
|
492
|
+
collection: this.ctx.collection,
|
|
493
|
+
error: err instanceof Error ? err.message : String(err)
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
attachLazyContent(meta) {
|
|
498
|
+
const slug = meta.item.slug;
|
|
499
|
+
const item = meta.item;
|
|
500
|
+
let payloadPromise;
|
|
501
|
+
const loadPayload = () => {
|
|
502
|
+
if (!payloadPromise) payloadPromise = this.loadOrBuildContent(slug, item);
|
|
503
|
+
return payloadPromise;
|
|
504
|
+
};
|
|
505
|
+
return Object.assign(Object.create(null), item, { content: {
|
|
506
|
+
async blocks() {
|
|
507
|
+
return (await loadPayload()).blocks;
|
|
372
508
|
},
|
|
373
509
|
async html() {
|
|
374
|
-
|
|
375
|
-
htmlCache = cached.html;
|
|
376
|
-
return htmlCache;
|
|
510
|
+
return (await loadPayload()).html;
|
|
377
511
|
},
|
|
378
512
|
async markdown() {
|
|
379
|
-
|
|
380
|
-
markdownCache = await ctx.source.loadMarkdown(item);
|
|
381
|
-
return markdownCache;
|
|
513
|
+
return (await loadPayload()).markdown;
|
|
382
514
|
}
|
|
383
|
-
};
|
|
384
|
-
if (cached.blocks) blocksCache = cached.blocks;
|
|
385
|
-
return Object.assign(Object.create(null), item, { content });
|
|
515
|
+
} });
|
|
386
516
|
}
|
|
387
517
|
async fetchList() {
|
|
388
518
|
const cached = await this.ctx.docCache.getList();
|
|
@@ -432,17 +562,18 @@ var CollectionClientImpl = class {
|
|
|
432
562
|
const item = await this.findRaw(slug);
|
|
433
563
|
if (!item) return;
|
|
434
564
|
if (this.ctx.source.getLastModified(item) !== cached.notionUpdatedAt) {
|
|
435
|
-
const
|
|
436
|
-
await this.
|
|
437
|
-
this.ctx.logger?.debug?.("SWR:
|
|
565
|
+
const meta = await this.persistMeta(slug, item);
|
|
566
|
+
await this.invalidateContent(slug);
|
|
567
|
+
this.ctx.logger?.debug?.("SWR: 差分を検出、メタを差し替え", {
|
|
438
568
|
operation: "getItem:bg",
|
|
439
569
|
slug,
|
|
440
570
|
collection: this.ctx.collection,
|
|
441
571
|
notionUpdatedAt: cached.notionUpdatedAt
|
|
442
572
|
});
|
|
443
|
-
this.ctx.hooks.onCacheRevalidated?.(slug,
|
|
573
|
+
this.ctx.hooks.onCacheRevalidated?.(slug, meta);
|
|
574
|
+
await this.rebuildContentBg(slug, item);
|
|
444
575
|
} else if (this.ctx.ttlMs !== void 0) {
|
|
445
|
-
await this.ctx.docCache.
|
|
576
|
+
await this.ctx.docCache.setItemMeta(slug, {
|
|
446
577
|
...cached,
|
|
447
578
|
cachedAt: Date.now()
|
|
448
579
|
});
|
|
@@ -524,6 +655,14 @@ var CollectionClientImpl = class {
|
|
|
524
655
|
return item;
|
|
525
656
|
}
|
|
526
657
|
};
|
|
658
|
+
function toContentPayload(c) {
|
|
659
|
+
return {
|
|
660
|
+
html: c.html,
|
|
661
|
+
markdown: c.markdown,
|
|
662
|
+
blocks: c.blocks,
|
|
663
|
+
notionUpdatedAt: c.notionUpdatedAt
|
|
664
|
+
};
|
|
665
|
+
}
|
|
527
666
|
function applyGetListOptions(items, opts) {
|
|
528
667
|
if (!opts) return items;
|
|
529
668
|
let result = items;
|
|
@@ -673,6 +812,15 @@ function scopeDocumentCache(base, collection) {
|
|
|
673
812
|
const itemKey = (slug) => `${collection}:${slug}`;
|
|
674
813
|
let listSlot = null;
|
|
675
814
|
let listInitialized = false;
|
|
815
|
+
const mapInvalidateScope = (scope) => {
|
|
816
|
+
if (scope === "all") return { collection };
|
|
817
|
+
if ("slug" in scope) return {
|
|
818
|
+
collection: scope.collection,
|
|
819
|
+
slug: itemKey(scope.slug),
|
|
820
|
+
kind: scope.kind
|
|
821
|
+
};
|
|
822
|
+
return scope;
|
|
823
|
+
};
|
|
676
824
|
return {
|
|
677
825
|
name: `${base.name}@${collection}`,
|
|
678
826
|
getList: async () => {
|
|
@@ -687,18 +835,18 @@ function scopeDocumentCache(base, collection) {
|
|
|
687
835
|
listInitialized = true;
|
|
688
836
|
return Promise.resolve();
|
|
689
837
|
},
|
|
690
|
-
|
|
691
|
-
|
|
838
|
+
getItemMeta: (slug) => base.getItemMeta(itemKey(slug)),
|
|
839
|
+
setItemMeta: (slug, data) => base.setItemMeta(itemKey(slug), data),
|
|
840
|
+
getItemContent: (slug) => base.getItemContent(itemKey(slug)),
|
|
841
|
+
setItemContent: (slug, data) => base.setItemContent(itemKey(slug), data),
|
|
692
842
|
async invalidate(scope) {
|
|
693
|
-
|
|
694
|
-
|
|
843
|
+
const kind = scope === "all" ? "all" : scope.kind ?? "all";
|
|
844
|
+
if (kind === "all" || kind === "meta") {
|
|
845
|
+
listSlot = null;
|
|
846
|
+
listInitialized = true;
|
|
847
|
+
}
|
|
695
848
|
if (!base.invalidate) return;
|
|
696
|
-
|
|
697
|
-
if ("slug" in scope) return base.invalidate({
|
|
698
|
-
collection: scope.collection,
|
|
699
|
-
slug: itemKey(scope.slug)
|
|
700
|
-
});
|
|
701
|
-
return base.invalidate(scope);
|
|
849
|
+
return base.invalidate(mapInvalidateScope(scope));
|
|
702
850
|
}
|
|
703
851
|
};
|
|
704
852
|
}
|