Skip to content

Commit

Permalink
Make cache configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
Fryuni committed Dec 22, 2024
1 parent 7f93ad2 commit 641dfce
Show file tree
Hide file tree
Showing 7 changed files with 587 additions and 121 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-laws-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@domain-expansion/astro": minor
---

Make cache configurable
52 changes: 30 additions & 22 deletions package/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,54 @@ export class Cache {
this.persisted = new RenderFileStore(cacheDir);
}

public saveRenderValue(
public saveRenderValue({ key, factoryValue, ...options }: {
key: string,
factoryValue: AstroFactoryReturnValue,
persist: boolean = true,
): Promise<ValueThunk> {
const promise = persist
persist: boolean,
skipInMemory: boolean,
}): Promise<ValueThunk> {
const promise = options.persist
? this.persisted.saveRenderValue(key, factoryValue)
: RenderFileStore.denormalizeValue(factoryValue).then(result => result.clone);
this.valueCache.storeLoading(key, promise);
if (!options.skipInMemory) this.valueCache.storeLoading(key, promise);
return promise;
}

public async getRenderValue(
public async getRenderValue({ key, loadFresh, ...options }: {
key: string,
loadFresh: Thunk<MaybePromise<AstroFactoryReturnValue>>,
persist: boolean = true,
force: boolean = false,
): Promise<{ cached: boolean, value: ValueThunk }> {
const value = await this.getStoredRenderValue(key, force);
persist: boolean,
force: boolean,
skipInMemory: boolean
}): Promise<{ cached: boolean, value: ValueThunk }> {
const value = await this.getStoredRenderValue(key, options.force, options.skipInMemory);

if (value) return { cached: true, value };

return {
cached: false,
value: await this.saveRenderValue(key, await loadFresh(), persist),
value: await this.saveRenderValue({
...options,
key,
factoryValue: await loadFresh(),
}),
};
}

public async saveMetadata({ key, metadata, persist = true }: {
public saveMetadata({ key, metadata, persist, skipInMemory }: {
key: string,
metadata: PersistedMetadata,
persist: boolean
}): Promise<void> {
this.metadataCache.storeSync(key, metadata);
if (persist) {
await this.persisted.saveMetadata(key, metadata);
}
persist: boolean,
skipInMemory: boolean,
}): void {
if (!skipInMemory) this.metadataCache.storeSync(key, metadata);
if (persist) this.persisted.saveMetadata(key, metadata);
}

public async getMetadata(key: string): Promise<PersistedMetadata | null> {
public async getMetadata({ key, skipInMemory }: {
key: string,
skipInMemory: boolean,
}): Promise<PersistedMetadata | null> {
const fromMemory = this.metadataCache.get(key);
if (fromMemory) {
debug(`Retrieve metadata for "${key}" from memory`);
Expand All @@ -72,12 +80,12 @@ export class Cache {
inMemoryCacheMiss();

const newPromise = this.persisted.loadMetadata(key);
this.metadataCache.storeLoading(key, newPromise);
if (!skipInMemory) this.metadataCache.storeLoading(key, newPromise);

return newPromise;
}

private getStoredRenderValue(key: string, force: boolean): MaybePromise<ValueThunk | null> {
private getStoredRenderValue(key: string, force: boolean, skipInMemory: boolean): MaybePromise<ValueThunk | null> {
const fromMemory = this.valueCache.get(key);
if (fromMemory) {
debug(`Retrieve renderer for "${key}" from memory`);
Expand All @@ -90,7 +98,7 @@ export class Cache {
if (force) return null;

const newPromise = this.persisted.loadRenderer(key);
this.valueCache.storeLoading(key, newPromise);
if (!skipInMemory) this.valueCache.storeLoading(key, newPromise);

return newPromise;
}
Expand Down
59 changes: 48 additions & 11 deletions package/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,61 @@ import { interceptorPlugin } from "./interceptor.js";
import { collectMetrics } from "./metrics.js";
import chalk from "chalk";
import humanFormat from "human-format";
import { z } from "astro/zod";

function getDefaultCacheComponents(): false | 'in-memory' | 'persistent' {
const env = process.env.DOMAIN_EXPANSION_CACHE_COMPONENT;

switch (env) {
case 'false':
return false;
case 'in-memory':
return 'in-memory';
case 'persistent':
return 'persistent';
case '':
case undefined:
return 'in-memory';
default:
console.warn(chalk.bold.redBright(`Invalid environment variable value for component cache: ${env}`));
console.warn(chalk.italic.yellow('Assuming "in-memory" as default.'));
return 'in-memory';
}
}

export const integration = defineIntegration({
name: "@domain-expansion/astro",
setup() {
optionsSchema: z.object({
/**
* Whether non-page components should be cached.
*
* - `false` means not caching at all
*/
cacheComponents: z.enum(['in-memory', 'persistent'])
.or(z.literal(false))
.default(getDefaultCacheComponents()),
cachePages: z.boolean()
.default((process.env.DOMAIN_EXPANSION_CACHE_PAGES || 'true') === 'true'),
})
.default({}),
setup({ options }) {
const routeEntrypoints: string[] = [];

return {
hooks: {
'astro:routes:resolved': (params) => {
routeEntrypoints.push(...params.routes.map(route => route.entrypoint));
},
'astro:build:setup': ({ updateConfig, target }) => {
if (target === 'server') {
updateConfig({
plugins: [interceptorPlugin({
...options,
routeEntrypoints,
})],
});
}
},
'astro:config:setup': (params) => {
if (params.command !== 'build') return;

Expand Down Expand Up @@ -49,16 +96,6 @@ ${chalk.bold.cyan('[Domain Expansion report]')}
},
})
},
'astro:build:setup': ({ updateConfig, target }) => {
if (target === 'server') {
updateConfig({
plugins: [interceptorPlugin(routeEntrypoints)],
});
}
},
'astro:routes:resolved': (params) => {
routeEntrypoints.push(...params.routes.map(route => route.entrypoint));
},
},
};
},
Expand Down
9 changes: 7 additions & 2 deletions package/src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const EXCLUDED_MODULE_IDS: string[] = [
'\0astro:assets',
];

export const interceptorPlugin = (routeEntrypoints: string[]): Plugin => {
export const interceptorPlugin = (options: {
cacheComponents: false | 'in-memory' | 'persistent',
cachePages: boolean,
routeEntrypoints: string[],
}): Plugin => {
const componentHashes = new Map<string, string>();

return {
Expand All @@ -29,9 +33,10 @@ export const interceptorPlugin = (routeEntrypoints: string[]): Plugin => {
const { resolve: resolver } = createResolver(config.root);

(globalThis as any)[Symbol.for('@domain-expansion:astro-component-caching')] = makeCaching({
...options,
cache: new Cache(resolver('node_modules/.domain-expansion')),
root: config.root,
routeEntrypoints: routeEntrypoints.map(entrypoint => resolver(entrypoint)),
routeEntrypoints: options.routeEntrypoints.map(entrypoint => resolver(entrypoint)),
componentHashes,
});
},
Expand Down
76 changes: 45 additions & 31 deletions package/src/renderCaching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ interface ExtendedSSRResult extends SSRResult {
[ASSET_SERVICE_CALLS]: PersistedMetadata['assetServiceCalls'];
}

export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
export const makeCaching = ({ cache, routeEntrypoints, componentHashes, ...cacheOptions }: {
cache: Cache,
root: string,
routeEntrypoints: string[],
componentHashes: Map<string, string>,
cacheComponents: false | 'in-memory' | 'persistent',
cachePages: boolean,
}): CacheRenderingFn => (originalFn) => {
debug('Render caching called with:', { routeEntrypoints });

Expand Down Expand Up @@ -62,6 +64,15 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
}

function cacheFn(cacheScope: string, factory: AstroComponentFactory, moduleId?: string): AstroComponentFactory {
const isEntrypoint = routeEntrypoints.includes(moduleId!);
const cacheParams: Record<'persist' | 'skipInMemory', boolean> = {
persist: (
(isEntrypoint && cacheOptions.cachePages)
|| (!isEntrypoint && cacheOptions.cacheComponents === 'persistent')
),
skipInMemory: isEntrypoint || cacheOptions.cacheComponents !== false,
};

return async (result: ExtendedSSRResult, props, slots) => {
const context = getCurrentContext();

Expand All @@ -71,6 +82,8 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
}
}

if (!cacheParams.persist && cacheParams.skipInMemory) return factory(result, props, slots);

if (slots !== undefined && Object.keys(slots).length > 0) {
debug('Skip caching of component instance with children', { moduleId });
return factory(result, props, slots);
Expand Down Expand Up @@ -104,14 +117,12 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
return enterTrackingScope(async () => {
const cachedMetadata = await getValidMetadata(cacheKey);

const isEntrypoint = routeEntrypoints.includes(moduleId!);

const cachedValue = await cache.getRenderValue(
cacheKey,
() => factory(result, props, slots),
isEntrypoint,
!cachedMetadata,
);
const cachedValue = await cache.getRenderValue({
key: cacheKey,
loadFresh: () => factory(result, props, slots),
force: !cachedMetadata,
...cacheParams,
});

const resultValue = cachedValue.value()

Expand Down Expand Up @@ -166,7 +177,7 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {

const context = collectTracking();

await cache.saveMetadata({
cache.saveMetadata({
key: cacheKey,
metadata: {
...context,
Expand All @@ -178,7 +189,7 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
rendererSpecificHydrationScripts: rendererSpecificHydrationScriptsDiff(result._metadata.rendererSpecificHydrationScripts),
},
},
persist: !context.doNotCache && isEntrypoint,
...cacheParams,
});

return originalRender.call(templateResult, destination);
Expand All @@ -188,33 +199,36 @@ export const makeCaching = ({ cache, routeEntrypoints, componentHashes }: {
return resultValue;
});
}
}

async function getValidMetadata(cacheKey: string): Promise<PersistedMetadata | null> {
const cachedMetadata = await cache.getMetadata(cacheKey);
if (!cachedMetadata) return null;
async function getValidMetadata(cacheKey: string): Promise<PersistedMetadata | null> {
const cachedMetadata = await cache.getMetadata({
key: cacheKey,
...cacheParams,
});
if (!cachedMetadata) return null;

for (const [component, hash] of Object.entries(cachedMetadata.nestedComponents)) {
const currentHash = componentHashes.get(component);
if (currentHash !== hash) return null;
}
for (const [component, hash] of Object.entries(cachedMetadata.nestedComponents)) {
const currentHash = componentHashes.get(component);
if (currentHash !== hash) return null;
}

for (const { options, config, resultingAttributes } of cachedMetadata.assetServiceCalls) {
debug('Replaying getImage call', { options, config });
const result = await runtime.getImage(options, config);
for (const { options, config, resultingAttributes } of cachedMetadata.assetServiceCalls) {
debug('Replaying getImage call', { options, config });
const result = await runtime.getImage(options, config);

if (!isDeepStrictEqual(result.attributes, resultingAttributes)) {
debug('Image call mismatch, bailing out of cache');
return null;
if (!isDeepStrictEqual(result.attributes, resultingAttributes)) {
debug('Image call mismatch, bailing out of cache');
return null;
}
}
}

for (const entry of cachedMetadata.renderEntryCalls) {
const currentHash = await computeEntryHash(entry.filePath);
if (currentHash !== entry.hash) return null;
}
for (const entry of cachedMetadata.renderEntryCalls) {
const currentHash = await computeEntryHash(entry.filePath);
if (currentHash !== entry.hash) return null;
}

return cachedMetadata;
return cachedMetadata;
}
}
}

Expand Down
Loading

0 comments on commit 641dfce

Please sign in to comment.