feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 => {},
|
||||
});
|
||||
31
frontend/packages/project-ide/core/src/resource/index.ts
Normal file
31
frontend/packages/project-ide/core/src/resource/index.ts
Normal 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';
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
69
frontend/packages/project-ide/core/src/resource/resource.ts
Normal file
69
frontend/packages/project-ide/core/src/resource/resource.ts
Normal 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;
|
||||
}
|
||||
42
frontend/packages/project-ide/core/src/resource/utils.ts
Normal file
42
frontend/packages/project-ide/core/src/resource/utils.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 { 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;
|
||||
}
|
||||
Reference in New Issue
Block a user