@nozbe/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
217 lines (185 loc) • 6.66 kB
JavaScript
// @flow
/* eslint-disable no-use-before-define */
import { invariant, logger } from '../utils/common'
import type Model from '../Model'
import type Database from './index'
export interface ReaderInterface {
/**
* Calls a Reader so that it runs as part of the current Reader (or Writer) instead of deadlocking.
*
* Specifically, the passed block should immediately call a method decorted with `@reader` or a
* function whose implementation is wrapped in `db.read()` block.
*
* See docs for more details.
*
* @example
* ```
* db.read(async reader => {
* // ...
* reader.callReader(() => someOtherReader())
* })
* ```
*/
callReader<T>(reader: () => Promise<T>): Promise<T>;
}
export interface WriterInterface extends ReaderInterface {
/**
* Calls another Writer so that it runs as part of the current Writer instead of deadlocking.
*
* Specifically, the passed block should immediately call a method decorated with `@writer` or
* a function whose implementation is wrapped in `db.write()` block.
*
* See docs for more details.
*
* @example
* ```
* db.write(async writer => {
* // ...
* writer.callWriter(() => someOtherWriter())
* })
* ```
*/
callWriter<T>(writer: () => Promise<T>): Promise<T>;
/** @see {Database#batch} */
batch(...records: $ReadOnlyArray<Model | Model[] | null | void | false>): Promise<void>;
}
class ReaderInterfaceImpl implements ReaderInterface {
__workItem: WorkQueueItem<any>
__workQueue: WorkQueue
constructor(queue: WorkQueue, item: WorkQueueItem<any>): void {
this.__workQueue = queue
this.__workItem = item
}
__validateQueue(): void {
invariant(
this.__workQueue._queue[0] === this.__workItem,
'Illegal call on a reader/writer that should no longer be running',
)
}
callReader<T>(reader: () => Promise<T>): Promise<T> {
this.__validateQueue()
return this.__workQueue.subAction(reader)
}
}
class WriterInterfaceImpl extends ReaderInterfaceImpl implements WriterInterface {
callWriter<T>(writer: () => Promise<T>): Promise<T> {
this.__validateQueue()
return this.__workQueue.subAction(writer)
}
batch(...records: any): Promise<any> {
this.__validateQueue()
return this.__workQueue._db.batch(records)
}
}
const actionInterface = (queue: WorkQueue, item: WorkQueueItem<any>) =>
item.isWriter ? new WriterInterfaceImpl(queue, item) : new ReaderInterfaceImpl(queue, item)
type WorkQueueItem<T> = $Exact<{
work: (ReaderInterface | WriterInterface) => Promise<T>,
isWriter: boolean,
resolve: (value: T) => void,
reject: (reason: any) => void,
description: ?string,
}>
export default class WorkQueue {
_db: Database
_queue: WorkQueueItem<any>[] = []
_subActionIncoming: boolean = false
constructor(db: Database): void {
this._db = db
}
get isWriterRunning(): boolean {
const [item] = this._queue
return Boolean(item && item.isWriter)
}
enqueue<T>(
work: ($FlowFixMe<ReaderInterface | WriterInterface>) => Promise<T>,
description: ?string,
isWriter: boolean,
): Promise<T> {
// If a subAction was scheduled using subAction(), database.write/read() calls skip the line
if (this._subActionIncoming) {
this._subActionIncoming = false
const currentWork = this._queue[0]
if (!currentWork.isWriter) {
invariant(!isWriter, 'Cannot call a writer block from a reader block')
}
return work(actionInterface(this, currentWork))
}
return new Promise((resolve, reject) => {
const workItem: WorkQueueItem<T> = { work, isWriter, resolve, reject, description }
if (process.env.NODE_ENV !== 'production' && this._queue.length) {
setTimeout(() => {
const queue = this._queue
const current = queue[0]
if (current === workItem || !queue.includes(workItem)) {
return
}
const enqueuedKind = isWriter ? 'writer' : 'reader'
const currentKind = current.isWriter ? 'writer' : 'reader'
logger.warn(
`The ${enqueuedKind} you're trying to run (${
description || 'unnamed'
}) can't be performed yet, because there are ${
queue.length
} other readers/writers in the queue.\n\nCurrent ${currentKind}: ${
current.description || 'unnamed'
}.\n\nIf everything is working fine, you can safely ignore this message (queueing is working as expected). But if your readers/writers are not running, it's because the current ${currentKind} is stuck.\nRemember that if you're calling a reader/writer from another reader/writer, you must use callReader()/callWriter(). See docs for more details.`,
)
logger.log(`Enqueued ${enqueuedKind}:`, work)
logger.log(`Running ${currentKind}:`, current.work)
}, 1500)
}
this._queue.push(workItem)
if (this._queue.length === 1) {
this._executeNext()
}
})
}
subAction<T>(work: () => Promise<T>): Promise<T> {
try {
this._subActionIncoming = true
const promise = work()
invariant(
!this._subActionIncoming,
'callReader/callWriter call must call a reader/writer synchronously',
)
return promise
} catch (error) {
this._subActionIncoming = false
return Promise.reject(error)
}
}
async _executeNext(): Promise<void> {
const workItem = this._queue[0]
const { work, resolve, reject, isWriter } = workItem
try {
const workPromise = work(actionInterface(this, workItem))
if (process.env.NODE_ENV !== 'production') {
invariant(
workPromise instanceof Promise,
`The function passed to database.${
isWriter ? 'write' : 'read'
}() or a method marked as @${
isWriter ? 'writer' : 'reader'
} must be asynchronous (marked as 'async' or always returning a promise) (in: ${
workItem.description || 'unnamed'
})`,
)
}
resolve(await workPromise)
} catch (error) {
reject(error)
}
this._queue.shift()
if (this._queue.length) {
setTimeout(() => this._executeNext(), 0)
}
}
_abortPendingWork(): void {
invariant(this._queue.length >= 1, '_abortPendingWork can only be called from a reader/writer')
const workToAbort = this._queue.splice(1) // leave only the caller on the queue
workToAbort.forEach(({ reject }) => {
reject(new Error('Reader/writer has been aborted because the database was reset'))
})
}
}