feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
18
frontend/packages/arch/bot-flags/src/constant.ts
Normal file
18
frontend/packages/arch/bot-flags/src/constant.ts
Normal 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';
|
||||
5595
frontend/packages/arch/bot-flags/src/feature-flags.ts
Normal file
5595
frontend/packages/arch/bot-flags/src/feature-flags.ts
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/packages/arch/bot-flags/src/get-flags.ts
Normal file
22
frontend/packages/arch/bot-flags/src/get-flags.ts
Normal 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;
|
||||
};
|
||||
17
frontend/packages/arch/bot-flags/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/bot-flags/src/global.d.ts
vendored
Normal 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' />
|
||||
21
frontend/packages/arch/bot-flags/src/index.ts
Normal file
21
frontend/packages/arch/bot-flags/src/index.ts
Normal 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';
|
||||
225
frontend/packages/arch/bot-flags/src/pull-feature-flags.ts
Normal file
225
frontend/packages/arch/bot-flags/src/pull-feature-flags.ts
Normal 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 };
|
||||
42
frontend/packages/arch/bot-flags/src/types.ts
Normal file
42
frontend/packages/arch/bot-flags/src/types.ts
Normal 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>;
|
||||
39
frontend/packages/arch/bot-flags/src/use-flags.ts
Normal file
39
frontend/packages/arch/bot-flags/src/use-flags.ts
Normal 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];
|
||||
};
|
||||
68
frontend/packages/arch/bot-flags/src/utils/persist-cache.ts
Normal file
68
frontend/packages/arch/bot-flags/src/utils/persist-cache.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
23
frontend/packages/arch/bot-flags/src/utils/repoter.ts
Normal file
23
frontend/packages/arch/bot-flags/src/utils/repoter.ts
Normal 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,
|
||||
});
|
||||
132
frontend/packages/arch/bot-flags/src/utils/storage.ts
Normal file
132
frontend/packages/arch/bot-flags/src/utils/storage.ts
Normal 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();
|
||||
45
frontend/packages/arch/bot-flags/src/utils/tools.ts
Normal file
45
frontend/packages/arch/bot-flags/src/utils/tools.ts
Normal 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;
|
||||
};
|
||||
24
frontend/packages/arch/bot-flags/src/utils/wait.ts
Normal file
24
frontend/packages/arch/bot-flags/src/utils/wait.ts
Normal 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));
|
||||
Reference in New Issue
Block a user