@epic-web/workshop-utils 6.84.3 → 6.84.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.
@@ -407,6 +407,14 @@ export declare function getGlobalCaches(): Promise<{
407
407
  }>;
408
408
  }>;
409
409
  }[]>;
410
+ export declare function readWorkshopCacheMetadata(workshopId: string): Promise<{
411
+ schemaVersion: 1;
412
+ workshopId: string;
413
+ displayName: string;
414
+ createdAt: number;
415
+ repoName?: string | undefined;
416
+ subtitle?: string | undefined;
417
+ } | null>;
410
418
  export declare function getWorkshopFileCaches(): Promise<Record<string, any>>;
411
419
  export declare function readEntryByPath(cacheFilePath: string): Promise<any>;
412
420
  export declare function deleteCache(): Promise<null | undefined>;
@@ -11,11 +11,13 @@ import { resolveCacheDir } from "./data-storage.server.js";
11
11
  import { logger } from "./logger.js";
12
12
  import { cachifiedTimingReporter } from "./timing.server.js";
13
13
  import { checkConnection } from "./utils.server.js";
14
+ import { ensureWorkshopCacheMetadataFile, getWorkshopCacheMetadataFilePath, readWorkshopCacheMetadataFile, } from "./workshop-cache-metadata.server.js";
14
15
  const MAX_CACHE_FILE_SIZE = 3 * 1024 * 1024; // 3MB in bytes
15
16
  const cacheDir = resolveCacheDir();
16
17
  const log = logger('epic:cache');
17
18
  // Throttle repeated Sentry reports for corrupted cache files to reduce noise
18
19
  const corruptedReportThrottle = remember('epic:cache:corruption-throttle', () => new LRUCache({ max: 2000, ttl: 60_000 }));
20
+ const ensuredWorkshopCacheMetadata = remember('epic:cache:ensured-workshop-cache-metadata', () => new Set());
19
21
  // Format cache time helper function (copied from @epic-web/cachified for consistency)
20
22
  function formatCacheTime(metadata, formatDuration) {
21
23
  const ttl = metadata?.ttl;
@@ -180,6 +182,51 @@ export const notificationsCache = makeSingletonCache('NotificationsCache');
180
182
  export const directoryEmptyCache = makeSingletonCache('DirectoryEmptyCache');
181
183
  export const discordCache = makeSingletonFsCache('DiscordCache');
182
184
  export const epicApiCache = makeSingletonFsCache('EpicApiCache');
185
+ async function getCurrentWorkshopDisplayInfo() {
186
+ const env = getEnv();
187
+ const contextCwd = env.EPICSHOP_CONTEXT_CWD;
188
+ const repoName = contextCwd
189
+ ? path.basename(contextCwd) || undefined
190
+ : undefined;
191
+ let displayName = repoName || env.EPICSHOP_WORKSHOP_INSTANCE_ID || 'Unknown';
192
+ let subtitle;
193
+ try {
194
+ const { getWorkshopConfig } = await import("./config.server.js");
195
+ const config = getWorkshopConfig();
196
+ displayName = config.title || displayName;
197
+ subtitle = config.subtitle;
198
+ }
199
+ catch {
200
+ // Best-effort only: cache metadata should never break caching.
201
+ }
202
+ return { displayName, repoName, subtitle };
203
+ }
204
+ async function ensureWorkshopCacheMetadata(workshopId) {
205
+ if (!workshopId)
206
+ return;
207
+ if (ensuredWorkshopCacheMetadata.has(workshopId)) {
208
+ try {
209
+ const filePath = getWorkshopCacheMetadataFilePath({ cacheDir, workshopId });
210
+ if (await fsExtra.pathExists(filePath))
211
+ return;
212
+ }
213
+ catch {
214
+ // Treat errors as "missing" and attempt to re-create.
215
+ }
216
+ ensuredWorkshopCacheMetadata.delete(workshopId);
217
+ }
218
+ const { displayName, repoName, subtitle } = await getCurrentWorkshopDisplayInfo();
219
+ const metadata = await ensureWorkshopCacheMetadataFile({
220
+ cacheDir,
221
+ workshopId,
222
+ displayName,
223
+ repoName,
224
+ subtitle,
225
+ });
226
+ if (metadata) {
227
+ ensuredWorkshopCacheMetadata.add(workshopId);
228
+ }
229
+ }
183
230
  export function makeGlobalFsCache(name) {
184
231
  return remember(`global-${name}`, () => {
185
232
  const cacheInstanceDir = path.join(cacheDir, 'global', name);
@@ -419,6 +466,9 @@ export async function getGlobalCaches() {
419
466
  workshopId: 'global',
420
467
  }));
421
468
  }
469
+ export async function readWorkshopCacheMetadata(workshopId) {
470
+ return readWorkshopCacheMetadataFile({ cacheDir, workshopId });
471
+ }
422
472
  export async function getWorkshopFileCaches() {
423
473
  const workshopCacheDir = path.join(cacheDir, getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID);
424
474
  const caches = readJsonFilesInDirectory(workshopCacheDir);
@@ -436,6 +486,7 @@ export async function deleteCache() {
436
486
  if (await fsExtra.exists(cacheDir)) {
437
487
  await fsExtra.remove(cacheDir);
438
488
  }
489
+ ensuredWorkshopCacheMetadata.clear();
439
490
  }
440
491
  catch (error) {
441
492
  console.error(`Error deleting the cache in ${cacheDir}`, error);
@@ -469,6 +520,7 @@ export async function deleteWorkshopCache(workshopId, cacheName) {
469
520
  if (await fsExtra.exists(workshopCachePath)) {
470
521
  await fsExtra.remove(workshopCachePath);
471
522
  }
523
+ ensuredWorkshopCacheMetadata.delete(workshopId);
472
524
  }
473
525
  }
474
526
  catch (error) {
@@ -526,7 +578,8 @@ export function makeSingletonCache(name) {
526
578
  }
527
579
  export function makeSingletonFsCache(name) {
528
580
  return remember(name, () => {
529
- const cacheInstanceDir = path.join(cacheDir, getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID, name);
581
+ const workshopId = getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID;
582
+ const cacheInstanceDir = path.join(cacheDir, workshopId, name);
530
583
  const fsCache = {
531
584
  name: `Filesystem cache (${name})`,
532
585
  async get(key) {
@@ -558,6 +611,12 @@ export function makeSingletonFsCache(name) {
558
611
  return null;
559
612
  },
560
613
  async set(key, entry) {
614
+ try {
615
+ await ensureWorkshopCacheMetadata(workshopId);
616
+ }
617
+ catch {
618
+ // Ignore: this is optional metadata only.
619
+ }
561
620
  const filePath = path.join(cacheInstanceDir, md5(key));
562
621
  const tempPath = `${filePath}.tmp`;
563
622
  await fsExtra.ensureDir(path.dirname(filePath));
@@ -0,0 +1,27 @@
1
+ import z from 'zod';
2
+ export declare const WORKSHOP_CACHE_METADATA_FILE_NAME: ".workshop-cache-metadata.json";
3
+ declare const WorkshopCacheMetadataSchema: z.ZodObject<{
4
+ schemaVersion: z.ZodLiteral<1>;
5
+ workshopId: z.ZodString;
6
+ displayName: z.ZodString;
7
+ repoName: z.ZodOptional<z.ZodString>;
8
+ subtitle: z.ZodOptional<z.ZodString>;
9
+ createdAt: z.ZodNumber;
10
+ }, z.core.$strip>;
11
+ export type WorkshopCacheMetadata = z.infer<typeof WorkshopCacheMetadataSchema>;
12
+ export declare function getWorkshopCacheMetadataFilePath({ cacheDir, workshopId, }: {
13
+ cacheDir: string;
14
+ workshopId: string;
15
+ }): string;
16
+ export declare function readWorkshopCacheMetadataFile({ cacheDir, workshopId, }: {
17
+ cacheDir: string;
18
+ workshopId: string;
19
+ }): Promise<WorkshopCacheMetadata | null>;
20
+ export declare function ensureWorkshopCacheMetadataFile({ cacheDir, workshopId, displayName, repoName, subtitle, }: {
21
+ cacheDir: string;
22
+ workshopId: string;
23
+ displayName: string;
24
+ repoName?: string;
25
+ subtitle?: string;
26
+ }): Promise<WorkshopCacheMetadata | null>;
27
+ export {};
@@ -0,0 +1,59 @@
1
+ import path from 'path';
2
+ import fsExtra from 'fs-extra';
3
+ import z from 'zod';
4
+ export const WORKSHOP_CACHE_METADATA_FILE_NAME = '.workshop-cache-metadata.json';
5
+ const WorkshopCacheMetadataSchema = z.object({
6
+ schemaVersion: z.literal(1),
7
+ workshopId: z.string().min(1),
8
+ displayName: z.string().min(1),
9
+ repoName: z.string().optional(),
10
+ subtitle: z.string().optional(),
11
+ createdAt: z.number(),
12
+ });
13
+ export function getWorkshopCacheMetadataFilePath({ cacheDir, workshopId, }) {
14
+ return path.join(cacheDir, workshopId, WORKSHOP_CACHE_METADATA_FILE_NAME);
15
+ }
16
+ async function writeWorkshopCacheMetadataAtomic({ filePath, metadata, }) {
17
+ const tmpPath = `${filePath}.tmp`;
18
+ await fsExtra.ensureDir(path.dirname(filePath));
19
+ await fsExtra.writeJSON(tmpPath, metadata, { spaces: 2 });
20
+ await fsExtra.move(tmpPath, filePath, { overwrite: true });
21
+ }
22
+ export async function readWorkshopCacheMetadataFile({ cacheDir, workshopId, }) {
23
+ const filePath = getWorkshopCacheMetadataFilePath({ cacheDir, workshopId });
24
+ try {
25
+ const raw = await fsExtra.readJSON(filePath);
26
+ const parsed = WorkshopCacheMetadataSchema.safeParse(raw);
27
+ return parsed.success ? parsed.data : null;
28
+ }
29
+ catch (error) {
30
+ if (error instanceof Error &&
31
+ 'code' in error &&
32
+ error.code === 'ENOENT') {
33
+ return null;
34
+ }
35
+ return null;
36
+ }
37
+ }
38
+ export async function ensureWorkshopCacheMetadataFile({ cacheDir, workshopId, displayName, repoName, subtitle, }) {
39
+ const filePath = getWorkshopCacheMetadataFilePath({ cacheDir, workshopId });
40
+ // Fast path: already exists and parses.
41
+ const existing = await readWorkshopCacheMetadataFile({ cacheDir, workshopId });
42
+ if (existing)
43
+ return existing;
44
+ const metadata = {
45
+ schemaVersion: 1,
46
+ workshopId,
47
+ displayName,
48
+ repoName,
49
+ subtitle,
50
+ createdAt: Date.now(),
51
+ };
52
+ try {
53
+ await writeWorkshopCacheMetadataAtomic({ filePath, metadata });
54
+ return metadata;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.84.3",
3
+ "version": "6.84.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },