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,356 @@
/*
* 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 { injectable, inject } from 'inversify';
import {
type CancellationToken,
CancellationTokenSource,
Disposable,
DisposableCollection,
Emitter,
type MaybePromise,
PromiseDeferred,
} from '@flowgram-adapter/common';
import { type URI } from '../common';
import { distinctUntilChangedFromEvent } from './utils';
import { type Resource, ResourceError, type ResourceInfo } from './resource';
export const AutoSaveResourceOptions = Symbol('AutoSaveResourceOptions');
export interface AutoSaveResourceOptions {
uri: URI;
}
/**
* 资源文件自动保存服务,目前只适用于文本文件
*/
@injectable()
export abstract class AutoSaveResource<
CHANGE_SET = string,
INFO extends ResourceInfo = ResourceInfo,
>
implements Disposable, Resource<CHANGE_SET, INFO>
{
readonly autoSave: 'on' | 'off' = 'on';
autoSaveDelay = 2000;
info: INFO = {
version: -1,
lastModification: -1,
displayName: '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
private _dirty = false;
protected readonly onPreSaveContentEmitter = new Emitter<CHANGE_SET>();
readonly toDispose = new DisposableCollection();
protected readonly onDirtyChangeEmitter = new Emitter<void>();
protected readonly onValidChangeEmitter = new Emitter<void>();
protected readonly onContentChangeEmitter = new Emitter<CHANGE_SET>();
protected readonly onInfoChangeEmitter = new Emitter<INFO>();
protected readonly onDisplayNameChangeEmitter = distinctUntilChangedFromEvent(
this.onInfoChangeEmitter,
_info => _info.displayName,
);
public onErrorEmitter = new Emitter<Error>();
protected readonly contentChanges: CHANGE_SET[] = [];
lastContent?: CHANGE_SET;
protected readonly toDisposeOnAutoSave = new DisposableCollection();
readonly onDirtyChange = this.onDirtyChangeEmitter.event;
readonly onPreSaveContent = this.onPreSaveContentEmitter.event;
readonly onValidChange = this.onValidChangeEmitter.event;
readonly onInfoChange = this.onInfoChangeEmitter.event;
readonly onContentChange = this.onContentChangeEmitter.event;
readonly onDisplayNameChange = this.onDisplayNameChangeEmitter.event;
readonly onError = this.onErrorEmitter.event;
protected _valid = false;
constructor(
@inject(AutoSaveResourceOptions) readonly options: AutoSaveResourceOptions,
) {
this.options = options;
this.toDispose.push(this.toDisposeOnAutoSave);
this.toDispose.push(this.onDirtyChangeEmitter);
this.toDispose.push(this.onInfoChangeEmitter);
this.toDispose.push(this.onValidChangeEmitter);
this.toDispose.push(this.onPreSaveContentEmitter);
this.toDispose.push(this.onContentChangeEmitter);
this.toDispose.push(this.onDisplayNameChangeEmitter);
this.toDispose.push(this.onErrorEmitter);
this.toDispose.push(Disposable.create(() => this.cancelSave()));
this.toDispose.push(Disposable.create(() => this.cancelSync()));
}
protected saveCancellationTokenSource = new CancellationTokenSource();
protected cancelSave(): CancellationToken {
this.saveCancellationTokenSource.cancel();
this.saveCancellationTokenSource = new CancellationTokenSource();
return this.saveCancellationTokenSource.token;
}
protected setDirty(dirty: boolean): void {
if (dirty === this._dirty) {
return;
}
this._dirty = dirty;
if (!dirty) {
// todo update version
}
this.onDirtyChangeEmitter.fire(undefined);
}
protected markAsDirty(): void {
this.setDirty(true);
this.doAutoSave();
}
protected syncCancellationTokenSource = new CancellationTokenSource();
protected cancelSync(): CancellationToken {
this.syncCancellationTokenSource.cancel();
this.syncCancellationTokenSource = new CancellationTokenSource();
return this.syncCancellationTokenSource.token;
}
async sync(): Promise<void> {
const token = this.cancelSync();
return this.run(() => this.doSync(token));
}
protected async doSync(token: CancellationToken): Promise<void> {
if (token.isCancellationRequested) {
return;
}
await this.readContent(false);
// TODO sync 逻辑需要刷新 widget 数据
// if (token.isCancellationRequested || this._dirty) {
// return;
// }
// this.onContentChangeEmitter.fire(newText);
}
/**
* 自动保存
*/
protected doAutoSave(): void {
if (this.autoSave === 'on') {
const token = this.cancelSave();
this.toDisposeOnAutoSave.dispose();
const handle = window.setTimeout(() => {
this.save(token);
}, this.autoSaveDelay);
this.toDisposeOnAutoSave.push(
Disposable.create(() => window.clearTimeout(handle)),
);
}
}
protected pendingOperation = Promise.resolve();
protected async run(operation: () => Promise<void>): Promise<void> {
if (this.toDispose.disposed) {
return;
}
return (this.pendingOperation = this.pendingOperation.then(async () => {
try {
await operation();
} catch (e) {
console.error(e);
}
}));
}
save(token: CancellationToken = this.cancelSave()): Promise<void> {
return this.run(() => this._doSave(token));
}
private _isSaving = false;
protected async _doSave(token: CancellationToken): Promise<void> {
if (token.isCancellationRequested || !this._valid || this._isSaving) {
return;
}
this._isSaving = true;
try {
const changes: CHANGE_SET[] = [...this.contentChanges];
if (changes.length === 0) {
return Promise.resolve(undefined);
}
const { version } = this.info;
const lastChanged = changes[changes.length - 1];
if (lastChanged === this.lastContent) {
this.contentChanges.length = 0;
this.setValid(true);
this.setDirty(false);
return;
}
const newInfo = await this.doSave(lastChanged, version);
this.lastContent = lastChanged;
this.updateInfo(newInfo);
if (lastChanged === this.contentChanges[this.contentChanges.length - 1]) {
this.contentChanges.length = 0;
this.setValid(true);
this.setDirty(false);
this.onContentChangeEmitter.fire(lastChanged);
} else {
this.doAutoSave();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (ResourceError.is(e, ResourceError.OutOfSync)) {
this.sync();
return;
}
this.onErrorEmitter.fire(e);
} finally {
this._isSaving = false;
}
}
protected setValid(valid: boolean): void {
if (valid === this._valid) {
return;
}
this._valid = valid;
this.onValidChangeEmitter.fire();
}
dispose(): void {
this.toDispose.dispose();
}
get dirty(): boolean {
return this._dirty;
}
get valid(): boolean {
return this._valid;
}
get uri(): URI {
return this.options.uri;
}
private _readContentPromise?: PromiseDeferred<CHANGE_SET>;
async readContent(fromCache = true): Promise<CHANGE_SET> {
try {
if (this.lastContent !== undefined && fromCache) {
return Promise.resolve(this.lastContent);
}
if (this._readContentPromise && fromCache) {
return this._readContentPromise.promise;
}
const promise = new PromiseDeferred<CHANGE_SET>();
this._readContentPromise = promise;
const result = await this.doRead();
this.lastContent = result.content;
this.setValid(true);
this.updateInfo(result.info);
this.onContentChangeEmitter.fire(result.content);
promise.resolve(result.content);
this._readContentPromise = undefined;
return result.content;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
this.setValid(false);
this.onErrorEmitter.fire(e);
this._readContentPromise = undefined;
throw e;
}
}
protected abstract doSave(
content: CHANGE_SET,
preVersion?: number | string,
): Promise<INFO>;
protected abstract doRead(): Promise<{ content: CHANGE_SET; info: INFO }>;
protected abstract doGetInfo(): MaybePromise<INFO>;
async getInfo(fromCache = true): Promise<INFO> {
if (fromCache && this.info.version !== -1) {
return this.info;
}
const info = await this.doGetInfo();
this.updateInfo(info);
return info;
}
updateInfo(info: INFO) {
// if (
// info.lastModification !== this.info.lastModification ||
// info.version !== this.info.version
// ) {
// this.sync();
// }
this.info = info;
this.onInfoChangeEmitter.fire(info);
}
saveContent(content: CHANGE_SET, patch = false): void {
// 若支持增量改动 (仅一层)
let newContent = content;
const preSaveContent =
this.contentChanges[this.contentChanges.length - 1] ?? this.lastContent;
if (
patch &&
preSaveContent &&
typeof preSaveContent === 'object' &&
typeof content === 'object'
) {
newContent = {
...preSaveContent,
...content,
};
}
this.contentChanges.push(newContent);
this.onPreSaveContentEmitter.fire(newContent);
this.markAsDirty();
}
/**
* 获取正在保存中的内容
*/
getPreSaveContent(): CHANGE_SET | undefined {
return (
this.contentChanges[this.contentChanges.length - 1] ?? this.lastContent
);
}
onDispose = this.toDispose.onDispose;
}

View File

@@ -0,0 +1,46 @@
/*
* 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 AsClass, bindContributionProvider } from '@flowgram-adapter/common';
// import { LabelHandler } from '../label';
import { definePluginCreator } from '../common';
import { ResourceService } from './resource-service';
import { ResourceManager } from './resource-manager';
import { ResourceHandler } from './resource';
export interface ResourcePluginOptions {
handlers?: (AsClass<ResourceHandler<any>> | ResourceHandler<any>)[];
}
export const createResourcePlugin = definePluginCreator<ResourcePluginOptions>({
onBind: ({ bind }, opts) => {
bind(ResourceManager).toSelf().inSingletonScope();
bind(ResourceService).toSelf().inSingletonScope();
bindContributionProvider(bind, ResourceHandler);
if (opts.handlers) {
opts.handlers.forEach(handler => {
if (typeof handler === 'function') {
bind(handler).toSelf().inSingletonScope();
bind(ResourceHandler).toService(handler);
} else {
bind(ResourceHandler).toConstantValue(handler);
}
});
}
},
onInit: ctx => {},
});

View File

@@ -0,0 +1,31 @@
/*
* 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 ResourcePluginOptions,
createResourcePlugin,
} from './create-resource-plugin';
export {
type Resource,
ResourceError,
ResourceHandler,
type ResourceInfo,
} from './resource';
export { ResourceService } from './resource-service';
export {
AutoSaveResource,
AutoSaveResourceOptions,
} from './auto-save-resource';

View File

@@ -0,0 +1,71 @@
/*
* 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 { injectable, inject, named } from 'inversify';
import { ContributionProvider, Emitter } from '@flowgram-adapter/common';
import { type URI, URIHandler } from '../common';
import { type Resource, ResourceHandler } from './resource';
@injectable()
export class ResourceManager {
protected resourceCacheMap = new Map<string, Resource>();
protected onResourceCreateEmitter = new Emitter<Resource>();
protected onResourceDisposeEmitter = new Emitter<Resource>();
readonly onResourceCreate = this.onResourceCreateEmitter.event;
readonly onResourceDispose = this.onResourceDisposeEmitter.event;
@inject(ContributionProvider)
@named(ResourceHandler)
protected readonly contributionProvider: ContributionProvider<ResourceHandler>;
get<T extends Resource>(uri: URI): T {
const uriWithoutQuery = uri.withoutQuery().toString();
const resourceFromCache = this.resourceCacheMap.get(uriWithoutQuery);
if (resourceFromCache) {
return resourceFromCache as T;
}
const handler = URIHandler.findSync<ResourceHandler>(
uri,
this.contributionProvider.getContributions(),
);
if (!handler) {
throw new Error(`Unknown Resource handler: ${uri.toString()}`);
}
const newResource = handler.resolve(uri) as T;
newResource.onDispose(() => {
this.resourceCacheMap.delete(uriWithoutQuery);
this.onResourceDisposeEmitter.fire(newResource);
});
this.resourceCacheMap.set(uriWithoutQuery, newResource);
this.onResourceCreateEmitter.fire(newResource);
return newResource;
}
getResourceListFromCache<T extends Resource = Resource>(): T[] {
return Array.from(this.resourceCacheMap.values()) as T[];
}
clearCache(): void {
for (const resource of this.resourceCacheMap.values()) {
resource.dispose();
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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 { injectable, inject } from 'inversify';
import { type URI } from '../common';
import { ResourceManager } from './resource-manager';
import { type Resource } from './resource';
@injectable()
export class ResourceService {
@inject(ResourceManager) protected resourceManager: ResourceManager;
get<T extends Resource>(uri: URI): T {
return this.resourceManager.get<T>(uri.withoutQuery());
}
get onResourceCreate() {
return this.resourceManager.onResourceCreate;
}
get onResourceDispose() {
return this.resourceManager.onResourceDispose;
}
getResourceListFromCache<T extends Resource = Resource>(): T[] {
return this.resourceManager.getResourceListFromCache<T>();
}
clearCache(): void {
this.resourceManager.clearCache();
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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 Disposable,
type MaybePromise,
type Event,
} from '@flowgram-adapter/common';
import { type URI, type URIHandler } from '../common';
export interface ResourceInfo {
displayName?: string; // 显示标题
lastModification?: number | string; // 最后修改时间
version?: number | string;
}
export interface Resource<T = any, INFO extends ResourceInfo = ResourceInfo>
extends Disposable {
readonly uri: URI;
getInfo: () => MaybePromise<INFO>;
updateInfo: (info: INFO) => void;
readContent: () => MaybePromise<T>;
saveContent: (content: T) => MaybePromise<void>;
onInfoChange: Event<INFO>;
onContentChange: Event<T>;
onDispose: Event<void>;
}
export class ResourceError extends Error {
static NotFound = -40000;
static OutOfSync = -40001;
static is(error: object | undefined, code: number): error is ResourceError {
return error instanceof ResourceError && error.code === code;
}
constructor(
readonly message: string,
readonly code: number,
readonly uri: URI,
) {
super(message);
}
}
export const ResourceHandler = Symbol('ResourceHandler');
export interface ResourceHandler<T extends Resource = Resource>
extends URIHandler {
/**
* 创建资源
* @param uri
*/
resolve: (uri: URI) => T;
}

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 { isEqual } from 'lodash';
import { Emitter } from '@flowgram-adapter/common';
export function distinctUntilChangedFromEvent<D, F>(
emitter: Emitter<D>,
filter: (_data: D) => F,
): Emitter<F> {
const nextEmitter = new Emitter<F>();
let _prevData: F | undefined;
const _disposeEmitter = nextEmitter.dispose.bind(nextEmitter);
const _disposeEventListener = emitter.event(_data => {
const _nextData = filter(_data);
if (!isEqual(_prevData, _nextData)) {
_prevData = _nextData;
nextEmitter.fire(_nextData);
}
});
nextEmitter.dispose = () => {
_disposeEmitter();
_disposeEventListener.dispose();
};
return nextEmitter;
}