Skip to content

Commit

Permalink
chore(db): add instance access cache layer over insert operations
Browse files Browse the repository at this point in the history
  • Loading branch information
kodemon committed Oct 26, 2022
1 parent cca22da commit e33fc6f
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 32 deletions.
4 changes: 2 additions & 2 deletions packages/db/src/Collection.ts
Expand Up @@ -118,7 +118,7 @@ export class Collection<D extends Document = any> {
* retrieve the document directly from the collections document Map.
*/
async findById(id: string): Promise<D | undefined> {
return this.storage.load().then(() => this.storage.getDocument(id));
return this.storage.waitForReady().then(() => this.storage.getDocument(id));
}

/**
Expand Down Expand Up @@ -167,7 +167,7 @@ export class Collection<D extends Document = any> {
* @url https://github.com/kofrasa/mingo#searching-and-filtering
*/
async query(criteria: RawObject = {}): Promise<Cursor> {
await this.storage.load();
await this.storage.waitForReady();
return new Query(criteria).find(await this.storage.getDocuments());
}

Expand Down
27 changes: 21 additions & 6 deletions packages/db/src/Storage/Adapters/IndexedDbStorage.ts
Expand Up @@ -7,17 +7,23 @@ const OBJECT_STORE_NAME = "documents";
export class IndexedDbStorage<D extends Document = Document> extends Storage<D> {
#db!: IDBPDatabase;

#cache = new Map<string, D>();

async init(): Promise<void> {
this.#db = await openDB(this.name, 1, {
upgrade(db: any) {
db.createObjectStore(OBJECT_STORE_NAME, {
upgrade(db: IDBPDatabase) {
const store = db.createObjectStore(OBJECT_STORE_NAME, {
keyPath: "id"
});
store.createIndex("primary", "id", { unique: true });
}
});
}

async hasDocument(id: string): Promise<boolean> {
if (this.#cache.has(id) === true) {
return true;
}
const document = await this.getDocument(id);
if (document !== undefined) {
return true;
Expand All @@ -26,19 +32,28 @@ export class IndexedDbStorage<D extends Document = Document> extends Storage<D>
}

async getDocument(id: string): Promise<D | undefined> {
const cachedDocument = this.#cache.get(id);
if (cachedDocument) {
return cachedDocument;
}
return this.#db.get(OBJECT_STORE_NAME, id);
}

async getDocuments(): Promise<D[]> {
return this.#db.getAll(OBJECT_STORE_NAME);
return [...(await this.#db.getAll(OBJECT_STORE_NAME)), ...Array.from(this.#cache.values())];
}

async setDocument(id: string, document: D): Promise<void> {
this.#db.put(OBJECT_STORE_NAME, document);
async setDocument(document: D): Promise<void> {
this.#cache.set(document.id, document);
this.#db.put(OBJECT_STORE_NAME, document).then(() => {
this.#cache.delete(document.id);
});
}

async delDocument(id: string): Promise<void> {
this.#db.delete(OBJECT_STORE_NAME, id);
this.#db.delete(OBJECT_STORE_NAME, id).then(() => {
this.#cache.delete(id);
});
}

async count(): Promise<number> {
Expand Down
4 changes: 2 additions & 2 deletions packages/db/src/Storage/Adapters/MemoryStorage.ts
Expand Up @@ -15,8 +15,8 @@ export class MemoryStorage<D extends Document = Document> extends Storage<D> {
return Array.from(this.#documents.values());
}

async setDocument(id: string, document: D): Promise<void> {
this.#documents.set(id, document);
async setDocument(document: D): Promise<void> {
this.#documents.set(document.id, document);
}

async delDocument(id: string): Promise<void> {
Expand Down
4 changes: 0 additions & 4 deletions packages/db/src/Storage/Operators/Remove/Remove.ts
@@ -1,13 +1,9 @@
import { DocumentNotFoundError } from "../../Errors";
import { Storage } from "../../Storage";
import { Remove } from "../Operators";
import { RemoveOneException } from "./Exceptions";
import { RemoveOneResult } from "./Result";

export async function remove(storage: Storage, operator: Remove): Promise<RemoveOneResult | RemoveOneException> {
if ((await storage.hasDocument(operator.id)) === false) {
return new RemoveOneException(new DocumentNotFoundError({ id: operator.id }));
}
storage.commit("remove", { id: operator.id });
return new RemoveOneResult();
}
34 changes: 18 additions & 16 deletions packages/db/src/Storage/Storage.ts
Expand Up @@ -26,6 +26,13 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte
constructor(readonly name: string) {
super();
this.#startBrowserListener();
this.#load();
}

#load() {
this.init().then(() => {
this.#setStatus("ready").process();
});
}

#startBrowserListener() {
Expand All @@ -35,16 +42,15 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte

/*
|--------------------------------------------------------------------------------
| Loaders
| Bootstrap
|--------------------------------------------------------------------------------
*/

async load() {
async waitForReady(): Promise<void> {
if (this.is("loading") === false) {
return this;
return;
}
await this.init();
return this.#setStatus("ready").process();
return new Promise((resolve) => this.once("ready", resolve));
}

/**
Expand All @@ -65,7 +71,7 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte

abstract getDocuments(): Promise<D[]>;

abstract setDocument(id: string, document: D): Promise<void>;
abstract setDocument(document: D): Promise<void>;

abstract delDocument(id: string): Promise<void>;

Expand Down Expand Up @@ -103,7 +109,7 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte
switch (type) {
case "insert":
case "update": {
this.setDocument(document.id, document);
this.setDocument(document);
this.emit("change", type, document);
break;
}
Expand Down Expand Up @@ -158,10 +164,8 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte

async run(operation: Omit<Operator<D>, "resolve" | "reject">): Promise<any> {
return new Promise((resolve, reject) => {
this.load().then(() => {
this.#operators.push({ ...operation, resolve, reject } as Operator<D>);
this.process();
});
this.#operators.push({ ...operation, resolve, reject } as Operator<D>);
this.process();
});
}

Expand All @@ -171,25 +175,23 @@ export abstract class Storage<D extends Document = Document> extends EventEmitte
|--------------------------------------------------------------------------------
*/

async process(): Promise<this> {
process(): this {
if (this.is("loading") || this.is("working")) {
return this;
}

this.#setStatus("working");

const operation = this.#operators.shift();
if (!operation) {
if (operation === undefined) {
return this.#setStatus("ready");
}

this.resolve(operation as any)
.then(operation.resolve)
.catch(operation.reject);

this.#setStatus("ready").process();

return this;
return this.#setStatus("ready").process();
}

resolve(operator: Insert<D>): Promise<InsertResult | InsertException>;
Expand Down
93 changes: 93 additions & 0 deletions sandbox/react/src/Modules/Database/database.controller.ts
@@ -0,0 +1,93 @@
import { Controller, ViewController } from "@valkyr/react";

import { Test } from "./test.model";

const mock = [
{
type: "insertOne",
data: {
id: "xyz",
foo: "bar"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar1"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar2"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar3"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar4"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar5"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar6"
}
},
{
type: "updateOne",
data: {
id: "xyz",
foo: "bar7"
}
}
];

class DatabaseController extends Controller<State> {
async onInit() {
return {
tests: await this.query(Test, {}, "tests")
};
}

async run() {
for (const { type, data } of mock) {
if (type === "insertOne") {
await Test.insertOne(data);
} else {
await Test.updateOne(
{ id: data.id },
{
$set: {
foo: data.foo
}
}
);
}
}
}
}

type State = {
tests: Test[];
};

export const controller = new ViewController(DatabaseController);
29 changes: 29 additions & 0 deletions sandbox/react/src/Modules/Database/database.module.ts
@@ -0,0 +1,29 @@
import { render } from "@App/Middleware/Render";
import { router } from "@App/Services/Router";
import { database, IndexedDbStorage } from "@valkyr/db";
import { Route } from "@valkyr/router";

import { DatabaseView } from "./database.view";
import { Test } from "./test.model";

/*
|--------------------------------------------------------------------------------
| Models
|--------------------------------------------------------------------------------
*/

database.register([{ name: "test", model: Test }], IndexedDbStorage);

/*
|--------------------------------------------------------------------------------
| Routes
|--------------------------------------------------------------------------------
*/

router.register([
new Route({
name: "Database",
path: "/database",
actions: [render(DatabaseView)]
})
]);
11 changes: 11 additions & 0 deletions sandbox/react/src/Modules/Database/database.view.tsx
@@ -0,0 +1,11 @@
import { controller } from "./database.controller";

export const DatabaseView = controller.view(({ state: { tests }, actions: { run } }) => {
return (
<div>
<div>Database Testing</div>
<button onClick={run}>Run</button>
<pre>{JSON.stringify(tests, null, 2)}</pre>
</div>
);
});
1 change: 1 addition & 0 deletions sandbox/react/src/Modules/Database/index.ts
@@ -0,0 +1 @@
import "./database.module";
11 changes: 11 additions & 0 deletions sandbox/react/src/Modules/Database/test.model.ts
@@ -0,0 +1,11 @@
import { Document, Model } from "@valkyr/db";

export type TestDocument = Document & {
foo: string;
};

export class Test extends Model<TestDocument> {
readonly foo!: string;
}

export type TestModel = typeof Test;
Expand Up @@ -13,7 +13,7 @@ export const RealmsList = controller.view<Props>(
Realms List Name: {name} | Realms: {realms.length}
</div>
<div style={{ margin: "10px 0" }}>---</div>
{[1, 10, 50, 100].map((amount) => {
{[1, 10, 50, 100, 1000].map((amount) => {
return (
<button
key={amount}
Expand Down
3 changes: 2 additions & 1 deletion sandbox/react/src/Modules/index.ts
@@ -1,2 +1,3 @@
import "./Realms";
import "./Auth";
import "./Database";
import "./Realms";

0 comments on commit e33fc6f

Please sign in to comment.