소스 검색

feat: add resources store

Thomas Zhang 3 달 전
부모
커밋
07a959059a
1개의 변경된 파일135개의 추가작업 그리고 0개의 파일을 삭제
  1. 135 0
      src/lib/stores/resources.svelte.ts

+ 135 - 0
src/lib/stores/resources.svelte.ts

@@ -0,0 +1,135 @@
+import { SvelteMap, SvelteSet } from 'svelte/reactivity';
+
+import { extractMessage } from '$lib/api/types';
+import { notificationsStore } from '$lib/stores/notifications.svelte';
+import type { ResourceEvent } from '$lib/types/events';
+
+import { eventsStore } from './events.svelte';
+
+type Resource = {
+	group: string;
+	version: string;
+	resources: string;
+};
+
+abstract class ResourceStore<T extends { id: string; name: string }, CRStatus = unknown> {
+	#resourceDisplayName: string;
+	#resources: Set<string>;
+	protected workspaceId = 'default';
+	list = $state<T[]>([]);
+	status = $state<Map<string, CRStatus | undefined>>(new SvelteMap());
+	creating = $state(0);
+	deleting = $state(0);
+
+	constructor(resourceName: string, resources: Resource[]) {
+		this.#resourceDisplayName = resourceName;
+		this.#resources = new SvelteSet(resources.map((r) => `${r.group}/${r.version}/${r.resources}`));
+		eventsStore.subscribe((event) => this.onResourceEvent(event as ResourceEvent<CRStatus>));
+	}
+
+	protected abstract resourceHref(id: string): string | undefined;
+	protected abstract apiGet(workspaceId: string, id: string): Promise<T>;
+	protected abstract apiGetStatus(workspaceId: string, id: string): Promise<CRStatus>;
+	protected abstract apiCreate(workspaceId: string, item: T): Promise<T>;
+	protected abstract apiUpdate(workspaceId: string, item: T): Promise<T>;
+	protected abstract apiDelete(workspaceId: string, id: string): Promise<void>;
+	protected abstract onStatusChange(id: string, status: CRStatus, notify?: boolean): void;
+
+	setWorkspaceId(id: string): void {
+		this.workspaceId = id;
+		this.list = [];
+		this.creating = 0;
+		this.deleting = 0;
+	}
+
+	hydrate(items: T[]): void {
+		this.list = items;
+	}
+
+	upsert(item: T, status?: CRStatus): T {
+		const existing = this.list.findIndex((i) => i.id === item.id);
+		let reactiveItem: T;
+		if (existing !== -1) {
+			this.list[existing] = item;
+			reactiveItem = this.list[existing];
+		} else {
+			this.list.push(item);
+			reactiveItem = this.list[this.list.length - 1];
+		}
+		if (status !== undefined) {
+			this.onStatusChange(item.id, status, false);
+		}
+		return reactiveItem;
+	}
+
+	get(id: string): T | undefined {
+		return this.list.find((item) => item.id === id);
+	}
+
+	getStatus(id: string): CRStatus | undefined {
+		return this.status.get(id);
+	}
+
+	async refresh(id: string): Promise<T | undefined> {
+		const item = await this.apiGet(this.workspaceId, id);
+		return this.upsert(item);
+	}
+
+	async refreshStatus(id: string): Promise<void> {
+		const status = await this.apiGetStatus(this.workspaceId, id);
+		if (status === undefined) return;
+		this.status.set(id, status);
+		this.onStatusChange(id, status, false);
+	}
+
+	async create(item: T): Promise<T> {
+		try {
+			const createdItem = await this.apiCreate(this.workspaceId, item);
+			notificationsStore.push({
+				type: 'info',
+				message: `${this.#resourceDisplayName} created - deploying...`,
+				href: this.resourceHref(createdItem.id),
+			});
+			return this.upsert(createdItem);
+		} catch (err) {
+			notificationsStore.push({ type: 'error', message: extractMessage(err) });
+			throw err;
+		}
+	}
+
+	async update(item: T): Promise<T> {
+		try {
+			const updatedItem = await this.apiUpdate(this.workspaceId, item);
+			notificationsStore.push({ type: 'success', message: `${this.#resourceDisplayName} updated` });
+			return this.upsert(updatedItem);
+		} catch (err) {
+			notificationsStore.push({ type: 'error', message: extractMessage(err) });
+			throw err;
+		}
+	}
+
+	async remove(id: string): Promise<void> {
+		try {
+			await this.apiDelete(this.workspaceId, id);
+			notificationsStore.push({
+				type: 'success',
+				message: `${this.#resourceDisplayName} deleted`,
+			});
+			const idx = this.list.findIndex((i) => i.id === id);
+			if (idx !== -1) this.list.splice(idx, 1);
+		} catch (err) {
+			notificationsStore.push({ type: 'error', message: extractMessage(err) });
+			throw err;
+		}
+	}
+
+	onResourceEvent(event: ResourceEvent<CRStatus>): void {
+		if (event.namespace) return;
+		if (!this.#resources.has(`${event.group}/${event.version}/${event.resource}`)) return;
+		if (this.list.every((item) => item.id !== event.name)) return;
+		this.status.set(event.name, event.status);
+		this.onStatusChange(event.name, event.status, true);
+	}
+}
+
+export { ResourceStore };