veryfront 0.0.81 → 0.0.82
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/esm/deno.js +1 -1
- package/esm/src/cache/backend.d.ts +20 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +57 -0
- package/esm/src/cache/hash.d.ts +107 -0
- package/esm/src/cache/hash.d.ts.map +1 -0
- package/esm/src/cache/hash.js +166 -0
- package/esm/src/cache/index.d.ts +3 -0
- package/esm/src/cache/index.d.ts.map +1 -1
- package/esm/src/cache/index.js +3 -0
- package/esm/src/cache/module-cache.d.ts +82 -0
- package/esm/src/cache/module-cache.d.ts.map +1 -0
- package/esm/src/cache/module-cache.js +214 -0
- package/esm/src/cache/multi-tier.d.ts +177 -0
- package/esm/src/cache/multi-tier.d.ts.map +1 -0
- package/esm/src/cache/multi-tier.js +352 -0
- package/esm/src/cli/templates/integration-loader.d.ts.map +1 -1
- package/esm/src/cli/templates/integration-loader.js +2 -4
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +121 -14
- package/esm/src/observability/tracing/span-names.d.ts +2 -0
- package/esm/src/observability/tracing/span-names.d.ts.map +1 -1
- package/esm/src/observability/tracing/span-names.js +2 -0
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts +10 -2
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/cache.js +11 -6
- package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/index.js +72 -77
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +6 -29
- package/esm/src/transforms/esm/transform-cache.d.ts +25 -0
- package/esm/src/transforms/esm/transform-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/transform-cache.js +45 -0
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +2 -36
- package/esm/src/utils/constants/cache.d.ts +4 -0
- package/esm/src/utils/constants/cache.d.ts.map +1 -1
- package/esm/src/utils/constants/cache.js +14 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/cache/backend.ts +62 -0
- package/src/src/cache/hash.ts +205 -0
- package/src/src/cache/index.ts +3 -0
- package/src/src/cache/module-cache.ts +252 -0
- package/src/src/cache/multi-tier.ts +503 -0
- package/src/src/cli/templates/integration-loader.ts +2 -8
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +137 -18
- package/src/src/observability/tracing/span-names.ts +2 -0
- package/src/src/rendering/orchestrator/module-loader/cache.ts +14 -8
- package/src/src/rendering/orchestrator/module-loader/index.ts +94 -89
- package/src/src/transforms/esm/http-cache.ts +12 -32
- package/src/src/transforms/esm/transform-cache.ts +53 -0
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +2 -40
- package/src/src/utils/constants/cache.ts +21 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Tier Cache Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Generic implementation for L1 → L2 → L3 cache flows with automatic backfill.
|
|
5
|
+
* This provides consistent caching behavior across the codebase:
|
|
6
|
+
*
|
|
7
|
+
* - L1: In-memory (fastest, per-pod, lost on restart)
|
|
8
|
+
* - L2: Local disk (fast, per-pod, survives restart)
|
|
9
|
+
* - L3: Distributed (Redis/API, cross-pod, shared state)
|
|
10
|
+
*
|
|
11
|
+
* When a cache hit occurs at a lower tier (e.g., L3), the value is automatically
|
|
12
|
+
* backfilled to higher tiers (L1, L2) for faster subsequent access.
|
|
13
|
+
*
|
|
14
|
+
* @module cache/multi-tier
|
|
15
|
+
*/
|
|
16
|
+
import { rendererLogger as logger } from "../utils/index.js";
|
|
17
|
+
import { withSpan } from "../observability/tracing/otlp-setup.js";
|
|
18
|
+
import { SpanNames } from "../observability/tracing/span-names.js";
|
|
19
|
+
/**
|
|
20
|
+
* Multi-tier cache implementation.
|
|
21
|
+
*
|
|
22
|
+
* Provides automatic fallthrough from L1 → L2 → L3 with backfill on hits.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const cache = new MultiTierCache({
|
|
27
|
+
* name: "http-module",
|
|
28
|
+
* l1: new MemoryTier(),
|
|
29
|
+
* l3: await CacheBackends.httpModule(),
|
|
30
|
+
* defaultTtlSeconds: 86400,
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* const value = await cache.get("my-key");
|
|
34
|
+
* // If found in L3, automatically backfills L1
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class MultiTierCache {
|
|
38
|
+
config;
|
|
39
|
+
stats = {
|
|
40
|
+
gets: 0,
|
|
41
|
+
l1Hits: 0,
|
|
42
|
+
l2Hits: 0,
|
|
43
|
+
l3Hits: 0,
|
|
44
|
+
misses: 0,
|
|
45
|
+
sets: 0,
|
|
46
|
+
backfills: 0,
|
|
47
|
+
};
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.config = {
|
|
50
|
+
defaultTtlSeconds: 300,
|
|
51
|
+
backfillOnHit: true,
|
|
52
|
+
asyncBackfill: true,
|
|
53
|
+
...config,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get a value from the cache.
|
|
58
|
+
*
|
|
59
|
+
* Checks tiers in order: L1 → L2 → L3.
|
|
60
|
+
* On hit at a lower tier, backfills higher tiers.
|
|
61
|
+
*/
|
|
62
|
+
get(key) {
|
|
63
|
+
return withSpan(SpanNames.CACHE_MULTI_TIER_GET, async (span) => {
|
|
64
|
+
this.stats.gets++;
|
|
65
|
+
span?.setAttribute("cache.name", this.config.name);
|
|
66
|
+
span?.setAttribute("cache.key", key);
|
|
67
|
+
// L1: Memory
|
|
68
|
+
if (this.config.l1) {
|
|
69
|
+
try {
|
|
70
|
+
const value = await this.config.l1.get(key);
|
|
71
|
+
if (value !== null) {
|
|
72
|
+
this.stats.l1Hits++;
|
|
73
|
+
span?.setAttribute("cache.hit_tier", "l1");
|
|
74
|
+
logger.debug(`[${this.config.name}] L1 hit`, { key });
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
logger.debug(`[${this.config.name}] L1 get error`, { key, error });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// L2: Disk
|
|
83
|
+
if (this.config.l2) {
|
|
84
|
+
try {
|
|
85
|
+
const value = await this.config.l2.get(key);
|
|
86
|
+
if (value !== null) {
|
|
87
|
+
this.stats.l2Hits++;
|
|
88
|
+
span?.setAttribute("cache.hit_tier", "l2");
|
|
89
|
+
logger.debug(`[${this.config.name}] L2 hit`, { key });
|
|
90
|
+
const backfillPromise = this.backfill(key, value, ["l1"]);
|
|
91
|
+
if (this.config.asyncBackfill) {
|
|
92
|
+
void backfillPromise;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await backfillPromise;
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.debug(`[${this.config.name}] L2 get error`, { key, error });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// L3: Distributed
|
|
105
|
+
if (this.config.l3) {
|
|
106
|
+
try {
|
|
107
|
+
const value = await this.config.l3.get(key);
|
|
108
|
+
if (value !== null) {
|
|
109
|
+
this.stats.l3Hits++;
|
|
110
|
+
span?.setAttribute("cache.hit_tier", "l3");
|
|
111
|
+
logger.debug(`[${this.config.name}] L3 hit`, { key });
|
|
112
|
+
const backfillPromise = this.backfill(key, value, ["l1", "l2"]);
|
|
113
|
+
if (this.config.asyncBackfill) {
|
|
114
|
+
void backfillPromise;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await backfillPromise;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.debug(`[${this.config.name}] L3 get error`, { key, error });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this.stats.misses++;
|
|
127
|
+
span?.setAttribute("cache.hit_tier", "miss");
|
|
128
|
+
return null;
|
|
129
|
+
}, { "cache.operation": "get" });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Set a value in all tiers.
|
|
133
|
+
*
|
|
134
|
+
* Writes to all configured tiers in parallel (or sequentially if asyncBackfill=false).
|
|
135
|
+
*/
|
|
136
|
+
set(key, value, ttlSeconds) {
|
|
137
|
+
return withSpan(SpanNames.CACHE_MULTI_TIER_SET, (span) => {
|
|
138
|
+
this.stats.sets++;
|
|
139
|
+
const ttl = ttlSeconds ?? this.config.defaultTtlSeconds;
|
|
140
|
+
span?.setAttribute("cache.name", this.config.name);
|
|
141
|
+
span?.setAttribute("cache.key", key);
|
|
142
|
+
span?.setAttribute("cache.ttl_seconds", ttl);
|
|
143
|
+
const tiers = [this.config.l1, this.config.l2, this.config.l3].filter((t) => t !== undefined);
|
|
144
|
+
const setOps = tiers.map((tier) => tier.set(key, value, ttl).catch((error) => {
|
|
145
|
+
logger.debug(`[${this.config.name}] Set error in ${tier.name}`, { key, error });
|
|
146
|
+
}));
|
|
147
|
+
if (this.config.asyncBackfill) {
|
|
148
|
+
// Fire-and-forget for performance (don't await)
|
|
149
|
+
void Promise.all(setOps);
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
}
|
|
152
|
+
// Wait for all tiers
|
|
153
|
+
return Promise.all(setOps).then(() => { });
|
|
154
|
+
}, { "cache.operation": "set" });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Delete a value from all tiers.
|
|
158
|
+
*/
|
|
159
|
+
async delete(key) {
|
|
160
|
+
const tiers = [this.config.l1, this.config.l2, this.config.l3].filter((t) => t !== undefined && t.delete !== undefined);
|
|
161
|
+
await Promise.all(tiers.map((tier) => tier.delete?.(key).catch((error) => {
|
|
162
|
+
logger.debug(`[${this.config.name}] Delete error in ${tier.name}`, { key, error });
|
|
163
|
+
})));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get or compute a value.
|
|
167
|
+
*
|
|
168
|
+
* If the key exists in any tier, returns it.
|
|
169
|
+
* Otherwise, calls the compute function and stores the result in all tiers.
|
|
170
|
+
*/
|
|
171
|
+
async getOrCompute(key, computeFn, ttlSeconds) {
|
|
172
|
+
const cached = await this.get(key);
|
|
173
|
+
if (cached !== null)
|
|
174
|
+
return cached;
|
|
175
|
+
const value = await computeFn();
|
|
176
|
+
await this.set(key, value, ttlSeconds);
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Batch get multiple values.
|
|
181
|
+
*
|
|
182
|
+
* Uses batch operations where available for efficiency.
|
|
183
|
+
* Returns a map of key → value (null if not found).
|
|
184
|
+
*/
|
|
185
|
+
async getBatch(keys) {
|
|
186
|
+
if (keys.length === 0)
|
|
187
|
+
return new Map();
|
|
188
|
+
const results = new Map();
|
|
189
|
+
let remainingKeys = [...keys];
|
|
190
|
+
const backfillPromises = [];
|
|
191
|
+
// Check L1
|
|
192
|
+
if (this.config.l1 && remainingKeys.length > 0) {
|
|
193
|
+
try {
|
|
194
|
+
const l1Results = this.config.l1.getBatch
|
|
195
|
+
? await this.config.l1.getBatch(remainingKeys)
|
|
196
|
+
: await this.individualGets(this.config.l1, remainingKeys);
|
|
197
|
+
for (const [key, value] of l1Results) {
|
|
198
|
+
if (value !== null) {
|
|
199
|
+
results.set(key, value);
|
|
200
|
+
this.stats.l1Hits++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
remainingKeys = remainingKeys.filter((k) => !results.has(k) || results.get(k) === null);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
logger.debug(`[${this.config.name}] L1 getBatch error`, {
|
|
207
|
+
keyCount: remainingKeys.length,
|
|
208
|
+
error,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Check L2
|
|
213
|
+
if (this.config.l2 && remainingKeys.length > 0) {
|
|
214
|
+
try {
|
|
215
|
+
const l2Results = this.config.l2.getBatch
|
|
216
|
+
? await this.config.l2.getBatch(remainingKeys)
|
|
217
|
+
: await this.individualGets(this.config.l2, remainingKeys);
|
|
218
|
+
for (const [key, value] of l2Results) {
|
|
219
|
+
if (value !== null) {
|
|
220
|
+
results.set(key, value);
|
|
221
|
+
this.stats.l2Hits++;
|
|
222
|
+
backfillPromises.push(this.backfill(key, value, ["l1"]));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
remainingKeys = remainingKeys.filter((k) => !results.has(k) || results.get(k) === null);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
logger.debug(`[${this.config.name}] L2 getBatch error`, {
|
|
229
|
+
keyCount: remainingKeys.length,
|
|
230
|
+
error,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Check L3
|
|
235
|
+
if (this.config.l3 && remainingKeys.length > 0) {
|
|
236
|
+
try {
|
|
237
|
+
const l3Results = this.config.l3.getBatch
|
|
238
|
+
? await this.config.l3.getBatch(remainingKeys)
|
|
239
|
+
: await this.individualGets(this.config.l3, remainingKeys);
|
|
240
|
+
for (const [key, value] of l3Results) {
|
|
241
|
+
if (value !== null) {
|
|
242
|
+
results.set(key, value);
|
|
243
|
+
this.stats.l3Hits++;
|
|
244
|
+
backfillPromises.push(this.backfill(key, value, ["l1", "l2"]));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
results.set(key, null);
|
|
248
|
+
this.stats.misses++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
logger.debug(`[${this.config.name}] L3 getBatch error`, {
|
|
254
|
+
keyCount: remainingKeys.length,
|
|
255
|
+
error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Mark remaining as misses
|
|
260
|
+
for (const key of remainingKeys) {
|
|
261
|
+
if (!results.has(key)) {
|
|
262
|
+
results.set(key, null);
|
|
263
|
+
this.stats.misses++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (backfillPromises.length > 0) {
|
|
267
|
+
if (this.config.asyncBackfill) {
|
|
268
|
+
void Promise.all(backfillPromises);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
await Promise.all(backfillPromises);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get cache statistics.
|
|
278
|
+
*/
|
|
279
|
+
getStats() {
|
|
280
|
+
const totalHits = this.stats.l1Hits + this.stats.l2Hits + this.stats.l3Hits;
|
|
281
|
+
const hitRate = this.stats.gets > 0 ? totalHits / this.stats.gets : 0;
|
|
282
|
+
return { ...this.stats, hitRate };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Reset statistics.
|
|
286
|
+
*/
|
|
287
|
+
resetStats() {
|
|
288
|
+
this.stats = {
|
|
289
|
+
gets: 0,
|
|
290
|
+
l1Hits: 0,
|
|
291
|
+
l2Hits: 0,
|
|
292
|
+
l3Hits: 0,
|
|
293
|
+
misses: 0,
|
|
294
|
+
sets: 0,
|
|
295
|
+
backfills: 0,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Backfill higher tiers with a value found at a lower tier.
|
|
300
|
+
*/
|
|
301
|
+
backfill(key, value, tiers) {
|
|
302
|
+
if (!this.config.backfillOnHit)
|
|
303
|
+
return Promise.resolve();
|
|
304
|
+
this.stats.backfills++;
|
|
305
|
+
const ttl = this.config.defaultTtlSeconds;
|
|
306
|
+
const backfillOps = [];
|
|
307
|
+
if (tiers.includes("l1") && this.config.l1) {
|
|
308
|
+
backfillOps.push(this.config.l1.set(key, value, ttl).catch((error) => {
|
|
309
|
+
logger.debug(`[${this.config.name}] L1 backfill error`, { key, error });
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
if (tiers.includes("l2") && this.config.l2) {
|
|
313
|
+
backfillOps.push(this.config.l2.set(key, value, ttl).catch((error) => {
|
|
314
|
+
logger.debug(`[${this.config.name}] L2 backfill error`, { key, error });
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
return Promise.all(backfillOps).then(() => { });
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Helper for individual gets when batch operation is not available.
|
|
321
|
+
*/
|
|
322
|
+
async individualGets(tier, keys) {
|
|
323
|
+
const results = await Promise.all(keys.map(async (key) => [key, await tier.get(key)]));
|
|
324
|
+
return new Map(results);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Create a memory-backed cache tier from a CacheBackend.
|
|
329
|
+
*/
|
|
330
|
+
export function createMemoryTier(backend) {
|
|
331
|
+
return {
|
|
332
|
+
name: "memory",
|
|
333
|
+
get: (key) => backend.get(key),
|
|
334
|
+
set: (key, value, ttl) => backend.set(key, value, ttl),
|
|
335
|
+
delete: backend.del?.bind(backend),
|
|
336
|
+
getBatch: backend.getBatch?.bind(backend),
|
|
337
|
+
setBatch: backend.setBatch?.bind(backend),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Create a distributed cache tier from a CacheBackend.
|
|
342
|
+
*/
|
|
343
|
+
export function createDistributedTier(backend) {
|
|
344
|
+
return {
|
|
345
|
+
name: `distributed-${backend.type}`,
|
|
346
|
+
get: (key) => backend.get(key),
|
|
347
|
+
set: (key, value, ttl) => backend.set(key, value, ttl),
|
|
348
|
+
delete: backend.del?.bind(backend),
|
|
349
|
+
getBatch: backend.getBatch?.bind(backend),
|
|
350
|
+
setBatch: backend.setBatch?.bind(backend),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration-loader.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/templates/integration-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAAe,EAqDnD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,EAM3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,aAAa,CA8C/D,CAAC;AAqBF;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAanC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"integration-loader.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/templates/integration-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAAe,EAqDnD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,EAM3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,aAAa,CA8C/D,CAAC;AAqBF;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAanC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAQrC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAYA;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,eAAe,EAAE,GAClC,OAAO,CAAC;IACT,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC,CAyBD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,aAAa,CAExE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,gBAAgB,EAAE,eAAe,EAAE,GAClC,OAAO,CACR,KAAK,CAAC;IACJ,WAAW,EAAE,eAAe,CAAC;IAC7B,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC;CACvC,CAAC,CACH,CAcA;AAED;;;GAGG;AACH,wBAAgB,qCAAqC,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAE/E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,YAAY,EAAE,CAiZxD"}
|
|
@@ -167,10 +167,9 @@ export async function loadIntegration(integrationName) {
|
|
|
167
167
|
const config = await loadIntegrationConfig(integrationName);
|
|
168
168
|
if (!config)
|
|
169
169
|
return null;
|
|
170
|
-
const filesDir = pathHelper.join(getIntegrationDirectory(integrationName), "files");
|
|
171
170
|
return {
|
|
172
171
|
config,
|
|
173
|
-
files: await loadTemplateFromDirectory(
|
|
172
|
+
files: await loadTemplateFromDirectory(`integration:${integrationName}`),
|
|
174
173
|
};
|
|
175
174
|
}
|
|
176
175
|
/**
|
|
@@ -248,8 +247,7 @@ export async function getAvailablePrompts(integrationNames) {
|
|
|
248
247
|
* These include setup guide page and status API
|
|
249
248
|
*/
|
|
250
249
|
export function loadIntegrationBaseFilesFromDirectory() {
|
|
251
|
-
|
|
252
|
-
return loadTemplateFromDirectory(filesDir);
|
|
250
|
+
return loadTemplateFromDirectory("integration:_base");
|
|
253
251
|
}
|
|
254
252
|
/**
|
|
255
253
|
* Load the _base integration config to get shared env vars like APP_URL
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/modules/react-loader/ssr-module-loader/loader.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAuCpC,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/modules/react-loader/ssr-module-loader/loader.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAuCpC,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AA2B3E;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,EAAE,CAAsB;IAChC,OAAO,CAAC,mBAAmB,CAAuB;gBAE9B,OAAO,EAAE,sBAAsB;IAEnD;;OAEG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAyFxD,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,wBAAwB;IAwBhC,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,yBAAyB;IAuBjC,OAAO,CAAC,kBAAkB;IAK1B;;OAEG;YACW,2BAA2B;IAkGzC,OAAO,CAAC,yBAAyB;YAiBnB,2BAA2B;IAyQzC;;;OAGG;YACW,mBAAmB;IA2CjC,OAAO,CAAC,yBAAyB;IAYjC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,wBAAwB;IAehC,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,2BAA2B;IAWnC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,aAAa;YAIP,uBAAuB;IAyCrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAUhB;;;OAGG;YACW,gBAAgB;YAgBhB,WAAW;YAeX,YAAY;CAiC3B"}
|
|
@@ -22,7 +22,28 @@ import { extractComponent } from "../extract-component.js";
|
|
|
22
22
|
import { CIRCUIT_BREAKER_RESET_MS, CIRCUIT_BREAKER_THRESHOLD, IN_PROGRESS_WAIT_TIMEOUT_MS, MAX_CONCURRENT_TRANSFORMS, MAX_TRANSFORM_DEPTH, TRANSFORM_ACQUIRE_TIMEOUT_MS, TRANSFORM_BATCH_SIZE, } from "./constants.js";
|
|
23
23
|
import { withTimeoutThrow } from "../../../rendering/utils/stream-utils.js";
|
|
24
24
|
import { failedComponents, getFromRedis, getRedisClientInstance, getRedisEnabled, globalCrossProjectCache, globalInProgress, globalModuleCache, globalTmpDirs, setInRedis, transformSemaphore, } from "./cache/index.js";
|
|
25
|
-
import { getCacheBaseDir } from "../../../utils/cache-dir.js";
|
|
25
|
+
import { getCacheBaseDir, getHttpBundleCacheDir } from "../../../utils/cache-dir.js";
|
|
26
|
+
import { ensureHttpBundlesExist } from "../../../transforms/esm/http-cache.js";
|
|
27
|
+
/** Pattern to match HTTP bundle file:// paths in transformed code */
|
|
28
|
+
const HTTP_BUNDLE_PATTERN = /file:\/\/([^"'\s]+veryfront-http-bundle\/http-([a-f0-9]+)\.mjs)/gi;
|
|
29
|
+
/** Extract HTTP bundle paths from transformed code for proactive recovery */
|
|
30
|
+
function extractHttpBundlePaths(code) {
|
|
31
|
+
const bundles = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = HTTP_BUNDLE_PATTERN.exec(code)) !== null) {
|
|
35
|
+
const path = match[1];
|
|
36
|
+
const hash = match[2];
|
|
37
|
+
if (!seen.has(hash)) {
|
|
38
|
+
seen.add(hash);
|
|
39
|
+
bundles.push({ path, hash });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
HTTP_BUNDLE_PATTERN.lastIndex = 0;
|
|
43
|
+
return bundles;
|
|
44
|
+
}
|
|
45
|
+
/** Track temp paths that have been verified for HTTP bundles to avoid redundant I/O */
|
|
46
|
+
const verifiedHttpBundlePaths = new Set();
|
|
26
47
|
/**
|
|
27
48
|
* SSR Module Loader with Redis Support.
|
|
28
49
|
*
|
|
@@ -59,7 +80,37 @@ export class SSRModuleLoader {
|
|
|
59
80
|
context: { file: filePath, phase: "transform" },
|
|
60
81
|
}));
|
|
61
82
|
}
|
|
62
|
-
|
|
83
|
+
let mod;
|
|
84
|
+
try {
|
|
85
|
+
mod = await withSpan(SpanNames.SSR_DYNAMIC_IMPORT, () => import(`file://${cacheEntry.tempPath}?v=${cacheEntry.contentHash}`), { "ssr.file": fileName });
|
|
86
|
+
}
|
|
87
|
+
catch (importError) {
|
|
88
|
+
// If import fails due to missing HTTP bundle, try to recover and retry once
|
|
89
|
+
const errorMsg = importError instanceof Error
|
|
90
|
+
? importError.message
|
|
91
|
+
: String(importError);
|
|
92
|
+
const bundleMatch = errorMsg.match(/veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/);
|
|
93
|
+
if (bundleMatch) {
|
|
94
|
+
const hash = bundleMatch[1];
|
|
95
|
+
logger.warn("[SSR-MODULE-LOADER] Import failed due to missing HTTP bundle, attempting recovery", {
|
|
96
|
+
file: filePath.slice(-40),
|
|
97
|
+
hash,
|
|
98
|
+
});
|
|
99
|
+
const { recoverHttpBundleByHash } = await import("../../../transforms/esm/http-cache.js");
|
|
100
|
+
const cacheDir = getHttpBundleCacheDir();
|
|
101
|
+
const recovered = await recoverHttpBundleByHash(hash, cacheDir);
|
|
102
|
+
if (recovered) {
|
|
103
|
+
logger.info("[SSR-MODULE-LOADER] HTTP bundle recovered, retrying import", { hash });
|
|
104
|
+
mod = await import(`file://${cacheEntry.tempPath}?v=${cacheEntry.contentHash}&retry=1`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw importError;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw importError;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
63
114
|
failedComponents.delete(circuitKey);
|
|
64
115
|
return extractComponent(mod, filePath);
|
|
65
116
|
}
|
|
@@ -247,24 +298,80 @@ export class SSRModuleLoader {
|
|
|
247
298
|
const inProgressKey = contentCacheKey;
|
|
248
299
|
const cachedEntry = globalModuleCache.get(contentCacheKey);
|
|
249
300
|
if (cachedEntry) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
301
|
+
// Verify HTTP bundles exist for in-memory cached transforms (once per path)
|
|
302
|
+
if (!verifiedHttpBundlePaths.has(cachedEntry.tempPath)) {
|
|
303
|
+
try {
|
|
304
|
+
const cachedCode = await this.fs.readTextFile(cachedEntry.tempPath);
|
|
305
|
+
const bundlePaths = extractHttpBundlePaths(cachedCode);
|
|
306
|
+
if (bundlePaths.length > 0) {
|
|
307
|
+
const cacheDir = getHttpBundleCacheDir();
|
|
308
|
+
const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
|
|
309
|
+
if (failed.length > 0) {
|
|
310
|
+
logger.warn("[SSR-MODULE-LOADER] In-memory cached module has unrecoverable HTTP bundles, re-transforming", {
|
|
311
|
+
file: filePath.slice(-40),
|
|
312
|
+
failed,
|
|
313
|
+
});
|
|
314
|
+
globalModuleCache.delete(contentCacheKey);
|
|
315
|
+
globalModuleCache.delete(filePathCacheKey);
|
|
316
|
+
// Fall through to Redis or fresh transform
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
verifiedHttpBundlePaths.add(cachedEntry.tempPath);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
verifiedHttpBundlePaths.add(cachedEntry.tempPath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// File doesn't exist or unreadable, invalidate cache
|
|
328
|
+
globalModuleCache.delete(contentCacheKey);
|
|
329
|
+
globalModuleCache.delete(filePathCacheKey);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Re-check after potential invalidation
|
|
333
|
+
if (globalModuleCache.has(contentCacheKey)) {
|
|
334
|
+
globalModuleCache.set(filePathCacheKey, cachedEntry);
|
|
335
|
+
await this.ensureDependenciesExist(code, filePath, depth);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
253
338
|
}
|
|
254
339
|
const redisEnabled = getRedisEnabled();
|
|
255
340
|
const redisClient = getRedisClientInstance();
|
|
256
341
|
if (redisEnabled && redisClient) {
|
|
257
342
|
const redisCode = await getFromRedis(contentCacheKey);
|
|
258
343
|
if (redisCode) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
344
|
+
// Proactively ensure HTTP bundles exist before using cached transform.
|
|
345
|
+
// The cached code may reference file:// paths to HTTP bundles that were
|
|
346
|
+
// created on a different pod and may not exist locally.
|
|
347
|
+
let httpBundlesOk = true;
|
|
348
|
+
const bundlePaths = extractHttpBundlePaths(redisCode);
|
|
349
|
+
if (bundlePaths.length > 0) {
|
|
350
|
+
const cacheDir = getHttpBundleCacheDir();
|
|
351
|
+
const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
|
|
352
|
+
if (failed.length > 0) {
|
|
353
|
+
logger.warn("[SSR-MODULE-LOADER] Redis cached code has unrecoverable HTTP bundles, re-transforming", {
|
|
354
|
+
file: filePath.slice(-40),
|
|
355
|
+
failed,
|
|
356
|
+
});
|
|
357
|
+
httpBundlesOk = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (httpBundlesOk) {
|
|
361
|
+
const tempPath = await this.getTempPath(filePath, contentHash);
|
|
362
|
+
await this.fs.mkdir(tempPath.substring(0, tempPath.lastIndexOf("/")), {
|
|
363
|
+
recursive: true,
|
|
364
|
+
});
|
|
365
|
+
await this.fs.writeTextFile(tempPath, redisCode);
|
|
366
|
+
verifiedHttpBundlePaths.add(tempPath);
|
|
367
|
+
const entry = { tempPath, contentHash };
|
|
368
|
+
globalModuleCache.set(contentCacheKey, entry);
|
|
369
|
+
globalModuleCache.set(filePathCacheKey, entry);
|
|
370
|
+
logger.debug("[SSR-MODULE-LOADER] Redis cache hit", { file: filePath.slice(-40) });
|
|
371
|
+
await this.ensureDependenciesExist(code, filePath, depth);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Fall through to re-transform, which will create HTTP bundles locally
|
|
268
375
|
}
|
|
269
376
|
}
|
|
270
377
|
const existingTransform = globalInProgress.get(inProgressKey);
|
|
@@ -80,6 +80,8 @@ export declare const SpanNames: {
|
|
|
80
80
|
readonly CACHE_REGISTRY_DELETE_REDIS_KEYS: "cache.registry.delete_redis_keys";
|
|
81
81
|
readonly CACHE_KEYS_GET_ALL_ASYNC: "cache.keys.get_all_async";
|
|
82
82
|
readonly CACHE_KEYS_DELETE_ALL_ASYNC: "cache.keys.delete_all_async";
|
|
83
|
+
readonly CACHE_MULTI_TIER_GET: "cache.multi_tier.get";
|
|
84
|
+
readonly CACHE_MULTI_TIER_SET: "cache.multi_tier.set";
|
|
83
85
|
readonly HTML_GENERATE_SHELL_PARTS: "html.generate_shell_parts";
|
|
84
86
|
readonly HTML_WRAP_IN_SHELL: "html.wrap_in_shell";
|
|
85
87
|
readonly HTML_GENERATE_TAILWIND_CSS: "html.generate_tailwind_css";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"span-names.d.ts","sourceRoot":"","sources":["../../../../src/src/observability/tracing/span-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS
|
|
1
|
+
{"version":3,"file":"span-names.d.ts","sourceRoot":"","sources":["../../../../src/src/observability/tracing/span-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8GZ,CAAC"}
|
|
@@ -80,6 +80,8 @@ export const SpanNames = {
|
|
|
80
80
|
CACHE_REGISTRY_DELETE_REDIS_KEYS: "cache.registry.delete_redis_keys",
|
|
81
81
|
CACHE_KEYS_GET_ALL_ASYNC: "cache.keys.get_all_async",
|
|
82
82
|
CACHE_KEYS_DELETE_ALL_ASYNC: "cache.keys.delete_all_async",
|
|
83
|
+
CACHE_MULTI_TIER_GET: "cache.multi_tier.get",
|
|
84
|
+
CACHE_MULTI_TIER_SET: "cache.multi_tier.set",
|
|
83
85
|
HTML_GENERATE_SHELL_PARTS: "html.generate_shell_parts",
|
|
84
86
|
HTML_WRAP_IN_SHELL: "html.wrap_in_shell",
|
|
85
87
|
HTML_GENERATE_TAILWIND_CSS: "html.generate_tailwind_css",
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Loader Cache Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides hash generation and cache factory functions.
|
|
5
|
+
* Module caches are now pod-level singletons (see src/cache/module-cache.ts)
|
|
6
|
+
* to ensure caches persist across requests within the same pod.
|
|
7
|
+
*
|
|
8
|
+
* @module rendering/orchestrator/module-loader/cache
|
|
9
|
+
*/
|
|
10
|
+
export { createEsmCache, createModuleCache } from "../../../cache/module-cache.js";
|
|
1
11
|
export declare function generateHash(str: string): Promise<string>;
|
|
2
|
-
export declare function createModuleCache(): Map<string, string>;
|
|
3
|
-
export declare function createEsmCache(): Map<string, string>;
|
|
4
12
|
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/orchestrator/module-loader/cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/orchestrator/module-loader/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAInF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW/D"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Loader Cache Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides hash generation and cache factory functions.
|
|
5
|
+
* Module caches are now pod-level singletons (see src/cache/module-cache.ts)
|
|
6
|
+
* to ensure caches persist across requests within the same pod.
|
|
7
|
+
*
|
|
8
|
+
* @module rendering/orchestrator/module-loader/cache
|
|
9
|
+
*/
|
|
10
|
+
// Re-export pod-level cache factories
|
|
1
11
|
import * as dntShim from "../../../../_dnt.shims.js";
|
|
12
|
+
export { createEsmCache, createModuleCache } from "../../../cache/module-cache.js";
|
|
2
13
|
const HEX_CHARS = "0123456789abcdef";
|
|
3
14
|
export async function generateHash(str) {
|
|
4
15
|
const data = new TextEncoder().encode(str);
|
|
@@ -11,9 +22,3 @@ export async function generateHash(str) {
|
|
|
11
22
|
}
|
|
12
23
|
return hex;
|
|
13
24
|
}
|
|
14
|
-
export function createModuleCache() {
|
|
15
|
-
return new Map();
|
|
16
|
-
}
|
|
17
|
-
export function createEsmCache() {
|
|
18
|
-
return new Map();
|
|
19
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/orchestrator/module-loader/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/orchestrator/module-loader/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAkBzE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA4BpE,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;IACnC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAqDD;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,cAAc,EAC5B,MAAM,EAAE,kBAAkB,EAC1B,eAAe,UAAQ,GACtB,OAAO,CAAC,MAAM,CAAC,CAsIjB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,CAmC3F"}
|