@nocturnium/svelte-ide 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/components/agents/AgentActivityPanel.svelte +565 -0
- package/dist/components/agents/AgentActivityPanel.svelte.d.ts +24 -0
- package/dist/components/agents/AgentAvatar.svelte +417 -0
- package/dist/components/agents/AgentAvatar.svelte.d.ts +23 -0
- package/dist/components/agents/AgentCursor.svelte +224 -0
- package/dist/components/agents/AgentCursor.svelte.d.ts +35 -0
- package/dist/components/agents/AgentPresenceBar.svelte +261 -0
- package/dist/components/agents/AgentPresenceBar.svelte.d.ts +20 -0
- package/dist/components/agents/index.d.ts +4 -0
- package/dist/components/agents/index.js +5 -0
- package/dist/components/ai/AIConversationList.svelte +524 -0
- package/dist/components/ai/AIConversationList.svelte.d.ts +17 -0
- package/dist/components/ai/AIEditPreview.svelte +132 -0
- package/dist/components/ai/AIEditPreview.svelte.d.ts +8 -0
- package/dist/components/ai/AIInlineEdit.svelte +155 -0
- package/dist/components/ai/AIInlineEdit.svelte.d.ts +10 -0
- package/dist/components/ai/AIMessage.svelte +239 -0
- package/dist/components/ai/AIMessage.svelte.d.ts +13 -0
- package/dist/components/ai/AIMessageActions.svelte +176 -0
- package/dist/components/ai/AIMessageActions.svelte.d.ts +12 -0
- package/dist/components/ai/AIMessageContent.svelte +355 -0
- package/dist/components/ai/AIMessageContent.svelte.d.ts +7 -0
- package/dist/components/ai/AIPanel.svelte +561 -0
- package/dist/components/ai/AIPanel.svelte.d.ts +7 -0
- package/dist/components/ai/AISuggestionWidget.svelte +132 -0
- package/dist/components/ai/AISuggestionWidget.svelte.d.ts +10 -0
- package/dist/components/ai/AIToolCallDisplay.svelte +317 -0
- package/dist/components/ai/AIToolCallDisplay.svelte.d.ts +12 -0
- package/dist/components/ai/index.d.ts +9 -0
- package/dist/components/ai/index.js +10 -0
- package/dist/components/core/Avatar.svelte +110 -0
- package/dist/components/core/Avatar.svelte.d.ts +12 -0
- package/dist/components/core/Badge.svelte +98 -0
- package/dist/components/core/Badge.svelte.d.ts +11 -0
- package/dist/components/core/Button.svelte +175 -0
- package/dist/components/core/Button.svelte.d.ts +18 -0
- package/dist/components/core/ConnectionStatus.svelte +294 -0
- package/dist/components/core/ConnectionStatus.svelte.d.ts +20 -0
- package/dist/components/core/ContextMenu.svelte +176 -0
- package/dist/components/core/ContextMenu.svelte.d.ts +19 -0
- package/dist/components/core/ErrorBoundary.svelte +277 -0
- package/dist/components/core/ErrorBoundary.svelte.d.ts +23 -0
- package/dist/components/core/Icon.svelte +107 -0
- package/dist/components/core/Icon.svelte.d.ts +8 -0
- package/dist/components/core/Input.svelte +138 -0
- package/dist/components/core/Input.svelte.d.ts +20 -0
- package/dist/components/core/Kbd.svelte +34 -0
- package/dist/components/core/Kbd.svelte.d.ts +7 -0
- package/dist/components/core/ResizeHandle.svelte +200 -0
- package/dist/components/core/ResizeHandle.svelte.d.ts +23 -0
- package/dist/components/core/Spinner.svelte +35 -0
- package/dist/components/core/Spinner.svelte.d.ts +7 -0
- package/dist/components/core/Textarea.svelte +112 -0
- package/dist/components/core/Textarea.svelte.d.ts +18 -0
- package/dist/components/core/Tooltip.svelte +103 -0
- package/dist/components/core/Tooltip.svelte.d.ts +11 -0
- package/dist/components/core/index.d.ts +13 -0
- package/dist/components/core/index.js +14 -0
- package/dist/components/editor/AIFocusLayer.svelte +430 -0
- package/dist/components/editor/AIFocusLayer.svelte.d.ts +32 -0
- package/dist/components/editor/Breadcrumbs.svelte +435 -0
- package/dist/components/editor/Breadcrumbs.svelte.d.ts +33 -0
- package/dist/components/editor/BreakpointLayer.svelte +642 -0
- package/dist/components/editor/BreakpointLayer.svelte.d.ts +20 -0
- package/dist/components/editor/CognitiveLoadMeter.svelte +324 -0
- package/dist/components/editor/CognitiveLoadMeter.svelte.d.ts +18 -0
- package/dist/components/editor/CollaborativeEditor.svelte +218 -0
- package/dist/components/editor/CollaborativeEditor.svelte.d.ts +32 -0
- package/dist/components/editor/CommandPalette.svelte +434 -0
- package/dist/components/editor/CommandPalette.svelte.d.ts +11 -0
- package/dist/components/editor/ComplexityLayer.svelte +293 -0
- package/dist/components/editor/ComplexityLayer.svelte.d.ts +23 -0
- package/dist/components/editor/ConflictZoneLayer.svelte +441 -0
- package/dist/components/editor/ConflictZoneLayer.svelte.d.ts +25 -0
- package/dist/components/editor/ContextLens.svelte +262 -0
- package/dist/components/editor/ContextLens.svelte.d.ts +27 -0
- package/dist/components/editor/CustomEditor.svelte +1242 -0
- package/dist/components/editor/CustomEditor.svelte.d.ts +37 -0
- package/dist/components/editor/DebugConsole.svelte +646 -0
- package/dist/components/editor/DebugConsole.svelte.d.ts +41 -0
- package/dist/components/editor/EchoCursorLayer.svelte +363 -0
- package/dist/components/editor/EchoCursorLayer.svelte.d.ts +24 -0
- package/dist/components/editor/Editor.svelte +61 -0
- package/dist/components/editor/Editor.svelte.d.ts +22 -0
- package/dist/components/editor/EditorGutter.svelte +119 -0
- package/dist/components/editor/EditorGutter.svelte.d.ts +19 -0
- package/dist/components/editor/EditorLines.svelte +182 -0
- package/dist/components/editor/EditorLines.svelte.d.ts +43 -0
- package/dist/components/editor/EditorPane.svelte +134 -0
- package/dist/components/editor/EditorPane.svelte.d.ts +9 -0
- package/dist/components/editor/EditorSelections.svelte +186 -0
- package/dist/components/editor/EditorSelections.svelte.d.ts +25 -0
- package/dist/components/editor/EditorTabs.svelte +170 -0
- package/dist/components/editor/EditorTabs.svelte.d.ts +12 -0
- package/dist/components/editor/FileExplorer.svelte +811 -0
- package/dist/components/editor/FileExplorer.svelte.d.ts +67 -0
- package/dist/components/editor/FileIcon.svelte +110 -0
- package/dist/components/editor/FileIcon.svelte.d.ts +10 -0
- package/dist/components/editor/FindReplace.svelte +448 -0
- package/dist/components/editor/FindReplace.svelte.d.ts +40 -0
- package/dist/components/editor/GhostBracketLayer.svelte +391 -0
- package/dist/components/editor/GhostBracketLayer.svelte.d.ts +24 -0
- package/dist/components/editor/GitBlameLayer.svelte +436 -0
- package/dist/components/editor/GitBlameLayer.svelte.d.ts +18 -0
- package/dist/components/editor/InlineDiagnosticsLayer.svelte +540 -0
- package/dist/components/editor/InlineDiagnosticsLayer.svelte.d.ts +35 -0
- package/dist/components/editor/InlineDiffLayer.svelte +337 -0
- package/dist/components/editor/InlineDiffLayer.svelte.d.ts +31 -0
- package/dist/components/editor/MinimalEditor.svelte +75 -0
- package/dist/components/editor/MinimalEditor.svelte.d.ts +6 -0
- package/dist/components/editor/MinimalEditor2.svelte +84 -0
- package/dist/components/editor/MinimalEditor2.svelte.d.ts +6 -0
- package/dist/components/editor/Minimap.svelte +327 -0
- package/dist/components/editor/Minimap.svelte.d.ts +34 -0
- package/dist/components/editor/PluginPreviewSandbox.svelte +793 -0
- package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +49 -0
- package/dist/components/editor/ProblemsPanel.svelte +628 -0
- package/dist/components/editor/ProblemsPanel.svelte.d.ts +25 -0
- package/dist/components/editor/QuickActionsMenu.svelte +403 -0
- package/dist/components/editor/QuickActionsMenu.svelte.d.ts +18 -0
- package/dist/components/editor/SnippetPalette.svelte +530 -0
- package/dist/components/editor/SnippetPalette.svelte.d.ts +16 -0
- package/dist/components/editor/StructureMap.svelte +431 -0
- package/dist/components/editor/StructureMap.svelte.d.ts +37 -0
- package/dist/components/editor/SymbolOutline.svelte +722 -0
- package/dist/components/editor/SymbolOutline.svelte.d.ts +44 -0
- package/dist/components/editor/TimelineScrubber.svelte +470 -0
- package/dist/components/editor/TimelineScrubber.svelte.d.ts +40 -0
- package/dist/components/editor/TokenRenderer.svelte +69 -0
- package/dist/components/editor/TokenRenderer.svelte.d.ts +15 -0
- package/dist/components/editor/constants.d.ts +32 -0
- package/dist/components/editor/constants.js +36 -0
- package/dist/components/editor/core/ai-awareness.d.ts +176 -0
- package/dist/components/editor/core/ai-awareness.js +210 -0
- package/dist/components/editor/core/bracket-healer.d.ts +189 -0
- package/dist/components/editor/core/bracket-healer.js +406 -0
- package/dist/components/editor/core/breakpoints.d.ts +203 -0
- package/dist/components/editor/core/breakpoints.js +414 -0
- package/dist/components/editor/core/commands.d.ts +108 -0
- package/dist/components/editor/core/commands.js +246 -0
- package/dist/components/editor/core/complexity-analyzer.d.ts +123 -0
- package/dist/components/editor/core/complexity-analyzer.js +376 -0
- package/dist/components/editor/core/conflict-predictor.d.ts +135 -0
- package/dist/components/editor/core/conflict-predictor.js +316 -0
- package/dist/components/editor/core/crdt-binding.d.ts +118 -0
- package/dist/components/editor/core/crdt-binding.js +286 -0
- package/dist/components/editor/core/diagnostics.d.ts +210 -0
- package/dist/components/editor/core/diagnostics.js +335 -0
- package/dist/components/editor/core/echo-cursor.d.ts +201 -0
- package/dist/components/editor/core/echo-cursor.js +267 -0
- package/dist/components/editor/core/folding.d.ts +124 -0
- package/dist/components/editor/core/folding.js +672 -0
- package/dist/components/editor/core/ghost-pair.d.ts +122 -0
- package/dist/components/editor/core/ghost-pair.js +221 -0
- package/dist/components/editor/core/git-blame.d.ts +170 -0
- package/dist/components/editor/core/git-blame.js +324 -0
- package/dist/components/editor/core/index.d.ts +26 -0
- package/dist/components/editor/core/index.js +24 -0
- package/dist/components/editor/core/keybindings.d.ts +79 -0
- package/dist/components/editor/core/keybindings.js +357 -0
- package/dist/components/editor/core/multi-cursor.d.ts +196 -0
- package/dist/components/editor/core/multi-cursor.js +521 -0
- package/dist/components/editor/core/navigation.d.ts +107 -0
- package/dist/components/editor/core/navigation.js +408 -0
- package/dist/components/editor/core/quick-actions.d.ts +189 -0
- package/dist/components/editor/core/quick-actions.js +427 -0
- package/dist/components/editor/core/search.d.ts +88 -0
- package/dist/components/editor/core/search.js +192 -0
- package/dist/components/editor/core/semantic-analyzer.d.ts +77 -0
- package/dist/components/editor/core/semantic-analyzer.js +424 -0
- package/dist/components/editor/core/snippet-manager.d.ts +202 -0
- package/dist/components/editor/core/snippet-manager.js +565 -0
- package/dist/components/editor/core/state.d.ts +367 -0
- package/dist/components/editor/core/state.js +900 -0
- package/dist/components/editor/core/timeline.d.ts +204 -0
- package/dist/components/editor/core/timeline.js +349 -0
- package/dist/components/editor/editor-find.d.ts +56 -0
- package/dist/components/editor/editor-find.js +148 -0
- package/dist/components/editor/editor-input.d.ts +77 -0
- package/dist/components/editor/editor-input.js +445 -0
- package/dist/components/editor/editor-multicursor.d.ts +21 -0
- package/dist/components/editor/editor-multicursor.js +196 -0
- package/dist/components/editor/editor-scroll.d.ts +14 -0
- package/dist/components/editor/editor-scroll.js +34 -0
- package/dist/components/editor/index.d.ts +15 -0
- package/dist/components/editor/index.js +21 -0
- package/dist/components/editor/languages.d.ts +62 -0
- package/dist/components/editor/languages.js +285 -0
- package/dist/components/editor/theme.d.ts +88 -0
- package/dist/components/editor/theme.js +139 -0
- package/dist/components/editor/tokenizer/base.d.ts +40 -0
- package/dist/components/editor/tokenizer/base.js +203 -0
- package/dist/components/editor/tokenizer/index.d.ts +56 -0
- package/dist/components/editor/tokenizer/index.js +215 -0
- package/dist/components/editor/tokenizer/languages/css.d.ts +17 -0
- package/dist/components/editor/tokenizer/languages/css.js +194 -0
- package/dist/components/editor/tokenizer/languages/go.d.ts +17 -0
- package/dist/components/editor/tokenizer/languages/go.js +220 -0
- package/dist/components/editor/tokenizer/languages/html.d.ts +24 -0
- package/dist/components/editor/tokenizer/languages/html.js +145 -0
- package/dist/components/editor/tokenizer/languages/javascript.d.ts +56 -0
- package/dist/components/editor/tokenizer/languages/javascript.js +452 -0
- package/dist/components/editor/tokenizer/languages/json.d.ts +12 -0
- package/dist/components/editor/tokenizer/languages/json.js +91 -0
- package/dist/components/editor/tokenizer/languages/markdown.d.ts +16 -0
- package/dist/components/editor/tokenizer/languages/markdown.js +156 -0
- package/dist/components/editor/tokenizer/languages/python.d.ts +20 -0
- package/dist/components/editor/tokenizer/languages/python.js +227 -0
- package/dist/components/editor/tokenizer/languages/svelte.d.ts +40 -0
- package/dist/components/editor/tokenizer/languages/svelte.js +326 -0
- package/dist/components/editor/tokenizer/types.d.ts +86 -0
- package/dist/components/editor/tokenizer/types.js +4 -0
- package/dist/components/layout/IDELayout.svelte +274 -0
- package/dist/components/layout/IDELayout.svelte.d.ts +29 -0
- package/dist/components/layout/StatusBar.svelte +511 -0
- package/dist/components/layout/StatusBar.svelte.d.ts +47 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +3 -0
- package/dist/components/lsp/AutocompleteWidget.svelte +364 -0
- package/dist/components/lsp/AutocompleteWidget.svelte.d.ts +33 -0
- package/dist/components/lsp/DiagnosticMarker.svelte +166 -0
- package/dist/components/lsp/DiagnosticMarker.svelte.d.ts +19 -0
- package/dist/components/lsp/DiagnosticsPanel.svelte +388 -0
- package/dist/components/lsp/DiagnosticsPanel.svelte.d.ts +21 -0
- package/dist/components/lsp/HoverTooltip.svelte +274 -0
- package/dist/components/lsp/HoverTooltip.svelte.d.ts +24 -0
- package/dist/components/lsp/LSPEditor.svelte +486 -0
- package/dist/components/lsp/LSPEditor.svelte.d.ts +39 -0
- package/dist/components/lsp/SignatureHelpWidget.svelte +216 -0
- package/dist/components/lsp/SignatureHelpWidget.svelte.d.ts +22 -0
- package/dist/components/lsp/index.d.ts +6 -0
- package/dist/components/lsp/index.js +7 -0
- package/dist/components/plugins/PluginCard.svelte +153 -0
- package/dist/components/plugins/PluginCard.svelte.d.ts +19 -0
- package/dist/components/plugins/PluginPanel.svelte +280 -0
- package/dist/components/plugins/PluginPanel.svelte.d.ts +8 -0
- package/dist/components/plugins/PluginProposalForm.svelte +250 -0
- package/dist/components/plugins/PluginProposalForm.svelte.d.ts +6 -0
- package/dist/components/plugins/PluginStatusBadge.svelte +14 -0
- package/dist/components/plugins/PluginStatusBadge.svelte.d.ts +8 -0
- package/dist/components/plugins/index.d.ts +4 -0
- package/dist/components/plugins/index.js +5 -0
- package/dist/components/vfs/LockConflictDialog.svelte +705 -0
- package/dist/components/vfs/LockConflictDialog.svelte.d.ts +21 -0
- package/dist/components/vfs/LockIndicator.svelte +194 -0
- package/dist/components/vfs/LockIndicator.svelte.d.ts +29 -0
- package/dist/components/vfs/LockOverlay.svelte +344 -0
- package/dist/components/vfs/LockOverlay.svelte.d.ts +17 -0
- package/dist/components/vfs/VersionConflictDialog.svelte +549 -0
- package/dist/components/vfs/VersionConflictDialog.svelte.d.ts +24 -0
- package/dist/components/vfs/index.d.ts +4 -0
- package/dist/components/vfs/index.js +5 -0
- package/dist/crdt/awareness.d.ts +42 -0
- package/dist/crdt/awareness.js +109 -0
- package/dist/crdt/document.d.ts +101 -0
- package/dist/crdt/document.js +187 -0
- package/dist/crdt/index.d.ts +9 -0
- package/dist/crdt/index.js +8 -0
- package/dist/crdt/provider.d.ts +85 -0
- package/dist/crdt/provider.js +150 -0
- package/dist/crdt/types.d.ts +61 -0
- package/dist/crdt/types.js +4 -0
- package/dist/crdt/undo.d.ts +34 -0
- package/dist/crdt/undo.js +70 -0
- package/dist/index.d.ts +277 -0
- package/dist/index.js +280 -0
- package/dist/plugins/index.d.ts +103 -0
- package/dist/plugins/index.js +153 -0
- package/dist/services/error-handling.d.ts +95 -0
- package/dist/services/error-handling.js +413 -0
- package/dist/services/ide-integration.d.ts +83 -0
- package/dist/services/ide-integration.js +367 -0
- package/dist/services/lsp-client.d.ts +69 -0
- package/dist/services/lsp-client.js +667 -0
- package/dist/services/mock-ai.d.ts +37 -0
- package/dist/services/mock-ai.js +318 -0
- package/dist/services/optimistic.d.ts +141 -0
- package/dist/services/optimistic.js +367 -0
- package/dist/services/vfs-client.d.ts +81 -0
- package/dist/services/vfs-client.js +348 -0
- package/dist/stores/agents.svelte.d.ts +85 -0
- package/dist/stores/agents.svelte.js +459 -0
- package/dist/stores/ai-persistence.svelte.d.ts +76 -0
- package/dist/stores/ai-persistence.svelte.js +334 -0
- package/dist/stores/ai.svelte.d.ts +140 -0
- package/dist/stores/ai.svelte.js +383 -0
- package/dist/stores/collaboration.svelte.d.ts +164 -0
- package/dist/stores/collaboration.svelte.js +334 -0
- package/dist/stores/editor.svelte.d.ts +131 -0
- package/dist/stores/editor.svelte.js +250 -0
- package/dist/stores/index.d.ts +10 -0
- package/dist/stores/index.js +29 -0
- package/dist/stores/layout.svelte.d.ts +171 -0
- package/dist/stores/layout.svelte.js +351 -0
- package/dist/stores/plugin.svelte.d.ts +121 -0
- package/dist/stores/plugin.svelte.js +410 -0
- package/dist/stores/vfs.svelte.d.ts +123 -0
- package/dist/stores/vfs.svelte.js +680 -0
- package/dist/styles/theme.css +623 -0
- package/dist/types/agents.d.ts +127 -0
- package/dist/types/agents.js +5 -0
- package/dist/types/ai.d.ts +137 -0
- package/dist/types/ai.js +4 -0
- package/dist/types/crdt.d.ts +222 -0
- package/dist/types/crdt.js +5 -0
- package/dist/types/editor.d.ts +52 -0
- package/dist/types/editor.js +18 -0
- package/dist/types/events.d.ts +133 -0
- package/dist/types/events.js +4 -0
- package/dist/types/filesystem.d.ts +77 -0
- package/dist/types/filesystem.js +4 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +12 -0
- package/dist/types/lsp.d.ts +691 -0
- package/dist/types/lsp.js +108 -0
- package/dist/types/plugin.d.ts +239 -0
- package/dist/types/plugin.js +5 -0
- package/dist/types/vfs.d.ts +191 -0
- package/dist/types/vfs.js +18 -0
- package/dist/utils/format.d.ts +55 -0
- package/dist/utils/format.js +152 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/keybindings.d.ts +33 -0
- package/dist/utils/keybindings.js +171 -0
- package/dist/utils/language.d.ts +27 -0
- package/dist/utils/language.js +222 -0
- package/package.json +178 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VFS store using Svelte 5 runes
|
|
3
|
+
* Manages workspace state, file tree, locks, and SSE events
|
|
4
|
+
*
|
|
5
|
+
* Note: Svelte 5 modules cannot directly export $derived values.
|
|
6
|
+
* We use getter functions to expose reactive derived state.
|
|
7
|
+
*/
|
|
8
|
+
import * as vfsClient from '../services/vfs-client';
|
|
9
|
+
import { parseError, isVFSError, isConflictError, logError } from '../services/error-handling';
|
|
10
|
+
import { optimisticUpdate } from '../services/optimistic';
|
|
11
|
+
// Reactive state using $state rune
|
|
12
|
+
let state = $state({
|
|
13
|
+
workspace: null,
|
|
14
|
+
workspaceLoading: false,
|
|
15
|
+
files: [],
|
|
16
|
+
fileMap: new Map(),
|
|
17
|
+
dirtyFiles: new Set(),
|
|
18
|
+
locks: new Map(),
|
|
19
|
+
lockStatuses: new Map(),
|
|
20
|
+
pendingLocks: new Set(),
|
|
21
|
+
activeTransactions: new Map(),
|
|
22
|
+
transactionHistory: [],
|
|
23
|
+
eventSource: null,
|
|
24
|
+
connected: false,
|
|
25
|
+
lastEventTime: null,
|
|
26
|
+
reconnectAttempts: 0,
|
|
27
|
+
syncing: false,
|
|
28
|
+
error: null,
|
|
29
|
+
errorCode: null,
|
|
30
|
+
structuredError: null,
|
|
31
|
+
version: 0,
|
|
32
|
+
pendingRetries: new Map(),
|
|
33
|
+
conflictQueue: []
|
|
34
|
+
});
|
|
35
|
+
// Event handlers for SSE
|
|
36
|
+
const eventHandlers = new Map();
|
|
37
|
+
// Lock TTL refresh intervals
|
|
38
|
+
const lockRefreshIntervals = new Map(); // path -> intervalId
|
|
39
|
+
// Current user ID (set during initialization)
|
|
40
|
+
let currentUserId = null;
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Getter Functions (Svelte 5 module-safe)
|
|
43
|
+
// ============================================================================
|
|
44
|
+
export function getWorkspace() {
|
|
45
|
+
return state.workspace;
|
|
46
|
+
}
|
|
47
|
+
export function getWorkspaceLoading() {
|
|
48
|
+
return state.workspaceLoading;
|
|
49
|
+
}
|
|
50
|
+
export function getFiles() {
|
|
51
|
+
return state.files;
|
|
52
|
+
}
|
|
53
|
+
export function getFileTree() {
|
|
54
|
+
return buildTree(state.files);
|
|
55
|
+
}
|
|
56
|
+
export function getFile(path) {
|
|
57
|
+
return state.fileMap.get(path);
|
|
58
|
+
}
|
|
59
|
+
export function getLocks() {
|
|
60
|
+
return Array.from(state.locks.values());
|
|
61
|
+
}
|
|
62
|
+
export function getLock(path) {
|
|
63
|
+
return state.locks.get(path);
|
|
64
|
+
}
|
|
65
|
+
export function getLockStatus(path) {
|
|
66
|
+
return state.lockStatuses.get(path) ?? { status: 'unlocked' };
|
|
67
|
+
}
|
|
68
|
+
export function isLocked(path) {
|
|
69
|
+
return state.locks.has(path);
|
|
70
|
+
}
|
|
71
|
+
export function isLockedByMe(path) {
|
|
72
|
+
if (!currentUserId)
|
|
73
|
+
return false;
|
|
74
|
+
const lock = state.locks.get(path);
|
|
75
|
+
return lock?.holder === currentUserId;
|
|
76
|
+
}
|
|
77
|
+
export function isLockedByOther(path) {
|
|
78
|
+
if (!currentUserId)
|
|
79
|
+
return false;
|
|
80
|
+
const lock = state.locks.get(path);
|
|
81
|
+
return lock !== undefined && lock.holder !== currentUserId;
|
|
82
|
+
}
|
|
83
|
+
export function getMyLocks() {
|
|
84
|
+
if (!currentUserId)
|
|
85
|
+
return [];
|
|
86
|
+
return getLocks().filter((lock) => lock.holder === currentUserId);
|
|
87
|
+
}
|
|
88
|
+
export function getOtherLocks() {
|
|
89
|
+
if (!currentUserId)
|
|
90
|
+
return getLocks();
|
|
91
|
+
return getLocks().filter((lock) => lock.holder !== currentUserId);
|
|
92
|
+
}
|
|
93
|
+
export function getDirtyFiles() {
|
|
94
|
+
return Array.from(state.dirtyFiles);
|
|
95
|
+
}
|
|
96
|
+
export function isDirty(path) {
|
|
97
|
+
return state.dirtyFiles.has(path);
|
|
98
|
+
}
|
|
99
|
+
export function getActiveTransactions() {
|
|
100
|
+
return Array.from(state.activeTransactions.values());
|
|
101
|
+
}
|
|
102
|
+
export function getTransactionHistory() {
|
|
103
|
+
return state.transactionHistory;
|
|
104
|
+
}
|
|
105
|
+
export function getConnected() {
|
|
106
|
+
return state.connected;
|
|
107
|
+
}
|
|
108
|
+
export function getSyncing() {
|
|
109
|
+
return state.syncing;
|
|
110
|
+
}
|
|
111
|
+
export function getError() {
|
|
112
|
+
return state.error;
|
|
113
|
+
}
|
|
114
|
+
export function getErrorCode() {
|
|
115
|
+
return state.errorCode;
|
|
116
|
+
}
|
|
117
|
+
export function getVersion() {
|
|
118
|
+
return state.version;
|
|
119
|
+
}
|
|
120
|
+
export function getReconnectAttempts() {
|
|
121
|
+
return state.reconnectAttempts;
|
|
122
|
+
}
|
|
123
|
+
export function getStructuredError() {
|
|
124
|
+
return state.structuredError;
|
|
125
|
+
}
|
|
126
|
+
export function getConflictQueue() {
|
|
127
|
+
return state.conflictQueue;
|
|
128
|
+
}
|
|
129
|
+
export function hasConflicts() {
|
|
130
|
+
return state.conflictQueue.length > 0;
|
|
131
|
+
}
|
|
132
|
+
export function getPendingRetryCount(operationId) {
|
|
133
|
+
return state.pendingRetries.get(operationId) ?? 0;
|
|
134
|
+
}
|
|
135
|
+
// Derived getters
|
|
136
|
+
export function getLockedFiles() {
|
|
137
|
+
return state.files.filter((f) => state.locks.has(f.path));
|
|
138
|
+
}
|
|
139
|
+
export function getMyLockedFiles() {
|
|
140
|
+
return state.files.filter((f) => isLockedByMe(f.path));
|
|
141
|
+
}
|
|
142
|
+
export function getDirectories() {
|
|
143
|
+
return state.files.filter((f) => f.isDir);
|
|
144
|
+
}
|
|
145
|
+
export function getFilesInDirectory(dirPath) {
|
|
146
|
+
const prefix = dirPath.endsWith('/') ? dirPath : `${dirPath}/`;
|
|
147
|
+
return state.files.filter((f) => f.path.startsWith(prefix) && !f.path.slice(prefix.length).includes('/'));
|
|
148
|
+
}
|
|
149
|
+
// Legacy aliases for backward compatibility
|
|
150
|
+
export const workspace = { get current() { return getWorkspace(); } };
|
|
151
|
+
export const workspaceLoading = { get current() { return getWorkspaceLoading(); } };
|
|
152
|
+
export const files = { get current() { return getFiles(); } };
|
|
153
|
+
export const fileTree = { get current() { return getFileTree(); } };
|
|
154
|
+
export const locks = { get current() { return getLocks(); } };
|
|
155
|
+
export const dirtyFiles = { get current() { return getDirtyFiles(); } };
|
|
156
|
+
export const activeTransactions = { get current() { return getActiveTransactions(); } };
|
|
157
|
+
export const connected = { get current() { return getConnected(); } };
|
|
158
|
+
export const syncing = { get current() { return getSyncing(); } };
|
|
159
|
+
export const error = { get current() { return getError(); } };
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Initialization
|
|
162
|
+
// ============================================================================
|
|
163
|
+
export function initialize(userId, workspaceId) {
|
|
164
|
+
currentUserId = userId;
|
|
165
|
+
if (workspaceId) {
|
|
166
|
+
loadWorkspace(workspaceId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Workspace Operations
|
|
171
|
+
// ============================================================================
|
|
172
|
+
export async function loadWorkspace(workspaceId) {
|
|
173
|
+
state.workspaceLoading = true;
|
|
174
|
+
state.error = null;
|
|
175
|
+
try {
|
|
176
|
+
const workspace = await vfsClient.getWorkspace(workspaceId);
|
|
177
|
+
state.workspace = workspace;
|
|
178
|
+
// Connect to SSE stream
|
|
179
|
+
connect(workspaceId);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
state.error = err instanceof Error ? err.message : 'Failed to load workspace';
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
state.workspaceLoading = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// File Operations
|
|
190
|
+
// ============================================================================
|
|
191
|
+
export function updateFileInfo(fileInfo) {
|
|
192
|
+
state.fileMap.set(fileInfo.path, fileInfo);
|
|
193
|
+
const index = state.files.findIndex((f) => f.path === fileInfo.path);
|
|
194
|
+
if (index >= 0) {
|
|
195
|
+
state.files[index] = fileInfo;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
state.files = [...state.files, fileInfo];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function removeFileInfo(path) {
|
|
202
|
+
state.fileMap.delete(path);
|
|
203
|
+
state.files = state.files.filter((f) => f.path !== path);
|
|
204
|
+
state.dirtyFiles.delete(path);
|
|
205
|
+
removeLock(path);
|
|
206
|
+
}
|
|
207
|
+
export function markDirty(path) {
|
|
208
|
+
state.dirtyFiles.add(path);
|
|
209
|
+
}
|
|
210
|
+
export function markClean(path) {
|
|
211
|
+
state.dirtyFiles.delete(path);
|
|
212
|
+
}
|
|
213
|
+
export function setFiles(files) {
|
|
214
|
+
state.files = files;
|
|
215
|
+
state.fileMap = new Map(files.map((f) => [f.path, f]));
|
|
216
|
+
}
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Optimistic File Operations with Error Recovery
|
|
219
|
+
// ============================================================================
|
|
220
|
+
/**
|
|
221
|
+
* Write file content with optimistic update and automatic rollback on failure
|
|
222
|
+
*/
|
|
223
|
+
export async function writeFileOptimistic(path, content, options = {}) {
|
|
224
|
+
if (!state.workspace) {
|
|
225
|
+
const error = parseError(new Error('Workspace not initialized'), { path });
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error,
|
|
229
|
+
operation: { id: '', type: 'file_write', payload: { path, content }, timestamp: Date.now(), status: 'failed', retryCount: 0, maxRetries: 3 }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const previousFile = state.fileMap.get(path);
|
|
233
|
+
const workspaceId = state.workspace.id;
|
|
234
|
+
return await optimisticUpdate({
|
|
235
|
+
type: 'file_write',
|
|
236
|
+
payload: { path, content },
|
|
237
|
+
apply: () => {
|
|
238
|
+
// Optimistically update file info
|
|
239
|
+
markDirty(path);
|
|
240
|
+
if (previousFile) {
|
|
241
|
+
updateFileInfo({
|
|
242
|
+
...previousFile,
|
|
243
|
+
size: new Blob([content]).size,
|
|
244
|
+
modTime: new Date().toISOString()
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
rollback: () => {
|
|
249
|
+
// Restore previous state
|
|
250
|
+
markClean(path);
|
|
251
|
+
if (previousFile) {
|
|
252
|
+
updateFileInfo(previousFile);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
commit: async () => {
|
|
256
|
+
const result = await vfsClient.quickWriteFile(workspaceId, path, content);
|
|
257
|
+
markClean(path);
|
|
258
|
+
updateFileInfo(result);
|
|
259
|
+
return result;
|
|
260
|
+
},
|
|
261
|
+
config: {
|
|
262
|
+
maxRetries: 3,
|
|
263
|
+
retryDelay: 1000,
|
|
264
|
+
onRollback: (op, error) => {
|
|
265
|
+
const vfsError = parseError(error, { path, workspaceId });
|
|
266
|
+
state.structuredError = vfsError;
|
|
267
|
+
logError(vfsError, 'error', { operation: op.type, path });
|
|
268
|
+
// Queue conflict if version mismatch
|
|
269
|
+
if (isConflictError(vfsError)) {
|
|
270
|
+
state.conflictQueue.push({
|
|
271
|
+
path,
|
|
272
|
+
localContent: content,
|
|
273
|
+
serverVersion: state.version
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Delete file with optimistic update
|
|
282
|
+
*/
|
|
283
|
+
export async function deleteFileOptimistic(path) {
|
|
284
|
+
if (!state.workspace) {
|
|
285
|
+
const error = parseError(new Error('Workspace not initialized'), { path });
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error,
|
|
289
|
+
operation: { id: '', type: 'file_delete', payload: { path }, timestamp: Date.now(), status: 'failed', retryCount: 0, maxRetries: 3 }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const previousFile = state.fileMap.get(path);
|
|
293
|
+
const previousLock = state.locks.get(path);
|
|
294
|
+
const workspaceId = state.workspace.id;
|
|
295
|
+
return await optimisticUpdate({
|
|
296
|
+
type: 'file_delete',
|
|
297
|
+
payload: { path },
|
|
298
|
+
apply: () => {
|
|
299
|
+
removeFileInfo(path);
|
|
300
|
+
},
|
|
301
|
+
rollback: () => {
|
|
302
|
+
if (previousFile) {
|
|
303
|
+
updateFileInfo(previousFile);
|
|
304
|
+
}
|
|
305
|
+
if (previousLock) {
|
|
306
|
+
setLock(previousLock);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
commit: async () => {
|
|
310
|
+
await vfsClient.deleteFile(workspaceId, path);
|
|
311
|
+
},
|
|
312
|
+
config: {
|
|
313
|
+
maxRetries: 2,
|
|
314
|
+
retryDelay: 500,
|
|
315
|
+
onRollback: (op, error) => {
|
|
316
|
+
const vfsError = parseError(error, { path, workspaceId });
|
|
317
|
+
state.structuredError = vfsError;
|
|
318
|
+
logError(vfsError, 'error', { operation: op.type, path });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Conflict Resolution
|
|
325
|
+
// ============================================================================
|
|
326
|
+
/**
|
|
327
|
+
* Resolve a file conflict
|
|
328
|
+
*/
|
|
329
|
+
export function resolveConflict(path, resolution, mergedContent) {
|
|
330
|
+
const conflictIndex = state.conflictQueue.findIndex(c => c.path === path);
|
|
331
|
+
if (conflictIndex === -1)
|
|
332
|
+
return;
|
|
333
|
+
const conflict = state.conflictQueue[conflictIndex];
|
|
334
|
+
state.conflictQueue = state.conflictQueue.filter((_, i) => i !== conflictIndex);
|
|
335
|
+
switch (resolution) {
|
|
336
|
+
case 'use_local':
|
|
337
|
+
// Re-attempt write with force flag
|
|
338
|
+
if (state.workspace) {
|
|
339
|
+
vfsClient.quickWriteFile(state.workspace.id, path, conflict.localContent)
|
|
340
|
+
.then(result => {
|
|
341
|
+
updateFileInfo(result);
|
|
342
|
+
markClean(path);
|
|
343
|
+
})
|
|
344
|
+
.catch(err => {
|
|
345
|
+
state.structuredError = parseError(err, { path });
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
case 'use_server':
|
|
350
|
+
// Discard local changes, mark clean
|
|
351
|
+
markClean(path);
|
|
352
|
+
break;
|
|
353
|
+
case 'merge':
|
|
354
|
+
// Write merged content
|
|
355
|
+
if (mergedContent && state.workspace) {
|
|
356
|
+
vfsClient.quickWriteFile(state.workspace.id, path, mergedContent)
|
|
357
|
+
.then(result => {
|
|
358
|
+
updateFileInfo(result);
|
|
359
|
+
markClean(path);
|
|
360
|
+
})
|
|
361
|
+
.catch(err => {
|
|
362
|
+
state.structuredError = parseError(err, { path });
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Dismiss all conflicts (use server versions)
|
|
370
|
+
*/
|
|
371
|
+
export function dismissAllConflicts() {
|
|
372
|
+
for (const conflict of state.conflictQueue) {
|
|
373
|
+
markClean(conflict.path);
|
|
374
|
+
}
|
|
375
|
+
state.conflictQueue = [];
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Retry a failed operation
|
|
379
|
+
*/
|
|
380
|
+
export async function retryOperation(operationId) {
|
|
381
|
+
const retryCount = state.pendingRetries.get(operationId) ?? 0;
|
|
382
|
+
if (retryCount >= 3) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
state.pendingRetries.set(operationId, retryCount + 1);
|
|
386
|
+
// The actual retry logic would be implemented based on the operation type
|
|
387
|
+
// This is a placeholder for the retry mechanism
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Clear structured error
|
|
392
|
+
*/
|
|
393
|
+
export function clearStructuredError() {
|
|
394
|
+
state.structuredError = null;
|
|
395
|
+
}
|
|
396
|
+
// ============================================================================
|
|
397
|
+
// Lock Operations
|
|
398
|
+
// ============================================================================
|
|
399
|
+
export function setLock(lock) {
|
|
400
|
+
state.locks.set(lock.path, lock);
|
|
401
|
+
state.lockStatuses.set(lock.path, { status: 'locked', lock });
|
|
402
|
+
state.pendingLocks.delete(lock.path);
|
|
403
|
+
// Start TTL refresh if it's our lock
|
|
404
|
+
if (lock.holder === currentUserId) {
|
|
405
|
+
startLockRefresh(lock);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
export function removeLock(path) {
|
|
409
|
+
state.locks.delete(path);
|
|
410
|
+
state.lockStatuses.set(path, { status: 'unlocked' });
|
|
411
|
+
state.pendingLocks.delete(path);
|
|
412
|
+
stopLockRefresh(path);
|
|
413
|
+
}
|
|
414
|
+
export function setLockStatus(path, status) {
|
|
415
|
+
state.lockStatuses.set(path, status);
|
|
416
|
+
if (status.status === 'acquiring') {
|
|
417
|
+
state.pendingLocks.add(path);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
state.pendingLocks.delete(path);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export function setLocks(locks) {
|
|
424
|
+
state.locks = new Map(locks.map((l) => [l.path, l]));
|
|
425
|
+
// Update lock statuses
|
|
426
|
+
for (const lock of locks) {
|
|
427
|
+
state.lockStatuses.set(lock.path, { status: 'locked', lock });
|
|
428
|
+
}
|
|
429
|
+
// Start refresh timers for our locks
|
|
430
|
+
for (const lock of locks) {
|
|
431
|
+
if (lock.holder === currentUserId) {
|
|
432
|
+
startLockRefresh(lock);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function startLockRefresh(lock) {
|
|
437
|
+
// Refresh at 80% of TTL to ensure we don't lose the lock
|
|
438
|
+
const refreshInterval = lock.ttl * 0.8;
|
|
439
|
+
const intervalId = window.setInterval(async () => {
|
|
440
|
+
await refreshLockTTL(lock.path);
|
|
441
|
+
}, refreshInterval);
|
|
442
|
+
lockRefreshIntervals.set(lock.path, intervalId);
|
|
443
|
+
}
|
|
444
|
+
function stopLockRefresh(path) {
|
|
445
|
+
const intervalId = lockRefreshIntervals.get(path);
|
|
446
|
+
if (intervalId) {
|
|
447
|
+
clearInterval(intervalId);
|
|
448
|
+
lockRefreshIntervals.delete(path);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function refreshLockTTL(path) {
|
|
452
|
+
if (!state.workspace || !currentUserId)
|
|
453
|
+
return;
|
|
454
|
+
try {
|
|
455
|
+
const lock = await vfsClient.refreshLock(state.workspace.id, path, currentUserId);
|
|
456
|
+
setLock(lock);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
// Refresh failed - lock may have expired
|
|
460
|
+
removeLock(path);
|
|
461
|
+
state.error = `Lock expired on ${path}`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Public lock acquisition
|
|
465
|
+
export async function acquireLock(path, purpose) {
|
|
466
|
+
if (!state.workspace || !currentUserId) {
|
|
467
|
+
state.error = 'Workspace or user not initialized';
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
setLockStatus(path, { status: 'acquiring' });
|
|
471
|
+
try {
|
|
472
|
+
const lock = await vfsClient.acquireLock(state.workspace.id, path, currentUserId, { purpose });
|
|
473
|
+
setLock(lock);
|
|
474
|
+
return lock;
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to acquire lock';
|
|
478
|
+
setLockStatus(path, { status: 'failed', error: errorMessage });
|
|
479
|
+
state.error = errorMessage;
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
export async function releaseLock(path) {
|
|
484
|
+
if (!state.workspace || !currentUserId)
|
|
485
|
+
return;
|
|
486
|
+
try {
|
|
487
|
+
await vfsClient.releaseLock(state.workspace.id, path, currentUserId);
|
|
488
|
+
removeLock(path);
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
state.error = err instanceof Error ? err.message : 'Failed to release lock';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
export async function releaseAllMyLocks() {
|
|
495
|
+
const myLocks = getMyLocks();
|
|
496
|
+
for (const lock of myLocks) {
|
|
497
|
+
await releaseLock(lock.path);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// Transaction Operations
|
|
502
|
+
// ============================================================================
|
|
503
|
+
export function startTransaction(transaction) {
|
|
504
|
+
state.activeTransactions.set(transaction.id, transaction);
|
|
505
|
+
}
|
|
506
|
+
export function completeTransaction(transactionId, status) {
|
|
507
|
+
const tx = state.activeTransactions.get(transactionId);
|
|
508
|
+
if (!tx)
|
|
509
|
+
return;
|
|
510
|
+
tx.status = status;
|
|
511
|
+
tx.committedAt = new Date().toISOString();
|
|
512
|
+
state.activeTransactions.delete(transactionId);
|
|
513
|
+
state.transactionHistory = [tx, ...state.transactionHistory].slice(0, 100); // Keep last 100
|
|
514
|
+
}
|
|
515
|
+
// ============================================================================
|
|
516
|
+
// SSE Connection
|
|
517
|
+
// ============================================================================
|
|
518
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
519
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
520
|
+
export function connect(workspaceId) {
|
|
521
|
+
if (state.eventSource) {
|
|
522
|
+
state.eventSource.close();
|
|
523
|
+
}
|
|
524
|
+
const endpoint = `/api/vfs/workspaces/${workspaceId}/stream`;
|
|
525
|
+
const eventSource = new EventSource(endpoint);
|
|
526
|
+
eventSource.onopen = () => {
|
|
527
|
+
state.connected = true;
|
|
528
|
+
state.error = null;
|
|
529
|
+
state.reconnectAttempts = 0;
|
|
530
|
+
};
|
|
531
|
+
eventSource.onerror = () => {
|
|
532
|
+
state.connected = false;
|
|
533
|
+
state.error = 'VFS connection lost';
|
|
534
|
+
if (state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
535
|
+
const delay = BASE_RECONNECT_DELAY * Math.pow(2, state.reconnectAttempts);
|
|
536
|
+
state.reconnectAttempts++;
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
if (!state.connected) {
|
|
539
|
+
connect(workspaceId);
|
|
540
|
+
}
|
|
541
|
+
}, delay);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
state.error = 'Failed to reconnect after multiple attempts';
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
eventSource.onmessage = (event) => {
|
|
548
|
+
try {
|
|
549
|
+
const vfsEvent = JSON.parse(event.data);
|
|
550
|
+
handleEvent(vfsEvent);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
console.error('Failed to parse VFS event');
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
state.eventSource = eventSource;
|
|
557
|
+
}
|
|
558
|
+
export function disconnect() {
|
|
559
|
+
if (state.eventSource) {
|
|
560
|
+
state.eventSource.close();
|
|
561
|
+
state.eventSource = null;
|
|
562
|
+
}
|
|
563
|
+
state.connected = false;
|
|
564
|
+
// Clear all lock refresh intervals
|
|
565
|
+
for (const intervalId of lockRefreshIntervals.values()) {
|
|
566
|
+
clearInterval(intervalId);
|
|
567
|
+
}
|
|
568
|
+
lockRefreshIntervals.clear();
|
|
569
|
+
}
|
|
570
|
+
function handleEvent(event) {
|
|
571
|
+
state.lastEventTime = event.timestamp;
|
|
572
|
+
switch (event.type) {
|
|
573
|
+
case 'snapshot':
|
|
574
|
+
handleSnapshot(event);
|
|
575
|
+
break;
|
|
576
|
+
case 'update':
|
|
577
|
+
handleUpdate(event);
|
|
578
|
+
break;
|
|
579
|
+
case 'ping':
|
|
580
|
+
// Keep-alive, no action needed
|
|
581
|
+
break;
|
|
582
|
+
case 'complete':
|
|
583
|
+
// Stream ended
|
|
584
|
+
disconnect();
|
|
585
|
+
break;
|
|
586
|
+
case 'error':
|
|
587
|
+
state.error = event.error;
|
|
588
|
+
state.errorCode = event.code;
|
|
589
|
+
break;
|
|
590
|
+
case 'lock_acquired':
|
|
591
|
+
handleLockAcquired(event);
|
|
592
|
+
break;
|
|
593
|
+
case 'lock_released':
|
|
594
|
+
handleLockReleased(event);
|
|
595
|
+
break;
|
|
596
|
+
case 'iteration_completed':
|
|
597
|
+
case 'change_classified':
|
|
598
|
+
case 'gate_progress':
|
|
599
|
+
// Forward to agent store (handled by subscriber)
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
// Notify subscribed handlers
|
|
603
|
+
const handlers = eventHandlers.get(event.type);
|
|
604
|
+
if (handlers) {
|
|
605
|
+
handlers.forEach((handler) => handler(event));
|
|
606
|
+
}
|
|
607
|
+
const wildcardHandlers = eventHandlers.get('*');
|
|
608
|
+
if (wildcardHandlers) {
|
|
609
|
+
wildcardHandlers.forEach((handler) => handler(event));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function handleSnapshot(event) {
|
|
613
|
+
setFiles(event.files);
|
|
614
|
+
setLocks(event.locks);
|
|
615
|
+
state.version = event.version;
|
|
616
|
+
}
|
|
617
|
+
function handleUpdate(event) {
|
|
618
|
+
if (event.files) {
|
|
619
|
+
event.files.forEach(updateFileInfo);
|
|
620
|
+
}
|
|
621
|
+
state.version = event.version;
|
|
622
|
+
}
|
|
623
|
+
function handleLockAcquired(event) {
|
|
624
|
+
setLock(event.lock);
|
|
625
|
+
}
|
|
626
|
+
function handleLockReleased(event) {
|
|
627
|
+
removeLock(event.path);
|
|
628
|
+
}
|
|
629
|
+
export function onEvent(type, handler) {
|
|
630
|
+
if (!eventHandlers.has(type)) {
|
|
631
|
+
eventHandlers.set(type, new Set());
|
|
632
|
+
}
|
|
633
|
+
eventHandlers.get(type).add(handler);
|
|
634
|
+
return () => {
|
|
635
|
+
eventHandlers.get(type)?.delete(handler);
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Helper Functions
|
|
640
|
+
// ============================================================================
|
|
641
|
+
function buildTree(files) {
|
|
642
|
+
// For now, return sorted files
|
|
643
|
+
// A full tree implementation would nest children under directories
|
|
644
|
+
return [...files].sort((a, b) => {
|
|
645
|
+
// Directories first, then alphabetically
|
|
646
|
+
if (a.isDir !== b.isDir)
|
|
647
|
+
return a.isDir ? -1 : 1;
|
|
648
|
+
return a.path.localeCompare(b.path);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
export function clearError() {
|
|
652
|
+
state.error = null;
|
|
653
|
+
state.errorCode = null;
|
|
654
|
+
}
|
|
655
|
+
export function reset() {
|
|
656
|
+
disconnect();
|
|
657
|
+
state.workspace = null;
|
|
658
|
+
state.files = [];
|
|
659
|
+
state.fileMap.clear();
|
|
660
|
+
state.dirtyFiles.clear();
|
|
661
|
+
state.locks.clear();
|
|
662
|
+
state.lockStatuses.clear();
|
|
663
|
+
state.pendingLocks.clear();
|
|
664
|
+
state.activeTransactions.clear();
|
|
665
|
+
state.transactionHistory = [];
|
|
666
|
+
state.error = null;
|
|
667
|
+
state.errorCode = null;
|
|
668
|
+
state.structuredError = null;
|
|
669
|
+
state.version = 0;
|
|
670
|
+
state.reconnectAttempts = 0;
|
|
671
|
+
state.pendingRetries.clear();
|
|
672
|
+
state.conflictQueue = [];
|
|
673
|
+
currentUserId = null;
|
|
674
|
+
}
|
|
675
|
+
// ============================================================================
|
|
676
|
+
// Sync Status
|
|
677
|
+
// ============================================================================
|
|
678
|
+
export function setSyncing(syncing) {
|
|
679
|
+
state.syncing = syncing;
|
|
680
|
+
}
|