@myrialabs/clopen 0.0.1
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/.env.example +6 -0
- package/.github/workflows/release.yml +60 -0
- package/.github/workflows/test.yml +40 -0
- package/CONTRIBUTING.md +499 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend/index.ts +156 -0
- package/backend/lib/chat/helpers.ts +42 -0
- package/backend/lib/chat/index.ts +2 -0
- package/backend/lib/chat/stream-manager.ts +1126 -0
- package/backend/lib/database/README.md +77 -0
- package/backend/lib/database/index.ts +119 -0
- package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
- package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
- package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
- package/backend/lib/database/migrations/index.ts +154 -0
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
- package/backend/lib/database/queries/engine-queries.ts +75 -0
- package/backend/lib/database/queries/index.ts +9 -0
- package/backend/lib/database/queries/message-queries.ts +472 -0
- package/backend/lib/database/queries/project-queries.ts +117 -0
- package/backend/lib/database/queries/session-queries.ts +271 -0
- package/backend/lib/database/queries/settings-queries.ts +34 -0
- package/backend/lib/database/queries/snapshot-queries.ts +326 -0
- package/backend/lib/database/queries/utils-queries.ts +59 -0
- package/backend/lib/database/seeders/index.ts +13 -0
- package/backend/lib/database/seeders/settings_seeder.ts +84 -0
- package/backend/lib/database/utils/connection.ts +174 -0
- package/backend/lib/database/utils/index.ts +4 -0
- package/backend/lib/database/utils/migration-runner.ts +118 -0
- package/backend/lib/database/utils/seeder-runner.ts +121 -0
- package/backend/lib/engine/adapters/claude/environment.ts +164 -0
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
- package/backend/lib/engine/adapters/claude/index.ts +1 -0
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
- package/backend/lib/engine/adapters/claude/stream.ts +177 -0
- package/backend/lib/engine/adapters/opencode/index.ts +2 -0
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
- package/backend/lib/engine/adapters/opencode/server.ts +104 -0
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
- package/backend/lib/engine/index.ts +196 -0
- package/backend/lib/engine/types.ts +58 -0
- package/backend/lib/files/file-operations.ts +478 -0
- package/backend/lib/files/file-reading.ts +308 -0
- package/backend/lib/files/file-watcher.ts +383 -0
- package/backend/lib/files/path-browsing.ts +382 -0
- package/backend/lib/git/git-executor.ts +88 -0
- package/backend/lib/git/git-parser.ts +411 -0
- package/backend/lib/git/git-service.ts +505 -0
- package/backend/lib/mcp/README.md +1144 -0
- package/backend/lib/mcp/config.ts +316 -0
- package/backend/lib/mcp/index.ts +35 -0
- package/backend/lib/mcp/project-context.ts +236 -0
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
- package/backend/lib/mcp/servers/helper.ts +143 -0
- package/backend/lib/mcp/servers/index.ts +45 -0
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
- package/backend/lib/mcp/servers/weather/index.ts +31 -0
- package/backend/lib/mcp/stdio-server.ts +103 -0
- package/backend/lib/mcp/types.ts +65 -0
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
- package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
- package/backend/lib/preview/browser/browser-pool.ts +357 -0
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
- package/backend/lib/preview/browser/types.ts +359 -0
- package/backend/lib/preview/index.ts +24 -0
- package/backend/lib/project/index.ts +2 -0
- package/backend/lib/project/status-manager.ts +182 -0
- package/backend/lib/shared/index.ts +2 -0
- package/backend/lib/shared/port-utils.ts +25 -0
- package/backend/lib/shared/process-manager.ts +281 -0
- package/backend/lib/snapshot/blob-store.ts +227 -0
- package/backend/lib/snapshot/gitignore.ts +307 -0
- package/backend/lib/snapshot/helpers.ts +397 -0
- package/backend/lib/snapshot/snapshot-service.ts +483 -0
- package/backend/lib/terminal/helpers.ts +14 -0
- package/backend/lib/terminal/index.ts +8 -0
- package/backend/lib/terminal/pty-manager.ts +4 -0
- package/backend/lib/terminal/pty-session-manager.ts +387 -0
- package/backend/lib/terminal/shell-utils.ts +313 -0
- package/backend/lib/terminal/stream-manager.ts +293 -0
- package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
- package/backend/lib/user/helpers.ts +87 -0
- package/backend/lib/utils/ws.ts +944 -0
- package/backend/lib/vite-dev.ts +353 -0
- package/backend/middleware/cors.ts +15 -0
- package/backend/middleware/error-handler.ts +49 -0
- package/backend/middleware/logger.ts +9 -0
- package/backend/types/api.ts +24 -0
- package/backend/ws/README.md +1505 -0
- package/backend/ws/chat/background.ts +198 -0
- package/backend/ws/chat/index.ts +21 -0
- package/backend/ws/chat/stream.ts +707 -0
- package/backend/ws/engine/claude/accounts.ts +401 -0
- package/backend/ws/engine/claude/index.ts +13 -0
- package/backend/ws/engine/claude/status.ts +43 -0
- package/backend/ws/engine/index.ts +14 -0
- package/backend/ws/engine/opencode/index.ts +11 -0
- package/backend/ws/engine/opencode/status.ts +30 -0
- package/backend/ws/engine/utils.ts +36 -0
- package/backend/ws/files/index.ts +30 -0
- package/backend/ws/files/read.ts +189 -0
- package/backend/ws/files/search.ts +453 -0
- package/backend/ws/files/watch.ts +124 -0
- package/backend/ws/files/write.ts +143 -0
- package/backend/ws/git/branch.ts +106 -0
- package/backend/ws/git/commit.ts +39 -0
- package/backend/ws/git/conflict.ts +68 -0
- package/backend/ws/git/diff.ts +69 -0
- package/backend/ws/git/index.ts +24 -0
- package/backend/ws/git/log.ts +41 -0
- package/backend/ws/git/remote.ts +214 -0
- package/backend/ws/git/staging.ts +84 -0
- package/backend/ws/git/status.ts +90 -0
- package/backend/ws/index.ts +69 -0
- package/backend/ws/mcp/index.ts +61 -0
- package/backend/ws/messages/crud.ts +74 -0
- package/backend/ws/messages/index.ts +14 -0
- package/backend/ws/preview/browser/cleanup.ts +129 -0
- package/backend/ws/preview/browser/console.ts +114 -0
- package/backend/ws/preview/browser/interact.ts +513 -0
- package/backend/ws/preview/browser/mcp.ts +129 -0
- package/backend/ws/preview/browser/native-ui.ts +235 -0
- package/backend/ws/preview/browser/stats.ts +55 -0
- package/backend/ws/preview/browser/tab-info.ts +126 -0
- package/backend/ws/preview/browser/tab.ts +166 -0
- package/backend/ws/preview/browser/webcodecs.ts +293 -0
- package/backend/ws/preview/index.ts +146 -0
- package/backend/ws/projects/crud.ts +113 -0
- package/backend/ws/projects/index.ts +25 -0
- package/backend/ws/projects/presence.ts +46 -0
- package/backend/ws/projects/status.ts +116 -0
- package/backend/ws/sessions/crud.ts +327 -0
- package/backend/ws/sessions/index.ts +33 -0
- package/backend/ws/settings/crud.ts +112 -0
- package/backend/ws/settings/index.ts +14 -0
- package/backend/ws/snapshot/index.ts +17 -0
- package/backend/ws/snapshot/restore.ts +173 -0
- package/backend/ws/snapshot/timeline.ts +141 -0
- package/backend/ws/system/index.ts +14 -0
- package/backend/ws/system/operations.ts +49 -0
- package/backend/ws/terminal/index.ts +40 -0
- package/backend/ws/terminal/persistence.ts +153 -0
- package/backend/ws/terminal/session.ts +382 -0
- package/backend/ws/terminal/stream.ts +79 -0
- package/backend/ws/tunnel/index.ts +14 -0
- package/backend/ws/tunnel/operations.ts +91 -0
- package/backend/ws/types.ts +20 -0
- package/backend/ws/user/crud.ts +156 -0
- package/backend/ws/user/index.ts +14 -0
- package/bin/clopen.ts +307 -0
- package/bun.lock +1352 -0
- package/frontend/App.svelte +34 -0
- package/frontend/app.css +313 -0
- package/frontend/lib/app-environment.ts +10 -0
- package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
- package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
- package/frontend/lib/components/chat/formatters/index.ts +3 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
- package/frontend/lib/components/chat/shared/index.ts +2 -0
- package/frontend/lib/components/chat/shared/utils.ts +116 -0
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
- package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
- package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
- package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
- package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
- package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
- package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
- package/frontend/lib/components/chat/tools/components/index.ts +7 -0
- package/frontend/lib/components/chat/tools/index.ts +26 -0
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
- package/frontend/lib/components/common/Alert.svelte +139 -0
- package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
- package/frontend/lib/components/common/Button.svelte +71 -0
- package/frontend/lib/components/common/Card.svelte +102 -0
- package/frontend/lib/components/common/Checkbox.svelte +48 -0
- package/frontend/lib/components/common/Dialog.svelte +249 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
- package/frontend/lib/components/common/Icon.svelte +58 -0
- package/frontend/lib/components/common/Input.svelte +72 -0
- package/frontend/lib/components/common/Lightbox.svelte +233 -0
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
- package/frontend/lib/components/common/Modal.svelte +177 -0
- package/frontend/lib/components/common/ModalProvider.svelte +28 -0
- package/frontend/lib/components/common/ModelSelector.svelte +110 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
- package/frontend/lib/components/common/NotificationToast.svelte +113 -0
- package/frontend/lib/components/common/PageTemplate.svelte +76 -0
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
- package/frontend/lib/components/common/Select.svelte +98 -0
- package/frontend/lib/components/common/Textarea.svelte +80 -0
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
- package/frontend/lib/components/common/lucide-icons.ts +1642 -0
- package/frontend/lib/components/common/material-icons.ts +1082 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
- package/frontend/lib/components/common/xterm/index.ts +16 -0
- package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
- package/frontend/lib/components/common/xterm/types.ts +31 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
- package/frontend/lib/components/files/FileNode.svelte +384 -0
- package/frontend/lib/components/files/FileTree.svelte +681 -0
- package/frontend/lib/components/files/FileViewer.svelte +728 -0
- package/frontend/lib/components/files/SearchResults.svelte +303 -0
- package/frontend/lib/components/git/BranchManager.svelte +458 -0
- package/frontend/lib/components/git/ChangesSection.svelte +107 -0
- package/frontend/lib/components/git/CommitForm.svelte +76 -0
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
- package/frontend/lib/components/git/DiffViewer.svelte +364 -0
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
- package/frontend/lib/components/git/GitButton.svelte +33 -0
- package/frontend/lib/components/git/GitLog.svelte +361 -0
- package/frontend/lib/components/git/GitModal.svelte +80 -0
- package/frontend/lib/components/history/HistoryModal.svelte +563 -0
- package/frontend/lib/components/history/HistoryView.svelte +615 -0
- package/frontend/lib/components/index.ts +34 -0
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
- package/frontend/lib/components/preview/index.ts +2 -0
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
- package/frontend/lib/components/settings/SettingsView.svelte +36 -0
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
- package/frontend/lib/components/terminal/Terminal.svelte +368 -0
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
- package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
- package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
- package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
- package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
- package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
- package/frontend/lib/constants/preview.ts +45 -0
- package/frontend/lib/services/chat/chat.service.ts +704 -0
- package/frontend/lib/services/chat/index.ts +7 -0
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
- package/frontend/lib/services/notification/index.ts +8 -0
- package/frontend/lib/services/notification/push.service.ts +144 -0
- package/frontend/lib/services/notification/sound.service.ts +127 -0
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
- package/frontend/lib/services/preview/index.ts +23 -0
- package/frontend/lib/services/project/index.ts +8 -0
- package/frontend/lib/services/project/status.service.ts +159 -0
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
- package/frontend/lib/services/terminal/background/index.ts +130 -0
- package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
- package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
- package/frontend/lib/services/terminal/index.ts +14 -0
- package/frontend/lib/services/terminal/persistence.service.ts +260 -0
- package/frontend/lib/services/terminal/project.service.ts +953 -0
- package/frontend/lib/services/terminal/session.service.ts +364 -0
- package/frontend/lib/services/terminal/terminal.service.ts +369 -0
- package/frontend/lib/stores/core/app.svelte.ts +117 -0
- package/frontend/lib/stores/core/files.svelte.ts +73 -0
- package/frontend/lib/stores/core/presence.svelte.ts +48 -0
- package/frontend/lib/stores/core/projects.svelte.ts +317 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
- package/frontend/lib/stores/features/models.svelte.ts +89 -0
- package/frontend/lib/stores/features/settings.svelte.ts +87 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
- package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
- package/frontend/lib/stores/features/user.svelte.ts +96 -0
- package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
- package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
- package/frontend/lib/types/native-ui.ts +73 -0
- package/frontend/lib/utils/chat/date-separator.ts +39 -0
- package/frontend/lib/utils/chat/message-grouper.ts +219 -0
- package/frontend/lib/utils/chat/message-processor.ts +135 -0
- package/frontend/lib/utils/chat/tool-handler.ts +161 -0
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
- package/frontend/lib/utils/click-outside.ts +20 -0
- package/frontend/lib/utils/context-manager.ts +257 -0
- package/frontend/lib/utils/file-icon-mappings.ts +769 -0
- package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
- package/frontend/lib/utils/git-status.ts +68 -0
- package/frontend/lib/utils/platform.ts +113 -0
- package/frontend/lib/utils/port-check.ts +65 -0
- package/frontend/lib/utils/terminalFormatter.ts +207 -0
- package/frontend/lib/utils/theme.ts +6 -0
- package/frontend/lib/utils/tree-visualizer.ts +320 -0
- package/frontend/lib/utils/ws.ts +44 -0
- package/frontend/main.ts +13 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/scripts/generate-icons.ts +87 -0
- package/scripts/pre-publish-check.sh +142 -0
- package/scripts/setup-hooks.sh +134 -0
- package/scripts/validate-branch-name.sh +47 -0
- package/scripts/validate-commit-msg.sh +42 -0
- package/shared/constants/engines.ts +134 -0
- package/shared/types/database/connection.ts +16 -0
- package/shared/types/database/index.ts +6 -0
- package/shared/types/database/schema.ts +141 -0
- package/shared/types/engine/index.ts +45 -0
- package/shared/types/filesystem/index.ts +22 -0
- package/shared/types/git.ts +171 -0
- package/shared/types/messaging/index.ts +239 -0
- package/shared/types/messaging/tool.ts +526 -0
- package/shared/types/network/api.ts +18 -0
- package/shared/types/network/index.ts +5 -0
- package/shared/types/stores/app.ts +23 -0
- package/shared/types/stores/dialog.ts +21 -0
- package/shared/types/stores/index.ts +3 -0
- package/shared/types/stores/settings.ts +15 -0
- package/shared/types/terminal/index.ts +44 -0
- package/shared/types/ui/components.ts +61 -0
- package/shared/types/ui/icons.ts +23 -0
- package/shared/types/ui/index.ts +22 -0
- package/shared/types/ui/notifications.ts +14 -0
- package/shared/types/ui/theme.ts +12 -0
- package/shared/types/websocket/index.ts +43 -0
- package/shared/types/window.d.ts +13 -0
- package/shared/utils/anonymous-user.ts +168 -0
- package/shared/utils/async.ts +10 -0
- package/shared/utils/diff-calculator.ts +184 -0
- package/shared/utils/file-type-detection.ts +166 -0
- package/shared/utils/logger.ts +158 -0
- package/shared/utils/message-formatter.ts +79 -0
- package/shared/utils/path.ts +47 -0
- package/shared/utils/ws-client.ts +768 -0
- package/shared/utils/ws-server.ts +660 -0
- package/static/audio/notification.ogg +0 -0
- package/static/favicon.svg +8 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
- package/static/fonts/dm-sans.css +96 -0
- package/svelte.config.js +20 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend WebSocket Server Singleton - Optimized
|
|
3
|
+
*
|
|
4
|
+
* High-performance WebSocket event emitter with:
|
|
5
|
+
* - Room-based architecture for O(1) project/user lookup
|
|
6
|
+
* - Stable connection identity via WeakMap on raw Bun ServerWebSocket
|
|
7
|
+
* - External state storage (not on ephemeral Elysia wrapper objects)
|
|
8
|
+
* - Backpressure handling to prevent OOM
|
|
9
|
+
* - Connection health monitoring
|
|
10
|
+
* - Clean emit API: ws.emit.project(), ws.emit.user(), ws.emit.global()
|
|
11
|
+
*
|
|
12
|
+
* CRITICAL: Elysia creates NEW ElysiaWS wrapper objects per handler call.
|
|
13
|
+
* We use `(ws as any).raw` (the underlying persistent Bun ServerWebSocket)
|
|
14
|
+
* as a stable identity key via WeakMap, and store all connection state
|
|
15
|
+
* in external Maps (connectionState). No properties are set on the wrapper.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { ws } from '$backend/lib/ws'
|
|
20
|
+
*
|
|
21
|
+
* // Project-specific event
|
|
22
|
+
* ws.emit.project(projectId, 'terminal:output', { content });
|
|
23
|
+
*
|
|
24
|
+
* // User-specific event
|
|
25
|
+
* ws.emit.user(userId, 'chat:error', { error });
|
|
26
|
+
*
|
|
27
|
+
* // System-wide event
|
|
28
|
+
* ws.emit.global('system:update', { version });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { WSAPI } from '$backend/ws';
|
|
33
|
+
import type { WSConnection } from '$shared/utils/ws-server';
|
|
34
|
+
import { encodeBinaryMessage, isBinaryAction } from '$shared/utils/ws-server';
|
|
35
|
+
import { debug } from '$shared/utils/logger';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Performance configuration
|
|
39
|
+
*/
|
|
40
|
+
const CONFIG = {
|
|
41
|
+
/** Maximum send buffer before dropping frames (bytes) */
|
|
42
|
+
MAX_BUFFER_SIZE: 1024 * 1024, // 1MB
|
|
43
|
+
/** Enable backpressure handling */
|
|
44
|
+
ENABLE_BACKPRESSURE: true,
|
|
45
|
+
/** Log dropped frames */
|
|
46
|
+
LOG_DROPPED_FRAMES: true
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Metrics for monitoring
|
|
51
|
+
*/
|
|
52
|
+
interface WSMetrics {
|
|
53
|
+
totalConnections: number;
|
|
54
|
+
activeProjects: number;
|
|
55
|
+
activeUsers: number;
|
|
56
|
+
droppedFrames: number;
|
|
57
|
+
totalEmits: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* External connection state (stored separately from ephemeral ws wrappers)
|
|
62
|
+
*/
|
|
63
|
+
interface ConnectionState {
|
|
64
|
+
userId: string | null;
|
|
65
|
+
projectId: string | null;
|
|
66
|
+
/** Chat session IDs this connection is subscribed to */
|
|
67
|
+
chatSessionIds: Set<string>;
|
|
68
|
+
/** Cleanup functions called automatically on unregister (connection close) */
|
|
69
|
+
cleanups: Set<() => void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* High-Performance WebSocket Server Manager
|
|
74
|
+
*
|
|
75
|
+
* Uses WeakMap<rawSocket, wsId> for stable identity across Elysia wrapper instances,
|
|
76
|
+
* and external Maps for all connection state (userId, projectId).
|
|
77
|
+
*/
|
|
78
|
+
class WSServer {
|
|
79
|
+
/** All connections by ID */
|
|
80
|
+
private connections = new Map<string, WSConnection>();
|
|
81
|
+
|
|
82
|
+
/** Raw Bun ServerWebSocket → wsId for stable identity across wrapper instances */
|
|
83
|
+
private rawToId = new WeakMap<object, string>();
|
|
84
|
+
|
|
85
|
+
/** External connection state (userId, projectId) keyed by wsId */
|
|
86
|
+
private connectionState = new Map<string, ConnectionState>();
|
|
87
|
+
|
|
88
|
+
/** Room-based: Project ID → Map<wsId, WSConnection> */
|
|
89
|
+
private projectRooms = new Map<string, Map<string, WSConnection>>();
|
|
90
|
+
|
|
91
|
+
/** Room-based: User ID → Map<wsId, WSConnection> */
|
|
92
|
+
private userConnections = new Map<string, Map<string, WSConnection>>();
|
|
93
|
+
|
|
94
|
+
/** Room-based: Chat Session ID → Map<wsId, WSConnection> */
|
|
95
|
+
private chatSessionRooms = new Map<string, Map<string, WSConnection>>();
|
|
96
|
+
|
|
97
|
+
/** Project membership: Project ID → Set<userId> (persists across project switches) */
|
|
98
|
+
private projectMembers = new Map<string, Set<string>>();
|
|
99
|
+
|
|
100
|
+
/** Metrics tracking */
|
|
101
|
+
private metrics: WSMetrics = {
|
|
102
|
+
totalConnections: 0,
|
|
103
|
+
activeProjects: 0,
|
|
104
|
+
activeUsers: 0,
|
|
105
|
+
droppedFrames: 0,
|
|
106
|
+
totalEmits: 0
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve stable wsId from any ws wrapper using the raw Bun ServerWebSocket.
|
|
111
|
+
* Returns null if the connection has never been registered.
|
|
112
|
+
*/
|
|
113
|
+
private resolveId(conn: WSConnection): string | null {
|
|
114
|
+
const raw = (conn as any).raw;
|
|
115
|
+
if (!raw) return null;
|
|
116
|
+
return this.rawToId.get(raw) ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Register a WebSocket connection (idempotent based on raw socket identity).
|
|
121
|
+
* Called automatically by Elysia WebSocket plugin on connection open.
|
|
122
|
+
* Also called lazily by ensureRegistered() if a message arrives before
|
|
123
|
+
* the async open handler completes (race condition with await import).
|
|
124
|
+
*/
|
|
125
|
+
register(conn: WSConnection): string {
|
|
126
|
+
// Check if already registered via raw socket identity
|
|
127
|
+
const existingId = this.resolveId(conn);
|
|
128
|
+
if (existingId && this.connections.has(existingId)) {
|
|
129
|
+
// Update stored wrapper reference (so room maps point to latest wrapper)
|
|
130
|
+
this.connections.set(existingId, conn);
|
|
131
|
+
return existingId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// New connection: create fresh registration
|
|
135
|
+
const raw = (conn as any).raw;
|
|
136
|
+
if (!raw) {
|
|
137
|
+
throw new Error('No raw socket found on ws wrapper - cannot register connection');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const id = crypto.randomUUID();
|
|
141
|
+
this.rawToId.set(raw, id);
|
|
142
|
+
this.connections.set(id, conn);
|
|
143
|
+
this.connectionState.set(id, { userId: null, projectId: null, chatSessionIds: new Set(), cleanups: new Set() });
|
|
144
|
+
|
|
145
|
+
this.metrics.totalConnections = this.connections.size;
|
|
146
|
+
debug.log('websocket', `Connection registered: ${id} (total: ${this.connections.size})`);
|
|
147
|
+
return id;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ensure a connection is registered, hydrated, and return its wsId.
|
|
152
|
+
* Handles the race condition where message handlers fire before the async
|
|
153
|
+
* open handler completes its await import('$backend/lib/utils/ws').
|
|
154
|
+
*/
|
|
155
|
+
private ensureRegistered(conn: WSConnection): string {
|
|
156
|
+
const existingId = this.resolveId(conn);
|
|
157
|
+
if (existingId && this.connections.has(existingId)) {
|
|
158
|
+
this.connections.set(existingId, conn);
|
|
159
|
+
return existingId;
|
|
160
|
+
}
|
|
161
|
+
return this.register(conn);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Unregister a WebSocket connection.
|
|
166
|
+
* Called automatically on connection close.
|
|
167
|
+
* Runs all registered cleanup functions before removing state.
|
|
168
|
+
*/
|
|
169
|
+
unregister(conn: WSConnection): void {
|
|
170
|
+
const id = this.resolveId(conn);
|
|
171
|
+
if (!id) return;
|
|
172
|
+
|
|
173
|
+
// Get state from external storage (NOT from wrapper which may be stale)
|
|
174
|
+
const state = this.connectionState.get(id);
|
|
175
|
+
|
|
176
|
+
// Run all registered cleanup functions
|
|
177
|
+
if (state?.cleanups.size) {
|
|
178
|
+
for (const cleanup of state.cleanups) {
|
|
179
|
+
try { cleanup(); } catch { /* ignore cleanup errors */ }
|
|
180
|
+
}
|
|
181
|
+
debug.log('websocket', `Ran ${state.cleanups.size} cleanup(s) for connection ${id}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Remove from project room
|
|
185
|
+
const projectId = state?.projectId;
|
|
186
|
+
if (projectId) {
|
|
187
|
+
const room = this.projectRooms.get(projectId);
|
|
188
|
+
if (room) {
|
|
189
|
+
room.delete(id);
|
|
190
|
+
if (room.size === 0) {
|
|
191
|
+
this.projectRooms.delete(projectId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Remove from all chat session rooms
|
|
197
|
+
if (state?.chatSessionIds) {
|
|
198
|
+
for (const csId of state.chatSessionIds) {
|
|
199
|
+
const csRoom = this.chatSessionRooms.get(csId);
|
|
200
|
+
if (csRoom) {
|
|
201
|
+
csRoom.delete(id);
|
|
202
|
+
if (csRoom.size === 0) {
|
|
203
|
+
this.chatSessionRooms.delete(csId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Remove from user connections
|
|
210
|
+
const userId = state?.userId;
|
|
211
|
+
if (userId) {
|
|
212
|
+
const userConns = this.userConnections.get(userId);
|
|
213
|
+
if (userConns) {
|
|
214
|
+
userConns.delete(id);
|
|
215
|
+
if (userConns.size === 0) {
|
|
216
|
+
this.userConnections.delete(userId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clean up all state
|
|
222
|
+
this.connections.delete(id);
|
|
223
|
+
this.connectionState.delete(id);
|
|
224
|
+
|
|
225
|
+
// Clean up raw socket mapping
|
|
226
|
+
const raw = (conn as any).raw;
|
|
227
|
+
if (raw) {
|
|
228
|
+
this.rawToId.delete(raw);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.metrics.totalConnections = this.connections.size;
|
|
232
|
+
this.metrics.activeProjects = this.projectRooms.size;
|
|
233
|
+
this.metrics.activeUsers = this.userConnections.size;
|
|
234
|
+
|
|
235
|
+
debug.log('websocket', `Connection unregistered: ${id} (total: ${this.connections.size})`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Register a cleanup function for a connection.
|
|
240
|
+
* Will be called automatically when the connection is unregistered (closed).
|
|
241
|
+
* Uses raw socket identity so it works across Elysia wrapper instances.
|
|
242
|
+
*/
|
|
243
|
+
addCleanup(conn: WSConnection, cleanup: () => void): void {
|
|
244
|
+
const wsId = this.ensureRegistered(conn);
|
|
245
|
+
const state = this.connectionState.get(wsId);
|
|
246
|
+
state?.cleanups.add(cleanup);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Remove a previously registered cleanup function.
|
|
251
|
+
*/
|
|
252
|
+
removeCleanup(conn: WSConnection, cleanup: () => void): void {
|
|
253
|
+
const id = this.resolveId(conn);
|
|
254
|
+
if (!id) return;
|
|
255
|
+
const state = this.connectionState.get(id);
|
|
256
|
+
state?.cleanups.delete(cleanup);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Set user ID for a connection.
|
|
261
|
+
* Tracks which user owns this connection (for per-user events).
|
|
262
|
+
*/
|
|
263
|
+
setUser(conn: WSConnection, userId: string | null): void {
|
|
264
|
+
const wsId = this.ensureRegistered(conn);
|
|
265
|
+
|
|
266
|
+
// Get OLD userId from external state (NOT from wrapper which may be stale)
|
|
267
|
+
const state = this.connectionState.get(wsId);
|
|
268
|
+
const oldUserId = state?.userId;
|
|
269
|
+
|
|
270
|
+
// Remove from old user group by ID
|
|
271
|
+
if (oldUserId && oldUserId !== userId) {
|
|
272
|
+
const oldUserConns = this.userConnections.get(oldUserId);
|
|
273
|
+
if (oldUserConns) {
|
|
274
|
+
oldUserConns.delete(wsId);
|
|
275
|
+
if (oldUserConns.size === 0) {
|
|
276
|
+
this.userConnections.delete(oldUserId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update connection state
|
|
282
|
+
if (state) {
|
|
283
|
+
state.userId = userId;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Add to new user group by ID (Map.set replaces if same key → no duplicates)
|
|
287
|
+
if (userId) {
|
|
288
|
+
if (!this.userConnections.has(userId)) {
|
|
289
|
+
this.userConnections.set(userId, new Map());
|
|
290
|
+
}
|
|
291
|
+
this.userConnections.get(userId)!.set(wsId, conn);
|
|
292
|
+
|
|
293
|
+
// If connection already has a project, track membership
|
|
294
|
+
// (handles case where setUser is called after setProject)
|
|
295
|
+
const currentProjectId = state?.projectId;
|
|
296
|
+
if (currentProjectId) {
|
|
297
|
+
if (!this.projectMembers.has(currentProjectId)) {
|
|
298
|
+
this.projectMembers.set(currentProjectId, new Set());
|
|
299
|
+
}
|
|
300
|
+
this.projectMembers.get(currentProjectId)!.add(userId);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.metrics.activeUsers = this.userConnections.size;
|
|
305
|
+
debug.log('websocket', `Connection ${wsId} set to user: ${userId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get user ID for a connection.
|
|
310
|
+
* Throws if not set - ensures single source of truth.
|
|
311
|
+
* Client must call ws:set-context before sending events.
|
|
312
|
+
*/
|
|
313
|
+
getUserId(conn: WSConnection): string {
|
|
314
|
+
const id = this.resolveId(conn);
|
|
315
|
+
if (!id) {
|
|
316
|
+
throw new Error('Connection not registered. Cannot resolve userId.');
|
|
317
|
+
}
|
|
318
|
+
const userId = this.connectionState.get(id)?.userId;
|
|
319
|
+
if (!userId) {
|
|
320
|
+
throw new Error('No userId on connection. Client must call ws:set-context first.');
|
|
321
|
+
}
|
|
322
|
+
return userId;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Set current project for a connection.
|
|
327
|
+
* Moves connection between project rooms efficiently using ID-based lookup.
|
|
328
|
+
*/
|
|
329
|
+
setProject(conn: WSConnection, projectId: string | null): void {
|
|
330
|
+
const wsId = this.ensureRegistered(conn);
|
|
331
|
+
|
|
332
|
+
// Get OLD projectId from external state (NOT from wrapper which may be stale)
|
|
333
|
+
const state = this.connectionState.get(wsId);
|
|
334
|
+
const oldProjectId = state?.projectId;
|
|
335
|
+
|
|
336
|
+
// Remove from old project room by ID
|
|
337
|
+
if (oldProjectId && oldProjectId !== projectId) {
|
|
338
|
+
const oldRoom = this.projectRooms.get(oldProjectId);
|
|
339
|
+
if (oldRoom) {
|
|
340
|
+
oldRoom.delete(wsId);
|
|
341
|
+
if (oldRoom.size === 0) {
|
|
342
|
+
this.projectRooms.delete(oldProjectId);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Update connection state
|
|
348
|
+
if (state) {
|
|
349
|
+
state.projectId = projectId;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add to new project room by ID (Map.set replaces if same key → no duplicates)
|
|
353
|
+
if (projectId) {
|
|
354
|
+
if (!this.projectRooms.has(projectId)) {
|
|
355
|
+
this.projectRooms.set(projectId, new Map());
|
|
356
|
+
}
|
|
357
|
+
this.projectRooms.get(projectId)!.set(wsId, conn);
|
|
358
|
+
|
|
359
|
+
// Track project membership (persists across project switches)
|
|
360
|
+
// Used by emit.projectMembers() for cross-project notifications
|
|
361
|
+
const userId = state?.userId;
|
|
362
|
+
if (userId) {
|
|
363
|
+
if (!this.projectMembers.has(projectId)) {
|
|
364
|
+
this.projectMembers.set(projectId, new Set());
|
|
365
|
+
}
|
|
366
|
+
this.projectMembers.get(projectId)!.add(userId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.metrics.activeProjects = this.projectRooms.size;
|
|
371
|
+
debug.log('websocket', `Connection ${wsId} set to project: ${projectId}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get project ID for a connection.
|
|
376
|
+
* Throws if not set - ensures single source of truth.
|
|
377
|
+
* Client must call ws:set-context before sending events.
|
|
378
|
+
*/
|
|
379
|
+
getProjectId(conn: WSConnection): string {
|
|
380
|
+
const id = this.resolveId(conn);
|
|
381
|
+
if (!id) {
|
|
382
|
+
throw new Error('Connection not registered. Cannot resolve projectId.');
|
|
383
|
+
}
|
|
384
|
+
const projectId = this.connectionState.get(id)?.projectId;
|
|
385
|
+
if (!projectId) {
|
|
386
|
+
throw new Error('No projectId on connection. Client must call ws:set-context first.');
|
|
387
|
+
}
|
|
388
|
+
return projectId;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Join a chat session room.
|
|
393
|
+
* A connection can be in multiple chat session rooms simultaneously,
|
|
394
|
+
* but typically only one at a time (leave old before joining new).
|
|
395
|
+
*/
|
|
396
|
+
joinChatSession(conn: WSConnection, chatSessionId: string): void {
|
|
397
|
+
const wsId = this.ensureRegistered(conn);
|
|
398
|
+
const state = this.connectionState.get(wsId);
|
|
399
|
+
|
|
400
|
+
if (!state) return;
|
|
401
|
+
|
|
402
|
+
// Add to chat session room
|
|
403
|
+
if (!this.chatSessionRooms.has(chatSessionId)) {
|
|
404
|
+
this.chatSessionRooms.set(chatSessionId, new Map());
|
|
405
|
+
}
|
|
406
|
+
this.chatSessionRooms.get(chatSessionId)!.set(wsId, conn);
|
|
407
|
+
|
|
408
|
+
// Track on connection state
|
|
409
|
+
state.chatSessionIds.add(chatSessionId);
|
|
410
|
+
|
|
411
|
+
debug.log('websocket', `Connection ${wsId} joined chat session: ${chatSessionId}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Leave a chat session room.
|
|
416
|
+
*/
|
|
417
|
+
leaveChatSession(conn: WSConnection, chatSessionId: string): void {
|
|
418
|
+
const wsId = this.resolveId(conn);
|
|
419
|
+
if (!wsId) return;
|
|
420
|
+
|
|
421
|
+
const state = this.connectionState.get(wsId);
|
|
422
|
+
if (state) {
|
|
423
|
+
state.chatSessionIds.delete(chatSessionId);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const room = this.chatSessionRooms.get(chatSessionId);
|
|
427
|
+
if (room) {
|
|
428
|
+
room.delete(wsId);
|
|
429
|
+
if (room.size === 0) {
|
|
430
|
+
this.chatSessionRooms.delete(chatSessionId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
debug.log('websocket', `Connection ${wsId} left chat session: ${chatSessionId}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Leave all chat session rooms for a connection.
|
|
439
|
+
*/
|
|
440
|
+
leaveAllChatSessions(conn: WSConnection): void {
|
|
441
|
+
const wsId = this.resolveId(conn);
|
|
442
|
+
if (!wsId) return;
|
|
443
|
+
|
|
444
|
+
const state = this.connectionState.get(wsId);
|
|
445
|
+
if (!state) return;
|
|
446
|
+
|
|
447
|
+
for (const csId of state.chatSessionIds) {
|
|
448
|
+
const room = this.chatSessionRooms.get(csId);
|
|
449
|
+
if (room) {
|
|
450
|
+
room.delete(wsId);
|
|
451
|
+
if (room.size === 0) {
|
|
452
|
+
this.chatSessionRooms.delete(csId);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
state.chatSessionIds.clear();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get user info for all connections in a chat session room.
|
|
461
|
+
* Returns deduplicated list of { userId, userName } for users currently viewing the session.
|
|
462
|
+
*/
|
|
463
|
+
getChatSessionUsers(chatSessionId: string): { userId: string; userName: string }[] {
|
|
464
|
+
const room = this.chatSessionRooms.get(chatSessionId);
|
|
465
|
+
if (!room || room.size === 0) return [];
|
|
466
|
+
|
|
467
|
+
const seen = new Map<string, string>(); // userId → userName
|
|
468
|
+
for (const [wsId] of room) {
|
|
469
|
+
const state = this.connectionState.get(wsId);
|
|
470
|
+
if (state?.userId) {
|
|
471
|
+
// Resolve userName from user connections or fallback
|
|
472
|
+
if (!seen.has(state.userId)) {
|
|
473
|
+
seen.set(state.userId, state.userId); // default to userId
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return Array.from(seen.entries()).map(([userId, userName]) => ({ userId, userName }));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get all chat session IDs that have active connections for a given project.
|
|
482
|
+
* Used for per-session presence data.
|
|
483
|
+
*/
|
|
484
|
+
getProjectChatSessions(projectId: string): Map<string, { userId: string }[]> {
|
|
485
|
+
const result = new Map<string, { userId: string }[]>();
|
|
486
|
+
|
|
487
|
+
// Iterate through all chat session rooms
|
|
488
|
+
for (const [chatSessionId, room] of this.chatSessionRooms) {
|
|
489
|
+
const users: { userId: string }[] = [];
|
|
490
|
+
const seenUsers = new Set<string>();
|
|
491
|
+
|
|
492
|
+
for (const [wsId] of room) {
|
|
493
|
+
const state = this.connectionState.get(wsId);
|
|
494
|
+
// Only include connections that belong to this project
|
|
495
|
+
if (state?.projectId === projectId && state?.userId && !seenUsers.has(state.userId)) {
|
|
496
|
+
seenUsers.add(state.userId);
|
|
497
|
+
users.push({ userId: state.userId });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (users.length > 0) {
|
|
502
|
+
result.set(chatSessionId, users);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get raw connection state for a connection.
|
|
511
|
+
* Used internally by ws:set-context to read back current values.
|
|
512
|
+
*/
|
|
513
|
+
getConnectionState(conn: WSConnection): ConnectionState | undefined {
|
|
514
|
+
const id = this.resolveId(conn);
|
|
515
|
+
if (!id) return undefined;
|
|
516
|
+
return this.connectionState.get(id);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check if connection can receive data (backpressure check)
|
|
521
|
+
*/
|
|
522
|
+
private canSend(conn: WSConnection): boolean {
|
|
523
|
+
if (!CONFIG.ENABLE_BACKPRESSURE) return true;
|
|
524
|
+
|
|
525
|
+
// Check WebSocket ready state
|
|
526
|
+
if (conn.readyState !== 1) return false;
|
|
527
|
+
|
|
528
|
+
// Check buffer size if available (Bun/uWebSockets)
|
|
529
|
+
const rawWs = conn as any;
|
|
530
|
+
if (typeof rawWs.getBufferedAmount === 'function') {
|
|
531
|
+
const buffered = rawWs.getBufferedAmount();
|
|
532
|
+
if (buffered > CONFIG.MAX_BUFFER_SIZE) {
|
|
533
|
+
if (CONFIG.LOG_DROPPED_FRAMES) {
|
|
534
|
+
this.metrics.droppedFrames++;
|
|
535
|
+
const id = this.resolveId(conn) || 'unknown';
|
|
536
|
+
debug.warn('websocket', `Backpressure: dropping frame for ${id} (buffer: ${buffered})`);
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Send message to a single connection with backpressure handling
|
|
547
|
+
*/
|
|
548
|
+
private sendToConnection(conn: WSConnection, message: string | ArrayBuffer): boolean {
|
|
549
|
+
if (!this.canSend(conn)) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
// For binary data (ArrayBuffer), wrap in Buffer for Bun/Elysia compatibility
|
|
555
|
+
if (message instanceof ArrayBuffer) {
|
|
556
|
+
conn.send(Buffer.from(message));
|
|
557
|
+
} else {
|
|
558
|
+
conn.send(message);
|
|
559
|
+
}
|
|
560
|
+
return true;
|
|
561
|
+
} catch (err) {
|
|
562
|
+
const id = this.resolveId(conn) || 'unknown';
|
|
563
|
+
debug.error('websocket', `Send error for ${id}:`, err);
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Clean stale connections from a room Map.
|
|
570
|
+
* Returns number of stale connections removed.
|
|
571
|
+
*/
|
|
572
|
+
private cleanStaleFromRoom(room: Map<string, WSConnection>): number {
|
|
573
|
+
const staleIds: string[] = [];
|
|
574
|
+
for (const [wsId, wsConn] of room) {
|
|
575
|
+
if (wsConn.readyState !== 1) {
|
|
576
|
+
staleIds.push(wsId);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const staleId of staleIds) {
|
|
580
|
+
const staleConn = room.get(staleId);
|
|
581
|
+
room.delete(staleId);
|
|
582
|
+
if (staleConn) {
|
|
583
|
+
this.unregister(staleConn);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return staleIds.length;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Emit API - Clean interface for sending events
|
|
591
|
+
*/
|
|
592
|
+
emit = {
|
|
593
|
+
/**
|
|
594
|
+
* Emit event to all connections in a project room
|
|
595
|
+
*/
|
|
596
|
+
project: <K extends keyof WSAPI['server']>(
|
|
597
|
+
projectId: string,
|
|
598
|
+
event: K,
|
|
599
|
+
payload: WSAPI['server'][K]
|
|
600
|
+
): void => {
|
|
601
|
+
this.metrics.totalEmits++;
|
|
602
|
+
|
|
603
|
+
const room = this.projectRooms.get(projectId);
|
|
604
|
+
if (!room || room.size === 0) {
|
|
605
|
+
debug.warn('websocket', `No connections in project room: ${projectId} for event: ${String(event)}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Clean stale connections before sending
|
|
610
|
+
const staleCount = this.cleanStaleFromRoom(room);
|
|
611
|
+
if (staleCount > 0) {
|
|
612
|
+
debug.log('websocket', `Cleaned ${staleCount} stale connections from project room ${projectId}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (room.size === 0) {
|
|
616
|
+
this.projectRooms.delete(projectId);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check if payload contains binary data - use binary encoding if so
|
|
621
|
+
const eventStr = String(event);
|
|
622
|
+
const hasBinary = isBinaryAction(eventStr, payload);
|
|
623
|
+
const message = hasBinary
|
|
624
|
+
? encodeBinaryMessage(eventStr, payload)
|
|
625
|
+
: JSON.stringify({ action: event, payload });
|
|
626
|
+
|
|
627
|
+
let sentCount = 0;
|
|
628
|
+
|
|
629
|
+
for (const wsConn of room.values()) {
|
|
630
|
+
if (this.sendToConnection(wsConn, message)) {
|
|
631
|
+
sentCount++;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${room.size} connections in project ${projectId}`);
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Emit event to all connections of a specific user
|
|
640
|
+
*/
|
|
641
|
+
user: <K extends keyof WSAPI['server']>(
|
|
642
|
+
userId: string,
|
|
643
|
+
event: K,
|
|
644
|
+
payload: WSAPI['server'][K]
|
|
645
|
+
): void => {
|
|
646
|
+
this.metrics.totalEmits++;
|
|
647
|
+
|
|
648
|
+
const userConns = this.userConnections.get(userId);
|
|
649
|
+
if (!userConns || userConns.size === 0) {
|
|
650
|
+
debug.warn('websocket', `No connections for user: ${userId} for event: ${String(event)}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Clean stale connections
|
|
655
|
+
const staleCount = this.cleanStaleFromRoom(userConns);
|
|
656
|
+
if (staleCount > 0) {
|
|
657
|
+
debug.log('websocket', `Cleaned ${staleCount} stale user connections for user ${userId}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (userConns.size === 0) {
|
|
661
|
+
this.userConnections.delete(userId);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Check if payload contains binary data - use binary encoding if so
|
|
666
|
+
const eventStr = String(event);
|
|
667
|
+
const hasBinary = isBinaryAction(eventStr, payload);
|
|
668
|
+
const message = hasBinary
|
|
669
|
+
? encodeBinaryMessage(eventStr, payload)
|
|
670
|
+
: JSON.stringify({ action: event, payload });
|
|
671
|
+
|
|
672
|
+
let sentCount = 0;
|
|
673
|
+
|
|
674
|
+
for (const wsConn of userConns.values()) {
|
|
675
|
+
if (this.sendToConnection(wsConn, message)) {
|
|
676
|
+
sentCount++;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${userConns.size} connections for user ${userId}`);
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Emit event to all users who have ever joined a project.
|
|
685
|
+
* Unlike emit.project() which only reaches connections currently in the room,
|
|
686
|
+
* this reaches ALL connections of users who have been associated with the project
|
|
687
|
+
* (even if they switched to a different project).
|
|
688
|
+
* Used for cross-project notifications (e.g., stream finished).
|
|
689
|
+
*/
|
|
690
|
+
projectMembers: <K extends keyof WSAPI['server']>(
|
|
691
|
+
projectId: string,
|
|
692
|
+
event: K,
|
|
693
|
+
payload: WSAPI['server'][K]
|
|
694
|
+
): void => {
|
|
695
|
+
this.metrics.totalEmits++;
|
|
696
|
+
|
|
697
|
+
const memberIds = this.projectMembers.get(projectId);
|
|
698
|
+
if (!memberIds || memberIds.size === 0) {
|
|
699
|
+
debug.warn('websocket', `No members for project: ${projectId} for event: ${String(event)}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Check if payload contains binary data - use binary encoding if so
|
|
704
|
+
const eventStr = String(event);
|
|
705
|
+
const hasBinary = isBinaryAction(eventStr, payload);
|
|
706
|
+
const message = hasBinary
|
|
707
|
+
? encodeBinaryMessage(eventStr, payload)
|
|
708
|
+
: JSON.stringify({ action: event, payload });
|
|
709
|
+
|
|
710
|
+
let sentCount = 0;
|
|
711
|
+
let totalConns = 0;
|
|
712
|
+
|
|
713
|
+
// Send to all connections of all member users
|
|
714
|
+
for (const userId of memberIds) {
|
|
715
|
+
const userConns = this.userConnections.get(userId);
|
|
716
|
+
if (!userConns || userConns.size === 0) continue;
|
|
717
|
+
|
|
718
|
+
// Clean stale connections
|
|
719
|
+
this.cleanStaleFromRoom(userConns);
|
|
720
|
+
if (userConns.size === 0) {
|
|
721
|
+
this.userConnections.delete(userId);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
totalConns += userConns.size;
|
|
726
|
+
for (const wsConn of userConns.values()) {
|
|
727
|
+
if (this.sendToConnection(wsConn, message)) {
|
|
728
|
+
sentCount++;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${totalConns} connections of ${memberIds.size} members in project ${projectId}`);
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Emit event to all connections in a chat session room.
|
|
738
|
+
* Used for session-scoped chat events (message, partial, complete, etc.)
|
|
739
|
+
*/
|
|
740
|
+
chatSession: <K extends keyof WSAPI['server']>(
|
|
741
|
+
chatSessionId: string,
|
|
742
|
+
event: K,
|
|
743
|
+
payload: WSAPI['server'][K]
|
|
744
|
+
): void => {
|
|
745
|
+
this.metrics.totalEmits++;
|
|
746
|
+
|
|
747
|
+
const room = this.chatSessionRooms.get(chatSessionId);
|
|
748
|
+
if (!room || room.size === 0) {
|
|
749
|
+
debug.warn('websocket', `No connections in chat session room: ${chatSessionId} for event: ${String(event)}`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Clean stale connections before sending
|
|
754
|
+
const staleCount = this.cleanStaleFromRoom(room);
|
|
755
|
+
if (staleCount > 0) {
|
|
756
|
+
debug.log('websocket', `Cleaned ${staleCount} stale connections from chat session room ${chatSessionId}`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (room.size === 0) {
|
|
760
|
+
this.chatSessionRooms.delete(chatSessionId);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const eventStr = String(event);
|
|
765
|
+
const hasBinary = isBinaryAction(eventStr, payload);
|
|
766
|
+
const message = hasBinary
|
|
767
|
+
? encodeBinaryMessage(eventStr, payload)
|
|
768
|
+
: JSON.stringify({ action: event, payload });
|
|
769
|
+
|
|
770
|
+
let sentCount = 0;
|
|
771
|
+
|
|
772
|
+
for (const wsConn of room.values()) {
|
|
773
|
+
if (this.sendToConnection(wsConn, message)) {
|
|
774
|
+
sentCount++;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${room.size} connections in chat session ${chatSessionId.slice(0, 8)}`);
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Emit event to all connections (broadcast)
|
|
783
|
+
*/
|
|
784
|
+
global: <K extends keyof WSAPI['server']>(
|
|
785
|
+
event: K,
|
|
786
|
+
payload: WSAPI['server'][K]
|
|
787
|
+
): void => {
|
|
788
|
+
this.metrics.totalEmits++;
|
|
789
|
+
|
|
790
|
+
// Check if payload contains binary data - use binary encoding if so
|
|
791
|
+
const eventStr = String(event);
|
|
792
|
+
const hasBinary = isBinaryAction(eventStr, payload);
|
|
793
|
+
const message = hasBinary
|
|
794
|
+
? encodeBinaryMessage(eventStr, payload)
|
|
795
|
+
: JSON.stringify({ action: event, payload });
|
|
796
|
+
|
|
797
|
+
let sentCount = 0;
|
|
798
|
+
|
|
799
|
+
for (const wsConn of this.connections.values()) {
|
|
800
|
+
if (this.sendToConnection(wsConn, message)) {
|
|
801
|
+
sentCount++;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${this.connections.size} connections (global)`);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Emit binary event to project room
|
|
811
|
+
*/
|
|
812
|
+
emitBinary = {
|
|
813
|
+
/**
|
|
814
|
+
* Emit binary data to all connections in a project room
|
|
815
|
+
*/
|
|
816
|
+
project: <K extends keyof WSAPI['server']>(
|
|
817
|
+
projectId: string,
|
|
818
|
+
_event: K,
|
|
819
|
+
binaryMessage: ArrayBuffer
|
|
820
|
+
): void => {
|
|
821
|
+
this.metrics.totalEmits++;
|
|
822
|
+
|
|
823
|
+
const room = this.projectRooms.get(projectId);
|
|
824
|
+
if (!room || room.size === 0) return;
|
|
825
|
+
|
|
826
|
+
let sentCount = 0;
|
|
827
|
+
for (const ws of room.values()) {
|
|
828
|
+
if (this.sendToConnection(ws, binaryMessage)) {
|
|
829
|
+
sentCount++;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
debug.log('websocket', `Emitted binary to ${sentCount}/${room.size} connections in project ${projectId}`);
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Emit binary data to all connections of a specific user
|
|
838
|
+
*/
|
|
839
|
+
user: <K extends keyof WSAPI['server']>(
|
|
840
|
+
userId: string,
|
|
841
|
+
_event: K,
|
|
842
|
+
binaryMessage: ArrayBuffer
|
|
843
|
+
): void => {
|
|
844
|
+
this.metrics.totalEmits++;
|
|
845
|
+
|
|
846
|
+
const userConns = this.userConnections.get(userId);
|
|
847
|
+
if (!userConns || userConns.size === 0) return;
|
|
848
|
+
|
|
849
|
+
let sentCount = 0;
|
|
850
|
+
for (const ws of userConns.values()) {
|
|
851
|
+
if (this.sendToConnection(ws, binaryMessage)) {
|
|
852
|
+
sentCount++;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
debug.log('websocket', `Emitted binary to ${sentCount}/${userConns.size} connections for user ${userId}`);
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Emit binary data to all connections (broadcast)
|
|
861
|
+
*/
|
|
862
|
+
global: <K extends keyof WSAPI['server']>(
|
|
863
|
+
_event: K,
|
|
864
|
+
binaryMessage: ArrayBuffer
|
|
865
|
+
): void => {
|
|
866
|
+
this.metrics.totalEmits++;
|
|
867
|
+
|
|
868
|
+
let sentCount = 0;
|
|
869
|
+
for (const ws of this.connections.values()) {
|
|
870
|
+
if (this.sendToConnection(ws, binaryMessage)) {
|
|
871
|
+
sentCount++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
debug.log('websocket', `Emitted binary to ${sentCount}/${this.connections.size} connections (global)`);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Get all connections (optionally filtered by project)
|
|
881
|
+
*/
|
|
882
|
+
getConnections(projectId?: string): WSConnection[] {
|
|
883
|
+
if (projectId) {
|
|
884
|
+
const room = this.projectRooms.get(projectId);
|
|
885
|
+
return room ? Array.from(room.values()) : [];
|
|
886
|
+
}
|
|
887
|
+
return Array.from(this.connections.values());
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Get connection count (optionally filtered by project)
|
|
892
|
+
*/
|
|
893
|
+
getConnectionCount(projectId?: string): number {
|
|
894
|
+
if (projectId) {
|
|
895
|
+
return this.projectRooms.get(projectId)?.size || 0;
|
|
896
|
+
}
|
|
897
|
+
return this.connections.size;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Get all active project IDs
|
|
902
|
+
*/
|
|
903
|
+
getActiveProjects(): Set<string> {
|
|
904
|
+
return new Set(this.projectRooms.keys());
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Get all active user IDs
|
|
909
|
+
*/
|
|
910
|
+
getActiveUsers(): Set<string> {
|
|
911
|
+
return new Set(this.userConnections.keys());
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Get current metrics for monitoring
|
|
916
|
+
*/
|
|
917
|
+
getMetrics(): WSMetrics {
|
|
918
|
+
return { ...this.metrics };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Get room sizes for debugging
|
|
923
|
+
*/
|
|
924
|
+
getRoomSizes(): { projects: Map<string, number>; users: Map<string, number> } {
|
|
925
|
+
const projects = new Map<string, number>();
|
|
926
|
+
const users = new Map<string, number>();
|
|
927
|
+
|
|
928
|
+
for (const [id, room] of this.projectRooms) {
|
|
929
|
+
projects.set(id, room.size);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
for (const [id, conns] of this.userConnections) {
|
|
933
|
+
users.set(id, conns.size);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return { projects, users };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Singleton instance
|
|
942
|
+
* Import this in any backend file to emit events
|
|
943
|
+
*/
|
|
944
|
+
export const ws = new WSServer();
|