@kabmo-labs/kabmo 0.7.0 → 0.9.0
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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The session-state cache: which entries are held, and for how long.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `store.ts` for the file-size ceiling, and it is the natural seam
|
|
10
|
+
* rather than an arbitrary cut. `store.ts` owns the PUBLIC API (read, write,
|
|
11
|
+
* load, migrate) and the decisions about what a write means; this owns the
|
|
12
|
+
* STORAGE — the maps, the LRU order, the idle TTL, and the dirty flag that says
|
|
13
|
+
* an entry holds writes no persist has carried yet.
|
|
14
|
+
*
|
|
15
|
+
* The maps live here and nowhere else, so "is this session dirty" and "drop this
|
|
16
|
+
* session" cannot be answered from two places that disagree.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type SessionState = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
const entries = new Map<string, SessionState>();
|
|
22
|
+
/** Epoch ms of the last user-driven access per session (get/set/hydrate). */
|
|
23
|
+
const lastTouched = new Map<string, number>();
|
|
24
|
+
/** A session with writes no persist has carried yet. */
|
|
25
|
+
const dirty = new Map<string, boolean>();
|
|
26
|
+
const persistTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
27
|
+
|
|
28
|
+
// Bounded LRU cache: cap the number of sessions kept in memory so the tab
|
|
29
|
+
// doesn't grow unboundedly as the user visits more sessions.
|
|
30
|
+
const MAX_CACHED_SESSIONS = 10;
|
|
31
|
+
/** A session whose last user access is older than this is evicted regardless
|
|
32
|
+
* of the size cap, so stale per-session UI state (drafts, panel layouts)
|
|
33
|
+
* stops occupying memory 10 minutes after the user last opened it. */
|
|
34
|
+
const SESSION_IDLE_TTL_MS = 10 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/** The cached entry, as read-only — write through the store's own setters. */
|
|
37
|
+
export function readEntry(sessionId: string): Readonly<SessionState> | undefined {
|
|
38
|
+
return entries.get(sessionId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reorder a cache entry to the end of the Map's iteration order (the
|
|
43
|
+
* most-recently-used position) via delete-then-set. Map.set on an existing
|
|
44
|
+
* key updates the value but keeps its original position, so the explicit
|
|
45
|
+
* delete is required for true LRU semantics.
|
|
46
|
+
*/
|
|
47
|
+
export function touchEntry(sessionId: string): void {
|
|
48
|
+
const value = entries.get(sessionId);
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
entries.delete(sessionId);
|
|
51
|
+
entries.set(sessionId, value);
|
|
52
|
+
}
|
|
53
|
+
lastTouched.set(sessionId, Date.now());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Stamp the entry as most-recently-used without reordering it. */
|
|
57
|
+
export function stampAccess(sessionId: string): void {
|
|
58
|
+
lastTouched.set(sessionId, Date.now());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function lastAccess(sessionId: string): number | undefined {
|
|
62
|
+
return lastTouched.get(sessionId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Replace an entry outright (a blob arriving, a migrate, a hydrate). */
|
|
66
|
+
export function writeEntry(sessionId: string, state: SessionState): void {
|
|
67
|
+
entries.set(sessionId, state);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The MUTABLE entry for a write, created on demand.
|
|
72
|
+
*
|
|
73
|
+
* `setSessionKey` and `clearSessionKey` mutate the cached object in place, as
|
|
74
|
+
* they always have: a copy per write would give every reader a different object
|
|
75
|
+
* for the same slot, and `useSessionState` compares the store's entry against
|
|
76
|
+
* its own copy by IDENTITY to decide whether a fresher write exists.
|
|
77
|
+
*/
|
|
78
|
+
export function entryForWrite(sessionId: string): SessionState {
|
|
79
|
+
const existing = entries.get(sessionId);
|
|
80
|
+
if (existing) return existing;
|
|
81
|
+
const created: SessionState = {};
|
|
82
|
+
entries.set(sessionId, created);
|
|
83
|
+
return created;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Mark a session as holding writes no persist has carried yet. */
|
|
87
|
+
export function markDirty(sessionId: string): void {
|
|
88
|
+
dirty.set(sessionId, true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function clearDirty(sessionId: string): void {
|
|
92
|
+
dirty.set(sessionId, false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isDirty(sessionId: string): boolean {
|
|
96
|
+
return dirty.get(sessionId) === true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function setPersistTimer(sessionId: string, timer: ReturnType<typeof setTimeout>): void {
|
|
100
|
+
persistTimers.set(sessionId, timer);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function persistTimer(sessionId: string): ReturnType<typeof setTimeout> | undefined {
|
|
104
|
+
return persistTimers.get(sessionId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function dropPersistTimer(sessionId: string): void {
|
|
108
|
+
const timer = persistTimers.get(sessionId);
|
|
109
|
+
if (timer) {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
persistTimers.delete(sessionId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Remove an entry and everything recorded about it. */
|
|
116
|
+
export function dropEntry(sessionId: string): void {
|
|
117
|
+
entries.delete(sessionId);
|
|
118
|
+
dirty.delete(sessionId);
|
|
119
|
+
lastTouched.delete(sessionId);
|
|
120
|
+
dropPersistTimer(sessionId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Evict the oldest clean sessions until the cache is within bounds.
|
|
125
|
+
*
|
|
126
|
+
* Dirty-safety rule: a session with unsaved writes (`isDirty`) must never be
|
|
127
|
+
* evicted, or the user's drafts and editor state would be silently lost. If
|
|
128
|
+
* every candidate is dirty (or is the protected id currently being written),
|
|
129
|
+
* stop evicting rather than lose data.
|
|
130
|
+
*
|
|
131
|
+
* Entries idle past SESSION_IDLE_TTL_MS are evicted first — even when the
|
|
132
|
+
* cache is under the size cap — so long-unused sessions drop out on schedule.
|
|
133
|
+
*/
|
|
134
|
+
export function evictIfNeeded(protectedId: string | null, onEvict: (sessionId: string) => void): void {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const evict = (id: string): void => {
|
|
137
|
+
dropEntry(id);
|
|
138
|
+
onEvict(id);
|
|
139
|
+
};
|
|
140
|
+
// TTL pass: safe to skip only while the timer still holds the unsaved state
|
|
141
|
+
// or the session is the one currently being written.
|
|
142
|
+
for (const [id, touched] of lastTouched) {
|
|
143
|
+
if (now - touched <= SESSION_IDLE_TTL_MS) continue;
|
|
144
|
+
if (id === protectedId) continue;
|
|
145
|
+
if (isDirty(id)) continue;
|
|
146
|
+
evict(id);
|
|
147
|
+
}
|
|
148
|
+
while (entries.size > MAX_CACHED_SESSIONS) {
|
|
149
|
+
let evicted = false;
|
|
150
|
+
for (const id of entries.keys()) {
|
|
151
|
+
if (id === protectedId) continue;
|
|
152
|
+
if (isDirty(id)) continue;
|
|
153
|
+
evict(id);
|
|
154
|
+
evicted = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (!evicted) break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared harness for the session-state store's suites.
|
|
8
|
+
*
|
|
9
|
+
* Split out when `store.test.ts` passed the per-file ceiling. Every case in
|
|
10
|
+
* every suite needs the same three things — an id that teardown forgets, a
|
|
11
|
+
* subscription that teardown cancels, and a counter — and a per-file copy is how
|
|
12
|
+
* two suites end up disagreeing about what teardown restores.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, setSystemTime } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { subscribeSessionKey } from '@/shared/lib/workspace/session-state/listeners';
|
|
18
|
+
import { forgetSession } from '@/shared/lib/workspace/session-state/store';
|
|
19
|
+
|
|
20
|
+
/** The runner's own fetch, reached through `Bun` so a stub leaked onto the global cannot be mistaken for it. */
|
|
21
|
+
export const originalFetch = Bun.fetch;
|
|
22
|
+
export const originalWindow = Reflect.get(globalThis, 'window');
|
|
23
|
+
|
|
24
|
+
const touched: string[] = [];
|
|
25
|
+
const unsubs: Array<() => void> = [];
|
|
26
|
+
|
|
27
|
+
/** Register a session id so teardown forgets it. */
|
|
28
|
+
export function use(id: string): string {
|
|
29
|
+
touched.push(id);
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Subscribe and auto-unsubscribe at teardown. */
|
|
34
|
+
export function follow(sessionId: string | null, key: string | null, listener: () => void): () => void {
|
|
35
|
+
const unsubscribe = subscribeSessionKey(sessionId, key, listener);
|
|
36
|
+
unsubs.push(unsubscribe);
|
|
37
|
+
return unsubscribe;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Count how many times a notification reaches a listener. */
|
|
41
|
+
export function counter(): { calls: number; listener: () => void } {
|
|
42
|
+
const state = { calls: 0, listener: () => { state.calls += 1; } };
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Install the suite's teardown. Called once per test file. */
|
|
47
|
+
export function installStoreTeardown(): void {
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
for (const unsubscribe of unsubs.splice(0)) unsubscribe();
|
|
50
|
+
for (const id of touched.splice(0)) forgetSession(id);
|
|
51
|
+
Reflect.set(globalThis, 'window', originalWindow);
|
|
52
|
+
Reflect.set(globalThis, 'fetch', originalFetch);
|
|
53
|
+
setSystemTime();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which copy wins when a session's stored blob arrives.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `store.ts` for the file-size ceiling, and it is a coherent unit
|
|
10
|
+
* rather than an arbitrary cut: both halves answer the same question, and the
|
|
11
|
+
* store is the only caller. The store owns the CACHE (an entry's lifetime, the
|
|
12
|
+
* persist debounce, eviction); this owns the DECISION (a response describes the
|
|
13
|
+
* row as it was when the request arrived, so anything written since is newer).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type SessionState = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A monotonic counter bumped by every local write, and the counter value each
|
|
20
|
+
* `(session, key)` was last written at.
|
|
21
|
+
*
|
|
22
|
+
* This exists for ONE race, and `dirtySessions` cannot cover it: a load's
|
|
23
|
+
* response is served from a snapshot taken when the request arrived, while a
|
|
24
|
+
* write made in the meantime persists on its own schedule. Once that persist
|
|
25
|
+
* succeeds the dirty flag is cleared — so a response that arrives after it but
|
|
26
|
+
* was computed before it would be adopted as-is and silently drop the write.
|
|
27
|
+
* Measured in the browser (a rare e2e failure, `outline.e2e.ts`): a file opened
|
|
28
|
+
* while the page's first `/state` read was in flight came back with
|
|
29
|
+
* `layout.openedFiles` gone, so the editor drew "Select a file to open" over a
|
|
30
|
+
* file the user had just clicked.
|
|
31
|
+
*
|
|
32
|
+
* `loadSession` records the counter when it STARTS and merges exactly the keys
|
|
33
|
+
* written after that point, which is what makes "wrote it while the read was in
|
|
34
|
+
* flight" survive regardless of when the persist landed.
|
|
35
|
+
*/
|
|
36
|
+
let writeSeq = 0;
|
|
37
|
+
const writeLog = new Map<string, Map<string, number>>();
|
|
38
|
+
|
|
39
|
+
/** The counter as of now — a read takes this before it issues its request. */
|
|
40
|
+
export function currentWriteSeq(): number {
|
|
41
|
+
return writeSeq;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Record that `key` was written locally just now. */
|
|
45
|
+
export function noteWrite(sessionId: string, key: string): void {
|
|
46
|
+
writeSeq += 1;
|
|
47
|
+
let perSession = writeLog.get(sessionId);
|
|
48
|
+
if (!perSession) {
|
|
49
|
+
perSession = new Map();
|
|
50
|
+
writeLog.set(sessionId, perSession);
|
|
51
|
+
}
|
|
52
|
+
perSession.set(key, writeSeq);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The keys of `sessionId` written after `since` (strictly later). */
|
|
56
|
+
export function keysWrittenAfter(sessionId: string, since: number): Set<string> {
|
|
57
|
+
const written = new Set<string>();
|
|
58
|
+
const perSession = writeLog.get(sessionId);
|
|
59
|
+
if (!perSession) return written;
|
|
60
|
+
for (const [key, seq] of perSession) {
|
|
61
|
+
if (seq > since) written.add(key);
|
|
62
|
+
}
|
|
63
|
+
return written;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Drop a session's write history — evicted, forgotten or adopted away. */
|
|
67
|
+
export function forgetWrites(sessionId: string): void {
|
|
68
|
+
writeLog.delete(sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The blob to cache, given the response, the local entry, the keys written
|
|
73
|
+
* while the read was in flight, and whether the entry is still dirty.
|
|
74
|
+
*
|
|
75
|
+
* Only the keys written during the window win over the stored blob. Merging the
|
|
76
|
+
* WHOLE local cache over the blob (the old behavior) resurrected stale values on
|
|
77
|
+
* every tab: a second browser opening the session had cached the pre-write blob,
|
|
78
|
+
* and its stale copy clobbered another tab's fresh writes — queue items written
|
|
79
|
+
* in one tab never appeared in the other.
|
|
80
|
+
*/
|
|
81
|
+
export function mergeStoredBlob(
|
|
82
|
+
incoming: SessionState,
|
|
83
|
+
existing: SessionState,
|
|
84
|
+
written: Set<string>,
|
|
85
|
+
dirty: boolean,
|
|
86
|
+
): SessionState {
|
|
87
|
+
// A dirty entry has a persist that has not landed: the whole local copy is
|
|
88
|
+
// newer than the response.
|
|
89
|
+
if (dirty) return { ...incoming, ...existing };
|
|
90
|
+
if (written.size === 0) return incoming;
|
|
91
|
+
const merged: SessionState = { ...incoming };
|
|
92
|
+
for (const key of written) {
|
|
93
|
+
// A key the local side still holds wins outright; one it no longer holds
|
|
94
|
+
// was CLEARED during the read (`clearSessionKey`), so the response's copy is
|
|
95
|
+
// the stale one and must not resurrect it.
|
|
96
|
+
if (key in existing) merged[key] = existing[key];
|
|
97
|
+
else delete merged[key];
|
|
98
|
+
}
|
|
99
|
+
return merged;
|
|
100
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The blob-arrival half of the session-state store: what wins when a stored
|
|
8
|
+
* blob lands, and what a read must not overwrite.
|
|
9
|
+
*
|
|
10
|
+
* Split from `store.test.ts` for the per-file ceiling. The contract pinned here
|
|
11
|
+
* is the one that produced two measured bugs:
|
|
12
|
+
*
|
|
13
|
+
* - A response is served from a snapshot taken when the request arrived, so a
|
|
14
|
+
* key written WHILE the read was in flight is newer — even once its own
|
|
15
|
+
* persist landed and cleared the dirty flag. Without the write counter, a
|
|
16
|
+
* file opened during the page's first `/state` read came back with
|
|
17
|
+
* `layout.openedFiles` gone.
|
|
18
|
+
* - A key CLEARED in that window must not be resurrected by a response that
|
|
19
|
+
* still carries it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, expect, test } from 'bun:test';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
clearSessionKey,
|
|
26
|
+
flushSession,
|
|
27
|
+
getSessionValue,
|
|
28
|
+
hydrateSession,
|
|
29
|
+
loadSession,
|
|
30
|
+
setSessionKey,
|
|
31
|
+
} from '@/shared/lib/workspace/session-state/store';
|
|
32
|
+
import { counter, follow, installStoreTeardown, use } from '@/shared/lib/workspace/session-state/harness.test-util';
|
|
33
|
+
|
|
34
|
+
installStoreTeardown();
|
|
35
|
+
|
|
36
|
+
describe('session-state store: blob arrival (loadSession)', () => {
|
|
37
|
+
function stubWindow(): void {
|
|
38
|
+
Reflect.set(globalThis, 'window', {});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stubFetch(response: unknown): void {
|
|
42
|
+
Reflect.set(globalThis, 'fetch', async () => response);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A fetch stub that HOLDS the blob read and lets writes through.
|
|
47
|
+
*
|
|
48
|
+
* Gating every call deadlocks the case: the write's own persist is a `POST`
|
|
49
|
+
* to the same path, and a stub that awaits the gate for both means the flush
|
|
50
|
+
* can never complete while the read is held.
|
|
51
|
+
*/
|
|
52
|
+
function gateReads(state: unknown): () => void {
|
|
53
|
+
const { promise: gate, resolve } = Promise.withResolvers<void>();
|
|
54
|
+
Reflect.set(globalThis, 'fetch', async (_url: string, init?: { method?: string }) => {
|
|
55
|
+
if (init?.method === 'POST') return { ok: true, json: async () => ({}) };
|
|
56
|
+
await gate;
|
|
57
|
+
return { ok: true, json: async () => ({ state }) };
|
|
58
|
+
});
|
|
59
|
+
return resolve;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('a clean session adopts the stored blob and notifies', async () => {
|
|
63
|
+
const id = use('load-clean');
|
|
64
|
+
stubWindow();
|
|
65
|
+
stubFetch({ ok: true, json: async () => ({ state: { a: 1, b: 2 } }) });
|
|
66
|
+
const probe = counter();
|
|
67
|
+
follow(id, null, probe.listener);
|
|
68
|
+
|
|
69
|
+
await loadSession(id);
|
|
70
|
+
expect<number | undefined>(getSessionValue(id, 'a')).toBe(1);
|
|
71
|
+
expect<string | number | boolean | Record<string, unknown> | unknown[] | null | undefined>(getSessionValue(id, 'b')).toBe(2);
|
|
72
|
+
expect(probe.calls).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a dirty session keeps its local writes over the blob', async () => {
|
|
76
|
+
const id = use('load-dirty');
|
|
77
|
+
stubWindow();
|
|
78
|
+
setSessionKey(id, 'a', 99);
|
|
79
|
+
stubFetch({ ok: true, json: async () => ({ state: { a: 1, b: 2 } }) });
|
|
80
|
+
|
|
81
|
+
await loadSession(id);
|
|
82
|
+
expect<string | number | boolean | Record<string, unknown> | unknown[] | null | undefined>(getSessionValue(id, 'a')).toBe(99);
|
|
83
|
+
expect<string | number | boolean | Record<string, unknown> | unknown[] | null | undefined>(getSessionValue(id, 'b')).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a non-ok response leaves the cache untouched but still notifies', async () => {
|
|
87
|
+
const id = use('load-error');
|
|
88
|
+
stubWindow();
|
|
89
|
+
stubFetch({ ok: false, json: async () => ({}) });
|
|
90
|
+
const probe = counter();
|
|
91
|
+
follow(id, null, probe.listener);
|
|
92
|
+
|
|
93
|
+
await loadSession(id);
|
|
94
|
+
expect(getSessionValue(id, 'a')).toBeUndefined();
|
|
95
|
+
expect(probe.calls).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a rejected fetch is swallowed and notifies', async () => {
|
|
99
|
+
const id = use('load-throw');
|
|
100
|
+
stubWindow();
|
|
101
|
+
Reflect.set(globalThis, 'fetch', async () => {
|
|
102
|
+
throw new Error('offline');
|
|
103
|
+
});
|
|
104
|
+
const probe = counter();
|
|
105
|
+
follow(id, null, probe.listener);
|
|
106
|
+
|
|
107
|
+
// The store logs the failure; keep the expected warning out of the run log.
|
|
108
|
+
const warn = console.warn;
|
|
109
|
+
console.warn = () => {};
|
|
110
|
+
try {
|
|
111
|
+
await loadSession(id);
|
|
112
|
+
} finally {
|
|
113
|
+
console.warn = warn;
|
|
114
|
+
}
|
|
115
|
+
expect(getSessionValue(id, 'a')).toBeUndefined();
|
|
116
|
+
expect(probe.calls).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('loadSession without a window returns early: no fetch, no notify', async () => {
|
|
120
|
+
const id = use('load-nowindow');
|
|
121
|
+
// Removed for the case and put back after it: another file's hook can have
|
|
122
|
+
// installed one while this file runs, and leaving it missing would break
|
|
123
|
+
// whichever case follows.
|
|
124
|
+
const hadWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
125
|
+
Reflect.deleteProperty(globalThis, 'window');
|
|
126
|
+
let fetched = false;
|
|
127
|
+
Reflect.set(globalThis, 'fetch', async () => {
|
|
128
|
+
fetched = true;
|
|
129
|
+
throw new Error('must not be called');
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const probe = counter();
|
|
133
|
+
follow(id, null, probe.listener);
|
|
134
|
+
await loadSession(id);
|
|
135
|
+
expect(fetched).toBe(false);
|
|
136
|
+
expect(probe.calls).toBe(0);
|
|
137
|
+
} finally {
|
|
138
|
+
if (hadWindow) Object.defineProperty(globalThis, 'window', hadWindow);
|
|
139
|
+
Reflect.set(globalThis, 'fetch', Bun.fetch);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A write that lands WHILE the read is in flight must survive the response,
|
|
145
|
+
* even when its own persist already landed (so the dirty flag is clear).
|
|
146
|
+
*
|
|
147
|
+
* This is the race behind a rare e2e failure: the page's first `/state` read
|
|
148
|
+
* is served from a snapshot taken before the click, the click writes and
|
|
149
|
+
* persists, the persist clears `dirty`, and the late response was then adopted
|
|
150
|
+
* wholesale — `layout.openedFiles` came back empty and the editor drew
|
|
151
|
+
* "Select a file to open" over the file the user had just opened.
|
|
152
|
+
*/
|
|
153
|
+
test('a key written during the read wins over the response, even once it persisted', async () => {
|
|
154
|
+
const id = use('load-inflight');
|
|
155
|
+
stubWindow();
|
|
156
|
+
const resolve = gateReads({ a: 1, b: 2 });
|
|
157
|
+
|
|
158
|
+
const pending = loadSession(id);
|
|
159
|
+
// The write and its persist both land while the response is held.
|
|
160
|
+
setSessionKey(id, 'a', 99);
|
|
161
|
+
await flushSession(id);
|
|
162
|
+
resolve();
|
|
163
|
+
await pending;
|
|
164
|
+
|
|
165
|
+
expect<number | undefined>(getSessionValue(id, 'a')).toBe(99);
|
|
166
|
+
// The rest of the blob is still adopted: only the written key is protected.
|
|
167
|
+
expect<number | undefined>(getSessionValue(id, 'b')).toBe(2);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('a key untouched during the read is adopted from the response', async () => {
|
|
171
|
+
const id = use('load-untouched');
|
|
172
|
+
stubWindow();
|
|
173
|
+
const resolve = gateReads({ a: 1 });
|
|
174
|
+
|
|
175
|
+
const pending = loadSession(id);
|
|
176
|
+
// A DIFFERENT key, written and persisted during the same window.
|
|
177
|
+
setSessionKey(id, 'other', true);
|
|
178
|
+
await flushSession(id);
|
|
179
|
+
resolve();
|
|
180
|
+
await pending;
|
|
181
|
+
|
|
182
|
+
expect<number | undefined>(getSessionValue(id, 'a')).toBe(1);
|
|
183
|
+
expect<boolean | undefined>(getSessionValue(id, 'other')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('a cleared key is not resurrected by a response that still carries it', async () => {
|
|
187
|
+
const id = use('load-cleared');
|
|
188
|
+
stubWindow();
|
|
189
|
+
hydrateSession(id, { retired: 'old', keep: 1 });
|
|
190
|
+
const resolve = gateReads({ retired: 'old', keep: 1 });
|
|
191
|
+
|
|
192
|
+
const pending = loadSession(id);
|
|
193
|
+
clearSessionKey(id, 'retired');
|
|
194
|
+
await flushSession(id);
|
|
195
|
+
resolve();
|
|
196
|
+
await pending;
|
|
197
|
+
|
|
198
|
+
expect(getSessionValue(id, 'retired')).toBeUndefined();
|
|
199
|
+
expect<number | undefined>(getSessionValue(id, 'keep')).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('flushSession is a resolved no-op without a window or id', async () => {
|
|
203
|
+
await expect(flushSession(null)).resolves.toBeUndefined();
|
|
204
|
+
await expect(flushSession(use('flush'))).resolves.toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
});
|