Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cli/commands/serve/proxy-extension-composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "#veryfront/schemas/_test-setup.ts";
import { assertEquals, assertRejects, assertStrictEquals } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import { type ExtensionLoader, tryResolve } from "veryfront/extensions";
import { RedisRuntimeProviderName } from "veryfront/extensions/distributed";
import type { TokenCacheStore } from "#veryfront/extensions/cache/index.ts";
import { createCacheFromEnv, TracingTokenCache } from "#veryfront/proxy/cache/index.ts";
import { acquireExtensionTokenCacheStoreFromEnv } from "#veryfront/proxy/cache/extension-store.ts";
Expand All @@ -28,12 +29,23 @@ describe("standalone proxy extension composition", () => {

it("does not import or activate a cache provider for the memory backend", async () => {
Deno.env.set("CACHE_TYPE", "memory");
Deno.env.delete("REDIS_URL");

loader = await activateStandaloneProxyCacheExtension();

assertEquals(loader, null);
});

it("activates ext-redis when routing invalidation uses Redis", async () => {
Deno.env.set("CACHE_TYPE", "memory");
Deno.env.set("REDIS_URL", "redis://127.0.0.1:6379");

loader = await activateStandaloneProxyCacheExtension();

assertEquals(loader !== null, true);
assertEquals(tryResolve(RedisRuntimeProviderName) !== undefined, true);
});

it("activates ext-cache-redis before standalone cache acquisition", async () => {
Deno.env.set("CACHE_TYPE", "extension");
Deno.env.set("REDIS_URL", "redis://127.0.0.1:6379");
Expand All @@ -44,6 +56,7 @@ describe("standalone proxy extension composition", () => {
const acquisition = await acquireExtensionTokenCacheStoreFromEnv();

assertEquals(loader !== null, true);
assertEquals(tryResolve(RedisRuntimeProviderName) !== undefined, true);
assertEquals(acquisition.kind, "borrowed");
assertStrictEquals(
acquisition.store,
Expand All @@ -60,6 +73,7 @@ describe("standalone proxy extension composition", () => {

assertEquals(await shutdownHooks.settle(), []);
assertEquals(tryResolve<TokenCacheStore>("TokenCacheStore"), undefined);
assertEquals(tryResolve(RedisRuntimeProviderName), undefined);
loader = null;
});

Expand All @@ -77,6 +91,7 @@ describe("standalone proxy extension composition", () => {
"shutdown registration failed",
);
assertEquals(tryResolve<TokenCacheStore>("TokenCacheStore"), undefined);
assertEquals(tryResolve(RedisRuntimeProviderName), undefined);
loader = null;
});

Expand All @@ -97,6 +112,7 @@ describe("standalone proxy extension composition", () => {
"shutdown-hook disposal failed",
);
assertEquals(tryResolve<TokenCacheStore>("TokenCacheStore"), undefined);
assertEquals(tryResolve(RedisRuntimeProviderName), undefined);
loader = null;
});

Expand Down Expand Up @@ -137,6 +153,7 @@ describe("standalone proxy extension composition", () => {
}
await cleanup;
assertEquals(tryResolve<TokenCacheStore>("TokenCacheStore"), undefined);
assertEquals(tryResolve(RedisRuntimeProviderName), undefined);
loader = null;
});
});
76 changes: 54 additions & 22 deletions cli/commands/serve/proxy-extension-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
registerProxyShutdownHook,
} from "veryfront/proxy/shutdown-hooks";

type CacheExtensionModule = Readonly<{ default: ExtensionFactory }>;
type FirstPartyExtensionModule = Readonly<{ default: ExtensionFactory }>;

// This module is evaluated before extension activation. Pin promises created
// by the later teardown path so extension code cannot replace Promise species
Expand All @@ -42,43 +42,75 @@ const resolvedBeforeExtensionActivation = pinCompositionPromise(

const CACHE_EXTENSION_SOURCE_DIRECTORY = "ext-cache-redis";
const CACHE_EXTENSION_PACKAGE_NAME = "@veryfront/ext-cache-redis";
const REDIS_EXTENSION_SOURCE_DIRECTORY = "ext-redis";
const REDIS_EXTENSION_PACKAGE_NAME = "@veryfront/ext-redis";

async function loadFirstPartyExtension(
sourceDirectory: string,
packageName: string,
): Promise<ExtensionFactory> {
const module = await importFirstPartyExtensionModule<FirstPartyExtensionModule>(
sourceDirectory,
packageName,
);
if (typeof module.default !== "function") {
throw new NativeTypeError(`${packageName} must export an ExtensionFactory`);
}
return module.default;
}

/**
* Activate the standalone proxy's explicitly selected cache extension.
* Memory mode performs no extension import. The returned loader owns provider
* teardown; the proxy borrows its registered `TokenCacheStore`.
* Activate the standalone proxy's explicitly selected infrastructure extensions.
* The returned loader owns provider teardown; the proxy borrows the registered
* Redis runtime and optional `TokenCacheStore` providers.
*/
async function activateStandaloneProxyCacheExtensionInternal(): Promise<ExtensionLoader | null> {
const cacheType = getEnv("CACHE_TYPE") || "memory";
if (cacheType !== "memory" && cacheType !== "extension") {
throw new NativeTypeError("CACHE_TYPE must be memory or extension");
}
if (cacheType === "memory") return null;

const module = await importFirstPartyExtensionModule<CacheExtensionModule>(
CACHE_EXTENSION_SOURCE_DIRECTORY,
CACHE_EXTENSION_PACKAGE_NAME,
);
if (typeof module.default !== "function") {
throw new NativeTypeError(`${CACHE_EXTENSION_PACKAGE_NAME} must export an ExtensionFactory`);
const extensions: Array<{
extension: ReturnType<ExtensionFactory>;
origin: string;
source: "config";
}> = [];
if (getEnv("REDIS_URL")) {
const redisExtension = await loadFirstPartyExtension(
REDIS_EXTENSION_SOURCE_DIRECTORY,
REDIS_EXTENSION_PACKAGE_NAME,
);
extensions.push({
extension: redisExtension(),
source: "config",
origin: "standalone proxy Redis runtime",
});
}
if (cacheType === "extension") {
const cacheExtension = await loadFirstPartyExtension(
CACHE_EXTENSION_SOURCE_DIRECTORY,
CACHE_EXTENSION_PACKAGE_NAME,
);
extensions.push({
extension: cacheExtension(),
source: "config",
origin: "standalone proxy cache selection",
});
}
if (extensions.length === 0) return null;

const loader = new ExtensionLoader(cliLogger);
try {
await loader.setupAll(
[{
extension: module.default(),
source: "config",
origin: "standalone proxy cache selection",
}],
{},
);
await loader.setupAll(extensions, {});
return loader;
} catch (error) {
try {
Comment thread
kwakayama marked this conversation as resolved.
await loader.teardownAll();
} catch (cleanupError) {
cliLogger.error("Failed to clean up standalone proxy cache extension", cleanupError);
cliLogger.error(
"Failed to clean up standalone proxy infrastructure extensions",
cleanupError,
);
}
throw error;
}
Expand Down Expand Up @@ -114,7 +146,7 @@ async function registerStandaloneProxyCacheExtensionTeardownInternal(
} catch (cleanupError) {
throw createProxyShutdownAggregateError(
[error, cleanupError],
"Failed to register and clean up standalone proxy cache extension teardown",
"Failed to register and clean up standalone proxy infrastructure extension teardown",
);
}
throw error;
Expand All @@ -137,7 +169,7 @@ async function registerStandaloneProxyCacheExtensionTeardownInternal(
if (disposalFailed) {
throw createProxyShutdownAggregateError(
[disposalError, teardownError],
"Failed to unregister and tear down standalone proxy cache extension",
"Failed to unregister and tear down standalone proxy infrastructure extensions",
);
}
throw teardownError;
Expand Down