feat(whispering): complete workspace state migration — replace TanStack Query with reactive $state#1546
Merged
feat(whispering): complete workspace state migration — replace TanStack Query with reactive $state#1546
Conversation
…mations, and steps Phase 1 of query layer switch: create SvelteMap + Yjs observer state modules that replace TanStack Query for workspace table reads/writes. - workspace-recordings.svelte.ts: SvelteMap<id, Recording> with sorted getter - workspace-transformations.svelte.ts: SvelteMap<id, Transformation> - workspace-transformation-steps.svelte.ts: SvelteMap<id, Step> with getByTransformationId
…pace state Phase 2 of query layer switch: replace createQuery reads with direct SvelteMap access from workspace state modules. - recordings/+page.svelte: workspaceRecordings.sorted replaces getAll query - RecordingRowActions: workspaceRecordings.get() replaces getById query - Home page: workspaceRecordings.sorted[0] replaces getLatest query - transformations/+page.svelte: workspaceTransformations.sorted replaces getAll - TransformationSelector: workspaceTransformations.sorted replaces getAll - Removed loading skeletons (SvelteMap has no pending state)
…workspace state Phase 3 of query layer switch: replace mutation calls with direct workspace writes. Workspace deletes are fire-and-forget via Yjs—no error handling needed. Recordings: - processRecordingPipeline: metadata to workspace, audio blob to DbService - transcription.ts: 3 update calls → workspaceRecordings.update() - recording-actions, recordings page, home page: delete → workspace + revokeAudioUrl Transformations: - Deletes switched (page bulk delete, TransformationRowActions) - Create/update deferred (Editor uses dot-notation field names incompatible with workspace flat schema—requires separate follow-up refactor)
…ing read in transformer Phase 4 of query layer switch: transformation runs (incremental). - Created workspace-transformation-runs.svelte.ts with getByTransformationId/getByRecordingId - Switched recording lookup in transformer.ts from DbService to workspaceRecordings.get() - Kept DbService run queries as fallback for historical runs - Deferred full run lifecycle wiring (complex step-by-step execution)
Phase 5 of query layer switch: additional cleanup and documentation. - EditRecordingModal: update mutation → workspaceRecordings.set() - TransformationPickerBody: TanStack Query → workspaceTransformations - EditTransformationModal: delete → workspaceTransformations.delete() - state/README.md: documented all 4 new workspace state modules - Spec updated to Partially Implemented with review section noting deferred work (Editor flat field refactor, full db.ts cleanup)
…space writes Complete the transformation create/update migration to workspace state. Editor components: - Configuration.svelte: all dot-notation fields → flat workspace names (e.g., 'prompt_transform.inference.provider' → inferenceProvider) - Editor.svelte: receives transformation + steps as separate props - Test.svelte: uses steps prop for length check - Runs loaded from workspace state instead of TanStack Query Parent components: - EditTransformationModal: workspace.batch() for atomic save (metadata + steps) - CreateTransformationButton: workspace.batch() for create - new/+page.svelte: same pattern transformer.ts: - handleStep() uses flat field names - runTransformation() accepts steps parameter - transformRecording gets steps from workspaceTransformationSteps
…ale type imports - RecordingRowActions: re-add createQuery import (runs query still uses TanStack Query) - MarkTransformationActiveButton: Transformation type from workspace state - download.ts: Recording type from workspace state
…nite render loop The sorted getter was returning a new array on every access. TanStack Table's createSvelteTable wraps options in $derived — new array reference triggers internal $state update → invalidates the outer $derived → re-calls sorted → new array again → infinite loop → page freeze. Fix: compute sorted arrays via $derived so the reference is stable until the underlying SvelteMap actually changes.
… 8 inline Omit usages Export TransformationStepDraft = Omit<TransformationStep, '_v'> from the state module so consumers don't repeat the version-tag omission everywhere. Pure type change—zero runtime impact.
… replace 8 inline Omit usages" This reverts commit 2fa316a.
…h workspace state latestTransformationRunByRecordingIdQuery → workspaceTransformationRuns.getLatestByRecordingId() Eliminates createQuery import entirely from this component. Removes loading spinner and error tooltip (data is always available from SvelteMap).
…Omit<_, '_v'> indirection Include _v in generateDefaultStep() and accept full TransformationStep in set(). All component props now use TransformationStep[] directly instead of Omit<TransformationStep, '_v'>[]—the version tag is an implementation detail that should flow through, not be stripped at every boundary.
Documents the SvelteMap + TanStack Table infinite loop pattern and fix: plain getters that return new arrays cause → → loops. Fix: memoize with $derived for stable references.
… of aliasing Replace individual const assignments (step.findText, step.inferenceProvider, etc.) with destructuring at the top of each case branch. Cosmetic only—no behavior change.
…module Move the duplicated function from CreateTransformationButton and new/+page into workspace-transformations.svelte.ts. Also fix set() to accept full Transformation (same _v pattern as the steps module).
The mutation expects { input, transformation, steps } but Test.svelte was
only passing { input, transformation }. Steps were received as a prop but
never forwarded, causing the transformation test runner to receive undefined
steps at runtime.
Replace three duplicated workspace.batch() save patterns with a single shared function. Works for both create and update—deleteByTransformationId is a no-op on fresh transformations, and updatedAt is always refreshed.
…kspace state - LatestTransformationRunOutputByRecordingId: createQuery → workspaceTransformationRuns - ViewTransformationRunsDialog: createQuery → workspaceTransformationRuns - Runs.svelte: rpc.db.runs.delete → workspaceTransformationRuns.delete rpc.db now only has 3 references left, all for audio blob playback URLs (getAudioPlaybackUrl) — binary data that's too large for Yjs CRDTs.
…config map
Replace 6 nearly identical provider cases (~90 lines) with a
STANDARD_PROVIDER_CONFIG lookup (~15 lines). Custom is kept inline since it
has unique per-step baseUrl logic. Adding a new standard provider is now a
one-line config entry instead of a copy-pasted case block.
Also fix interpolateTemplate systemPrompt call that was missing { input }.
…s from transformer.ts All 5 invalidateQueries calls used dbKeys.runs.* and dbKeys.transformations.* which no component subscribes to after the workspace state migration. Also removes the now-unused queryClient and dbKeys imports.
The entire db query module (345 lines of CRUD queries, mutations, and cache management) is dead after migrating to Yjs workspace state modules. Only getAudioPlaybackUrl survives—audio blobs are too large for CRDTs. Renames the module, export, and all 3 consumer call sites: rpc.db.recordings.getAudioPlaybackUrl → rpc.audio.getPlaybackUrl
The Three Layers section now shows workspace state as the primary data layer for recordings, transformations, and runs. Query Layer vs State section explains what lives where: CRDTs for domain data, TanStack Query for external APIs, hardware state, and audio blob access.
Skill file examples now use live APIs (rpc.recorder, rpc.transformer) instead of dead rpc.db.recordings. Accessor pattern article gets a staleness note since its examples use the removed API.
…ation Replace dead optimistic update example in ARCHITECTURE.md with current workspace state pattern. Fix stale inline comment in EditRecordingModal. Document audit findings in cleanup spec.
Rename all 5 workspace-backed state modules to remove the redundant workspace- prefix from filenames and workspace prefix from export names. The state/ directory is the namespace—consumers don't need storage-backend information at every call site. File renames: workspace-recordings.svelte.ts → recordings.svelte.ts workspace-settings.svelte.ts → settings.svelte.ts workspace-transformations.svelte.ts → transformations.svelte.ts workspace-transformation-steps.svelte.ts → transformation-steps.svelte.ts workspace-transformation-runs.svelte.ts → transformation-runs.svelte.ts Export renames: workspaceRecordings → recordings workspaceSettings → settings workspaceTransformations → transformations workspaceTransformationSteps → transformationSteps workspaceTransformationRuns → transformationRuns Resolves 2 local variable collisions in TransformationSelector and TransformationPickerBody by renaming const transformations to sortedTransformations.
Update ARCHITECTURE.md, state README, query README, settings README, and 2 docs/articles to use the new unprefixed names (recordings, settings, transformations, etc.) in all code examples and prose.
…query README straggler The recordings parameter in deleteWithConfirmation shadowed the imported recordings module after the rename, causing .delete() to call on the parameter instead of the state module. Renamed parameter to toDelete. Also fixed a missed old reference in query/README.md line 1071.
Archive the original query layer README as a standalone article preserving the co-location philosophy, optimistic updates patterns, and dual-interface teaching examples from before workspace migration. Also fix 4 remaining stale workspaceRecordings imports in the bottom sections that were missed by the state flatten rename.
Check off all items, add review section documenting 3 deviations: additional collision in recording-actions.ts, 51 files instead of 48, and typecheck script name correction.
… examples The Common Patterns #3 code examples still used workspaceRecordings after the state flatten rename. Now matches the actual transcription.ts source which uses recordings.update().
…cles Accessor pattern article: rpc.recordings.get(id) → rpc.audio.getPlaybackUrl(id) (the only current parameterized query in the codebase). Mutation callbacks pattern: rpc.clipboard.copyToClipboard → rpc.text.copyToClipboard.
During deletion, Svelte's reactive teardown races with prop updates—the recording prop becomes undefined before onDestroy fires, throwing a TypeError that blocks the confirmation dialog from closing. Capturing the ID at mount time sidesteps the reactive teardown race entirely.
…ys closes The sync branch of confirm() had no error handling—if onConfirm threw (e.g. from a reactive teardown during deletion), isOpen was never set to false and the dialog stayed open. Now uses try/finally to guarantee the dialog closes regardless of what onConfirm does, matching the resilience the async branch already had.
…tress testing Provides Y.Doc size monitoring, per-table row counts, and bulk recording generation/deletion with timing results. Helps developers understand storage costs and stress-test at scale without leaving the app. Uses factory function pattern (createMetrics, createStressTest) to co-locate reactive state with its operations. Dev-only via import.meta.env.DEV guard.
… nav link Remove onComplete callback coupling between createStressTest and createMetrics—the template now calls both explicitly. Remove debug nav link from NavItems since it only renders in nav-items layout mode; /debug is accessible via URL directly.
# Conflicts: # .agents/skills/svelte/SKILL.md # apps/whispering/src/lib/query/index.ts
Contributor
|
Thank you for your contribution! Before we can merge this PR, we need you to sign our Contributor License Agreement. This is a one-time requirement—it lets us offer commercial licenses for the sync server while you retain full copyright on your code. To sign, please comment on this PR with:
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
query-layer: drop inline cache management section (moved to references/advanced-query-patterns.md on main). svelte: keep both our reactive table state patterns and main's prop-first data derivation section.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This finishes the migration started in #1534, replacing every TanStack Query read and mutation in the UI with direct workspace
$statemodules — because keeping a caching layer over data that's already in memory was pointless overhead.The pattern is consistent across all five domain areas (recordings, transformations, steps, runs, step-runs):
The result is simpler data flow: components read reactive getters backed by
SvelteMapover the Y.Doc, and write directly to workspace tables. No query keys, no cache invalidation, no loading states for local data.The work broke down into four phases. First,
$state-backed modules for each domain area — each wrapping a workspace table with sorted getters, lookup helpers, and domain-specific derived state. Then the component migration itself: Editor, RecordingRowActions, Test.svelte, the transformer pipeline — all switched over. After that, dead code removal:invalidateQueriescalls, unused RPC imports, a duplicated provider switch inhandleStep, and a rename ofdb.tstoaudio.ts(after the migration, the only thing left in that file wasgetPlaybackUrl, so the name was just wrong). Finally, a cleanup pass dropped theworkspaceprefix from state filenames (workspaceRecordings.svelte.ts→recordings.svelte.ts) and updated all docs, READMEs, and skills to match.Three standalone fixes came out of testing: sidebar button centering in the collapsed rail, a confirmation dialog that was closing even when the operation errored, and a recording ID teardown crash.
Why not keep TanStack Query for local reads? It adds a caching layer over data that's already in memory. The workspace tables are the cache. TanStack Query's value is remote data fetching, not local CRDT state.
Why
$stateover$derivedfor sorted arrays? Referential stability.$derivedwithArray.from().sort()creates a new array on every access, which causes infinite loops with TanStack Table (seedocs/skills/sveltefor the full write-up).$statewith explicit refresh gives stable references.Why rename
db.ts→audio.ts? After the migration, the only thing left indb.tswasgetPlaybackUrl()for audio blob retrieval. The name was misleading.Changelog