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,234 @@
/*
* 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 StoreApi, type UseBoundStore } from 'zustand';
import { describe, beforeEach, afterEach, vi, expect } from 'vitest';
import { DebounceTime } from '../../src/type';
import { AutosaveManager } from '../../src/core/manager';
vi.mock('lodash-es', () => ({
debounce: vi.fn((func, wait) => {
let timeout;
const debounced = (...args) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), wait);
};
debounced.cancel = vi.fn(() => clearTimeout(timeout));
debounced.flush = vi.fn(() => {
if (timeout) {
clearTimeout(timeout);
func();
}
});
return debounced;
}),
}));
const mockStore = {
subscribe: vi.fn().mockReturnValue(() => {
console.log('Unsubscribed');
}),
} as unknown as UseBoundStore<StoreApi<any>>;
describe('AutosaveManager', () => {
let manager;
const saveRequest = vi.fn().mockResolvedValue(Promise.resolve());
const registers = [
{
key: 'testKey',
debounce: DebounceTime.Immediate,
selector: state => state,
},
];
beforeEach(() => {
manager = new AutosaveManager({
store: mockStore,
registers,
saveRequest,
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should initialize and set initial values correctly', () => {
expect(manager.observerList.length).toBe(0);
expect(manager.configList).toMatchObject(
registers.map(r => ({
...r,
eventCallBacks: undefined,
saveRequest,
})),
);
});
it('should start observers correctly', () => {
manager.start();
expect(manager.observerList.length).toBe(1);
const observer = manager.observerList[0];
expect(observer.config.key).toBe('testKey');
expect(observer.config.saveRequest).toBe(saveRequest);
});
it('should not start observers if already started', () => {
manager.start();
expect(manager.observerList.length).toBe(1);
manager.start(); // 再次调用 start
expect(manager.observerList.length).toBe(1);
});
it('should handle manualSave correctly when config is undefined', async () => {
await manager.manualSave('undefinedKey', { value: 'test' });
expect(saveRequest).not.toHaveBeenCalled();
});
it('should close observers correctly', () => {
manager.start();
const observer = manager.observerList[0];
const closeSpy = vi.spyOn(observer, 'close');
manager.close();
expect(closeSpy).toHaveBeenCalled();
expect(manager.observerList.length).toBe(0);
});
it('should handle manualSave correctly', async () => {
const params = { value: 'test' };
await manager.manualSave('testKey', params);
expect(saveRequest).toHaveBeenCalledWith(params, 'testKey', []);
});
it('call middleware correctly', async () => {
const mockOnBeforeSave = vi.fn().mockResolvedValue({ value: 'before' });
const mockOnAfterSave = vi.fn().mockResolvedValue({ value: 'after' });
const registersWithMiddleware = [
{
key: 'testWithMiddlewareKey',
debounce: DebounceTime.Immediate,
selector: state => state,
middleware: {
onBeforeSave: mockOnBeforeSave,
onAfterSave: mockOnAfterSave,
},
},
];
manager = new AutosaveManager({
store: mockStore,
registers: registersWithMiddleware,
saveRequest,
});
const params = { value: 'test' };
await manager.manualSave('testWithMiddlewareKey', params);
expect(mockOnBeforeSave).toHaveBeenCalledWith(params);
expect(saveRequest).toHaveBeenCalledWith(
{ value: 'before' },
'testWithMiddlewareKey',
[],
);
expect(mockOnAfterSave).toHaveBeenCalledWith(params);
});
it('should call eventCallBacks', async () => {
const onBeforeSaveCallback = vi.fn();
const onAfterSaveCallback = vi.fn();
const eventCallBacks = {
onBeforeSave: onBeforeSaveCallback,
onAfterSave: onAfterSaveCallback,
};
manager = new AutosaveManager({
store: mockStore,
registers,
saveRequest,
eventCallBacks,
});
await manager.manualSave('testKey', { value: 'test' });
expect(onBeforeSaveCallback).toHaveBeenCalledWith({ value: 'test' });
expect(onAfterSaveCallback).toHaveBeenCalledWith({ value: 'test' });
});
it('should save without auto save', async () => {
const handler = vi.fn().mockResolvedValue(undefined);
manager.start();
await manager.handleWithoutAutosave({
key: 'testKey',
handler,
});
const observer = manager.getObserver('testKey');
// 确保所有异步操作完成
await Promise.resolve();
expect(observer.lock).toBe(false);
expect(handler).toHaveBeenCalled();
});
it('should save flush correctly', async () => {
manager.start();
vi.spyOn(manager, 'getObserver').mockReturnValue({
debouncedSaveFunc: {
flush: vi.fn(),
},
});
await manager.saveFlush('testKey');
const observer = manager.getObserver('testKey');
expect(observer.debouncedSaveFunc.flush).toHaveBeenCalled();
});
it('should save flush all correctly', async () => {
manager.start();
const observer = manager.getObserver('testKey');
const nextState = { value: 'next' };
const prevState = { value: 'prev' };
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(1000);
await observer.subscribeCallback(nextState, prevState);
manager.observerList.forEach(ob => {
ob.debouncedSaveFunc.flush = vi.fn();
});
manager.saveFlushAll();
manager.observerList.forEach(ob => {
expect(ob.debouncedSaveFunc.flush).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,354 @@
/*
* 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 StoreApi, type UseBoundStore } from 'zustand';
import { describe, beforeEach, afterEach, vi, expect } from 'vitest';
import { diff, type Diff } from 'deep-diff';
import { DebounceTime, type AutosaveObserverConfig } from '../../src/type';
import { AutosaveObserver } from '../../src/core/observer';
const debounceMock = vi.fn((func, wait) => {
let timeout;
const debounced = (...args) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), wait);
};
debounced.cancel = vi.fn(() => clearTimeout(timeout));
debounced.flush = vi.fn(() => {
if (timeout) {
clearTimeout(timeout);
func();
}
});
return debounced;
});
vi.mock('lodash-es/debounce', () => {
const originalModule = vi.importActual('lodash-es/debounce');
return {
__esModule: true,
default: debounceMock,
...originalModule,
};
});
const mockStore = {
subscribe: vi.fn(),
} as unknown as UseBoundStore<StoreApi<any>>;
describe('AutosaveObserver', () => {
let observer;
const saveRequest = vi.fn().mockResolvedValue(Promise.resolve());
const config: AutosaveObserverConfig<any, any, any> = {
key: 'testKey',
debounce: DebounceTime.Immediate,
selector: state => state,
saveRequest,
};
beforeEach(() => {
vi.useFakeTimers(); // 使用假定时器
observer = new AutosaveObserver({
store: mockStore,
...config,
});
});
afterEach(() => {
vi.resetAllMocks();
vi.useRealTimers(); // 恢复真实的定时器
});
it('should initialize and set initial values correctly', () => {
expect(observer.lock).toBe(false);
expect(observer.config).toStrictEqual(config);
expect(observer.store).toBe(mockStore);
expect(mockStore.subscribe).toHaveBeenCalled();
});
it('should trigger callback and parsedSaveFunc', async () => {
const nextState = { value: 'next' };
const prevState = { value: 'prev' };
const diffChange: Diff<any, any>[] = [
{
kind: 'E',
path: ['value'],
lhs: 'prev',
rhs: 'next',
},
];
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(0);
await observer.subscribeCallback(nextState, prevState);
expect(observer.nextState).toBe(nextState);
expect(observer.prevState).toBe(prevState);
expect(observer.diff).toEqual(diffChange);
expect(saveRequest).toHaveBeenCalledWith(nextState, 'testKey', diffChange);
});
it('should handle array diff change', async () => {
const nextState = { value: [1, 2, 3] };
const prevState = { value: [1, 2] };
const diffChange = diff(prevState, nextState);
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(500);
await observer.subscribeCallback(nextState, prevState);
expect(observer.debouncedSaveFunc).toBeInstanceOf(Function);
vi.runAllTimers(); // 手动推进定时器时间以触发防抖函数
await vi.runAllTimersAsync(); // 确保所有异步操作完成
expect(saveRequest).toHaveBeenCalledWith(nextState, 'testKey', diffChange);
});
it('should cancel and unsubscribe correctly', async () => {
const prevState = {
value: {
a: 1,
},
};
const nextState = {
value: {
a: 2,
},
};
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(1000);
await observer.subscribeCallback(nextState, prevState);
const cancelSpy = vi.spyOn(observer.debouncedSaveFunc, 'flush');
const unsubscribeSpy = vi.spyOn(observer, 'unsubscribe');
observer.close();
expect(cancelSpy).toHaveBeenCalled();
expect(unsubscribeSpy).toHaveBeenCalled();
});
it('selector with deps', () => {
const deps = [state => state.value];
const transformer = value => value * 2;
const observerConfig: AutosaveObserverConfig<any, any, any> = {
...config,
selector: { deps, transformer },
};
observer = new AutosaveObserver({
store: mockStore,
...observerConfig,
});
const selector = observer.getMemoizeSelector();
const state = { value: 3 };
const result = selector(state);
expect(result).toBe(transformer(3));
});
it('should return in callback if lock is true', async () => {
observer.lock = true;
const nextState = { value: 'next' };
const prevState = { value: 'prev' };
const getTriggerDelayTimeSpy = vi.spyOn(observer, 'getTriggerDelayTime');
const parsedSaveFuncSpy = vi.spyOn(observer, 'parsedSaveFunc');
await observer.subscribeCallback(nextState, prevState);
expect(observer.nextState).toBe(nextState);
expect(observer.prevState).toBe(prevState);
expect(getTriggerDelayTimeSpy).not.toHaveBeenCalled();
expect(parsedSaveFuncSpy).not.toHaveBeenCalled();
});
it('should return in callback if diffChange is undefined', async () => {
const nextState = { value: 'prev' };
const prevState = { value: 'prev' };
const getTriggerDelayTimeSpy = vi.spyOn(observer, 'getTriggerDelayTime');
const parsedSaveFuncSpy = vi.spyOn(observer, 'parsedSaveFunc');
await observer.subscribeCallback(nextState, prevState);
expect(observer.nextState).toBe(nextState);
expect(observer.prevState).toBe(prevState);
expect(getTriggerDelayTimeSpy).not.toHaveBeenCalled();
expect(parsedSaveFuncSpy).not.toHaveBeenCalled();
});
it('should call onBeforeSave lifecycle callback', async () => {
const onBeforeSave = vi.fn();
observer.config.eventCallBacks = { onBeforeSave };
observer.nextState = { value: 'next' };
await observer.parsedSaveFunc();
expect(onBeforeSave).toHaveBeenCalledWith({
key: observer.config.key,
data: observer.nextState,
});
});
it('should call onAfterSave lifecycle callback', async () => {
const onAfterSave = vi.fn();
observer.config.eventCallBacks = { onAfterSave };
observer.nextState = { value: 'next' };
await observer.parsedSaveFunc();
expect(onAfterSave).toHaveBeenCalledWith({
key: observer.config.key,
data: observer.nextState,
});
});
it('should call onError lifecycle method on error', async () => {
const onError = vi.fn();
observer.config.eventCallBacks = { onError };
saveRequest.mockRejectedValueOnce(new Error('Failed request'));
observer.nextState = { value: 'next' };
await observer.parsedSaveFunc();
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
key: observer.config.key,
error: expect.any(Error),
}),
);
});
it('should return the correct delay time based on debounce configuration', () => {
const prevState = { value: 'prev' };
const diffChange = [{ kind: 'E', path: ['value'] }];
observer.config.debounce = () => 200;
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(200);
observer.config.debounce = { default: 300 };
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(300);
observer.config.debounce = 100;
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(100);
observer.config.debounce = null;
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(
DebounceTime.Immediate,
);
});
it('should return default debounce time when diffChange is empty or undefined', () => {
const prevState = {};
const diffChange = undefined;
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(DebounceTime.Immediate);
});
it('should push default debounce time when changePath does not exist or is a number', () => {
const prevState = { value: 'prev' };
const diffChange: Diff<any, any>[] = [
{
kind: 'E',
path: [123],
lhs: 'prev',
rhs: 'next',
},
];
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(DebounceTime.Immediate);
});
it('should push action delay time when DebounceConfig.action is not an object', () => {
const prevState = { value: 'prev' };
const diffChange = [
{
kind: 'E',
path: ['value'],
lhs: 'prev',
rhs: 'next',
},
];
const debounceActionTime = DebounceTime.Long;
observer.config.debounce = {
default: DebounceTime.Long,
value: {
action: debounceActionTime,
},
};
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(debounceActionTime);
});
it('should return an empty string if changePath does not exist', () => {
const changePath = undefined;
const debouncePath = observer.getdebouncePath(changePath);
expect(debouncePath).toBe('');
});
it('should return the first element of changePath', () => {
const changePath = ['value', 0, 'key'];
const debouncePath = observer.getdebouncePath(changePath);
expect(debouncePath).toBe('value');
});
it('should return default debounce time when diffChange is undefined or empty', () => {
const prevState = {};
let diffChange: Diff<any, any>[] | undefined = undefined;
let delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(DebounceTime.Immediate);
diffChange = [];
delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(DebounceTime.Immediate);
});
it('should push default debounce time when changePath does not exist or has conditions', () => {
const prevState = { value: 'prev' };
observer.config.debounce = { default: DebounceTime.Long };
const diffChange = [
{ kind: 'E', path: undefined },
{ kind: 'E', path: ['nonexistentPath'] },
{ kind: 'E', path: [123] },
];
vi.spyOn(observer, 'getdebouncePath').mockReturnValue(123);
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
expect(delayTime).toBe(DebounceTime.Long);
});
});

View File

@@ -0,0 +1,70 @@
/*
* 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 {
isFunction,
isObject,
getPayloadByFormatter,
} from '../../src/utils/index';
import { type DebounceConfig, DebounceTime } from '../../src/type/index';
describe('isFunction', () => {
const fn: DebounceConfig = () => DebounceTime.Immediate;
const ob: DebounceConfig = {
default: DebounceTime.Immediate,
};
it('isFunction should return true when the input is function', () => {
const result = isFunction(fn);
expect(result).toBe(true);
});
it('isFunction should return false when the input is object', () => {
const result = isFunction(ob);
expect(result).toBe(false);
});
it('isObject should return true when the input is object', () => {
const result = isObject(ob);
expect(result).toBe(true);
});
});
describe('getPayloadByFormatter', () => {
it('should return state directly if formatter is not provided', async () => {
const state = { key: 'value' };
const result = await getPayloadByFormatter(state);
expect(result).toEqual(state);
});
it('should call formatter and return its result if formatter is provided', async () => {
const state = { key: 'value' };
const formatter = vi
.fn()
.mockResolvedValue({ formattedKey: 'formattedValue' });
const result = await getPayloadByFormatter(state, formatter);
expect(formatter).toHaveBeenCalledWith(state);
expect(result).toEqual({ formattedKey: 'formattedValue' });
});
});