Skip to main content
Glama
funcs.store.ts35.8 kB
import * as _ from "lodash-es"; import { defineStore } from "pinia"; import { addStoreHooks, ApiRequest } from "@si/vue-lib/pinia"; import { ulid } from "ulid"; import { useToast } from "vue-toastification"; import { URLPattern } from "@si/vue-lib"; import { Visibility } from "@/api/sdf/dal/visibility"; import { FuncArgument, FuncId, FuncArgumentId, FuncSummary, FuncCode, FuncBinding, FuncBindingKind, FuncKind, Action, Attribute, CodeGeneration, Authentication, Qualification, FuncBackendKind, BindingWithBackendKind, Management, ManagementPrototypeId, } from "@/api/sdf/dal/func"; import { nilId } from "@/utils/nilId"; import { trackEvent } from "@/utils/tracking"; import keyedDebouncer from "@/utils/keyedDebouncer"; import { useWorkspacesStore } from "@/store/workspaces.store"; import { useAssetStore } from "@/store/asset.store"; import { ComponentType, SchemaVariant, SchemaVariantId, } from "@/api/sdf/dal/schema"; import { DefaultMap } from "@/utils/defaultmap"; import { ComponentId } from "@/api/sdf/dal/component"; import { ViewId } from "@/api/sdf/dal/views"; import { useChangeSetsStore } from "../change_sets.store"; import { useRealtimeStore } from "../realtime/realtime.store"; import { FuncRunId } from "../func_runs.store"; import { useViewsStore } from "../views.store"; import { useAuthStore } from "../auth.store"; type FuncExecutionState = | "Create" | "Dispatch" | "Failure" | "Run" | "Start" | "Success"; // TODO: remove when fn log stuff gets figured out a bit deeper /* eslint-disable @typescript-eslint/no-explicit-any */ export type FuncExecutionLog = { id: FuncId; state: FuncExecutionState; value?: any; outputStream?: any[]; functionFailure?: any; // FunctionResultFailure }; export interface DeleteFuncResponse { success: boolean; name: string; } export interface AttributeWithVariant extends Attribute { schemaVariantId: NonNullable<SchemaVariantId>; } export interface BindingWithDisplayName extends Action { displayName?: string | null; name: string; } export type MgmtPrototype = { label: string; managementPrototypeId: ManagementPrototypeId; funcId: FuncId; }; export type MgmtPrototypeResult = { message?: string; status: "ok" | "error"; createdComponentIds?: ComponentId[]; }; export function actionBindingsForVariant( variant: SchemaVariant, summaries: Record<FuncId, FuncSummary>, actionBindings: Record<FuncId, Action[]>, ) { const bindings = [] as BindingWithDisplayName[]; variant?.funcIds.forEach((funcId) => { const summary = summaries[funcId]; const actions = actionBindings[funcId]?.filter( (b) => b.schemaVariantId === variant.schemaVariantId, ); if (actions && actions.length > 0) { actions.forEach((b) => { const a = _.clone(b) as BindingWithDisplayName; a.displayName = summary?.displayName; a.name = summary?.name || "<not set>"; bindings.push(a); }); } }); return bindings; } export const INTRINSICS_DISPLAYED = [ FuncBackendKind.Unset, FuncBackendKind.Identity, FuncBackendKind.NormalizeToArray, ]; export const useFuncStore = () => { const viewStore = useViewsStore(); const changeSetsStore = useChangeSetsStore(); const selectedChangeSetId: string | undefined = changeSetsStore.selectedChangeSet?.id; // TODO(nick): we need to allow for empty visibility here. Temporarily send down "nil" to mean that we want the // query to find the default change set. const visibility: Visibility = { visibility_change_set_pk: selectedChangeSetId ?? changeSetsStore.headChangeSetId ?? nilId(), }; const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; const authStore = useAuthStore(); const toast = useToast(); const realtimeStore = useRealtimeStore(); let funcSaveDebouncer: ReturnType<typeof keyedDebouncer> | undefined; const processBindings = (func: FuncSummary) => { const actionBindings = [] as Action[]; const attributeBindings = [] as Attribute[]; const authenticationBindings = [] as Authentication[]; const codegenBindings = [] as CodeGeneration[]; const qualificationBindings = [] as Qualification[]; const managementBindings = [] as Management[]; func.bindings.forEach((binding) => { switch (binding.bindingKind) { case FuncBindingKind.Action: actionBindings.push(binding as Action); break; case FuncBindingKind.Attribute: attributeBindings.push(binding as Attribute); break; case FuncBindingKind.Authentication: authenticationBindings.push(binding as Authentication); break; case FuncBindingKind.CodeGeneration: codegenBindings.push(binding as CodeGeneration); break; case FuncBindingKind.Qualification: qualificationBindings.push(binding as Qualification); break; case FuncBindingKind.Management: managementBindings.push(binding as Management); break; default: throw new Error(`Unexpected FuncBinding ${JSON.stringify(binding)}`); } }); return { actionBindings, attributeBindings, authenticationBindings, codegenBindings, qualificationBindings, managementBindings, }; }; const BASE_API = [ "v2", "workspaces", { workspaceId }, "change-sets", { selectedChangeSetId }, ] as URLPattern; const API_PREFIX = BASE_API.concat(["funcs"]) as URLPattern; return addStoreHooks( workspaceId, selectedChangeSetId, defineStore(`ws${workspaceId || "NONE"}/cs${selectedChangeSetId}/funcs`, { state: () => ({ // this powers the list funcsById: {} as Record<FuncId, FuncSummary>, // this is the code funcCodeById: {} as Record<FuncId, FuncCode>, // bindings actionBindings: {} as Record<FuncId, Action[]>, attributeBindings: {} as Record<FuncId, Attribute[]>, authenticationBindings: {} as Record<FuncId, Authentication[]>, codegenBindings: {} as Record<FuncId, CodeGeneration[]>, qualificationBindings: {} as Record<FuncId, Qualification[]>, managementBindings: {} as Record<FuncId, Management[]>, // represents the last, or "focused" func clicked on/open by the editor selectedFuncId: undefined as FuncId | undefined, editingFuncLatestCode: {} as Record<FuncId, string>, managementOperationExecution: {} as MgmtPrototypeResult, // So we can ignore websocket update originated by this client clientUlid: ulid(), }), getters: { selectedFuncSummary(state): FuncSummary | undefined { return state.funcsById[this.selectedFuncId || ""]; }, selectedFuncCode(state): FuncCode | undefined { return state.funcCodeById[this.selectedFuncId || ""]; }, nameForSchemaVariantId: (_state) => (schemaVariantId: string) => useAssetStore().variantFromListById[schemaVariantId]?.schemaName, funcList: (state) => _.values(state.funcsById), managementFunctionsForSelectedComponent(state) { const mgmtFuncs: MgmtPrototype[] = []; if ( viewStore.selectedComponent?.def.componentType === ComponentType.View ) return mgmtFuncs; const variant = useAssetStore().variantFromListById[ viewStore.selectedComponent?.def.schemaVariantId || "" ]; if (!variant) return []; variant.funcIds.forEach((funcId) => { const func = state.funcsById[funcId]; if (func?.kind === FuncKind.Management) { const funcId = func.funcId; const binding = state.managementBindings[funcId]?.find( (b) => b.schemaVariantId === variant.schemaVariantId, ); if (binding && binding.managementPrototypeId) mgmtFuncs.push({ managementPrototypeId: binding.managementPrototypeId, label: func.displayName || func.name, funcId, }); } }); return mgmtFuncs; }, actionBindingsForSelectedComponent(): BindingWithDisplayName[] { if ( viewStore.selectedComponent?.def.componentType === ComponentType.View ) return []; const variant = useAssetStore().variantFromListById[ viewStore.selectedComponent?.def.schemaVariantId || "" ]; if (!variant) return []; const summaries: Record<FuncId, FuncSummary> = {}; const actionBindings: Record<FuncId, Action[]> = {}; variant?.funcIds.forEach((funcId) => { const summary = this.funcsById[funcId]; if (summary) summaries[funcId] = summary; const actions = this.actionBindings[funcId]; if (actions) actionBindings[funcId] = actions; }); return actionBindingsForVariant(variant, summaries, actionBindings); }, intrinsicBindingsByVariant( state, ): Map<SchemaVariantId, BindingWithBackendKind[]> { const _bindings = new DefaultMap< SchemaVariantId, BindingWithBackendKind[] >(() => []); Object.values(state.funcsById) .filter((func) => INTRINSICS_DISPLAYED.includes(func.backendKind)) .forEach((func) => { func.bindings .filter( (binding): binding is AttributeWithVariant => !!binding.schemaVariantId && binding.bindingKind === FuncBindingKind.Attribute, ) .forEach((binding) => { const _curr = _bindings.get(binding.schemaVariantId); const b = binding as BindingWithBackendKind; b.backendKind = func.backendKind; _curr.push(b); _bindings.set(binding.schemaVariantId, _curr); }); }); return _bindings; }, }, actions: { async RUN_MGMT_PROTOTYPE( prototypeId: ManagementPrototypeId, componentId: ComponentId, viewId: ViewId, ) { return new ApiRequest<MgmtPrototypeResult>({ method: "post", keyRequestStatusBy: [prototypeId, componentId], url: BASE_API.concat([ "management", "prototype", { prototypeId }, { componentId }, { viewId }, ]), onFail: (err) => { if (err.response.status === 400) { toast( `Error executing management function: ${err.response.data.error.message}`, ); } }, }); }, async FETCH_FUNC_LIST() { return new ApiRequest<FuncSummary[], Visibility>({ url: API_PREFIX, onSuccess: (response) => { response.forEach((func) => { const bindings = processBindings(func); this.actionBindings[func.funcId] = bindings.actionBindings; this.attributeBindings[func.funcId] = bindings.attributeBindings; this.authenticationBindings[func.funcId] = bindings.authenticationBindings; this.qualificationBindings[func.funcId] = bindings.qualificationBindings; this.codegenBindings[func.funcId] = bindings.codegenBindings; this.managementBindings[func.funcId] = bindings.managementBindings; }); this.funcsById = _.keyBy(response, (f) => f.funcId); }, }); }, async FETCH_CODE(funcId: FuncId) { return new ApiRequest<FuncCode[]>({ url: API_PREFIX.concat(["code"]), params: { id: funcId, }, keyRequestStatusBy: funcId, onSuccess: (response) => { response.forEach((func: FuncCode) => { this.funcCodeById[func.funcId] = func; }); }, }); }, async CREATE_FUNC(createFuncRequest: { name: string; displayName: string; description: string; kind: FuncKind; binding: FuncBinding; }) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<{ summary: FuncSummary; code: FuncCode }>({ method: "post", url: API_PREFIX, params: { ...createFuncRequest }, onSuccess: (response) => { // summary coming through the WsEvent this.funcCodeById[response.code.funcId] = response.code; // select the fn to load it in the editor done in the component }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async CREATE_UNLOCKED_COPY( funcId: FuncId, schemaVariantId?: SchemaVariantId, ) { return new ApiRequest<{ summary: FuncSummary; code: FuncCode }>({ method: "post", url: API_PREFIX.concat([{ funcId }]), params: { schemaVariantId, }, onSuccess: (response) => { for (const binding of response.summary.bindings) { if (!binding.schemaVariantId || !binding.funcId) continue; useAssetStore().replaceFuncForVariant( binding.schemaVariantId, funcId, binding.funcId, ); } this.funcsById[response.summary.funcId] = response.summary; this.funcCodeById[response.code.funcId] = response.code; }, }); }, async DELETE_UNLOCKED_FUNC(funcId: FuncId) { return new ApiRequest<DeleteFuncResponse>({ method: "delete", url: API_PREFIX.concat([{ funcId }]), }); }, async UPDATE_FUNC(func: FuncSummary) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; const isHead = changeSetsStore.headSelected; return new ApiRequest({ method: "put", url: API_PREFIX.concat([{ funcId: func.funcId }]), params: { displayName: func.displayName, description: func.description, clientUlid: this.clientUlid, }, optimistic: () => { if (isHead) return () => {}; const current = _.cloneDeep(this.funcsById[func.funcId]); const editing = this.funcsById[func.funcId]; if (editing) { editing.displayName = func.displayName; editing.description = func.description; } return () => { if (current) { this.funcsById[func.funcId] = current; } else { delete this.funcCodeById[func.funcId]; delete this.funcsById[func.funcId]; } }; }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, keyRequestStatusBy: func.funcId, }); }, async CREATE_BINDING(funcId: FuncId, bindings: FuncBinding[]) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<FuncBinding[]>({ method: "post", url: API_PREFIX.concat([{ funcId }, "bindings"]), params: { bindings, }, optimistic: () => { // if it has prototype, replace // otherwise append const func = this.funcsById[funcId]; if (func) { bindings.forEach((binding) => { // TS not being friendly to genericize this if ("attributePrototypeId" in binding) { if (binding.attributePrototypeId) { const bIdx = func.bindings.findIndex((b) => { if ("attributePrototypeId" in b) { if ( b.attributePrototypeId && b.attributePrototypeId === binding.attributePrototypeId ) return true; } return false; }); if (bIdx !== -1) { func.bindings[bIdx] = binding; } else func.bindings.push(binding); } } else if ("managementPrototypeId" in binding) { if (binding.managementPrototypeId) { const bIdx = func.bindings.findIndex((b) => { if ("managementPrototypeId" in b) { if ( b.managementPrototypeId && b.managementPrototypeId === binding.managementPrototypeId ) return true; } return false; }); if (bIdx !== -1) { func.bindings[bIdx] = binding; } else func.bindings.push(binding); } } else { func.bindings.push(binding); } }); // reset the secondary data structures const _bindings = processBindings(func); this.actionBindings[func.funcId] = _bindings.actionBindings; this.attributeBindings[func.funcId] = _bindings.attributeBindings; this.authenticationBindings[func.funcId] = _bindings.authenticationBindings; this.qualificationBindings[func.funcId] = _bindings.qualificationBindings; this.codegenBindings[func.funcId] = _bindings.codegenBindings; this.managementBindings[func.funcId] = _bindings.managementBindings; } }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async UPDATE_BINDING(funcId: FuncId, bindings: FuncBinding[]) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "put", url: API_PREFIX.concat([{ funcId }, "bindings"]), params: { funcId, bindings, }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, // How you "DETACH" an attribute function async RESET_ATTRIBUTE_BINDING(funcId: FuncId, bindings: FuncBinding[]) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "post", url: API_PREFIX.concat([{ funcId }, "reset_attribute_binding"]), params: { bindings, }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, // How you "DETACH" all other function bindings async DELETE_BINDING(funcId: FuncId, bindings: FuncBinding[]) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "delete", url: API_PREFIX.concat([{ funcId }, "bindings"]), params: { bindings, }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async CREATE_FUNC_ARGUMENT(funcId: FuncId, funcArg: FuncArgument) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "post", url: API_PREFIX.concat([{ funcId }, "arguments"]), params: { ...funcArg, }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async UPDATE_FUNC_ARGUMENT(funcId: FuncId, funcArg: FuncArgument) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "put", url: API_PREFIX.concat([ { funcId }, "arguments", { funcArgumentId: funcArg.id }, ]), params: { ...funcArg, }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async DELETE_FUNC_ARGUMENT( funcId: FuncId, funcArgumentId: FuncArgumentId, ) { if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest<null>({ method: "delete", url: API_PREFIX.concat([ { funcId }, "arguments", { funcArgumentId }, ]), onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async EXEC_FUNC(funcId: FuncId) { const func = this.funcsById[funcId]; if (func) { trackEvent("func_execute", { id: func.funcId, name: func.name, }); } if (changeSetsStore.creatingChangeSet) throw new Error("race, wait until the change set is created"); if (changeSetsStore.headSelected) changeSetsStore.creatingChangeSet = true; return new ApiRequest({ method: "post", url: API_PREFIX.concat([{ funcId }, "execute"]), keyRequestStatusBy: funcId, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, async FETCH_PROTOTYPE_ARGUMENTS( propId?: string, outputSocketId?: string, ) { return new ApiRequest<{ preparedArguments: Record<string, unknown>; }>({ url: "attribute/get_prototype_arguments", params: { propId, outputSocketId, ...visibility }, }); }, async TEST_EXECUTE(executeRequest: { funcId: FuncId; args: unknown; code: string; componentId: string; }) { const func = this.funcsById[executeRequest.funcId]; if (func) { trackEvent("function_test_execute", { id: func.funcId, name: func.name, }); } // why aren't we doing anything with the result of this?! return new ApiRequest<{ funcRunId: FuncRunId; }>({ method: "post", url: API_PREFIX.concat([ { funcId: executeRequest.funcId }, "test_execute", ]), params: { ...executeRequest }, }); }, updateFuncCode(funcId: FuncId, code: string, debounce: boolean) { const func = _.cloneDeep(this.funcCodeById[funcId]); if (!func || func.code === code) return; func.code = code; this.enqueueFuncSave(func, debounce); }, enqueueFuncSave(func: FuncCode, debounce: boolean) { if (!debounce) { return this.SAVE_FUNC(func); } this.editingFuncLatestCode[func.funcId] = func.code; // Lots of ways to handle this... we may want to handle this debouncing in the component itself // so the component has its own "draft" state that it passes back to the store when it's ready to save // however this should work for now, and lets the store handle this logic if (!funcSaveDebouncer) { funcSaveDebouncer = keyedDebouncer((id: FuncId) => { const f = this.funcCodeById[id]; const code = this.editingFuncLatestCode[id]; if (!f || !code) return; f.code = code; this.SAVE_FUNC(f); }, 500); } // call debounced function which will trigger sending the save to the backend const saveFunc = funcSaveDebouncer(func.funcId); if (saveFunc) { saveFunc(func.funcId); } }, async SAVE_FUNC(func: FuncCode) { return new ApiRequest<FuncCode>({ method: "put", url: API_PREFIX.concat([{ funcId: func.funcId }, "code"]), params: { code: func.code }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, }); }, registerRequestsBegin(requestUlid: string, actionName: string) { realtimeStore.inflightRequests.set(requestUlid, actionName); }, registerRequestsEnd(requestUlid: string) { // Removing the Ulid is delayed for 10 seconds to prevent WSEvent race conditions with saving // Such race conditions could cause the FuncEditor to suddenly reload when a WSEvent arrives // to the frontend after its corresponding request has been deleted from inflightRequests setTimeout(() => { realtimeStore.inflightRequests.delete(requestUlid); }, 10000); }, }, async onActivated() { realtimeStore.subscribe(this.$id, `changeset/${selectedChangeSetId}`, [ // we need func list on new schema variants, b/c the updated bindings // don't come back in the schema variant WsEvent payload // but we have a FoC while the request is in flight { eventType: "SchemaVariantCreated", callback: () => { this.FETCH_FUNC_LIST(); }, }, { eventType: "SchemaVariantUpdated", callback: () => { this.FETCH_FUNC_LIST(); }, }, { eventType: "SchemaVariantReplaced", callback: () => { this.FETCH_FUNC_LIST(); }, }, { eventType: "SchemaVariantCloned", callback: () => { this.FETCH_FUNC_LIST(); }, }, { eventType: "FuncBindingsUpdated", callback: (data) => { if (data.changeSetId !== selectedChangeSetId) return; // all the bindings for one given func const funcId = data.bindings[0]?.funcId; if (funcId) { const func = this.funcsById[funcId]; if (func) func.bindings = data.bindings; } }, }, { eventType: "FuncCreated", callback: (data, metadata) => { if (data.changeSetId !== selectedChangeSetId) return; this.funcsById[data.funcSummary.funcId] = data.funcSummary; const bindings = processBindings(data.funcSummary); this.actionBindings[data.funcSummary.funcId] = bindings.actionBindings; this.attributeBindings[data.funcSummary.funcId] = bindings.attributeBindings; this.authenticationBindings[data.funcSummary.funcId] = bindings.authenticationBindings; this.qualificationBindings[data.funcSummary.funcId] = bindings.qualificationBindings; this.codegenBindings[data.funcSummary.funcId] = bindings.codegenBindings; this.managementBindings[data.funcSummary.funcId] = bindings.managementBindings; if ( metadata.actor !== "System" && metadata.actor.User === authStore.userPk ) { const assetStore = useAssetStore(selectedChangeSetId); // NOTE: `SchemaVariantCreated` will fire the selection for it assetStore.setFuncSelection(data.funcSummary.funcId); } }, }, { eventType: "FuncUpdated", callback: (data) => { if (data.changeSetId !== selectedChangeSetId) return; // Requests that send client ID are assumed to update the state directly // So we skip updating them from the websocket event // This is implemented to fix funcSummary being wiped on high latency systems // but possibly needs to be implemented for the other func update endpoints if (data.clientUlid === this.clientUlid) return; this.funcsById[data.funcSummary.funcId] = data.funcSummary; const bindings = processBindings(data.funcSummary); this.actionBindings[data.funcSummary.funcId] = bindings.actionBindings; this.attributeBindings[data.funcSummary.funcId] = bindings.attributeBindings; this.authenticationBindings[data.funcSummary.funcId] = bindings.authenticationBindings; this.qualificationBindings[data.funcSummary.funcId] = bindings.qualificationBindings; this.codegenBindings[data.funcSummary.funcId] = bindings.codegenBindings; this.managementBindings[data.funcSummary.funcId] = bindings.managementBindings; }, }, { eventType: "FuncDeleted", callback: (data) => { if (data.changeSetId !== selectedChangeSetId) return; this.FETCH_FUNC_LIST(); }, }, { eventType: "ModuleImported", callback: () => { if (this.selectedFuncId) this.FETCH_CODE(this.selectedFuncId); }, }, { eventType: "FuncCodeSaved", callback: ({ funcCode: { funcId } }, metadata) => { if (metadata.change_set_id !== selectedChangeSetId) return; // TODO we update every time *any* function is generated unless you are the // one that made the change in the function editor. If we were to remove that check // every time your code editor saved, it would remount with the new code, and you'd lose // your cursor. It would feel like you can't just keep typing, and you'd get angry // This check is why we need to delay removing Ulids from inflightRequests by 10 seconds if (funcId === this.selectedFuncId || !this.selectedFuncId) { const didIFireThisRequest = !!realtimeStore.inflightRequests.get(metadata.request_ulid); if (metadata.actor === "System" || !didIFireThisRequest) { this.FETCH_CODE(funcId); } } }, }, { eventType: "FuncSaved", callback: (data) => { if (data.changeSetId !== selectedChangeSetId) return; // TODO: jobelenus, send data over the wire so i dont need this call this.FETCH_FUNC_LIST(); // TODO, i dont know how this would ever fire to someone sitting on head? // wouldn't we listen for an event "changeset applied"??? if (this.selectedFuncId) { // Only fetch if we don't have the selected func in our state or if we are on HEAD. // If we are on HEAD, the func is immutable, so we are safe to fetch. However, if // we are not on HEAD, then the func is mutable. Therefore, we can only fetch // relevant metadata in order to avoid overwriting functions with their previous // value before the save queue is drained. if (data.funcId === this.selectedFuncId) { if ( typeof this.funcCodeById[this.selectedFuncId] === "undefined" || changeSetsStore.headSelected ) { this.FETCH_CODE(this.selectedFuncId); } } } }, }, { eventType: "ManagementOperationsComplete", callback: ( { status, message, createdComponentIds, funcName }, _metadata, ) => { const didIFireThisRequest = realtimeStore.inflightRequests.get( _metadata.request_ulid, ); if (didIFireThisRequest) { if (createdComponentIds?.length) { viewStore.setSelectedComponentId(createdComponentIds); } this.managementOperationExecution = { status, message, createdComponentIds, }; } const toastOptions = { timeout: 5000, }; const toastMessage = message?.length ? `Ran ${funcName}: ${message}` : `Ran ${funcName}`; if (status === "ok") { toast.success(toastMessage, toastOptions); } else { toast.warning(toastMessage, toastOptions); } }, }, ]); return () => { 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