@dispatchcms/next 0.0.1 → 0.0.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/.turbo/turbo-build.log +8 -8
- package/README.md +6 -2
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/client.ts +35 -2
- package/test/client.cache.test.ts +113 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @dispatchcms/next@0.0.
|
|
3
|
+
> @dispatchcms/next@0.0.2 build /Users/jamescalmus/Documents/dispatch/packages/next
|
|
4
4
|
> tsup
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
[34mCLI[39m Cleaning output folder
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
13
|
[34mESM[39m Build start
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
19
|
-
[
|
|
14
|
+
[32mCJS[39m [1mdist/index.js [22m[32m3.81 KB[39m
|
|
15
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m5.37 KB[39m
|
|
16
|
+
[32mCJS[39m ⚡️ Build success in 4ms
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m2.72 KB[39m
|
|
18
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m5.17 KB[39m
|
|
19
|
+
[32mESM[39m ⚡️ Build success in 4ms
|
|
20
20
|
DTS Build start
|
|
21
|
-
DTS ⚡️ Build success in
|
|
21
|
+
DTS ⚡️ Build success in 314ms
|
|
22
22
|
DTS dist/index.d.ts 670.00 B
|
|
23
23
|
DTS dist/index.d.mts 670.00 B
|
package/README.md
CHANGED
|
@@ -34,18 +34,22 @@ initDispatch({
|
|
|
34
34
|
|
|
35
35
|
You can omit `initDispatch` entirely if `NEXT_PUBLIC_DISPATCH_SITE_KEY` is set; the package will use it automatically.
|
|
36
36
|
|
|
37
|
+
## Caching
|
|
38
|
+
|
|
39
|
+
The client caches responses in memory so that repeated requests (e.g. when navigating around a site) do not trigger unnecessary API calls. If you have already fetched all posts with `getPosts()`, single-post lookups with `getPost(slug)` are served from that cache when the slug is in the list, so no extra request is made.
|
|
40
|
+
|
|
37
41
|
## API
|
|
38
42
|
|
|
39
43
|
### `getPosts(siteKey?)`
|
|
40
44
|
|
|
41
|
-
Returns all published posts for the site.
|
|
45
|
+
Returns all published posts for the site. Results are cached per site key.
|
|
42
46
|
|
|
43
47
|
- **Returns:** `Promise<Post[]>`
|
|
44
48
|
- **Optional:** pass `siteKey` to override the configured site for this call.
|
|
45
49
|
|
|
46
50
|
### `getPost(slug, siteKey?)`
|
|
47
51
|
|
|
48
|
-
Returns a single published post by slug, or `null` if not found.
|
|
52
|
+
Returns a single published post by slug, or `null` if not found. If the full list was already loaded via `getPosts()` for the same site, the post is returned from cache when present; otherwise a single-post request is made and cached.
|
|
49
53
|
|
|
50
54
|
- **Returns:** `Promise<Post | null>`
|
|
51
55
|
- **Optional:** pass `siteKey` as the second argument to override the configured site.
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
30
|
// src/client.ts
|
|
31
31
|
var DISPATCH_API_BASE = "https://dispatch-cms.vercel.app";
|
|
32
32
|
var config = null;
|
|
33
|
+
var allPostsBySiteKey = /* @__PURE__ */ new Map();
|
|
34
|
+
var postBySlugBySiteKey = /* @__PURE__ */ new Map();
|
|
35
|
+
var fullListFetchedForSiteKey = /* @__PURE__ */ new Set();
|
|
33
36
|
function initDispatch(options) {
|
|
34
37
|
config = { siteKey: options.siteKey };
|
|
35
38
|
}
|
|
@@ -44,6 +47,10 @@ function getConfig() {
|
|
|
44
47
|
async function getPosts(siteKey) {
|
|
45
48
|
const { siteKey: key } = getConfig();
|
|
46
49
|
const resolvedKey = siteKey ?? key;
|
|
50
|
+
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
51
|
+
if (cached !== void 0) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
47
54
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
48
55
|
const res = await fetch(url, {
|
|
49
56
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -54,11 +61,31 @@ async function getPosts(siteKey) {
|
|
|
54
61
|
}
|
|
55
62
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
56
63
|
}
|
|
57
|
-
|
|
64
|
+
const posts = await res.json();
|
|
65
|
+
allPostsBySiteKey.set(resolvedKey, posts);
|
|
66
|
+
fullListFetchedForSiteKey.add(resolvedKey);
|
|
67
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const post of posts) {
|
|
69
|
+
bySlug.set(post.slug, post);
|
|
70
|
+
}
|
|
71
|
+
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
72
|
+
return posts;
|
|
58
73
|
}
|
|
59
74
|
async function getPost(slug, siteKey) {
|
|
60
75
|
const { siteKey: key } = getConfig();
|
|
61
76
|
const resolvedKey = siteKey ?? key;
|
|
77
|
+
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
78
|
+
const list = allPostsBySiteKey.get(resolvedKey);
|
|
79
|
+
if (list) {
|
|
80
|
+
const post2 = list.find((p) => p.slug === slug);
|
|
81
|
+
return post2 ?? null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
85
|
+
const cachedPost = bySlug?.get(slug);
|
|
86
|
+
if (cachedPost !== void 0) {
|
|
87
|
+
return cachedPost;
|
|
88
|
+
}
|
|
62
89
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
63
90
|
const res = await fetch(url, {
|
|
64
91
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -72,7 +99,12 @@ async function getPost(slug, siteKey) {
|
|
|
72
99
|
}
|
|
73
100
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
74
101
|
}
|
|
75
|
-
|
|
102
|
+
const post = await res.json();
|
|
103
|
+
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
104
|
+
postBySlugBySiteKey.set(resolvedKey, /* @__PURE__ */ new Map());
|
|
105
|
+
}
|
|
106
|
+
postBySlugBySiteKey.get(resolvedKey).set(slug, post);
|
|
107
|
+
return post;
|
|
76
108
|
}
|
|
77
109
|
// Annotate the CommonJS export names for ESM import in node:
|
|
78
110
|
0 && (module.exports = {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Post, DispatchConfig } from \"./client\";\nexport { initDispatch, getConfig, getPosts, getPost } from \"./client\";\n","export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);\n }\n
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Post, DispatchConfig } from \"./client\";\nexport { initDispatch, getConfig, getPosts, getPost } from \"./client\";\n","export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nconst allPostsBySiteKey = new Map<string, Post[]>();\nconst postBySlugBySiteKey = new Map<string, Map<string, Post>>();\nconst fullListFetchedForSiteKey = new Set<string>();\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const cached = allPostsBySiteKey.get(resolvedKey);\n if (cached !== undefined) {\n return cached;\n }\n const url = `${DISPATCH_API_BASE}/api/posts`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);\n }\n const posts = (await res.json()) as Post[];\n allPostsBySiteKey.set(resolvedKey, posts);\n fullListFetchedForSiteKey.add(resolvedKey);\n const bySlug = new Map<string, Post>();\n for (const post of posts) {\n bySlug.set(post.slug, post);\n }\n postBySlugBySiteKey.set(resolvedKey, bySlug);\n return posts;\n}\n\nexport async function getPost(slug: string, siteKey?: string): Promise<Post | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n if (fullListFetchedForSiteKey.has(resolvedKey)) {\n const list = allPostsBySiteKey.get(resolvedKey);\n if (list) {\n const post = list.find((p) => p.slug === slug);\n return post ?? null;\n }\n }\n const bySlug = postBySlugBySiteKey.get(resolvedKey);\n const cachedPost = bySlug?.get(slug);\n if (cachedPost !== undefined) {\n return cachedPost;\n }\n const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);\n }\n const post = (await res.json()) as Post;\n if (!postBySlugBySiteKey.has(resolvedKey)) {\n postBySlugBySiteKey.set(resolvedKey, new Map());\n }\n postBySlugBySiteKey.get(resolvedKey)!.set(slug, post);\n return post;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAEpC,IAAM,oBAAoB,oBAAI,IAAoB;AAClD,IAAM,sBAAsB,oBAAI,IAA+B;AAC/D,IAAM,4BAA4B,oBAAI,IAAY;AAE3C,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,SAAS,SAAmC;AAChE,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,SAAS,kBAAkB,IAAI,WAAW;AAChD,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AACA,QAAM,MAAM,GAAG,iBAAiB;AAChC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,QAAM,QAAS,MAAM,IAAI,KAAK;AAC9B,oBAAkB,IAAI,aAAa,KAAK;AACxC,4BAA0B,IAAI,WAAW;AACzC,QAAM,SAAS,oBAAI,IAAkB;AACrC,aAAW,QAAQ,OAAO;AACxB,WAAO,IAAI,KAAK,MAAM,IAAI;AAAA,EAC5B;AACA,sBAAoB,IAAI,aAAa,MAAM;AAC3C,SAAO;AACT;AAEA,eAAsB,QAAQ,MAAc,SAAwC;AAClF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,MAAI,0BAA0B,IAAI,WAAW,GAAG;AAC9C,UAAM,OAAO,kBAAkB,IAAI,WAAW;AAC9C,QAAI,MAAM;AACR,YAAMA,QAAO,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC7C,aAAOA,SAAQ;AAAA,IACjB;AAAA,EACF;AACA,QAAM,SAAS,oBAAoB,IAAI,WAAW;AAClD,QAAM,aAAa,QAAQ,IAAI,IAAI;AACnC,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,EACT;AACA,QAAM,MAAM,GAAG,iBAAiB,cAAc,mBAAmB,IAAI,CAAC;AACtE,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,yBAAyB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACzE;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,MAAI,CAAC,oBAAoB,IAAI,WAAW,GAAG;AACzC,wBAAoB,IAAI,aAAa,oBAAI,IAAI,CAAC;AAAA,EAChD;AACA,sBAAoB,IAAI,WAAW,EAAG,IAAI,MAAM,IAAI;AACpD,SAAO;AACT;","names":["post"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
var DISPATCH_API_BASE = "https://dispatch-cms.vercel.app";
|
|
3
3
|
var config = null;
|
|
4
|
+
var allPostsBySiteKey = /* @__PURE__ */ new Map();
|
|
5
|
+
var postBySlugBySiteKey = /* @__PURE__ */ new Map();
|
|
6
|
+
var fullListFetchedForSiteKey = /* @__PURE__ */ new Set();
|
|
4
7
|
function initDispatch(options) {
|
|
5
8
|
config = { siteKey: options.siteKey };
|
|
6
9
|
}
|
|
@@ -15,6 +18,10 @@ function getConfig() {
|
|
|
15
18
|
async function getPosts(siteKey) {
|
|
16
19
|
const { siteKey: key } = getConfig();
|
|
17
20
|
const resolvedKey = siteKey ?? key;
|
|
21
|
+
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
22
|
+
if (cached !== void 0) {
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
18
25
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
19
26
|
const res = await fetch(url, {
|
|
20
27
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -25,11 +32,31 @@ async function getPosts(siteKey) {
|
|
|
25
32
|
}
|
|
26
33
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
const posts = await res.json();
|
|
36
|
+
allPostsBySiteKey.set(resolvedKey, posts);
|
|
37
|
+
fullListFetchedForSiteKey.add(resolvedKey);
|
|
38
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const post of posts) {
|
|
40
|
+
bySlug.set(post.slug, post);
|
|
41
|
+
}
|
|
42
|
+
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
43
|
+
return posts;
|
|
29
44
|
}
|
|
30
45
|
async function getPost(slug, siteKey) {
|
|
31
46
|
const { siteKey: key } = getConfig();
|
|
32
47
|
const resolvedKey = siteKey ?? key;
|
|
48
|
+
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
49
|
+
const list = allPostsBySiteKey.get(resolvedKey);
|
|
50
|
+
if (list) {
|
|
51
|
+
const post2 = list.find((p) => p.slug === slug);
|
|
52
|
+
return post2 ?? null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
56
|
+
const cachedPost = bySlug?.get(slug);
|
|
57
|
+
if (cachedPost !== void 0) {
|
|
58
|
+
return cachedPost;
|
|
59
|
+
}
|
|
33
60
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
34
61
|
const res = await fetch(url, {
|
|
35
62
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -43,7 +70,12 @@ async function getPost(slug, siteKey) {
|
|
|
43
70
|
}
|
|
44
71
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
45
72
|
}
|
|
46
|
-
|
|
73
|
+
const post = await res.json();
|
|
74
|
+
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
75
|
+
postBySlugBySiteKey.set(resolvedKey, /* @__PURE__ */ new Map());
|
|
76
|
+
}
|
|
77
|
+
postBySlugBySiteKey.get(resolvedKey).set(slug, post);
|
|
78
|
+
return post;
|
|
47
79
|
}
|
|
48
80
|
export {
|
|
49
81
|
getConfig,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);\n }\n
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nconst allPostsBySiteKey = new Map<string, Post[]>();\nconst postBySlugBySiteKey = new Map<string, Map<string, Post>>();\nconst fullListFetchedForSiteKey = new Set<string>();\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const cached = allPostsBySiteKey.get(resolvedKey);\n if (cached !== undefined) {\n return cached;\n }\n const url = `${DISPATCH_API_BASE}/api/posts`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);\n }\n const posts = (await res.json()) as Post[];\n allPostsBySiteKey.set(resolvedKey, posts);\n fullListFetchedForSiteKey.add(resolvedKey);\n const bySlug = new Map<string, Post>();\n for (const post of posts) {\n bySlug.set(post.slug, post);\n }\n postBySlugBySiteKey.set(resolvedKey, bySlug);\n return posts;\n}\n\nexport async function getPost(slug: string, siteKey?: string): Promise<Post | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n if (fullListFetchedForSiteKey.has(resolvedKey)) {\n const list = allPostsBySiteKey.get(resolvedKey);\n if (list) {\n const post = list.find((p) => p.slug === slug);\n return post ?? null;\n }\n }\n const bySlug = postBySlugBySiteKey.get(resolvedKey);\n const cachedPost = bySlug?.get(slug);\n if (cachedPost !== undefined) {\n return cachedPost;\n }\n const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);\n }\n const post = (await res.json()) as Post;\n if (!postBySlugBySiteKey.has(resolvedKey)) {\n postBySlugBySiteKey.set(resolvedKey, new Map());\n }\n postBySlugBySiteKey.get(resolvedKey)!.set(slug, post);\n return post;\n}\n"],"mappings":";AAcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAEpC,IAAM,oBAAoB,oBAAI,IAAoB;AAClD,IAAM,sBAAsB,oBAAI,IAA+B;AAC/D,IAAM,4BAA4B,oBAAI,IAAY;AAE3C,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,SAAS,SAAmC;AAChE,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,SAAS,kBAAkB,IAAI,WAAW;AAChD,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AACA,QAAM,MAAM,GAAG,iBAAiB;AAChC,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,QAAM,QAAS,MAAM,IAAI,KAAK;AAC9B,oBAAkB,IAAI,aAAa,KAAK;AACxC,4BAA0B,IAAI,WAAW;AACzC,QAAM,SAAS,oBAAI,IAAkB;AACrC,aAAW,QAAQ,OAAO;AACxB,WAAO,IAAI,KAAK,MAAM,IAAI;AAAA,EAC5B;AACA,sBAAoB,IAAI,aAAa,MAAM;AAC3C,SAAO;AACT;AAEA,eAAsB,QAAQ,MAAc,SAAwC;AAClF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,MAAI,0BAA0B,IAAI,WAAW,GAAG;AAC9C,UAAM,OAAO,kBAAkB,IAAI,WAAW;AAC9C,QAAI,MAAM;AACR,YAAMA,QAAO,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC7C,aAAOA,SAAQ;AAAA,IACjB;AAAA,EACF;AACA,QAAM,SAAS,oBAAoB,IAAI,WAAW;AAClD,QAAM,aAAa,QAAQ,IAAI,IAAI;AACnC,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,EACT;AACA,QAAM,MAAM,GAAG,iBAAiB,cAAc,mBAAmB,IAAI,CAAC;AACtE,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,yBAAyB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACzE;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,MAAI,CAAC,oBAAoB,IAAI,WAAW,GAAG;AACzC,wBAAoB,IAAI,aAAa,oBAAI,IAAI,CAAC;AAAA,EAChD;AACA,sBAAoB,IAAI,WAAW,EAAG,IAAI,MAAM,IAAI;AACpD,SAAO;AACT;","names":["post"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dispatchcms/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Fetch published posts from a Dispatch CMS in Next.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsup",
|
|
17
|
-
"test": "tsx test/
|
|
17
|
+
"test": "tsx test/client.cache.test.ts",
|
|
18
|
+
"test:manual": "tsx test/getPage.test.ts"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/node": "^22.10.1",
|
package/src/client.ts
CHANGED
|
@@ -20,6 +20,10 @@ export type DispatchConfig = {
|
|
|
20
20
|
|
|
21
21
|
let config: DispatchConfig | null = null;
|
|
22
22
|
|
|
23
|
+
const allPostsBySiteKey = new Map<string, Post[]>();
|
|
24
|
+
const postBySlugBySiteKey = new Map<string, Map<string, Post>>();
|
|
25
|
+
const fullListFetchedForSiteKey = new Set<string>();
|
|
26
|
+
|
|
23
27
|
export function initDispatch(options: DispatchConfig): void {
|
|
24
28
|
config = { siteKey: options.siteKey };
|
|
25
29
|
}
|
|
@@ -36,6 +40,10 @@ export function getConfig(): { siteKey: string } {
|
|
|
36
40
|
export async function getPosts(siteKey?: string): Promise<Post[]> {
|
|
37
41
|
const { siteKey: key } = getConfig();
|
|
38
42
|
const resolvedKey = siteKey ?? key;
|
|
43
|
+
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
44
|
+
if (cached !== undefined) {
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
39
47
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
40
48
|
const res = await fetch(url, {
|
|
41
49
|
headers: { "X-Site-Key": resolvedKey },
|
|
@@ -46,12 +54,32 @@ export async function getPosts(siteKey?: string): Promise<Post[]> {
|
|
|
46
54
|
}
|
|
47
55
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
const posts = (await res.json()) as Post[];
|
|
58
|
+
allPostsBySiteKey.set(resolvedKey, posts);
|
|
59
|
+
fullListFetchedForSiteKey.add(resolvedKey);
|
|
60
|
+
const bySlug = new Map<string, Post>();
|
|
61
|
+
for (const post of posts) {
|
|
62
|
+
bySlug.set(post.slug, post);
|
|
63
|
+
}
|
|
64
|
+
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
65
|
+
return posts;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
export async function getPost(slug: string, siteKey?: string): Promise<Post | null> {
|
|
53
69
|
const { siteKey: key } = getConfig();
|
|
54
70
|
const resolvedKey = siteKey ?? key;
|
|
71
|
+
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
72
|
+
const list = allPostsBySiteKey.get(resolvedKey);
|
|
73
|
+
if (list) {
|
|
74
|
+
const post = list.find((p) => p.slug === slug);
|
|
75
|
+
return post ?? null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
79
|
+
const cachedPost = bySlug?.get(slug);
|
|
80
|
+
if (cachedPost !== undefined) {
|
|
81
|
+
return cachedPost;
|
|
82
|
+
}
|
|
55
83
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
56
84
|
const res = await fetch(url, {
|
|
57
85
|
headers: { "X-Site-Key": resolvedKey },
|
|
@@ -65,5 +93,10 @@ export async function getPost(slug: string, siteKey?: string): Promise<Post | nu
|
|
|
65
93
|
}
|
|
66
94
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
67
95
|
}
|
|
68
|
-
|
|
96
|
+
const post = (await res.json()) as Post;
|
|
97
|
+
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
98
|
+
postBySlugBySiteKey.set(resolvedKey, new Map());
|
|
99
|
+
}
|
|
100
|
+
postBySlugBySiteKey.get(resolvedKey)!.set(slug, post);
|
|
101
|
+
return post;
|
|
69
102
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { initDispatch, getPosts, getPost } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const mockPost = (slug: string, title: string) => ({
|
|
4
|
+
id: `id-${slug}`,
|
|
5
|
+
created_at: "",
|
|
6
|
+
published_at: "",
|
|
7
|
+
updated_at: "",
|
|
8
|
+
site_id: "",
|
|
9
|
+
title,
|
|
10
|
+
slug,
|
|
11
|
+
content: null,
|
|
12
|
+
excerpt: null,
|
|
13
|
+
featured_image: null,
|
|
14
|
+
published: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type FetchCall = { url: string };
|
|
18
|
+
const fetchCalls: FetchCall[] = [];
|
|
19
|
+
let listResponse: ReturnType<typeof mockPost>[] = [];
|
|
20
|
+
let singleResponse: ReturnType<typeof mockPost> | null = null;
|
|
21
|
+
let singleStatus = 200;
|
|
22
|
+
|
|
23
|
+
function mockFetch(url: string, options?: RequestInit): Promise<Response> {
|
|
24
|
+
fetchCalls.push({ url });
|
|
25
|
+
const u = new URL(url);
|
|
26
|
+
if (u.pathname === "/api/posts") {
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
ok: true,
|
|
29
|
+
status: 200,
|
|
30
|
+
json: () => Promise.resolve(listResponse),
|
|
31
|
+
} as Response);
|
|
32
|
+
}
|
|
33
|
+
const slugMatch = u.pathname.match(/^\/api\/posts\/(.+)$/);
|
|
34
|
+
if (slugMatch) {
|
|
35
|
+
const slug = decodeURIComponent(slugMatch[1]);
|
|
36
|
+
if (singleStatus === 404) {
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 404,
|
|
40
|
+
} as Response);
|
|
41
|
+
}
|
|
42
|
+
return Promise.resolve({
|
|
43
|
+
ok: true,
|
|
44
|
+
status: 200,
|
|
45
|
+
json: () => Promise.resolve(singleResponse ?? mockPost(slug, "Single")),
|
|
46
|
+
} as Response);
|
|
47
|
+
}
|
|
48
|
+
return Promise.reject(new Error(`Unexpected URL: ${url}`));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assert(condition: boolean, message: string): void {
|
|
52
|
+
if (!condition) {
|
|
53
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function run(): Promise<void> {
|
|
58
|
+
const originalFetch = globalThis.fetch;
|
|
59
|
+
globalThis.fetch = mockFetch as typeof fetch;
|
|
60
|
+
|
|
61
|
+
initDispatch({ siteKey: "pk_test" });
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Test 1: Second getPosts() with same siteKey does not call fetch
|
|
65
|
+
fetchCalls.length = 0;
|
|
66
|
+
listResponse = [mockPost("a", "Post A"), mockPost("b", "Post B")];
|
|
67
|
+
const list1 = await getPosts("pk_cache1");
|
|
68
|
+
const list2 = await getPosts("pk_cache1");
|
|
69
|
+
assert(fetchCalls.length === 1, "getPosts should be fetched once, then served from cache");
|
|
70
|
+
assert(list1.length === 2 && list2.length === 2, "both calls return same list");
|
|
71
|
+
assert(list1[0].slug === "a" && list2[0].slug === "a", "content matches");
|
|
72
|
+
|
|
73
|
+
// Test 2: After getPosts(), getPost(slug) for slug in list does not call fetch
|
|
74
|
+
fetchCalls.length = 0;
|
|
75
|
+
listResponse = [mockPost("my-slug", "My Post")];
|
|
76
|
+
await getPosts("pk_cache2");
|
|
77
|
+
const post = await getPost("my-slug", "pk_cache2");
|
|
78
|
+
assert(fetchCalls.length === 1, "only getPosts should trigger fetch, getPost uses cache");
|
|
79
|
+
assert(post !== null && post.slug === "my-slug" && post.title === "My Post", "getPost returns correct post from list");
|
|
80
|
+
|
|
81
|
+
// Test 3: After getPosts(), getPost(slug) for slug not in list returns null without fetch
|
|
82
|
+
fetchCalls.length = 0;
|
|
83
|
+
listResponse = [mockPost("other", "Other")];
|
|
84
|
+
await getPosts("pk_cache3");
|
|
85
|
+
const missing = await getPost("nonexistent", "pk_cache3");
|
|
86
|
+
assert(fetchCalls.length === 1, "getPost(nonexistent) should not fetch when full list is cached");
|
|
87
|
+
assert(missing === null, "getPost returns null for missing slug");
|
|
88
|
+
|
|
89
|
+
// Test 4: getPost(slug) first triggers one fetch; then getPosts() triggers one fetch; then getPost(slug) uses cache
|
|
90
|
+
fetchCalls.length = 0;
|
|
91
|
+
listResponse = [];
|
|
92
|
+
singleResponse = mockPost("first", "First Post");
|
|
93
|
+
const singleFirst = await getPost("first", "pk_cache4");
|
|
94
|
+
assert(fetchCalls.length === 1 && singleFirst?.slug === "first", "first getPost fetches");
|
|
95
|
+
listResponse = [mockPost("first", "First Post")];
|
|
96
|
+
await getPosts("pk_cache4");
|
|
97
|
+
assert(fetchCalls.length === 2, "getPosts triggers second fetch");
|
|
98
|
+
const singleCached = await getPost("first", "pk_cache4");
|
|
99
|
+
assert(fetchCalls.length === 2, "getPost after getPosts does not fetch again");
|
|
100
|
+
assert(singleCached !== null && singleCached.slug === "first", "getPost returns post from list cache");
|
|
101
|
+
} finally {
|
|
102
|
+
globalThis.fetch = originalFetch;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
run()
|
|
107
|
+
.then(() => {
|
|
108
|
+
console.log("All cache tests passed.");
|
|
109
|
+
})
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
console.error(err);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|