@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +234 -134
- package/README.md +5 -5
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +160 -77
- package/dist/analytics/log-build.d.cts +2 -2
- package/dist/analytics/log-build.d.ts +2 -2
- package/dist/analytics/log-build.js +7 -5
- package/dist/analytics/studio-usage.cjs +90 -0
- package/dist/analytics/studio-usage.d.cts +5 -0
- package/dist/analytics/studio-usage.d.ts +5 -0
- package/dist/analytics/studio-usage.js +10 -0
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-BH2F5WPN.js → chunk-2UCCTEIH.js} +1 -1
- package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
- package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
- package/dist/{chunk-NDJOAITI.js → chunk-BNSLGNQI.js} +9 -3
- package/dist/chunk-CA6WCQ2T.js +54 -0
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
- package/dist/{chunk-423S2KHN.js → chunk-OUEGYMBA.js} +4 -2
- package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-GYIVJXOJ.js → chunk-WADZVNCA.js} +1 -1
- package/dist/{chunk-6OC46WQJ.js → chunk-XRFL642Z.js} +66 -35
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +34 -41
- package/dist/eventcatalog-config-file-utils.d.cts +56 -58
- package/dist/eventcatalog-config-file-utils.d.ts +56 -58
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +531 -356
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +63 -25
- package/dist/features.cjs +34 -41
- package/dist/features.js +2 -2
- package/dist/federation/federate.cjs +37 -44
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +37 -42
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +34 -41
- package/dist/resolve-catalog-dependencies.js +2 -2
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +13 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/child-process-signals.cjs +53 -0
- package/dist/utils/child-process-signals.d.cts +19 -0
- package/dist/utils/child-process-signals.d.ts +19 -0
- package/dist/utils/child-process-signals.js +6 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +78 -45
- package/dist/utils/license-status.d.cts +15 -5
- package/dist/utils/license-status.d.ts +15 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +23 -1
- package/eventcatalog/integrations/config-loader.mjs +56 -58
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
- package/eventcatalog/src/components/Search/search-utils.ts +26 -0
- package/eventcatalog/src/components/Seo.astro +5 -1
- package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
- package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
- package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
- package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
- package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
- package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
- package/eventcatalog/src/components/Settings/Row.tsx +7 -4
- package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- package/eventcatalog/src/components/TrialBadge.astro +20 -9
- package/eventcatalog/src/content.config.ts +19 -2
- package/eventcatalog/src/features/auth/error.astro +4 -2
- package/eventcatalog/src/features/auth/login.astro +2 -1
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
- package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +40 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +173 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
- package/eventcatalog/src/features/studio/identity.ts +59 -0
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
- package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
- package/eventcatalog/src/pages/_index.astro +5 -342
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
- package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
- package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/index.astro +4 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
- package/eventcatalog/src/pages/settings/license.astro +9 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
- package/eventcatalog/src/utils/collections/util.ts +8 -4
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +22 -5
- package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
- package/eventcatalog/src/utils/license.ts +1 -1
- package/eventcatalog/src/utils/link-validation.ts +25 -19
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
- package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
- package/eventcatalog/src/utils/page-title.ts +15 -0
- package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
- package/eventcatalog/src/utils/trial.ts +14 -1
- package/package.json +19 -13
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import { Server } from '@hocuspocus/server';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness';
|
|
5
|
+
import type { XYPosition } from '@xyflow/react';
|
|
6
|
+
import type { AgentPresence } from '../agent-choreography';
|
|
7
|
+
import {
|
|
8
|
+
addCanvasContent,
|
|
9
|
+
getCanvasMaps,
|
|
10
|
+
readMeta,
|
|
11
|
+
setMeta,
|
|
12
|
+
getCanvasStatus,
|
|
13
|
+
type CanvasContent,
|
|
14
|
+
type CanvasStatus,
|
|
15
|
+
} from '@eventcatalog/visualiser/studio';
|
|
16
|
+
import { CANVAS_DELETED_REASON, canvasDocumentName, canvasIdFromDocumentName } from '../document-name';
|
|
17
|
+
import { getCanvasPreview, type CanvasPreview } from '../canvas-preview';
|
|
18
|
+
import { MAX_TITLE_LENGTH } from '../limits';
|
|
19
|
+
import { fromBase64, toBase64, type SyncRequest, type SyncResponse } from '../tool-sync';
|
|
20
|
+
import config from '../../../utils/eventcatalog-config/source';
|
|
21
|
+
import { isAuthEnabled } from '../../../utils/feature';
|
|
22
|
+
import { createStorage, memoryStorage, type CanvasStorage } from './storage';
|
|
23
|
+
import type { StorageConfig } from '../../storage/database';
|
|
24
|
+
import { isDatabaseTooNew } from '../../storage/migrations';
|
|
25
|
+
|
|
26
|
+
export const STUDIO_SOCKET_PATH = '/_eventcatalog/studio';
|
|
27
|
+
const SOCKET_PATH_PATTERN = /\/_eventcatalog\/studio\/?$/;
|
|
28
|
+
// Agents show as present while they work, and leave a while after their last change (before awareness's own
|
|
29
|
+
// 30 second timeout would drop them anyway)
|
|
30
|
+
const AGENT_PRESENCE_TTL_MS = 25_000;
|
|
31
|
+
// Application close codes are 4000-4999
|
|
32
|
+
const CANVAS_DELETED_CLOSE_CODE = 4410;
|
|
33
|
+
|
|
34
|
+
import type { AgentIdentity } from '../agents';
|
|
35
|
+
export type { AgentIdentity };
|
|
36
|
+
export type CanvasSummary = {
|
|
37
|
+
canvasId: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
status: CanvasStatus;
|
|
40
|
+
createdAt?: number;
|
|
41
|
+
/** Who started it (a person, or an agent) */
|
|
42
|
+
createdBy?: string;
|
|
43
|
+
/** When anyone (a person, or an agent) last changed it */
|
|
44
|
+
updatedAt?: number;
|
|
45
|
+
nodeCount: number;
|
|
46
|
+
edgeCount: number;
|
|
47
|
+
openComments: number;
|
|
48
|
+
people: string[];
|
|
49
|
+
/** The canvas drawn small (asked for with `withPreview`, e.g. for the canvases page) */
|
|
50
|
+
preview?: CanvasPreview | null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What the collaboration server keeps for the life of the process: one Hocuspocus instance, and every canvas's
|
|
55
|
+
* latest snapshot in memory (read from storage when the server starts, and written through to it each time a
|
|
56
|
+
* canvas is stored), so listing canvases and opening one never wait on storage.
|
|
57
|
+
*/
|
|
58
|
+
type RuntimeState = {
|
|
59
|
+
server: Server;
|
|
60
|
+
snapshots: Map<string, Uint8Array>;
|
|
61
|
+
/** When each canvas last changed (kept with it in storage) */
|
|
62
|
+
updatedAt: Map<string, number>;
|
|
63
|
+
/** Canvases deleted (by document name): refused from then on, so a browser still holding one can't bring it back */
|
|
64
|
+
deleted: Set<string>;
|
|
65
|
+
agents: Map<string, { awareness: Awareness; expires: ReturnType<typeof setTimeout> }>;
|
|
66
|
+
attached: WeakSet<HttpServer>;
|
|
67
|
+
storage: CanvasStorage;
|
|
68
|
+
/** The storage config in use (to tell when it changes) */
|
|
69
|
+
storageKey?: string;
|
|
70
|
+
/** Settles once the stored canvases are in `snapshots` */
|
|
71
|
+
ready: Promise<void>;
|
|
72
|
+
/** Studio started (by startStudio): its storage loaded */
|
|
73
|
+
started?: Promise<void>;
|
|
74
|
+
savesOnExit: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether a collaboration connection comes from someone signed in, when sign-in is on. Given by the pages
|
|
77
|
+
* (see startStudio): Auth.js's config only loads in the app, not in the dev server's integration.
|
|
78
|
+
*/
|
|
79
|
+
isSignedIn?: (request: Request) => Promise<boolean>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const createState = (): RuntimeState => {
|
|
83
|
+
const state: RuntimeState = {
|
|
84
|
+
snapshots: new Map(),
|
|
85
|
+
updatedAt: new Map(),
|
|
86
|
+
deleted: new Set(),
|
|
87
|
+
agents: new Map(),
|
|
88
|
+
attached: new WeakSet(),
|
|
89
|
+
storage: memoryStorage(),
|
|
90
|
+
ready: Promise.resolve(),
|
|
91
|
+
savesOnExit: false,
|
|
92
|
+
server: new Server({
|
|
93
|
+
quiet: true,
|
|
94
|
+
// With sign-in on, only signed-in people can open a canvas (refused until the pages have said how to check)
|
|
95
|
+
onConnect: async ({ request, documentName }) => {
|
|
96
|
+
if (isAuthEnabled() && !(await state.isSignedIn?.(request).catch(() => false))) {
|
|
97
|
+
throw new Error('Sign in to open this canvas');
|
|
98
|
+
}
|
|
99
|
+
if (state.deleted.has(documentName)) {
|
|
100
|
+
throw Object.assign(new Error('This canvas was deleted'), { reason: CANVAS_DELETED_REASON });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
onLoadDocument: async ({ documentName, document }) => {
|
|
104
|
+
// A hook that throws leaves the document half loaded in Hocuspocus (ueberdosis/hocuspocus#1156)
|
|
105
|
+
try {
|
|
106
|
+
await state.ready;
|
|
107
|
+
const snapshot = state.snapshots.get(documentName);
|
|
108
|
+
if (snapshot) Y.applyUpdate(document, snapshot);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`[studio] Could not load canvas ${documentName}, starting it empty`, error);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
// Changes only: Hocuspocus starts listening once a canvas has loaded
|
|
114
|
+
onChange: async ({ documentName }) => {
|
|
115
|
+
state.updatedAt.set(documentName, Date.now());
|
|
116
|
+
},
|
|
117
|
+
onStoreDocument: async ({ documentName, document }) => saveSnapshot(state, documentName, document),
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
return state;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A canvas just started: its title, and when and by whom (a person, or an agent) */
|
|
124
|
+
const setStarted = (doc: Y.Doc, { title, createdBy }: { title?: string; createdBy?: string }) =>
|
|
125
|
+
setMeta(doc, { ...(title && { title }), createdAt: Date.now(), ...(createdBy && { createdBy }) });
|
|
126
|
+
|
|
127
|
+
/** Nothing on it, no title, and nobody on it */
|
|
128
|
+
const isBlank = (canvas: CanvasSummary) =>
|
|
129
|
+
canvas.nodeCount === 0 && canvas.openComments === 0 && !canvas.title && canvas.people.length === 0;
|
|
130
|
+
|
|
131
|
+
/** Keeps a canvas's snapshot, and writes it to storage (failing to store it doesn't lose it from memory) */
|
|
132
|
+
const saveSnapshot = (state: RuntimeState, documentName: string, document: Y.Doc) => {
|
|
133
|
+
// Deleted while it was still open (e.g. waiting to be stored)
|
|
134
|
+
if (state.deleted.has(documentName)) return;
|
|
135
|
+
const snapshot = Y.encodeStateAsUpdate(document);
|
|
136
|
+
state.snapshots.set(documentName, snapshot);
|
|
137
|
+
try {
|
|
138
|
+
state.storage.save(documentName, snapshot, state.updatedAt.get(documentName));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`[studio] Could not store canvas ${documentName} (${state.storage.type} storage)`, error);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The collaboration server, shared (through globalThis) by the WebSocket upgrade handler and server code such
|
|
146
|
+
* as MCP tools, which edit canvases as agents. Only the state is shared, so code changes apply without a restart.
|
|
147
|
+
*/
|
|
148
|
+
export class StudioRuntime {
|
|
149
|
+
constructor(private readonly state: RuntimeState) {}
|
|
150
|
+
|
|
151
|
+
get server() {
|
|
152
|
+
return this.state.server;
|
|
153
|
+
}
|
|
154
|
+
private get snapshots() {
|
|
155
|
+
return this.state.snapshots;
|
|
156
|
+
}
|
|
157
|
+
private get agents() {
|
|
158
|
+
return this.state.agents;
|
|
159
|
+
}
|
|
160
|
+
private get attached() {
|
|
161
|
+
return this.state.attached;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Where canvases are kept (memory: until the server restarts) */
|
|
165
|
+
get storage(): Pick<CanvasStorage, 'type' | 'location'> {
|
|
166
|
+
const { type, location } = this.state.storage;
|
|
167
|
+
return { type, location };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Keeps canvases in the storage configured (the catalog's `storage`, memory by default), and loads the ones
|
|
172
|
+
* stored there. Set once when the server starts; calling it again with the same config does nothing.
|
|
173
|
+
*/
|
|
174
|
+
async useStorage(config: StorageConfig | undefined, projectDirectory: string) {
|
|
175
|
+
const key = JSON.stringify({ config: config ?? null, projectDirectory });
|
|
176
|
+
if (this.state.storageKey === key) return this.state.ready;
|
|
177
|
+
const storage = createStorage(config, projectDirectory);
|
|
178
|
+
this.state.storageKey = key;
|
|
179
|
+
this.state.storage = storage;
|
|
180
|
+
this.state.ready = storage.loadAll().then(({ canvases, deleted }) => {
|
|
181
|
+
deleted.forEach((name) => this.state.deleted.add(name));
|
|
182
|
+
// Canvases already open here (e.g. the dev server reloading) are kept as they are
|
|
183
|
+
canvases.forEach(({ state, updatedAt }, name) => {
|
|
184
|
+
if (this.snapshots.has(name) || this.state.deleted.has(name)) return;
|
|
185
|
+
this.snapshots.set(name, state);
|
|
186
|
+
if (updatedAt) this.state.updatedAt.set(name, updatedAt);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
this.saveOnExit();
|
|
190
|
+
return this.state.ready;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Starts Studio once: canvases kept in the storage configured (in memory if it can't be used), and in production the
|
|
195
|
+
* collaboration connection on the server `eventcatalog start` runs (the dev server's integration attaches it there).
|
|
196
|
+
*/
|
|
197
|
+
start() {
|
|
198
|
+
this.state.started ??= (async () => {
|
|
199
|
+
const projectDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
200
|
+
try {
|
|
201
|
+
await this.useStorage(config.storage, projectDirectory);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Not kept in memory instead: everyone's changes would quietly stop being saved
|
|
204
|
+
if (isDatabaseTooNew(error)) {
|
|
205
|
+
console.error(`[studio] Studio isn't available: ${(error as Error).message}`);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
console.error(
|
|
209
|
+
`[studio] Could not use the Studio storage configured, keeping canvases in memory: ${(error as Error).message}`
|
|
210
|
+
);
|
|
211
|
+
await this.useStorage({ type: 'memory' }, projectDirectory);
|
|
212
|
+
}
|
|
213
|
+
const httpServer = (globalThis as Record<symbol, unknown>)[HTTP_SERVER_KEY] as HttpServer | undefined;
|
|
214
|
+
if (httpServer && !this.attached.has(httpServer)) {
|
|
215
|
+
this.attach(httpServer);
|
|
216
|
+
const { type, location } = this.storage;
|
|
217
|
+
console.log(
|
|
218
|
+
type === 'memory'
|
|
219
|
+
? `[studio] Collaboration server ready at ${STUDIO_SOCKET_PATH}. Canvases are kept in memory and lost when the server restarts or redeploys: set storage in eventcatalog.config.js to keep them.`
|
|
220
|
+
: `[studio] Collaboration server ready at ${STUDIO_SOCKET_PATH} (canvases stored in ${location})`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
return this.state.started;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Canvases change a moment before they're stored (Hocuspocus waits for edits to settle): as the process exits,
|
|
229
|
+
* the open ones are stored straight away so the last edits aren't lost. Storage writes are synchronous for this.
|
|
230
|
+
*/
|
|
231
|
+
private saveOnExit() {
|
|
232
|
+
if (this.state.savesOnExit) return;
|
|
233
|
+
this.state.savesOnExit = true;
|
|
234
|
+
const saveOpenCanvases = () =>
|
|
235
|
+
this.server.hocuspocus.documents.forEach((document, name) => {
|
|
236
|
+
saveSnapshot(this.state, name, document);
|
|
237
|
+
});
|
|
238
|
+
process.once('exit', saveOpenCanvases);
|
|
239
|
+
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
240
|
+
process.once(signal, () => {
|
|
241
|
+
saveOpenCanvases();
|
|
242
|
+
// Stopping as it would have without this (unless something else handles the signal)
|
|
243
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** How to tell whether a collaboration connection comes from someone signed in (checked when sign-in is on) */
|
|
249
|
+
useSignIn(isSignedIn: (request: Request) => Promise<boolean>) {
|
|
250
|
+
this.state.isSignedIn = isSignedIn;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Handle collaboration WebSocket upgrades on an HTTP server (the dev server, or the production server) */
|
|
254
|
+
attach(httpServer: HttpServer) {
|
|
255
|
+
if (this.attached.has(httpServer)) return;
|
|
256
|
+
this.attached.add(httpServer);
|
|
257
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
258
|
+
const { pathname } = new URL(request.url ?? '/', 'http://localhost');
|
|
259
|
+
if (SOCKET_PATH_PATTERN.test(pathname)) this.server.httpServer.emit('upgrade', request, socket, head);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
exists(canvasId: string) {
|
|
264
|
+
const name = canvasDocumentName(canvasId);
|
|
265
|
+
return !this.state.deleted.has(name) && (this.server.hocuspocus.documents.has(name) || this.snapshots.has(name));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
isDeleted(canvasId: string) {
|
|
269
|
+
return this.state.deleted.has(canvasDocumentName(canvasId));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Deletes a canvas for everyone: from storage, and from the browsers that have it open (they're disconnected, told
|
|
274
|
+
* why, and refused from then on, so their copy can't bring it back). Returns false if there's no such canvas.
|
|
275
|
+
* Throws if storage can't delete it, leaving it as it was.
|
|
276
|
+
*/
|
|
277
|
+
deleteCanvas(canvasId: string) {
|
|
278
|
+
if (!this.exists(canvasId)) return false;
|
|
279
|
+
const name = canvasDocumentName(canvasId);
|
|
280
|
+
this.state.storage.remove(name);
|
|
281
|
+
this.state.deleted.add(name);
|
|
282
|
+
this.snapshots.delete(name);
|
|
283
|
+
this.state.updatedAt.delete(name);
|
|
284
|
+
this.agents.forEach(({ awareness, expires }, key) => {
|
|
285
|
+
if (!key.startsWith(`${name}::`)) return;
|
|
286
|
+
clearTimeout(expires);
|
|
287
|
+
awareness.destroy();
|
|
288
|
+
this.agents.delete(key);
|
|
289
|
+
});
|
|
290
|
+
const { hocuspocus } = this.server;
|
|
291
|
+
const document = hocuspocus.documents.get(name);
|
|
292
|
+
if (document) {
|
|
293
|
+
// Told why, so they can say so (Hocuspocus's closeConnections only says it reset them)
|
|
294
|
+
document.connections.forEach((_clients, connection) =>
|
|
295
|
+
connection.close({ code: CANVAS_DELETED_CLOSE_CODE, reason: CANVAS_DELETED_REASON })
|
|
296
|
+
);
|
|
297
|
+
// Unloaded now, or once it's done storing (which skips deleted canvases)
|
|
298
|
+
void hocuspocus.unloadDocument(document);
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Starts a canvas with a title, recording when and by whom (a person, or an agent), and what's on it to begin
|
|
305
|
+
* with, if anything. Returns its id.
|
|
306
|
+
*/
|
|
307
|
+
async createCanvas(started: { title?: string; createdBy?: string }, content?: CanvasContent) {
|
|
308
|
+
const canvasId = crypto.randomUUID();
|
|
309
|
+
await this.withCanvas(canvasId, (doc) => {
|
|
310
|
+
setStarted(doc, started);
|
|
311
|
+
if (content) addCanvasContent(doc, content);
|
|
312
|
+
});
|
|
313
|
+
return canvasId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Makes a copy of a canvas's design (its nodes and connections, not its comments or status) as a new draft, called
|
|
318
|
+
* "Copy of" the original unless given a title. Returns the copy's id, or undefined if there's no such canvas.
|
|
319
|
+
*/
|
|
320
|
+
async copyCanvas(canvasId: string, { title, createdBy }: { title?: string; createdBy?: string } = {}) {
|
|
321
|
+
if (!this.exists(canvasId)) return undefined;
|
|
322
|
+
const original = await this.withCanvas(canvasId, (doc) => {
|
|
323
|
+
const { nodes, edges } = getCanvasMaps(doc);
|
|
324
|
+
return { title: readMeta(doc).title, nodes: [...nodes.entries()], edges: [...edges.entries()] };
|
|
325
|
+
});
|
|
326
|
+
const copyId = crypto.randomUUID();
|
|
327
|
+
await this.withCanvas(copyId, (doc) => {
|
|
328
|
+
const { nodes, edges } = getCanvasMaps(doc);
|
|
329
|
+
setStarted(doc, { title: title ?? `Copy of ${original.title ?? 'Untitled canvas'}`.slice(0, MAX_TITLE_LENGTH), createdBy });
|
|
330
|
+
original.nodes.forEach(([id, node]) => nodes.set(id, structuredClone(node)));
|
|
331
|
+
original.edges.forEach(([id, edge]) => edges.set(id, structuredClone(edge)));
|
|
332
|
+
});
|
|
333
|
+
return copyId;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Read or change a canvas as the server. Changes sync to everyone on it, like anyone else's. */
|
|
337
|
+
async withCanvas<T>(canvasId: string, fn: (doc: Y.Doc) => T): Promise<T> {
|
|
338
|
+
const connection = await this.server.hocuspocus.openDirectConnection(canvasDocumentName(canvasId));
|
|
339
|
+
let result!: T;
|
|
340
|
+
try {
|
|
341
|
+
await connection.transact((doc) => {
|
|
342
|
+
result = fn(doc);
|
|
343
|
+
});
|
|
344
|
+
} finally {
|
|
345
|
+
await connection.disconnect({ unloadImmediately: false });
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Sync a view that can't use the WebSocket (see tool-sync): apply its changes and presence (everyone on the
|
|
352
|
+
* WebSocket gets them live), and send back what it's missing and everyone else's presence.
|
|
353
|
+
*/
|
|
354
|
+
async syncCanvas({ canvasId, clientId, stateVector, update, awareness }: SyncRequest): Promise<SyncResponse> {
|
|
355
|
+
const name = canvasDocumentName(canvasId);
|
|
356
|
+
const connection = await this.server.hocuspocus.openDirectConnection(name);
|
|
357
|
+
let response!: SyncResponse;
|
|
358
|
+
try {
|
|
359
|
+
await connection.transact((doc) => {
|
|
360
|
+
Y.applyUpdate(doc, fromBase64(update));
|
|
361
|
+
response = {
|
|
362
|
+
update: toBase64(Y.encodeStateAsUpdate(doc, fromBase64(stateVector))),
|
|
363
|
+
stateVector: toBase64(Y.encodeStateVector(doc)),
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
const document = this.server.hocuspocus.documents.get(name);
|
|
367
|
+
if (document) {
|
|
368
|
+
applyAwarenessUpdate(document.awareness, fromBase64(awareness), 'tool-sync');
|
|
369
|
+
const others = [...document.awareness.getStates().keys()].filter((id) => id !== clientId);
|
|
370
|
+
if (others.length) response.awareness = toBase64(encodeAwarenessUpdate(document.awareness, others));
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
await connection.disconnect({ unloadImmediately: false });
|
|
374
|
+
}
|
|
375
|
+
return response;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The canvases, the most recently changed first. Ones started and left empty (e.g. a made-up link opened, then left)
|
|
380
|
+
* aren't listed: they can still be opened by their link.
|
|
381
|
+
*/
|
|
382
|
+
listCanvases({ withPreview = false }: { withPreview?: boolean } = {}): CanvasSummary[] {
|
|
383
|
+
const names = new Set([...this.server.hocuspocus.documents.keys(), ...this.snapshots.keys()]);
|
|
384
|
+
return [...names]
|
|
385
|
+
.flatMap((name) => {
|
|
386
|
+
const canvasId = canvasIdFromDocumentName(name);
|
|
387
|
+
return canvasId && this.exists(canvasId) ? [this.summarize(canvasId, { withPreview })] : [];
|
|
388
|
+
})
|
|
389
|
+
.filter((canvas) => !isBlank(canvas))
|
|
390
|
+
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** One canvas, as listed (undefined if there's no such canvas) */
|
|
394
|
+
getCanvas(canvasId: string): CanvasSummary | undefined {
|
|
395
|
+
return this.exists(canvasId) ? this.summarize(canvasId) : undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private summarize(canvasId: string, { withPreview = false }: { withPreview?: boolean } = {}): CanvasSummary {
|
|
399
|
+
const name = canvasDocumentName(canvasId);
|
|
400
|
+
const live = this.server.hocuspocus.documents.get(name);
|
|
401
|
+
const doc = live ?? new Y.Doc();
|
|
402
|
+
if (!live) Y.applyUpdate(doc, this.snapshots.get(name)!);
|
|
403
|
+
const { nodes, edges, threads } = getCanvasMaps(doc);
|
|
404
|
+
const meta = readMeta(doc);
|
|
405
|
+
return {
|
|
406
|
+
canvasId,
|
|
407
|
+
title: meta.title,
|
|
408
|
+
status: getCanvasStatus(meta),
|
|
409
|
+
createdAt: meta.createdAt,
|
|
410
|
+
...(meta.createdBy && { createdBy: meta.createdBy }),
|
|
411
|
+
updatedAt: this.state.updatedAt.get(name) ?? meta.createdAt,
|
|
412
|
+
nodeCount: nodes.size,
|
|
413
|
+
edgeCount: edges.size,
|
|
414
|
+
openComments: Array.from(threads.values()).filter((thread) => !thread.get('resolved')).length,
|
|
415
|
+
people: live ? this.peopleOn(canvasId) : [],
|
|
416
|
+
...(withPreview && { preview: getCanvasPreview(Array.from(nodes.values()), Array.from(edges.values())) }),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Names of the people and agents on a canvas right now */
|
|
421
|
+
peopleOn(canvasId: string): string[] {
|
|
422
|
+
const document = this.server.hocuspocus.documents.get(canvasDocumentName(canvasId));
|
|
423
|
+
if (!document) return [];
|
|
424
|
+
return Array.from(document.awareness.getStates().values())
|
|
425
|
+
.map((state) => state.user?.name)
|
|
426
|
+
.filter((name): name is string => !!name);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Where an agent's pointer was left on a canvas, so its next action carries on from there */
|
|
430
|
+
getAgentPointer(canvasId: string, agent: AgentIdentity): XYPosition | null {
|
|
431
|
+
const state = this.agents.get(`${canvasDocumentName(canvasId)}::${agent.name}`)?.awareness.getLocalState();
|
|
432
|
+
return (state?.pointer as XYPosition | undefined) ?? null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Show an agent on a canvas like a person: in the list of who's here, with a pointer and selection.
|
|
437
|
+
* Each agent gets its own awareness client, kept while it works and removed for everyone after.
|
|
438
|
+
*/
|
|
439
|
+
setAgentPresence(canvasId: string, agent: AgentIdentity, presence: AgentPresence) {
|
|
440
|
+
const name = canvasDocumentName(canvasId);
|
|
441
|
+
const document = this.server.hocuspocus.documents.get(name);
|
|
442
|
+
if (!document) return;
|
|
443
|
+
|
|
444
|
+
const key = `${name}::${agent.name}`;
|
|
445
|
+
const existing = this.agents.get(key);
|
|
446
|
+
if (existing) clearTimeout(existing.expires);
|
|
447
|
+
const awareness = existing?.awareness ?? new Awareness(new Y.Doc());
|
|
448
|
+
awareness.setLocalState({ user: { ...agent, agent: true }, ...presence });
|
|
449
|
+
applyAwarenessUpdate(document.awareness, encodeAwarenessUpdate(awareness, [awareness.clientID]), 'agent');
|
|
450
|
+
|
|
451
|
+
const expires = setTimeout(() => {
|
|
452
|
+
const current = this.server.hocuspocus.documents.get(name);
|
|
453
|
+
if (current) removeAwarenessStates(current.awareness, [awareness.clientID], 'agent');
|
|
454
|
+
awareness.destroy();
|
|
455
|
+
this.agents.delete(key);
|
|
456
|
+
}, AGENT_PRESENCE_TTL_MS);
|
|
457
|
+
this.agents.set(key, { awareness, expires });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const RUNTIME_KEY = Symbol.for('eventcatalog.studio.runtime');
|
|
462
|
+
const store = globalThis as typeof globalThis & { [RUNTIME_KEY]?: RuntimeState };
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* State kept from before a code change (the dev server reloads code but keeps globalThis) gets what's been added to it
|
|
466
|
+
* since. Hooks given to Hocuspocus when it was created (e.g. onChange, onConnect) only change on a restart.
|
|
467
|
+
*/
|
|
468
|
+
const upgrade = (state: RuntimeState) => {
|
|
469
|
+
state.updatedAt ??= new Map();
|
|
470
|
+
state.deleted ??= new Set();
|
|
471
|
+
return state;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/** The process wide collaboration runtime (created on first use) */
|
|
475
|
+
export const getStudioRuntime = (): StudioRuntime => new StudioRuntime(upgrade((store[RUNTIME_KEY] ??= createState())));
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* In production, `eventcatalog start` leaves the HTTP server it runs here, so Studio's collaboration connection can be
|
|
479
|
+
* served on it (Astro's Node adapter doesn't expose it otherwise)
|
|
480
|
+
*/
|
|
481
|
+
const HTTP_SERVER_KEY = Symbol.for('eventcatalog.http-server');
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Studio, started: what pages and tools use before working with canvases. Pages say how to tell who's signed in,
|
|
485
|
+
* for the collaboration connection.
|
|
486
|
+
*/
|
|
487
|
+
export const startStudio = async ({ isSignedIn }: { isSignedIn?: (request: Request) => Promise<boolean> } = {}) => {
|
|
488
|
+
const runtime = getStudioRuntime();
|
|
489
|
+
if (isSignedIn) runtime.useSignIn(isSignedIn);
|
|
490
|
+
await runtime.start();
|
|
491
|
+
return runtime;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/** The runtime if the collaboration server is running in this process */
|
|
495
|
+
export const findStudioRuntime = (): StudioRuntime | undefined => {
|
|
496
|
+
const state = store[RUNTIME_KEY];
|
|
497
|
+
return state ? new StudioRuntime(upgrade(state)) : undefined;
|
|
498
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getSession } from '@utils/auth-astro/server';
|
|
2
|
+
import type { NormalizedUser } from '@features/auth/middleware/middleware-auth';
|
|
3
|
+
|
|
4
|
+
/** Someone signed in with the catalog's sign-in (SSO): Studio shows them by these, and they can't change them */
|
|
5
|
+
export type SignedInUser = { name: string; picture?: string };
|
|
6
|
+
|
|
7
|
+
/** For the collaboration connection (see startStudio): it has the same cookies as the pages */
|
|
8
|
+
export const isSignedIn = async (request: Request) => Boolean(await getSession(request));
|
|
9
|
+
|
|
10
|
+
/** Who's signed in, from what the sign-in middleware found (nobody when sign-in is off) */
|
|
11
|
+
export const getSignedInUser = (locals: object): SignedInUser | undefined => {
|
|
12
|
+
const user = (locals as { user?: NormalizedUser }).user;
|
|
13
|
+
const name = user?.name || user?.email;
|
|
14
|
+
if (!user || !name) return undefined;
|
|
15
|
+
// Pictures are loaded from the provider (Auth.js calls it `image`), over HTTPS only
|
|
16
|
+
const picture = user.picture ?? user.raw?.user?.image;
|
|
17
|
+
return { name, ...(typeof picture === 'string' && picture.startsWith('https://') && { picture }) };
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- The statements Studio's SQLite storage runs (see storage.ts), each named by the `-- name:` line above it. The tables
|
|
2
|
+
-- are made by the catalog's migrations (features/storage/sql/migrations).
|
|
3
|
+
|
|
4
|
+
-- name: saveCanvas
|
|
5
|
+
INSERT INTO studio_canvases (name, state, updated_at) VALUES (?, ?, ?)
|
|
6
|
+
ON CONFLICT (name) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at;
|
|
7
|
+
|
|
8
|
+
-- name: removeCanvas
|
|
9
|
+
DELETE FROM studio_canvases WHERE name = ?;
|
|
10
|
+
|
|
11
|
+
-- name: recordDeleted
|
|
12
|
+
INSERT OR IGNORE INTO studio_deleted_canvases (name, deleted_at) VALUES (?, ?);
|
|
13
|
+
|
|
14
|
+
-- name: allCanvases
|
|
15
|
+
SELECT name, state, updated_at FROM studio_canvases;
|
|
16
|
+
|
|
17
|
+
-- name: allDeleted
|
|
18
|
+
SELECT name FROM studio_deleted_canvases;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getDatabasePath, openDatabase, type Database, type StorageConfig, type StorageType } from '../../storage/database';
|
|
2
|
+
import { namedStatements } from '../../storage/sql';
|
|
3
|
+
import storageSql from './sql/storage.sql?raw';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where Studio keeps canvases: the catalog's storage (`storage` in eventcatalog.config.js). People and agents always
|
|
7
|
+
* work on the copy the collaboration server holds in memory, so storage is only read when the server starts, and
|
|
8
|
+
* written a moment after a canvas changes: it doesn't slow down editing. Canvases are stored as their whole Yjs
|
|
9
|
+
* document (Y.encodeStateAsUpdate), so nothing is lost between restarts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A canvas as stored: its whole document, and when it last changed */
|
|
13
|
+
export type StoredCanvas = { state: Uint8Array; updatedAt?: number };
|
|
14
|
+
|
|
15
|
+
/** What's stored: every canvas, and the canvases deleted (so a browser still holding one can't bring it back) */
|
|
16
|
+
export type StoredCanvases = { canvases: Map<string, StoredCanvas>; deleted: Set<string> };
|
|
17
|
+
|
|
18
|
+
export type CanvasStorage = {
|
|
19
|
+
type: StorageType;
|
|
20
|
+
/** Where it is, for logs (e.g. the database file) */
|
|
21
|
+
location?: string;
|
|
22
|
+
/** Every stored canvas by document name, and the deleted ones (read once, when the server starts) */
|
|
23
|
+
loadAll: () => Promise<StoredCanvases>;
|
|
24
|
+
/**
|
|
25
|
+
* Stores a canvas's document, with when it last changed (now when not known). Synchronous, so canvases can still
|
|
26
|
+
* be saved as the process exits.
|
|
27
|
+
*/
|
|
28
|
+
save: (documentName: string, state: Uint8Array, updatedAt?: number) => void;
|
|
29
|
+
/** Deletes a canvas, and records that it was deleted */
|
|
30
|
+
remove: (documentName: string) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const memoryStorage = (): CanvasStorage => ({
|
|
34
|
+
type: 'memory',
|
|
35
|
+
loadAll: async () => ({ canvases: new Map(), deleted: new Set() }),
|
|
36
|
+
save: () => {},
|
|
37
|
+
remove: () => {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** The queries Studio runs, from ./sql/storage.sql (its tables are made by the catalog's migrations) */
|
|
41
|
+
const QUERIES = ['saveCanvas', 'removeCanvas', 'recordDeleted', 'allCanvases', 'allDeleted'] as const;
|
|
42
|
+
const SQL = namedStatements(storageSql, QUERIES);
|
|
43
|
+
|
|
44
|
+
type Statements = Record<(typeof QUERIES)[number], ReturnType<Database['prepare']>>;
|
|
45
|
+
|
|
46
|
+
/** Canvases in the catalog's SQLite database, in `file` */
|
|
47
|
+
export const sqliteStorage = (file: string): CanvasStorage => {
|
|
48
|
+
let opened: { database: Database; statements: Statements } | undefined;
|
|
49
|
+
const use = () => {
|
|
50
|
+
if (!opened) throw new Error('SQLite storage was used before it was opened');
|
|
51
|
+
return opened;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
type: 'sqlite',
|
|
55
|
+
location: file,
|
|
56
|
+
loadAll: async () => {
|
|
57
|
+
const database = await openDatabase(file);
|
|
58
|
+
const statements = Object.fromEntries(QUERIES.map((name) => [name, database.prepare(SQL[name])])) as Statements;
|
|
59
|
+
opened = { database, statements };
|
|
60
|
+
|
|
61
|
+
const rows = statements.allCanvases.all() as { name: string; state: Uint8Array; updated_at: number }[];
|
|
62
|
+
const deleted = statements.allDeleted.all() as { name: string }[];
|
|
63
|
+
return {
|
|
64
|
+
canvases: new Map(rows.map((row) => [row.name, { state: new Uint8Array(row.state), updatedAt: Number(row.updated_at) }])),
|
|
65
|
+
deleted: new Set(deleted.map((row) => row.name)),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
save: (documentName, state, updatedAt = Date.now()) => {
|
|
69
|
+
use().statements.saveCanvas.run(documentName, state, updatedAt);
|
|
70
|
+
},
|
|
71
|
+
remove: (documentName) => {
|
|
72
|
+
const { database, statements } = use();
|
|
73
|
+
database.exec('BEGIN');
|
|
74
|
+
try {
|
|
75
|
+
statements.recordDeleted.run(documentName, Date.now());
|
|
76
|
+
statements.removeCanvas.run(documentName);
|
|
77
|
+
database.exec('COMMIT');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
database.exec('ROLLBACK');
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Canvases in the storage a config asks for (paths relative to the catalog). Unknown types are an error. */
|
|
87
|
+
export const createStorage = (config: StorageConfig | undefined, projectDirectory: string): CanvasStorage => {
|
|
88
|
+
const file = getDatabasePath(config, projectDirectory);
|
|
89
|
+
return file ? sqliteStorage(file) : memoryStorage();
|
|
90
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
/** A tool icon (MCP 2025-11-25) */
|
|
4
|
+
export type ToolIcon = { src: string; mimeType?: string; sizes?: string[] };
|
|
5
|
+
type ListTools = (request: unknown, extra: unknown) => Promise<{ tools: { name: string }[] }>;
|
|
6
|
+
|
|
7
|
+
/** The SDK's request handlers (private in SDK 1.x, so checked rather than assumed) */
|
|
8
|
+
const getRequestHandlers = (server: McpServer): Map<string, ListTools> | undefined => {
|
|
9
|
+
const handlers: unknown = Reflect.get(server.server, '_requestHandlers');
|
|
10
|
+
return handlers instanceof Map ? handlers : undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adds icons to tools in tools/list (MCP 2025-11-25 tool icons, which ChatGPT shows on entrypoints). The MCP
|
|
15
|
+
* TypeScript SDK 1.x doesn't list tool icons, so this wraps its tools/list handler. If a new SDK changes how
|
|
16
|
+
* it keeps handlers, tools are listed without icons and this says so.
|
|
17
|
+
*/
|
|
18
|
+
export const addToolIcons = (server: McpServer, icons: Record<string, ToolIcon[]>) => {
|
|
19
|
+
const handlers = getRequestHandlers(server);
|
|
20
|
+
const listTools = handlers?.get('tools/list');
|
|
21
|
+
if (!handlers || !listTools) {
|
|
22
|
+
console.warn('[EventCatalog MCP] Could not add tool icons: the MCP SDK no longer exposes its tools/list handler');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
handlers.set('tools/list', async (request, extra) => {
|
|
26
|
+
const result = await listTools(request, extra);
|
|
27
|
+
return { ...result, tools: result.tools.map((tool) => (icons[tool.name] ? { ...tool, icons: icons[tool.name] } : tool)) };
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
};
|