refactor: avoid deallocing note controller until local propagation complete
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
|||||||
SNTag,
|
SNTag,
|
||||||
ItemsClientInterface,
|
ItemsClientInterface,
|
||||||
SNNote,
|
SNNote,
|
||||||
|
Deferred,
|
||||||
|
SyncServiceInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||||
import { NoteViewController } from './NoteViewController'
|
import { NoteViewController } from './NoteViewController'
|
||||||
@@ -17,10 +19,16 @@ describe('note view controller', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
application = {} as jest.Mocked<WebApplication>
|
application = {} as jest.Mocked<WebApplication>
|
||||||
application.streamItems = jest.fn()
|
application.streamItems = jest.fn().mockReturnValue(() => {})
|
||||||
application.getPreference = jest.fn().mockReturnValue(true)
|
application.getPreference = jest.fn().mockReturnValue(true)
|
||||||
|
application.noAccount = jest.fn().mockReturnValue(false)
|
||||||
|
application.isNativeMobileWeb = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemsClientInterface> })
|
Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemsClientInterface> })
|
||||||
|
|
||||||
|
Object.defineProperty(application, 'sync', { value: {} as jest.Mocked<SyncServiceInterface> })
|
||||||
|
application.sync.sync = jest.fn().mockReturnValue(Promise.resolve())
|
||||||
|
|
||||||
componentManager = {} as jest.Mocked<SNComponentManager>
|
componentManager = {} as jest.Mocked<SNComponentManager>
|
||||||
componentManager.legacyGetDefaultEditor = jest.fn()
|
componentManager.legacyGetDefaultEditor = jest.fn()
|
||||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||||
@@ -80,4 +88,30 @@ describe('note view controller', () => {
|
|||||||
expect(controller['defaultTag']).toEqual(tag)
|
expect(controller['defaultTag']).toEqual(tag)
|
||||||
expect(application.items.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything())
|
expect(application.items.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should wait until item finishes saving locally before deiniting', async () => {
|
||||||
|
const note = {
|
||||||
|
uuid: 'note-uuid',
|
||||||
|
} as jest.Mocked<SNNote>
|
||||||
|
|
||||||
|
application.items.findItem = jest.fn().mockReturnValue(note)
|
||||||
|
|
||||||
|
const controller = new NoteViewController(application, note)
|
||||||
|
await controller.initialize()
|
||||||
|
|
||||||
|
const changePromise = Deferred()
|
||||||
|
|
||||||
|
application.mutator.changeItem = jest.fn().mockReturnValue(changePromise.promise)
|
||||||
|
|
||||||
|
const savePromise = controller.saveAndAwaitLocalPropagation({ isUserModified: true, bypassDebouncer: true })
|
||||||
|
controller.deinit()
|
||||||
|
|
||||||
|
expect(controller.dealloced).toEqual(false)
|
||||||
|
|
||||||
|
changePromise.resolve(true)
|
||||||
|
await changePromise.promise
|
||||||
|
await savePromise
|
||||||
|
|
||||||
|
expect(controller.dealloced).toEqual(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
PrefKey,
|
PrefKey,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { UuidString } from '@standardnotes/snjs'
|
import { UuidString } from '@standardnotes/snjs'
|
||||||
import { removeFromArray } from '@standardnotes/utils'
|
import { removeFromArray, Deferred } from '@standardnotes/utils'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||||
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||||
@@ -29,14 +29,16 @@ const NotePreviewCharLimit = 160
|
|||||||
export class NoteViewController implements ItemViewControllerInterface {
|
export class NoteViewController implements ItemViewControllerInterface {
|
||||||
public item!: SNNote
|
public item!: SNNote
|
||||||
public dealloced = false
|
public dealloced = false
|
||||||
|
public isTemplateNote = false
|
||||||
|
public runtimeId = `${Math.random()}`
|
||||||
|
public needsInit = true
|
||||||
|
|
||||||
private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = []
|
private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = []
|
||||||
private disposers: (() => void)[] = []
|
private disposers: (() => void)[] = []
|
||||||
public isTemplateNote = false
|
|
||||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||||
private defaultTagUuid: UuidString | undefined
|
private defaultTagUuid: UuidString | undefined
|
||||||
private defaultTag?: SNTag
|
private defaultTag?: SNTag
|
||||||
public runtimeId = `${Math.random()}`
|
private savingLocallyPromise = Deferred<void>()
|
||||||
public needsInit = true
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private application: WebApplication,
|
private application: WebApplication,
|
||||||
@@ -57,7 +59,14 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit(): void {
|
deinit(): void {
|
||||||
|
void this.savingLocallyPromise.promise.then(() => {
|
||||||
|
this.performDeinitSafely()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private performDeinitSafely(): void {
|
||||||
this.dealloced = true
|
this.dealloced = true
|
||||||
|
|
||||||
for (const disposer of this.disposers) {
|
for (const disposer of this.disposers) {
|
||||||
disposer()
|
disposer()
|
||||||
}
|
}
|
||||||
@@ -184,6 +193,8 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
throw Error('NoteViewController not initialized')
|
throw Error('NoteViewController not initialized')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.savingLocallyPromise = Deferred<void>()
|
||||||
|
|
||||||
if (this.saveTimeout) {
|
if (this.saveTimeout) {
|
||||||
clearTimeout(this.saveTimeout)
|
clearTimeout(this.saveTimeout)
|
||||||
}
|
}
|
||||||
@@ -198,7 +209,13 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.saveTimeout = setTimeout(() => {
|
this.saveTimeout = setTimeout(() => {
|
||||||
void this.undebouncedSave({ ...params, onLocalPropagationComplete: resolve })
|
void this.undebouncedSave({
|
||||||
|
...params,
|
||||||
|
onLocalPropagationComplete: () => {
|
||||||
|
this.savingLocallyPromise.resolve()
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
})
|
||||||
}, syncDebouceMs)
|
}, syncDebouceMs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -262,10 +279,10 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
params.isUserModified,
|
params.isUserModified,
|
||||||
)
|
)
|
||||||
|
|
||||||
params.onLocalPropagationComplete?.()
|
|
||||||
|
|
||||||
void this.application.sync.sync().then(() => {
|
void this.application.sync.sync().then(() => {
|
||||||
params.onRemoteSyncComplete?.()
|
params.onRemoteSyncComplete?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
params.onLocalPropagationComplete?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user