@notion-headless-cms/core 0.3.7 → 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 +59 -20
- package/dist/index.mjs +188 -70
- 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,7 +32,11 @@ 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
|
};
|
|
@@ -47,13 +51,17 @@ interface GetListResult<T extends BaseContentItem = BaseContentItem> {
|
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* `checkForUpdate` の戻り値。
|
|
50
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* **本文(HTML 等)は含めない**。差分判定はメタデータのみで完結し、
|
|
56
|
+
* 本文は `invalidate({ kind: "content" })` で失効 + バックグラウンド再生成される。
|
|
57
|
+
* クライアント側 (useSWR) は `mutate(metaKey, result.meta)` と `mutate(contentKey)` を
|
|
58
|
+
* 順に呼ぶことで透過的に最新化できる。
|
|
51
59
|
*/
|
|
52
60
|
type CheckForUpdateResult<T extends BaseContentItem = BaseContentItem> = {
|
|
53
61
|
changed: false;
|
|
54
62
|
} | {
|
|
55
63
|
changed: true;
|
|
56
|
-
|
|
64
|
+
meta: T;
|
|
57
65
|
};
|
|
58
66
|
/**
|
|
59
67
|
* `checkListForUpdate` の戻り値。
|
|
@@ -72,16 +80,29 @@ type CheckListForUpdateResult<T extends BaseContentItem = BaseContentItem> = {
|
|
|
72
80
|
*/
|
|
73
81
|
interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
74
82
|
/**
|
|
75
|
-
* スラッグで単件取得 (
|
|
83
|
+
* スラッグで単件取得 (本文 lazy アクセサ付き)。
|
|
84
|
+
*
|
|
85
|
+
* メタデータはキャッシュ or Notion から即時取得し、
|
|
86
|
+
* 本文(html/markdown/blocks)は `result.content.*()` を呼んだ時点で初めてロードする。
|
|
76
87
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* TTL 未設定の場合はキャッシュを即時返却しバックグラウンドで差分チェックする。
|
|
80
|
-
* 明示的に同期リフレッシュしたい場合は {@link revalidate} を先に呼ぶ。
|
|
88
|
+
* SWR: TTL 未設定 or 期限内ならキャッシュ即時返却 + バックグラウンド差分チェック。
|
|
89
|
+
* TTL 期限切れならブロッキングフェッチ。
|
|
81
90
|
*
|
|
82
91
|
* @returns キャッシュまたは Notion から取得したアイテム。存在しない場合は null。
|
|
83
92
|
*/
|
|
84
93
|
getItem(slug: string): Promise<ItemWithContent<T> | null>;
|
|
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>;
|
|
85
106
|
/** 公開済みアイテム一覧 (本文なし、一覧ページ向け)。items とバージョン文字列を返す。 */
|
|
86
107
|
getList(opts?: GetListOptions<T>): Promise<GetListResult<T>>;
|
|
87
108
|
/** Next App Router の `generateStaticParams` 向け。 */
|
|
@@ -96,7 +117,7 @@ interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
|
96
117
|
next: T | null;
|
|
97
118
|
}>;
|
|
98
119
|
/**
|
|
99
|
-
*
|
|
120
|
+
* 指定スラッグのアイテムキャッシュを無効化する(メタ + 本文両方)。
|
|
100
121
|
* 次回の getItem 呼び出しで Notion から再取得される。
|
|
101
122
|
*/
|
|
102
123
|
revalidate(slug: string): Promise<void>;
|
|
@@ -104,8 +125,13 @@ interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
|
104
125
|
revalidateAll(): Promise<void>;
|
|
105
126
|
/**
|
|
106
127
|
* 指定アイテムが since 以降に更新されたか確認する。
|
|
107
|
-
*
|
|
108
|
-
*
|
|
128
|
+
*
|
|
129
|
+
* メタデータのみを比較する軽量 API(本文 cache は破棄しない)。
|
|
130
|
+
* 差分があれば本文 cache を `kind: "content"` で失効させ、
|
|
131
|
+
* バックグラウンドで再生成を発火する(`waitUntil` あり時)。
|
|
132
|
+
*
|
|
133
|
+
* クライアント側 (useSWR) は戻り値の meta で `mutate(metaKey, meta)` し、
|
|
134
|
+
* `mutate(contentKey)` を呼べば透過的に最新化される。
|
|
109
135
|
*/
|
|
110
136
|
checkForUpdate(args: {
|
|
111
137
|
slug: string;
|
|
@@ -113,14 +139,13 @@ interface CollectionClient<T extends BaseContentItem = BaseContentItem> {
|
|
|
113
139
|
}): Promise<CheckForUpdateResult<T>>;
|
|
114
140
|
/**
|
|
115
141
|
* リスト全体が since 以降に更新されたか確認する。
|
|
116
|
-
*
|
|
117
|
-
* 更新があった場合は最新の items と version を返す。
|
|
142
|
+
* 差分があった場合のみリストキャッシュを書き換える(本文 cache は触らない)。
|
|
118
143
|
*/
|
|
119
144
|
checkListForUpdate(args: {
|
|
120
145
|
since: string;
|
|
121
146
|
filter?: GetListOptions<T>;
|
|
122
147
|
}): Promise<CheckListForUpdateResult<T>>;
|
|
123
|
-
/**
|
|
148
|
+
/** 全コンテンツをプリフェッチしてキャッシュ(メタ + 本文)に保存。 */
|
|
124
149
|
prefetch(opts?: {
|
|
125
150
|
concurrency?: number;
|
|
126
151
|
onProgress?: (done: number, total: number) => void;
|
|
@@ -368,7 +393,7 @@ interface CMSGlobalOps<D extends DataSourceMap> {
|
|
|
368
393
|
declare function createCMS<D extends DataSourceMap>(opts: CreateCMSOptions<D>): CMSClient<D>;
|
|
369
394
|
//#endregion
|
|
370
395
|
//#region src/rendering.d.ts
|
|
371
|
-
/**
|
|
396
|
+
/** 本文レンダリングに必要な依存を束ねたコンテキスト。 */
|
|
372
397
|
interface RenderContext<T extends BaseContentItem> {
|
|
373
398
|
source: DataSource<T>;
|
|
374
399
|
rendererFn: RendererFn | undefined;
|
|
@@ -433,6 +458,8 @@ declare class CollectionClientImpl<T extends BaseContentItem> implements Collect
|
|
|
433
458
|
private readonly ctx;
|
|
434
459
|
constructor(ctx: CollectionContext<T>);
|
|
435
460
|
getItem(slug: string): Promise<ItemWithContent<T> | null>;
|
|
461
|
+
getItemMeta(slug: string): Promise<T | null>;
|
|
462
|
+
getItemContent(slug: string): Promise<ItemContentPayload | null>;
|
|
436
463
|
getList(opts?: GetListOptions<T>): Promise<GetListResult<T>>;
|
|
437
464
|
getStaticParams(): Promise<{
|
|
438
465
|
slug: string;
|
|
@@ -465,7 +492,19 @@ declare class CollectionClientImpl<T extends BaseContentItem> implements Collect
|
|
|
465
492
|
ok: number;
|
|
466
493
|
failed: number;
|
|
467
494
|
}>;
|
|
468
|
-
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;
|
|
469
508
|
private fetchList;
|
|
470
509
|
private checkAndUpdateItemBg;
|
|
471
510
|
private checkAndUpdateListBg;
|
|
@@ -503,5 +542,5 @@ interface NodePresetOptions {
|
|
|
503
542
|
*/
|
|
504
543
|
declare function nodePreset(opts?: NodePresetOptions): Pick<CreateCMSOptions, "cache" | "renderer">;
|
|
505
544
|
//#endregion
|
|
506
|
-
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 };
|
|
507
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,11 +293,35 @@ 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);
|
|
@@ -338,22 +371,49 @@ var CollectionClientImpl = class {
|
|
|
338
371
|
await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
|
|
339
372
|
}
|
|
340
373
|
async checkForUpdate({ slug, since }) {
|
|
341
|
-
await this.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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 {
|
|
345
399
|
changed: true,
|
|
346
|
-
|
|
347
|
-
}
|
|
400
|
+
meta: fresh
|
|
401
|
+
};
|
|
348
402
|
}
|
|
349
403
|
async checkListForUpdate({ since, filter }) {
|
|
350
|
-
await this.
|
|
351
|
-
const
|
|
352
|
-
|
|
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 {
|
|
353
413
|
changed: true,
|
|
354
414
|
items,
|
|
355
415
|
version
|
|
356
|
-
}
|
|
416
|
+
};
|
|
357
417
|
}
|
|
358
418
|
async prefetch(opts) {
|
|
359
419
|
const items = await this.fetchListRaw();
|
|
@@ -364,8 +424,9 @@ var CollectionClientImpl = class {
|
|
|
364
424
|
const chunk = items.slice(i, i + concurrency);
|
|
365
425
|
await Promise.all(chunk.map(async (item) => {
|
|
366
426
|
try {
|
|
367
|
-
|
|
368
|
-
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);
|
|
369
430
|
ok++;
|
|
370
431
|
} catch (err) {
|
|
371
432
|
failed++;
|
|
@@ -387,32 +448,71 @@ var CollectionClientImpl = class {
|
|
|
387
448
|
failed
|
|
388
449
|
};
|
|
389
450
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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;
|
|
402
508
|
},
|
|
403
509
|
async html() {
|
|
404
|
-
|
|
405
|
-
htmlCache = cached.html;
|
|
406
|
-
return htmlCache;
|
|
510
|
+
return (await loadPayload()).html;
|
|
407
511
|
},
|
|
408
512
|
async markdown() {
|
|
409
|
-
|
|
410
|
-
markdownCache = await ctx.source.loadMarkdown(item);
|
|
411
|
-
return markdownCache;
|
|
513
|
+
return (await loadPayload()).markdown;
|
|
412
514
|
}
|
|
413
|
-
};
|
|
414
|
-
if (cached.blocks) blocksCache = cached.blocks;
|
|
415
|
-
return Object.assign(Object.create(null), item, { content });
|
|
515
|
+
} });
|
|
416
516
|
}
|
|
417
517
|
async fetchList() {
|
|
418
518
|
const cached = await this.ctx.docCache.getList();
|
|
@@ -462,17 +562,18 @@ var CollectionClientImpl = class {
|
|
|
462
562
|
const item = await this.findRaw(slug);
|
|
463
563
|
if (!item) return;
|
|
464
564
|
if (this.ctx.source.getLastModified(item) !== cached.notionUpdatedAt) {
|
|
465
|
-
const
|
|
466
|
-
await this.
|
|
467
|
-
this.ctx.logger?.debug?.("SWR:
|
|
565
|
+
const meta = await this.persistMeta(slug, item);
|
|
566
|
+
await this.invalidateContent(slug);
|
|
567
|
+
this.ctx.logger?.debug?.("SWR: 差分を検出、メタを差し替え", {
|
|
468
568
|
operation: "getItem:bg",
|
|
469
569
|
slug,
|
|
470
570
|
collection: this.ctx.collection,
|
|
471
571
|
notionUpdatedAt: cached.notionUpdatedAt
|
|
472
572
|
});
|
|
473
|
-
this.ctx.hooks.onCacheRevalidated?.(slug,
|
|
573
|
+
this.ctx.hooks.onCacheRevalidated?.(slug, meta);
|
|
574
|
+
await this.rebuildContentBg(slug, item);
|
|
474
575
|
} else if (this.ctx.ttlMs !== void 0) {
|
|
475
|
-
await this.ctx.docCache.
|
|
576
|
+
await this.ctx.docCache.setItemMeta(slug, {
|
|
476
577
|
...cached,
|
|
477
578
|
cachedAt: Date.now()
|
|
478
579
|
});
|
|
@@ -554,6 +655,14 @@ var CollectionClientImpl = class {
|
|
|
554
655
|
return item;
|
|
555
656
|
}
|
|
556
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
|
+
}
|
|
557
666
|
function applyGetListOptions(items, opts) {
|
|
558
667
|
if (!opts) return items;
|
|
559
668
|
let result = items;
|
|
@@ -703,6 +812,15 @@ function scopeDocumentCache(base, collection) {
|
|
|
703
812
|
const itemKey = (slug) => `${collection}:${slug}`;
|
|
704
813
|
let listSlot = null;
|
|
705
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
|
+
};
|
|
706
824
|
return {
|
|
707
825
|
name: `${base.name}@${collection}`,
|
|
708
826
|
getList: async () => {
|
|
@@ -717,18 +835,18 @@ function scopeDocumentCache(base, collection) {
|
|
|
717
835
|
listInitialized = true;
|
|
718
836
|
return Promise.resolve();
|
|
719
837
|
},
|
|
720
|
-
|
|
721
|
-
|
|
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),
|
|
722
842
|
async invalidate(scope) {
|
|
723
|
-
|
|
724
|
-
|
|
843
|
+
const kind = scope === "all" ? "all" : scope.kind ?? "all";
|
|
844
|
+
if (kind === "all" || kind === "meta") {
|
|
845
|
+
listSlot = null;
|
|
846
|
+
listInitialized = true;
|
|
847
|
+
}
|
|
725
848
|
if (!base.invalidate) return;
|
|
726
|
-
|
|
727
|
-
if ("slug" in scope) return base.invalidate({
|
|
728
|
-
collection: scope.collection,
|
|
729
|
-
slug: itemKey(scope.slug)
|
|
730
|
-
});
|
|
731
|
-
return base.invalidate(scope);
|
|
849
|
+
return base.invalidate(mapInvalidateScope(scope));
|
|
732
850
|
}
|
|
733
851
|
};
|
|
734
852
|
}
|