feat(core-shared): add withRateLimit wrapper and conformance enforcement
- Add withRateLimit(rateLimit, fn) in rate-limit/with-rate-limit.ts, attaching the RateLimited brand at DI bind time - Extend wireUseCase to accept optional rateLimit?: IRateLimit and compose withRateLimit innermost (before analytics/audit); propagate __rateLimited through analytics + audit inline wrappers - Extend withSpan and withCapture PROPAGATED_BRANDS to include __rateLimited so the outermost binding carries the brand - Extend assertFeatureConformance to require __rateLimited brand when manifest.useCases[name].rateLimit.length > 0; refactored into helper functions to stay within complexity thresholds - Add rateLimit?: IRateLimit to BindContext; default to NoopRateLimit in web-next bindAllProduction and bindAllDevSeed aggregators - Unit tests for withRateLimit brand attachment, factory passthrough, and composition; synthetic fixture tests for conformance errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,3 +5,4 @@ export type {
|
||||
} from "./rate-limit.interface";
|
||||
export { NoopRateLimit } from "./noop-rate-limit";
|
||||
export { InMemoryRateLimit } from "./in-memory-rate-limit";
|
||||
export { withRateLimit } from "./with-rate-limit";
|
||||
|
||||
85
packages/core-shared/src/rate-limit/with-rate-limit.test.ts
Normal file
85
packages/core-shared/src/rate-limit/with-rate-limit.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, expectTypeOf } from "vitest";
|
||||
import { withRateLimit } from "@/rate-limit/with-rate-limit";
|
||||
import {
|
||||
isRateLimited,
|
||||
isInstrumented,
|
||||
isCaptured,
|
||||
} from "@/conformance/brand-runtime";
|
||||
import { attachBrand } from "@/conformance/brand-runtime";
|
||||
import type { RateLimited } from "@/conformance/brands";
|
||||
import { NoopRateLimit } from "@/rate-limit/noop-rate-limit";
|
||||
|
||||
function makeRateLimit() {
|
||||
return new NoopRateLimit();
|
||||
}
|
||||
|
||||
describe("withRateLimit — brand", () => {
|
||||
it("returns a RateLimited<F> type", () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const fn = async (a: number) => a + 1;
|
||||
const wrapped = withRateLimit(rateLimit, fn);
|
||||
expectTypeOf(wrapped).toMatchTypeOf<RateLimited<typeof fn>>();
|
||||
});
|
||||
|
||||
it("attaches __rateLimited as a non-enumerable property", () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const wrapped = withRateLimit(rateLimit, async (x: number) => x + 1);
|
||||
expect(isRateLimited(wrapped)).toBe(true);
|
||||
expect(Object.keys(wrapped)).not.toContain("__rateLimited");
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRateLimit — factory passthrough", () => {
|
||||
it("forwards arguments and return value unchanged on success", async () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const fn = async (a: number, b: number) => a + b;
|
||||
const wrapped = withRateLimit(rateLimit, fn);
|
||||
await expect(wrapped(3, 4)).resolves.toBe(7);
|
||||
});
|
||||
|
||||
it("re-throws errors from the inner function", async () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const err = new Error("inner failure");
|
||||
const fn = async () => {
|
||||
throw err;
|
||||
};
|
||||
const wrapped = withRateLimit(rateLimit, fn);
|
||||
await expect(wrapped()).rejects.toBe(err);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRateLimit — does not attach other brands", () => {
|
||||
it("does not attach __instrumented or __captured", () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const wrapped = withRateLimit(rateLimit, async (x: number) => x);
|
||||
expect(isInstrumented(wrapped)).toBe(false);
|
||||
expect(isCaptured(wrapped)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withRateLimit — composition with other wrappers", () => {
|
||||
it("composed result carries __rateLimited when withRateLimit is innermost", () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const raw = async (x: number) => x * 2;
|
||||
const rateLimited = withRateLimit(rateLimit, raw);
|
||||
|
||||
// Simulate outer wrapper propagating __rateLimited
|
||||
const outer: typeof raw = (...args) => rateLimited(...args);
|
||||
if (
|
||||
(rateLimited as unknown as Record<string, unknown>)["__rateLimited"] ===
|
||||
true
|
||||
) {
|
||||
attachBrand(outer, "__rateLimited");
|
||||
}
|
||||
expect(isRateLimited(outer)).toBe(true);
|
||||
});
|
||||
|
||||
it("original fn reference is not mutated", () => {
|
||||
const rateLimit = makeRateLimit();
|
||||
const fn = async (x: number) => x;
|
||||
const wrapped = withRateLimit(rateLimit, fn);
|
||||
expect(wrapped).not.toBe(fn);
|
||||
expect(isRateLimited(fn)).toBe(false);
|
||||
expect(isRateLimited(wrapped)).toBe(true);
|
||||
});
|
||||
});
|
||||
25
packages/core-shared/src/rate-limit/with-rate-limit.ts
Normal file
25
packages/core-shared/src/rate-limit/with-rate-limit.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { IRateLimit } from "./rate-limit.interface";
|
||||
import type { RateLimited } from "../conformance/brands";
|
||||
import { attachBrand } from "../conformance/brand-runtime";
|
||||
|
||||
/**
|
||||
* Use-case wrapper applied at DI bind time. Attaches the `__rateLimited`
|
||||
* brand so the boot-time assertion can verify rate-limited use cases were
|
||||
* bound through the rate-limit-aware path.
|
||||
*
|
||||
* The forward closure keeps the brand on a fresh function so the original
|
||||
* `fn` reference is not mutated — important when the same factory output is
|
||||
* used elsewhere unwrapped (dev-seed paths, tests).
|
||||
*
|
||||
* Composition order (outermost to innermost):
|
||||
* withSpan → withCapture → withAudit → withAnalytics → withRateLimit → factory(deps)
|
||||
*/
|
||||
export function withRateLimit<Args extends unknown[], R>(
|
||||
rateLimit: IRateLimit,
|
||||
fn: (...args: Args) => Promise<R>,
|
||||
): RateLimited<(...args: Args) => Promise<R>> {
|
||||
void rateLimit; // future: enforce rate limit at runtime per manifest budgets
|
||||
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||
attachBrand(wrapped, "__rateLimited");
|
||||
return wrapped as RateLimited<(...args: Args) => Promise<R>>;
|
||||
}
|
||||
Reference in New Issue
Block a user