humainflow-web/src/app/services/blocks/blocks.ts

153 lines
5.0 KiB
TypeScript

import { computed, Injectable, signal } from '@angular/core';
import { environment } from '@environment';
import { BlockType, BlockTypeName, FlowData, FlowNode, NodeFamily } from '@models/flow';
import { BlocksCallServiceBase } from './block-call.base';
import { catchError, finalize, firstValueFrom, forkJoin, map, Observable, of, shareReplay, throwError } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class BlocksService {
blocksCallService: BlocksCallServiceBase = new environment.blocksCallService();
toInit: boolean = true;
private loadingPromise: Promise<void> | null = null;
private readonly emptyBlockCache = new Map<string, FlowNode>();
private readonly pendingEmptyBlockRequests = new Map<string, Observable<FlowNode>>();
private readonly pendingServerSyncCount = signal(0);
private _blockTypes = signal<BlockType[]>([]);
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
async getAllBlocksTypes() {
if (this.toInit) {
await this.refresh();
this.toInit = false;
}
return this._blockTypes.asReadonly();
}
async refresh(force = false): Promise<void> {
if (this.loadingPromise && !force) {
return this.loadingPromise;
}
this.loadingPromise = firstValueFrom(forkJoin({
blocks: this.blocksCallService.retrieveAllBlocksTypes(),
containers: this.blocksCallService.retrieveAllContainerTypes()
}))
.then(({ blocks, containers }) => {
this._blockTypes.set([...blocks, ...containers]);
this.clearEmptyBlockCache();
})
.catch((err) => {
console.error('Retrieve blocks types failed', err);
throw err;
})
.finally(() => {
this.loadingPromise = null;
});
return this.loadingPromise;
}
async getBlockType(typeName: BlockTypeName) {
const current = this._blockTypes().find((blockType) => blockType.type === typeName);
if (current) return current;
const { blocks, containers } = await firstValueFrom(forkJoin({
blocks: this.blocksCallService.retrieveAllBlocksTypes(),
containers: this.blocksCallService.retrieveAllContainerTypes()
}));
const blockTypes = [...blocks, ...containers];
this._blockTypes.set(blockTypes);
this.clearEmptyBlockCache();
return blockTypes.find((blockType) => blockType.type === typeName);
}
createEmptyBlock(blockType: BlockTypeName, family?: NodeFamily) {
const cacheKey = `${family ?? 'auto'}:${String(blockType)}`;
const cached = this.emptyBlockCache.get(cacheKey);
if (cached) {
return of(this.cloneEmptyNode(cached));
}
const pending = this.pendingEmptyBlockRequests.get(cacheKey);
if (pending) {
return pending.pipe(map((block) => this.cloneEmptyNode(block)));
}
const request = this.createEmptyNodeRequest(blockType, family).pipe(
map((block) => {
this.emptyBlockCache.set(cacheKey, this.cloneEmptyNode(block));
return block;
}),
finalize(() => {
this.pendingEmptyBlockRequests.delete(cacheKey);
}),
shareReplay(1)
);
this.pendingEmptyBlockRequests.set(cacheKey, request);
return request.pipe(
map((block) => this.cloneEmptyNode(block)),
catchError((err) => {
console.error('Create empty block failed', err);
return throwError(() => err);
})
);
}
updateBlock(blockId: string, configuration: any) {
this.pendingServerSyncCount.update((count) => count + 1);
return this.blocksCallService.updateBlock(blockId, configuration).pipe(
finalize(() => {
this.pendingServerSyncCount.update((count) => Math.max(0, count - 1));
}),
catchError((err) => {
console.error('Update block failed', err);
return throwError(() => err);
})
);
}
validateContainerSubflow(subFlow: FlowData) {
return this.blocksCallService.validateContainerSubflow(this.deepClone(subFlow)).pipe(
catchError((err) => {
console.error('Validate container subflow failed', err);
return throwError(() => err);
})
);
}
private clearEmptyBlockCache() {
this.emptyBlockCache.clear();
this.pendingEmptyBlockRequests.clear();
}
private createEmptyNodeRequest(blockType: BlockTypeName, family?: NodeFamily): Observable<FlowNode> {
const normalizedFamily = family ?? this._blockTypes().find((type) => type.type === blockType)?.family ?? 'block';
return normalizedFamily === 'container'
? this.blocksCallService.createEmptyContainer(blockType)
: this.blocksCallService.createEmptyBlock(blockType);
}
private cloneEmptyNode(block: FlowNode): FlowNode {
const clone = this.deepClone(block);
return {
...clone,
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,
position: undefined
};
}
private deepClone<T>(value: T): T {
if (typeof globalThis.structuredClone === 'function') {
return globalThis.structuredClone(value);
}
return JSON.parse(JSON.stringify(value)) as T;
}
}