From 6be413781a00f0d559c90c16fd3eba60511fa64e Mon Sep 17 00:00:00 2001 From: tiansheng Date: Mon, 25 Aug 2025 18:45:16 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=8F=90=E4=BE=9B=20koa=20?= =?UTF-8?q?=E9=A2=84=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/flags/src/koa/README.md | 185 +++++++++++++ packages/flags/src/koa/precompute.test.ts | 306 ++++++++++++++++++++++ packages/flags/src/koa/precompute.ts | 206 +++++++++++++++ 3 files changed, 697 insertions(+) create mode 100644 packages/flags/src/koa/README.md create mode 100644 packages/flags/src/koa/precompute.test.ts create mode 100644 packages/flags/src/koa/precompute.ts diff --git a/packages/flags/src/koa/README.md b/packages/flags/src/koa/README.md new file mode 100644 index 00000000..5bfeaac3 --- /dev/null +++ b/packages/flags/src/koa/README.md @@ -0,0 +1,185 @@ +# Koa 预计算 (Precompute) + +## 背景信息 + +在 Next.js 生态系统中,Flag 系统提供了强大的预计算功能,允许在服务端预先生成 flag 值并序列化为字符串,然后在客户端快速反序列化获取结果。这种机制可以显著提升性能,减少服务端的重复计算。 + +然而,在 Koa 框架中使用 Flag 的预计算功能时,遇到了一个关键限制: + +### 问题分析 + +1. **Flag 预计算的限制**:Flag 的 `precompute` 和 `evaluate` 函数不支持动态传入 `request` 对象 +2. **Flag 函数的灵活性**:`flag/next` 提供的 flag 函数本身是支持传递 `request` 参数的 [参考 Pages Router](https://flags-sdk.dev/frameworks/next#pages-router) +3. **Koa 环境的需求**:在 Koa 应用中,不支持 Flag 内部自动获取 `request` ,需要手动传入 + +### 核心矛盾 + +```typescript +// flag 的预计算函数不支持 request 参数 +export async function precompute( + flags: T, + // ❌ 没有 request 参数 +): Promise; + +// 但 flag 函数本身支持 request 参数 +const flag = flag({ + key: 'user-feature', + decide: (request) => { + // ✅ 可以访问 request 中的 cookies、headers 等 + return request?.cookies?.['user-type'] === 'premium'; + }, +}); +``` + +## 解决方案 + +为了解决这个问题,我们为 Koa 环境提供了增强版的预计算函数,支持动态传入 `request` 对象: + +### 新增功能 + +1. **支持 request 参数的 evaluate 函数** +2. **支持 request 参数的 precompute 函数** +3. **完整的类型安全支持** +4. **向后兼容性** + +### 实现原理 + +```typescript +// Koa 的增强版预计算函数 +export async function evaluate( + flags: T, + request?: KoaRequest, // ✅ 支持可选的 request 参数 +): Promise<{ [K in keyof T]: Awaited> }>; + +export async function precompute( + flags: T, + request?: KoaRequest, // ✅ 支持可选的 request 参数 +): Promise; +``` + +## 使用示例 + +### 基本用法 + +```typescript +import { flag } from '@web-widget/flags/next'; +import { precompute, evaluate } from '@web-widget/flags/koa'; + +// 定义支持 request 的 flag +const userFeatureFlag = flag({ + key: 'user-feature', + decide: (request) => { + // 基于请求上下文决定 flag 值 + const userType = request?.cookies?.['user-type']; + return userType === 'premium'; + }, +}); + +const themeFlag = flag({ + key: 'theme', + decide: (request) => { + // 基于 cookies 决定主题 + return request?.cookies?.['theme'] || 'light'; + }, +}); + +const flags = [userFeatureFlag, themeFlag]; +``` + +### 预计算使用 + +```typescript +// 在 Koa 中间件中使用 +app.use(async (ctx, next) => { + // 创建 Koa 兼容的 request 对象 + const koaRequest = { + cookies: ctx.cookies, + headers: ctx.headers, + // 其他必要的属性... + }; + + try { + // 预计算所有 flags + const precomputedCode = await precompute(flags, koaRequest); + + // 将预计算的结果传递给客户端 + ctx.state.precomputedFlags = precomputedCode; + + await next(); + } catch (error) { + console.error('Flag precompute failed:', error); + await next(); + } +}); +``` + +### 动态评估 + +```typescript +// 直接评估 flags(不进行预计算) +app.use(async (ctx, next) => { + const koaRequest = { + cookies: ctx.cookies, + headers: ctx.headers, + }; + + try { + // 直接评估 flags + const flagValues = await evaluate(flags, koaRequest); + + // 使用评估结果 + ctx.state.userFeature = flagValues[0]; // userFeatureFlag 的值 + ctx.state.theme = flagValues[1]; // themeFlag 的值 + + await next(); + } catch (error) { + console.error('Flag evaluation failed:', error); + await next(); + } +}); +``` + +## 类型定义 + +### KoaRequest 类型 + +```typescript +type KoaRequestCookies = Partial<{ + [key: string]: string; +}>; + +type KoaRequest = IncomingMessage & { + cookies: KoaRequestCookies; +}; +``` + +### 函数签名 + +```typescript +// 评估 flags +export async function evaluate( + flags: T, + request?: KoaRequest, +): Promise<{ [K in keyof T]: Awaited> }>; + +// 预计算 flags +export async function precompute( + flags: T, + request?: KoaRequest, +): Promise; + +// 反序列化 +export async function deserialize( + flags: FlagsArray, + code: string, + secret?: string, +): Promise>; + +// 获取预计算的值 +export async function getPrecomputed( + flag: Flag, + precomputeFlags: FlagsArray, + code: string, + secret?: string, +): Promise; +``` diff --git a/packages/flags/src/koa/precompute.test.ts b/packages/flags/src/koa/precompute.test.ts new file mode 100644 index 00000000..6749d6c5 --- /dev/null +++ b/packages/flags/src/koa/precompute.test.ts @@ -0,0 +1,306 @@ +import { describe, expect, it } from 'vitest'; +import { + deserialize, + evaluate, + precompute, + generatePermutations, + getPrecomputed, + serialize, +} from './precompute'; +import type { Flag } from '../next/types'; +import crypto from 'node:crypto'; +import type { JsonValue } from '../types'; + +// Create simple test flag objects for testing +function createTestFlag( + key: string, + decide: (request?: any) => T | Promise, + options?: T[], +): Flag { + const flagFn = (request?: any) => decide(request); + flagFn.key = key; + flagFn.options = options?.map((opt) => ({ value: opt })); + flagFn.decide = decide; + return flagFn as Flag; +} + +/** + * Helper function to assert the generated permutations. + * + * @param group the group of flags to generate permutations for + * @param expected the expected permutations + */ +async function expectPermutations( + group: Flag[], + expected: any[], + filter?: ((permutation: Record) => boolean) | null, +) { + const permutationsPromise = generatePermutations(group, filter); + await expect(permutationsPromise).resolves.toHaveLength(expected.length); + + const permutations = await permutationsPromise; + await expect( + Promise.all(permutations.map((p) => deserialize(group, p))), + ).resolves.toEqual(expected); +} + +describe('generatePermutations', () => { + describe('when flag declares no options', () => { + it('should infer boolean options', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => false); + await expectPermutations([flagA], [{ a: false }, { a: true }]); + }); + }); + + describe('when flag declares empty options', () => { + it('should not infer any options', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => false, []); + await expectPermutations([flagA], []); + }); + }); + + describe('when flag declares options', () => { + it('should generate permutations', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => 'two', ['one', 'two', 'three']); + + await expectPermutations( + [flagA], + [{ a: 'one' }, { a: 'two' }, { a: 'three' }], + ); + }); + }); + + describe('when flag declares options with a filter', () => { + it('should generate permutations', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => 'two', ['one', 'two', 'three']); + + await expectPermutations( + [flagA], + [{ a: 'two' }], + // the filter passed to generatePermutations() + (permutation) => permutation.a === 'two', + ); + }); + }); + + describe('multiple flags with inferred options', () => { + it('should generate permutations', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => false); + + const flagB = createTestFlag('b', () => false); + + await expectPermutations( + [flagA, flagB], + [ + { a: false, b: false }, + { a: true, b: false }, + { a: false, b: true }, + { a: true, b: true }, + ], + ); + }); + }); + + describe('multiple flags with a mix of inferred and declared options', () => { + it('should generate permutations', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => false); + + const flagB = createTestFlag('b', () => false); + + const flagC = createTestFlag('c', () => 'two', ['one', 'two', 'three']); + + await expectPermutations( + [flagA, flagB, flagC], + [ + { a: false, b: false, c: 'one' }, + { a: true, b: false, c: 'one' }, + { a: false, b: true, c: 'one' }, + { a: true, b: true, c: 'one' }, + + { a: false, b: false, c: 'two' }, + { a: true, b: false, c: 'two' }, + { a: false, b: true, c: 'two' }, + { a: true, b: true, c: 'two' }, + + { a: false, b: false, c: 'three' }, + { a: true, b: false, c: 'three' }, + { a: false, b: true, c: 'three' }, + { a: true, b: true, c: 'three' }, + ], + ); + }); + }); + + describe('multiple flags with a mix of inferred and declared options, filtered', () => { + it('should generate permutations', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => false); + + const flagB = createTestFlag('b', () => false); + + const flagC = createTestFlag('c', () => 'two', ['one', 'two', 'three']); + + await expectPermutations( + [flagA, flagB, flagC], + [ + { a: false, b: true, c: 'two' }, + { a: true, b: true, c: 'two' }, + ], + (permutation) => permutation.c === 'two' && permutation.b, + ); + }); + }); +}); + +describe('getPrecomputed', () => { + it('should return the precomputed value', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => true); + const flagB = createTestFlag('b', () => false); + + const group = [flagA, flagB]; + const code = await serialize(group, [true, false]); + await expect(getPrecomputed(flagA, group, code)).resolves.toBe(true); + }); +}); + +describe('evaluate', () => { + it('should evaluate flags without request', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => true); + const flagB = createTestFlag('b', () => 'test'); + const flags = [flagA, flagB]; + + const result = await evaluate(flags); + expect(result).toEqual([true, 'test']); + }); + + it('should evaluate flags with request object', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const mockRequest = { + cookies: { 'user-id': '123' }, + } as any; + + const flagA = createTestFlag( + 'a', + (request) => request?.cookies?.['user-id'] === '123', + ); + const flagB = createTestFlag('b', (request) => + request?.cookies?.['user-id'] ? 'authenticated' : 'anonymous', + ); + const flags = [flagA, flagB]; + + const result = await evaluate(flags, mockRequest); + expect(result).toEqual([true, 'authenticated']); + }); + + it('should handle flags that use request context', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const mockRequest = { + cookies: { theme: 'dark' }, + } as any; + + const flagA = createTestFlag( + 'theme', + (request) => request?.cookies?.['theme'] || 'light', + ); + const flags = [flagA]; + + const result = await evaluate(flags, mockRequest); + expect(result).toEqual(['dark']); + }); +}); + +describe('precompute', () => { + it('should precompute flags without request', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const flagA = createTestFlag('a', () => true); + const flagB = createTestFlag('b', () => 'test'); + const flags = [flagA, flagB]; + + const result = await precompute(flags); + expect(typeof result).toBe('string'); + + // Verify the result can be deserialized + const deserialized = await deserialize(flags, result); + expect(deserialized).toEqual({ a: true, b: 'test' }); + }); + + it('should precompute flags with request object', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const mockRequest = { + cookies: { 'user-id': '456' }, + } as any; + + const flagA = createTestFlag( + 'a', + (request) => request?.cookies?.['user-id'] === '456', + ); + const flagB = createTestFlag('b', (request) => + request?.cookies?.['user-id'] ? 'user-456' : 'unknown', + ); + const flags = [flagA, flagB]; + + const result = await precompute(flags, mockRequest); + expect(typeof result).toBe('string'); + + // Verify the result can be deserialized + const deserialized = await deserialize(flags, result); + expect(deserialized).toEqual({ a: true, b: 'user-456' }); + }); + + it('should handle complex request scenarios', async () => { + process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url'); + + const mockRequest = { + cookies: { + 'user-type': 'premium', + region: 'us-east', + }, + } as any; + + const flagA = createTestFlag( + 'premium-features', + (request) => request?.cookies?.['user-type'] === 'premium', + ); + const flagB = createTestFlag( + 'region', + (request) => request?.cookies?.['region'] || 'default', + ); + const flagC = createTestFlag('feature-limit', (request) => { + const userType = request?.cookies?.['user-type']; + return userType === 'premium' ? 100 : 10; + }); + const flags = [flagA, flagB, flagC]; + + const result = await precompute(flags, mockRequest); + expect(typeof result).toBe('string'); + + // Verify the result can be deserialized + const deserialized = await deserialize(flags, result); + expect(deserialized).toEqual({ + 'premium-features': true, + region: 'us-east', + 'feature-limit': 100, + }); + }); +}); diff --git a/packages/flags/src/koa/precompute.ts b/packages/flags/src/koa/precompute.ts new file mode 100644 index 00000000..a20de391 --- /dev/null +++ b/packages/flags/src/koa/precompute.ts @@ -0,0 +1,206 @@ +import { Flag } from '../next/types'; +import type { JsonValue } from '..'; +import * as s from '../lib/serialization'; +import type { IncomingMessage } from 'node:http'; + +type FlagsArray = readonly Flag[]; +type ValuesArray = readonly any[]; +type KoaRequestCookies = Partial<{ + [key: string]: string; +}>; +type KoaRequest = IncomingMessage & { cookies: KoaRequestCookies }; + +/** + * Resolves a list of flags + * @param flags - list of flags + * @returns - an array of evaluated flag values with one entry per flag + */ +export async function evaluate( + flags: T, + request?: KoaRequest, +): Promise<{ [K in keyof T]: Awaited> }> { + return Promise.all(flags.map((flag) => flag(request))) as Promise<{ + [K in keyof T]: Awaited>; + }>; +} + +/** + * Evaluate a list of feature flags and generate a signed string representing their values. + * + * This convenience function call combines `evaluate` and `serialize`. + * + * @param flags - list of flags + * @returns - a string representing evaluated flags + */ +export async function precompute( + flags: T, + request?: KoaRequest, +): Promise { + const values = await evaluate(flags, request); + return serialize(flags, values); +} + +/** + * Combines flag declarations with values. + * @param flags - flag declarations + * @param values - flag values + * @returns - A record where the keys are flag keys and the values are flag values. + */ +export function combine(flags: FlagsArray, values: ValuesArray) { + return Object.fromEntries(flags.map((flag, i) => [flag.key, values[i]])); +} + +/** + * Takes a list of feature flag declarations and their values and turns them into a short, signed string. + * + * The returned string is signed to avoid enumeration attacks. + * + * When a feature flag's `options` contains the value the flag resolved to, then the encoding will store it's index only, leading to better compression. Boolean values and null are compressed even when the options are not declared on the flag. + * + * @param flags - A list of feature flags + * @param values - A list of the values of the flags declared in ´flags` + * @param secret - The secret to use for signing the result + * @returns - A short string representing the values. + */ +export async function serialize( + flags: FlagsArray, + values: ValuesArray, + secret: string | undefined = process.env.FLAGS_SECRET, +) { + if (!secret) { + throw new Error('flags: Can not serialize due to missing secret'); + } + + return s.serialize(combine(flags, values), flags, secret); +} + +/** + * Decodes all flags given the list of flags used to encode. Returns an object consisting of each flag's key and its resolved value. + * @param flags - Flags used when `code` was generated by `precompute` or `serialize`. + * @param code - The code returned from `serialize` + * @param secret - The secret to use for signing the result + * @returns - An object consisting of each flag's key and its resolved value. + */ +export async function deserialize( + flags: FlagsArray, + code: string, + secret: string | undefined = process.env.FLAGS_SECRET, +) { + if (!secret) { + throw new Error('flags: Can not serialize due to missing secret'); + } + + return s.deserialize(code, flags, secret); +} + +/** + * Decodes the value of one or multiple flags given the list of flags used to encode and the code. + * + * @param flag - Flag or list of flags to decode + * @param precomputeFlags - Flags used when `code` was generated by `serialize` + * @param code - The code returned from `serialize` + * @param secret - The secret to use for verifying the signature + */ +export async function getPrecomputed( + flag: Flag, + precomputeFlags: FlagsArray, + code: string, + secret?: string, +): Promise; + +/** + * Decodes the value of one or multiple flags given the list of flags used to encode and the code. + * + * @param flag - Flag or list of flags to decode + * @param precomputeFlags - Flags used when `code` was generated by `serialize` + * @param code - The code returned from `serialize` + * @param secret - The secret to use for verifying the signature + */ +export async function getPrecomputed< + T extends JsonValue, + K extends readonly Flag[], +>( + flags: readonly [...K], + precomputeFlags: FlagsArray, + code: string, + secret?: string, +): Promise<{ [P in keyof K]: K[P] extends Flag ? U : never }>; + +/** + * Decodes the value of one or multiple flags given the list of flags used to encode and the code. + * + * @param flag - Flag or list of flags to decode + * @param precomputeFlags - Flags used when `code` was generated by `serialize` + * @param code - The code returned from `serialize` + * @param secret - The secret to use for verifying the signature + */ +export async function getPrecomputed( + flagOrFlags: Flag | readonly Flag[], + precomputeFlags: FlagsArray, + code: string, + secret: string | undefined = process.env.FLAGS_SECRET, +): Promise { + if (!secret) { + throw new Error( + 'flags: getPrecomputed was called without a secret. Please set FLAGS_SECRET environment variable.', + ); + } + + const flagSet = await deserialize(precomputeFlags, code, secret); + + if (Array.isArray(flagOrFlags)) { + // Handle case when an array of flags is passed + return flagOrFlags.map((flag) => flagSet[flag.key]); + } else { + // Handle case when a single flag is passed + return flagSet[(flagOrFlags as Flag).key]; + } +} + +// see https://stackoverflow.com/a/44344803 +function* cartesianIterator(items: T[][]): Generator { + const remainder = items.length > 1 ? cartesianIterator(items.slice(1)) : [[]]; + for (let r of remainder) for (let h of items.at(0)!) yield [h, ...r]; +} + +/** + * Generates all permutations given a list of feature flags based on the options declared on each flag. + * @param flags - The list of feature flags + * @param filter - An optional filter function which gets called with each permutation. + * @param secret - The secret sign the generated permutation with + * @returns An array of strings representing each permutation + */ +export async function generatePermutations( + flags: FlagsArray, + filter: ((permutation: Record) => boolean) | null = null, + secret: string = process.env.FLAGS_SECRET!, +): Promise { + if (!secret) { + throw new Error( + 'flags: generatePermutations was called without a secret. Please set FLAGS_SECRET environment variable.', + ); + } + + const options = flags.map((flag) => { + // infer boolean permutations if you don't declare any options. + // + // to explicitly opt out you need to use "filter" + if (!flag.options) return [false, true]; + return flag.options.map((option) => option.value); + }); + + const list: Record[] = []; + + for (const permutation of cartesianIterator(options)) { + const permObject = permutation.reduce>( + (acc, value, index) => { + acc[flags[index]!.key] = value; + return acc; + }, + {}, + ); + if (!filter || filter(permObject)) list.push(permObject); + } + + return Promise.all(list.map((values) => s.serialize(values, flags, secret))); +} From e101a64bdc2e48e5c4eed59337f7007a72396eb2 Mon Sep 17 00:00:00 2001 From: tiansheng Date: Mon, 25 Aug 2025 18:59:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=E5=AF=BC=E5=87=BA=20koa-precomput?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/metal-donkeys-eat.md | 5 +++++ packages/flags/package.json | 4 ++++ packages/flags/src/koa/precompute.ts | 4 +++- packages/flags/tsup.config.js | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/metal-donkeys-eat.md diff --git a/.changeset/metal-donkeys-eat.md b/.changeset/metal-donkeys-eat.md new file mode 100644 index 00000000..5b5e7a7e --- /dev/null +++ b/.changeset/metal-donkeys-eat.md @@ -0,0 +1,5 @@ +--- +'@web-widget/flags-kit': minor +--- + +提供 koa 预计算 diff --git a/packages/flags/package.json b/packages/flags/package.json index ee40afc0..2fe15e3f 100644 --- a/packages/flags/package.json +++ b/packages/flags/package.json @@ -48,6 +48,10 @@ "./web-router": { "import": "./dist/web-router.js", "require": "./dist/web-router.cjs" + }, + "./koa-precompute": { + "import": "./dist/koa-precompute.js", + "require": "./dist/koa-precompute.cjs" } }, "typesVersions": { diff --git a/packages/flags/src/koa/precompute.ts b/packages/flags/src/koa/precompute.ts index a20de391..ffb3d9cc 100644 --- a/packages/flags/src/koa/precompute.ts +++ b/packages/flags/src/koa/precompute.ts @@ -19,7 +19,9 @@ export async function evaluate( flags: T, request?: KoaRequest, ): Promise<{ [K in keyof T]: Awaited> }> { - return Promise.all(flags.map((flag) => flag(request))) as Promise<{ + return Promise.all( + flags.map((flag) => (request ? flag(request) : flag())), + ) as Promise<{ [K in keyof T]: Awaited>; }>; } diff --git a/packages/flags/tsup.config.js b/packages/flags/tsup.config.js index 87606c0d..a6b4a23d 100644 --- a/packages/flags/tsup.config.js +++ b/packages/flags/tsup.config.js @@ -20,6 +20,7 @@ export default defineConfig({ react: 'src/react/index.tsx', analytics: 'src/analytics.ts', 'web-router': 'src/web-router/index.ts', + 'koa-precompute': 'src/koa/precompute.ts', }, ...defaultConfig, }); From ced86d4ce4d4e4772801b8220e3afd85b179cff2 Mon Sep 17 00:00:00 2001 From: tiansheng Date: Tue, 26 Aug 2025 13:45:34 +0800 Subject: [PATCH 3/3] chore: readme --- packages/flags/src/koa/README.md | 96 ++++++++++++++++---------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/flags/src/koa/README.md b/packages/flags/src/koa/README.md index 5bfeaac3..4570d641 100644 --- a/packages/flags/src/koa/README.md +++ b/packages/flags/src/koa/README.md @@ -1,75 +1,75 @@ -# Koa 预计算 (Precompute) +# Koa Precompute -## 背景信息 +## Background -在 Next.js 生态系统中,Flag 系统提供了强大的预计算功能,允许在服务端预先生成 flag 值并序列化为字符串,然后在客户端快速反序列化获取结果。这种机制可以显著提升性能,减少服务端的重复计算。 +In the Next.js ecosystem, the Flag system provides powerful precompute functionality that allows pre-generating flag values on the server side and serializing them into strings, then quickly deserializing them on the client side to obtain results. This mechanism can significantly improve performance and reduce repeated server-side calculations. -然而,在 Koa 框架中使用 Flag 的预计算功能时,遇到了一个关键限制: +However, when using Flag's precompute functionality in the Koa framework, we encountered a critical limitation: -### 问题分析 +### Problem Analysis -1. **Flag 预计算的限制**:Flag 的 `precompute` 和 `evaluate` 函数不支持动态传入 `request` 对象 -2. **Flag 函数的灵活性**:`flag/next` 提供的 flag 函数本身是支持传递 `request` 参数的 [参考 Pages Router](https://flags-sdk.dev/frameworks/next#pages-router) -3. **Koa 环境的需求**:在 Koa 应用中,不支持 Flag 内部自动获取 `request` ,需要手动传入 +1. **Flag Precompute Limitations**: Flag's `precompute` and `evaluate` functions do not support dynamically passing `request` objects +2. **Flag Function Flexibility**: The flag functions provided by `flag/next` themselves support passing `request` parameters [Reference Pages Router](https://flags-sdk.dev/frameworks/next#pages-router) +3. **Koa Environment Requirements**: In Koa applications, Flag cannot automatically obtain `request` internally and requires manual passing -### 核心矛盾 +### Core Contradiction ```typescript -// flag 的预计算函数不支持 request 参数 +// flag's precompute function doesn't support request parameter export async function precompute( flags: T, - // ❌ 没有 request 参数 + // ❌ No request parameter ): Promise; -// 但 flag 函数本身支持 request 参数 +// But flag functions themselves support request parameters const flag = flag({ key: 'user-feature', decide: (request) => { - // ✅ 可以访问 request 中的 cookies、headers 等 + // ✅ Can access cookies, headers, etc. from request return request?.cookies?.['user-type'] === 'premium'; }, }); ``` -## 解决方案 +## Solution -为了解决这个问题,我们为 Koa 环境提供了增强版的预计算函数,支持动态传入 `request` 对象: +To solve this problem, we provide enhanced precompute functions for the Koa environment that support dynamically passing `request` objects: -### 新增功能 +### New Features -1. **支持 request 参数的 evaluate 函数** -2. **支持 request 参数的 precompute 函数** -3. **完整的类型安全支持** -4. **向后兼容性** +1. **evaluate function supporting request parameters** +2. **precompute function supporting request parameters** +3. **Complete type safety support** +4. **Backward compatibility** -### 实现原理 +### Implementation Principle ```typescript -// Koa 的增强版预计算函数 +// Koa's enhanced precompute functions export async function evaluate( flags: T, - request?: KoaRequest, // ✅ 支持可选的 request 参数 + request?: KoaRequest, // ✅ Supports optional request parameter ): Promise<{ [K in keyof T]: Awaited> }>; export async function precompute( flags: T, - request?: KoaRequest, // ✅ 支持可选的 request 参数 + request?: KoaRequest, // ✅ Supports optional request parameter ): Promise; ``` -## 使用示例 +## Usage Examples -### 基本用法 +### Basic Usage ```typescript import { flag } from '@web-widget/flags/next'; import { precompute, evaluate } from '@web-widget/flags/koa'; -// 定义支持 request 的 flag +// Define flags that support request const userFeatureFlag = flag({ key: 'user-feature', decide: (request) => { - // 基于请求上下文决定 flag 值 + // Decide flag value based on request context const userType = request?.cookies?.['user-type']; return userType === 'premium'; }, @@ -78,7 +78,7 @@ const userFeatureFlag = flag({ const themeFlag = flag({ key: 'theme', decide: (request) => { - // 基于 cookies 决定主题 + // Decide theme based on cookies return request?.cookies?.['theme'] || 'light'; }, }); @@ -86,23 +86,23 @@ const themeFlag = flag({ const flags = [userFeatureFlag, themeFlag]; ``` -### 预计算使用 +### Precompute Usage ```typescript -// 在 Koa 中间件中使用 +// Use in Koa middleware app.use(async (ctx, next) => { - // 创建 Koa 兼容的 request 对象 + // Create Koa-compatible request object const koaRequest = { cookies: ctx.cookies, headers: ctx.headers, - // 其他必要的属性... + // Other necessary properties... }; try { - // 预计算所有 flags + // Precompute all flags const precomputedCode = await precompute(flags, koaRequest); - // 将预计算的结果传递给客户端 + // Pass precomputed results to client ctx.state.precomputedFlags = precomputedCode; await next(); @@ -113,10 +113,10 @@ app.use(async (ctx, next) => { }); ``` -### 动态评估 +### Dynamic Evaluation ```typescript -// 直接评估 flags(不进行预计算) +// Directly evaluate flags (without precompute) app.use(async (ctx, next) => { const koaRequest = { cookies: ctx.cookies, @@ -124,12 +124,12 @@ app.use(async (ctx, next) => { }; try { - // 直接评估 flags + // Directly evaluate flags const flagValues = await evaluate(flags, koaRequest); - // 使用评估结果 - ctx.state.userFeature = flagValues[0]; // userFeatureFlag 的值 - ctx.state.theme = flagValues[1]; // themeFlag 的值 + // Use evaluation results + ctx.state.userFeature = flagValues[0]; // userFeatureFlag value + ctx.state.theme = flagValues[1]; // themeFlag value await next(); } catch (error) { @@ -139,9 +139,9 @@ app.use(async (ctx, next) => { }); ``` -## 类型定义 +## Type Definitions -### KoaRequest 类型 +### KoaRequest Type ```typescript type KoaRequestCookies = Partial<{ @@ -153,29 +153,29 @@ type KoaRequest = IncomingMessage & { }; ``` -### 函数签名 +### Function Signatures ```typescript -// 评估 flags +// Evaluate flags export async function evaluate( flags: T, request?: KoaRequest, ): Promise<{ [K in keyof T]: Awaited> }>; -// 预计算 flags +// Precompute flags export async function precompute( flags: T, request?: KoaRequest, ): Promise; -// 反序列化 +// Deserialize export async function deserialize( flags: FlagsArray, code: string, secret?: string, ): Promise>; -// 获取预计算的值 +// Get precomputed value export async function getPrecomputed( flag: Flag, precomputeFlags: FlagsArray,