Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-Unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [LokiJS] Multitab sync issue fix
- [Android] Added linker flag for building with 16kB page alignment
- [TS] make catchError visible to typescript
- [LokiJS] Fixed `unsafeResetDatabase()` failing with "another operation is already in progress" when it's called while an autosave is in progress

### Performance

Expand Down
75 changes: 75 additions & 0 deletions src/adapters/lokijs/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,81 @@ describe('LokiJSAdapter (Synchronous / Memory persistence)', () => {
})
})

// Mimics IncrementalIndexedDBAdapter, which throws if asked to do something while an operation is
// in progress. While `holdSaves` is set, saves stay in progress until `finishSave()` is called
class SlowIncrementalAdapter {
mode = 'incremental'

operationInProgress = false

holdSaves = false

finishSave = null

deleteCount = 0

loadDatabase(dbname, callback) {
callback(null)
}

saveDatabase(dbname, getLokiCopy, callback) {
if (this.operationInProgress) {
throw new Error('Error while saving to database - another operation is already in progress')
}
this.operationInProgress = true
getLokiCopy()
const finish = () => {
this.operationInProgress = false
callback()
}
if (this.holdSaves) {
this.finishSave = finish
} else {
setTimeout(finish, 1)
}
}

deleteDatabase(dbname, callback) {
if (this.operationInProgress) {
throw new Error('Error while deleting database - another operation is already in progress')
}
this.deleteCount += 1
callback({ success: true })
}
}

describe('LokiJSAdapter (Incremental persistence)', () => {
it('waits for a save in progress before resetting database', async () => {
const persistenceAdapter = new SlowIncrementalAdapter()
const adapter = new DatabaseAdapterCompat(
new LokiJSAdapter({
dbName: `test${Math.random()}`,
schema: testSchema,
useWebWorker: false,
useIncrementalIndexedDB: true,
_testLokiAdapter: persistenceAdapter,
}),
)
await adapter.setLocal('foo', 'bar')
const deleteCount = persistenceAdapter.deleteCount

// start a save, which clears Loki's dirty flags while it's still in progress
persistenceAdapter.holdSaves = true
await adapter.unsafeExecute({ loki: (loki) => loki.saveDatabase() })
persistenceAdapter.holdSaves = false
expect(persistenceAdapter.operationInProgress).toBe(true)

const reset = adapter.unsafeResetDatabase()
await new Promise((resolve) => setTimeout(resolve, 10))
expect(persistenceAdapter.deleteCount).toBe(deleteCount)

persistenceAdapter.finishSave()
await reset
expect(persistenceAdapter.deleteCount).toBe(deleteCount + 1)
expect(await adapter.getLocal('foo')).toBe(null)
})
})

// TODO: Run tests with:
// - mocked/polyfilled web worker
// - legacy indexeddb adapter (fake-indexeddb polyfill)
Expand Down
32 changes: 25 additions & 7 deletions src/adapters/lokijs/worker/lokiExtensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,37 @@ export async function deleteDatabase(loki: Loki): Promise<void> {
// Works around a race condition - Loki doesn't disable autosave or drain save queue before
// deleting database, so it's possible to delete and then have the database be saved
loki.close(() => {
loki.deleteDatabase({}, (response) => {
// LokiIndexedAdapter responds with `{ success: true }`, while
// LokiMemory adapter just calls it with no params
if ((response && response.success) || response === undefined) {
resolve()
} else {
reject(response)
// close() only waits for a save if Loki is dirty, but a save may still be in flight (dirty
// flags are cleared when it starts) - and IncrementalIndexedDBAdapter throws if we try to
// delete database while it is
drainSaves(loki, () => {
try {
loki.deleteDatabase({}, (response) => {
// LokiIndexedAdapter responds with `{ success: true }`, while
// LokiMemory adapter just calls it with no params
if ((response && response.success) || response === undefined) {
resolve()
} else {
reject(response)
}
})
} catch (error) {
reject(error)
}
})
})
})
}

function drainSaves(loki: Loki, callback: () => void): void {
// NOTE: throttledSaveDrain calls back twice if throttledSaves is disabled
if (loki.throttledSaves) {
loki.throttledSaveDrain(() => callback())
} else {
callback()
}
}

// In case of a fatal error, break Loki so that it cannot save its contents to disk anymore
// This might result in a loss of data in recent changes, but we assume that whatever caused the
// fatal error has corrupted the database, so we want to prevent it from being persisted
Expand Down
Loading