feat(app): make crash recovery configurable
* feat(app): make crash recovery configurable - Add an enabled-by-default persisted recovery preference and General settings control - Stop recovery writes and remove the active document snapshot when disabled - Suppress startup recovery discovery while the preference is disabled - Cover disabled persistence and re-enable behavior * fix(i18n): translate recovery preferences * fix(app): serialize recovery disable cleanup - Block re-enabled persistence until pending snapshot removal completes - Preserve disable generations so stale cleanup cannot reset newer recovery state - Display runtime-overridden recovery state in Settings - Deep-clone nested preferences before updating recovery
This commit is contained in:
parent
0f981ff2af
commit
f75d67ad4d
|
|
@ -7,6 +7,10 @@
|
|||
"restore": "Wiederherstellen",
|
||||
"discard": "Verwerfen",
|
||||
"recoveryFailed": "Dieses Dokument konnte nicht wiederhergestellt werden.",
|
||||
"settingsRecovery": "Wiederherstellung",
|
||||
"settingsRecoveryDescription": "Lokale Wiederherstellungskopien für ungespeicherte Dokumente verwalten.",
|
||||
"preserveUnsavedWork": "Ungespeicherte Arbeit automatisch sichern",
|
||||
"preserveUnsavedWorkDescription": "Lokale Wiederherstellungskopien speichern, um Dokumente nach einem unerwarteten Beenden wiederherzustellen.",
|
||||
"rename": "Umbenennen",
|
||||
"renameLayers": "{count} Ebenen umbenennen",
|
||||
"renamePreview": "Vorschau",
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "Recuperación",
|
||||
"settingsRecoveryDescription": "Controla las copias locales de recuperación de documentos no guardados.",
|
||||
"preserveUnsavedWork": "Conservar automáticamente el trabajo no guardado",
|
||||
"preserveUnsavedWorkDescription": "Guarda copias locales para restaurar documentos después de un cierre inesperado."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "Récupération",
|
||||
"settingsRecoveryDescription": "Gérez les copies locales de récupération des documents non enregistrés.",
|
||||
"preserveUnsavedWork": "Conserver automatiquement le travail non enregistré",
|
||||
"preserveUnsavedWorkDescription": "Stockez des copies locales afin de restaurer les documents après un arrêt inattendu."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "Ripristino",
|
||||
"settingsRecoveryDescription": "Gestisci le copie locali di ripristino dei documenti non salvati.",
|
||||
"preserveUnsavedWork": "Conserva automaticamente il lavoro non salvato",
|
||||
"preserveUnsavedWorkDescription": "Salva copie locali per ripristinare i documenti dopo una chiusura imprevista."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "復元",
|
||||
"settingsRecoveryDescription": "未保存ドキュメントのローカル復元コピーを管理します。",
|
||||
"preserveUnsavedWork": "未保存の作業を自動的に保持",
|
||||
"preserveUnsavedWorkDescription": "予期しない終了後にドキュメントを復元できるよう、ローカルコピーを保存します。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "Odzyskiwanie",
|
||||
"settingsRecoveryDescription": "Zarządzaj lokalnymi kopiami odzyskiwania niezapisanych dokumentów.",
|
||||
"preserveUnsavedWork": "Automatycznie zachowuj niezapisaną pracę",
|
||||
"preserveUnsavedWorkDescription": "Przechowuj lokalne kopie, aby odtworzyć dokumenty po nieoczekiwanym zamknięciu."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "Восстановление",
|
||||
"settingsRecoveryDescription": "Управляйте локальными резервными копиями несохранённых документов.",
|
||||
"preserveUnsavedWork": "Автоматически сохранять несохранённую работу",
|
||||
"preserveUnsavedWorkDescription": "Храните локальные копии, чтобы восстановить документы после неожиданного завершения работы."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,5 +350,9 @@
|
|||
"mcpStatus_starting": "Starting",
|
||||
"mcpStatus_running": "Running",
|
||||
"mcpStatus_stopped": "Stopped",
|
||||
"mcpStatus_error": "Error"
|
||||
"mcpStatus_error": "Error",
|
||||
"settingsRecovery": "恢复",
|
||||
"settingsRecoveryDescription": "管理未保存文档的本地恢复副本。",
|
||||
"preserveUnsavedWork": "自动保留未保存的工作",
|
||||
"preserveUnsavedWorkDescription": "存储本地恢复副本,以便在意外关闭后恢复文档。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ export const dialogMessageDefaults = {
|
|||
restore: 'Restore',
|
||||
discard: 'Discard',
|
||||
recoveryFailed: 'Could not restore this document.',
|
||||
settingsRecovery: 'Recovery',
|
||||
settingsRecoveryDescription: 'Control local crash-recovery copies for unsaved documents.',
|
||||
preserveUnsavedWork: 'Automatically preserve unsaved work',
|
||||
preserveUnsavedWorkDescription:
|
||||
'Store local recovery copies so documents can be restored after an unexpected shutdown.',
|
||||
showUI: params('Show UI ({shortcut})'),
|
||||
fontSettings: 'Font settings',
|
||||
fontIssueFound: '1 font face is unavailable or substituted',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { createSaveActions } from '@/app/document/io/save'
|
|||
import { createDocumentSourceState } from '@/app/document/io/source-state'
|
||||
import type { DocumentSourceAccess } from '@/app/document/io/types'
|
||||
import { createDocumentRecovery } from '@/app/document/recovery'
|
||||
import { recoveryEnabled } from '@/app/document/recovery/preferences'
|
||||
import type { StorageDocumentBinding } from '@/app/integrations/storage/types'
|
||||
|
||||
type DocumentSourceState = EditorState & {
|
||||
|
|
@ -58,6 +59,7 @@ export function createDocumentSourceActions({
|
|||
|
||||
const recovery = createDocumentRecovery({
|
||||
state,
|
||||
isEnabled: () => recoveryEnabled.value,
|
||||
buildFigFile: buildRecoveryFigFile,
|
||||
hasWritableSource: () => !!getFileHandle() || !!getFilePath() || !!getStorageBinding()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { watchDebounced } from '@vueuse/core'
|
||||
import type { WatchHandle } from 'vue'
|
||||
import { watch, type WatchHandle } from 'vue'
|
||||
|
||||
import type { EditorState } from '@open-pencil/core/editor'
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ interface DocumentRecoveryOptions {
|
|||
state: RecoveryState
|
||||
buildFigFile: () => Promise<Uint8Array> | Uint8Array
|
||||
hasWritableSource: () => boolean
|
||||
isEnabled?: () => boolean
|
||||
store?: RecoveryStore
|
||||
recoveryId?: string
|
||||
}
|
||||
|
|
@ -30,6 +31,7 @@ export function createDocumentRecovery({
|
|||
state,
|
||||
buildFigFile,
|
||||
hasWritableSource,
|
||||
isEnabled = () => true,
|
||||
store = getRecoveryStore(),
|
||||
recoveryId = createCanvasId()
|
||||
}: DocumentRecoveryOptions): DocumentRecoveryController {
|
||||
|
|
@ -39,14 +41,15 @@ export function createDocumentRecovery({
|
|||
let requestedVersion = protectedVersion
|
||||
let lifecycleGeneration = 0
|
||||
let writing: Promise<void> | null = null
|
||||
let cleanup: Promise<void> = Promise.resolve()
|
||||
let disposed = false
|
||||
|
||||
async function runWrites(generation: number): Promise<void> {
|
||||
if (disposed || generation !== lifecycleGeneration) return
|
||||
if (disposed || generation !== lifecycleGeneration || !isEnabled()) return
|
||||
if (hasWritableSource() || requestedVersion === protectedVersion) return
|
||||
const version = requestedVersion
|
||||
const bytes = await buildFigFile()
|
||||
if (generation !== lifecycleGeneration || hasWritableSource()) return
|
||||
if (generation !== lifecycleGeneration || hasWritableSource() || !isEnabled()) return
|
||||
await store.write({
|
||||
id,
|
||||
documentName: state.documentName,
|
||||
|
|
@ -60,7 +63,8 @@ export function createDocumentRecovery({
|
|||
}
|
||||
|
||||
async function persistNow(): Promise<void> {
|
||||
if (disposed || hasWritableSource()) return
|
||||
await cleanup
|
||||
if (disposed || hasWritableSource() || !isEnabled()) return
|
||||
requestedVersion = state.sceneVersion
|
||||
if (requestedVersion === protectedVersion) return
|
||||
if (!writing) {
|
||||
|
|
@ -72,7 +76,7 @@ export function createDocumentRecovery({
|
|||
await writing
|
||||
}
|
||||
|
||||
const stop: WatchHandle = watchDebounced(
|
||||
const stopVersionWatch: WatchHandle = watchDebounced(
|
||||
() => state.sceneVersion,
|
||||
() => {
|
||||
void persistNow().catch((error) => console.warn('[Recovery] Snapshot failed:', error))
|
||||
|
|
@ -80,9 +84,35 @@ export function createDocumentRecovery({
|
|||
{ debounce: 3000, maxWait: 10000 }
|
||||
)
|
||||
|
||||
const stopEnabledWatch: WatchHandle = watch(
|
||||
isEnabled,
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
protectedVersion = state.sceneVersion
|
||||
requestedVersion = state.sceneVersion
|
||||
return
|
||||
}
|
||||
lifecycleGeneration++
|
||||
const cleanupGeneration = lifecycleGeneration
|
||||
const snapshotId = id
|
||||
requestedVersion = state.sceneVersion
|
||||
protectedVersion = state.sceneVersion
|
||||
const activeWrite = writing
|
||||
cleanup = cleanup
|
||||
.then(async () => {
|
||||
await activeWrite
|
||||
await store.remove(snapshotId)
|
||||
if (cleanupGeneration === lifecycleGeneration) persistedVersion = null
|
||||
return undefined
|
||||
})
|
||||
.catch((error) => console.warn('[Recovery] Failed to disable recovery:', error))
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
|
||||
async function invalidateActiveWrite(): Promise<void> {
|
||||
lifecycleGeneration++
|
||||
await writing
|
||||
await Promise.all([writing, cleanup])
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -117,7 +147,8 @@ export function createDocumentRecovery({
|
|||
disposeRecovery() {
|
||||
disposed = true
|
||||
lifecycleGeneration++
|
||||
stop()
|
||||
stopVersionWatch()
|
||||
stopEnabledWatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/app/document/recovery/preferences.ts
Normal file
17
src/app/document/recovery/preferences.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { computed, shallowRef, type ComputedRef } from 'vue'
|
||||
|
||||
import { appPreferences, updateRecoveryEnabled } from '@/app/settings/preferences/store'
|
||||
|
||||
const runtimeOverride = shallowRef<boolean | null>(null)
|
||||
|
||||
export const recoveryEnabled: ComputedRef<boolean> = computed(
|
||||
() => runtimeOverride.value ?? appPreferences.value.recovery.enabled
|
||||
)
|
||||
|
||||
export function setRecoveryEnabled(enabled: boolean): void {
|
||||
updateRecoveryEnabled(enabled)
|
||||
}
|
||||
|
||||
export function setRecoveryRuntimeOverride(enabled: boolean | null): void {
|
||||
runtimeOverride.value = enabled
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import { DEFAULT_SNAPPING_PREFERENCES, type SnappingPreferences } from '@open-pe
|
|||
|
||||
export interface AppPreferences {
|
||||
version: 1
|
||||
recovery: {
|
||||
enabled: boolean
|
||||
}
|
||||
editing: {
|
||||
snapping: SnappingPreferences
|
||||
}
|
||||
|
|
@ -11,6 +14,7 @@ export interface AppPreferences {
|
|||
|
||||
export const DEFAULT_APP_PREFERENCES: Readonly<AppPreferences> = {
|
||||
version: 1,
|
||||
recovery: { enabled: true },
|
||||
editing: {
|
||||
snapping: { ...DEFAULT_SNAPPING_PREFERENCES }
|
||||
}
|
||||
|
|
@ -28,12 +32,9 @@ interface StoredSnappingPreferences {
|
|||
pixelGrid?: unknown
|
||||
}
|
||||
|
||||
interface StoredEditingPreferences {
|
||||
snapping?: StoredSnappingPreferences
|
||||
}
|
||||
|
||||
interface StoredAppPreferences {
|
||||
editing?: StoredEditingPreferences
|
||||
recovery?: { enabled?: unknown }
|
||||
editing?: { snapping?: StoredSnappingPreferences }
|
||||
}
|
||||
|
||||
function isStoredAppPreferences(value: unknown): value is StoredAppPreferences {
|
||||
|
|
@ -41,10 +42,14 @@ function isStoredAppPreferences(value: unknown): value is StoredAppPreferences {
|
|||
}
|
||||
|
||||
function normalizePreferences(value: unknown): AppPreferences {
|
||||
const snapping = isStoredAppPreferences(value) ? value.editing?.snapping : undefined
|
||||
const stored = isStoredAppPreferences(value) ? value : undefined
|
||||
const snapping = stored?.editing?.snapping
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
recovery: {
|
||||
enabled: booleanOrDefault(stored?.recovery?.enabled, DEFAULT_APP_PREFERENCES.recovery.enabled)
|
||||
},
|
||||
editing: {
|
||||
snapping: {
|
||||
geometry: booleanOrDefault(
|
||||
|
|
@ -70,6 +75,12 @@ export const appPreferences = useLocalStorage<AppPreferences>(
|
|||
{ mergeDefaults: (storageValue) => normalizePreferences(storageValue) }
|
||||
)
|
||||
|
||||
export function updateRecoveryEnabled(enabled: boolean): void {
|
||||
const preferences = structuredClone(appPreferences.value)
|
||||
preferences.recovery.enabled = enabled
|
||||
appPreferences.value = preferences
|
||||
}
|
||||
|
||||
export function updateSnappingPreferences(changes: Partial<SnappingPreferences>): void {
|
||||
appPreferences.value = {
|
||||
...appPreferences.value,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
|||
import { AlertDialogCancel, AlertDialogDescription, AlertDialogTitle } from 'reka-ui'
|
||||
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
import { recoveryEnabled } from '@/app/document/recovery/preferences'
|
||||
import { useNotificationMessages } from '@/app/i18n/notifications'
|
||||
import { discardRecoverySnapshot, listRecoverySnapshots, restoreRecoverySnapshot } from '@/app/tabs'
|
||||
import type { RecoverySnapshotMeta } from '@/app/document/recovery'
|
||||
|
|
@ -54,7 +55,7 @@ async function discard(snapshot: RecoverySnapshotMeta): Promise<void> {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.path !== '/') return
|
||||
if (route.path !== '/' || !recoveryEnabled.value) return
|
||||
try {
|
||||
snapshots.value = await listRecoverySnapshots()
|
||||
open.value = snapshots.value.length > 0
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@
|
|||
import { computed } from 'vue'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
import { recoveryEnabled, setRecoveryEnabled } from '@/app/document/recovery/preferences'
|
||||
import { setSnappingPreference } from '@/app/settings/preferences/apply'
|
||||
import { appPreferences } from '@/app/settings/preferences/store'
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue'
|
||||
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const preserveUnsavedWork = computed({
|
||||
get: () => recoveryEnabled.value,
|
||||
set: setRecoveryEnabled
|
||||
})
|
||||
|
||||
const snapToGeometry = computed({
|
||||
get: () => appPreferences.value.editing.snapping.geometry,
|
||||
set: (enabled: boolean) => setSnappingPreference('geometry', enabled)
|
||||
|
|
@ -26,6 +32,27 @@ const snapToPixelGrid = computed({
|
|||
|
||||
<template>
|
||||
<section class="flex flex-col gap-4" data-test-id="settings-general-panel">
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-surface">{{ dialogs.settingsRecovery }}</h3>
|
||||
<p class="mt-1 text-[11px] text-muted">{{ dialogs.settingsRecoveryDescription }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col rounded border border-border">
|
||||
<label class="flex items-center justify-between gap-4 px-3 py-2.5">
|
||||
<span>
|
||||
<span class="block text-xs text-surface">{{ dialogs.preserveUnsavedWork }}</span>
|
||||
<span class="block text-[10px] text-muted">{{
|
||||
dialogs.preserveUnsavedWorkDescription
|
||||
}}</span>
|
||||
</span>
|
||||
<AppSwitch
|
||||
v-model="preserveUnsavedWork"
|
||||
:label="dialogs.preserveUnsavedWork"
|
||||
data-test-id="settings-recovery-enabled"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-surface">{{ dialogs.settingsEditing }}</h3>
|
||||
<p class="mt-1 text-[11px] text-muted">{{ dialogs.settingsSnappingDescription }}</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { reactive } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
import { createDefaultEditorState } from '@open-pencil/core/editor'
|
||||
|
||||
|
|
@ -23,18 +23,45 @@ function deferredWriteStore() {
|
|||
return { store, release: () => release?.() }
|
||||
}
|
||||
|
||||
function setup(buildFigFile = async () => new Uint8Array([1, 2, 3])) {
|
||||
function deferredRemoveStore() {
|
||||
const memory = createMemoryRecoveryStore()
|
||||
let release: (() => void) | null = null
|
||||
const store: RecoveryStore = {
|
||||
...memory,
|
||||
async remove(id: string) {
|
||||
await new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
await memory.remove(id)
|
||||
}
|
||||
}
|
||||
return { store, release: () => release?.() }
|
||||
}
|
||||
|
||||
function setup(
|
||||
buildFigFile = async () => new Uint8Array([1, 2, 3]),
|
||||
initialEnabled = true,
|
||||
injectedStore?: RecoveryStore
|
||||
) {
|
||||
const state = reactive({ ...createDefaultEditorState('page-1'), documentName: 'Agent draft' })
|
||||
const store = createMemoryRecoveryStore()
|
||||
const store = injectedStore ?? createMemoryRecoveryStore()
|
||||
let writable = false
|
||||
const enabled = ref(initialEnabled)
|
||||
const recovery = createDocumentRecovery({
|
||||
state,
|
||||
store,
|
||||
recoveryId: 'recovery-1',
|
||||
hasWritableSource: () => writable,
|
||||
isEnabled: () => enabled.value,
|
||||
buildFigFile
|
||||
})
|
||||
return { state, store, recovery, setWritable: (value: boolean) => (writable = value) }
|
||||
return {
|
||||
state,
|
||||
store,
|
||||
recovery,
|
||||
setWritable: (value: boolean) => (writable = value),
|
||||
setEnabled: (value: boolean) => (enabled.value = value)
|
||||
}
|
||||
}
|
||||
|
||||
describe('document recovery controller', () => {
|
||||
|
|
@ -49,6 +76,60 @@ describe('document recovery controller', () => {
|
|||
recovery.disposeRecovery()
|
||||
})
|
||||
|
||||
test('does not serialize or persist while disabled', async () => {
|
||||
let builds = 0
|
||||
const { state, store, recovery } = setup(async () => {
|
||||
builds++
|
||||
return new Uint8Array([1])
|
||||
}, false)
|
||||
state.sceneVersion = 1
|
||||
await recovery.persistNow()
|
||||
expect(builds).toBe(0)
|
||||
expect(await store.list()).toEqual([])
|
||||
recovery.disposeRecovery()
|
||||
})
|
||||
|
||||
test('removes the owned snapshot when disabled and resumes from the current version', async () => {
|
||||
const { state, store, recovery, setEnabled } = setup()
|
||||
state.sceneVersion = 1
|
||||
await recovery.persistNow()
|
||||
expect(await store.list()).toHaveLength(1)
|
||||
|
||||
setEnabled(false)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(await store.list()).toEqual([])
|
||||
|
||||
state.sceneVersion = 2
|
||||
setEnabled(true)
|
||||
await recovery.persistNow()
|
||||
expect(await store.list()).toEqual([])
|
||||
|
||||
state.sceneVersion = 3
|
||||
await recovery.persistNow()
|
||||
expect((await store.read('recovery-1'))?.sceneVersion).toBe(3)
|
||||
recovery.disposeRecovery()
|
||||
})
|
||||
|
||||
test('waits for disable cleanup before writing after re-enable', async () => {
|
||||
const deferred = deferredRemoveStore()
|
||||
const { state, store, recovery, setEnabled } = setup(undefined, true, deferred.store)
|
||||
state.sceneVersion = 1
|
||||
await recovery.persistNow()
|
||||
|
||||
setEnabled(false)
|
||||
setEnabled(true)
|
||||
state.sceneVersion = 2
|
||||
const nextWrite = recovery.persistNow()
|
||||
await Promise.resolve()
|
||||
expect((await store.read('recovery-1'))?.sceneVersion).toBe(1)
|
||||
|
||||
deferred.release()
|
||||
await nextWrite
|
||||
expect((await store.read('recovery-1'))?.sceneVersion).toBe(2)
|
||||
recovery.disposeRecovery()
|
||||
})
|
||||
|
||||
test('does not persist documents with writable sources', async () => {
|
||||
const { state, store, recovery, setWritable } = setup()
|
||||
setWritable(true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue