mirror of
https://github.com/supabase/supabase.git
synced 2026-06-29 03:50:30 -04:00
5cb81123ae
## What
PR 5 of a stacked refactor. Moves *when to save* out of a module-load
`subscribe` and into an injectable **scheduler** armed by a headless
**provider**, splits the save queue, and adds an unsaved-close warning.
### Scheduler (`sql-editor-save-scheduler.ts`)
`createSaveScheduler({ state, saveMechanism, notify, getSaveMode })`
owns the save *policy*:
- **auto** mode drains the dirty snippet queue as edits land; **manual**
mode (the seam for a future opt-in; defaults to `auto`) leaves snippets
queued until `requestSave`. Folder saves always drain.
- `start()` returns an unsubscribe; `requestSave(id)` is the
explicit-save entry.
### Provider (`sql-editor-save-coordinator.tsx`)
Headless `SqlEditorSaveCoordinatorProvider` instantiates the mechanism
(invalidation via the **React Query client from context**, not the
global `getQueryClient`) + scheduler, `start()`s it in an effect
(start/stop with the provider), and exposes `requestSave` via
`useSqlEditorSaveCoordinator()`. Mounted in `ProjectContext` (under the
app's QueryClientProvider). Cmd+S and the SavingIndicator Retry now go
through `requestSave`.
### Queue split
`needsSaving` (snippets) and `pendingFolderSaves` (folders) are separate
queues, drained independently — the old snippet-vs-folder `if/else` is
gone.
### Unsaved-close warning
A `beforeunload` guard triggers the browser's native "Leave site?"
prompt while any snippet's `status !== 'saved'` (failed / in-flight /
never-saved).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved SQL editor saving with a centralized save flow, including
automatic/manual save handling and immediate “Save Query” requests.
* Added unsaved-change detection so the app can warn before closing or
reloading when edits are still pending.
* **Bug Fixes**
* Retry actions now use the updated save flow for more reliable
re-saving.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
import { subscribe } from 'valtio'
|
|
|
|
import { isNewFolder } from './sql-editor-lifecycle'
|
|
import { validateMoveToFolder } from './sql-editor-rules'
|
|
import type { SaveMechanism } from './sql-editor-save'
|
|
import type { StateSnippet, StateSnippetFolder } from './types'
|
|
import type { Notifier } from '@/lib/notifier'
|
|
|
|
/**
|
|
* Whether snippet edits persist on their own ('auto') or only when the user
|
|
* asks ('manual'). Only 'auto' is wired today; 'manual' is the seam for a future
|
|
* opt-in. Folder creates/renames are explicit user actions and always persist.
|
|
*/
|
|
export type SaveMode = 'auto' | 'manual'
|
|
|
|
/** The slice of the store the scheduler watches and reads. */
|
|
export interface SaveSchedulerStore {
|
|
/** Snippet ids queued for save; value is whether to invalidate the lists. */
|
|
needsSaving: Map<string, boolean>
|
|
/** Folder ids queued for save (create or rename). */
|
|
pendingFolderSaves: Map<string, boolean>
|
|
snippets: { [id: string]: StateSnippet | undefined }
|
|
folders: { [id: string]: StateSnippetFolder | undefined }
|
|
}
|
|
|
|
export interface SaveSchedulerDeps {
|
|
state: SaveSchedulerStore
|
|
saveMechanism: Pick<SaveMechanism, 'saveSnippet' | 'createFolder' | 'updateFolder'>
|
|
notify: Notifier
|
|
/** Resolves the current save mode. Defaults to 'auto'. */
|
|
getSaveMode?: () => SaveMode
|
|
}
|
|
|
|
/**
|
|
* The save *scheduler*: it owns the policy of *when* the save mechanism runs.
|
|
* In 'auto' mode it drains the dirty snippet queue as edits land; in 'manual'
|
|
* mode it leaves snippets queued until `requestSave` is called. Folder saves are
|
|
* always drained. This is the unit that the headless provider `start()`s; it is
|
|
* decoupled from React so it can be exercised directly.
|
|
*/
|
|
export function createSaveScheduler({
|
|
state,
|
|
saveMechanism,
|
|
notify,
|
|
getSaveMode = () => 'auto',
|
|
}: SaveSchedulerDeps) {
|
|
function flushSnippet(id: string, shouldInvalidate: boolean) {
|
|
const stateSnippet = state.snippets[id]
|
|
if (stateSnippet === undefined) return
|
|
|
|
const { visibility, folder_id } = stateSnippet.snippet
|
|
const moveCheck = validateMoveToFolder({ visibility, folderId: folder_id })
|
|
if (!moveCheck.ok) {
|
|
notify.error(moveCheck.error)
|
|
return
|
|
}
|
|
|
|
saveMechanism.saveSnippet({ id, projectRef: stateSnippet.projectRef, shouldInvalidate })
|
|
}
|
|
|
|
function flushFolder(id: string) {
|
|
const stateFolder = state.folders[id]
|
|
if (stateFolder === undefined) return
|
|
|
|
const { projectRef, folder, status } = stateFolder
|
|
if (isNewFolder(status)) {
|
|
saveMechanism.createFolder({ projectRef, name: folder.name, placeholderId: id })
|
|
} else {
|
|
saveMechanism.updateFolder({ id, projectRef, name: folder.name })
|
|
}
|
|
}
|
|
|
|
function drainSnippetQueue() {
|
|
for (const [id, shouldInvalidate] of Array.from(state.needsSaving.entries())) {
|
|
state.needsSaving.delete(id)
|
|
flushSnippet(id, shouldInvalidate)
|
|
}
|
|
}
|
|
|
|
function drainFolderQueue() {
|
|
for (const [id] of Array.from(state.pendingFolderSaves.entries())) {
|
|
state.pendingFolderSaves.delete(id)
|
|
flushFolder(id)
|
|
}
|
|
}
|
|
|
|
function start() {
|
|
const unsubscribeSnippets = subscribe(state.needsSaving, () => {
|
|
// In manual mode, edits stay queued until an explicit requestSave.
|
|
if (getSaveMode() !== 'auto') return
|
|
drainSnippetQueue()
|
|
})
|
|
const unsubscribeFolders = subscribe(state.pendingFolderSaves, () => {
|
|
drainFolderQueue()
|
|
})
|
|
return () => {
|
|
unsubscribeSnippets()
|
|
unsubscribeFolders()
|
|
}
|
|
}
|
|
|
|
/** Explicit save (e.g. Cmd+S / retry): persist now regardless of save mode. */
|
|
function requestSave(id: string) {
|
|
state.needsSaving.delete(id)
|
|
flushSnippet(id, true)
|
|
}
|
|
|
|
return { start, requestSave }
|
|
}
|
|
|
|
export type SaveScheduler = ReturnType<typeof createSaveScheduler>
|