Skip to content

Commit

Permalink
refactor(Project): ensure pending auto-save is cancelled after `dispo…
Browse files Browse the repository at this point in the history
…se` is called

This also removes `@utils/utils.debounce` in favor of `lodash.debounce`.

Signed-off-by: Aofei Sheng <[email protected]>
  • Loading branch information
aofei committed Aug 23, 2024
1 parent 53e3ab0 commit 43de6ca
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 33 deletions.
2 changes: 1 addition & 1 deletion spx-gui/src/components/asset/library/AssetLibraryModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ import {
UIDivider
} from '@/components/ui'
import { listAsset, AssetType, type AssetData, IsPublic } from '@/apis/asset'
import { debounce } from '@/utils/utils'
import { debounce } from 'lodash'
import { useMessageHandle, useQuery } from '@/utils/exception'
import { type Category, categories as categoriesWithoutAll, categoryAll } from './category'
import { type Project } from '@/models/project'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ import {
UIButtonGroupItem
} from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { debounce, round } from '@/utils/utils'
import { round } from '@/utils/utils'
import { debounce } from 'lodash'
import {
RotationStyle,
LeftRight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ import {
UIButtonGroupItem,
UIIcon
} from '@/components/ui'
import { debounce, round } from '@/utils/utils'
import { round } from '@/utils/utils'
import { debounce } from 'lodash'
import { useMessageHandle } from '@/utils/exception'
import type { Monitor } from '@/models/widget/monitor'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/ui/form/UIFormItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<script setup lang="ts">
import { useSlots, computed } from 'vue'
import { NFormItem } from 'naive-ui'
import { debounce } from '@/utils/utils'
import { debounce } from 'lodash'
import UIFormItemInternal from './UIFormItemInternal.vue'
import { useForm } from './UIForm.vue'
Expand Down
50 changes: 50 additions & 0 deletions spx-gui/src/models/project/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ describe('ProjectAutoSave', () => {
await flushPromises()
await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
await flushPromises()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)

await vi.advanceTimersByTimeAsync(1500) // wait for auto-save to trigger
Expand All @@ -180,6 +181,7 @@ describe('ProjectAutoSave', () => {
await flushPromises()
await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
await flushPromises()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Failed)
expect(project.hasUnsyncedChanges).toBe(false)

await vi.advanceTimersByTimeAsync(5000) // wait for auto-retry to trigger
Expand All @@ -190,4 +192,52 @@ describe('ProjectAutoSave', () => {
expect(localSaveMock).toHaveBeenCalledTimes(1)
expect(localClearMock).toHaveBeenCalledTimes(1)
})

it('should cancel pending auto-save-to-cloud when project is disposed', async () => {
const project = makeProject()

const cloudSaveMock = vi.spyOn(cloudHelper, 'save').mockRejectedValue(undefined)

await project.startEditing('localCacheKey')
project.setAutoSaveMode(AutoSaveMode.Cloud)

const newSprite = new Sprite('newSprite')
project.addSprite(newSprite)
await flushPromises()
await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
await flushPromises()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)

project.dispose()

await vi.advanceTimersByTimeAsync(1500 * 2) // wait longer to ensure auto-save does not trigger
await flushPromises()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)
expect(cloudSaveMock).toHaveBeenCalledTimes(0)
})

it('should cancel pending auto-save-to-local-cache when project is disposed', async () => {
const project = makeProject()

const localSaveMock = vi.spyOn(localHelper, 'save').mockResolvedValue(undefined)

await project.startEditing('localCacheKey')
project.setAutoSaveMode(AutoSaveMode.LocalCache)

const newSprite = new Sprite('newSprite')
project.addSprite(newSprite)
await flushPromises()
await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
await flushPromises()
expect(project.hasUnsyncedChanges).toBe(true)

project.dispose()

await vi.advanceTimersByTimeAsync(1000 * 2) // wait longer to ensure auto-save does not trigger
await flushPromises()
expect(project.hasUnsyncedChanges).toBe(true)
expect(localSaveMock).toHaveBeenCalledTimes(0)
})
})
34 changes: 16 additions & 18 deletions spx-gui/src/models/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { reactive, watch } from 'vue'

import { join } from '@/utils/path'
import { debounce } from '@/utils/utils'
import { debounce } from 'lodash'
import { Disposable } from '@/utils/disposable'
import { IsPublic, type ProjectData } from '@/apis/project'
import { toConfig, type Files, fromConfig } from '../common/file'
Expand Down Expand Up @@ -432,7 +432,7 @@ export class Project extends Disposable {
this.autoSaveToCloudState = AutoSaveToCloudState.Saved
} catch (e) {
this.autoSaveToCloudState = AutoSaveToCloudState.Failed
startRetry()
retryAutoSave()
await this.saveToLocalCache(localCacheKey) // prevent data loss
console.error('failed to auto save to cloud', e)
return
Expand All @@ -441,25 +441,21 @@ export class Project extends Disposable {
if (this.hasUnsyncedChanges) autoSaveToCloud()
else await localHelper.clear(localCacheKey)
}, 1500)
this.addDisposer(save.cancel)

let retryTimeoutId: ReturnType<typeof setTimeout>
const startRetry = () => {
stopRetry()
retryTimeoutId = setTimeout(async () => {
if (this.autoSaveToCloudState !== AutoSaveToCloudState.Failed) return
if (this.hasUnsyncedChanges) {
autoSaveToCloud()
} else {
this.autoSaveToCloudState = AutoSaveToCloudState.Saved
await localHelper.clear(localCacheKey)
}
}, 5000)
}
const stopRetry = () => clearTimeout(retryTimeoutId)
this.addDisposer(stopRetry)
const retryAutoSave = debounce(async () => {
if (this.autoSaveToCloudState !== AutoSaveToCloudState.Failed) return
if (this.hasUnsyncedChanges) {
autoSaveToCloud()
} else {
this.autoSaveToCloudState = AutoSaveToCloudState.Saved
await localHelper.clear(localCacheKey)
}
}, 5000)
this.addDisposer(retryAutoSave.cancel)

return () => {
stopRetry()
retryAutoSave.cancel()
if (this.autoSaveToCloudState !== AutoSaveToCloudState.Saving)
this.autoSaveToCloudState = AutoSaveToCloudState.Pending
if (this.autoSaveMode === AutoSaveMode.Cloud) save()
Expand All @@ -483,12 +479,14 @@ export class Project extends Disposable {
// watch for all changes, auto save to local cache, or touch all game files to trigger lazy loading to ensure they are in memory
const autoSaveToLocalCache = (() => {
const save = debounce(() => this.saveToLocalCache(localCacheKey), 1000)
this.addDisposer(save.cancel)

const delazyLoadGameFiles = debounce(() => {
const files = this.exportGameFiles()
const fileList = Object.keys(files)
fileList.map((path) => files[path]!.arrayBuffer())
}, 1000)
this.addDisposer(delazyLoadGameFiles.cancel)

return () => {
if (this.autoSaveMode === AutoSaveMode.LocalCache) save()
Expand Down
11 changes: 0 additions & 11 deletions spx-gui/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,6 @@ export const isSound = (url: string): boolean => {
return ['wav', 'mp3', 'ogg'].includes(extension)
}

export function debounce<T extends (...args: any[]) => any>(func: T, delay: number = 300) {
let timeoutId: ReturnType<typeof setTimeout>
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const context = this
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
func.apply(context, args)
}, delay)
}
}

/**
* If add-to-public-library features are enabled.
* In release v1.3, we do not allow users to add asset to public library (the corresponding features are disabled).
Expand Down

0 comments on commit 43de6ca

Please sign in to comment.