@epic-web/workshop-utils 6.84.3 → 6.84.5

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,54 @@ 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({
210
+ cacheDir,
211
+ workshopId,
212
+ });
213
+ if (await fsExtra.pathExists(filePath))
214
+ return;
215
+ }
216
+ catch {
217
+ // Treat errors as "missing" and attempt to re-create.
218
+ }
219
+ ensuredWorkshopCacheMetadata.delete(workshopId);
220
+ }
221
+ const { displayName, repoName, subtitle } = await getCurrentWorkshopDisplayInfo();
222
+ const metadata = await ensureWorkshopCacheMetadataFile({
223
+ cacheDir,
224
+ workshopId,
225
+ displayName,
226
+ repoName,
227
+ subtitle,
228
+ });
229
+ if (metadata) {
230
+ ensuredWorkshopCacheMetadata.add(workshopId);
231
+ }
232
+ }
183
233
  export function makeGlobalFsCache(name) {
184
234
  return remember(`global-${name}`, () => {
185
235
  const cacheInstanceDir = path.join(cacheDir, 'global', name);
@@ -419,6 +469,9 @@ export async function getGlobalCaches() {
419
469
  workshopId: 'global',
420
470
  }));
421
471
  }
472
+ export async function readWorkshopCacheMetadata(workshopId) {
473
+ return readWorkshopCacheMetadataFile({ cacheDir, workshopId });
474
+ }
422
475
  export async function getWorkshopFileCaches() {
423
476
  const workshopCacheDir = path.join(cacheDir, getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID);
424
477
  const caches = readJsonFilesInDirectory(workshopCacheDir);
@@ -436,6 +489,7 @@ export async function deleteCache() {
436
489
  if (await fsExtra.exists(cacheDir)) {
437
490
  await fsExtra.remove(cacheDir);
438
491
  }
492
+ ensuredWorkshopCacheMetadata.clear();
439
493
  }
440
494
  catch (error) {
441
495
  console.error(`Error deleting the cache in ${cacheDir}`, error);
@@ -469,6 +523,7 @@ export async function deleteWorkshopCache(workshopId, cacheName) {
469
523
  if (await fsExtra.exists(workshopCachePath)) {
470
524
  await fsExtra.remove(workshopCachePath);
471
525
  }
526
+ ensuredWorkshopCacheMetadata.delete(workshopId);
472
527
  }
473
528
  }
474
529
  catch (error) {
@@ -526,7 +581,8 @@ export function makeSingletonCache(name) {
526
581
  }
527
582
  export function makeSingletonFsCache(name) {
528
583
  return remember(name, () => {
529
- const cacheInstanceDir = path.join(cacheDir, getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID, name);
584
+ const workshopId = getEnv().EPICSHOP_WORKSHOP_INSTANCE_ID;
585
+ const cacheInstanceDir = path.join(cacheDir, workshopId, name);
530
586
  const fsCache = {
531
587
  name: `Filesystem cache (${name})`,
532
588
  async get(key) {
@@ -558,6 +614,12 @@ export function makeSingletonFsCache(name) {
558
614
  return null;
559
615
  },
560
616
  async set(key, entry) {
617
+ try {
618
+ await ensureWorkshopCacheMetadata(workshopId);
619
+ }
620
+ catch {
621
+ // Ignore: this is optional metadata only.
622
+ }
561
623
  const filePath = path.join(cacheInstanceDir, md5(key));
562
624
  const tempPath = `${filePath}.tmp`;
563
625
  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.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },