@nozbe/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
167 lines (148 loc) • 5.44 kB
JavaScript
// @flow
/* eslint-disable global-require */
import { NativeModules, Platform } from 'react-native'
import { type ConnectionTag, logger, invariant } from '../../../utils/common'
import { fromPromise, type ResultCallback } from '../../../utils/fp/Result'
import type {
DispatcherType,
SQLiteAdapterOptions,
SqliteDispatcher,
SqliteDispatcherMethod,
SqliteDispatcherOptions,
} from '../type'
const { WMDatabaseBridge, WMDatabaseJSIBridge } = NativeModules
class SqliteNativeModulesDispatcher implements SqliteDispatcher {
_tag: ConnectionTag
_unsafeNativeReuse: boolean
_bridge: any
constructor(
tag: ConnectionTag,
bridge: any,
{ experimentalUnsafeNativeReuse }: SqliteDispatcherOptions,
): void {
this._tag = tag
this._bridge = bridge
this._unsafeNativeReuse = experimentalUnsafeNativeReuse
if (process.env.NODE_ENV !== 'production') {
invariant(
this._bridge,
`NativeModules.WMDatabaseBridge is not defined! This means that you haven't properly linked WatermelonDB native module. Refer to docs for instructions about installation (and the changelog if this happened after an upgrade).`,
)
invariant(
Platform.OS !== 'windows',
'Windows is only supported via JSI. Pass { jsi: true } to SQLiteAdapter constructor.',
)
}
}
call(name: SqliteDispatcherMethod, _args: any[], callback: ResultCallback<any>): void {
let methodName: string = name
let args = _args
if (methodName === 'batch' && this._bridge.batchJSON) {
methodName = 'batchJSON'
args = [JSON.stringify(args[0])]
} else if (
['initialize', 'setUpWithSchema', 'setUpWithMigrations'].includes(methodName) &&
Platform.OS === 'android'
) {
// FIXME: Hacky, refactor once native reuse isn't an "unsafe experimental" option
args.push(this._unsafeNativeReuse)
}
fromPromise(this._bridge[methodName](this._tag, ...args), callback)
}
}
class SqliteJsiDispatcher implements SqliteDispatcher {
_db: any
_unsafeErrorListener: (Error) => void // debug hook for NT use
constructor(dbName: string, { usesExclusiveLocking }: SqliteDispatcherOptions): void {
this._db = global.nativeWatermelonCreateAdapter(dbName, usesExclusiveLocking)
this._unsafeErrorListener = () => {}
}
call(name: SqliteDispatcherMethod, _args: any[], callback: ResultCallback<any>): void {
let methodName: string = name
let args = _args
if (methodName === 'query' && !global.HermesInternal) {
// NOTE: compressing results of a query into a compact array makes querying 15-30% faster on JSC
// but actually 9% slower on Hermes (presumably because Hermes has faster C++ JSI and slower JS execution)
methodName = 'queryAsArray'
} else if (methodName === 'batch') {
methodName = 'batchJSON'
args = [JSON.stringify(args[0])]
} else if (
Platform.OS === 'windows' &&
(methodName === 'provideSyncJson' || methodName === 'unsafeLoadFromSync')
) {
callback({ error: new Error(`${methodName} unavailable on Windows. Please contribute.`) })
} else if (methodName === 'provideSyncJson') {
fromPromise(WMDatabaseBridge.provideSyncJson(...args), callback)
return
}
try {
const method = this._db[methodName]
if (!method) {
throw new Error(
`Cannot run database method ${methodName} because database failed to open. Hint: Did you install JSI correctly? This happens if you forgot to configure Proguard correctly ${Object.keys(
this._db,
).join(',')}`,
)
}
let result = method(...args)
// On Android, errors are returned, not thrown - see DatabaseBridge.cpp
if (result instanceof Error) {
throw result
} else {
if (methodName === 'queryAsArray') {
result = require('./decodeQueryResult').default(result)
}
callback({ value: result })
}
} catch (error) {
this._unsafeErrorListener(error)
callback({ error })
}
}
}
export const makeDispatcher = (
type: DispatcherType,
tag: ConnectionTag,
dbName: string,
options: SqliteDispatcherOptions,
): SqliteDispatcher => {
switch (type) {
case 'jsi':
return new SqliteJsiDispatcher(dbName, options)
case 'asynchronous':
return new SqliteNativeModulesDispatcher(tag, WMDatabaseBridge, options)
default:
throw new Error('Unknown DispatcherType')
}
}
const initializeJSI = () => {
if (global.nativeWatermelonCreateAdapter) {
return true
}
const bridge = WMDatabaseBridge
if (bridge.initializeJSI) {
try {
bridge.initializeJSI()
return !!global.nativeWatermelonCreateAdapter
} catch (e) {
logger.error('[SQLite] Failed to initialize JSI')
logger.error(e)
}
} else if (WMDatabaseJSIBridge && WMDatabaseJSIBridge.install) {
WMDatabaseJSIBridge.install()
return !!global.nativeWatermelonCreateAdapter
}
return false
}
export function getDispatcherType(options: SQLiteAdapterOptions): DispatcherType {
if (options.jsi) {
if (initializeJSI()) {
return 'jsi'
}
logger.warn(
`JSI SQLiteAdapter not available… falling back to asynchronous operation. This will happen if you're using remote debugger, and may happen if you forgot to recompile native app after WatermelonDB update`,
)
}
return 'asynchronous'
}