Skip to main content
Glama
status.store.ts9.1 kB
import { defineStore } from "pinia"; import * as _ from "lodash-es"; import { ApiRequest, addStoreHooks } from "@si/vue-lib/pinia"; import { POSITION, useToast } from "vue-toastification"; import { watch } from "vue"; import { useWorkspacesStore } from "@/store/workspaces.store"; import { ChangeSetId } from "@/api/sdf/dal/change_set"; import { ComponentId } from "@/api/sdf/dal/component"; import { useChangeSetsStore } from "./change_sets.store"; import { useRealtimeStore } from "./realtime/realtime.store"; import UpdatingModel from "../components/toasts/UpdatingModel.vue"; import handleStoreError from "./errors"; const GLOBAL_STATUS_TOAST_TIMEOUT = 300; const GLOBAL_STATUS_TOAST_DEBOUNCE = 150; export const GLOBAL_STATUS_TOAST_ID = "global_status_toast"; export type StatusMessageState = "statusStarted" | "statusFinished"; export interface DependentValuesUpdateStatusUpdate { kind: "dependentValueUpdate"; status: StatusMessageState; componentId: string; timestamp: Date; } export interface RebaseStatusUpdate { kind: "rebase"; status: StatusMessageState; timestamp: Date; } export type StatusUpdate = | DependentValuesUpdateStatusUpdate | RebaseStatusUpdate; export type GlobalUpdateStatus = { isUpdating: boolean; }; export type ComponentUpdateStatus = { componentId: string; componentLabel: string; isUpdating: boolean; lastUpdateAt: Date; statusMessage: string; }; export type AttributeValueId = string; export interface AttributeValueStatus { componentId: string; startedAt: Date; finishedAt?: Date; } export type ValueIsForKind = "prop" | "inputSocket" | "outputSocket"; export interface ValueIsFor { kind: ValueIsForKind; id?: string; } export type ComponentStatusDetails = { lastUpdatedAt?: Date; message: string; }; export interface RebaseStatus { rebaseStart?: Date; rebaseFinished?: Date; count: number; } export type Conflict = string; export interface StatusStoreState { activeComponents: Record<ComponentId, DependentValuesUpdateStatusUpdate>; dvuRootsCount: number; rebaseStatus: RebaseStatus; } const FIFTEEN_SECONDS_MS = 1000 * 15; export const useStatusStore = (forceChangeSetId?: ChangeSetId) => { // this needs some work... but we'll probably want a way to force using HEAD // so we can load HEAD data in some scenarios while also loading a change set? let changeSetId: ChangeSetId | undefined; if (forceChangeSetId) { changeSetId = forceChangeSetId; } else { const changeSetsStore = useChangeSetsStore(); changeSetId = changeSetsStore.selectedChangeSetId; } const realtimeStore = useRealtimeStore(); const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; const toast = useToast(); const visibilityParams = { visibility_change_set_pk: changeSetId, workspaceId, }; return addStoreHooks( workspaceId, changeSetId, defineStore( `ws${workspaceId || "NONE"}/cs${changeSetId || "NONE"}/status`, { state: (): StatusStoreState => ({ activeComponents: {}, rebaseStatus: { count: 0 }, dvuRootsCount: 0, }), getters: { globalStatus(state): GlobalUpdateStatus { const nowMs = Date.now(); const isUpdatingDvus = Object.keys(this.activeComponents).length > 0 || this.dvuRootsCount > 0; const isRebasing = !state.rebaseStatus.rebaseFinished && nowMs - Number(state.rebaseStatus.rebaseStart) > FIFTEEN_SECONDS_MS; return { isUpdating: isUpdatingDvus || isRebasing, }; }, globalStatusMessage(): string { if (this.globalStatus.isUpdating) { return "Updating the model"; } return "Model is up to date"; }, componentIsLoading: (state) => (id: ComponentId) => { return state.activeComponents[id] !== undefined; }, }, actions: { resetWhenChangingChangeset() { this.activeComponents = {}; this.rebaseStatus = { rebaseStart: undefined, rebaseFinished: undefined, count: 0, }; this.dvuRootsCount = 0; }, async FETCH_DVU_ROOTS() { return new ApiRequest<{ count: number }>({ url: "diagram/dvu_roots", params: { ...visibilityParams, }, onSuccess: (response) => { this.dvuRootsCount = response.count; if (response.count < 1) { this.activeComponents = {}; } }, }); }, registerRequestsBegin(requestUlid: string, actionName: string) { realtimeStore.inflightRequests.set(requestUlid, actionName); }, registerRequestsEnd(requestUlid: string) { realtimeStore.inflightRequests.delete(requestUlid); }, }, onActivated() { if (!changeSetId) return; const debouncedFetchDvuRoots = _.debounce(this.FETCH_DVU_ROOTS, 1000); // Just in case we miss a change set written and we still think there // are roots, but we don't know about any active components. const dvuRootCheck = setInterval(async () => { if ( this.dvuRootsCount > 0 && Object.keys(this.activeComponents).length < 1 ) { const resp = await debouncedFetchDvuRoots(); if (!resp?.result.success) { clearInterval(dvuRootCheck); } } }, 3500); realtimeStore.subscribe(this.$id, `changeset/${changeSetId}`, [ { eventType: "ChangeSetWritten", callback: () => { if (this.dvuRootsCount > 0) { debouncedFetchDvuRoots(); } }, }, { eventType: "StatusUpdate", callback: (update, _metadata) => { if (!this.globalStatus.isUpdating) { // if we're done updating, clear the old data this.activeComponents = {}; this.rebaseStatus = { rebaseStart: undefined, rebaseFinished: undefined, count: 0, }; } if (update.kind === "dependentValueUpdate") { if (update.status === "statusStarted") { this.activeComponents[update.componentId] = update; } else if (update.status === "statusFinished") { if ( this.activeComponents[update.componentId] !== undefined ) { delete this.activeComponents[update.componentId]; } } } else if (update.kind === "rebase") { if (update.status === "statusStarted") { if (!this.rebaseStatus.rebaseStart) { this.rebaseStatus.rebaseStart = update.timestamp; } this.rebaseStatus.count++; } else if (update.status === "statusFinished") { if (this.rebaseStatus.rebaseStart) { this.rebaseStatus.count--; if (this.rebaseStatus.count <= 0) { this.rebaseStatus.rebaseFinished = update.timestamp; } } } } }, }, ]); // This watcher updates the GlobalStatus toast watch( () => this.globalStatus.isUpdating, () => { _.debounce( () => { toast.update( GLOBAL_STATUS_TOAST_ID, { content: { component: UpdatingModel, }, options: { position: POSITION.TOP_CENTER, timeout: this.globalStatus.isUpdating ? false : GLOBAL_STATUS_TOAST_TIMEOUT, closeOnClick: !this.globalStatus.isUpdating, toastClassName: "si-toast-no-defaults", }, }, true, ); }, GLOBAL_STATUS_TOAST_DEBOUNCE, { leading: true }, )(); }, ); const actionUnsub = this.$onAction(handleStoreError); return () => { actionUnsub(); clearInterval(dvuRootCheck); realtimeStore.unsubscribe(this.$id); }; }, }, ), )(); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server