@notion-headless-cms/core 0.1.1 → 0.1.2
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/README.md +145 -47
- package/dist/cache/memory.d.ts +54 -0
- package/dist/cache/memory.js +15 -0
- package/dist/cache/memory.js.map +1 -0
- package/dist/cache/noop.d.ts +9 -0
- package/dist/cache/noop.js +9 -0
- package/dist/cache/noop.js.map +1 -0
- package/dist/cache-DvbyemBK.d.ts +33 -0
- package/dist/chunk-4KGKWKKI.js +80 -0
- package/dist/chunk-4KGKWKKI.js.map +1 -0
- package/dist/chunk-6DG63XUF.js +42 -0
- package/dist/chunk-6DG63XUF.js.map +1 -0
- package/dist/chunk-6LHROEPI.js +104 -0
- package/dist/chunk-6LHROEPI.js.map +1 -0
- package/dist/chunk-V6ML4QE5.js +26 -0
- package/dist/chunk-V6ML4QE5.js.map +1 -0
- package/dist/content-Biqf0l_o.d.ts +51 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/hooks-B83RUclt.d.ts +41 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +9 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +57 -253
- package/dist/index.js +176 -344
- package/dist/index.js.map +1 -0
- package/package.json +29 -11
package/dist/index.js
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CMSError,
|
|
3
|
+
isCMSError,
|
|
4
|
+
isCMSErrorInNamespace
|
|
5
|
+
} from "./chunk-V6ML4QE5.js";
|
|
6
|
+
import {
|
|
7
|
+
mergeHooks,
|
|
8
|
+
mergeLoggers
|
|
9
|
+
} from "./chunk-4KGKWKKI.js";
|
|
10
|
+
import {
|
|
11
|
+
memoryCache,
|
|
12
|
+
memoryDocumentCache,
|
|
13
|
+
memoryImageCache
|
|
14
|
+
} from "./chunk-6LHROEPI.js";
|
|
15
|
+
import {
|
|
16
|
+
noopDocumentCache,
|
|
17
|
+
noopImageCache
|
|
18
|
+
} from "./chunk-6DG63XUF.js";
|
|
19
|
+
|
|
1
20
|
// src/cache.ts
|
|
2
21
|
async function sha256Hex(input) {
|
|
3
22
|
const data = new TextEncoder().encode(input);
|
|
@@ -9,178 +28,6 @@ function isStale(cachedAt, ttlMs) {
|
|
|
9
28
|
return Date.now() - cachedAt > ttlMs;
|
|
10
29
|
}
|
|
11
30
|
|
|
12
|
-
// src/cache/memory.ts
|
|
13
|
-
var MemoryDocumentCache = class {
|
|
14
|
-
name = "memory-document";
|
|
15
|
-
list = null;
|
|
16
|
-
items = /* @__PURE__ */ new Map();
|
|
17
|
-
getList() {
|
|
18
|
-
return Promise.resolve(this.list);
|
|
19
|
-
}
|
|
20
|
-
setList(data) {
|
|
21
|
-
this.list = data;
|
|
22
|
-
return Promise.resolve();
|
|
23
|
-
}
|
|
24
|
-
getItem(slug) {
|
|
25
|
-
return Promise.resolve(this.items.get(slug) ?? null);
|
|
26
|
-
}
|
|
27
|
-
setItem(slug, data) {
|
|
28
|
-
this.items.set(slug, data);
|
|
29
|
-
return Promise.resolve();
|
|
30
|
-
}
|
|
31
|
-
async invalidate(scope) {
|
|
32
|
-
if (scope === "all") {
|
|
33
|
-
this.list = null;
|
|
34
|
-
this.items.clear();
|
|
35
|
-
} else if ("slug" in scope) {
|
|
36
|
-
this.items.delete(scope.slug);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var MemoryImageCache = class {
|
|
41
|
-
name = "memory-image";
|
|
42
|
-
store = /* @__PURE__ */ new Map();
|
|
43
|
-
get(hash) {
|
|
44
|
-
return Promise.resolve(this.store.get(hash) ?? null);
|
|
45
|
-
}
|
|
46
|
-
set(hash, data, contentType) {
|
|
47
|
-
this.store.set(hash, { data, contentType });
|
|
48
|
-
return Promise.resolve();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
function memoryDocumentCache() {
|
|
52
|
-
return new MemoryDocumentCache();
|
|
53
|
-
}
|
|
54
|
-
function memoryImageCache() {
|
|
55
|
-
return new MemoryImageCache();
|
|
56
|
-
}
|
|
57
|
-
function memoryCache() {
|
|
58
|
-
return new MemoryDocumentCache();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/cache/noop.ts
|
|
62
|
-
var NoopDocumentCache = class {
|
|
63
|
-
name = "noop-document";
|
|
64
|
-
getList() {
|
|
65
|
-
return Promise.resolve(null);
|
|
66
|
-
}
|
|
67
|
-
setList(_data) {
|
|
68
|
-
return Promise.resolve();
|
|
69
|
-
}
|
|
70
|
-
getItem(_slug) {
|
|
71
|
-
return Promise.resolve(null);
|
|
72
|
-
}
|
|
73
|
-
setItem(_slug, _data) {
|
|
74
|
-
return Promise.resolve();
|
|
75
|
-
}
|
|
76
|
-
invalidate(_scope) {
|
|
77
|
-
return Promise.resolve();
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
var NoopImageCache = class {
|
|
81
|
-
name = "noop-image";
|
|
82
|
-
get(_hash) {
|
|
83
|
-
return Promise.resolve(null);
|
|
84
|
-
}
|
|
85
|
-
set(_hash, _data, _contentType) {
|
|
86
|
-
return Promise.resolve();
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var _noopDocument = new NoopDocumentCache();
|
|
90
|
-
var _noopImage = new NoopImageCache();
|
|
91
|
-
function noopDocumentCache() {
|
|
92
|
-
return _noopDocument;
|
|
93
|
-
}
|
|
94
|
-
function noopImageCache() {
|
|
95
|
-
return _noopImage;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/cms.ts
|
|
99
|
-
import { renderMarkdown } from "@notion-headless-cms/renderer";
|
|
100
|
-
|
|
101
|
-
// src/errors.ts
|
|
102
|
-
var CMSError = class extends Error {
|
|
103
|
-
code;
|
|
104
|
-
cause;
|
|
105
|
-
context;
|
|
106
|
-
constructor(params) {
|
|
107
|
-
super(params.message, { cause: params.cause });
|
|
108
|
-
this.name = "CMSError";
|
|
109
|
-
this.code = params.code;
|
|
110
|
-
this.cause = params.cause;
|
|
111
|
-
this.context = params.context;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
function isCMSError(error) {
|
|
115
|
-
return error instanceof CMSError;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// src/hooks.ts
|
|
119
|
-
function mergeHooks(plugins, directHooks) {
|
|
120
|
-
const allHooks = [
|
|
121
|
-
...plugins.map((p) => p.hooks ?? {}),
|
|
122
|
-
...directHooks ? [directHooks] : []
|
|
123
|
-
];
|
|
124
|
-
if (allHooks.length === 0) return {};
|
|
125
|
-
return {
|
|
126
|
-
beforeCache: buildPipeline(allHooks, "beforeCache"),
|
|
127
|
-
afterRender: buildRenderPipeline(allHooks),
|
|
128
|
-
onCacheHit: buildObserver(allHooks, "onCacheHit"),
|
|
129
|
-
onCacheMiss: buildObserver(allHooks, "onCacheMiss"),
|
|
130
|
-
onListCacheHit: buildObserver(allHooks, "onListCacheHit"),
|
|
131
|
-
onListCacheMiss: buildObserver(allHooks, "onListCacheMiss"),
|
|
132
|
-
onError: buildObserver(allHooks, "onError")
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function buildPipeline(hooks, key) {
|
|
136
|
-
const fns = hooks.map((h) => h[key]).filter(Boolean);
|
|
137
|
-
if (fns.length === 0) return void 0;
|
|
138
|
-
return async (item) => {
|
|
139
|
-
let current = item;
|
|
140
|
-
for (const fn of fns) {
|
|
141
|
-
current = await fn(current);
|
|
142
|
-
}
|
|
143
|
-
return current;
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function buildRenderPipeline(hooks) {
|
|
147
|
-
const fns = hooks.map((h) => h.afterRender).filter(Boolean);
|
|
148
|
-
if (fns.length === 0) return void 0;
|
|
149
|
-
return async (html, item) => {
|
|
150
|
-
let current = html;
|
|
151
|
-
for (const fn of fns) {
|
|
152
|
-
current = await fn(current, item);
|
|
153
|
-
}
|
|
154
|
-
return current;
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function buildObserver(hooks, key) {
|
|
158
|
-
const fns = hooks.map((h) => h[key]).filter(Boolean);
|
|
159
|
-
if (fns.length === 0) return void 0;
|
|
160
|
-
return ((...args) => {
|
|
161
|
-
for (const fn of fns) {
|
|
162
|
-
fn(...args);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
function mergeLoggers(plugins, directLogger) {
|
|
167
|
-
const loggers = [
|
|
168
|
-
...plugins.map((p) => p.logger ?? {}),
|
|
169
|
-
...directLogger ? [directLogger] : []
|
|
170
|
-
];
|
|
171
|
-
if (loggers.length === 0) return void 0;
|
|
172
|
-
const merged = {};
|
|
173
|
-
for (const level of ["debug", "info", "warn", "error"]) {
|
|
174
|
-
const fns = loggers.map((l) => l[level]).filter(Boolean);
|
|
175
|
-
if (fns.length > 0) {
|
|
176
|
-
merged[level] = (message, context) => {
|
|
177
|
-
for (const fn of fns) fn(message, context);
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return merged;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
31
|
// src/image.ts
|
|
185
32
|
function inferContentType(url, responseContentType) {
|
|
186
33
|
if (responseContentType?.startsWith("image/")) {
|
|
@@ -200,7 +47,17 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
|
|
|
200
47
|
const response = await fetch(notionUrl, {
|
|
201
48
|
signal: AbortSignal.timeout(1e4)
|
|
202
49
|
});
|
|
203
|
-
if (!response.ok)
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new CMSError({
|
|
52
|
+
code: "cache/image_fetch_failed",
|
|
53
|
+
message: `Failed to fetch Notion image: HTTP ${response.status}`,
|
|
54
|
+
context: {
|
|
55
|
+
operation: "fetchAndCacheImage",
|
|
56
|
+
notionUrl,
|
|
57
|
+
httpStatus: response.status
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
204
61
|
const data = await response.arrayBuffer();
|
|
205
62
|
const contentType = inferContentType(
|
|
206
63
|
notionUrl,
|
|
@@ -208,8 +65,9 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
|
|
|
208
65
|
);
|
|
209
66
|
await cache.set(hash, data, contentType);
|
|
210
67
|
} catch (err) {
|
|
68
|
+
if (isCMSError(err)) throw err;
|
|
211
69
|
throw new CMSError({
|
|
212
|
-
code: "
|
|
70
|
+
code: "cache/io_failed",
|
|
213
71
|
message: "Failed to fetch or cache Notion image.",
|
|
214
72
|
cause: err,
|
|
215
73
|
context: { operation: "fetchAndCacheImage", notionUrl }
|
|
@@ -318,9 +176,20 @@ var QueryBuilder = class {
|
|
|
318
176
|
const result = await this.execute();
|
|
319
177
|
return result.items[0] ?? null;
|
|
320
178
|
}
|
|
179
|
+
/** 前後アイテムを返す。sortBy() で指定したソート順を適用する。 */
|
|
321
180
|
async adjacent(slug) {
|
|
322
181
|
const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
|
|
323
|
-
|
|
182
|
+
let items = await this.source.list({ publishedStatuses: statuses });
|
|
183
|
+
if (this._sortField) {
|
|
184
|
+
const field = this._sortField;
|
|
185
|
+
const dir = this._sortDir;
|
|
186
|
+
items = [...items].sort((a, b) => {
|
|
187
|
+
const av = a[field];
|
|
188
|
+
const bv = b[field];
|
|
189
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
190
|
+
return dir === "asc" ? cmp : -cmp;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
324
193
|
const idx = items.findIndex((item) => item.slug === slug);
|
|
325
194
|
if (idx === -1) return { prev: null, next: null };
|
|
326
195
|
return {
|
|
@@ -328,14 +197,18 @@ var QueryBuilder = class {
|
|
|
328
197
|
next: idx < items.length - 1 ? items[idx + 1] : null
|
|
329
198
|
};
|
|
330
199
|
}
|
|
200
|
+
/** 最初の 1 件を返す。`.paginate({ page: 1, perPage: 1 }).executeOne()` の短縮形。 */
|
|
201
|
+
first() {
|
|
202
|
+
return this.paginate({ page: 1, perPage: 1 }).executeOne();
|
|
203
|
+
}
|
|
331
204
|
};
|
|
332
205
|
|
|
333
206
|
// src/retry.ts
|
|
334
207
|
var DEFAULT_RETRY_CONFIG = {
|
|
335
|
-
maxConcurrent: 3,
|
|
336
208
|
retryOn: [429, 502, 503],
|
|
337
209
|
maxRetries: 4,
|
|
338
|
-
baseDelayMs: 1e3
|
|
210
|
+
baseDelayMs: 1e3,
|
|
211
|
+
jitter: true
|
|
339
212
|
};
|
|
340
213
|
async function withRetry(fn, config) {
|
|
341
214
|
let lastError;
|
|
@@ -350,9 +223,9 @@ async function withRetry(fn, config) {
|
|
|
350
223
|
lastError = err;
|
|
351
224
|
if (attempt < config.maxRetries) {
|
|
352
225
|
config.onRetry?.(attempt + 1, status);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
);
|
|
226
|
+
const jitterFactor = config.jitter !== false ? 0.5 + Math.random() * 0.5 : 1;
|
|
227
|
+
const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
356
229
|
}
|
|
357
230
|
}
|
|
358
231
|
}
|
|
@@ -362,17 +235,25 @@ async function withRetry(fn, config) {
|
|
|
362
235
|
// src/cms.ts
|
|
363
236
|
var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
|
|
364
237
|
function resolveDocumentCache(cache) {
|
|
365
|
-
if (!cache || cache
|
|
238
|
+
if (!cache || cache === "disabled" || !cache.document) {
|
|
366
239
|
return noopDocumentCache();
|
|
367
240
|
}
|
|
368
241
|
return cache.document;
|
|
369
242
|
}
|
|
370
243
|
function resolveImageCache(cache) {
|
|
371
|
-
if (!cache || cache
|
|
244
|
+
if (!cache || cache === "disabled" || !cache.image) {
|
|
372
245
|
return noopImageCache();
|
|
373
246
|
}
|
|
374
247
|
return cache.image;
|
|
375
248
|
}
|
|
249
|
+
function resolveTtl(cache) {
|
|
250
|
+
if (!cache || cache === "disabled") return void 0;
|
|
251
|
+
return cache.ttlMs;
|
|
252
|
+
}
|
|
253
|
+
function hasImageCacheConfigured(cache) {
|
|
254
|
+
if (!cache || cache === "disabled") return false;
|
|
255
|
+
return !!cache.image;
|
|
256
|
+
}
|
|
376
257
|
var CMS = class {
|
|
377
258
|
source;
|
|
378
259
|
docCache;
|
|
@@ -383,34 +264,35 @@ var CMS = class {
|
|
|
383
264
|
accessibleStatuses;
|
|
384
265
|
imageProxyBase;
|
|
385
266
|
contentConfig;
|
|
267
|
+
rendererFn;
|
|
386
268
|
waitUntil;
|
|
387
269
|
hooks;
|
|
388
270
|
logger;
|
|
389
271
|
retryConfig;
|
|
390
|
-
|
|
272
|
+
maxConcurrent;
|
|
391
273
|
cache;
|
|
392
274
|
constructor(opts) {
|
|
393
275
|
this.source = opts.source;
|
|
394
276
|
this.docCache = resolveDocumentCache(opts.cache);
|
|
395
277
|
this.imgCache = resolveImageCache(opts.cache);
|
|
396
|
-
this.hasImageCache =
|
|
397
|
-
this.ttlMs = opts.cache
|
|
278
|
+
this.hasImageCache = hasImageCacheConfigured(opts.cache);
|
|
279
|
+
this.ttlMs = resolveTtl(opts.cache);
|
|
398
280
|
this.publishedStatuses = opts.schema?.publishedStatuses ?? (opts.source.publishedStatuses ? [...opts.source.publishedStatuses] : []);
|
|
399
281
|
this.accessibleStatuses = opts.schema?.accessibleStatuses ?? (opts.source.accessibleStatuses ? [...opts.source.accessibleStatuses] : []);
|
|
400
282
|
this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
|
|
401
283
|
this.contentConfig = opts.content;
|
|
284
|
+
this.rendererFn = opts.renderer;
|
|
402
285
|
this.waitUntil = opts.waitUntil;
|
|
403
|
-
this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks);
|
|
404
286
|
this.logger = mergeLoggers(opts.plugins ?? [], opts.logger);
|
|
287
|
+
this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks, this.logger);
|
|
288
|
+
this.maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;
|
|
405
289
|
this.retryConfig = {
|
|
406
290
|
...DEFAULT_RETRY_CONFIG,
|
|
407
|
-
...opts.rateLimiter
|
|
408
|
-
};
|
|
409
|
-
this.cached = {
|
|
410
|
-
list: this.cachedList.bind(this),
|
|
411
|
-
get: this.cachedGet.bind(this)
|
|
291
|
+
...opts.rateLimiter ?? {}
|
|
412
292
|
};
|
|
413
293
|
this.cache = {
|
|
294
|
+
getList: this.cachedList.bind(this),
|
|
295
|
+
get: this.cachedGet.bind(this),
|
|
414
296
|
prefetchAll: this.prefetchAll.bind(this),
|
|
415
297
|
revalidate: this.revalidate.bind(this),
|
|
416
298
|
sync: this.syncFromWebhook.bind(this),
|
|
@@ -442,21 +324,28 @@ var CMS = class {
|
|
|
442
324
|
}
|
|
443
325
|
});
|
|
444
326
|
if (!item) return null;
|
|
445
|
-
if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
|
|
327
|
+
if (this.accessibleStatuses.length > 0 && (!item.status || !this.accessibleStatuses.includes(item.status))) {
|
|
446
328
|
return null;
|
|
447
329
|
}
|
|
448
330
|
return item;
|
|
449
331
|
}
|
|
450
|
-
/**
|
|
451
|
-
|
|
452
|
-
|
|
332
|
+
/** 複数スラッグをまとめてソースから直接取得する。accessibleStatuses フィルタを適用する。 */
|
|
333
|
+
async findMany(slugs) {
|
|
334
|
+
const results = /* @__PURE__ */ new Map();
|
|
335
|
+
await Promise.all(
|
|
336
|
+
slugs.map(async (slug) => {
|
|
337
|
+
const item = await this.find(slug);
|
|
338
|
+
if (item) results.set(slug, item);
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
return results;
|
|
453
342
|
}
|
|
454
343
|
/** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
|
|
455
344
|
isPublished(item) {
|
|
456
345
|
if (this.publishedStatuses.length === 0) return true;
|
|
457
|
-
return this.publishedStatuses.includes(item.status);
|
|
346
|
+
return !!item.status && this.publishedStatuses.includes(item.status);
|
|
458
347
|
}
|
|
459
|
-
/** コンテンツを Markdown → HTML にレンダリングし、CachedItem
|
|
348
|
+
/** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。キャッシュには保存しない。 */
|
|
460
349
|
async render(item) {
|
|
461
350
|
return this.buildCachedItem(item);
|
|
462
351
|
}
|
|
@@ -464,10 +353,65 @@ var CMS = class {
|
|
|
464
353
|
query() {
|
|
465
354
|
return new QueryBuilder(this.source, this.publishedStatuses);
|
|
466
355
|
}
|
|
467
|
-
/**
|
|
356
|
+
/** 静的生成用のスラッグ一覧を返す。 */
|
|
357
|
+
async getStaticSlugs() {
|
|
358
|
+
const items = await this.list();
|
|
359
|
+
return items.map((item) => item.slug);
|
|
360
|
+
}
|
|
361
|
+
// ── 画像配信 ──────────────────────────────────────────────────────────
|
|
362
|
+
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
363
|
+
getCachedImage(hash) {
|
|
364
|
+
return this.imgCache.get(hash);
|
|
365
|
+
}
|
|
366
|
+
/** ハッシュキーでキャッシュ画像を Response として返す。 */
|
|
367
|
+
async createCachedImageResponse(hash) {
|
|
368
|
+
const object = await this.imgCache.get(hash);
|
|
369
|
+
if (!object) return null;
|
|
370
|
+
const headers = new Headers();
|
|
371
|
+
if (object.contentType) headers.set("content-type", object.contentType);
|
|
372
|
+
headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
373
|
+
return new Response(object.data, { headers });
|
|
374
|
+
}
|
|
375
|
+
// ── キャッシュ優先取得(Stale-While-Revalidate) ─────────────────────
|
|
376
|
+
async cachedList() {
|
|
377
|
+
const cached = await this.docCache.getList();
|
|
378
|
+
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
379
|
+
this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
|
|
380
|
+
return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
|
|
381
|
+
}
|
|
382
|
+
this.hooks.onListCacheMiss?.();
|
|
383
|
+
const items = await this.list();
|
|
384
|
+
const cachedAt = Date.now();
|
|
385
|
+
const save = this.docCache.setList({ items, cachedAt });
|
|
386
|
+
if (this.waitUntil) {
|
|
387
|
+
this.waitUntil(save);
|
|
388
|
+
} else {
|
|
389
|
+
await save;
|
|
390
|
+
}
|
|
391
|
+
return { items, isStale: !!cached, cachedAt };
|
|
392
|
+
}
|
|
393
|
+
async cachedGet(slug) {
|
|
394
|
+
const cached = await this.docCache.getItem(slug);
|
|
395
|
+
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
396
|
+
this.hooks.onCacheHit?.(slug, cached);
|
|
397
|
+
return cached;
|
|
398
|
+
}
|
|
399
|
+
this.hooks.onCacheMiss?.(slug);
|
|
400
|
+
const item = await this.find(slug);
|
|
401
|
+
if (!item) return null;
|
|
402
|
+
const entry = await this.buildCachedItem(item);
|
|
403
|
+
const save = this.docCache.setItem(slug, entry);
|
|
404
|
+
if (this.waitUntil) {
|
|
405
|
+
this.waitUntil(save);
|
|
406
|
+
} else {
|
|
407
|
+
await save;
|
|
408
|
+
}
|
|
409
|
+
return entry;
|
|
410
|
+
}
|
|
411
|
+
// ── キャッシュ管理 ────────────────────────────────────────────────────
|
|
468
412
|
async prefetchAll(opts) {
|
|
469
413
|
const items = await this.list();
|
|
470
|
-
const concurrency = opts?.concurrency ??
|
|
414
|
+
const concurrency = opts?.concurrency ?? this.maxConcurrent;
|
|
471
415
|
let ok = 0;
|
|
472
416
|
let failed = 0;
|
|
473
417
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
@@ -478,8 +422,16 @@ var CMS = class {
|
|
|
478
422
|
const rendered = await this.buildCachedItem(item);
|
|
479
423
|
await this.docCache.setItem(item.slug, rendered);
|
|
480
424
|
ok++;
|
|
481
|
-
} catch {
|
|
425
|
+
} catch (err) {
|
|
482
426
|
failed++;
|
|
427
|
+
this.logger?.warn?.(
|
|
428
|
+
"prefetchAll: \u30A2\u30A4\u30C6\u30E0\u306E\u4E8B\u524D\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u306B\u5931\u6557",
|
|
429
|
+
{
|
|
430
|
+
slug: item.slug,
|
|
431
|
+
pageId: item.id,
|
|
432
|
+
error: err instanceof Error ? err.message : String(err)
|
|
433
|
+
}
|
|
434
|
+
);
|
|
483
435
|
}
|
|
484
436
|
})
|
|
485
437
|
);
|
|
@@ -488,17 +440,10 @@ var CMS = class {
|
|
|
488
440
|
await this.docCache.setList({ items, cachedAt: Date.now() });
|
|
489
441
|
return { ok, failed };
|
|
490
442
|
}
|
|
491
|
-
/** 静的生成用のスラッグ一覧を返す。 */
|
|
492
|
-
async getStaticSlugs() {
|
|
493
|
-
const items = await this.list();
|
|
494
|
-
return items.map((item) => item.slug);
|
|
495
|
-
}
|
|
496
|
-
/** 指定スコープのキャッシュを無効化する。 */
|
|
497
443
|
async revalidate(scope) {
|
|
498
444
|
if (!this.docCache.invalidate) return;
|
|
499
445
|
await this.docCache.invalidate(scope ?? "all");
|
|
500
446
|
}
|
|
501
|
-
/** Webhook ペイロードを元にキャッシュを同期する。 */
|
|
502
447
|
async syncFromWebhook(payload) {
|
|
503
448
|
const updated = [];
|
|
504
449
|
if (payload?.slug) {
|
|
@@ -517,44 +462,6 @@ var CMS = class {
|
|
|
517
462
|
}
|
|
518
463
|
return { updated };
|
|
519
464
|
}
|
|
520
|
-
// ── キャッシュ優先取得(Stale-While-Revalidate) ──────────────────────
|
|
521
|
-
/** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
|
|
522
|
-
async cachedList() {
|
|
523
|
-
const cached = await this.docCache.getList();
|
|
524
|
-
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
525
|
-
this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
|
|
526
|
-
return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
|
|
527
|
-
}
|
|
528
|
-
this.hooks.onListCacheMiss?.();
|
|
529
|
-
const items = await this.list();
|
|
530
|
-
const cachedAt = Date.now();
|
|
531
|
-
const save = this.docCache.setList({ items, cachedAt });
|
|
532
|
-
if (this.waitUntil) {
|
|
533
|
-
this.waitUntil(save);
|
|
534
|
-
} else {
|
|
535
|
-
await save;
|
|
536
|
-
}
|
|
537
|
-
return { items, isStale: !!cached, cachedAt };
|
|
538
|
-
}
|
|
539
|
-
/** キャッシュ優先で単一コンテンツを返す(SWR)。 */
|
|
540
|
-
async cachedGet(slug) {
|
|
541
|
-
const cached = await this.docCache.getItem(slug);
|
|
542
|
-
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
543
|
-
this.hooks.onCacheHit?.(slug, cached);
|
|
544
|
-
return cached;
|
|
545
|
-
}
|
|
546
|
-
this.hooks.onCacheMiss?.(slug);
|
|
547
|
-
const item = await this.find(slug);
|
|
548
|
-
if (!item) return null;
|
|
549
|
-
const entry = await this.buildCachedItem(item);
|
|
550
|
-
const save = this.docCache.setItem(slug, entry);
|
|
551
|
-
if (this.waitUntil) {
|
|
552
|
-
this.waitUntil(save);
|
|
553
|
-
} else {
|
|
554
|
-
await save;
|
|
555
|
-
}
|
|
556
|
-
return entry;
|
|
557
|
-
}
|
|
558
465
|
async checkListUpdate(version) {
|
|
559
466
|
const items = await this.list();
|
|
560
467
|
const serverVersion = buildListVersion(items);
|
|
@@ -576,59 +483,6 @@ var CMS = class {
|
|
|
576
483
|
notionUpdatedAt: entry.notionUpdatedAt
|
|
577
484
|
};
|
|
578
485
|
}
|
|
579
|
-
// ── 後方互換 SWR ────────────────────────────────────────────────────────
|
|
580
|
-
/** @deprecated cached.list() を使用してください。 */
|
|
581
|
-
async getList() {
|
|
582
|
-
const result = await this.cachedList();
|
|
583
|
-
return { items: result.items, listVersion: buildListVersion(result.items) };
|
|
584
|
-
}
|
|
585
|
-
/** @deprecated cached.get() を使用してください。 */
|
|
586
|
-
getItem(slug) {
|
|
587
|
-
return this.cachedGet(slug);
|
|
588
|
-
}
|
|
589
|
-
/** @deprecated cache.prefetchAll() を使用してください。 */
|
|
590
|
-
async prefetchAllLegacy(opts) {
|
|
591
|
-
return this.prefetchAll(opts);
|
|
592
|
-
}
|
|
593
|
-
// ── 後方互換クエリ API ──────────────────────────────────────────────────
|
|
594
|
-
/** @deprecated query().status(s).execute() を使用してください。 */
|
|
595
|
-
async listByStatus(status) {
|
|
596
|
-
const statuses = Array.isArray(status) ? status : [status];
|
|
597
|
-
return this.query().status(statuses).execute().then((r) => r.items);
|
|
598
|
-
}
|
|
599
|
-
/** @deprecated query().where(pred).execute() を使用してください。 */
|
|
600
|
-
async where(predicate) {
|
|
601
|
-
return this.query().where(predicate).execute().then((r) => r.items);
|
|
602
|
-
}
|
|
603
|
-
/** @deprecated query().paginate(opts).execute() を使用してください。 */
|
|
604
|
-
async paginate(opts) {
|
|
605
|
-
const result = await this.query().paginate(opts).execute();
|
|
606
|
-
return {
|
|
607
|
-
items: result.items,
|
|
608
|
-
total: result.total,
|
|
609
|
-
page: result.page,
|
|
610
|
-
perPage: result.perPage,
|
|
611
|
-
hasNext: result.hasNext
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
/** @deprecated query().adjacent(slug) を使用してください。 */
|
|
615
|
-
getAdjacent(slug) {
|
|
616
|
-
return this.query().adjacent(slug);
|
|
617
|
-
}
|
|
618
|
-
// ── 画像配信 ───────────────────────────────────────────────────────────
|
|
619
|
-
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
620
|
-
getCachedImage(hash) {
|
|
621
|
-
return this.imgCache.get(hash);
|
|
622
|
-
}
|
|
623
|
-
/** ハッシュキーでキャッシュ画像を Response として返す。 */
|
|
624
|
-
async createCachedImageResponse(hash) {
|
|
625
|
-
const object = await this.imgCache.get(hash);
|
|
626
|
-
if (!object) return null;
|
|
627
|
-
const headers = new Headers();
|
|
628
|
-
if (object.contentType) headers.set("content-type", object.contentType);
|
|
629
|
-
headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
630
|
-
return new Response(object.data, { headers });
|
|
631
|
-
}
|
|
632
486
|
// ── プライベートヘルパー ────────────────────────────────────────────────
|
|
633
487
|
async buildCachedItem(item) {
|
|
634
488
|
const start = Date.now();
|
|
@@ -636,13 +490,14 @@ var CMS = class {
|
|
|
636
490
|
slug: item.slug,
|
|
637
491
|
pageId: item.id
|
|
638
492
|
});
|
|
493
|
+
this.hooks.onRenderStart?.(item.slug);
|
|
639
494
|
let markdown;
|
|
640
495
|
try {
|
|
641
496
|
markdown = await this.source.loadMarkdown(item);
|
|
642
497
|
} catch (err) {
|
|
643
498
|
if (isCMSError(err)) throw err;
|
|
644
499
|
throw new CMSError({
|
|
645
|
-
code: "
|
|
500
|
+
code: "source/load_markdown_failed",
|
|
646
501
|
message: "Failed to load markdown from source.",
|
|
647
502
|
cause: err,
|
|
648
503
|
context: {
|
|
@@ -654,18 +509,18 @@ var CMS = class {
|
|
|
654
509
|
}
|
|
655
510
|
const cacheImage = this.hasImageCache ? buildCacheImageFn(this.imgCache, this.imageProxyBase) : void 0;
|
|
656
511
|
let html;
|
|
512
|
+
const rendererFn = this.rendererFn ?? await loadDefaultRenderer();
|
|
657
513
|
try {
|
|
658
|
-
html = await
|
|
514
|
+
html = await rendererFn(markdown, {
|
|
659
515
|
imageProxyBase: this.imageProxyBase,
|
|
660
516
|
cacheImage,
|
|
661
517
|
remarkPlugins: this.contentConfig?.remarkPlugins,
|
|
662
|
-
rehypePlugins: this.contentConfig?.rehypePlugins
|
|
663
|
-
render: this.contentConfig?.render
|
|
518
|
+
rehypePlugins: this.contentConfig?.rehypePlugins
|
|
664
519
|
});
|
|
665
520
|
} catch (err) {
|
|
666
521
|
if (isCMSError(err)) throw err;
|
|
667
522
|
throw new CMSError({
|
|
668
|
-
code: "
|
|
523
|
+
code: "renderer/failed",
|
|
669
524
|
message: "Failed to render markdown.",
|
|
670
525
|
cause: err,
|
|
671
526
|
context: {
|
|
@@ -687,13 +542,28 @@ var CMS = class {
|
|
|
687
542
|
if (this.hooks.beforeCache) {
|
|
688
543
|
result = await this.hooks.beforeCache(result);
|
|
689
544
|
}
|
|
545
|
+
const durationMs = Date.now() - start;
|
|
690
546
|
this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u5B8C\u4E86", {
|
|
691
547
|
slug: item.slug,
|
|
692
|
-
durationMs
|
|
548
|
+
durationMs
|
|
693
549
|
});
|
|
550
|
+
this.hooks.onRenderEnd?.(item.slug, durationMs);
|
|
694
551
|
return result;
|
|
695
552
|
}
|
|
696
553
|
};
|
|
554
|
+
async function loadDefaultRenderer() {
|
|
555
|
+
try {
|
|
556
|
+
const mod = await import("@notion-headless-cms/renderer");
|
|
557
|
+
return mod.renderMarkdown;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
throw new CMSError({
|
|
560
|
+
code: "renderer/failed",
|
|
561
|
+
message: "renderer \u30AA\u30D7\u30B7\u30E7\u30F3\u304C\u672A\u6307\u5B9A\u3067 @notion-headless-cms/renderer \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002 createCMS({ renderer }) \u3067\u30EC\u30F3\u30C0\u30E9\u30FC\u3092\u6CE8\u5165\u3059\u308B\u304B\u3001@notion-headless-cms/renderer \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
562
|
+
cause: err,
|
|
563
|
+
context: { operation: "loadDefaultRenderer" }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
697
567
|
function buildListVersion(items) {
|
|
698
568
|
return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
|
|
699
569
|
}
|
|
@@ -701,44 +571,6 @@ function createCMS(opts) {
|
|
|
701
571
|
return new CMS(opts);
|
|
702
572
|
}
|
|
703
573
|
|
|
704
|
-
// src/mapper.ts
|
|
705
|
-
import { z } from "zod";
|
|
706
|
-
var baseContentItemSchema = z.object({
|
|
707
|
-
id: z.string().min(1),
|
|
708
|
-
slug: z.string(),
|
|
709
|
-
status: z.string(),
|
|
710
|
-
publishedAt: z.string().min(1),
|
|
711
|
-
updatedAt: z.string().min(1)
|
|
712
|
-
});
|
|
713
|
-
function getPlainText(items) {
|
|
714
|
-
return items?.map((item) => item.plain_text).join("") ?? "";
|
|
715
|
-
}
|
|
716
|
-
function mapItem(page, props) {
|
|
717
|
-
const statusProperty = page.properties[props.status];
|
|
718
|
-
const dateProperty = page.properties[props.date];
|
|
719
|
-
const parsed = baseContentItemSchema.safeParse({
|
|
720
|
-
id: page.id,
|
|
721
|
-
slug: getPlainText(
|
|
722
|
-
page.properties[props.slug]?.rich_text
|
|
723
|
-
),
|
|
724
|
-
status: statusProperty?.status?.name ?? statusProperty?.select?.name ?? "",
|
|
725
|
-
publishedAt: dateProperty?.date?.start ?? page.created_time,
|
|
726
|
-
updatedAt: page.last_edited_time
|
|
727
|
-
});
|
|
728
|
-
if (!parsed.success) {
|
|
729
|
-
throw new CMSError({
|
|
730
|
-
code: "NOTION_ITEM_SCHEMA_INVALID",
|
|
731
|
-
message: "Failed to parse Notion page into BaseContentItem.",
|
|
732
|
-
context: {
|
|
733
|
-
operation: "mapItem",
|
|
734
|
-
pageId: page.id,
|
|
735
|
-
issues: JSON.stringify(parsed.error.issues)
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
return parsed.data;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
574
|
// src/types/plugin.ts
|
|
743
575
|
function definePlugin(plugin) {
|
|
744
576
|
return plugin;
|
|
@@ -750,10 +582,9 @@ export {
|
|
|
750
582
|
QueryBuilder,
|
|
751
583
|
createCMS,
|
|
752
584
|
definePlugin,
|
|
753
|
-
getPlainText,
|
|
754
585
|
isCMSError,
|
|
586
|
+
isCMSErrorInNamespace,
|
|
755
587
|
isStale,
|
|
756
|
-
mapItem,
|
|
757
588
|
memoryCache,
|
|
758
589
|
memoryDocumentCache,
|
|
759
590
|
memoryImageCache,
|
|
@@ -764,3 +595,4 @@ export {
|
|
|
764
595
|
sha256Hex,
|
|
765
596
|
withRetry
|
|
766
597
|
};
|
|
598
|
+
//# sourceMappingURL=index.js.map
|