diff --git a/package-lock.json b/package-lock.json
index e46620d..44136ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@creately/rxdata",
- "version": "5.1.4",
+ "version": "6.2.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -209,6 +209,11 @@
"integrity": "sha512-0VU8WsYEOYmoxVtUuafB66/+9G8gMdGAC3dhCE/CMpjXgNIz9fwC6g41kRlNxFgpQI/uX/aWNLArNg7KmeFYvw==",
"dev": true
},
+ "@types/lokijs": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/@types/lokijs/-/lokijs-1.5.7.tgz",
+ "integrity": "sha512-WEFQLgO3u2Wa7yFybqkTZYumqF1GcHvUwx8Tv2SUHy2qpnIainMMoLmEUGdjhPNp/v5pyC9e91fSMC3mkxBIDw=="
+ },
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@@ -2526,11 +2531,6 @@
"minimatch": "^3.0.4"
}
},
- "immediate": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
- "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
- },
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -3053,14 +3053,6 @@
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
},
- "lie": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
- "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
- "requires": {
- "immediate": "~3.0.5"
- }
- },
"loader-runner": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz",
@@ -3078,22 +3070,6 @@
"json5": "^0.5.0"
}
},
- "localforage": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
- "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
- "requires": {
- "lie": "3.1.1"
- }
- },
- "localforage-setitems": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/localforage-setitems/-/localforage-setitems-1.4.0.tgz",
- "integrity": "sha1-NrhZDVB9+1yAQDPih+zljYiZbV8=",
- "requires": {
- "localforage": ">=1.4.0"
- }
- },
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@@ -3126,6 +3102,11 @@
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
+ "lodash.omit": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
+ "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
+ },
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@@ -3206,6 +3187,11 @@
"object.assign": "^4.1.0"
}
},
+ "lokijs": {
+ "version": "1.5.12",
+ "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.12.tgz",
+ "integrity": "sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q=="
+ },
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
@@ -4206,9 +4192,9 @@
}
},
"rxjs": {
- "version": "6.3.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
- "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==",
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
"requires": {
"tslib": "^1.9.0"
}
diff --git a/package.json b/package.json
index 23b65a0..62c6b6a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@creately/rxdata",
- "version": "5.1.4",
+ "version": "6.2.4",
"description": "",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -39,10 +39,11 @@
"dependencies": {
"@creately/lschannel": "^2.0.3",
"@creately/mungo": "^1.2.1",
- "localforage": "^1.7.3",
- "localforage-setitems": "^1.4.0",
+ "@types/lokijs": "^1.5.7",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
+ "lodash.omit": "^4.5.0",
+ "lokijs": "^1.5.12",
"mingo": "^2.2.6",
"rxjs": "^6.0.0"
}
diff --git a/src/__module.d.ts b/src/__module.d.ts
index 265677d..669f5a7 100644
--- a/src/__module.d.ts
+++ b/src/__module.d.ts
@@ -2,3 +2,4 @@ declare module 'mingo';
declare module 'clone';
declare module 'lodash.isequal';
declare module 'lodash.clonedeep';
+declare module 'lodash.omit';
diff --git a/src/__tests__/collection.spec.ts b/src/__tests__/collection.spec.ts
index 08ed8a8..d9b9e66 100644
--- a/src/__tests__/collection.spec.ts
+++ b/src/__tests__/collection.spec.ts
@@ -105,11 +105,16 @@ describe('Collection', () => {
it('should emit modified documents if they match the selector', async done => {
const { col } = await prepare();
const watchPromise = watchN(col, 1, { z: 3 });
+ const watchPromise2 = watchN(col, 1, { z: 2 });
await col.insert(TEST_DOCS);
const out = await watchPromise;
+ const out2 = await watchPromise2;
expect(out).toEqual([
{ id: (jasmine.any(Number) as any) as number, type: 'insert', docs: TEST_DOCS.filter(doc => doc.z === 3) },
]);
+ expect(out2).toEqual([
+ { id: (jasmine.any(Number) as any) as number, type: 'insert', docs: TEST_DOCS.filter(doc => doc.z === 2) },
+ ]);
done();
});
@@ -336,6 +341,25 @@ describe('Collection', () => {
done();
});
+ it('should update the exsiting query / upsert', async done => {
+ const { col } = await prepare();
+ const promise1 = watchN(col, 1);
+ await col.insert(TEST_DOCS);
+ let out = await promise1;
+ expect(out).toEqual([{ id: (jasmine.any(Number) as any) as number, type: 'insert', docs: [...TEST_DOCS] }]);
+
+ const promise2 = watchN(col, 1);
+ await col.insert(Object.freeze({ id: 'd111', x: 2, y: 2, z: 2 }));
+ out = await promise2;
+ expect((col as any).storage.data.length).toEqual(TEST_DOCS.length);
+ expect((col as any).storage.data.filter((doc: any) => doc.id === 'd111').length).toEqual(1);
+ const updated = (col as any).storage.data.find((doc: any) => doc.id === 'd111');
+ expect(updated.x).toEqual(2);
+ expect(updated.y).toEqual(2);
+ expect(updated.z).toEqual(2);
+ done();
+ });
+
it('should emit the inserted document as a change (remote)');
});
diff --git a/src/__tests__/database.spec.ts b/src/__tests__/database.spec.ts
index 5404617..bd32e8e 100644
--- a/src/__tests__/database.spec.ts
+++ b/src/__tests__/database.spec.ts
@@ -94,6 +94,7 @@ describe('Database', () => {
const { db } = prepare();
const c1 = db.collection('test');
await c1.insert([{ id: 'd1' }]);
+ expect(Collection.loki.getCollection(`rxdata.test-db.test`).data[0].id).toEqual('d1');
expect(await findN(c1, 1)).toEqual([[{ id: 'd1' }]]);
await db.drop();
expect(await findN(c1, 1)).toEqual([[]]);
diff --git a/src/collection.ts b/src/collection.ts
index 28237f5..756f4d6 100644
--- a/src/collection.ts
+++ b/src/collection.ts
@@ -1,10 +1,8 @@
-///
-
import mingo from 'mingo';
-import * as LocalForage from 'localforage';
-import 'localforage-setitems';
+import * as Loki from 'lokijs';
import * as isequal from 'lodash.isequal';
import * as cloneDeep from 'lodash.clonedeep';
+import * as omit from 'lodash.omit';
import { Observable, Subject, empty, of, defer, from, Subscription } from 'rxjs';
import { switchMap, concatMap, concat, map, distinctUntilChanged } from 'rxjs/operators';
import { modify } from '@creately/mungo';
@@ -72,17 +70,14 @@ export type DocumentChange = InsertDocumentChange | RemoveDocumentChange {
+ public static loki = new Loki('rxdatalokijs');
// allDocs
// allDocs emits all documents in the collection when they get modified.
protected allDocs: Subject;
// storage
// storage stores documents in a suitable storage backend.
- protected storage: LocalForage;
-
- // cachedDocs
- // cachedDocs is an in memory cache of all documents in the database.
- protected cachedDocs: T[] | null = null;
+ protected storage: Loki.Collection;
// channel
// channel sends/receives messages between browser tabs.
@@ -107,7 +102,7 @@ export class Collection {
// constructor
constructor(public name: string) {
this.allDocs = new Subject();
- this.storage = LocalForage.createInstance({ name });
+ this.storage = Collection.loki.addCollection(name);
this.changes = Channel.create(`rxdata.${name}.channel`);
this.changesSub = this.changes.pipe(concatMap(change => from(this.apply(change)))).subscribe();
}
@@ -130,12 +125,17 @@ export class Collection {
// made to documents which match the selector.
public watch(selector?: Selector): Observable> {
if (!selector) {
- return this.changes.asObservable();
+ return this.changes.asObservable().pipe(
+ map(change => {
+ const docs = change.docs.map(doc => omit(doc, '$loki', 'meta'));
+ return Object.assign({}, change, { docs });
+ })
+ );
}
const mq = new mingo.Query(selector);
return this.changes.pipe(
switchMap(change => {
- const docs = change.docs.filter(doc => mq.test(doc));
+ const docs = change.docs.filter(doc => mq.test(doc)).map(doc => omit(doc, '$loki', 'meta'));
if (!docs.length) {
return empty();
}
@@ -178,7 +178,17 @@ export class Collection {
// with the id already exists in the collection, it will be replaced.
public async insert(docOrDocs: T | T[]): Promise {
const docs: T[] = cloneDeep(Array.isArray(docOrDocs) ? docOrDocs : [docOrDocs]);
- await this.storage.setItems(docs.map(doc => ({ key: (doc as any).id, value: doc })));
+ docs.forEach(doc => {
+ var existingDoc = this.storage.findOne({ id: doc.id });
+ if (existingDoc) {
+ // If a matching document exists, update it
+ Object.assign(existingDoc, doc);
+ this.storage.update(existingDoc);
+ } else {
+ // If no matching document exists, insert the new document
+ this.storage.insert(doc);
+ }
+ });
await this.emitAndApply({ id: this.nextChangeId(), type: 'insert', docs: docs });
}
@@ -189,8 +199,10 @@ export class Collection {
const modifier = cloneDeep(_modifier);
const filter = this.createFilter(selector);
const docs: T[] = cloneDeep((await this.load()).filter(doc => filter(doc)));
- docs.forEach(doc => modify(doc, modifier));
- await this.storage.setItems(docs.map(doc => ({ key: (doc as any).id, value: doc })));
+ docs.forEach(doc => {
+ modify(doc, modifier);
+ this.storage.update(doc);
+ });
await this.emitAndApply({ id: this.nextChangeId(), type: 'update', docs: docs, modifier: modifier });
}
@@ -200,10 +212,15 @@ export class Collection {
public async remove(selector: Selector): Promise {
const filter = this.createFilter(selector);
const docs = (await this.load()).filter(doc => filter(doc));
- await Promise.all(docs.map(doc => this.storage.removeItem((doc as any).id)));
+ docs.map(doc => this.storage.remove(doc));
await this.emitAndApply({ id: this.nextChangeId(), type: 'remove', docs: docs });
}
+ public async dropCollection() {
+ Collection.loki.removeCollection(this.name);
+ await this.reload();
+ }
+
// filter
// filter returns an array of documents which match the selector and
// filter options. The selector, options and all option fields are optional.
@@ -218,7 +235,7 @@ export class Collection {
if (options.limit) {
cursor = cursor.limit(options.limit);
}
- return cursor.all();
+ return cursor.all().map((doc: any) => omit(doc, '$loki', 'meta'));
}
// createFilter
@@ -231,13 +248,7 @@ export class Collection {
// load
// load loads all documents from the database to the in-memory cache.
protected load(): Promise {
- if (this.cachedDocs) {
- return Promise.resolve(this.cachedDocs);
- }
- if (!this.loadPromise) {
- this.loadPromise = this.loadAll().then(docs => (this.cachedDocs = docs));
- }
- return this.loadPromise.then(() => this.cachedDocs as T[]);
+ return Promise.resolve(this.storage.data);
}
// Reload
@@ -245,8 +256,7 @@ export class Collection {
// the cachedDocs and emit the updated docs.
public async reload() {
return this.loadAll().then(docs => {
- this.cachedDocs = docs;
- this.allDocs.next(this.cachedDocs);
+ this.allDocs.next(docs);
});
}
@@ -254,41 +264,14 @@ export class Collection {
// loadAll loads all documents from storage without filtering.
// Returns a promise which resolves to an array of documents.
protected async loadAll(): Promise {
- const docs: T[] = [];
- await this.storage.iterate((doc: T) => {
- docs.push(doc);
- // If a non-undefined value is returned,
- // the localforage iterator will stop.
- return undefined;
- });
- return docs;
+ return Promise.resolve(this.storage.data);
}
// refresh
// refresh loads all documents from localForage storage and emits it
// to all listening queries. Called when the collection gets changed.
protected async apply(change: DocumentChange) {
- if (!this.cachedDocs) {
- this.cachedDocs = await this.load();
- }
- if (change.type === 'insert' || change.type === 'update') {
- for (const doc of change.docs) {
- const index = this.cachedDocs.findIndex(d => d.id === doc.id);
- if (index === -1) {
- this.cachedDocs.push(doc);
- } else {
- this.cachedDocs[index] = doc;
- }
- }
- } else if (change.type === 'remove') {
- for (const doc of change.docs) {
- const index = this.cachedDocs.findIndex(d => d.id === doc.id);
- if (index !== -1) {
- this.cachedDocs.splice(index, 1);
- }
- }
- }
- this.allDocs.next(this.cachedDocs);
+ this.allDocs.next(this.storage.data);
const resolveFn = this.changeResolve[change.id];
if (resolveFn) {
resolveFn();
@@ -306,7 +289,7 @@ export class Collection {
// emitAndApply emits the change and waits until it is applied.
private async emitAndApply(change: DocumentChange): Promise {
await new Promise(resolve => {
- this.changeResolve[change.id] = resolve;
+ this.changeResolve[change.id] = resolve as any;
this.changes.next(change);
});
}
diff --git a/src/database.ts b/src/database.ts
index 7b34c2c..607207d 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -53,10 +53,10 @@ export class Database {
// drop clears all data in all collections in the database.
// It also closes all active subscriptions in all collections.
public async drop(): Promise {
- this.collections = new Map();
- const collections = this.collectionsList;
this.collectionsList = [];
- await Promise.all(collections.map(name => new Collection(name).remove({})));
+ this.collections.forEach(async c => await c.dropCollection());
+ this.collections = new Map();
+ await Promise.resolve();
}
// collectionsListKey