@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,1505 @@
|
|
|
1
|
+
# WebSocket API
|
|
2
|
+
|
|
3
|
+
Type-safe WebSocket API for real-time communication between frontend and backend. Handles both request-response (HTTP-style) and real-time event streaming patterns.
|
|
4
|
+
|
|
5
|
+
## 📚 Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [Architecture](#architecture)
|
|
10
|
+
- [Creating WebSocket Handlers](#creating-websocket-handlers)
|
|
11
|
+
- [Patterns](#patterns)
|
|
12
|
+
- [API Reference](#api-reference)
|
|
13
|
+
- [Examples](#examples)
|
|
14
|
+
- [Best Practices](#best-practices)
|
|
15
|
+
- [Troubleshooting](#troubleshooting)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
**What is WebSocket API?**
|
|
22
|
+
Type-safe WebSocket communication system with two distinct patterns: HTTP-style request-response and real-time event streaming.
|
|
23
|
+
|
|
24
|
+
**Features:**
|
|
25
|
+
- Type-safe with TypeScript
|
|
26
|
+
- Auto-validation with TypeBox
|
|
27
|
+
- Dual pattern support (HTTP + Events)
|
|
28
|
+
- Scope-based event filtering (user, project, global)
|
|
29
|
+
- Auto-wrap response format
|
|
30
|
+
- Router-based modular architecture
|
|
31
|
+
- **Consistent API**: `.emit()` for sending, `.on()` for listening (both frontend & backend)
|
|
32
|
+
|
|
33
|
+
**API Naming Consistency:**
|
|
34
|
+
```typescript
|
|
35
|
+
// Frontend
|
|
36
|
+
ws.emit('action', data) // Send action to server
|
|
37
|
+
ws.on('event', handler) // Listen to events from server
|
|
38
|
+
|
|
39
|
+
// Backend
|
|
40
|
+
.on('action', handler) // Listen to actions from client
|
|
41
|
+
.emit('event', schema) // Declare events to send to client
|
|
42
|
+
ws.emit.user() // Emit event to specific user
|
|
43
|
+
ws.emit.project() // Emit event to project room (current viewers)
|
|
44
|
+
ws.emit.projectMembers() // Emit event to all users who have the project
|
|
45
|
+
ws.emit.global() // Emit event to all users
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Why `.emit()` everywhere?**
|
|
49
|
+
- Consistent: "emit" always means "send something"
|
|
50
|
+
- Clear: "on" always means "listen to something"
|
|
51
|
+
- Symmetric: Frontend and backend use same terminology
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
### 1. Create a New Module
|
|
58
|
+
|
|
59
|
+
Create a new folder in `./` (e.g., `todo/`) and create handlers:
|
|
60
|
+
|
|
61
|
+
**File: `./todo/crud.ts`**
|
|
62
|
+
```typescript
|
|
63
|
+
import { t } from 'elysia';
|
|
64
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
65
|
+
|
|
66
|
+
export const crudHandler = createRouter()
|
|
67
|
+
// HTTP-style request-response
|
|
68
|
+
.http('todo:create', {
|
|
69
|
+
data: t.Object({
|
|
70
|
+
title: t.String(),
|
|
71
|
+
description: t.Optional(t.String())
|
|
72
|
+
}),
|
|
73
|
+
response: t.Object({
|
|
74
|
+
id: t.String(),
|
|
75
|
+
title: t.String(),
|
|
76
|
+
description: t.String(),
|
|
77
|
+
createdAt: t.String()
|
|
78
|
+
})
|
|
79
|
+
}, async ({ data }) => {
|
|
80
|
+
// Business logic
|
|
81
|
+
const todo = await createTodo(data);
|
|
82
|
+
return todo; // Direct return, no manual wrapping
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Real-time event listener
|
|
86
|
+
.on('todo:mark-complete', {
|
|
87
|
+
data: t.Object({
|
|
88
|
+
id: t.String()
|
|
89
|
+
})
|
|
90
|
+
}, async ({ data, conn }) => {
|
|
91
|
+
const todo = await markComplete(data.id);
|
|
92
|
+
|
|
93
|
+
// Emit event to all users in project
|
|
94
|
+
const projectId = ws.getProjectId(conn);
|
|
95
|
+
ws.emit.project(projectId, 'todo:completed', {
|
|
96
|
+
id: todo.id,
|
|
97
|
+
completedAt: new Date().toISOString()
|
|
98
|
+
});
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Event declarations (Server → Client)
|
|
102
|
+
.emit('todo:completed', t.Object({
|
|
103
|
+
id: t.String(),
|
|
104
|
+
completedAt: t.String()
|
|
105
|
+
}));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**File: `./todo/index.ts`**
|
|
109
|
+
```typescript
|
|
110
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
111
|
+
import { crudHandler } from './crud';
|
|
112
|
+
|
|
113
|
+
export const todoRouter = createRouter()
|
|
114
|
+
.merge(crudHandler);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 2. Register the Module
|
|
118
|
+
|
|
119
|
+
Add to `./index.ts`:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { todoRouter } from './todo';
|
|
123
|
+
|
|
124
|
+
export const wsRouter = createRouter()
|
|
125
|
+
// ... existing routers
|
|
126
|
+
.merge(todoRouter);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Use in Frontend
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import ws from '$lib/utils/ws';
|
|
133
|
+
|
|
134
|
+
// HTTP-style request-response
|
|
135
|
+
try {
|
|
136
|
+
const todo = await ws.http('todo:create', {
|
|
137
|
+
title: 'Buy milk',
|
|
138
|
+
description: 'Get 2 liters'
|
|
139
|
+
});
|
|
140
|
+
console.log('Created:', todo.id); // Type-safe access
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Failed:', error.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Real-time event listener
|
|
146
|
+
const cleanup = ws.on('todo:completed', (data) => {
|
|
147
|
+
console.log('Todo completed:', data.id);
|
|
148
|
+
updateUI(data);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Emit action (no response expected)
|
|
152
|
+
ws.emit('todo:mark-complete', { id: 'todo-123' });
|
|
153
|
+
|
|
154
|
+
// Cleanup when component unmounts
|
|
155
|
+
cleanup();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 4. Done!
|
|
159
|
+
|
|
160
|
+
HTTP endpoint available as: `todo:create`
|
|
161
|
+
Event listener available as: `todo:mark-complete` (frontend emits this)
|
|
162
|
+
Event emission available as: `todo:completed` (backend emits this)
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Architecture
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
backend/ws/
|
|
170
|
+
├── index.ts # Main router - merges all module routers
|
|
171
|
+
├── types.ts # Shared types
|
|
172
|
+
├── chat/ # Chat module
|
|
173
|
+
│ ├── index.ts # Module router
|
|
174
|
+
│ ├── stream.ts # Streaming events
|
|
175
|
+
│ └── background.ts # Background processing
|
|
176
|
+
├── terminal/ # Terminal module
|
|
177
|
+
│ ├── index.ts # Module router
|
|
178
|
+
│ ├── session.ts # Session management (HTTP)
|
|
179
|
+
│ ├── stream.ts # Real-time I/O (Events)
|
|
180
|
+
│ └── persistence.ts # Stream persistence
|
|
181
|
+
├── files/ # File operations module
|
|
182
|
+
│ ├── index.ts # Module router
|
|
183
|
+
│ ├── read.ts # Read operations (HTTP)
|
|
184
|
+
│ ├── write.ts # Write operations (HTTP)
|
|
185
|
+
│ └── search.ts # Search operations (HTTP)
|
|
186
|
+
├── preview/ # Browser preview module
|
|
187
|
+
│ └── browser/
|
|
188
|
+
│ ├── tab.ts # Tab management (HTTP)
|
|
189
|
+
│ ├── interact.ts # User interactions (Events)
|
|
190
|
+
│ ├── webcodecs.ts # Video streaming (Events)
|
|
191
|
+
│ └── ...
|
|
192
|
+
└── ... (other modules)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Module Organization
|
|
196
|
+
|
|
197
|
+
**Simple modules** (1-2 files):
|
|
198
|
+
```
|
|
199
|
+
module/
|
|
200
|
+
├── index.ts # All handlers here
|
|
201
|
+
└── ...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Complex modules** (multiple concerns):
|
|
205
|
+
```
|
|
206
|
+
module/
|
|
207
|
+
├── index.ts # Module router
|
|
208
|
+
├── crud.ts # CRUD operations (HTTP)
|
|
209
|
+
├── stream.ts # Real-time events
|
|
210
|
+
└── helpers.ts # Shared utilities
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Data Flow
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
1. Handler Definition (module/handler.ts)
|
|
217
|
+
└─> createRouter() with .http() or .on()
|
|
218
|
+
↓
|
|
219
|
+
2. Module Router (module/index.ts)
|
|
220
|
+
└─> Merge handlers into module router
|
|
221
|
+
↓
|
|
222
|
+
3. Main Router (index.ts)
|
|
223
|
+
└─> Merge all module routers
|
|
224
|
+
↓
|
|
225
|
+
4. Frontend (ws.http() or ws.on())
|
|
226
|
+
└─> Type-safe API calls
|
|
227
|
+
↓
|
|
228
|
+
5. Auto-validation & processing
|
|
229
|
+
↓
|
|
230
|
+
6. Response (HTTP) or Event emission
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Key Components
|
|
234
|
+
|
|
235
|
+
**`createRouter()`**
|
|
236
|
+
Factory function to create type-safe WebSocket routers.
|
|
237
|
+
|
|
238
|
+
**`.http(action, config, handler)`**
|
|
239
|
+
Define HTTP-style request-response endpoints.
|
|
240
|
+
|
|
241
|
+
**`.on(action, config, handler)`**
|
|
242
|
+
Define event listeners (Client → Server actions).
|
|
243
|
+
|
|
244
|
+
**`.emit(event, schema)`**
|
|
245
|
+
Declare events (Server → Client notifications).
|
|
246
|
+
|
|
247
|
+
**`ws.emit.user()` / `ws.emit.project()` / `ws.emit.projectMembers()` / `ws.emit.global()`**
|
|
248
|
+
Emit events with scope-based filtering.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Creating WebSocket Handlers
|
|
253
|
+
|
|
254
|
+
### File Structure
|
|
255
|
+
|
|
256
|
+
Each module should be in its own folder under `./`:
|
|
257
|
+
|
|
258
|
+
1. **Create a folder**: `./your-module/`
|
|
259
|
+
2. **Create handler files**: Split by concern (crud.ts, stream.ts, etc.)
|
|
260
|
+
3. **Create index.ts**: Module router that merges handlers
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
```
|
|
264
|
+
your-module/
|
|
265
|
+
├── index.ts # Module router
|
|
266
|
+
├── crud.ts # CRUD operations
|
|
267
|
+
└── stream.ts # Real-time events
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Handler Types
|
|
271
|
+
|
|
272
|
+
There are three types of handlers:
|
|
273
|
+
|
|
274
|
+
#### 1. HTTP-style Request-Response (`.http()`)
|
|
275
|
+
|
|
276
|
+
Use when you need to get data back from the server.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
.http('namespace:action', {
|
|
280
|
+
data: t.Object({
|
|
281
|
+
param: t.String()
|
|
282
|
+
}),
|
|
283
|
+
response: t.Object({
|
|
284
|
+
field1: t.String(),
|
|
285
|
+
field2: t.Number()
|
|
286
|
+
})
|
|
287
|
+
}, async ({ data }) => {
|
|
288
|
+
// Business logic
|
|
289
|
+
const result = await doSomething(data.param);
|
|
290
|
+
return result; // Direct return
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Rules:**
|
|
295
|
+
- Response schema ONLY contains data fields
|
|
296
|
+
- NO `success`, `error` fields in schema
|
|
297
|
+
- Handler returns data directly
|
|
298
|
+
- Handler throws error if failed
|
|
299
|
+
- Server auto-wraps with `{ success, data?, error? }`
|
|
300
|
+
|
|
301
|
+
#### 2. Event Listener (`.on()`)
|
|
302
|
+
|
|
303
|
+
Use when client sends an action without expecting a response.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
.on('namespace:action', {
|
|
307
|
+
data: t.Object({
|
|
308
|
+
param: t.String()
|
|
309
|
+
})
|
|
310
|
+
}, async ({ data, conn }) => {
|
|
311
|
+
// Process action
|
|
312
|
+
await doSomething(data.param);
|
|
313
|
+
|
|
314
|
+
// Optionally emit events
|
|
315
|
+
const projectId = ws.getProjectId(conn);
|
|
316
|
+
ws.emit.project(projectId, 'namespace:updated', {
|
|
317
|
+
message: 'Update complete'
|
|
318
|
+
});
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Rules:**
|
|
323
|
+
- Use for actions that don't need response
|
|
324
|
+
- Can emit events to notify clients
|
|
325
|
+
- Access WebSocket connection via `conn`
|
|
326
|
+
- Get identity from connection context: `ws.getProjectId(conn)`, `ws.getUserId(conn)`
|
|
327
|
+
|
|
328
|
+
#### 3. Event Declaration (`.emit()`)
|
|
329
|
+
|
|
330
|
+
Declare events that server can emit to clients.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
.emit('namespace:event', t.Object({
|
|
334
|
+
field1: t.String(),
|
|
335
|
+
field2: t.Number()
|
|
336
|
+
}))
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Rules:**
|
|
340
|
+
- Direct schema (NOT wrapped)
|
|
341
|
+
- Declare in ALL files that emit this event
|
|
342
|
+
- Match actual emission in handlers
|
|
343
|
+
|
|
344
|
+
### Input Schema (TypeBox)
|
|
345
|
+
|
|
346
|
+
Define schema as TypeBox objects:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
data: t.Object({
|
|
350
|
+
// Required string
|
|
351
|
+
name: t.String(),
|
|
352
|
+
|
|
353
|
+
// Required number with constraints
|
|
354
|
+
age: t.Number({ minimum: 0, maximum: 150 }),
|
|
355
|
+
|
|
356
|
+
// Optional with default
|
|
357
|
+
format: t.Optional(t.String()),
|
|
358
|
+
|
|
359
|
+
// Literal union (enum-like)
|
|
360
|
+
type: t.Union([
|
|
361
|
+
t.Literal('file'),
|
|
362
|
+
t.Literal('directory')
|
|
363
|
+
]),
|
|
364
|
+
|
|
365
|
+
// Array
|
|
366
|
+
tags: t.Array(t.String()),
|
|
367
|
+
|
|
368
|
+
// Nested object
|
|
369
|
+
metadata: t.Object({
|
|
370
|
+
author: t.String(),
|
|
371
|
+
date: t.String()
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Handler Function
|
|
377
|
+
|
|
378
|
+
The handler receives validated arguments:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// HTTP handler
|
|
382
|
+
async ({ data }) => {
|
|
383
|
+
try {
|
|
384
|
+
const result = await someOperation(data);
|
|
385
|
+
return result; // Direct return
|
|
386
|
+
} catch (error) {
|
|
387
|
+
throw new Error('Operation failed'); // Throw on error
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Event handler
|
|
392
|
+
async ({ data, conn }) => {
|
|
393
|
+
const projectId = ws.getProjectId(conn);
|
|
394
|
+
|
|
395
|
+
// Process action
|
|
396
|
+
await doSomething(data);
|
|
397
|
+
|
|
398
|
+
// Emit events
|
|
399
|
+
ws.emit.project(projectId, 'namespace:event', {
|
|
400
|
+
message: 'Done'
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Return Format
|
|
406
|
+
|
|
407
|
+
**HTTP handlers:**
|
|
408
|
+
```typescript
|
|
409
|
+
// Success: return data directly
|
|
410
|
+
return { field1: 'value', field2: 123 };
|
|
411
|
+
|
|
412
|
+
// Error: throw Error
|
|
413
|
+
throw new Error('Something went wrong');
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Event handlers:**
|
|
417
|
+
```typescript
|
|
418
|
+
// No return value needed
|
|
419
|
+
// Use ws.emit() to notify clients
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Patterns
|
|
425
|
+
|
|
426
|
+
### Pattern 1: HTTP Request-Response
|
|
427
|
+
|
|
428
|
+
**Backend:**
|
|
429
|
+
```typescript
|
|
430
|
+
.http('files:read-file', {
|
|
431
|
+
data: t.Object({
|
|
432
|
+
file_path: t.String()
|
|
433
|
+
}),
|
|
434
|
+
response: t.Object({
|
|
435
|
+
content: t.String(),
|
|
436
|
+
size: t.Number(),
|
|
437
|
+
modified: t.String()
|
|
438
|
+
})
|
|
439
|
+
}, async ({ data }) => {
|
|
440
|
+
const result = await readFile(data.file_path);
|
|
441
|
+
return result;
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Frontend:**
|
|
446
|
+
```typescript
|
|
447
|
+
try {
|
|
448
|
+
const file = await ws.http('files:read-file', {
|
|
449
|
+
file_path: '/path/to/file.txt'
|
|
450
|
+
});
|
|
451
|
+
console.log('Content:', file.content); // Type-safe
|
|
452
|
+
console.log('Size:', file.size);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error('Failed:', error.message);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Use when:**
|
|
459
|
+
- Client needs immediate response
|
|
460
|
+
- One-time data query
|
|
461
|
+
- Request-response pattern
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### Pattern 2: Real-Time Events
|
|
466
|
+
|
|
467
|
+
**Backend:**
|
|
468
|
+
```typescript
|
|
469
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
470
|
+
|
|
471
|
+
.on('terminal:input', {
|
|
472
|
+
data: t.Object({
|
|
473
|
+
sessionId: t.String(),
|
|
474
|
+
content: t.String()
|
|
475
|
+
})
|
|
476
|
+
}, async ({ data, conn }) => {
|
|
477
|
+
// Process input
|
|
478
|
+
pty.write(data.content);
|
|
479
|
+
|
|
480
|
+
// Emit output to project
|
|
481
|
+
const projectId = ws.getProjectId(conn);
|
|
482
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
483
|
+
sessionId: data.sessionId,
|
|
484
|
+
content: data.content,
|
|
485
|
+
timestamp: new Date().toISOString()
|
|
486
|
+
});
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
.emit('terminal:output', t.Object({
|
|
490
|
+
sessionId: t.String(),
|
|
491
|
+
content: t.String(),
|
|
492
|
+
timestamp: t.String()
|
|
493
|
+
}))
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Frontend:**
|
|
497
|
+
```typescript
|
|
498
|
+
// Listen to events
|
|
499
|
+
const cleanup = ws.on('terminal:output', (data) => {
|
|
500
|
+
console.log('Output:', data.content);
|
|
501
|
+
terminal.write(data.content);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Emit action
|
|
505
|
+
ws.emit('terminal:input', {
|
|
506
|
+
sessionId: 'term-123',
|
|
507
|
+
content: 'ls -la\n'
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Cleanup
|
|
511
|
+
cleanup();
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Use when:**
|
|
515
|
+
- Server pushes updates to client
|
|
516
|
+
- Real-time streaming data
|
|
517
|
+
- Multiple clients need same update
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### Pattern 3: Scope-Based Event Filtering
|
|
522
|
+
|
|
523
|
+
Events can be scoped to specific users, projects, or broadcast globally.
|
|
524
|
+
|
|
525
|
+
**User-scoped (only sender receives):**
|
|
526
|
+
```typescript
|
|
527
|
+
ws.emit.user(userId, 'browser:interacted', {
|
|
528
|
+
action: 'click',
|
|
529
|
+
message: 'Success'
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Project-scoped (all users in same project):**
|
|
534
|
+
```typescript
|
|
535
|
+
const projectId = ws.getProjectId(conn);
|
|
536
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
537
|
+
sessionId: 'term-123',
|
|
538
|
+
content: 'Hello World\n',
|
|
539
|
+
timestamp: new Date().toISOString()
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Global broadcast (all connected users):**
|
|
544
|
+
```typescript
|
|
545
|
+
ws.emit.global('system:update', {
|
|
546
|
+
version: '2.0.0',
|
|
547
|
+
message: 'New version available'
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Critical: Room-Based Architecture**
|
|
552
|
+
|
|
553
|
+
Each connection can only be in **one project room at a time**. When a user switches from Project A to Project B, `ws.setProject()` removes their connection from Project A's room and adds it to Project B's room. This means:
|
|
554
|
+
|
|
555
|
+
- `ws.emit.project(projectA, ...)` will **NOT** reach users who switched away from Project A
|
|
556
|
+
- `ws.emit.projectMembers(projectA, ...)` reaches ALL users who have ever joined Project A, even if they switched to another project
|
|
557
|
+
- `ws.emit.user(userId, ...)` reaches the user regardless of which project room they're in
|
|
558
|
+
- `ws.emit.global(...)` reaches all connected clients regardless of project room
|
|
559
|
+
|
|
560
|
+
**4 Emit Scopes:**
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
ws.emit.user(userId, event, payload) // → specific user (all their connections)
|
|
564
|
+
ws.emit.project(projectId, event, payload) // → connections currently in the project room
|
|
565
|
+
ws.emit.projectMembers(projectId, event, payload) // → all users who have the project (cross-project)
|
|
566
|
+
ws.emit.global(event, payload) // → all connected clients
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**How `projectMembers` tracking works:**
|
|
570
|
+
- When `setProject()` or `setUser()` is called, the userId is recorded in a `projectMembers` Map for that project
|
|
571
|
+
- This membership **persists** even after the user switches to a different project
|
|
572
|
+
- `emit.projectMembers()` iterates over all member userIds, then sends to all their connections via `userConnections`
|
|
573
|
+
|
|
574
|
+
**Scope Decision Guide:**
|
|
575
|
+
|
|
576
|
+
| Event Type | Scope | Who Receives |
|
|
577
|
+
|------------|-------|--------------|
|
|
578
|
+
| User interaction feedback | `user` | Only the user who triggered action |
|
|
579
|
+
| Terminal output | `project` | Users **currently viewing** same project |
|
|
580
|
+
| File changes | `project` | Users **currently viewing** same project |
|
|
581
|
+
| Chat messages | `project` | Users **currently viewing** same project |
|
|
582
|
+
| Stream finished notification | `projectMembers` | All users who have the project (any active project) |
|
|
583
|
+
| System notifications | `global` | All connected users |
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
### Pattern 4: External Event Emission
|
|
588
|
+
|
|
589
|
+
Emit events from external sources (PTY, file watchers, etc.):
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
593
|
+
|
|
594
|
+
// PTY output
|
|
595
|
+
pty.onData((output) => {
|
|
596
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
597
|
+
sessionId,
|
|
598
|
+
content: output,
|
|
599
|
+
timestamp: new Date().toISOString()
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// File watcher
|
|
604
|
+
watcher.on('change', (path) => {
|
|
605
|
+
ws.emit.project(projectId, 'files:changed', {
|
|
606
|
+
path,
|
|
607
|
+
event: 'change',
|
|
608
|
+
timestamp: new Date().toISOString()
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Browser screencast
|
|
613
|
+
browser.on('screencast-frame', (frame) => {
|
|
614
|
+
ws.emit.project(projectId, 'browser:frame', {
|
|
615
|
+
sessionId: frame.sessionId,
|
|
616
|
+
frameId: String(frame.frameId),
|
|
617
|
+
timestamp: frame.timestamp,
|
|
618
|
+
data: frame.data // Uint8Array
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Rules:**
|
|
624
|
+
- Import `ws` singleton
|
|
625
|
+
- Use appropriate scope
|
|
626
|
+
- Capture userId/projectId in closure
|
|
627
|
+
- Declare events in router
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
### Pattern 5: User & Project Tracking
|
|
632
|
+
|
|
633
|
+
**Backend - Connection Setup:**
|
|
634
|
+
```typescript
|
|
635
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
636
|
+
|
|
637
|
+
app.ws('/ws', {
|
|
638
|
+
open(wsRaw) {
|
|
639
|
+
const conn = wsRaw as WSConnection;
|
|
640
|
+
ws.register(conn);
|
|
641
|
+
|
|
642
|
+
// Set user from auth
|
|
643
|
+
const userId = getUserFromAuth(conn);
|
|
644
|
+
ws.setUser(conn, userId);
|
|
645
|
+
},
|
|
646
|
+
close(wsRaw) {
|
|
647
|
+
const conn = wsRaw as WSConnection;
|
|
648
|
+
ws.unregister(conn);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Backend - Connection Context (`ws:set-context`):**
|
|
654
|
+
|
|
655
|
+
The frontend automatically sets `userId` and `projectId` on the connection via `ws:set-context` before sending any events. This is handled by the WSClient's `syncContext()` method.
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// Frontend sets context (handled automatically by WSClient)
|
|
659
|
+
ws.setUser('user-123'); // Calls ws:set-context { userId }
|
|
660
|
+
ws.setProject('proj-456'); // Calls ws:set-context { projectId }
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Backend - Reading Context in Handlers:**
|
|
664
|
+
```typescript
|
|
665
|
+
// In any handler, get identity from connection (single source of truth)
|
|
666
|
+
async ({ data, conn }) => {
|
|
667
|
+
const projectId = ws.getProjectId(conn); // Throws if not set
|
|
668
|
+
const userId = ws.getUserId(conn); // Throws if not set
|
|
669
|
+
|
|
670
|
+
// Use for business logic and event emission
|
|
671
|
+
ws.emit.project(projectId, 'namespace:event', { ... });
|
|
672
|
+
ws.emit.user(userId, 'namespace:feedback', { ... });
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## API Reference
|
|
679
|
+
|
|
680
|
+
### Main Router
|
|
681
|
+
|
|
682
|
+
**File: `./index.ts`**
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
export const wsRouter = createRouter()
|
|
686
|
+
.merge(chatRouter)
|
|
687
|
+
.merge(terminalRouter)
|
|
688
|
+
.merge(filesRouter)
|
|
689
|
+
// ... other modules
|
|
690
|
+
|
|
691
|
+
export type WSAPI = typeof wsRouter['$api'];
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Router Methods
|
|
695
|
+
|
|
696
|
+
#### `.http(action, config, handler)`
|
|
697
|
+
|
|
698
|
+
Define HTTP-style endpoint.
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
.http('namespace:action', {
|
|
702
|
+
data: t.Object({ ... }),
|
|
703
|
+
response: t.Object({ ... })
|
|
704
|
+
}, async ({ data }) => {
|
|
705
|
+
return result;
|
|
706
|
+
})
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Parameters:**
|
|
710
|
+
- `action` - Action name (format: `namespace:action`)
|
|
711
|
+
- `config.data` - Input schema (TypeBox)
|
|
712
|
+
- `config.response` - Response schema (TypeBox, data only)
|
|
713
|
+
- `handler` - Async function that returns data or throws error
|
|
714
|
+
|
|
715
|
+
#### `.on(action, config, handler)`
|
|
716
|
+
|
|
717
|
+
Define event listener.
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
.on('namespace:action', {
|
|
721
|
+
data: t.Object({ ... })
|
|
722
|
+
}, async ({ data, conn }) => {
|
|
723
|
+
// Process action
|
|
724
|
+
})
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
**Parameters:**
|
|
728
|
+
- `action` - Action name (format: `namespace:action`)
|
|
729
|
+
- `config.data` - Input schema (TypeBox)
|
|
730
|
+
- `handler` - Async function with `data` and `conn` params
|
|
731
|
+
|
|
732
|
+
#### `.emit(event, schema)`
|
|
733
|
+
|
|
734
|
+
Declare event.
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
.emit('namespace:event', t.Object({ ... }))
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Parameters:**
|
|
741
|
+
- `event` - Event name (format: `namespace:event`)
|
|
742
|
+
- `schema` - Event data schema (TypeBox, direct)
|
|
743
|
+
|
|
744
|
+
#### `.merge(router)`
|
|
745
|
+
|
|
746
|
+
Merge another router.
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
.merge(otherRouter)
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### WebSocket Singleton API
|
|
753
|
+
|
|
754
|
+
**File: `backend/lib/utils/ws.ts`**
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
758
|
+
|
|
759
|
+
// Emit events
|
|
760
|
+
ws.emit.user(userId, 'event', payload)
|
|
761
|
+
ws.emit.project(projectId, 'event', payload)
|
|
762
|
+
ws.emit.projectMembers(projectId, 'event', payload)
|
|
763
|
+
ws.emit.global('event', payload)
|
|
764
|
+
|
|
765
|
+
// Connection context (single source of truth - throws if not set)
|
|
766
|
+
ws.getProjectId(conn) // Returns string, throws if not set
|
|
767
|
+
ws.getUserId(conn) // Returns string, throws if not set
|
|
768
|
+
|
|
769
|
+
// Context setters (used internally by ws:set-context)
|
|
770
|
+
ws.setUser(conn, userId)
|
|
771
|
+
ws.setProject(conn, projectId)
|
|
772
|
+
|
|
773
|
+
// Connection management
|
|
774
|
+
ws.register(conn)
|
|
775
|
+
ws.unregister(conn)
|
|
776
|
+
ws.addCleanup(conn, fn)
|
|
777
|
+
ws.removeCleanup(conn, fn)
|
|
778
|
+
ws.getConnections(projectId?)
|
|
779
|
+
ws.getConnectionCount(projectId?)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Frontend Client API
|
|
783
|
+
|
|
784
|
+
**File: `frontend/lib/utils/ws.ts`**
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
import ws from '$lib/utils/ws';
|
|
788
|
+
|
|
789
|
+
// HTTP request
|
|
790
|
+
const data = await ws.http('namespace:action', { ... });
|
|
791
|
+
|
|
792
|
+
// Emit action (no response)
|
|
793
|
+
ws.emit('namespace:action', { ... });
|
|
794
|
+
|
|
795
|
+
// Listen to events
|
|
796
|
+
const cleanup = ws.on('namespace:event', (data) => {
|
|
797
|
+
console.log(data);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Cleanup
|
|
801
|
+
cleanup();
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## Examples
|
|
807
|
+
|
|
808
|
+
### Example 1: Simple CRUD (Files Module)
|
|
809
|
+
|
|
810
|
+
**Backend: `./files/read.ts`**
|
|
811
|
+
```typescript
|
|
812
|
+
import { t } from 'elysia';
|
|
813
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
814
|
+
|
|
815
|
+
export const readHandler = createRouter()
|
|
816
|
+
.http('files:read-file', {
|
|
817
|
+
data: t.Object({
|
|
818
|
+
file_path: t.String()
|
|
819
|
+
}),
|
|
820
|
+
response: t.Object({
|
|
821
|
+
content: t.String(),
|
|
822
|
+
size: t.Number(),
|
|
823
|
+
modified: t.String()
|
|
824
|
+
})
|
|
825
|
+
}, async ({ data }) => {
|
|
826
|
+
const result = await readFile(data.file_path);
|
|
827
|
+
return result;
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
.http('files:list-tree', {
|
|
831
|
+
data: t.Object({
|
|
832
|
+
project_path: t.String(),
|
|
833
|
+
expanded: t.Optional(t.String())
|
|
834
|
+
}),
|
|
835
|
+
response: t.Recursive((Self) => t.Union([
|
|
836
|
+
t.Object({ type: t.Literal('file'), ... }),
|
|
837
|
+
t.Object({ type: t.Literal('directory'), ... })
|
|
838
|
+
]))
|
|
839
|
+
}, async ({ data }) => {
|
|
840
|
+
const tree = await buildFileTree(data.project_path);
|
|
841
|
+
return tree;
|
|
842
|
+
});
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Frontend:**
|
|
846
|
+
```typescript
|
|
847
|
+
// Read file
|
|
848
|
+
const file = await ws.http('files:read-file', {
|
|
849
|
+
file_path: '/path/to/file.txt'
|
|
850
|
+
});
|
|
851
|
+
console.log(file.content);
|
|
852
|
+
|
|
853
|
+
// List tree
|
|
854
|
+
const tree = await ws.http('files:list-tree', {
|
|
855
|
+
project_path: '/path/to/project'
|
|
856
|
+
});
|
|
857
|
+
renderTree(tree);
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
### Example 2: Real-Time Streaming (Terminal Module)
|
|
863
|
+
|
|
864
|
+
**Backend: `./terminal/stream.ts`**
|
|
865
|
+
```typescript
|
|
866
|
+
import { t } from 'elysia';
|
|
867
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
868
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
869
|
+
|
|
870
|
+
export const streamHandler = createRouter()
|
|
871
|
+
.on('terminal:input', {
|
|
872
|
+
data: t.Object({
|
|
873
|
+
sessionId: t.String(),
|
|
874
|
+
data: t.Any()
|
|
875
|
+
})
|
|
876
|
+
}, async ({ data, conn }) => {
|
|
877
|
+
const projectId = ws.getProjectId(conn);
|
|
878
|
+
|
|
879
|
+
// Write to PTY
|
|
880
|
+
ptySessionManager.write(data.sessionId, data.data);
|
|
881
|
+
|
|
882
|
+
// PTY will emit output via external event
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
.emit('terminal:output', t.Object({
|
|
886
|
+
sessionId: t.String(),
|
|
887
|
+
content: t.String(),
|
|
888
|
+
timestamp: t.String()
|
|
889
|
+
}))
|
|
890
|
+
|
|
891
|
+
.emit('terminal:exit', t.Object({
|
|
892
|
+
sessionId: t.String(),
|
|
893
|
+
exitCode: t.Number()
|
|
894
|
+
}));
|
|
895
|
+
|
|
896
|
+
// External event emission (in PTY manager)
|
|
897
|
+
pty.onData((output) => {
|
|
898
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
899
|
+
sessionId,
|
|
900
|
+
content: output,
|
|
901
|
+
timestamp: new Date().toISOString()
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
pty.onExit((exitCode) => {
|
|
906
|
+
ws.emit.project(projectId, 'terminal:exit', {
|
|
907
|
+
sessionId,
|
|
908
|
+
exitCode
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**Frontend:**
|
|
914
|
+
```typescript
|
|
915
|
+
// Listen to output
|
|
916
|
+
const cleanupOutput = ws.on('terminal:output', (data) => {
|
|
917
|
+
terminal.write(data.content);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const cleanupExit = ws.on('terminal:exit', (data) => {
|
|
921
|
+
console.log('Exited with code:', data.exitCode);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Emit input
|
|
925
|
+
ws.emit('terminal:input', {
|
|
926
|
+
sessionId: 'term-123',
|
|
927
|
+
data: 'ls -la\n'
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Cleanup
|
|
931
|
+
onDestroy(() => {
|
|
932
|
+
cleanupOutput();
|
|
933
|
+
cleanupExit();
|
|
934
|
+
});
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
### Example 3: User Interaction Feedback (Browser Module)
|
|
940
|
+
|
|
941
|
+
**Backend: `./preview/browser/interact.ts`**
|
|
942
|
+
```typescript
|
|
943
|
+
import { t } from 'elysia';
|
|
944
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
945
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
946
|
+
|
|
947
|
+
export const interactHandler = createRouter()
|
|
948
|
+
.on('browser:interact', {
|
|
949
|
+
data: t.Object({
|
|
950
|
+
sessionId: t.String(),
|
|
951
|
+
action: t.Object({
|
|
952
|
+
type: t.Union([
|
|
953
|
+
t.Literal('click'),
|
|
954
|
+
t.Literal('type'),
|
|
955
|
+
t.Literal('scroll')
|
|
956
|
+
]),
|
|
957
|
+
x: t.Optional(t.Number()),
|
|
958
|
+
y: t.Optional(t.Number()),
|
|
959
|
+
text: t.Optional(t.String())
|
|
960
|
+
})
|
|
961
|
+
})
|
|
962
|
+
}, async ({ data, conn }) => {
|
|
963
|
+
const userId = ws.getUserId(conn);
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
// Perform interaction
|
|
967
|
+
const session = browserService.getSession(data.sessionId);
|
|
968
|
+
|
|
969
|
+
if (data.action.type === 'click') {
|
|
970
|
+
await session.page.mouse.click(data.action.x, data.action.y);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// User-scoped feedback
|
|
974
|
+
ws.emit.user(userId, 'browser:interacted', {
|
|
975
|
+
action: data.action.type,
|
|
976
|
+
message: 'Success'
|
|
977
|
+
});
|
|
978
|
+
} catch (error) {
|
|
979
|
+
ws.emit.user(userId, 'browser:error', {
|
|
980
|
+
message: error.message
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
.emit('browser:interacted', t.Object({
|
|
986
|
+
action: t.String(),
|
|
987
|
+
message: t.String()
|
|
988
|
+
}))
|
|
989
|
+
|
|
990
|
+
.emit('browser:error', t.Object({
|
|
991
|
+
message: t.String()
|
|
992
|
+
}));
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Frontend:**
|
|
996
|
+
```typescript
|
|
997
|
+
// Listen to feedback
|
|
998
|
+
const cleanup = ws.on('browser:interacted', (data) => {
|
|
999
|
+
showNotification(`${data.action}: ${data.message}`);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// Emit interaction
|
|
1003
|
+
ws.emit('browser:interact', {
|
|
1004
|
+
sessionId: 'browser-123',
|
|
1005
|
+
action: {
|
|
1006
|
+
type: 'click',
|
|
1007
|
+
x: 100,
|
|
1008
|
+
y: 200
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
---
|
|
1014
|
+
|
|
1015
|
+
## Best Practices
|
|
1016
|
+
|
|
1017
|
+
### 1. One Endpoint = One Purpose
|
|
1018
|
+
|
|
1019
|
+
**DO:**
|
|
1020
|
+
```typescript
|
|
1021
|
+
.http('files:write-file', { ... })
|
|
1022
|
+
.http('files:create-file', { ... })
|
|
1023
|
+
.http('files:delete', { ... })
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
**DON'T:**
|
|
1027
|
+
```typescript
|
|
1028
|
+
.http('files:operation', {
|
|
1029
|
+
data: t.Object({
|
|
1030
|
+
action: t.Union([
|
|
1031
|
+
t.Literal('write'),
|
|
1032
|
+
t.Literal('create'),
|
|
1033
|
+
t.Literal('delete')
|
|
1034
|
+
]),
|
|
1035
|
+
// ...
|
|
1036
|
+
})
|
|
1037
|
+
}, async ({ data }) => {
|
|
1038
|
+
switch (data.action) { // ❌ Branching logic
|
|
1039
|
+
case 'write': ...
|
|
1040
|
+
case 'create': ...
|
|
1041
|
+
}
|
|
1042
|
+
})
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
**Why:**
|
|
1046
|
+
- Better type safety
|
|
1047
|
+
- Clearer API contract
|
|
1048
|
+
- Easier to maintain
|
|
1049
|
+
- No switch/if logic
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
### 2. TypeBox Schemas Inline
|
|
1054
|
+
|
|
1055
|
+
**DO:**
|
|
1056
|
+
```typescript
|
|
1057
|
+
.http('files:read-file', {
|
|
1058
|
+
data: t.Object({
|
|
1059
|
+
file_path: t.String()
|
|
1060
|
+
}),
|
|
1061
|
+
response: t.Object({
|
|
1062
|
+
content: t.String(),
|
|
1063
|
+
size: t.Number()
|
|
1064
|
+
})
|
|
1065
|
+
}, async ({ data }) => { ... })
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
**DON'T:**
|
|
1069
|
+
```typescript
|
|
1070
|
+
const ReadFileResponse = t.Object({ ... });
|
|
1071
|
+
|
|
1072
|
+
.http('files:read-file', {
|
|
1073
|
+
response: ReadFileResponse
|
|
1074
|
+
}, ...)
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
**Why:**
|
|
1078
|
+
- Schema visible when reading code
|
|
1079
|
+
- No need to scroll to find definition
|
|
1080
|
+
- Easier to develop
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
### 3. Direct Return, No Manual Wrapping
|
|
1085
|
+
|
|
1086
|
+
**DO:**
|
|
1087
|
+
```typescript
|
|
1088
|
+
.http('files:read-file', {
|
|
1089
|
+
response: t.Object({
|
|
1090
|
+
content: t.String()
|
|
1091
|
+
})
|
|
1092
|
+
}, async ({ data }) => {
|
|
1093
|
+
const result = await readFile(data.file_path);
|
|
1094
|
+
return result; // ✅ Direct return
|
|
1095
|
+
})
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
**DON'T:**
|
|
1099
|
+
```typescript
|
|
1100
|
+
.http('files:read-file', {
|
|
1101
|
+
response: t.Object({
|
|
1102
|
+
success: t.Boolean(), // ❌ Manual wrapper
|
|
1103
|
+
data: t.Optional(t.Object({ ... })),
|
|
1104
|
+
error: t.Optional(t.String())
|
|
1105
|
+
})
|
|
1106
|
+
}, async ({ data }) => {
|
|
1107
|
+
try {
|
|
1108
|
+
return { success: true, data: result }; // ❌
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
return { success: false, error: error.message }; // ❌
|
|
1111
|
+
}
|
|
1112
|
+
})
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
**Why:**
|
|
1116
|
+
- Server auto-wraps response
|
|
1117
|
+
- Cleaner handler code
|
|
1118
|
+
- Consistent error handling
|
|
1119
|
+
|
|
1120
|
+
---
|
|
1121
|
+
|
|
1122
|
+
### 4. Event vs HTTP Decision
|
|
1123
|
+
|
|
1124
|
+
Choose the right pattern based on your use case:
|
|
1125
|
+
|
|
1126
|
+
| Use Case | Direction | Pattern | Example |
|
|
1127
|
+
|----------|-----------|---------|---------|
|
|
1128
|
+
| **Request-Response** | Client → Server | `.http()` | `files:read-file`, `browser:get-info` |
|
|
1129
|
+
| **Real-time Streaming** | Server → Client | Backend: `.emit()` declaration + emission<br>Frontend: `.on()` listener | `terminal:output`, `browser:frame` |
|
|
1130
|
+
| **Fire-and-Forget Action** | Client → Server | Frontend: `.emit()`<br>Backend: `.on()` | `terminal:input`, `browser:interact` |
|
|
1131
|
+
| **Action with Confirmation** | Client → Server → Client | Frontend: `.emit()` + `.on()`<br>Backend: `.on()` + `.emit()` | `browser:start-stream` → `browser:stream-started` |
|
|
1132
|
+
|
|
1133
|
+
**Key Differences:**
|
|
1134
|
+
|
|
1135
|
+
- **`.http()`**: Request needs immediate response (like REST API)
|
|
1136
|
+
- **`.emit()` + `.on()`**: One-way communication (fire-and-forget or push notifications)
|
|
1137
|
+
- **Combined**: Bidirectional flow (action triggers server event)
|
|
1138
|
+
|
|
1139
|
+
**Don't duplicate:**
|
|
1140
|
+
```typescript
|
|
1141
|
+
// ❌ WRONG - Same data available via two patterns
|
|
1142
|
+
.http('browser:get-frame', ...) // HTTP endpoint to fetch frame
|
|
1143
|
+
.emit('browser:frame', ...) // Real-time frame streaming
|
|
1144
|
+
// Choose ONE! If streaming exists, remove HTTP endpoint.
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
**When to use what:**
|
|
1148
|
+
- Use `.http()` when you need **immediate data back**
|
|
1149
|
+
- Use `.emit()` + `.on()` when you need **real-time updates**
|
|
1150
|
+
- Don't mix both for the same data
|
|
1151
|
+
|
|
1152
|
+
---
|
|
1153
|
+
|
|
1154
|
+
### 5. Declare Only Emitted Events
|
|
1155
|
+
|
|
1156
|
+
**DO:**
|
|
1157
|
+
```typescript
|
|
1158
|
+
// Declare events that are actually emitted
|
|
1159
|
+
.emit('terminal:output', ...) // ✅ Emitted in line 53
|
|
1160
|
+
.emit('terminal:exit', ...) // ✅ Emitted in line 68
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
**DON'T:**
|
|
1164
|
+
```typescript
|
|
1165
|
+
// Declare events that are never emitted
|
|
1166
|
+
.emit('terminal:resize', ...) // ❌ Never emitted
|
|
1167
|
+
.emit('terminal:test', ...) // ❌ Never emitted
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
**Why:**
|
|
1171
|
+
- Type safety matches reality
|
|
1172
|
+
- No dead code
|
|
1173
|
+
- Frontend doesn't listen to non-existent events
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
### 6. Security - Connection Context as Single Source of Truth
|
|
1178
|
+
|
|
1179
|
+
**DO:**
|
|
1180
|
+
```typescript
|
|
1181
|
+
.on('browser:interact', {
|
|
1182
|
+
data: t.Object({
|
|
1183
|
+
sessionId: t.String()
|
|
1184
|
+
// ✅ NO userId/projectId in payload - use connection context
|
|
1185
|
+
})
|
|
1186
|
+
}, async ({ data, conn }) => {
|
|
1187
|
+
const userId = ws.getUserId(conn); // ✅ From connection state
|
|
1188
|
+
const projectId = ws.getProjectId(conn); // ✅ From connection state
|
|
1189
|
+
ws.emit.user(userId, 'browser:interacted', { ... });
|
|
1190
|
+
})
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
**DON'T:**
|
|
1194
|
+
```typescript
|
|
1195
|
+
.on('browser:interact', {
|
|
1196
|
+
data: t.Object({
|
|
1197
|
+
sessionId: t.String(),
|
|
1198
|
+
userId: t.String(), // ❌ Client can fake
|
|
1199
|
+
projectId: t.String() // ❌ Client can fake
|
|
1200
|
+
})
|
|
1201
|
+
}, async ({ data }) => {
|
|
1202
|
+
ws.emit.user(data.userId, 'event', { ... }); // ❌ Using client-sent identity
|
|
1203
|
+
})
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
**Why:**
|
|
1207
|
+
- `ws.getProjectId()`/`ws.getUserId()` are the **single source of truth**
|
|
1208
|
+
- Identity comes from server-side connection state, not client payload
|
|
1209
|
+
- Cannot be faked by client
|
|
1210
|
+
- Throws if context not set (fail-fast, no silent empty strings)
|
|
1211
|
+
- Frontend sets context via `ws:set-context` before sending any events
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
### 7. Error Handling
|
|
1216
|
+
|
|
1217
|
+
**HTTP handlers - throw errors:**
|
|
1218
|
+
```typescript
|
|
1219
|
+
async ({ data }) => {
|
|
1220
|
+
if (!isValid(data)) {
|
|
1221
|
+
throw new Error('Validation failed');
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const result = await operation(data);
|
|
1225
|
+
return result;
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
**Event handlers - emit error events:**
|
|
1230
|
+
```typescript
|
|
1231
|
+
async ({ data, conn }) => {
|
|
1232
|
+
try {
|
|
1233
|
+
await operation(data);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
const userId = ws.getUserId(conn);
|
|
1236
|
+
ws.emit.user(userId, 'namespace:error', {
|
|
1237
|
+
message: error.message
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
---
|
|
1244
|
+
|
|
1245
|
+
### 8. Resource Cleanup
|
|
1246
|
+
|
|
1247
|
+
Use `ws.addCleanup()` to register cleanup functions that run automatically when the connection closes:
|
|
1248
|
+
|
|
1249
|
+
```typescript
|
|
1250
|
+
.on('terminal:create', {
|
|
1251
|
+
data: t.Object({ ... })
|
|
1252
|
+
}, async ({ data, conn }) => {
|
|
1253
|
+
const sessionId = await createSession(data);
|
|
1254
|
+
|
|
1255
|
+
// Register cleanup - runs automatically on disconnect
|
|
1256
|
+
ws.addCleanup(conn, () => {
|
|
1257
|
+
cleanupSession(sessionId);
|
|
1258
|
+
});
|
|
1259
|
+
})
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
---
|
|
1263
|
+
|
|
1264
|
+
### 9. TypeBox Type Constraints
|
|
1265
|
+
|
|
1266
|
+
**Use literal unions for enums:**
|
|
1267
|
+
```typescript
|
|
1268
|
+
type: t.Union([
|
|
1269
|
+
t.Literal('file'),
|
|
1270
|
+
t.Literal('directory'),
|
|
1271
|
+
t.Literal('drive')
|
|
1272
|
+
])
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
**Use string for free-form text:**
|
|
1276
|
+
```typescript
|
|
1277
|
+
description: t.String()
|
|
1278
|
+
content: t.String()
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
**Use constraints for validation:**
|
|
1282
|
+
```typescript
|
|
1283
|
+
age: t.Number({ minimum: 0, maximum: 150 })
|
|
1284
|
+
email: t.String({ format: 'email' })
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
---
|
|
1288
|
+
|
|
1289
|
+
## Troubleshooting
|
|
1290
|
+
|
|
1291
|
+
### Handler Not Working
|
|
1292
|
+
|
|
1293
|
+
**Problem:** WebSocket handler doesn't respond.
|
|
1294
|
+
|
|
1295
|
+
**Solutions:**
|
|
1296
|
+
1. Verify module router is imported in `./index.ts`
|
|
1297
|
+
2. Check handler is merged in module's `index.ts`
|
|
1298
|
+
3. Verify action name format: `namespace:action`
|
|
1299
|
+
4. Check console for TypeBox validation errors
|
|
1300
|
+
5. Run `bun run check` for TypeScript errors
|
|
1301
|
+
|
|
1302
|
+
---
|
|
1303
|
+
|
|
1304
|
+
### Type Errors
|
|
1305
|
+
|
|
1306
|
+
**Problem:** TypeScript errors in handlers.
|
|
1307
|
+
|
|
1308
|
+
**Solutions:**
|
|
1309
|
+
1. Ensure TypeBox schemas are inline in `.http()` / `.on()`
|
|
1310
|
+
2. Verify response schema has NO `success` or `error` fields
|
|
1311
|
+
3. Check handler returns data directly (not wrapped)
|
|
1312
|
+
4. Ensure `.emit()` uses direct schema (not wrapped)
|
|
1313
|
+
5. Run `bun run check` to see all errors
|
|
1314
|
+
|
|
1315
|
+
**Common mistakes:**
|
|
1316
|
+
```typescript
|
|
1317
|
+
// ❌ Wrong - wrapped schema
|
|
1318
|
+
.emit('event', { schema: t.Object({ ... }) })
|
|
1319
|
+
|
|
1320
|
+
// ✅ Correct - direct schema
|
|
1321
|
+
.emit('event', t.Object({ ... }))
|
|
1322
|
+
|
|
1323
|
+
// ❌ Wrong - manual wrapper in response
|
|
1324
|
+
response: t.Object({
|
|
1325
|
+
success: t.Boolean(),
|
|
1326
|
+
data: t.Object({ ... })
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
// ✅ Correct - data only
|
|
1330
|
+
response: t.Object({
|
|
1331
|
+
field1: t.String(),
|
|
1332
|
+
field2: t.Number()
|
|
1333
|
+
})
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
### Events Not Received
|
|
1339
|
+
|
|
1340
|
+
**Problem:** Frontend doesn't receive events.
|
|
1341
|
+
|
|
1342
|
+
**Check:**
|
|
1343
|
+
1. Event is declared with `.emit()` in router
|
|
1344
|
+
2. Event name matches between backend and frontend
|
|
1345
|
+
3. Event is actually emitted (search for `ws.emit`)
|
|
1346
|
+
4. Correct scope is used (`user`, `project`, `global`)
|
|
1347
|
+
5. User/project tracking is set up correctly
|
|
1348
|
+
6. Frontend listener is set up before event is emitted
|
|
1349
|
+
|
|
1350
|
+
**Debug:**
|
|
1351
|
+
```typescript
|
|
1352
|
+
// Backend - log emission
|
|
1353
|
+
ws.emit.project(projectId, 'event', payload);
|
|
1354
|
+
console.log('Emitted event to project:', projectId);
|
|
1355
|
+
|
|
1356
|
+
// Frontend - log reception
|
|
1357
|
+
ws.on('event', (data) => {
|
|
1358
|
+
console.log('Received event:', data);
|
|
1359
|
+
});
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
---
|
|
1363
|
+
|
|
1364
|
+
### Validation Errors
|
|
1365
|
+
|
|
1366
|
+
**Problem:** TypeBox validation fails.
|
|
1367
|
+
|
|
1368
|
+
**Solutions:**
|
|
1369
|
+
1. Check payload matches schema exactly
|
|
1370
|
+
2. Verify all required fields are present
|
|
1371
|
+
3. Check field types match (string vs number)
|
|
1372
|
+
4. Ensure literal unions use exact values
|
|
1373
|
+
5. Look at error message for specific field
|
|
1374
|
+
|
|
1375
|
+
**Example error:**
|
|
1376
|
+
```
|
|
1377
|
+
Expected type: { sessionId: string, content: string }
|
|
1378
|
+
Received: { sessionId: "term-123" }
|
|
1379
|
+
Error: Missing required property 'content'
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
### Scope Filtering Issues
|
|
1385
|
+
|
|
1386
|
+
**Problem:** Events received by wrong users/projects.
|
|
1387
|
+
|
|
1388
|
+
**Check:**
|
|
1389
|
+
1. Correct scope is used in `ws.emit()`
|
|
1390
|
+
2. User tracking is set up (`ws.setUser()`)
|
|
1391
|
+
3. Project tracking is set up (`ws.setProject()`)
|
|
1392
|
+
4. Connection context is passed to `ws.emit()`
|
|
1393
|
+
|
|
1394
|
+
**Debug:**
|
|
1395
|
+
```typescript
|
|
1396
|
+
// Check current user/project
|
|
1397
|
+
const userId = ws.getUserId(conn);
|
|
1398
|
+
const projectId = ws.getProjectId(conn);
|
|
1399
|
+
console.log('User:', userId, 'Project:', projectId);
|
|
1400
|
+
|
|
1401
|
+
// Emit with logging
|
|
1402
|
+
console.log('Emitting to project:', projectId);
|
|
1403
|
+
ws.emit.project(projectId, 'event', payload);
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
|
|
1408
|
+
## Module Audit Checklist
|
|
1409
|
+
|
|
1410
|
+
Use this checklist when creating or auditing modules:
|
|
1411
|
+
|
|
1412
|
+
### 1. Pattern Duplication
|
|
1413
|
+
- [ ] Check if data is available via HTTP AND Event
|
|
1414
|
+
- [ ] If streaming exists, remove HTTP endpoint for same data
|
|
1415
|
+
- [ ] If HTTP is sufficient, don't create Event for same data
|
|
1416
|
+
|
|
1417
|
+
### 2. Event Declaration Discipline
|
|
1418
|
+
- [ ] List all `.emit()` declarations
|
|
1419
|
+
- [ ] Search for actual `ws.emit()` calls
|
|
1420
|
+
- [ ] Remove declarations that are never emitted
|
|
1421
|
+
|
|
1422
|
+
### 3. Frontend Listener Audit
|
|
1423
|
+
- [ ] List all `ws.on()` listeners in frontend
|
|
1424
|
+
- [ ] Verify backend actually emits these events
|
|
1425
|
+
- [ ] Remove listeners for non-existent events
|
|
1426
|
+
|
|
1427
|
+
### 4. File Structure
|
|
1428
|
+
- [ ] Are there files with only 1 endpoint?
|
|
1429
|
+
- [ ] Can they be merged into other files?
|
|
1430
|
+
- [ ] Are there redundant files with streaming?
|
|
1431
|
+
|
|
1432
|
+
### 5. Scope Consistency
|
|
1433
|
+
- [ ] Same event uses same scope across files
|
|
1434
|
+
- [ ] User-specific events use `user` scope
|
|
1435
|
+
- [ ] Project-shared events use `project` scope
|
|
1436
|
+
|
|
1437
|
+
---
|
|
1438
|
+
|
|
1439
|
+
## Additional Resources
|
|
1440
|
+
|
|
1441
|
+
- [TypeBox Documentation](https://github.com/sinclairzx81/typebox)
|
|
1442
|
+
- [Elysia Documentation](https://elysiajs.com/)
|
|
1443
|
+
- [WebSocket API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
|
1444
|
+
|
|
1445
|
+
---
|
|
1446
|
+
|
|
1447
|
+
## Quick Reference
|
|
1448
|
+
|
|
1449
|
+
### Backend Handler Types
|
|
1450
|
+
|
|
1451
|
+
```typescript
|
|
1452
|
+
// HTTP request-response
|
|
1453
|
+
.http('namespace:action', {
|
|
1454
|
+
data: t.Object({ ... }),
|
|
1455
|
+
response: t.Object({ ... })
|
|
1456
|
+
}, async ({ data }) => {
|
|
1457
|
+
return result;
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
// Event listener
|
|
1461
|
+
.on('namespace:action', {
|
|
1462
|
+
data: t.Object({ ... })
|
|
1463
|
+
}, async ({ data, conn }) => {
|
|
1464
|
+
// Process action
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
// Event declaration
|
|
1468
|
+
.emit('namespace:event', t.Object({ ... }))
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
### Frontend API
|
|
1472
|
+
|
|
1473
|
+
```typescript
|
|
1474
|
+
// HTTP request
|
|
1475
|
+
const data = await ws.http('namespace:action', { ... });
|
|
1476
|
+
|
|
1477
|
+
// Emit action
|
|
1478
|
+
ws.emit('namespace:action', { ... });
|
|
1479
|
+
|
|
1480
|
+
// Listen to events
|
|
1481
|
+
const cleanup = ws.on('namespace:event', (data) => { ... });
|
|
1482
|
+
cleanup();
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
### Event Emission
|
|
1486
|
+
|
|
1487
|
+
```typescript
|
|
1488
|
+
import { ws } from '$backend/lib/utils/ws';
|
|
1489
|
+
|
|
1490
|
+
// User-specific (all connections of a user)
|
|
1491
|
+
ws.emit.user(userId, 'event', payload);
|
|
1492
|
+
|
|
1493
|
+
// Project room (connections currently viewing the project)
|
|
1494
|
+
ws.emit.project(projectId, 'event', payload);
|
|
1495
|
+
|
|
1496
|
+
// Project members (all users who have the project, even if viewing another project)
|
|
1497
|
+
ws.emit.projectMembers(projectId, 'event', payload);
|
|
1498
|
+
|
|
1499
|
+
// Global broadcast (all connections)
|
|
1500
|
+
ws.emit.global('event', payload);
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
---
|
|
1504
|
+
|
|
1505
|
+
**Happy coding! 🚀**
|