refactor: drop await on draft-delete in reset flows, refetch deployed directly

This commit is contained in:
Diego Imbert
2026-06-04 15:03:09 +02:00
parent 8ceb71dcdc
commit 1ecc8411e4
17 changed files with 131 additions and 80 deletions
@@ -58,19 +58,21 @@
// separate `.draft` field), so `saved.is_draft` is the signal that
// there's actually a draft worth deleting; the syncer's
// `value: null` POST is the canonical per-user delete.
//
// Fire-and-forget: every read here (`saved`, the snapshot we build
// below, the UserDraft.discard write) is purely in-memory, so we
// don't need the DELETE to have landed to finish the restore. We
// flip `is_draft` optimistically so the UI matches the new intent
// immediately. A failed DELETE only matters across a hard reload
// before it lands — log and move on.
if (saved.is_draft) {
try {
await UserDraftDbSyncer.save({
workspace: workspaceId,
itemKind: 'script',
path: saved.path,
value: null
})
saved.is_draft = false
} catch (e: any) {
sendUserToast(`Could not delete draft: ${e?.body ?? e}`, true)
return
}
saved.is_draft = false
UserDraftDbSyncer.save({
workspace: workspaceId,
itemKind: 'script',
path: saved.path,
value: null
}).catch((e) => console.error('restoreDeployed: draft delete failed', e))
}
const deployed = structuredClone($state.snapshot(saved)) as NewScript & { draft?: unknown }
delete deployed.draft
@@ -185,7 +185,11 @@
}
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -194,7 +198,7 @@
const s = await AzureTriggerService.getAzureTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -203,7 +207,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -207,7 +207,11 @@
preservePermissionedAs = !!cfg?.permissioned_as
}
async function loadTrigger(defaultConfig?: Partial<EmailTrigger>): Promise<void> {
async function loadTrigger(
defaultConfig?: Partial<EmailTrigger>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -215,7 +219,7 @@
const s = await EmailTriggerService.getEmailTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -224,7 +228,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -189,7 +189,11 @@
}
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -198,7 +202,7 @@
const s = await GcpTriggerService.getGcpTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -207,7 +211,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -340,7 +340,11 @@
preservePermissionedAs = !!cfg?.permissioned_as
}
async function loadTrigger(defaultConfig?: Partial<HttpTrigger>): Promise<void> {
async function loadTrigger(
defaultConfig?: Partial<HttpTrigger>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -348,7 +352,7 @@
const s = await HttpTriggerService.getHttpTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -357,7 +361,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -246,7 +246,11 @@
preservePermissionedAs = !!cfg?.permissioned_as
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -254,7 +258,7 @@
const s = await KafkaTriggerService.getKafkaTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -263,7 +267,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -245,7 +245,11 @@
}
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
try {
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
@@ -254,7 +258,7 @@
const s = await MqttTriggerService.getMqttTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -263,7 +267,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -228,7 +228,11 @@
preservePermissionedAs = !!cfg?.permissioned_as
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -236,7 +240,7 @@
const s = await NatsTriggerService.getNatsTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -245,7 +249,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -368,7 +368,11 @@
preservePermissionedAs = !!cfg?.permissioned_as
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
if (defaultConfig?.publication) {
@@ -380,7 +384,7 @@
const s = await PostgresTriggerService.getPostgresTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -389,7 +393,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -278,8 +278,10 @@
nis_flow: boolean,
initial_script_path?: string,
defaultValues?: Schedule,
schedule_path?: string
schedule_path?: string,
opts: { getDraft?: boolean } = {}
) {
const getDraft = opts.getDraft ?? true
let loadingTimeout = setTimeout(() => {
showLoading = true
}, 100) // Do not show loading spinner for the first 100ms
@@ -290,7 +292,7 @@
const resp = await ScheduleService.getSchedule({
workspace: $workspaceStore!,
path: schedule_path,
getDraft: true
getDraft
})
if (resp.is_draft) {
notifyDraftLoaded({
@@ -299,7 +301,9 @@
path: schedule_path,
draftOnly: resp.no_deployed,
onResetToDeployed: async () => {
await openNew(nis_flow, initial_script_path, defaultValues, schedule_path)
await openNew(nis_flow, initial_script_path, defaultValues, schedule_path, {
getDraft: false
})
}
})
}
@@ -466,13 +470,17 @@
}
}
async function loadSchedule(defaultCfg?: Record<string, any>): Promise<void> {
async function loadSchedule(
defaultCfg?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (!defaultCfg) {
try {
const s = await ScheduleService.getSchedule({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s.is_draft) {
notifyDraftLoaded({
@@ -481,7 +489,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadSchedule()
await loadSchedule(undefined, { getDraft: false })
}
})
}
@@ -222,7 +222,11 @@
}
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
try {
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
@@ -231,7 +235,7 @@
const s = await SqsTriggerService.getSqsTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -240,7 +244,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
@@ -303,7 +303,11 @@
}
}
async function loadTrigger(defaultConfig?: Record<string, any>): Promise<void> {
async function loadTrigger(
defaultConfig?: Record<string, any>,
opts: { getDraft?: boolean } = {}
): Promise<void> {
const getDraft = opts.getDraft ?? true
if (defaultConfig) {
loadTriggerConfig(defaultConfig)
return
@@ -311,7 +315,7 @@
const s = await WebsocketTriggerService.getWebsocketTrigger({
workspace: $workspaceStore!,
path: initialPath,
getDraft: true
getDraft
})
if (s?.is_draft) {
notifyDraftLoaded({
@@ -320,7 +324,7 @@
path: initialPath,
draftOnly: s.no_deployed,
onResetToDeployed: async () => {
await loadTrigger()
await loadTrigger(undefined, { getDraft: false })
}
})
}
+14 -16
View File
@@ -43,11 +43,14 @@ export function notifyRestoredFromLocal(
* draft) but omit the action.
*
* The action:
* 1. POSTs `value: null` to delete the user's draft for this entity
* 2. invokes `onResetToDeployed` so the editor can re-fetch without
* the overlay and apply the pure deployed version
* Failures in either step surface a follow-up toast and leave the editor
* state untouched.
* 1. Fires `value: null` at the syncer (fire-and-forget — the user's
* delete races no reader; `onResetToDeployed` doesn't depend on it).
* 2. invokes `onResetToDeployed`, which MUST fetch deployed directly
* (e.g. `getDraft: false`) instead of trusting that the delete has
* already landed in the DB.
*
* A delete failure logs to console only — there's no UI state that
* depends on it. A reset-callback failure surfaces a toast.
*/
export function notifyDraftLoaded(opts: {
workspace: string
@@ -64,17 +67,12 @@ export function notifyDraftLoaded(opts: {
{
label: 'Reset to deployed',
callback: async () => {
try {
await UserDraftDbSyncer.save({
workspace: opts.workspace,
itemKind: opts.itemKind,
path: opts.path,
value: null
})
} catch (e: any) {
sendUserToast(`Could not delete draft: ${e?.body ?? e}`, true)
return
}
UserDraftDbSyncer.save({
workspace: opts.workspace,
itemKind: opts.itemKind,
path: opts.path,
value: null
}).catch((e) => console.error('Reset to deployed: draft delete failed', e))
try {
await opts.onResetToDeployed()
} catch (e: any) {
@@ -76,7 +76,8 @@
* navigation races a draft-discard reload) bail at the next checkpoint
* after their captured token no longer matches. */
let loadAppToken = 0
async function loadApp(): Promise<void> {
async function loadApp(opts: { getDraft?: boolean } = {}): Promise<void> {
const getDraft = opts.getDraft ?? true
const tok = ++loadAppToken
// `?new_draft=true` (set by `/apps/add`'s redirect) means we landed
// on a fresh `u/{user}/draft_{uuid}` path that's never been saved.
@@ -115,7 +116,7 @@
const backendApp = await AppService.getAppByPath({
path: page.params.path ?? '',
workspace: $workspaceStore!,
getDraft: true
getDraft
})
if (tok !== loadAppToken) return
if (backendApp.is_draft) {
@@ -126,7 +127,7 @@
draftOnly: backendApp.no_deployed,
onResetToDeployed: async () => {
UserDraft.remove('app', path)
await loadApp()
await loadApp({ getDraft: false })
redraw++
}
})
@@ -162,7 +162,8 @@
* navigation races a draft-discard reload) bail at the next checkpoint
* after their captured token no longer matches. */
let loadAppToken = 0
async function loadApp(): Promise<void> {
async function loadApp(opts: { getDraft?: boolean } = {}): Promise<void> {
const getDraft = opts.getDraft ?? true
const tok = ++loadAppToken
// `?new_draft=true` (set by `/apps_raw/add`'s redirect) means we
// landed on a fresh `u/{user}/draft_{uuid}` path that's never
@@ -194,7 +195,7 @@
const backendApp = (await AppService.getAppByPath({
path: page.params.path ?? '',
workspace: $workspaceStore!,
getDraft: true,
getDraft,
rawApp: true
})) as any
if (tok !== loadAppToken) return
@@ -206,7 +207,7 @@
draftOnly: backendApp.no_deployed,
onResetToDeployed: async () => {
draftHandle.setDraftAndMeta(undefined, {})
await loadApp()
await loadApp({ getDraft: false })
}
})
}
@@ -147,7 +147,8 @@
* (e.g. picker navigation while a draft-discard reload is in flight),
* the older promise no-ops at the next checkpoint. */
let loadFlowToken = 0
async function loadFlow(): Promise<void> {
async function loadFlow(opts: { getDraft?: boolean } = {}): Promise<void> {
const getDraft = opts.getDraft ?? true
const tok = ++loadFlowToken
loading = true
let flow: Flow
@@ -214,7 +215,7 @@
const backendFlow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: page.params.path ?? '',
getDraft: true
getDraft
})
if (tok !== loadFlowToken) return
if (backendFlow.is_draft) {
@@ -225,7 +226,7 @@
draftOnly: backendFlow.no_deployed,
onResetToDeployed: async () => {
flowHandle.setDraftAndMeta(undefined, {})
await loadFlow()
await loadFlow({ getDraft: false })
}
})
}
@@ -135,7 +135,8 @@
* navigation races a draft-discard reload) bail at the next checkpoint
* after their captured token no longer matches. */
let loadScriptToken = 0
async function loadScript(): Promise<void> {
async function loadScript(opts: { getDraft?: boolean } = {}): Promise<void> {
const getDraft = opts.getDraft ?? true
const tok = ++loadScriptToken
fullyLoaded = false
// `?new_draft=true` (set by `/scripts/add`'s redirect) means we
@@ -179,7 +180,7 @@
const backendScript = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: page.params.path ?? '',
getDraft: true
getDraft
})
if (tok !== loadScriptToken) return
if (backendScript.is_draft) {
@@ -189,12 +190,11 @@
path: page.params.path ?? '',
draftOnly: backendScript.no_deployed,
onResetToDeployed: async () => {
// Drop the in-memory draft so loadScript's no-localDraft
// branch fires; the next fetch with `getDraft: true` will
// return pure deployed because we just deleted the draft
// row server-side.
// Drop the in-memory draft and refetch *without* the
// draft overlay — we don't trust the eventual delete
// to have landed, so we read deployed directly.
scriptHandle.setDraftAndMeta(undefined, {})
await loadScript()
await loadScript({ getDraft: false })
}
})
}