@faststore/core 3.100.6 → 3.100.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.
Files changed (46) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +23 -23
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +2 -2
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  9. package/.next/cache/webpack/server-production/0.pack +2 -2
  10. package/.next/cache/webpack/server-production/index.pack +0 -0
  11. package/.next/prerender-manifest.js +1 -1
  12. package/.next/prerender-manifest.json +1 -1
  13. package/.next/react-loadable-manifest.json +2 -2
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/948.js +2 -2
  16. package/.next/server/functions-config-manifest.json +1 -1
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  19. package/.next/server/pages/api/preview.js +1 -1
  20. package/.next/server/pages/en-US/404.html +2 -2
  21. package/.next/server/pages/en-US/500.html +2 -2
  22. package/.next/server/pages/en-US/checkout.html +2 -2
  23. package/.next/server/pages/en-US/login.html +2 -2
  24. package/.next/server/pages/en-US/password-protection.html +2 -2
  25. package/.next/server/pages/en-US/s.html +2 -2
  26. package/.next/server/pages/en-US.html +2 -2
  27. package/.next/server/pages-manifest.json +1 -1
  28. package/.next/static/{TfWW2xib3Snr8xF4Ldqws → 3we-qu8I2EmorLqlDX8oP}/_buildManifest.js +1 -1
  29. package/.next/static/chunks/7783.017603f0dd9e1dd7.js +1 -0
  30. package/.next/static/chunks/pages/{[...slug]-cf9e3bf4febd21c3.js → [...slug]-d2b8abf5b0c0bb19.js} +1 -1
  31. package/.next/static/chunks/{webpack-6a844a3f6a390f62.js → webpack-62bcb2a452e15476.js} +1 -1
  32. package/.next/trace +156 -156
  33. package/.turbo/turbo-build.log +9 -9
  34. package/.turbo/turbo-test.log +12 -10
  35. package/CHANGELOG.md +15 -0
  36. package/package.json +2 -2
  37. package/src/server/cms/index.ts +59 -7
  38. package/src/server/cms/pdp.ts +10 -5
  39. package/src/server/cms/plp.ts +10 -5
  40. package/src/server/content/promiseCache.ts +68 -0
  41. package/src/server/content/service.ts +93 -27
  42. package/test/server/cms/index.test.ts +112 -8
  43. package/test/server/content/promiseCache.test.ts +150 -0
  44. package/test/server/content/service.test.ts +446 -0
  45. package/.next/static/chunks/3802.904f302bf009f795.js +0 -1
  46. /package/.next/static/{TfWW2xib3Snr8xF4Ldqws → 3we-qu8I2EmorLqlDX8oP}/_ssgManifest.js +0 -0
@@ -0,0 +1,150 @@
1
+ import { jest } from '@jest/globals'
2
+
3
+ import { createPromiseCache } from '../../../src/server/content/promiseCache'
4
+
5
+ describe('createPromiseCache', () => {
6
+ afterEach(() => {
7
+ jest.useRealTimers()
8
+ })
9
+
10
+ it('shares one in-flight load across concurrent callers', async () => {
11
+ const cache = createPromiseCache<string>(1000)
12
+ const load = jest.fn(async () => 'value')
13
+
14
+ const results = await Promise.all([
15
+ cache.get('key', load),
16
+ cache.get('key', load),
17
+ cache.get('key', load),
18
+ ])
19
+
20
+ expect(load).toHaveBeenCalledTimes(1)
21
+ expect(results).toEqual(['value', 'value', 'value'])
22
+ })
23
+
24
+ it('keeps separate entries per key', async () => {
25
+ const cache = createPromiseCache<string>(1000)
26
+ const load = jest.fn(async () => 'value')
27
+
28
+ await cache.get('a', load)
29
+ await cache.get('b', load)
30
+
31
+ expect(load).toHaveBeenCalledTimes(2)
32
+ })
33
+
34
+ it('loads again after the TTL', async () => {
35
+ jest.useFakeTimers()
36
+ const cache = createPromiseCache<string>(1000)
37
+ const load = jest.fn(async () => 'value')
38
+
39
+ await cache.get('key', load)
40
+ jest.advanceTimersByTime(999)
41
+ await cache.get('key', load)
42
+ expect(load).toHaveBeenCalledTimes(1)
43
+
44
+ jest.advanceTimersByTime(1)
45
+ await cache.get('key', load)
46
+ expect(load).toHaveBeenCalledTimes(2)
47
+ })
48
+
49
+ it('does not cache failures', async () => {
50
+ const cache = createPromiseCache<string>(1000)
51
+ const error = new Error('boom')
52
+ const load = jest
53
+ .fn<() => Promise<string>>()
54
+ .mockRejectedValueOnce(error)
55
+ .mockResolvedValueOnce('value')
56
+
57
+ await expect(cache.get('key', load)).rejects.toBe(error)
58
+ await expect(cache.get('key', load)).resolves.toBe('value')
59
+ expect(load).toHaveBeenCalledTimes(2)
60
+ })
61
+
62
+ it('does not let a late failure evict a newer load', async () => {
63
+ jest.useFakeTimers()
64
+ const cache = createPromiseCache<string>(1000)
65
+ let rejectFirst: (err: Error) => void = () => {}
66
+ const load = jest
67
+ .fn<() => Promise<string>>()
68
+ .mockImplementationOnce(
69
+ () =>
70
+ new Promise((_, reject) => {
71
+ rejectFirst = reject
72
+ })
73
+ )
74
+ .mockResolvedValue('value')
75
+
76
+ const first = cache.get('key', load)
77
+ const firstResult = expect(first).rejects.toThrow('late failure')
78
+ jest.advanceTimersByTime(1000)
79
+ await cache.get('key', load)
80
+
81
+ rejectFirst(new Error('late failure'))
82
+ await firstResult
83
+ await cache.get('key', load)
84
+
85
+ expect(load).toHaveBeenCalledTimes(2)
86
+ })
87
+
88
+ it('does not keep values rejected by shouldCache', async () => {
89
+ const cache = createPromiseCache<string>(1000)
90
+ const load = jest.fn(async () => '')
91
+ const shouldCache = (value: string) => value !== ''
92
+
93
+ const concurrent = await Promise.all([
94
+ cache.get('key', load, shouldCache),
95
+ cache.get('key', load, shouldCache),
96
+ ])
97
+ await cache.get('key', load, shouldCache)
98
+
99
+ expect(concurrent).toEqual(['', ''])
100
+ expect(load).toHaveBeenCalledTimes(2)
101
+ })
102
+
103
+ it('keeps values accepted by shouldCache', async () => {
104
+ const cache = createPromiseCache<string>(1000)
105
+ const load = jest.fn(async () => 'value')
106
+
107
+ await cache.get('key', load, (value) => value !== '')
108
+ await cache.get('key', load, (value) => value !== '')
109
+
110
+ expect(load).toHaveBeenCalledTimes(1)
111
+ })
112
+
113
+ it('drops expired entries when a new key is stored', async () => {
114
+ jest.useFakeTimers()
115
+ const cache = createPromiseCache<string>(1000)
116
+ const load = jest.fn(async () => 'value')
117
+
118
+ await cache.get('old-version', load)
119
+ await cache.get('other', load)
120
+ expect(cache.size).toBe(2)
121
+
122
+ jest.advanceTimersByTime(1000)
123
+ await cache.get('new-version', load)
124
+
125
+ expect(cache.size).toBe(1)
126
+ })
127
+
128
+ it('keeps entries that have not expired when a new key is stored', async () => {
129
+ jest.useFakeTimers()
130
+ const cache = createPromiseCache<string>(1000)
131
+ const load = jest.fn(async () => 'value')
132
+
133
+ await cache.get('a', load)
134
+ jest.advanceTimersByTime(999)
135
+ await cache.get('b', load)
136
+
137
+ expect(cache.size).toBe(2)
138
+ })
139
+
140
+ it('drops every entry on clear', async () => {
141
+ const cache = createPromiseCache<string>(1000)
142
+ const load = jest.fn(async () => 'value')
143
+
144
+ await cache.get('key', load)
145
+ cache.clear()
146
+ await cache.get('key', load)
147
+
148
+ expect(load).toHaveBeenCalledTimes(2)
149
+ })
150
+ })
@@ -0,0 +1,446 @@
1
+ import type { ContentEntry, EntryPathParams } from '@vtex/client-cp'
2
+
3
+ import { contentSource } from '../../../discovery.config'
4
+ import { clientCMS, cmsPageCache } from '../../../src/server/cms'
5
+ import { ContentService } from '../../../src/server/content/service'
6
+ import {
7
+ type ContentParams,
8
+ ContentSourceType,
9
+ } from '../../../src/server/content/types'
10
+
11
+ jest.mock('../../../discovery.config', () => {
12
+ const actual = jest.requireActual('../../../discovery.config')
13
+ const base = actual.default ?? actual
14
+ const mutableContentSource = { ...base.contentSource }
15
+
16
+ return {
17
+ __esModule: true,
18
+ ...actual,
19
+ default: { ...base, contentSource: mutableContentSource },
20
+ contentSource: mutableContentSource,
21
+ }
22
+ })
23
+
24
+ const DEFAULT_CONTENT_SOURCE_TYPE = contentSource.type
25
+
26
+ beforeEach(() => {
27
+ contentSource.type = ContentSourceType.ContentPlatform
28
+ })
29
+
30
+ afterAll(() => {
31
+ contentSource.type = DEFAULT_CONTENT_SOURCE_TYPE
32
+ })
33
+
34
+ const NOT_FOUND = 'Request failed with status code 404'
35
+ const FIVE_MINUTES = 5 * 60 * 1000
36
+
37
+ const entryData: Record<string, Record<string, unknown>> = {
38
+ generic: { sections: [{ name: 'Generic' }] },
39
+ collection: {
40
+ settings: { template: { value: '/colecao' } },
41
+ sections: [{ name: 'Collection' }],
42
+ },
43
+ other: {
44
+ settings: { template: { value: '/outra' } },
45
+ sections: [{ name: 'Other' }],
46
+ },
47
+ }
48
+
49
+ const toEntry = (id: string) => ({ id, name: id }) as ContentEntry
50
+
51
+ function createFakeClientCP(pages: string[][]) {
52
+ return {
53
+ listEntries: jest.fn(async (_params: EntryPathParams, scroll?: string) => {
54
+ const index = scroll ? Number(scroll) : 0
55
+ return {
56
+ entries: pages[index].map(toEntry),
57
+ scroll: index + 1 < pages.length ? String(index + 1) : null,
58
+ }
59
+ }),
60
+ getEntry: jest.fn(async ({ entryId }: EntryPathParams) => ({
61
+ ...entryData[entryId as string],
62
+ })),
63
+ previewEntryById: jest.fn(async ({ entryId }: EntryPathParams) => ({
64
+ ...entryData[entryId as string],
65
+ })),
66
+ getEntryBySlug: jest.fn(
67
+ async (_params: EntryPathParams): Promise<Record<string, unknown>> => {
68
+ throw new Error(NOT_FOUND)
69
+ }
70
+ ),
71
+ previewEntryBySlug: jest.fn(
72
+ async (_params: EntryPathParams): Promise<Record<string, unknown>> => {
73
+ throw new Error(NOT_FOUND)
74
+ }
75
+ ),
76
+ }
77
+ }
78
+
79
+ function createServiceWithClient(
80
+ pages: string[][] = [['generic', 'collection', 'other']]
81
+ ) {
82
+ const service = new ContentService()
83
+ const clientCP = createFakeClientCP(pages)
84
+ Object.defineProperty(service, 'clientCP', { value: clientCP })
85
+ return { service, clientCP }
86
+ }
87
+
88
+ describe('ContentService Content Platform template list cache', () => {
89
+ beforeEach(() => {
90
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
91
+ })
92
+
93
+ afterEach(() => {
94
+ jest.restoreAllMocks()
95
+ jest.useRealTimers()
96
+ })
97
+
98
+ it('shares one list and entry fetch across concurrent PLPs with different slugs', async () => {
99
+ const { service, clientCP } = createServiceWithClient()
100
+
101
+ const [collectionPage, otherPage, unknownPage] = await Promise.all([
102
+ service.getPlpContent({ slug: 'colecao/camisetas' }, []),
103
+ service.getPlpContent({ slug: 'outra' }, []),
104
+ service.getPlpContent({ slug: 'nao-existe' }, []),
105
+ ])
106
+
107
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
108
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(3)
109
+ expect(collectionPage.sections).toEqual([{ name: 'Collection' }])
110
+ expect(otherPage.sections).toEqual([{ name: 'Other' }])
111
+ expect(unknownPage.sections).toEqual([{ name: 'Generic' }])
112
+ })
113
+
114
+ it('reuses the cached list within the TTL', async () => {
115
+ const { service, clientCP } = createServiceWithClient()
116
+
117
+ await service.getPlpContent({ slug: 'colecao' }, [])
118
+ await service.getPlpContent({ slug: 'outra' }, [])
119
+
120
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
121
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(3)
122
+ })
123
+
124
+ it('still prefers a direct slug match over the cached list', async () => {
125
+ const { service, clientCP } = createServiceWithClient()
126
+ await service.getPlpContent({ slug: 'colecao' }, [])
127
+
128
+ clientCP.getEntryBySlug.mockResolvedValueOnce({
129
+ sections: [{ name: 'Direct' }],
130
+ })
131
+ const page = await service.getPlpContent({ slug: 'colecao' }, [])
132
+
133
+ expect(page.sections).toEqual([{ name: 'Direct' }])
134
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
135
+ })
136
+
137
+ it('consumes every scroll page once', async () => {
138
+ const { service, clientCP } = createServiceWithClient([
139
+ ['generic'],
140
+ ['collection'],
141
+ ['other'],
142
+ ])
143
+
144
+ await Promise.all([
145
+ service.getMultipleContent({ contentType: 'plp' }),
146
+ service.getMultipleContent({ contentType: 'plp' }),
147
+ ])
148
+ const { data } = await service.getMultipleContent({ contentType: 'plp' })
149
+
150
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
151
+ expect(clientCP.listEntries.mock.calls.map(([, scroll]) => scroll)).toEqual(
152
+ [undefined, '1', '2']
153
+ )
154
+ expect(data.map((entry) => entry.id)).toEqual([
155
+ 'generic',
156
+ 'collection',
157
+ 'other',
158
+ ])
159
+ })
160
+
161
+ it('keeps separate entries per content type', async () => {
162
+ const { service, clientCP } = createServiceWithClient()
163
+
164
+ await service.getMultipleContent({ contentType: 'plp' })
165
+ await service.getMultipleContent({ contentType: 'pdp' })
166
+ await service.getMultipleContent({ contentType: 'pdp' })
167
+
168
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
169
+ expect(
170
+ clientCP.listEntries.mock.calls.map(([params]) => params.contentType)
171
+ ).toEqual(['plp', 'pdp'])
172
+ })
173
+
174
+ it('bypasses the cache for previews', async () => {
175
+ const { service, clientCP } = createServiceWithClient()
176
+ const previewParams: ContentParams = {
177
+ contentType: 'plp',
178
+ previewData: {
179
+ contentType: 'plp',
180
+ documentId: 'collection',
181
+ versionId: 'version-1',
182
+ },
183
+ }
184
+
185
+ await service.getMultipleContent({ contentType: 'plp' })
186
+ await service.getMultipleContent(previewParams)
187
+ await service.getMultipleContent(previewParams)
188
+ await service.getMultipleContent({ contentType: 'plp' })
189
+
190
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
191
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(3)
192
+ expect(clientCP.previewEntryById).toHaveBeenCalledTimes(6)
193
+ })
194
+
195
+ it('does not cache failures', async () => {
196
+ const { service, clientCP } = createServiceWithClient()
197
+ const error = new Error('Request failed with status code 500')
198
+ clientCP.listEntries.mockRejectedValueOnce(error)
199
+
200
+ await expect(
201
+ service.getMultipleContent({ contentType: 'plp' })
202
+ ).rejects.toBe(error)
203
+ const { data } = await service.getMultipleContent({ contentType: 'plp' })
204
+
205
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
206
+ expect(data).toHaveLength(3)
207
+ })
208
+
209
+ it('does not let a late failure evict a newer fetch', async () => {
210
+ jest.useFakeTimers()
211
+ const { service, clientCP } = createServiceWithClient()
212
+ let rejectFirst: (err: Error) => void = () => {}
213
+ clientCP.listEntries.mockImplementationOnce(
214
+ () =>
215
+ new Promise((_, reject) => {
216
+ rejectFirst = reject
217
+ })
218
+ )
219
+
220
+ const first = service.getMultipleContent({ contentType: 'plp' })
221
+ const firstResult = expect(first).rejects.toThrow('late failure')
222
+ jest.advanceTimersByTime(FIVE_MINUTES)
223
+ await service.getMultipleContent({ contentType: 'plp' })
224
+
225
+ rejectFirst(new Error('late failure'))
226
+ await firstResult
227
+ await service.getMultipleContent({ contentType: 'plp' })
228
+
229
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
230
+ })
231
+
232
+ it('fetches the list again after the TTL', async () => {
233
+ jest.useFakeTimers()
234
+ const { service, clientCP } = createServiceWithClient()
235
+
236
+ await service.getMultipleContent({ contentType: 'plp' })
237
+ jest.advanceTimersByTime(FIVE_MINUTES - 1)
238
+ await service.getMultipleContent({ contentType: 'plp' })
239
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
240
+
241
+ jest.advanceTimersByTime(1)
242
+ await service.getMultipleContent({ contentType: 'plp' })
243
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
244
+ })
245
+ })
246
+
247
+ describe('ContentService Content Platform single entry cache', () => {
248
+ beforeEach(() => {
249
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
250
+ })
251
+
252
+ afterEach(() => {
253
+ jest.restoreAllMocks()
254
+ jest.useRealTimers()
255
+ })
256
+
257
+ it('shares one list and entry fetch across concurrent pages', async () => {
258
+ const { service, clientCP } = createServiceWithClient([['generic']])
259
+
260
+ const results = await Promise.all(
261
+ Array.from({ length: 20 }, () =>
262
+ service.getSingleContent({ contentType: 'globalSections' })
263
+ )
264
+ )
265
+
266
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
267
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
268
+ for (const result of results) {
269
+ expect(result.sections).toEqual([{ name: 'Generic' }])
270
+ }
271
+ })
272
+
273
+ it('keeps separate entries per content type', async () => {
274
+ const { service, clientCP } = createServiceWithClient([['generic']])
275
+
276
+ await service.getSingleContent({ contentType: 'globalSections' })
277
+ await service.getSingleContent({ contentType: 'globalHeaderSections' })
278
+ await service.getSingleContent({ contentType: 'globalSections' })
279
+
280
+ expect(
281
+ clientCP.listEntries.mock.calls.map(([params]) => params.contentType)
282
+ ).toEqual(['globalSections', 'globalHeaderSections'])
283
+ })
284
+
285
+ it('caches an empty optional content type', async () => {
286
+ const { service, clientCP } = createServiceWithClient([[]])
287
+
288
+ const first = await service.getSingleContent({
289
+ contentType: 'globalFooterSections',
290
+ })
291
+ await service.getSingleContent({ contentType: 'globalFooterSections' })
292
+
293
+ expect(first).toEqual({})
294
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
295
+ expect(clientCP.getEntry).not.toHaveBeenCalled()
296
+ })
297
+
298
+ it('does not cache an empty required content type', async () => {
299
+ const { service, clientCP } = createServiceWithClient([[]])
300
+
301
+ const first = await service.getSingleContent({
302
+ contentType: 'globalSections',
303
+ })
304
+ await service.getSingleContent({ contentType: 'globalSections' })
305
+
306
+ expect(first).toEqual({})
307
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
308
+ })
309
+
310
+ it('picks up a required content type published after an empty lookup', async () => {
311
+ const { service, clientCP } = createServiceWithClient([['generic']])
312
+ clientCP.listEntries.mockResolvedValueOnce({ entries: [], scroll: null })
313
+
314
+ const before = await service.getSingleContent({
315
+ contentType: 'globalSections',
316
+ })
317
+ const after = await service.getSingleContent({
318
+ contentType: 'globalSections',
319
+ })
320
+ await service.getSingleContent({ contentType: 'globalSections' })
321
+
322
+ expect(before).toEqual({})
323
+ expect(after.sections).toEqual([{ name: 'Generic' }])
324
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
325
+ })
326
+
327
+ it('does not cache lookups by slug', async () => {
328
+ const { service, clientCP } = createServiceWithClient([['generic']])
329
+
330
+ await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
331
+ await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
332
+
333
+ expect(clientCP.getEntryBySlug).toHaveBeenCalledTimes(2)
334
+ expect(clientCP.listEntries).not.toHaveBeenCalled()
335
+ })
336
+
337
+ it('does not cache filtered lookups', async () => {
338
+ const { service, clientCP } = createServiceWithClient([['generic']])
339
+ const params: ContentParams = {
340
+ contentType: 'globalSections',
341
+ filters: { page: 1 },
342
+ }
343
+
344
+ await service.getSingleContent(params)
345
+ await service.getSingleContent(params)
346
+
347
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
348
+ })
349
+
350
+ it('bypasses the cache for branch previews', async () => {
351
+ const { service, clientCP } = createServiceWithClient([['generic']])
352
+ const branchPreview: ContentParams = {
353
+ contentType: 'globalSections',
354
+ previewData: {
355
+ contentType: 'landingPage',
356
+ documentId: 'some-landing-page',
357
+ versionId: 'branch-1',
358
+ },
359
+ }
360
+
361
+ await service.getSingleContent({ contentType: 'globalSections' })
362
+ await service.getSingleContent(branchPreview)
363
+ await service.getSingleContent(branchPreview)
364
+ await service.getSingleContent({ contentType: 'globalSections' })
365
+
366
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
367
+ expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
368
+ expect(clientCP.previewEntryById).toHaveBeenCalledTimes(2)
369
+ })
370
+
371
+ it('does not cache a multiple-entries error', async () => {
372
+ const { service, clientCP } = createServiceWithClient([
373
+ ['generic', 'collection'],
374
+ ])
375
+
376
+ await expect(
377
+ service.getSingleContent({ contentType: 'globalSections' })
378
+ ).rejects.toThrow()
379
+ await expect(
380
+ service.getSingleContent({ contentType: 'globalSections' })
381
+ ).rejects.toThrow()
382
+
383
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
384
+ })
385
+
386
+ it('fetches again after the TTL', async () => {
387
+ jest.useFakeTimers()
388
+ const { service, clientCP } = createServiceWithClient([['generic']])
389
+
390
+ await service.getSingleContent({ contentType: 'globalSections' })
391
+ jest.advanceTimersByTime(FIVE_MINUTES)
392
+ await service.getSingleContent({ contentType: 'globalSections' })
393
+
394
+ expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
395
+ })
396
+ })
397
+
398
+ describe('ContentService Headless CMS single content cache', () => {
399
+ const locator = {
400
+ contentType: 'globalSections',
401
+ documentId: 'doc-1',
402
+ versionId: 'version-1',
403
+ }
404
+
405
+ beforeEach(() => {
406
+ contentSource.type = DEFAULT_CONTENT_SOURCE_TYPE
407
+ cmsPageCache.clear()
408
+ jest.spyOn(clientCMS, 'getCMSPage').mockResolvedValue({
409
+ id: 'doc-1',
410
+ name: 'doc-1',
411
+ status: 'published',
412
+ type: 'globalSections',
413
+ releaseId: 'release-1',
414
+ sections: [{ name: 'Children' }],
415
+ } as Awaited<ReturnType<typeof clientCMS.getCMSPage>>)
416
+ })
417
+
418
+ afterEach(() => {
419
+ jest.restoreAllMocks()
420
+ })
421
+
422
+ it('reuses a published document fetched by locator', async () => {
423
+ const service = new ContentService()
424
+
425
+ await Promise.all([
426
+ service.getSingleContent(locator),
427
+ service.getSingleContent(locator),
428
+ ])
429
+ await service.getSingleContent(locator)
430
+
431
+ expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(1)
432
+ })
433
+
434
+ it('fetches the document on every preview request', async () => {
435
+ const service = new ContentService()
436
+ const previewParams: ContentParams = {
437
+ ...locator,
438
+ previewData: locator,
439
+ }
440
+
441
+ await service.getSingleContent(previewParams)
442
+ await service.getSingleContent(previewParams)
443
+
444
+ expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(2)
445
+ })
446
+ })
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3802],{35095:function(e,t,n){"use strict";var r=n(77900),i=n(41325),o=n(90427),s=n.n(o),c=n(31549);function Incentives(e){var{incentives:t,label:n}=e;return(0,c.jsx)(i.Z,{className:"".concat(s().section," section-incentives layout__section"),children:(0,c.jsx)(r.Z,{incentives:t,colored:!0,label:null!=n?n:""})})}Incentives.$componentKey="Incentives",t.Z=Incentives},3802:function(e,t,n){"use strict";n.r(t),n.d(t,{default:function(){return LandingPage},getLandingPageBySlug:function(){return X}});var r,i,o,s,c,a,l,d,p,u,v,f=n(74988),g=n(33067),y=n(35935),b=n(27322),P=n(19173),O=n(35949),j=n(93143),h=n(94173),m=n(25758),w=n(33882),S=n(35095),_=n(31841),I=n(80552),E=n(45859),C=n(76924),T=n(23091),Z=n(43412),x=n.n(Z);let MissingContentError=class MissingContentError extends Error{constructor(e){super("Missing content on the ".concat(Z.contentSource.type," for content type ").concat(e.contentType,". Add content before proceeding. Context: ").concat(JSON.stringify(e,null,2)))}};var D=n(56272),L=n(34235),B=n(18626),M=n.n(B),K=n(72692);let MultipleContentError=class MultipleContentError extends Error{constructor(e){super("Multiple content defined on the ".concat(Z.contentSource.type," for content type ").concat(e.contentType,". Remove duplicated content before proceeding. Context: ").concat(JSON.stringify(e,null,2)))}};var k=n(4018);function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function _objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach(function(t){(0,g.Z)(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}var isLocator=e=>"string"==typeof e.contentType&&("string"==typeof e.releaseId||"string"==typeof e.documentId||"string"==typeof e.versionId),N=new K.Z({workspace:x().api.workspace,tenant:x().api.storeId,builder:null!==(u=null===(v=x().contentSource)||void 0===v?void 0:v.project)&&void 0!==u?u:"faststore",host:(0,k.Y1)(x().storeUrl)}),F=(r=(0,f.Z)(function*(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N;if(isLocator(e))return yield t.getCMSPage(e).then(e=>({data:[e]}));var n=[],r=yield t.getCMSPagesByContentType(e.contentType,_objectSpread(_objectSpread({},e.filters),{},{page:1,perPage:10}));n.push(...r.data);var i=Array.from({length:Math.ceil(r.totalItems/10)-1},(e,t)=>t+2);return r.totalItems>n.length&&(yield Promise.all(i.map(n=>t.getCMSPagesByContentType(e.contentType,_objectSpread(_objectSpread({},e.filters),{},{page:n,perPage:10}))))).forEach(e=>{n.push(...e.data)}),{data:n}}),function(e){return r.apply(this,arguments)}),W=(i=(0,f.Z)(function*(e){var t=(yield F(e)).data;if(t.length>1)throw new MultipleContentError(e);return t[0]}),function(e){return i.apply(this,arguments)});function normalizePLPSlug(e){var t=e.replace(/^\/+|\/+$/g,"");return"/"+(t=t.replace(/\/+/g,"/").replace(/\s+/g,"")).toLowerCase()}function findPLPTemplateBySlug(e,t,n){return e.find(e=>{if(!(null!==(r=e.settings)&&void 0!==r&&null!==(i=r.template)&&void 0!==i&&i.value))return!1;var r,i,o,s,c=normalizePLPSlug(null===(o=e.settings)||void 0===o?void 0:null===(s=o.template)||void 0===s?void 0:s.value);return c===t||function(e){var{rewrites:t,templateValue:n,slug:r}=e;return!!t&&!!(Array.isArray(t)?t:[...(null==t?void 0:t.beforeFiles)||[],...(null==t?void 0:t.afterFiles)||[],...(null==t?void 0:t.fallback)||[]]).find(e=>e.source===n&&e.destination===r)}({rewrites:n,templateValue:c,slug:t})})}function findBestPLPTemplate(e,t,n){for(var r=normalizePLPSlug(t),i=findPLPTemplateBySlug(e,r,n);!i&&-1!==r.lastIndexOf("/");)i=findPLPTemplateBySlug(e,r=r.substring(0,r.lastIndexOf("/")),n);return i||e.find(e=>{var t,n;return!(null!==(t=e.settings)&&void 0!==t&&null!==(n=t.template)&&void 0!==n&&n.value)})||e[0]}var getSlugsWithoutSkuIdFromPDP=e=>{for(var t=[],n=e,r=n.match(/-\d+\/p$/);r;){var i=n.replace(/-\d+\/p$/,"/p");t.push(i),r=(n=i).match(/-\d+\/p$/)}return t};function findBestPDPTemplate(e,t){for(var n of function(e){var{itemListElement:t=[]}=e,n=[...t].reverse().map(e=>{var{item:t}=e;return t}),r=n[0],i=getSlugsWithoutSkuIdFromPDP(r);return[r,...i,...n.slice(1)].filter((e,t,n)=>e&&n.indexOf(e)===t)}({itemListElement:null!==(i=t.breadcrumbList.itemListElement)&&void 0!==i?i:[]}))for(var r of e)if(null!==(o=r.settings)&&void 0!==o&&null!==(s=o.template)&&void 0!==s&&s.value){var i,o,s;if(function(e){var t=e.trim().replace(/^\/+|\/+$/g,"");return t.startsWith("/")||(t="/"+t),t.endsWith("/p")||(t+="/"),(t=t.replace(/\/+/g,"/").replace(/\s+/g,"")).toLowerCase()}(r.settings.template.value)===n)return r}return e.find(e=>{var t,n;return!(null!==(t=e.settings)&&void 0!==t&&null!==(n=t.template)&&void 0!==n&&n.value)})||e[0]}function plp_ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function plp_objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?plp_ownKeys(Object(n),!0).forEach(function(t){(0,g.Z)(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):plp_ownKeys(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}var A=(o=(0,f.Z)(function*(e,t,n){if(x().cms.data){var r=JSON.parse(x().cms.data).plp;return yield $("/".concat(e,"/"),r,plp_objectSpread(plp_objectSpread({},(null==t?void 0:t.contentType)==="plp"?t:null),{},{contentType:"plp"}),n)}return yield J("/".concat(e,"/"),plp_objectSpread(plp_objectSpread({},(null==t?void 0:t.contentType)==="plp"?t:null),{},{contentType:"plp"}),n)}),function(e,t,n){return o.apply(this,arguments)}),$=(s=(0,f.Z)(function*(e,t,n,r){var i=null!=t?t:[];if(!i[0])throw new MissingContentError(n);var o=findBestPLPTemplate(i,e,r);return W({contentType:"plp",documentId:o.documentId,versionId:o.versionId})}),function(e,t,n,r){return s.apply(this,arguments)}),J=(c=(0,f.Z)(function*(e,t,n){var r=(yield F(t)).data;if(!r[0])throw new MissingContentError(t);return findBestPLPTemplate(r,e,n)}),function(e,t,n){return c.apply(this,arguments)});function pdp_ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function pdp_objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?pdp_ownKeys(Object(n),!0).forEach(function(t){(0,g.Z)(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):pdp_ownKeys(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}var U=(a=(0,f.Z)(function*(e,t){if(x().cms.data){var n=JSON.parse(x().cms.data).pdp;return yield q(e,n,pdp_objectSpread(pdp_objectSpread({},(null==t?void 0:t.contentType)==="pdp"?t:null),{},{contentType:"pdp"}))}return yield z(e,pdp_objectSpread(pdp_objectSpread({},(null==t?void 0:t.contentType)==="pdp"?t:null),{},{contentType:"pdp"}))}),function(e,t){return a.apply(this,arguments)}),q=(l=(0,f.Z)(function*(e,t,n){var r=null!=t?t:[];if(!r[0])throw new MissingContentError(n);var i=findBestPDPTemplate(r,e);return W({contentType:"pdp",documentId:i.documentId,versionId:i.versionId})}),function(e,t,n){return l.apply(this,arguments)}),z=(d=(0,f.Z)(function*(e,t){var n=(yield F(t)).data;if(!n[0])throw new MissingContentError(t);return findBestPDPTemplate(n,e)}),function(e,t){return d.apply(this,arguments)}),G=n(20306),R=["slug"];function service_ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function service_objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?service_ownKeys(Object(n),!0).forEach(function(t){(0,g.Z)(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):service_ownKeys(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}var V=new class{constructor(){(0,g.Z)(this,"clientCP",void 0),this.clientCP=new(M())({tenant:x().api.storeId})}getSingleContent(e){var t=this;return(0,f.Z)(function*(){var n=t.createContentOptions(e);return(0,G.w)()?t.getFromCP(n):W(n.cmsOptions)})()}getMultipleContent(e){var t=this;return(0,f.Z)(function*(){var n=t.createContentOptions(e);if((0,G.w)()){var r=t.convertOptionsToParams(n),i=[],o=0;do{var s,c,a=yield t.clientCP.listEntries(r,s);i.push(...a.entries);var l=null!==(c=a.scroll)&&void 0!==c?c:void 0;if(l&&l===s)break;s=l}while(s&&++o<25);return o>=25&&console.warn("listEntries pagination hit MAX_PAGES cap",r),t.fillEntriesWithData(i,r,n.isPreview)}return F(n.cmsOptions)})()}getPlpContent(e,t){var n=this;return(0,f.Z)(function*(){var r=service_objectSpread(service_objectSpread({},e),{},{contentType:"plp"}),i=n.createContentOptions(r);if((0,G.w)()){var o=yield n.getSingleContent(r);if(o)return o;var s=(yield n.getMultipleContent(r)).data;if(!(null!=s&&s.length))throw new MissingContentError(i.cmsOptions);return findBestPLPTemplate(s,i.slug,t)}return A(i.slug,i.cmsOptions,t)})()}getPdpContent(e,t){var n=this;return(0,f.Z)(function*(){var r=service_objectSpread(service_objectSpread({},t),{},{contentType:"pdp"}),i=n.createContentOptions(r);if((0,G.w)()){var o=yield n.getSingleContent(r);if(o)return o;var s=(yield n.getMultipleContent(r)).data;if(!s.length)throw new MissingContentError(i.cmsOptions);return findBestPDPTemplate(s,e)}return U(e,i.cmsOptions)})()}getFromCP(e){var t=this;return(0,f.Z)(function*(){var n=t.convertOptionsToParams(e);try{return yield t.getEntry(n,e.isPreview)}catch(e){if(!(e instanceof MissingContentError||e instanceof Error&&/\b404\b/.test(e.message)))throw e;console.warn("Content not found",e)}})()}fillEntriesWithData(e,t,n){var r=this;return(0,f.Z)(function*(){var i;return{data:yield Promise.all(e.map((i=(0,f.Z)(function*(e){var i=yield r.getEntryData(service_objectSpread(service_objectSpread({},t),{},{entryId:e.id}),n);return r.mergeEntryWithData(e,i)}),function(e){return i.apply(this,arguments)})))}})()}getEntry(e,t){var n=this;return(0,f.Z)(function*(){return e.entryId||e.slug?yield n.getEntryData(e,t):yield n.fetchFirstEntryFromList(e,t)})()}getEntryData(e,t){var n=this;return(0,f.Z)(function*(){if(!e.entryId&&!e.slug)throw Error("".concat(t?"Preview":"getEntry"," requires entryId or slug"));return t?e.entryId?n.clientCP.previewEntryById(e):n.clientCP.previewEntryBySlug(e):e.entryId?n.clientCP.getEntry(e):n.clientCP.getEntryBySlug(e)})()}fetchFirstEntryFromList(e,t){var n=this;return(0,f.Z)(function*(){var{entries:r}=yield n.clientCP.listEntries(e);if(!r||0===r.length)return console.warn("No entries found for params",e),{};if(r.length>1)throw new MultipleContentError(e);return n.getEntryData(service_objectSpread(service_objectSpread({},e),{},{entryId:r[0].id}),t)})()}createContentOptions(e){var{contentType:t,previewData:n,slug:r}=e,i=(null==n?void 0:n.contentType)===t,o=(0,G.b)(n);return service_objectSpread(service_objectSpread({cmsOptions:this.buildCmsOptions(e,i,o)},void 0!==r&&{slug:r}),{},{isPreview:i||o})}convertOptionsToParams(e){var t,n,{cmsOptions:r}=e,i={accountName:x().api.storeId,storeId:null!==(t=null===(n=x().contentSource)||void 0===n?void 0:n.project)&&void 0!==t?t:"faststore",contentType:r.contentType,slug:e.slug};if("documentId"in r&&r.documentId&&(i.entryId=r.documentId),"versionId"in r&&r.versionId&&(i.branchId=r.versionId),"releaseId"in r&&r.releaseId&&(i.branchId=r.releaseId),"filters"in r&&r.filters){var o=r.filters.filters;if(o&&o["settings.seo.slug"]){var s=o["settings.seo.slug"];i.slug=s.replace(/^\//,"")}Object.assign(i,r.filters)}return i}buildCmsOptions(e,t,n){var{contentType:r,previewData:i,documentId:o,versionId:s,releaseId:c,filters:a}=e,l=i||{},{slug:d}=l,p=(0,L.Z)(l,R);return service_objectSpread(service_objectSpread(service_objectSpread(service_objectSpread(service_objectSpread(service_objectSpread({contentType:r},t?p:{}),n&&{versionId:null==i?void 0:i.versionId,releaseId:null==i?void 0:i.releaseId}),void 0!==o&&{documentId:o}),void 0!==s&&{versionId:s}),void 0!==c&&{releaseId:c}),a&&{filters:a})}mergeEntryWithData(e,t){return service_objectSpread(service_objectSpread(service_objectSpread({},e),t),{},{id:e.id,name:e.name||""})}},H=n(31549);function LandingPage_ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function LandingPage_objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?LandingPage_ownKeys(Object(n),!0).forEach(function(t){(0,g.Z)(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):LandingPage_ownKeys(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}var Q=LandingPage_objectSpread(LandingPage_objectSpread(LandingPage_objectSpread({},b.Z),{},{[(0,O.x)(w.V,"Hero")]:w.V,[(0,O.x)(h.OverriddenDefaultBannerText,"BannerText")]:h.OverriddenDefaultBannerText,[(0,O.x)(j.default,"BannerNewsletter")]:j.default,[(0,O.x)(S.Z,"Incentives")]:S.Z,[(0,O.x)(m.Z,"CrossSellingShelf")]:m.Z,[(0,O.x)(_.OverriddenDefaultNewsletter,"Newsletter")]:_.OverriddenDefaultNewsletter,[(0,O.x)(I.OverriddenDefaultProductShelf,"ProductShelf")]:I.OverriddenDefaultProductShelf,[(0,O.x)(E.Z,"ProductTiles")]:E.Z},T.Z),C.Z);function LandingPage(e){var t,n,r,i,o,s,c,a,l,d,p,u,v,f,{page:{sections:g,settings:b},slug:O,serverData:j,globalSections:h,globalSettings:m}=e;return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(y.PB,{title:null!==(t=null==b?void 0:null===(n=b.seo)||void 0===n?void 0:n.title)&&void 0!==t?t:x().seo.title,description:null!==(r=null==b?void 0:null===(i=b.seo)||void 0===i?void 0:i.description)&&void 0!==r?r:null===(o=x().seo)||void 0===o?void 0:o.description,titleTemplate:null!==(s=null===(c=x().seo)||void 0===c?void 0:c.titleTemplate)&&void 0!==s?s:null===(a=x().seo)||void 0===a?void 0:a.title,canonical:null!==(l=null==b?void 0:null===(d=b.seo)||void 0===d?void 0:d.canonical)&&void 0!==l?l:"".concat(x().storeUrl,"/").concat(O),openGraph:{type:"website",url:x().storeUrl,title:null!==(p=null==b?void 0:null===(u=b.seo)||void 0===u?void 0:u.title)&&void 0!==p?p:x().seo.title,description:null!==(v=null==b?void 0:null===(f=b.seo)||void 0===f?void 0:f.description)&&void 0!==v?v:x().seo.description}}),(0,H.jsx)(y.iu,{url:x().storeUrl,potentialActions:[{target:"".concat(x().storeUrl,"/s/?q"),queryInput:"search_term_string"}]}),(0,H.jsx)(D.ZP,{context:{data:j,globalSettings:m},children:(0,H.jsx)(P.ZP,{sections:g,globalSections:h,components:Q})})]})}var X=(p=(0,f.Z)(function*(e,t){try{if(x().cms.data){var n,r,i=JSON.parse(x().cms.data).landingPage.find(e=>{var t,n;null===(t=e.settings)||void 0===t||null===(n=t.seo)||void 0===n||n.slug});if(i)return yield V.getSingleContent({contentType:"landingPage",previewData:t,documentId:i.documentId,versionId:i.versionId,releaseId:i.releaseId,slug:null===(n=i.settings)||void 0===n?void 0:null===(r=n.seo)||void 0===r?void 0:r.slug})}return yield V.getSingleContent({contentType:"landingPage",previewData:t,slug:e,filters:(null==t?void 0:t.contentType)!=="landingPage"?{filters:{"settings.seo.slug":"/".concat(e)}}:void 0})}catch(e){if(e instanceof MissingContentError)return null;throw e}}),function(e,t){return p.apply(this,arguments)})},77900:function(e,t,n){"use strict";var r=n(94564),i=n(42659),o=n(2614),s=n(31549);t.Z=function(e){var{incentives:t,variant:n="horizontal",colored:c=!1,label:a}=e;return(0,s.jsx)("section",{"data-fs-incentives":!0,"data-fs-incentives-colored":c,"data-fs-incentives-variant":n,"aria-label":"Incentives List ".concat(a),children:(0,s.jsx)(r.Z,{"data-fs-content":"incentives",children:t.map((e,t)=>(0,s.jsx)("li",{role:"listitem",children:(0,s.jsxs)(i.Z,{tabIndex:0,children:[(0,s.jsx)(o.Z,{"data-fs-incentive-icon":!0,"aria-label":e.alt,name:e.icon,width:32,height:32}),(0,s.jsxs)("section",{"data-fs-incentive-content":!0,children:[(0,s.jsx)("p",{"data-fs-incentive-title":!0,children:e.title}),(0,s.jsx)("span",{"data-fs-incentive-description":!0,children:e.firstLineText}),e.secondLineText&&(0,s.jsx)("span",{"data-fs-incentive-description":!0,children:e.secondLineText})]})]})},String(t)))})})}},90427:function(e){e.exports={section:"section_section__jnQ3q"}},42659:function(e,t,n){"use strict";var r=n(44194);let i=(0,r.forwardRef)(function({testId:e="store-incentive",children:t,...n},i){return r.createElement("section",{ref:i,"data-fs-incentive":!0,"data-testid":e,...n},t)});t.Z=i}}]);