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

152 lines
4.9 KiB
TypeScript

import { computed, Injectable, signal } from '@angular/core';
import { environment } from '@environment';
import { BlockType, BlockTypeName, FlowData, FlowNode } from '@models/flow';
import { catchError, finalize, firstValueFrom, map, Observable, of, shareReplay, throwError } from 'rxjs';
import { ContainersCallServiceBase } from './container-call.base';
@Injectable({
providedIn: 'root',
})
export class ContainersService {
containersCallService: ContainersCallServiceBase = new environment.containersCallService();
toInit = true;
private loadingPromise: Promise<void> | null = null;
private readonly emptyContainerCache = new Map<string, FlowNode>();
private readonly pendingEmptyContainerRequests = new Map<string, Observable<FlowNode>>();
private readonly pendingServerSyncCount = signal(0);
private _containerTypes = signal<BlockType[]>([]);
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
readonly containerTypes = this._containerTypes.asReadonly();
hasLoadedContainerTypes() {
return this._containerTypes().length > 0 || !this.toInit;
}
async getAllContainerTypes() {
if (this.toInit) {
this.toInit = false;
await this.refresh();
}
return this._containerTypes.asReadonly();
}
async refresh(force = false): Promise<void> {
if (this.loadingPromise && !force) {
return this.loadingPromise;
}
this.loadingPromise = firstValueFrom(this.containersCallService.retrieveAllContainerTypes())
.then((containerTypes) => {
this._containerTypes.set(containerTypes);
this.clearEmptyContainerCache();
})
.catch((err) => {
console.error('Retrieve container types failed', err);
throw err;
})
.finally(() => {
this.loadingPromise = null;
});
return this.loadingPromise;
}
async getContainerType(typeName: BlockTypeName) {
const current = this._containerTypes().find((containerType) => containerType.type === typeName);
if (current) return current;
const containerTypes = await firstValueFrom(this.containersCallService.retrieveAllContainerTypes());
this._containerTypes.set(containerTypes);
this.clearEmptyContainerCache();
return containerTypes.find((containerType) => containerType.type === typeName);
}
peekContainerType(typeName: BlockTypeName) {
return this._containerTypes().find((containerType) => containerType.type === typeName) ?? null;
}
createEmptyContainer(containerType: BlockTypeName) {
const cacheKey = String(containerType);
const cached = this.emptyContainerCache.get(cacheKey);
if (cached) {
return of(this.cloneEmptyNode(cached));
}
const pending = this.pendingEmptyContainerRequests.get(cacheKey);
if (pending) {
return pending.pipe(map((container) => this.cloneEmptyNode(container)));
}
const request = this.containersCallService.createEmptyContainer(containerType).pipe(
map((container) => {
this.emptyContainerCache.set(cacheKey, this.cloneEmptyNode(container));
return container;
}),
finalize(() => {
this.pendingEmptyContainerRequests.delete(cacheKey);
}),
shareReplay(1)
);
this.pendingEmptyContainerRequests.set(cacheKey, request);
return request.pipe(
map((container) => this.cloneEmptyNode(container)),
catchError((err) => {
console.error('Create empty container failed', err);
return throwError(() => err);
})
);
}
createContainer(containerId: string, configuration: any) {
this.pendingServerSyncCount.update((count) => count + 1);
return this.containersCallService.createContainer(containerId, configuration).pipe(
finalize(() => {
this.pendingServerSyncCount.update((count) => Math.max(0, count - 1));
}),
catchError((err) => {
console.error('Create container failed', err);
return throwError(() => err);
})
);
}
validateContainerSubflow(subFlow: FlowData, validationUrl?: string | null) {
return this.containersCallService.validateContainerSubflow(this.deepClone(subFlow), validationUrl).pipe(
catchError((err) => {
console.error('Validate container subflow failed', err);
return throwError(() => err);
})
);
}
private clearEmptyContainerCache() {
this.emptyContainerCache.clear();
this.pendingEmptyContainerRequests.clear();
}
private cloneEmptyNode(node: FlowNode): FlowNode {
const clone = this.deepClone(node);
return {
...clone,
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,
position: undefined
};
}
private deepClone<T>(value: T): T {
if (typeof globalThis.structuredClone === 'function') {
try {
return globalThis.structuredClone(value);
} catch {
// Some cached payloads may carry non-cloneable runtime fields.
}
}
return JSON.parse(JSON.stringify(value)) as T;
}
}