diff --git a/src/Frontend/src/components/messages/MessageView.vue b/src/Frontend/src/components/messages/MessageView.vue index b54f6679d..fef7c0a3c 100644 --- a/src/Frontend/src/components/messages/MessageView.vue +++ b/src/Frontend/src/components/messages/MessageView.vue @@ -10,6 +10,7 @@ import TimeSince from "../TimeSince.vue"; import moment from "moment"; import ConfirmDialog from "../ConfirmDialog.vue"; import FlowDiagram from "./FlowDiagram.vue"; +import SequenceDiagram from "./SequenceDiagram.vue"; import EditRetryDialog from "../failedmessages/EditRetryDialog.vue"; import routeLinks from "@/router/routeLinks"; import { EditAndRetryConfig } from "@/resources/Configuration"; @@ -43,6 +44,7 @@ const showEditRetryModal = ref(false); const configuration = useConfiguration(); const isMassTransitConnected = useIsMassTransitConnected(); +const showAllMessages = window.defaultConfig.showAllMessages; async function loadFailedMessage() { const response = await useFetchFromServiceControl(`errors/last/${id.value}`); @@ -71,7 +73,7 @@ async function loadFailedMessage() { } updateMessageDeleteDate(message); - await downloadHeadersAndBody(message); + await fetchMessageDetails(message); failedMessage.value = message; } @@ -115,7 +117,7 @@ async function retryMessage() { } } -async function downloadHeadersAndBody(message: ExtendedFailedMessage) { +async function fetchMessageDetails(message: ExtendedFailedMessage) { if (isError(message)) return; try { @@ -349,11 +351,13 @@ onUnmounted(() => { + + diff --git a/src/Frontend/src/components/messages/SequenceDiagram.vue b/src/Frontend/src/components/messages/SequenceDiagram.vue new file mode 100644 index 000000000..bcfa1b396 --- /dev/null +++ b/src/Frontend/src/components/messages/SequenceDiagram.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/Frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue b/src/Frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue new file mode 100644 index 000000000..10cb1da76 --- /dev/null +++ b/src/Frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/Frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue b/src/Frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue new file mode 100644 index 000000000..19bc2b00f --- /dev/null +++ b/src/Frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/Frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue b/src/Frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue new file mode 100644 index 000000000..6e3497acd --- /dev/null +++ b/src/Frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/Frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue b/src/Frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue new file mode 100644 index 000000000..faad6f882 --- /dev/null +++ b/src/Frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/Frontend/src/resources/FailedMessage.ts b/src/Frontend/src/resources/FailedMessage.ts index 74f1b208d..20002e990 100644 --- a/src/Frontend/src/resources/FailedMessage.ts +++ b/src/Frontend/src/resources/FailedMessage.ts @@ -1,5 +1,6 @@ import type EndpointDetails from "@/resources/EndpointDetails"; import type Header from "@/resources/Header"; +import { ConversationModel } from "./SequenceDiagram/SequenceModel"; export default interface FailedMessage { id: string; @@ -37,6 +38,7 @@ export interface ExtendedFailedMessage extends FailedMessage { bodyUnavailable: boolean; headers: Header[]; conversationId: string; + conversation?: ConversationModel; messageBody: string; contentType: string; isEditAndRetryEnabled: boolean; diff --git a/src/Frontend/src/resources/Message.ts b/src/Frontend/src/resources/Message.ts index 977497bbb..c2d38b17e 100644 --- a/src/Frontend/src/resources/Message.ts +++ b/src/Frontend/src/resources/Message.ts @@ -16,10 +16,12 @@ export default interface Message { conversation_id: string; headers: Header[]; status: MessageStatus; - message_intent: string; + message_intent: MessageIntent; body_url: string; body_size: number; instance_id: string; + invoked_sagas?: SagaInfo[]; + originates_from_saga?: SagaInfo; } export interface ExtendedMessage extends Message { notFound: boolean; @@ -36,3 +38,18 @@ export enum MessageStatus { ArchivedFailure = "archivedFailure", RetryIssued = "retryIssued", } + +export enum MessageIntent { + Send = "send", + Publish = "publish", + Subscribe = "subscribe", + Unsubscribe = "unsubscribe", + Reply = "reply", + Init = "init", +} + +interface SagaInfo { + change_status?: string; + saga_type: string; + saga_id: string; +} diff --git a/src/Frontend/src/resources/SequenceDiagram/Endpoint.ts b/src/Frontend/src/resources/SequenceDiagram/Endpoint.ts new file mode 100644 index 000000000..dfa1babd5 --- /dev/null +++ b/src/Frontend/src/resources/SequenceDiagram/Endpoint.ts @@ -0,0 +1,121 @@ +import { NServiceBusHeaders } from "../Header"; +import Message from "../Message"; +import { Handler } from "./Handler"; + +export interface Endpoint { + readonly name: string; + readonly hosts: EndpointHost[]; + readonly hostId: string; + readonly handlers: Handler[]; + addHandler(handler: Handler): void; +} + +export interface EndpointHost { + readonly host: string; + readonly hostId: string; + readonly versions: string[]; +} + +export function createProcessingEndpoint(message: Message): Endpoint { + return new EndpointItem( + message.receiving_endpoint.name, + message.receiving_endpoint.host, + message.receiving_endpoint.host_id, + message.receiving_endpoint.name === message.sending_endpoint.name && message.receiving_endpoint.host === message.sending_endpoint.host ? message.headers.find((h) => h.key === NServiceBusHeaders.NServiceBusVersion)?.value : undefined + ); +} + +export function createSendingEndpoint(message: Message): Endpoint { + return new EndpointItem(message.sending_endpoint.name, message.sending_endpoint.host, message.sending_endpoint.host_id, message.headers.find((h) => h.key === NServiceBusHeaders.NServiceBusVersion)?.value); +} + +export class EndpointRegistry { + #store = new Map(); + + register(item: Endpoint) { + let endpoint = this.#store.get(item.name); + if (!endpoint) { + endpoint = item as EndpointItem; + this.#store.set(endpoint.name, endpoint); + } + + item.hosts.forEach((host) => endpoint.addHost(host as Host)); + } + + get(item: Endpoint) { + return this.#store.get(item.name)! as Endpoint; + } +} + +class EndpointItem implements Endpoint { + private _hosts: Map; + private _name: string; + private _handlers: Handler[] = []; + + constructor(name: string, host: string, id: string, version?: string) { + const initialHost = new Host(host, id, version); + this._hosts = new Map([[initialHost.equatableKey, initialHost]]); + this._name = name; + } + + get name() { + return this._name; + } + get hosts() { + return [...this._hosts].map(([, host]) => host); + } + get host() { + return [...this._hosts].map(([, host]) => host.host).join(","); + } + get hostId() { + return [...this._hosts].map(([, host]) => host.hostId).join(","); + } + get handlers() { + return [...this._handlers]; + } + + addHost(host: Host) { + if (!this._hosts.has(host.equatableKey)) { + this._hosts.set(host.equatableKey, host); + } else { + const existing = this._hosts.get(host.equatableKey)!; + existing.addVersions(host.versions); + } + } + + addHandler(handler: Handler) { + this._handlers.push(handler); + } +} + +class Host implements EndpointHost { + private _host: string; + private _hostId: string; + private _versions: Set; + + constructor(host: string, hostId: string, version?: string) { + this._host = host; + this._hostId = hostId; + this._versions = new Set(); + this.addVersions([version]); + } + + get host() { + return this._host; + } + get hostId() { + return this._hostId; + } + + get versions() { + return [...this._versions]; + } + + get equatableKey() { + return `${this._hostId}###${this._host}`; + } + + addVersions(versions: (string | undefined)[]) { + versions.filter((version) => version).forEach((version) => this._versions.add(version!.toLowerCase())); + } +} diff --git a/src/Frontend/src/resources/SequenceDiagram/Handler.ts b/src/Frontend/src/resources/SequenceDiagram/Handler.ts new file mode 100644 index 000000000..5e13c367b --- /dev/null +++ b/src/Frontend/src/resources/SequenceDiagram/Handler.ts @@ -0,0 +1,133 @@ +import { NServiceBusHeaders } from "../Header"; +import Message, { MessageStatus } from "../Message"; +import { Direction, MessageProcessingRoute, RoutedMessage } from "./RoutedMessage"; +import { Endpoint } from "./Endpoint"; +import { friendlyTypeName } from "./SequenceModel"; + +export interface Handler { + readonly id: string; + name?: string; + readonly endpoint: Endpoint; + readonly isPartOfSaga: boolean; + partOfSaga?: string; + state: HandlerState; + inMessage?: RoutedMessage; + readonly outMessages: RoutedMessage[]; + processedAt?: Date; + readonly handledAt?: Date; + processingTime?: number; + readonly direction: Direction; + route?: MessageProcessingRoute; + readonly selectedMessage?: Message; + updateProcessedAt(timeSent: Date): void; + addOutMessage(routedMessage: RoutedMessage): void; +} + +export enum HandlerState { + Fail, + Success, +} + +export const ConversationStartHandlerName = "First"; + +export function createSendingHandler(message: Message, sendingEndpoint: Endpoint): Handler { + return new HandlerItem(message.headers.find((h) => h.key === NServiceBusHeaders.RelatedTo)?.value ?? ConversationStartHandlerName, sendingEndpoint); +} + +export function createProcessingHandler(message: Message, processingEndpoint: Endpoint): Handler { + const handler = new HandlerItem(message.message_id, processingEndpoint); + updateProcessingHandler(handler, message); + return handler; +} + +export class HandlerRegistry { + #store = new Map(); + private storeKey = (id: string, endpointName: string) => `${id}###${endpointName}`; + + register(handler: Handler) { + const existing = this.#store.get(this.storeKey(handler.id, handler.endpoint.name)); + if (existing) return { handler: existing, isNew: false }; + + this.#store.set(this.storeKey(handler.id, handler.endpoint.name), handler as HandlerItem); + return { handler, isNew: true }; + } +} + +export function updateProcessingHandler(handler: Handler, message: Message) { + handler.processedAt = new Date(message.processed_at); + //assuming if we have days in the timespan then something is very, very wrong + //TODO: extract logic since it's also currently used in AuditList + const [hh, mm, ss] = message.processing_time.split(":"); + handler.processingTime = ((parseInt(hh) * 60 + parseInt(mm)) * 60 + parseFloat(ss)) * 1000; + handler.name = friendlyTypeName(message.message_type); + + if ((message.invoked_sagas?.length ?? 0) > 0) { + handler.partOfSaga = message.invoked_sagas!.map((saga) => friendlyTypeName(saga.saga_type)).join(", "); + } + + switch (message.status) { + case MessageStatus.ArchivedFailure: + case MessageStatus.Failed: + case MessageStatus.RepeatedFailure: + handler.state = HandlerState.Fail; + break; + default: + handler.state = HandlerState.Success; + } +} + +class HandlerItem implements Handler { + private _id: string; + private _endpoint: Endpoint; + private _processedAtGuess?: Date; + private _outMessages: RoutedMessage[]; + name?: string; + partOfSaga?: string; + inMessage?: RoutedMessage; + state: HandlerState = HandlerState.Fail; + processedAt?: Date; + processingTime?: number; + route?: MessageProcessingRoute; + + constructor(id: string, endpoint: Endpoint) { + this._id = id; + this._endpoint = endpoint; + this._outMessages = []; + } + + get id() { + return this._id; + } + + get endpoint() { + return this._endpoint; + } + + get isPartOfSaga() { + return this.partOfSaga != null; + } + + get handledAt() { + return this.processedAt ?? this._processedAtGuess; + } + + get selectedMessage() { + return this.route?.fromRoutedMessage?.selectedMessage; + } + + get outMessages() { + return [...this._outMessages]; + } + + get direction() { + return this.outMessages[0]?.direction ?? Direction.Right; + } + + updateProcessedAt(timeSent: Date) { + if (!this._processedAtGuess || this._processedAtGuess.getTime() > timeSent.getTime()) this._processedAtGuess = timeSent; + } + + addOutMessage(routedMessage: RoutedMessage) { + this._outMessages = [routedMessage, ...this._outMessages].sort((a, b) => (a.sentTime?.getTime() ?? 0) - (b.sentTime?.getTime() ?? 0)); + } +} diff --git a/src/Frontend/src/resources/SequenceDiagram/RoutedMessage.ts b/src/Frontend/src/resources/SequenceDiagram/RoutedMessage.ts new file mode 100644 index 000000000..04c5a62f1 --- /dev/null +++ b/src/Frontend/src/resources/SequenceDiagram/RoutedMessage.ts @@ -0,0 +1,110 @@ +import EndpointDetails from "../EndpointDetails"; +import { NServiceBusHeaders } from "../Header"; +import Message, { MessageIntent, MessageStatus } from "../Message"; +import { Handler } from "./Handler"; +import { friendlyTypeName } from "./SequenceModel"; + +export interface RoutedMessage { + name: string; + readonly selectedMessage: Message; + fromHandler?: Handler; + toHandler?: Handler; + route?: MessageProcessingRoute; + direction: Direction; + type: RoutedMessageType; + readonly receiving: EndpointDetails; + readonly sending: EndpointDetails; + readonly sentTime: Date | undefined; + readonly messageId: string; + readonly status: MessageStatus; +} + +export interface MessageProcessingRoute { + readonly name?: string; + readonly fromRoutedMessage?: RoutedMessage; + readonly processingHandler?: Handler; +} + +export enum Direction { + Left, + Right, +} + +export enum RoutedMessageType { + Event, + Command, + Local, + Timeout, +} + +export function createRoute(routedMessage: RoutedMessage, processingHandler: Handler): MessageProcessingRoute { + return new MessageProcessingRouteItem(routedMessage, processingHandler); +} + +export function createRoutedMessage(message: Message): RoutedMessage { + const routedMessage = new RoutedMessageItem(message); + + if (message.message_intent === MessageIntent.Publish) routedMessage.type = RoutedMessageType.Event; + else { + const isTimeoutString = message.headers.find((h) => h.key === NServiceBusHeaders.IsSagaTimeoutMessage)?.value; + const isTimeout = (isTimeoutString ?? "") === "true"; + if (isTimeout) routedMessage.type = RoutedMessageType.Timeout; + else if (message.receiving_endpoint.host_id === message.sending_endpoint.host_id && message.receiving_endpoint.name === message.sending_endpoint.name) routedMessage.type = RoutedMessageType.Local; + else routedMessage.type = RoutedMessageType.Command; + } + + return routedMessage; +} + +class MessageProcessingRouteItem implements MessageProcessingRoute { + readonly name?: string; + private _fromRoutedMessage?: RoutedMessageItem; + readonly processingHandler?: Handler; + + constructor(routedMessage?: RoutedMessageItem, processingHandler?: Handler) { + this._fromRoutedMessage = routedMessage; + this.processingHandler = processingHandler; + + if (routedMessage && this.processingHandler) { + this.name = `${processingHandler?.name}(${routedMessage.messageId})`; + } + + if (routedMessage) routedMessage.route = this; + if (processingHandler) processingHandler.route = this; + } + + get fromRoutedMessage() { + return this._fromRoutedMessage as RoutedMessage | undefined; + } +} + +class RoutedMessageItem implements RoutedMessage { + readonly selectedMessage: Message; + readonly name: string; + fromHandler?: Handler; + toHandler?: Handler; + route?: MessageProcessingRoute; + direction = Direction.Left; + type = RoutedMessageType.Command; + + constructor(message: Message) { + this.selectedMessage = message; + this.name = friendlyTypeName(message.message_type) ?? ""; + } + + get receiving() { + return this.selectedMessage.receiving_endpoint; + } + get sending() { + return this.selectedMessage.sending_endpoint; + } + get sentTime() { + return this.selectedMessage.time_sent ? new Date(this.selectedMessage.time_sent) : undefined; + } + get messageId() { + return this.selectedMessage.message_id; + } + get status() { + return this.selectedMessage.status; + } +} diff --git a/src/Frontend/src/resources/SequenceDiagram/SequenceModel.ts b/src/Frontend/src/resources/SequenceDiagram/SequenceModel.ts new file mode 100644 index 000000000..db8313fc4 --- /dev/null +++ b/src/Frontend/src/resources/SequenceDiagram/SequenceModel.ts @@ -0,0 +1,142 @@ +import { NServiceBusHeaders } from "../Header"; +import Message from "../Message"; +import { createRoutedMessage, createRoute, MessageProcessingRoute } from "./RoutedMessage"; +import { createProcessingEndpoint, createSendingEndpoint, Endpoint, EndpointRegistry } from "./Endpoint"; +import { ConversationStartHandlerName, createProcessingHandler, createSendingHandler, Handler, HandlerRegistry, updateProcessingHandler } from "./Handler"; + +export interface ConversationModel { + endpoints: Endpoint[]; +} + +//TODO: extract to common area if this continues to be used in AuditList +export function friendlyTypeName(messageType: string) { + if (messageType == null) return undefined; + + const typeClass = messageType.split(",")[0]; + const typeName = typeClass.split(".").reverse()[0]; + return typeName.replace(/\+/g, "."); +} + +export class ModelCreator implements ConversationModel { + #endpoints: Endpoint[]; + #handlers: Handler[]; + #processingRoutes: MessageProcessingRoute[]; + + constructor(messages: Message[]) { + this.#endpoints = []; + this.#processingRoutes = []; + + const endpointRegistry = new EndpointRegistry(); + const handlerRegistry = new HandlerRegistry(); + const firstOrderHandlers: Handler[] = []; + const messagesInOrder = MessageTreeNode.createTree(messages).flatMap((node) => node.walk()); + + // NOTE: All sending endpoints are created first to ensure version info is retained + for (const message of messagesInOrder) { + endpointRegistry.register(createSendingEndpoint(message)); + } + for (const message of messagesInOrder) { + endpointRegistry.register(createProcessingEndpoint(message)); + } + + for (const message of messagesInOrder) { + const sendingEndpoint = endpointRegistry.get(createSendingEndpoint(message)); + if (!this.#endpoints.find((endpoint) => endpoint.name === sendingEndpoint?.name)) { + this.#endpoints.push(sendingEndpoint); + } + const processingEndpoint = endpointRegistry.get(createProcessingEndpoint(message)); + if (!this.#endpoints.find((endpoint) => endpoint.name === processingEndpoint?.name)) { + this.#endpoints.push(processingEndpoint); + } + + const { handler: sendingHandler, isNew: sendingHandlerIsNew } = handlerRegistry.register(createSendingHandler(message, sendingEndpoint)); + if (sendingHandlerIsNew) { + firstOrderHandlers.push(sendingHandler); + sendingEndpoint.addHandler(sendingHandler); + } + sendingHandler.updateProcessedAt(new Date(message.time_sent)); + + const { handler: processingHandler, isNew: processingHandlerIsNew } = handlerRegistry.register(createProcessingHandler(message, processingEndpoint)); + if (processingHandlerIsNew) { + firstOrderHandlers.push(processingHandler); + processingEndpoint.addHandler(processingHandler); + } else { + updateProcessingHandler(processingHandler, message); + } + + const routedMessage = createRoutedMessage(message); + routedMessage.toHandler = processingHandler; + routedMessage.fromHandler = sendingHandler; + this.#processingRoutes.push(createRoute(routedMessage, processingHandler)); + processingHandler.inMessage = routedMessage; + sendingHandler.addOutMessage(routedMessage); + } + + const start = firstOrderHandlers.find((h) => h.id === ConversationStartHandlerName); + const orderByHandledAt = firstOrderHandlers.filter((h) => h.id !== ConversationStartHandlerName).sort((a, b) => (a.handledAt?.getTime() ?? 0) - (b.handledAt?.getTime() ?? 0)); + + this.#handlers = [start!, ...orderByHandledAt]; + } + + get endpoints(): Endpoint[] { + return [...this.#endpoints]; + } + + get handlers(): Handler[] { + return [...this.#handlers]; + } + + get routes(): MessageProcessingRoute[] { + return [...this.#processingRoutes]; + } +} + +class MessageTreeNode { + #message: Message; + #parent?: string; + #children: MessageTreeNode[]; + + static createTree(messages: Message[]) { + const nodes = messages.map((message) => new MessageTreeNode(message)); + const resolved: MessageTreeNode[] = []; + const index = new Map(nodes.map((node) => [node.id, node])); + + for (const node of nodes) { + const parent = index.get(node.parent ?? ""); + if (parent) { + parent.addChild(node); + resolved.push(node); + } + } + + return nodes.filter((node) => !resolved.includes(node)); + } + + constructor(message: Message) { + this.#message = message; + this.#parent = message.headers.find((h) => h.key === NServiceBusHeaders.RelatedTo)?.value; + this.#children = []; + } + + get id() { + return this.#message.message_id; + } + get parent() { + return this.#parent; + } + get message() { + return this.#message; + } + get children() { + return [...this.#children]; + } + + addChild(childNode: MessageTreeNode) { + this.#children.push(childNode); + } + + walk(): Message[] { + //TODO: check performance of this. We may need to pre-calculate the processed_at as a date on the message object + return [this.#message, ...this.children.sort((a, b) => new Date(a.message.processed_at).getTime() - new Date(b.message.processed_at).getTime()).flatMap((child) => child.walk())]; + } +} diff --git a/src/Frontend/src/stores/SequenceDiagramStore.ts b/src/Frontend/src/stores/SequenceDiagramStore.ts new file mode 100644 index 000000000..e8b152d70 --- /dev/null +++ b/src/Frontend/src/stores/SequenceDiagramStore.ts @@ -0,0 +1,95 @@ +import { useFetchFromServiceControl } from "@/composables/serviceServiceControlUrls"; +import { acceptHMRUpdate, defineStore } from "pinia"; +import { ref, watch } from "vue"; +import { ModelCreator } from "@/resources/SequenceDiagram/SequenceModel"; +import Message from "@/resources/Message"; +import { Endpoint } from "@/resources/SequenceDiagram/Endpoint"; +import { Handler } from "@/resources/SequenceDiagram/Handler"; +import { MessageProcessingRoute } from "@/resources/SequenceDiagram/RoutedMessage"; + +export interface EndpointCentrePoint { + name: string; + centre: number; + top: number; +} + +export interface HandlerLocation { + id: string; + left: number; + right: number; + y: number; + height: number; +} + +export const useSequenceDiagramStore = defineStore("SequenceDiagramStore", () => { + const conversationId = ref(); + + const endpoints = ref([]); + const handlers = ref([]); + const routes = ref([]); + const endpointCentrePoints = ref([]); + const maxWidth = ref(150); + const maxHeight = ref(150); + const handlerLocations = ref([]); + const highlightId = ref(); + + watch(conversationId, async () => { + if (!conversationId.value) return; + const response = await useFetchFromServiceControl(`conversations/${conversationId.value}`); + if (response.status === 404) { + return; + } + + const model = new ModelCreator((await response.json()) as Message[]); + endpoints.value = model.endpoints; + handlers.value = model.handlers; + routes.value = model.routes; + }); + + function setConversationId(id: string) { + conversationId.value = id; + } + + function setMaxWidth(width: number) { + maxWidth.value = width; + } + + function setMaxHeight(height: number) { + maxHeight.value = height; + } + + function setEndpointCentrePoints(centrePoints: EndpointCentrePoint[]) { + endpointCentrePoints.value = centrePoints; + } + + function setHandlerLocations(locations: HandlerLocation[]) { + handlerLocations.value = locations; + } + + function setHighlightId(id?: string) { + highlightId.value = id; + } + + return { + setConversationId, + endpoints, + handlers, + routes, + endpointCentrePoints, + maxWidth, + maxHeight, + handlerLocations, + highlightId, + setMaxWidth, + setMaxHeight, + setEndpointCentrePoints, + setHandlerLocations, + setHighlightId, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useSequenceDiagramStore, import.meta.hot)); +} + +export type SequenceDiagramStore = ReturnType;