@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/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as StorageBinary, c as ImageRef, i as CachedItemList, l as InlineNode, n as CMSSchemaProperties, o as ContentBlock, r as CachedItem, s as ContentResult, t as BaseContentItem } from "./content-WydAfQtk.mjs";
2
- import { a as CollectionConfig, c as InferCollectionItem, d as PropertyMap, f as WebhookConfig, i as CMSSchema, l as InvalidateScope, n as DocumentCacheAdapter, o as DataSource, r as ImageCacheAdapter, s as DataSourceFactory, t as CacheConfig, u as PropertyDef } from "./cache-D051BP4G.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-D8Lgf-Co.mjs";
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
- /** `getItem` の返り値 (本文常時同梱)。 */
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
- * changed: true の場合は最新の ItemWithContent を含む。
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
- item: ItemWithContent<T>;
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
- * キャッシュヒット時はキャッシュを即時返却する (SWR)。
78
- * TTL が切れている場合はブロッキングで Notion から再取得する。
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
- * 内部で revalidate → getItem を実行し updatedAt を比較する。
108
- * 更新があった場合は最新の ItemWithContent を返す。
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
- * 内部で revalidateAll → getList を実行しバージョンを比較する。
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
- /** `buildCachedItem` に必要な CMS の依存を束ねたコンテキスト。 */
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 attachContent;
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 CachedItem, type CachedItemList, 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 InvalidateScope, 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 };
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
- * コンテンツアイテムをソースから Markdown ロード blocks 生成 → HTML レンダリング
98
- * → フック適用まで実行し、キャッシュ保存用の `CachedItem` を返す。
97
+ * メタデータキャッシュエントリを生成する。Notion API renderer も呼ばない軽量関数。
99
98
  */
100
- async function buildCachedItem(item, ctx) {
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: "buildCachedItem:loadMarkdown",
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: "buildCachedItem:renderMarkdown",
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.beforeCache) result = await ctx.hooks.beforeCache(result);
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 cached = await this.ctx.docCache.getItem(slug);
244
- if (cached) {
245
- if (this.ctx.ttlMs !== void 0 && isStale(cached.cachedAt, this.ctx.ttlMs)) {
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 entry = await buildCachedItem(item, this.ctx.render);
256
- await this.ctx.docCache.setItem(slug, entry);
257
- return this.attachContent(entry.item, entry);
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, cached);
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: cached.cachedAt
275
+ cachedAt: cachedMeta.cachedAt
267
276
  });
268
- this.ctx.hooks.onCacheHit?.(slug, cached);
269
- return this.attachContent(cached.item, cached);
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 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);
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.revalidate(slug);
342
- const item = await this.getItem(slug);
343
- if (!item) return { changed: false };
344
- return item.updatedAt !== 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 {
345
399
  changed: true,
346
- item
347
- } : { changed: false };
400
+ meta: fresh
401
+ };
348
402
  }
349
403
  async checkListForUpdate({ since, filter }) {
350
- await this.revalidateAll();
351
- const { items, version } = await this.getList(filter);
352
- return version !== since ? {
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
- } : { changed: false };
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
- const rendered = await buildCachedItem(item, this.ctx.render);
368
- await this.ctx.docCache.setItem(item.slug, rendered);
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
- attachContent(item, cached) {
391
- const ctx = this.ctx;
392
- let blocksCache;
393
- let htmlCache = cached.html;
394
- let markdownCache;
395
- const content = {
396
- get blocks() {
397
- if (!blocksCache) blocksCache = [{
398
- type: "raw",
399
- html: cached.html
400
- }];
401
- return blocksCache;
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
- if (htmlCache !== void 0) return htmlCache;
405
- htmlCache = cached.html;
406
- return htmlCache;
510
+ return (await loadPayload()).html;
407
511
  },
408
512
  async markdown() {
409
- if (markdownCache !== void 0) return markdownCache;
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 entry = await buildCachedItem(item, this.ctx.render);
466
- await this.ctx.docCache.setItem(slug, entry);
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, entry);
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.setItem(slug, {
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
- getItem: (slug) => base.getItem(itemKey(slug)),
721
- setItem: (slug, data) => base.setItem(itemKey(slug), data),
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
- listSlot = null;
724
- listInitialized = true;
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
- 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);
849
+ return base.invalidate(mapInvalidateScope(scope));
732
850
  }
733
851
  };
734
852
  }