@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/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,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
- /** `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
  };
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
- /** スラッグで単件取得 (本文込み)。キャッシュを経由 (SWR)。 */
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
- getList(opts?: GetListOptions<T>): Promise<T[]>;
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
- revalidate(scope?: "all" | {
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
- }): Promise<void>;
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
- /** `buildCachedItem` に必要な CMS の依存を束ねたコンテキスト。 */
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
- getList(opts?: GetListOptions<T>): Promise<T[]>;
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(scope?: "all" | {
472
+ revalidate(slug: string): Promise<void>;
473
+ revalidateAll(): Promise<void>;
474
+ checkForUpdate({
475
+ slug,
476
+ since
477
+ }: {
386
478
  slug: string;
387
- }): Promise<void>;
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 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;
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 CachedItem, type CachedItemList, 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 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 };
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
- * コンテンツアイテムをソースから 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,14 +293,42 @@ 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
- return applyGetListOptions(await this.fetchList(), opts);
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(scope) {
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: typeof scope === "object" ? scope.slug : void 0
356
+ slug
320
357
  });
321
358
  if (!this.ctx.docCache.invalidate) return;
322
- if (scope === void 0 || scope === "all") await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
323
- else await this.ctx.docCache.invalidate({
359
+ await this.ctx.docCache.invalidate({
324
360
  collection: this.ctx.collection,
325
- slug: scope.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
- const rendered = await buildCachedItem(item, this.ctx.render);
338
- 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);
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
- attachContent(item, cached) {
361
- const ctx = this.ctx;
362
- let blocksCache;
363
- let htmlCache = cached.html;
364
- let markdownCache;
365
- const content = {
366
- get blocks() {
367
- if (!blocksCache) blocksCache = [{
368
- type: "raw",
369
- html: cached.html
370
- }];
371
- 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;
372
508
  },
373
509
  async html() {
374
- if (htmlCache !== void 0) return htmlCache;
375
- htmlCache = cached.html;
376
- return htmlCache;
510
+ return (await loadPayload()).html;
377
511
  },
378
512
  async markdown() {
379
- if (markdownCache !== void 0) return markdownCache;
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 entry = await buildCachedItem(item, this.ctx.render);
436
- await this.ctx.docCache.setItem(slug, entry);
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, entry);
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.setItem(slug, {
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
- getItem: (slug) => base.getItem(itemKey(slug)),
691
- 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),
692
842
  async invalidate(scope) {
693
- listSlot = null;
694
- listInitialized = true;
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
- if (scope === "all") return base.invalidate({ collection });
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
  }