This commit is contained in:
John Yeo
2026-04-01 12:34:53 +01:00
parent bd959cd6ce
commit 5dd82c5688
8 changed files with 806 additions and 192 deletions

View File

@@ -0,0 +1,171 @@
---
name: edge-config
description: Understand and create S3-backed edge configs with poll-based caching. Use when adding new runtime configs (feature rollouts, request blocking, operational toggles), debugging edge config polling, or working with the EdgeConfigStore factory.
---
# Edge Configs
Edge configs are JSON files stored in S3 that are polled into server memory at a regular cadence. They let us change server behavior at runtime without deploys -- load shedding, feature rollouts, operational toggles, etc.
## Architecture
Each edge config has its own S3 file under the `admin/` prefix in the admin S3 bucket (`autumn-dev-server` / `autumn-prod-server`). Configs are independent -- a parse error in one does not affect another.
### Key files
| File | Purpose |
|------|---------|
| `server/src/internal/misc/edgeConfig/edgeConfigStore.ts` | `createEdgeConfigStore<T>` factory -- generic S3 read/write, polling, status tracking |
| `server/src/internal/misc/edgeConfig/edgeConfigRegistry.ts` | Registry -- `registerEdgeConfig`, `startAllEdgeConfigPolling`, `stopAllEdgeConfigPolling` |
| `server/src/init.ts` | Calls `startAllEdgeConfigPolling` on boot, `stopAllEdgeConfigPolling` on shutdown |
| `server/src/external/aws/s3/adminS3Config.ts` | Bucket/region resolution (dev vs prod) |
### Existing configs
| Config | S3 Key | Module |
|--------|--------|--------|
| Request blocks | `admin/request-block-config.json` | `server/src/internal/misc/requestBlocks/requestBlockStore.ts` |
## Lifecycle
```
Boot:
1. Config module is imported (top-level side effect) -> calls registerEdgeConfig()
2. init.ts calls startAllEdgeConfigPolling({ logger })
3. Each store: await refresh() (initial S3 fetch, blocks until done)
4. Each store: setInterval(refresh, pollIntervalMs)
5. Server starts accepting traffic
Runtime:
- get() reads from in-memory cache (zero I/O, sync)
- refresh() runs every pollIntervalMs (fire-and-forget)
- writeToSource() writes to S3 + updates local cache immediately
Shutdown:
- stopAllEdgeConfigPolling() clears all intervals
```
## Fail-Open Guarantee
On **any** S3 failure (network error, auth error, bad JSON, schema mismatch), `runtimeConfig` resets to `defaultValue()`. This means:
- No request is ever blocked due to infrastructure failure
- If S3 goes down, configs degrade to their default (empty/safe) state
- The store does NOT hold onto stale data -- it resets to default
`NoSuchKey` (file doesn't exist yet) is treated as a normal empty state, not an error.
## How to Add a New Edge Config
### 1. Define the schema
Create a schemas file with a Zod schema:
```typescript
// server/src/internal/misc/featureRollouts/featureRolloutSchemas.ts
import { z } from "zod/v4";
export const FeatureRolloutConfigSchema = z.object({
features: z.record(z.string(), z.object({
percentage: z.number().min(0).max(100).default(0),
enabled: z.boolean().default(false),
})).default({}),
});
export type FeatureRolloutConfig = z.infer<typeof FeatureRolloutConfigSchema>;
```
### 2. Create the store module
```typescript
// server/src/internal/misc/featureRollouts/featureRolloutStore.ts
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
import {
type FeatureRolloutConfig,
FeatureRolloutConfigSchema,
} from "./featureRolloutSchemas.js";
const store = createEdgeConfigStore<FeatureRolloutConfig>({
s3Key: "admin/feature-rollout-config.json",
schema: FeatureRolloutConfigSchema,
defaultValue: () => ({ features: {} }),
pollIntervalMs: 300_000, // 5 minutes
});
registerEdgeConfig({ store });
export const getFeatureRolloutConfig = () => store.get();
export const isFeatureEnabled = ({
featureId,
}: {
featureId: string;
}): boolean => {
const feature = store.get().features[featureId];
return feature?.enabled ?? false;
};
```
### 3. Register the import in init.ts
Add a side-effect import so the module runs `registerEdgeConfig` at boot:
```typescript
// server/src/init.ts
import "./internal/misc/featureRollouts/featureRolloutStore.js";
```
### 4. (Optional) Add the S3 key constant
```typescript
// server/src/external/aws/s3/adminS3Config.ts
export const ADMIN_FEATURE_ROLLOUT_CONFIG_KEY = "admin/feature-rollout-config.json";
```
### 5. Use it
```typescript
import { isFeatureEnabled } from "@/internal/misc/featureRollouts/featureRolloutStore.js";
if (isFeatureEnabled({ featureId: "new-billing-engine" })) {
// new path
}
```
## Factory API Reference
```typescript
const store = createEdgeConfigStore<T>({
s3Key: string, // S3 object key under admin bucket
schema: z.ZodType<T>, // Zod schema for validation
defaultValue: () => T, // Factory for empty/safe default
pollIntervalMs?: number, // Poll interval (default: 60_000)
s3Client?: S3Client, // Optional DI for testing
});
store.get() // T -- in-memory cached config (sync, no I/O)
store.getStatus() // EdgeConfigStatus -- health/timing info
store.refresh({ logger? }) // Re-fetch from S3, update cache
store.startPolling({ logger? }) // Initial fetch + start interval
store.stopPolling() // Clear interval
store.readFromSource() // Direct S3 read (bypasses cache)
store.writeToSource({ config }) // S3 write + update cache immediately
```
## Testing
Unit tests use the `s3Client` DI parameter to inject a mock:
```typescript
const mockClient = { send: jest.fn(async () => ({ Body: ... })) } as unknown as S3Client;
const store = createEdgeConfigStore<MyConfig>({
s3Key: "admin/test.json",
schema: MyConfigSchema,
defaultValue: () => ({ ... }),
s3Client: mockClient,
});
```
Existing tests: `server/tests/unit/edge-config/edge-config-store.test.ts`

View File

@@ -22,8 +22,6 @@ export const requestBlockMiddleware = async (
return;
}
console.log("Entry:", entry);
if (entry.blockAll) {
ctx.logger.warn("Rejecting blocked /v1 request (block all)");

View File

@@ -12,9 +12,12 @@ import {
} from "./db/pgHealthMonitor.js";
import { logger } from "./external/logtail/logtailUtils.js";
import {
startRequestBlockPolling,
stopRequestBlockPolling,
} from "./internal/misc/requestBlocks/requestBlockStore.js";
startAllEdgeConfigPolling,
stopAllEdgeConfigPolling,
} from "./internal/misc/edgeConfig/edgeConfigRegistry.js";
// Edge config modules self-register on import
import "./internal/misc/requestBlocks/requestBlockStore.js";
import { warmupRegionalRedis } from "./external/redis/initRedis.js";
import { createHonoApp } from "./initHono.js";
import { otelSdk } from "./instrumentation.js";
@@ -28,7 +31,7 @@ const init = async () => {
initPgHealthMonitor({ client: clientCritical });
await Promise.all([warmupRegionalRedis()]);
await startRequestBlockPolling({ logger });
await startAllEdgeConfigPolling({ logger });
const PORT = process.env.SERVER_PORT
? Number.parseInt(process.env.SERVER_PORT)
@@ -88,7 +91,7 @@ async function gracefulShutdown() {
await otelSdk.shutdown();
}
shutdownPgHealthMonitor();
stopRequestBlockPolling();
stopAllEdgeConfigPolling();
await Promise.all([
client.end(),
clientCritical.end(),

View File

@@ -12,7 +12,7 @@ export const handleGetAdminOrgRequestBlock = createRoute({
handler: async (c) => {
const { org_id: orgId } = c.req.valid("param");
const status = getRuntimeRequestBlockStatus();
const entry = await getOrgRequestBlockFromSource(orgId);
const entry = await getOrgRequestBlockFromSource({ orgId });
return c.json({
blockAll: entry?.blockAll ?? false,

View File

@@ -0,0 +1,28 @@
import type { Logger } from "@/external/logtail/logtailUtils.js";
type EdgeConfigLifecycle = {
startPolling: (options?: { logger?: Logger }) => Promise<void>;
stopPolling: () => void;
};
const stores: EdgeConfigLifecycle[] = [];
export const registerEdgeConfig = ({
store,
}: {
store: EdgeConfigLifecycle;
}) => {
stores.push(store);
};
export const startAllEdgeConfigPolling = async ({
logger,
}: {
logger?: Logger;
} = {}) => {
await Promise.all(stores.map((store) => store.startPolling({ logger })));
};
export const stopAllEdgeConfigPolling = () => {
for (const store of stores) store.stopPolling();
};

View File

@@ -0,0 +1,187 @@
import { ErrCode, ms } from "@autumn/shared";
import type { S3Client } from "@aws-sdk/client-s3";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import type { z } from "zod/v4";
import { getAdminS3Config } from "@/external/aws/s3/adminS3Config.js";
import { getS3Client } from "@/external/aws/s3/initS3.js";
import { getS3BodyAsString } from "@/external/aws/s3/s3Utils.js";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import RecaseError from "@/utils/errorUtils.js";
export type EdgeConfigStatus = {
configured: boolean;
healthy: boolean;
lastFetchAt?: string;
lastSuccessAt?: string;
error?: string;
};
const nowIso = () => new Date().toISOString();
/**
* Factory that creates a typed, poll-based edge config backed by S3.
* Fail-open: any S3 error resets the in-memory config to `defaultValue()`.
*/
export const createEdgeConfigStore = <T>({
s3Key,
schema,
defaultValue,
pollIntervalMs = ms.seconds(30),
s3Client: injectedS3Client,
}: {
s3Key: string;
schema: z.ZodType<T>;
defaultValue: () => T;
pollIntervalMs?: number;
s3Client?: S3Client;
}) => {
let runtimeConfig: T = defaultValue();
let runtimeStatus: EdgeConfigStatus = {
configured: false,
healthy: false,
error: "Edge config not yet initialized",
};
let pollTimer: ReturnType<typeof setInterval> | null = null;
const getConfigLocation = () => {
const { bucket, region } = getAdminS3Config();
return {
bucket,
region,
key: s3Key,
configured: Boolean(bucket && s3Key),
};
};
const resolveClient = () => {
if (injectedS3Client) return injectedS3Client;
const { region } = getConfigLocation();
return getS3Client({ region });
};
const readFromSource = async (): Promise<T> => {
const { bucket, key, configured } = getConfigLocation();
if (!configured || !bucket || !key) return defaultValue();
const client = resolveClient();
try {
const response = await client.send(
new GetObjectCommand({ Bucket: bucket, Key: key }),
);
if (!response.Body) return defaultValue();
const raw = (await getS3BodyAsString({ body: response.Body })).trim();
if (!raw) return defaultValue();
return schema.parse(JSON.parse(raw));
} catch (error) {
const name = error instanceof Error ? error.name : "";
if (name === "NoSuchKey") return defaultValue();
throw error;
}
};
const writeToSource = async ({ config }: { config: T }) => {
const { bucket, key, configured } = getConfigLocation();
if (!configured || !bucket || !key) {
throw new RecaseError({
message: "Edge config S3 is not configured",
code: ErrCode.InvalidRequest,
statusCode: 503,
});
}
const client = resolveClient();
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: JSON.stringify(config, null, 2),
ContentType: "application/json",
}),
);
runtimeConfig = config;
runtimeStatus = {
configured: true,
healthy: true,
lastFetchAt: nowIso(),
lastSuccessAt: nowIso(),
};
};
const refresh = async ({ logger }: { logger?: Logger } = {}) => {
const { configured } = getConfigLocation();
runtimeStatus = {
...runtimeStatus,
configured,
lastFetchAt: nowIso(),
};
if (!configured) {
runtimeConfig = defaultValue();
runtimeStatus = {
configured: false,
healthy: false,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: runtimeStatus.lastSuccessAt,
error: "Edge config S3 is not configured",
};
return;
}
try {
const config = await readFromSource();
runtimeConfig = config;
runtimeStatus = {
configured: true,
healthy: true,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: nowIso(),
};
} catch (error) {
runtimeConfig = defaultValue();
runtimeStatus = {
configured: true,
healthy: false,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: runtimeStatus.lastSuccessAt,
error: error instanceof Error ? error.message : "Failed to load config",
};
logger?.warn(`Failed to refresh edge config "${s3Key}": ${error}`);
}
};
const startPolling = async ({ logger }: { logger?: Logger } = {}) => {
if (pollTimer) return;
await refresh({ logger });
pollTimer = setInterval(() => {
void refresh({ logger });
}, pollIntervalMs);
};
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
};
return {
get: () => runtimeConfig,
getStatus: () => runtimeStatus,
refresh,
startPolling,
stopPolling,
readFromSource,
writeToSource,
};
};
export type EdgeConfigStore<T = unknown> = ReturnType<
typeof createEdgeConfigStore<T>
>;

View File

@@ -1,13 +1,6 @@
import { ErrCode } from "@autumn/shared";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import {
ADMIN_REQUEST_BLOCK_CONFIG_KEY,
getAdminS3Config,
} from "@/external/aws/s3/adminS3Config.js";
import { getS3Client } from "@/external/aws/s3/initS3.js";
import { getS3BodyAsString } from "@/external/aws/s3/s3Utils.js";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { ADMIN_REQUEST_BLOCK_CONFIG_KEY } from "@/external/aws/s3/adminS3Config.js";
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
import {
type RequestBlockConfig,
RequestBlockConfigSchema,
@@ -15,179 +8,32 @@ import {
type RequestBlockUpdate,
} from "./requestBlockSchemas.js";
const POLL_INTERVAL_MS = 60_000;
type RequestBlockStatus = {
configured: boolean;
healthy: boolean;
lastFetchAt?: string;
lastSuccessAt?: string;
error?: string;
};
const emptyConfig = (): RequestBlockConfig => ({ orgs: {} });
let runtimeConfig: RequestBlockConfig = emptyConfig();
let runtimeStatus: RequestBlockStatus = {
configured: false,
healthy: false,
error: "Request block config is not configured",
};
let pollTimer: ReturnType<typeof setInterval> | null = null;
const getConfigLocation = () => {
const { bucket, region } = getAdminS3Config();
const key = ADMIN_REQUEST_BLOCK_CONFIG_KEY;
return {
bucket,
key,
region,
configured: Boolean(bucket && key),
};
};
const readConfigFromS3 = async (): Promise<RequestBlockConfig> => {
const { bucket, key, configured, region } = getConfigLocation();
if (!configured || !bucket || !key) {
return emptyConfig();
}
const client = getS3Client({ region });
try {
const response = await client.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
}),
);
if (!response.Body) {
return emptyConfig();
}
const raw = (await getS3BodyAsString({ body: response.Body })).trim();
if (!raw) {
return emptyConfig();
}
return RequestBlockConfigSchema.parse(JSON.parse(raw));
} catch (error) {
const name = error instanceof Error ? error.name : "";
if (name === "NoSuchKey") {
return emptyConfig();
}
throw error;
}
};
const writeConfigToS3 = async (config: RequestBlockConfig) => {
const { bucket, key, configured, region } = getConfigLocation();
if (!configured || !bucket || !key) {
throw new RecaseError({
message: "Request block config is not configured",
code: ErrCode.InvalidRequest,
statusCode: 503,
});
}
const client = getS3Client({ region });
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: JSON.stringify(config, null, 2),
ContentType: "application/json",
}),
);
};
const nowIso = () => new Date().toISOString();
export const getRuntimeRequestBlockStatus = () => runtimeStatus;
const store = createEdgeConfigStore<RequestBlockConfig>({
s3Key: ADMIN_REQUEST_BLOCK_CONFIG_KEY,
schema: RequestBlockConfigSchema,
defaultValue: () => ({ orgs: {} }),
});
registerEdgeConfig({ store });
export const getRuntimeRequestBlockStatus = () => store.getStatus();
export const getRuntimeRequestBlockEntry = (
orgId: string,
): RequestBlockEntry | undefined => runtimeConfig.orgs[orgId];
export const getRuntimeRequestBlockConfig = () => runtimeConfig;
export const refreshRequestBlockConfig = async ({
logger,
}: {
logger?: Logger;
} = {}) => {
const { configured } = getConfigLocation();
runtimeStatus = {
...runtimeStatus,
configured,
lastFetchAt: nowIso(),
};
if (!configured) {
runtimeConfig = emptyConfig();
runtimeStatus = {
configured: false,
healthy: false,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: runtimeStatus.lastSuccessAt,
error: "Request block config is not configured",
};
return;
}
try {
const config = await readConfigFromS3();
runtimeConfig = config;
runtimeStatus = {
configured: true,
healthy: true,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: nowIso(),
};
} catch (error) {
runtimeConfig = emptyConfig();
runtimeStatus = {
configured: true,
healthy: false,
lastFetchAt: runtimeStatus.lastFetchAt,
lastSuccessAt: runtimeStatus.lastSuccessAt,
error: error instanceof Error ? error.message : "Failed to load config",
};
logger?.warn(`Failed to refresh request block config: ${error}`);
}
};
export const startRequestBlockPolling = async ({
logger,
}: {
logger?: Logger;
} = {}) => {
if (pollTimer) {
return;
}
await refreshRequestBlockConfig({ logger });
pollTimer = setInterval(() => {
void refreshRequestBlockConfig({ logger });
}, POLL_INTERVAL_MS);
};
export const stopRequestBlockPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
};
): RequestBlockEntry | undefined => store.get().orgs[orgId];
export const getRequestBlockConfigFromSource = async () => {
return await readConfigFromS3();
return await store.readFromSource();
};
export const getOrgRequestBlockFromSource = async (orgId: string) => {
const config = await readConfigFromS3();
export const getOrgRequestBlockFromSource = async ({
orgId,
}: {
orgId: string;
}) => {
const config = await store.readFromSource();
return config.orgs[orgId];
};
@@ -200,7 +46,7 @@ export const updateOrgRequestBlockInSource = async ({
update: RequestBlockUpdate;
updatedBy?: string;
}) => {
const config = await readConfigFromS3();
const config = await store.readFromSource();
const shouldDelete = !update.blockAll && update.blockedEndpoints.length === 0;
if (shouldDelete) {
@@ -214,15 +60,7 @@ export const updateOrgRequestBlockInSource = async ({
};
}
await writeConfigToS3(config);
runtimeConfig = config;
runtimeStatus = {
configured: true,
healthy: true,
lastFetchAt: nowIso(),
lastSuccessAt: nowIso(),
};
await store.writeToSource({ config });
return config.orgs[orgId];
};

View File

@@ -0,0 +1,389 @@
import { afterEach, describe, expect, jest, test } from "bun:test";
import type { S3Client } from "@aws-sdk/client-s3";
import { z } from "zod/v4";
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
const TestConfigSchema = z.object({
enabled: z.boolean().default(false),
message: z.string().default("hello"),
});
type TestConfig = z.infer<typeof TestConfigSchema>;
const defaultConfig = (): TestConfig => ({ enabled: false, message: "hello" });
const createMockS3Client = ({
getResponse,
}: {
getResponse: () => {
Body?: { transformToString: () => Promise<string> } | null;
};
}): S3Client => {
const sendFn = jest.fn(async (command: unknown) => {
const commandName =
command?.constructor?.name ?? (command as { name?: string })?.name;
if (commandName === "GetObjectCommand") {
return getResponse();
}
if (commandName === "PutObjectCommand") {
return {};
}
throw new Error(`Unexpected command: ${commandName}`);
});
return { send: sendFn } as unknown as S3Client;
};
const makeBody = (data: unknown) => ({
Body: {
transformToString: async () => JSON.stringify(data),
},
});
const makeNoSuchKeyError = () => {
const error = new Error("NoSuchKey");
error.name = "NoSuchKey";
return error;
};
describe("createEdgeConfigStore", () => {
let store: ReturnType<typeof createEdgeConfigStore<TestConfig>>;
afterEach(() => {
store?.stopPolling();
});
describe("initial fetch via startPolling", () => {
test("populates get() with parsed config from S3", async () => {
const mockClient = createMockS3Client({
getResponse: () => makeBody({ enabled: true, message: "from-s3" }),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual({ enabled: true, message: "from-s3" });
expect(store.getStatus().healthy).toBe(true);
expect(store.getStatus().configured).toBe(true);
expect(store.getStatus().lastSuccessAt).toBeDefined();
});
test("uses defaultValue before startPolling is called", () => {
const mockClient = createMockS3Client({
getResponse: () => makeBody({ enabled: true, message: "from-s3" }),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
expect(store.get()).toEqual(defaultConfig());
});
});
describe("fail-open behavior", () => {
test("returns defaultValue when S3 throws a network error", async () => {
const mockClient = createMockS3Client({
getResponse: () => {
throw new Error("NetworkingError: socket hang up");
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(false);
expect(store.getStatus().error).toContain("NetworkingError");
});
test("returns defaultValue when S3 file does not exist (NoSuchKey)", async () => {
const mockClient = createMockS3Client({
getResponse: () => {
throw makeNoSuchKeyError();
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(true);
});
test("returns defaultValue when S3 body is malformed JSON", async () => {
const mockClient = createMockS3Client({
getResponse: () => ({
Body: {
transformToString: async () => "not-valid-json{{{",
},
}),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(false);
});
test("returns defaultValue when S3 body is null", async () => {
const mockClient = createMockS3Client({
getResponse: () => ({ Body: null }),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(true);
});
test("returns defaultValue when S3 body is empty string", async () => {
const mockClient = createMockS3Client({
getResponse: () => ({
Body: { transformToString: async () => " " },
}),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(true);
});
test("returns defaultValue when schema validation fails", async () => {
const strictSchema = z.object({
enabled: z.boolean(),
message: z.string(),
requiredField: z.string(),
});
const mockClient = createMockS3Client({
getResponse: () =>
makeBody({ enabled: true, message: "no-required-field" }),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: strictSchema as unknown as z.ZodType<TestConfig>,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get()).toEqual(defaultConfig());
expect(store.getStatus().healthy).toBe(false);
});
});
describe("refresh", () => {
test("updates cached config when S3 content changes", async () => {
let callCount = 0;
const mockClient = createMockS3Client({
getResponse: () => {
callCount++;
if (callCount === 1) {
return makeBody({ enabled: false, message: "first" });
}
return makeBody({ enabled: true, message: "second" });
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get().message).toBe("first");
await store.refresh();
expect(store.get().message).toBe("second");
expect(store.get().enabled).toBe(true);
});
});
describe("writeToSource", () => {
test("updates local cache immediately after write", async () => {
const mockClient = createMockS3Client({
getResponse: () => makeBody(defaultConfig()),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get().enabled).toBe(false);
await store.writeToSource({
config: { enabled: true, message: "written" },
});
expect(store.get()).toEqual({ enabled: true, message: "written" });
expect(store.getStatus().healthy).toBe(true);
});
test("calls S3 PutObject with correct payload", async () => {
const mockClient = createMockS3Client({
getResponse: () => makeBody(defaultConfig()),
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.writeToSource({
config: { enabled: true, message: "test-write" },
});
const sendFn = (
mockClient as unknown as { send: ReturnType<typeof jest.fn> }
).send;
const calls = sendFn.mock.calls;
const lastCall = calls[calls.length - 1]?.[0];
expect(lastCall?.constructor?.name).toBe("PutObjectCommand");
});
});
describe("readFromSource", () => {
test("returns fresh data from S3 without updating cache", async () => {
let callCount = 0;
const mockClient = createMockS3Client({
getResponse: () => {
callCount++;
return makeBody({
enabled: callCount > 1,
message: `call-${callCount}`,
});
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
s3Client: mockClient,
});
await store.startPolling();
expect(store.get().message).toBe("call-1");
const fresh = await store.readFromSource();
expect(fresh.message).toBe("call-2");
expect(store.get().message).toBe("call-1");
});
});
describe("polling lifecycle", () => {
test("double startPolling does not create a second interval", async () => {
let callCount = 0;
const mockClient = createMockS3Client({
getResponse: () => {
callCount++;
return makeBody(defaultConfig());
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
pollIntervalMs: 50,
s3Client: mockClient,
});
await store.startPolling();
await store.startPolling();
const countAfterStart = callCount;
await new Promise((resolve) => setTimeout(resolve, 130));
const countAfterWait = callCount;
const intervalFetchCount = countAfterWait - countAfterStart;
expect(intervalFetchCount).toBeGreaterThanOrEqual(1);
expect(intervalFetchCount).toBeLessThanOrEqual(3);
});
test("stopPolling prevents further refreshes", async () => {
let callCount = 0;
const mockClient = createMockS3Client({
getResponse: () => {
callCount++;
return makeBody(defaultConfig());
},
});
store = createEdgeConfigStore<TestConfig>({
s3Key: "admin/test-config.json",
schema: TestConfigSchema,
defaultValue: defaultConfig,
pollIntervalMs: 50,
s3Client: mockClient,
});
await store.startPolling();
store.stopPolling();
const countAfterStop = callCount;
await new Promise((resolve) => setTimeout(resolve, 150));
expect(callCount).toBe(countAfterStop);
});
});
});