diff --git a/packages/rstack/src/cachedImport.ts b/packages/rstack/src/cachedImport.ts new file mode 100644 index 00000000..24cae1a6 --- /dev/null +++ b/packages/rstack/src/cachedImport.ts @@ -0,0 +1,12 @@ +/** Share the pending import while allowing a failed import to be retried. */ +export const cachedImport = ( + importer: () => Promise, +): (() => Promise) => { + let promise: Promise | undefined; + + return () => + (promise ??= importer().catch((error: unknown) => { + promise = undefined; + throw error; + })); +}; diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index 74252679..dfe27df7 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -2,6 +2,7 @@ import { hash } from 'node:crypto'; import { readFileSync, writeFileSync } from 'node:fs'; +import { cachedImport } from '../cachedImport.ts'; import type { FmtCacheEntry } from './cacheStore.ts'; import { hasDottedBasename } from './pathHelpers.ts'; import type { FmtFileCache, FmtFileRequest, FmtWorkerResult } from './types.ts'; @@ -12,6 +13,8 @@ interface FormatFileTask { cache?: FmtFileCache; } +const getFormatModule = cachedImport(() => import('./format.ts')); + const hashContent = (content: string | Uint8Array): string => hash('sha256', content, 'base64url').slice(0, 16); @@ -65,7 +68,7 @@ const formatFile = async ({ } } - const { formatFmtSource } = await import('./format.ts'); + const { formatFmtSource } = await getFormatModule(); const result = await formatFmtSource(file, () => (source ??= readSource())); if (result.status === 'unsupported') { return cache diff --git a/packages/rstack/tests/cachedImport.test.ts b/packages/rstack/tests/cachedImport.test.ts new file mode 100644 index 00000000..5f590f58 --- /dev/null +++ b/packages/rstack/tests/cachedImport.test.ts @@ -0,0 +1,30 @@ +import { expect, rs, test } from 'rstack/test'; +import { cachedImport } from '../src/cachedImport.ts'; + +test('loads lazily and shares pending and fulfilled imports', async () => { + const module = { value: 1 }; + const importer = rs.fn(() => Promise.resolve(module)); + const load = cachedImport(importer); + + expect(importer).not.toHaveBeenCalled(); + const pending = load(); + expect(load()).toBe(pending); + await expect(pending).resolves.toBe(module); + await expect(load()).resolves.toBe(module); + expect(importer).toHaveBeenCalledTimes(1); +}); + +test('allows retrying a failed import', async () => { + const error = new Error('Failed to load module'); + const module = { value: 1 }; + const importer = rs + .fn<() => Promise>() + .mockRejectedValueOnce(error) + .mockResolvedValue(module); + const load = cachedImport(importer); + + await expect(load()).rejects.toBe(error); + await expect(load()).resolves.toBe(module); + await expect(load()).resolves.toBe(module); + expect(importer).toHaveBeenCalledTimes(2); +});