test-rxdb
Version:
A local realtime NoSQL Database for JavaScript applications -
280 lines (262 loc) • 10.8 kB
text/typescript
import type {
RxChangeEvent,
RxDocument,
RxDocumentData
} from './types/index.d.ts';
import {
getFromMapOrThrow,
getHeightOfRevision,
overwriteGetterForCaching,
requestIdlePromiseNoQueue
} from './plugins/utils/index.ts';
import {
overwritable
} from './overwritable.ts';
import { Observable } from 'rxjs';
/**
* Because we have to create many cache items,
* we use an array instead of an object with properties
* for better performance and less memory usage.
* @link https://stackoverflow.com/questions/17295056/array-vs-object-efficiency-in-javascript
*/
declare type CacheItem<RxDocType, OrmMethods> = [
/**
* Store the different document states of time
* based on their revision height (rev height = array index).
* We store WeakRefs so that we can later clean up
* document states that are no longer needed.
*/
Map<number, WeakRef<RxDocument<RxDocType, OrmMethods>>>,
/**
* Store the latest known document state.
* As long as any state of the document is in the cache,
* we observe the changestream and update the latestDoc accordingly.
* This makes it easier to optimize performance on other parts
* because for each known document we can always get the current state
* in the storage.
* Also it makes it possible to call RxDocument.latest() in a non-async way
* to retrieve the latest document state or to observe$ some property.
*
* To not prevent the whole cacheItem from being garbage collected,
* we store only the document data here, but not the RxDocument.
*/
RxDocumentData<RxDocType>
];
/**
* @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
*/
declare type FinalizationRegistryValue = {
docId: string;
revisionHeight: number;
};
/**
* The DocumentCache stores RxDocument objects
* by their primary key and revision.
* This is useful on client side applications where
* it is not known how much memory can be used, so
* we de-duplicate RxDocument states to save memory.
* To not fill up the memory with old document states, the DocumentCache
* only contains weak references to the RxDocuments themself.
* @link https://caniuse.com/?search=weakref
*/
export class DocumentCache<RxDocType, OrmMethods> {
public readonly cacheItemByDocId = new Map<string, CacheItem<RxDocType, OrmMethods>>();
/**
* Process stuff lazy to not block the CPU
* on critical paths.
*/
public readonly tasks = new Set<Function>();
/**
* Some JavaScript runtimes like QuickJS,
* so not have a FinalizationRegistry or WeakRef.
* Therefore we need a workaround which might waste a lot of memory,
* but at least works.
*/
public readonly registry?: FinalizationRegistry<FinalizationRegistryValue> = typeof FinalizationRegistry === 'function' ?
new FinalizationRegistry<FinalizationRegistryValue>(docMeta => {
const docId = docMeta.docId;
const cacheItem = this.cacheItemByDocId.get(docId);
if (cacheItem) {
cacheItem[0].delete(docMeta.revisionHeight);
if (cacheItem[0].size === 0) {
/**
* No state of the document is cached anymore,
* so we can clean up.
*/
this.cacheItemByDocId.delete(docId);
}
}
}) :
undefined;
constructor(
public readonly primaryPath: string,
public readonly changes$: Observable<RxChangeEvent<RxDocType>[]>,
/**
* A method that can create a RxDocument by the given document data.
*/
public documentCreator: (docData: RxDocumentData<RxDocType>) => RxDocument<RxDocType, OrmMethods>
) {
changes$.subscribe(events => {
this.tasks.add(() => {
const cacheItemByDocId = this.cacheItemByDocId;
for (let index = 0; index < events.length; index++) {
const event = events[index];
const cacheItem = cacheItemByDocId.get(event.documentId);
if (cacheItem) {
let documentData = event.documentData;
if (!documentData) {
documentData = event.previousDocumentData as any;
}
cacheItem[1] = documentData;
}
}
});
if (this.tasks.size <= 1) {
requestIdlePromiseNoQueue().then(() => {
this.processTasks();
});
}
});
}
public processTasks() {
if (this.tasks.size === 0) {
return;
}
const tasks = Array.from(this.tasks);
tasks.forEach(task => task());
this.tasks.clear();
}
/**
* Get the RxDocument from the cache
* and create a new one if not exits before.
* @overwrites itself with the actual function
* because this is @performance relevant.
* It is called on each document row for each write and read.
*/
get getCachedRxDocuments(): (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] {
const fn = getCachedRxDocumentMonad(this);
return overwriteGetterForCaching(
this,
'getCachedRxDocuments',
fn
);
}
get getCachedRxDocument(): (docData: RxDocumentData<RxDocType>) => RxDocument<RxDocType, OrmMethods> {
const fn = getCachedRxDocumentMonad(this);
return overwriteGetterForCaching(
this,
'getCachedRxDocument',
doc => fn([doc])[0]
);
}
/**
* Throws if not exists
*/
public getLatestDocumentData(docId: string): RxDocumentData<RxDocType> {
this.processTasks();
const cacheItem = getFromMapOrThrow(this.cacheItemByDocId, docId);
return cacheItem[1];
}
public getLatestDocumentDataIfExists(docId: string): RxDocumentData<RxDocType> | undefined {
this.processTasks();
const cacheItem = this.cacheItemByDocId.get(docId);
if (cacheItem) {
return cacheItem[1];
}
}
}
/**
* This function is called very very often.
* This is likely the most important function for RxDB overall performance
* @hotPath This is one of the most important methods for performance.
* It is used in many places to transform the raw document data into RxDocuments.
*/
function getCachedRxDocumentMonad<RxDocType, OrmMethods>(
docCache: DocumentCache<RxDocType, OrmMethods>
): (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] {
const primaryPath = docCache.primaryPath;
const cacheItemByDocId = docCache.cacheItemByDocId;
const registry = docCache.registry;
const deepFreezeWhenDevMode = overwritable.deepFreezeWhenDevMode;
const documentCreator = docCache.documentCreator;
const fn: (docsData: RxDocumentData<RxDocType>[]) => RxDocument<RxDocType, OrmMethods>[] = (docsData: RxDocumentData<RxDocType>[]) => {
const ret: RxDocument<RxDocType, OrmMethods>[] = new Array(docsData.length);
const registryTasks: RxDocument<RxDocType, OrmMethods>[] = [];
for (let index = 0; index < docsData.length; index++) {
let docData = docsData[index];
const docId: string = (docData as any)[primaryPath];
const revisionHeight = getHeightOfRevision(docData._rev);
let byRev: Map<number, WeakRef<RxDocument<RxDocType, OrmMethods>>>;
let cachedRxDocumentWeakRef: WeakRef<RxDocument<RxDocType, OrmMethods>> | undefined;
let cacheItem = cacheItemByDocId.get(docId);
if (!cacheItem) {
byRev = new Map();
cacheItem = [
byRev,
docData
];
cacheItemByDocId.set(docId, cacheItem);
} else {
byRev = cacheItem[0];
cachedRxDocumentWeakRef = byRev.get(revisionHeight);
}
let cachedRxDocument = cachedRxDocumentWeakRef ? cachedRxDocumentWeakRef.deref() : undefined;
if (!cachedRxDocument) {
docData = deepFreezeWhenDevMode(docData) as any;
cachedRxDocument = documentCreator(docData) as RxDocument<RxDocType, OrmMethods>;
byRev.set(revisionHeight, createWeakRefWithFallback(cachedRxDocument));
if (registry) {
registryTasks.push(cachedRxDocument);
}
}
ret[index] = cachedRxDocument;
}
if (registryTasks.length > 0 && registry) {
/**
* Calling registry.register() has shown to have
* really bad performance. So we add the cached documents
* lazily.
*/
docCache.tasks.add(() => {
for (let index = 0; index < registryTasks.length; index++) {
const doc = registryTasks[index];
registry.register(doc, {
docId: doc.primary,
revisionHeight: getHeightOfRevision(doc.revision)
});
}
});
if (docCache.tasks.size <= 1) {
requestIdlePromiseNoQueue().then(() => {
docCache.processTasks();
});
}
}
return ret;
};
return fn;
}
export function mapDocumentsDataToCacheDocs<RxDocType, OrmMethods>(
docCache: DocumentCache<RxDocType, OrmMethods>,
docsData: RxDocumentData<RxDocType>[]
) {
const getCachedRxDocuments = docCache.getCachedRxDocuments;
return getCachedRxDocuments(docsData);
}
/**
* Fallback for JavaScript runtimes that do not support WeakRef.
* The fallback will keep the items in cache forever,
* but at least works.
*/
const HAS_WEAK_REF = typeof WeakRef === 'function';
const createWeakRefWithFallback = HAS_WEAK_REF ? createWeakRef : createWeakRefFallback;
function createWeakRef<T extends object>(obj: T): WeakRef<T> {
return new WeakRef(obj) as any;
}
function createWeakRefFallback<T extends object>(obj: T): WeakRef<T> {
return {
deref() {
return obj;
}
} as any;
}