@ecomiq/storefront 2.1.3 → 2.1.4
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 +8 -28
- package/package.json +2 -7
- package/src/codegen.mjs +12 -0
- package/src/index.ts +0 -5
- package/src/pages/pages.$pageSlug.tsx +31 -68
- package/src/server.ts +0 -15
- package/src/styles.css +0 -21
- package/src/cloud-runtime-client.ts +0 -378
- package/src/storefront-definition/contract.ts +0 -1
- package/src/storefront-definition/data.ts +0 -188
- package/src/storefront-definition/index.ts +0 -2
- package/src/storefront-definition/renderer.tsx +0 -1481
- package/src/storefront-layout.ts +0 -59
- package/src/storefront-runtime.ts +0 -173
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { StorefrontDefinitionSchema } from '@ecomiq/storefront-definition'
|
|
3
|
-
|
|
4
|
-
export const CloudRuntimeStoreSlugSchema = z
|
|
5
|
-
.string()
|
|
6
|
-
.trim()
|
|
7
|
-
.min(2)
|
|
8
|
-
.max(100)
|
|
9
|
-
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
|
10
|
-
|
|
11
|
-
export const ActiveStorefrontRuntimeSchema = z
|
|
12
|
-
.object({
|
|
13
|
-
schemaVersion: z.literal(1),
|
|
14
|
-
store: z
|
|
15
|
-
.object({
|
|
16
|
-
slug: CloudRuntimeStoreSlugSchema,
|
|
17
|
-
})
|
|
18
|
-
.strict(),
|
|
19
|
-
environment: z
|
|
20
|
-
.object({
|
|
21
|
-
name: z.literal('production'),
|
|
22
|
-
status: z.literal('ready'),
|
|
23
|
-
})
|
|
24
|
-
.strict(),
|
|
25
|
-
publication: z
|
|
26
|
-
.object({
|
|
27
|
-
sequence: z.number().int().nonnegative(),
|
|
28
|
-
releaseSequence: z.number().int().nonnegative(),
|
|
29
|
-
publishedAt: z.string().datetime({ offset: true }),
|
|
30
|
-
runtimeReleaseId: z.string().optional(),
|
|
31
|
-
artifactDigest: z.string().optional(),
|
|
32
|
-
})
|
|
33
|
-
.strict(),
|
|
34
|
-
definition: StorefrontDefinitionSchema,
|
|
35
|
-
})
|
|
36
|
-
.strict()
|
|
37
|
-
|
|
38
|
-
export type ActiveStorefrontRuntime = z.infer<
|
|
39
|
-
typeof ActiveStorefrontRuntimeSchema
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
export const EnvironmentPointerSchema = z
|
|
43
|
-
.object({
|
|
44
|
-
schemaVersion: z.literal(1),
|
|
45
|
-
storeSlug: CloudRuntimeStoreSlugSchema,
|
|
46
|
-
projectId: z.string().min(1),
|
|
47
|
-
environmentId: z.string().min(1),
|
|
48
|
-
generation: z.number().int().nonnegative(),
|
|
49
|
-
releaseSequence: z.number().int().nonnegative(),
|
|
50
|
-
deploymentSequence: z.number().int().nonnegative(),
|
|
51
|
-
artifactDigest: z.string().min(1),
|
|
52
|
-
artifactKey: z.string().min(1),
|
|
53
|
-
manifestDigest: z.string().min(1),
|
|
54
|
-
manifestKey: z.string().min(1),
|
|
55
|
-
runtimeReleaseId: z.string().min(1),
|
|
56
|
-
publishedAt: z.string().datetime({ offset: true }),
|
|
57
|
-
cachePolicy: z
|
|
58
|
-
.object({
|
|
59
|
-
pointerMaxAgeSeconds: z.number().int().nonnegative(),
|
|
60
|
-
staleIfErrorSeconds: z.number().int().nonnegative(),
|
|
61
|
-
})
|
|
62
|
-
.strict(),
|
|
63
|
-
})
|
|
64
|
-
.strict()
|
|
65
|
-
|
|
66
|
-
export type EnvironmentPointer = z.infer<typeof EnvironmentPointerSchema>
|
|
67
|
-
|
|
68
|
-
export type CloudRuntimeFetch = (
|
|
69
|
-
input: RequestInfo | URL,
|
|
70
|
-
init?: RequestInit,
|
|
71
|
-
) => Promise<Response>
|
|
72
|
-
|
|
73
|
-
export interface CloudRuntimeClientOptions {
|
|
74
|
-
baseUrl: string
|
|
75
|
-
artifactBaseUrl?: string
|
|
76
|
-
timeoutMs?: number
|
|
77
|
-
cacheTtlMs?: number
|
|
78
|
-
fetch?: CloudRuntimeFetch
|
|
79
|
-
now?: () => number
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface RuntimeCacheEntry {
|
|
83
|
-
value: ActiveStorefrontRuntime | null
|
|
84
|
-
etag?: string
|
|
85
|
-
expiresAt: number
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export class CloudRuntimeResponseError extends Error {
|
|
89
|
-
readonly status: number
|
|
90
|
-
|
|
91
|
-
constructor(status: number) {
|
|
92
|
-
super(`Cloud runtime respondió HTTP ${status}`)
|
|
93
|
-
this.name = 'CloudRuntimeResponseError'
|
|
94
|
-
this.status = status
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const DEFAULT_TIMEOUT_MS = 3_000
|
|
99
|
-
const DEFAULT_CACHE_TTL_MS = 15_000
|
|
100
|
-
const MAX_CACHE_ENTRIES = 100
|
|
101
|
-
|
|
102
|
-
interface LastKnownGood {
|
|
103
|
-
runtime: ActiveStorefrontRuntime
|
|
104
|
-
generation: number
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export class CloudRuntimeClient {
|
|
108
|
-
readonly baseUrl: string
|
|
109
|
-
readonly artifactBaseUrl: string | null
|
|
110
|
-
readonly timeoutMs: number
|
|
111
|
-
readonly cacheTtlMs: number
|
|
112
|
-
|
|
113
|
-
private readonly fetchRuntime: CloudRuntimeFetch
|
|
114
|
-
private readonly now: () => number
|
|
115
|
-
private readonly cache = new Map<string, RuntimeCacheEntry>()
|
|
116
|
-
private readonly lastKnownGood = new Map<string, LastKnownGood>()
|
|
117
|
-
private readonly artifactCache = new Map<string, ActiveStorefrontRuntime>()
|
|
118
|
-
private readonly inFlight = new Map<
|
|
119
|
-
string,
|
|
120
|
-
Promise<ActiveStorefrontRuntime | null>
|
|
121
|
-
>()
|
|
122
|
-
|
|
123
|
-
constructor(options: CloudRuntimeClientOptions) {
|
|
124
|
-
this.baseUrl = new URL(options.baseUrl).toString().replace(/\/$/, '')
|
|
125
|
-
this.artifactBaseUrl = options.artifactBaseUrl
|
|
126
|
-
? new URL(options.artifactBaseUrl).toString().replace(/\/$/, '')
|
|
127
|
-
: null
|
|
128
|
-
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
129
|
-
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
|
|
130
|
-
this.fetchRuntime = options.fetch ?? globalThis.fetch
|
|
131
|
-
this.now = options.now ?? Date.now
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async resolveActiveStorefront(
|
|
135
|
-
storeSlugInput: string,
|
|
136
|
-
): Promise<ActiveStorefrontRuntime | null> {
|
|
137
|
-
const storeSlug = CloudRuntimeStoreSlugSchema.parse(storeSlugInput)
|
|
138
|
-
const cached = this.cache.get(storeSlug)
|
|
139
|
-
if (cached && cached.expiresAt > this.now()) {
|
|
140
|
-
return cached.value
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const activeRequest = this.inFlight.get(storeSlug)
|
|
144
|
-
if (activeRequest) return activeRequest
|
|
145
|
-
|
|
146
|
-
const request = (
|
|
147
|
-
this.artifactBaseUrl
|
|
148
|
-
? this.requestFromArtifactStore(storeSlug)
|
|
149
|
-
: this.requestActiveStorefront(storeSlug, cached)
|
|
150
|
-
).finally(() => {
|
|
151
|
-
this.inFlight.delete(storeSlug)
|
|
152
|
-
})
|
|
153
|
-
this.inFlight.set(storeSlug, request)
|
|
154
|
-
return request
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
clearCache(): void {
|
|
158
|
-
this.cache.clear()
|
|
159
|
-
this.lastKnownGood.clear()
|
|
160
|
-
this.artifactCache.clear()
|
|
161
|
-
this.inFlight.clear()
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private async requestFromArtifactStore(
|
|
165
|
-
storeSlug: string,
|
|
166
|
-
): Promise<ActiveStorefrontRuntime | null> {
|
|
167
|
-
const survivor = this.lastKnownGood.get(storeSlug)
|
|
168
|
-
let pointer: EnvironmentPointer | null
|
|
169
|
-
try {
|
|
170
|
-
pointer = await this.fetchPointer(storeSlug)
|
|
171
|
-
} catch {
|
|
172
|
-
if (survivor) return survivor.runtime
|
|
173
|
-
throw new CloudRuntimeResponseError(503)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (pointer === null) {
|
|
177
|
-
if (survivor) return survivor.runtime
|
|
178
|
-
this.setCache(storeSlug, {
|
|
179
|
-
value: null,
|
|
180
|
-
expiresAt: this.now() + this.cacheTtlMs,
|
|
181
|
-
})
|
|
182
|
-
return null
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (survivor && pointer.generation < survivor.generation) {
|
|
186
|
-
this.setCache(storeSlug, {
|
|
187
|
-
value: survivor.runtime,
|
|
188
|
-
expiresAt: this.now() + this.cacheTtlMs,
|
|
189
|
-
})
|
|
190
|
-
return survivor.runtime
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let runtime: ActiveStorefrontRuntime
|
|
194
|
-
try {
|
|
195
|
-
runtime = await this.fetchArtifactRuntime(pointer)
|
|
196
|
-
} catch {
|
|
197
|
-
if (survivor) return survivor.runtime
|
|
198
|
-
throw new CloudRuntimeResponseError(503)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (runtime.store.slug !== storeSlug) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`El pointer de ${storeSlug} apunta a un artifact de ${runtime.store.slug}`,
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const ttlMs = pointer.cachePolicy.pointerMaxAgeSeconds * 1_000
|
|
208
|
-
this.setCache(storeSlug, {
|
|
209
|
-
value: runtime,
|
|
210
|
-
expiresAt: this.now() + (ttlMs > 0 ? ttlMs : this.cacheTtlMs),
|
|
211
|
-
})
|
|
212
|
-
this.lastKnownGood.set(storeSlug, {
|
|
213
|
-
runtime,
|
|
214
|
-
generation: pointer.generation,
|
|
215
|
-
})
|
|
216
|
-
return runtime
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private async fetchPointer(
|
|
220
|
-
storeSlug: string,
|
|
221
|
-
): Promise<EnvironmentPointer | null> {
|
|
222
|
-
const response = await this.fetchWithTimeout(
|
|
223
|
-
`${this.artifactBaseUrl}/stores/${encodeURIComponent(storeSlug)}/pointer.json`,
|
|
224
|
-
)
|
|
225
|
-
if (response.status === 404) return null
|
|
226
|
-
if (!response.ok) throw new CloudRuntimeResponseError(response.status)
|
|
227
|
-
return EnvironmentPointerSchema.parse(await response.json())
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private async fetchArtifactRuntime(
|
|
231
|
-
pointer: EnvironmentPointer,
|
|
232
|
-
): Promise<ActiveStorefrontRuntime> {
|
|
233
|
-
const cached = this.artifactCache.get(pointer.artifactKey)
|
|
234
|
-
const base =
|
|
235
|
-
cached ?? (await this.requestArtifactBody(pointer.artifactKey))
|
|
236
|
-
if (!cached) {
|
|
237
|
-
if (this.artifactCache.size >= MAX_CACHE_ENTRIES) {
|
|
238
|
-
const oldestKey = this.artifactCache.keys().next().value
|
|
239
|
-
if (oldestKey !== undefined) this.artifactCache.delete(oldestKey)
|
|
240
|
-
}
|
|
241
|
-
this.artifactCache.set(pointer.artifactKey, base)
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
...base,
|
|
245
|
-
publication: {
|
|
246
|
-
sequence: pointer.deploymentSequence,
|
|
247
|
-
releaseSequence: pointer.releaseSequence,
|
|
248
|
-
publishedAt: pointer.publishedAt,
|
|
249
|
-
runtimeReleaseId: pointer.runtimeReleaseId,
|
|
250
|
-
artifactDigest: pointer.artifactDigest,
|
|
251
|
-
},
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private async requestArtifactBody(
|
|
256
|
-
artifactKey: string,
|
|
257
|
-
): Promise<ActiveStorefrontRuntime> {
|
|
258
|
-
const response = await this.fetchWithTimeout(
|
|
259
|
-
`${this.artifactBaseUrl}/${artifactKey}`,
|
|
260
|
-
)
|
|
261
|
-
if (!response.ok) throw new CloudRuntimeResponseError(response.status)
|
|
262
|
-
return ActiveStorefrontRuntimeSchema.parse(await response.json())
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private async fetchWithTimeout(url: string): Promise<Response> {
|
|
266
|
-
const controller = new AbortController()
|
|
267
|
-
const timeout = setTimeout(() => controller.abort(), this.timeoutMs)
|
|
268
|
-
try {
|
|
269
|
-
return await this.fetchRuntime(url, {
|
|
270
|
-
method: 'GET',
|
|
271
|
-
headers: { accept: 'application/json' },
|
|
272
|
-
signal: controller.signal,
|
|
273
|
-
})
|
|
274
|
-
} finally {
|
|
275
|
-
clearTimeout(timeout)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private setCache(storeSlug: string, entry: RuntimeCacheEntry): void {
|
|
280
|
-
if (!this.cache.has(storeSlug) && this.cache.size >= MAX_CACHE_ENTRIES) {
|
|
281
|
-
const oldestKey = this.cache.keys().next().value
|
|
282
|
-
if (oldestKey !== undefined) this.cache.delete(oldestKey)
|
|
283
|
-
}
|
|
284
|
-
this.cache.set(storeSlug, entry)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private async requestActiveStorefront(
|
|
288
|
-
storeSlug: string,
|
|
289
|
-
cached?: RuntimeCacheEntry,
|
|
290
|
-
): Promise<ActiveStorefrontRuntime | null> {
|
|
291
|
-
const controller = new AbortController()
|
|
292
|
-
const timeout = setTimeout(() => controller.abort(), this.timeoutMs)
|
|
293
|
-
const headers = new Headers({ accept: 'application/json' })
|
|
294
|
-
if (cached?.etag) headers.set('if-none-match', cached.etag)
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const response = await this.fetchRuntime(
|
|
298
|
-
new URL(
|
|
299
|
-
`/v1/runtime/storefronts/${encodeURIComponent(storeSlug)}`,
|
|
300
|
-
this.baseUrl,
|
|
301
|
-
),
|
|
302
|
-
{
|
|
303
|
-
method: 'GET',
|
|
304
|
-
headers,
|
|
305
|
-
signal: controller.signal,
|
|
306
|
-
},
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
if (response.status === 304) {
|
|
310
|
-
if (!cached) throw new CloudRuntimeResponseError(304)
|
|
311
|
-
this.setCache(storeSlug, {
|
|
312
|
-
...cached,
|
|
313
|
-
expiresAt: this.now() + this.cacheTtlMs,
|
|
314
|
-
})
|
|
315
|
-
return cached.value
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (response.status === 404) {
|
|
319
|
-
this.setCache(storeSlug, {
|
|
320
|
-
value: null,
|
|
321
|
-
etag: response.headers.get('etag') ?? undefined,
|
|
322
|
-
expiresAt: this.now() + this.cacheTtlMs,
|
|
323
|
-
})
|
|
324
|
-
return null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!response.ok) {
|
|
328
|
-
throw new CloudRuntimeResponseError(response.status)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const runtime = ActiveStorefrontRuntimeSchema.parse(
|
|
332
|
-
await response.json(),
|
|
333
|
-
)
|
|
334
|
-
if (runtime.store.slug !== storeSlug) {
|
|
335
|
-
throw new Error(
|
|
336
|
-
`Cloud runtime devolvió el store slug ${runtime.store.slug} para ${storeSlug}`,
|
|
337
|
-
)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
this.setCache(storeSlug, {
|
|
341
|
-
value: runtime,
|
|
342
|
-
etag: response.headers.get('etag') ?? undefined,
|
|
343
|
-
expiresAt: this.now() + this.cacheTtlMs,
|
|
344
|
-
})
|
|
345
|
-
return runtime
|
|
346
|
-
} finally {
|
|
347
|
-
clearTimeout(timeout)
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const sharedClients = new Map<string, CloudRuntimeClient>()
|
|
353
|
-
|
|
354
|
-
export function getCloudRuntimeClient(
|
|
355
|
-
options: Omit<CloudRuntimeClientOptions, 'fetch' | 'now'>,
|
|
356
|
-
): CloudRuntimeClient {
|
|
357
|
-
const key = [
|
|
358
|
-
new URL(options.baseUrl).toString(),
|
|
359
|
-
options.artifactBaseUrl ? new URL(options.artifactBaseUrl).toString() : '',
|
|
360
|
-
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
361
|
-
options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
362
|
-
].join('|')
|
|
363
|
-
const current = sharedClients.get(key)
|
|
364
|
-
if (current) return current
|
|
365
|
-
|
|
366
|
-
const client = new CloudRuntimeClient(options)
|
|
367
|
-
if (sharedClients.size >= 8) {
|
|
368
|
-
const oldestKey = sharedClients.keys().next().value
|
|
369
|
-
if (oldestKey !== undefined) sharedClients.delete(oldestKey)
|
|
370
|
-
}
|
|
371
|
-
sharedClients.set(key, client)
|
|
372
|
-
return client
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function resetCloudRuntimeClients(): void {
|
|
376
|
-
for (const client of sharedClients.values()) client.clearCache()
|
|
377
|
-
sharedClients.clear()
|
|
378
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from '@ecomiq/storefront-definition'
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Collection,
|
|
3
|
-
Collections,
|
|
4
|
-
ProductsParamsInput,
|
|
5
|
-
ProductsResponse,
|
|
6
|
-
ProductSummary,
|
|
7
|
-
} from '@ecomiq/core'
|
|
8
|
-
import {
|
|
9
|
-
type CollectionGridStorefrontSection,
|
|
10
|
-
type ProductGridStorefrontSection,
|
|
11
|
-
type StorefrontDefinition,
|
|
12
|
-
} from '@ecomiq/storefront-definition'
|
|
13
|
-
import { storefrontPageFor } from './renderer'
|
|
14
|
-
import type { DefinitionData } from './renderer'
|
|
15
|
-
|
|
16
|
-
const MAX_UNIQUE_PRODUCT_SOURCES = 12
|
|
17
|
-
const MANUAL_PRODUCT_PAGE_SIZE = 100
|
|
18
|
-
const MAX_MANUAL_PRODUCT_SCAN_PAGES = 5
|
|
19
|
-
|
|
20
|
-
export interface StorefrontRuntimeCatalogClient {
|
|
21
|
-
getProducts(params?: ProductsParamsInput): Promise<ProductsResponse>
|
|
22
|
-
getCollections(): Promise<Collections>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ResolvedStorefrontDefinitionData
|
|
26
|
-
extends DefinitionData {
|
|
27
|
-
productsBySectionId: Record<string, ProductSummary[]>
|
|
28
|
-
collectionsBySectionId: Record<string, Collection[]>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function emptyRendererData(): ResolvedStorefrontDefinitionData {
|
|
32
|
-
return {
|
|
33
|
-
productsBySectionId: {},
|
|
34
|
-
collectionsBySectionId: {},
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function productSourceKey(
|
|
39
|
-
section: ProductGridStorefrontSection,
|
|
40
|
-
): string | null {
|
|
41
|
-
if (section.source.kind === 'manual') return null
|
|
42
|
-
if (section.source.kind === 'collection') {
|
|
43
|
-
return `collection:${section.source.slug}`
|
|
44
|
-
}
|
|
45
|
-
return 'all'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function loadProductSource(
|
|
49
|
-
section: ProductGridStorefrontSection,
|
|
50
|
-
client: StorefrontRuntimeCatalogClient,
|
|
51
|
-
): Promise<ProductSummary[]> {
|
|
52
|
-
const params: ProductsParamsInput = {
|
|
53
|
-
page: 1,
|
|
54
|
-
pageSize: section.limit,
|
|
55
|
-
sort: 'newest',
|
|
56
|
-
}
|
|
57
|
-
if (section.source.kind === 'collection') {
|
|
58
|
-
params.collection = section.source.slug
|
|
59
|
-
}
|
|
60
|
-
const response = await client.getProducts(params)
|
|
61
|
-
return response.products.slice(0, section.limit)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function scanManualProducts(
|
|
65
|
-
productIds: ReadonlySet<string>,
|
|
66
|
-
client: StorefrontRuntimeCatalogClient,
|
|
67
|
-
): Promise<Map<string, ProductSummary>> {
|
|
68
|
-
const found = new Map<string, ProductSummary>()
|
|
69
|
-
|
|
70
|
-
for (let page = 1; page <= MAX_MANUAL_PRODUCT_SCAN_PAGES; page += 1) {
|
|
71
|
-
const response = await client.getProducts({
|
|
72
|
-
page,
|
|
73
|
-
pageSize: MANUAL_PRODUCT_PAGE_SIZE,
|
|
74
|
-
sort: 'newest',
|
|
75
|
-
})
|
|
76
|
-
for (const product of response.products) {
|
|
77
|
-
if (productIds.has(product.id)) found.set(product.id, product)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
found.size === productIds.size ||
|
|
82
|
-
page >= response.totalPages ||
|
|
83
|
-
response.products.length === 0
|
|
84
|
-
) {
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return found
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function resolveStorefrontDefinitionData(
|
|
93
|
-
definition: StorefrontDefinition,
|
|
94
|
-
client: StorefrontRuntimeCatalogClient,
|
|
95
|
-
pageId?: string,
|
|
96
|
-
): Promise<ResolvedStorefrontDefinitionData> {
|
|
97
|
-
const data = emptyRendererData()
|
|
98
|
-
const page = storefrontPageFor(definition, pageId)
|
|
99
|
-
const sections = page?.sections ?? []
|
|
100
|
-
const productSections = sections.filter(
|
|
101
|
-
(section): section is ProductGridStorefrontSection =>
|
|
102
|
-
section.enabled && section.type === 'product-grid',
|
|
103
|
-
)
|
|
104
|
-
const collectionSections = sections.filter(
|
|
105
|
-
(section): section is CollectionGridStorefrontSection =>
|
|
106
|
-
section.enabled && section.type === 'collection-grid',
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const sourceSections = new Map<string, ProductGridStorefrontSection>()
|
|
110
|
-
for (const section of productSections) {
|
|
111
|
-
const sourceKey = productSourceKey(section)
|
|
112
|
-
if (!sourceKey) continue
|
|
113
|
-
const current = sourceSections.get(sourceKey)
|
|
114
|
-
if (current) {
|
|
115
|
-
if (section.limit > current.limit) sourceSections.set(sourceKey, section)
|
|
116
|
-
continue
|
|
117
|
-
}
|
|
118
|
-
if (sourceSections.size >= MAX_UNIQUE_PRODUCT_SOURCES) continue
|
|
119
|
-
sourceSections.set(sourceKey, section)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const sourceRequests = new Map<string, Promise<ProductSummary[]>>()
|
|
123
|
-
for (const [sourceKey, section] of sourceSections) {
|
|
124
|
-
sourceRequests.set(
|
|
125
|
-
sourceKey,
|
|
126
|
-
loadProductSource(section, client).catch(() => []),
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const manualIds = new Set(
|
|
131
|
-
productSections.flatMap((section) =>
|
|
132
|
-
section.source.kind === 'manual' ? section.source.productIds : [],
|
|
133
|
-
),
|
|
134
|
-
)
|
|
135
|
-
const manualProducts =
|
|
136
|
-
manualIds.size > 0
|
|
137
|
-
? scanManualProducts(manualIds, client).catch(
|
|
138
|
-
() => new Map<string, ProductSummary>(),
|
|
139
|
-
)
|
|
140
|
-
: Promise.resolve(new Map<string, ProductSummary>())
|
|
141
|
-
|
|
142
|
-
await Promise.all(
|
|
143
|
-
productSections.map(async (section) => {
|
|
144
|
-
if (section.source.kind === 'manual') {
|
|
145
|
-
const productsById = await manualProducts
|
|
146
|
-
data.productsBySectionId[section.id] = section.source.productIds
|
|
147
|
-
.map((productId) => productsById.get(productId))
|
|
148
|
-
.filter(
|
|
149
|
-
(product): product is ProductSummary => product !== undefined,
|
|
150
|
-
)
|
|
151
|
-
.slice(0, section.limit)
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const sourceKey = productSourceKey(section)
|
|
156
|
-
const products = sourceKey
|
|
157
|
-
? await (sourceRequests.get(sourceKey) ?? Promise.resolve([]))
|
|
158
|
-
: []
|
|
159
|
-
data.productsBySectionId[section.id] = products.slice(0, section.limit)
|
|
160
|
-
}),
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
if (collectionSections.length > 0) {
|
|
164
|
-
const collections = await client.getCollections().catch(() => [])
|
|
165
|
-
const collectionsBySlug = new Map(
|
|
166
|
-
collections.map((collection) => [collection.slug, collection]),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
for (const section of collectionSections) {
|
|
170
|
-
if (section.source.kind === 'manual') {
|
|
171
|
-
data.collectionsBySectionId[section.id] = section.source.slugs
|
|
172
|
-
.map((slug) => collectionsBySlug.get(slug))
|
|
173
|
-
.filter(
|
|
174
|
-
(collection): collection is Collection =>
|
|
175
|
-
collection !== undefined,
|
|
176
|
-
)
|
|
177
|
-
.slice(0, section.limit)
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
data.collectionsBySectionId[section.id] = collections.slice(
|
|
181
|
-
0,
|
|
182
|
-
section.limit,
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return data
|
|
188
|
-
}
|