minimongo
Version:
Client-side mongo database with server sync over http
357 lines (316 loc) • 9.54 kB
text/typescript
import _ from "lodash"
import * as utils from "./utils"
import { compileSort } from "./selector"
import {
Doc,
MinimongoCollectionFindOneOptions,
MinimongoLocalCollection,
MinimongoLocalDb
} from "./types"
/** Replicates data into a both a master and a replica db. Assumes both are identical at start
* and then only uses master for finds and does all changes to both
* Warning: removing a collection removes it from the underlying master and replica!
*/
export default class ReplicatingDb implements MinimongoLocalDb {
collections: { [collectionName: string]: Collection<any> }
masterDb: MinimongoLocalDb
replicaDb: MinimongoLocalDb
constructor(masterDb: MinimongoLocalDb, replicaDb: MinimongoLocalDb) {
this.collections = {}
this.masterDb = masterDb
this.replicaDb = replicaDb
}
addCollection(name: any, success: any, error: any) {
const collection = new Collection(name, this.masterDb[name], this.replicaDb[name])
this[name] = collection
this.collections[name] = collection
if (success != null) {
return success()
}
}
removeCollection(name: any, success: any, error: any) {
delete this[name]
delete this.collections[name]
if (success != null) {
return success()
}
}
getCollectionNames() {
return _.keys(this.collections)
}
}
// Replicated collection.
class Collection<T extends Doc> implements MinimongoLocalCollection<T> {
name: string
masterCol: MinimongoLocalCollection<T>
replicaCol: MinimongoLocalCollection<T>
constructor(name: string, masterCol: MinimongoLocalCollection, replicaCol: MinimongoLocalCollection) {
this.name = name
this.masterCol = masterCol
this.replicaCol = replicaCol
}
find(selector: any, options: any) {
return this.masterCol.find(selector, options)
}
findOne(selector: any, options?: MinimongoCollectionFindOneOptions): Promise<T | null>
findOne(
selector: any,
options: MinimongoCollectionFindOneOptions,
success: (doc: T | null) => void,
error: (err: any) => void
): void
findOne(selector: any, success: (doc: T | null) => void, error: (err: any) => void): void
findOne(selector: any, options?: any, success?: any, error?: any) {
if (_.isFunction(options)) {
;[options, success, error] = [{}, options, success]
}
options = options || {}
// If promise case
if (success == null) {
return new Promise((resolve, reject) => {
this.findOne(selector, options, resolve, reject)
})
}
return this.masterCol.findOne(selector, options, success, error)
}
upsert(doc: T): Promise<T | null>
upsert(doc: T, base: T | null | undefined): Promise<T | null>
upsert(docs: T[]): Promise<(T | null)[]>
upsert(docs: T[], bases: (T | null | undefined)[]): Promise<(T | null)[]>
upsert(doc: T, success: (doc: T | null) => void, error: (err: any) => void): void
upsert(doc: T, base: T | null | undefined, success: (doc: T | null) => void, error: (err: any) => void): void
upsert(docs: T[], success: (docs: (T | null)[]) => void, error: (err: any) => void): void
upsert(
docs: T[],
bases: (T | null | undefined)[],
success: (item: (T | null)[]) => void,
error: (err: any) => void
): void
upsert(docs: any, bases?: any, success?: any, error?: any): any {
// If promise case
if (!success && !_.isFunction(bases)) {
return new Promise((resolve, reject) => {
this.upsert(
docs,
bases,
resolve,
reject
)
})
}
let items: { doc: T; base?: T }[]
;[items, success, error] = utils.regularizeUpsert(docs, bases, success, error)
// Upsert does to both
this.masterCol.upsert(
_.map(items, "doc"),
_.map(items, "base"),
() => {
return this.replicaCol.upsert(
_.map(items, "doc"),
_.map(items, "base"),
(results: any) => {
return success(docs)
},
error
)
},
error
)
}
remove(id: any): Promise<void>
remove(id: any, success: () => void, error: (err: any) => void): void
remove(id: any, success?: () => void, error?: (err: any) => void): any {
if (!success) {
return new Promise<void>((resolve, reject) => {
this.remove(id, resolve, reject)
})
}
// Do to both
this.masterCol.remove(
id,
() => {
this.replicaCol.remove(id, success, error!)
},
error!
)
}
cache(docs: any, selector: any, options: any, success: any, error: any) {
// Calculate what has to be done for cache using the master database which is faster (usually MemoryDb)
// then do minimum to both databases
// Index docs
let sort: any
const docsMap = _.keyBy(docs, "_id")
// Compile sort
if (options.sort) {
sort = compileSort(options.sort)
}
// Perform query
return this.masterCol.find(selector, options).fetch((results: any) => {
let result
const resultsMap = _.keyBy(results, "_id")
// Determine if each result needs to be cached
const toCache: any = []
for (let doc of docs) {
result = resultsMap[doc._id]
// Exclude any excluded _ids from being cached/uncached
if (options && options.exclude && options.exclude.includes(doc._id)) {
continue
}
// If not present locally, cache it
if (!result) {
toCache.push(doc)
continue
}
// If both have revisions (_rev) and new one is same or lower, do not cache
if (doc._rev && result._rev && doc._rev <= result._rev) {
continue
}
// Only cache if different
if (JSON.stringify(doc) != JSON.stringify(result)) {
toCache.push(doc)
}
}
const toUncache: any = []
for (result of results) {
// If at limit
if (options.limit && docs.length === options.limit) {
// If past end on sorted limited, ignore
if (options.sort && sort(result, _.last(docs)) >= 0) {
continue
}
// If no sort, ignore
if (!options.sort) {
continue
}
}
// Exclude any excluded _ids from being cached/uncached
if (options && options.exclude && options.exclude.includes(result._id)) {
continue
}
// Determine which ones to uncache
if (!docsMap[result._id]) {
toUncache.push(result._id)
}
}
// Cache ones needing caching
const performCaches = (next: any) => {
if (toCache.length > 0) {
return this.masterCol.cacheList(
toCache,
() => {
return this.replicaCol.cacheList(
toCache,
() => {
return next()
},
error
)
},
error
)
} else {
return next()
}
}
// Uncache list
const performUncaches = (next: any) => {
if (toUncache.length > 0) {
return this.masterCol.uncacheList(
toUncache,
() => {
return this.replicaCol.uncacheList(
toUncache,
() => {
return next()
},
error
)
},
error
)
} else {
return next()
}
}
return performCaches(() => {
return performUncaches(() => {
if (success != null) {
success()
}
})
})
}, error)
}
pendingUpserts(success: any, error: any) {
return this.masterCol.pendingUpserts(success, error)
}
pendingRemoves(success: any, error: any) {
return this.masterCol.pendingRemoves(success, error)
}
resolveUpserts(upserts: any, success: any, error: any) {
return this.masterCol.resolveUpserts(
upserts,
() => {
return this.replicaCol.resolveUpserts(upserts, success, error)
},
error
)
}
resolveRemove(id: any, success: any, error: any) {
return this.masterCol.resolveRemove(
id,
() => {
return this.replicaCol.resolveRemove(id, success, error)
},
error
)
}
// Add but do not overwrite or record as upsert
seed(docs: any, success: any, error: any) {
return this.masterCol.seed(
docs,
() => {
return this.replicaCol.seed(docs, success, error)
},
error
)
}
// Add but do not overwrite upserts or removes
cacheOne(doc: any, success: any, error: any) {
return this.masterCol.cacheOne(
doc,
() => {
return this.replicaCol.cacheOne(doc, success, error)
},
error
)
}
// Add but do not overwrite upserts or removes
cacheList(docs: any, success: any, error: any) {
return this.masterCol.cacheList(
docs,
() => {
return this.replicaCol.cacheList(docs, success, error)
},
error
)
}
uncache(selector: any, success: any, error: any) {
return this.masterCol.uncache(
selector,
() => {
return this.replicaCol.uncache(selector, success, error)
},
error
)
}
uncacheList(ids: any, success: any, error: any) {
return this.masterCol.uncacheList(
ids,
() => {
return this.replicaCol.uncacheList(ids, success, error)
},
error
)
}
}