feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const IS_DEV = process.env.NODE_ENV === 'development';
export const PACKAGE_NAMESPACE = '@flow-arch/flags';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { featureFlagStorage } from './utils/storage';
export const getFlags = () => {
const flags = featureFlagStorage.getFlags();
return flags;
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { type FEATURE_FLAGS, type FetchFeatureGatingFunction } from './types';
export { getFlags } from './get-flags';
export { useFlags } from './use-flags';
export { pullFeatureFlags } from './pull-feature-flags';

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { logger } from '@coze-arch/logger';
import { wait, ONE_SEC } from './utils/wait';
import { isObject } from './utils/tools';
import { featureFlagStorage } from './utils/storage';
import { reporter } from './utils/repoter';
import {
readFgValuesFromContext,
readFgPromiseFromContext,
} from './utils/read-from-context';
import { readFromCache, saveToCache } from './utils/persist-cache';
import { type FEATURE_FLAGS, type FetchFeatureGatingFunction } from './types';
import { PACKAGE_NAMESPACE } from './constant';
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const DEFAULT_POLLING_INTERVAL = 5 * ONE_SEC;
// 设置 17 作为时间分片大小
const TIME_PIECE = 17;
interface PullFeatureFlagsParams {
// 取值超时时间
timeout: number;
// 严格模式下,不会插入兜底逻辑,且取不到数值时直接报错
strict: boolean;
// 轮训间隔,生产环境默认 60 秒;开发 & 测试环境默认 10 秒
pollingInterval: number;
fetchFeatureGating: FetchFeatureGatingFunction;
}
interface WorkResult {
values: FEATURE_FLAGS;
source: 'context' | 'remote' | 'bailout' | 'persist' | 'static_context';
}
const runPipeline = async (
context: PullFeatureFlagsParams,
): Promise<WorkResult> => {
try {
const fgValues = readFgValuesFromContext();
if (fgValues) {
saveToCache(fgValues);
return { values: fgValues, source: 'static_context' };
}
} catch (e) {
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: (e as Error).message,
error: e as Error,
});
}
const { timeout: to, strict } = context;
// 超时时间不应该小于 1s
const timeout = Math.max(to, ONE_SEC);
const works: (() => Promise<WorkResult | undefined>)[] = [];
const waitTimeout = wait.bind(null, timeout + ONE_SEC);
// 从线上环境取值
works.push(async () => {
try {
const values = await context.fetchFeatureGating();
if (isObject(values)) {
saveToCache(values);
return { values, source: 'remote' };
}
await waitTimeout();
} catch (e) {
// TODO: 这里加埋点,上报接口异常
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'Fetch fg by "fetchFeatureGating" failure',
error: e as Error,
});
await waitTimeout();
}
});
// 从浏览器全局对象取值
// 这里需要判断一下,只有浏览器环境才执行
works.push(async () => {
try {
const values = await readFgPromiseFromContext();
if (values && isObject(values)) {
saveToCache(values);
return { values: values as FEATURE_FLAGS, source: 'context' };
}
logger.persist.info({
namespace: PACKAGE_NAMESPACE,
message: "Can't not read fg from global context",
});
// 强制等等超时,以免整个 works resolve 到错误的值
await waitTimeout();
} catch (e) {
// TODO: 这里加埋点,上报接口异常
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'Fetch fg from context failure',
error: e as Error,
});
await waitTimeout();
}
});
// 从缓存中取值
works.push(async () => {
try {
const values = await readFromCache();
if (values) {
// 等待 xx ms 后再读 persist以确保优先从 context 取值
await wait(timeout - TIME_PIECE);
return { values, source: 'persist' };
}
await waitTimeout();
} catch (e) {
// TODO: 这里加埋点,上报接口异常
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'Fetch fg from persist cache failure',
error: e as Error,
});
await waitTimeout();
}
});
// 兜底,超时取不到值返回默认值,也就是全部都是 false
works.push(async () => {
await wait(timeout + TIME_PIECE);
if (strict) {
throw new Error('Fetch Feature Flags timeout.');
}
return { values: {} as unknown as FEATURE_FLAGS, source: 'bailout' };
});
// 这里不可能返回 undefined所以做一次强制转换
const res = (await Promise.race(
works.map(work => work()),
)) as unknown as WorkResult;
return res;
};
const normalize = (
context?: Partial<PullFeatureFlagsParams>,
): PullFeatureFlagsParams => {
const ctx = context || {};
if (!ctx.fetchFeatureGating) {
throw new Error('fetchFeatureGating is required');
}
const DEFAULT_CONTEXT: Partial<PullFeatureFlagsParams> = {
timeout: 2000,
strict: false,
pollingInterval: DEFAULT_POLLING_INTERVAL,
};
const normalizeContext = Object.assign(
DEFAULT_CONTEXT,
Object.keys(ctx)
// 只取不为 undefined 的东西
.filter(k => typeof ctx[k] !== 'undefined')
.reduce((acc, k) => ({ ...acc, [k]: ctx[k] }), {}),
);
return normalizeContext as PullFeatureFlagsParams;
};
const pullFeatureFlags = async (context?: Partial<PullFeatureFlagsParams>) => {
const tracer = reporter.tracer({
eventName: 'load-fg',
});
const normalizeContext = normalize(context);
const { strict, pollingInterval } = normalizeContext;
tracer.trace('start');
const start = performance.now();
const retry = async () => {
// 出现错误时,自动重试
await wait(pollingInterval);
await pullFeatureFlags(context);
};
try {
const res = await runPipeline(normalizeContext);
const { values, source } = res;
// TODO: 这里应该上报数量,后续 logger 提供相关能力后要改一下
logger.persist.success({
namespace: PACKAGE_NAMESPACE,
message: `Load FG from ${source} start at ${start}ms and spend ${
performance.now() - start
}ms`,
});
tracer.trace('finish');
featureFlagStorage.setFlags(values);
if (['bailout', 'persist'].includes(source)) {
await retry();
}
} catch (e) {
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'Failure to load FG',
error: e as Error,
});
if (!strict) {
featureFlagStorage.setFlags({} as unknown as FEATURE_FLAGS);
await retry();
} else {
throw e;
}
}
};
export { pullFeatureFlags };

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FEATURE_FLAGS as ORIGIN_FEATURE_FLAGS } from './feature-flags';
// eslint-disable-next-line @typescript-eslint/naming-convention
type FEATURE_FLAGS = ORIGIN_FEATURE_FLAGS & {
/**
* 返回所有可用 key 列表
*/
keys: string[];
/**
* FG 是否已经完成初始化
*/
isInited: boolean;
};
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
__fetch_fg_promise__: Promise<{ data: FEATURE_FLAGS }>;
// eslint-disable-next-line @typescript-eslint/naming-convention
__fg_values__: FEATURE_FLAGS;
}
}
export { type FEATURE_FLAGS };
export type FetchFeatureGatingFunction = () => Promise<FEATURE_FLAGS>;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect } from 'react';
import { featureFlagStorage } from './utils/storage';
import { type FEATURE_FLAGS } from './types';
import { getFlags } from './get-flags';
export const useFlags = (): [FEATURE_FLAGS] => {
const plainFlags = getFlags();
// 监听 fg store 事件,触发 react 组件响应变化
const [, setTick] = useState<number>(0);
useEffect(() => {
const cb = () => {
setTick(Date.now());
};
featureFlagStorage.on('change', cb);
return () => {
featureFlagStorage.off('change', cb);
};
}, []);
return [plainFlags];
};

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { logger } from '@coze-arch/logger';
import { type FEATURE_FLAGS } from '../types';
import { PACKAGE_NAMESPACE } from '../constant';
import { nextTick } from './wait';
const PERSIST_CACHE_KEY = 'cache:@coze-arch/bot-flags';
const isFlagsShapeObj = (obj: unknown) => {
if (typeof obj === 'object') {
const shape = obj as FEATURE_FLAGS;
return (
// 如果包含任意属性值不是 boolean则认为不是 flags 对象
Object.keys(shape).some(r => typeof shape[r] !== 'boolean') === false
);
}
return false;
};
export const readFromCache = async (): Promise<FEATURE_FLAGS | undefined> => {
await Promise.resolve(undefined);
const content = window.localStorage.getItem(PERSIST_CACHE_KEY);
if (!content) {
return undefined;
}
try {
const res = JSON.parse(content);
if (isFlagsShapeObj(res)) {
return res;
}
return undefined;
} catch (e) {
return undefined;
}
};
export const saveToCache = async (flags: FEATURE_FLAGS) => {
await nextTick();
try {
if (isFlagsShapeObj(flags)) {
const content = JSON.stringify(flags);
window.localStorage.setItem(PERSIST_CACHE_KEY, content);
}
} catch (e) {
// do nothing
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'save fg failure',
error: e as Error,
});
}
};

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FEATURE_FLAGS } from '../types';
export const readFgPromiseFromContext = async (): Promise<
FEATURE_FLAGS | undefined
> => {
const { __fetch_fg_promise__: globalFetchFgPromise } = window;
if (globalFetchFgPromise) {
const res = await globalFetchFgPromise;
return res.data as FEATURE_FLAGS;
}
return undefined;
};
export const readFgValuesFromContext = () => {
const { __fg_values__: globalFgValues } = window;
if (globalFgValues && Object.keys(globalFgValues).length > 0) {
return globalFgValues as FEATURE_FLAGS;
}
return undefined;
};

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { reporter as originReporter } from '@coze-arch/logger';
import { PACKAGE_NAMESPACE } from '../constant';
export const reporter = originReporter.createReporterWithPreset({
namespace: PACKAGE_NAMESPACE,
});

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'eventemitter3';
import { logger } from '@coze-arch/logger';
import type { FEATURE_FLAGS } from '../types';
import { IS_DEV } from '../constant';
import { isEqual } from './tools';
type Interceptor = (key: string) => boolean | undefined;
class FeatureFlagStorage extends EventEmitter {
#proxy: FEATURE_FLAGS | undefined = undefined;
#cache: FEATURE_FLAGS | undefined = undefined;
#inited = false;
#interceptors: Interceptor[] = [];
constructor() {
super();
// fallback
this.#interceptors.push((name: string) => {
const cache = this.#cache;
if (!cache) {
return false;
}
// 从 remote 取值
if (Reflect.has(cache, name)) {
return Reflect.get(cache, name);
}
});
this.#proxy = new Proxy(Object.create(null), {
get: (target, name: string) => {
const cache = this.#cache;
switch (name) {
case 'keys': {
return typeof cache === 'object' ? Reflect.ownKeys(cache) : [];
}
case 'isInited': {
return this.#inited;
}
default: {
return this.#retrieveValueFromInterceptors(name);
}
}
},
set() {
throw new Error('Do not set flag value anytime anyway.');
},
}) as FEATURE_FLAGS;
}
#retrieveValueFromInterceptors(key: string) {
const interceptors = this.#interceptors;
for (const func of interceptors) {
const res = func(key);
if (typeof res === 'boolean') {
return res;
}
}
return false;
}
// has first set FG value
get inited() {
return this.#inited;
}
setFlags(values: FEATURE_FLAGS) {
const cache = this.#cache;
if (isEqual(cache, values)) {
return false;
}
this.#cache = values;
this.#inited = true;
this.notify(values);
return true;
}
notify(values?: FEATURE_FLAGS) {
this.emit('change', values);
}
getFlags(): FEATURE_FLAGS {
if (!this.#inited) {
const error = new Error(
'Trying access feature flag values before the storage been init.',
);
logger.persist.error({ namespace: '@coze-arch/bot-flags', error });
if (IS_DEV) {
throw error;
}
}
return this.#proxy as FEATURE_FLAGS;
}
clear() {
this.#cache = undefined;
this.#inited = false;
}
use(func: Interceptor) {
if (typeof func === 'function') {
this.#interceptors.unshift(func);
} else {
throw new Error('Unexpected retrieve func');
}
}
getPureFlags() {
return this.#cache;
}
}
// singleton
export const featureFlagStorage = new FeatureFlagStorage();

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const isObject = (obj: unknown) => typeof obj === 'object';
export const isEqual = (
obj1: Record<string, boolean> | undefined,
obj2: Record<string, boolean> | undefined,
) => {
// 有任意一个不是对象时,则直接返回 false
if (!isObject(obj1) || !isObject(obj2)) {
return false;
}
const o1 = obj1 as Record<string, boolean>;
const o2 = obj2 as Record<string, boolean>;
// 检查两个对象有相同的键数,如果数量不同,则一定不相等
if (Object.keys(o1).length !== Object.keys(o2).length) {
return false;
}
// 如果键数相同,然后我们检查每个键的值
for (const key in o1) {
// 如果键不存在于第二个对象或者值不同返回false
if (!(key in o2) || o1[key] !== o2[key]) {
return false;
}
}
// 如果所有键都存在于两个对象,并且所有的值都相同,返回 true
return true;
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ONE_SEC = 1000;
export const wait = (ms: number) =>
new Promise(r => {
setTimeout(r, ms);
});
export const nextTick = () => new Promise(r => requestAnimationFrame(r));