refactor(drafts): UserDraft.useReactive — kill array-of-one boilerplate

The script + flow routes both wanted a handle that re-keys when the URL
path changes. UserDraft.use() can't do that (its opts getter is
untracked), so each route hand-rolled the same useMany-array-of-one +
proxy idiom:

  const handles = useMany(() => [{ kind, path: reactive }])
  const handle = { get draft() { return handles[0]?.draft }, ... }

Add UserDraft.useReactive(getSpec) that internally wraps useMany with a
single spec and returns the stable proxy. Callers collapse to one line.
This commit is contained in:
Diego Imbert
2026-06-08 15:23:26 +02:00
parent 6d263bdf1e
commit 02b578bca3
3 changed files with 42 additions and 30 deletions
+28 -2
View File
@@ -355,14 +355,40 @@ export const UserDraft = {
// the getter so reactive opts (e.g. `$workspaceStore`) are
// captured once at call time — the current contract is "the
// handle stays bound to this workspace until the component
// unmounts." Use `useMany` directly if you want spec changes to
// release/acquire entries as you go.
// unmounts." For reactive `(kind, path)` use `useReactive`.
const handles = UserDraft.useMany<V>(() =>
untrack(() => [{ itemKind, path, workspace: opts?.workspace }])
)
return handles[0]
},
/**
* Reactive single-spec variant of `use()`. `getSpec` is read inside the
* `useMany` reconcile, so when the spec's `(workspace, kind, path)`
* changes, the previous entry is released and a new one acquired. The
* returned object is a stable handle proxy — its `draft` getter/setter
* forwards to whichever underlying handle is current, so `bind:` lvalues
* survive re-keying.
*
* Use this when the editor's path is reactive (`/scripts/edit/[...path]`
* navigation between paths must re-key the autosave cell). For a
* non-reactive path, `use()` is simpler.
*/
useReactive<V = unknown>(
getSpec: () => { itemKind: UserDraftItemKind; path: string; workspace?: string }
): UserDraftHandle<V> {
const handles = UserDraft.useMany<V>(() => [getSpec()])
return {
get draft(): V | undefined {
return handles[0]?.draft
},
set draft(value: V | undefined) {
const h = handles[0]
if (h) h.draft = value
}
}
},
useMany<V = unknown>(
getSpecs: () => {
itemKind: UserDraftItemKind
@@ -18,7 +18,7 @@
import { tick, untrack } from 'svelte'
import type { stepState } from '$lib/components/stepHistoryLoader.svelte'
import { page } from '$app/state'
import { UserDraft, type UserDraftHandle } from '$lib/userDraft.svelte'
import { UserDraft } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
let version: undefined | number = $state(undefined)
@@ -56,18 +56,12 @@
* deploy lands at this path. */
let isNewFlow = $state(false)
// `useMany` keyed off the reactive `flowDraftPath` re-keys the handle on nav;
// `flowHandle` proxies the current handle so `flowStore` keeps a fixed ref.
const flowHandles = UserDraft.useMany<Flow>(() => [{ itemKind: 'flow', path: flowDraftPath }])
const flowHandle: UserDraftHandle<Flow> = {
get draft() {
return flowHandles[0]?.draft
},
set draft(value) {
const handle = flowHandles[0]
if (handle) handle.draft = value
}
}
// Re-keys the handle on nav (the reactive `flowDraftPath` is read inside
// the reconcile) while keeping `flowStore` a stable ref.
const flowHandle = UserDraft.useReactive<Flow>(() => ({
itemKind: 'flow',
path: flowDraftPath
}))
function emptyFlow(): Flow {
return {
@@ -15,7 +15,7 @@
import { get } from 'svelte/store'
import { untrack } from 'svelte'
import { page } from '$app/state'
import { UserDraft, type UserDraftHandle } from '$lib/userDraft.svelte'
import { UserDraft } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
@@ -35,20 +35,12 @@
// local draft — that view is read-only relative to drafts.
let draftPath = $derived(hash ? '' : (page.params.path ?? ''))
// `useMany` keyed off the reactive `draftPath` re-keys the handle on nav;
// `scriptHandle` proxies the current handle so `bind:script` stays a fixed lvalue.
const scriptHandles = UserDraft.useMany<EditableScript>(() => [
{ itemKind: 'script', path: draftPath }
])
const scriptHandle: UserDraftHandle<EditableScript> = {
get draft() {
return scriptHandles[0]?.draft
},
set draft(value) {
const handle = scriptHandles[0]
if (handle) handle.draft = value
}
}
// Re-keys the handle on nav (the reactive `draftPath` is read inside
// the reconcile) while keeping `bind:script` a stable lvalue.
const scriptHandle = UserDraft.useReactive<EditableScript>(() => ({
itemKind: 'script',
path: draftPath
}))
$effect(() => {
if (hash || !$workspaceStore) return