diff options
author | Nick Van Doorn <vandoorn.nick@gmail.com> | 2019-02-09 01:31:39 -0800 |
---|---|---|
committer | Nick Van Doorn <vandoorn.nick@gmail.com> | 2019-02-09 01:31:39 -0800 |
commit | cc3c82a0b2816a4e1601d237a130cf7bae953335 (patch) | |
tree | df65babf3a3870de2bc706b43efd815721a83044 /core/src | |
parent | 7aa0d4141516cf03e332d70e374f46f35ea09f76 (diff) |
Blah
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/change-handlers.model.ts | 6 | ||||
-rw-r--r-- | core/src/context.model.ts | 6 | ||||
-rw-r--r-- | core/src/database.model.ts | 14 | ||||
-rw-r--r-- | core/src/database.test.ts | 197 | ||||
-rw-r--r-- | core/src/database.ts | 114 | ||||
-rw-r--r-- | core/src/index.ts | 6 |
6 files changed, 172 insertions, 171 deletions
diff --git a/core/src/change-handlers.model.ts b/core/src/change-handlers.model.ts index 0bddecf..0ef499a 100644 --- a/core/src/change-handlers.model.ts +++ b/core/src/change-handlers.model.ts @@ -1,6 +1,6 @@ export interface ChangeHandlers { [handlerKey: string]: { - path: string; - callback: (e: any) => Promise<any>; - }; + path: string + callback: (e: any) => Promise<any> + } } diff --git a/core/src/context.model.ts b/core/src/context.model.ts index 193f152..3551fc8 100644 --- a/core/src/context.model.ts +++ b/core/src/context.model.ts @@ -6,13 +6,13 @@ export interface Context { * but it may be handy to pipe it elsewhere * later */ - logger: (e: any) => void; + logger: (e: any) => void /** * Location of the database cache file (JSON file) */ - cachePath: string; + cachePath: string /** * Maximum database size in megabytes */ - maxDbSizeMB: number; + maxDbSizeMB: number } diff --git a/core/src/database.model.ts b/core/src/database.model.ts index a0cd3a5..0ceed6e 100644 --- a/core/src/database.model.ts +++ b/core/src/database.model.ts @@ -7,7 +7,7 @@ export interface DatabaseInterface { * but is required for any form * of non-volatile storage */ - init(): Promise<void>; + init(): Promise<void> /** * Read data from the "node" described by path @@ -16,12 +16,12 @@ export interface DatabaseInterface { * null is returned if there is no data * at path */ - read(path: string): Promise<Object>; + read(path: string): Promise<Object> /** * Subscribe to all data changes at path */ - subscribe(path: string, callback: (e: any) => any): () => any; + subscribe(path: string, callback: (e: any) => any): () => any /** * Write data to the node described by path @@ -32,20 +32,20 @@ export interface DatabaseInterface { * * An exception is thrown if the write fails */ - write(path: string, toWrite: Object): Promise<void>; + write(path: string, toWrite: Object): Promise<void> /** * Remove data at path */ - remove(path: string): Promise<void>; + remove(path: string): Promise<void> /** * Empty the database */ - flush(): Promise<void>; + flush(): Promise<void> /** * String representation (JSON) of the database */ - toString(): Object; + toString(): Object } diff --git a/core/src/database.test.ts b/core/src/database.test.ts index 7f81308..7b2be8d 100644 --- a/core/src/database.test.ts +++ b/core/src/database.test.ts @@ -1,130 +1,131 @@ -import { Database } from "./database"; -import { NaiveErrorCode } from "../../lib/error.model"; +import { Database } from './database' +import { NaiveErrorCode } from '../../lib/error.model' -const generateString = (sizeBytes: number) => - Array(Math.ceil(sizeBytes)).fill("a"); +const generateString = (sizeBytes: number): string => + Array(Math.ceil(sizeBytes)) + .fill('a') + .join('') -const maxDbSizeMB = 0.1; +const maxDbSizeMB = 0.1 -describe("Database module", () => { - let db: Database; +describe('Database module', () => { + let db: Database beforeAll(async () => { db = new Database({ logger: console.log, maxDbSizeMB, cachePath: `${__dirname}/db.json` - }); - await db.init(); - }); + }) + await db.init() + }) beforeEach(async () => { - await db.flush(); - }); - - // Next 2 tests simply look - // for expections on operations that - // should be harmless - test("it should write data", () => { - return db.write("/hello/world", { - formula: "hello world" - }); - }); - - test("it should read data", () => { - return db.read("/hello/world"); - }); - - test("it should have a string representation", () => { - expect(db.toString()).not.toHaveLength(0); - }); - - test("it should write and then read data", async () => { - const path = "/hello/world"; + await db.flush() + }) + + // Next 2 tests run simple operations + // that should _not_ throw exceptions + test('it should write data', () => { + return db.write('/hello/world', { + formula: 'hello world' + }) + }) + + test('it should read data', () => { + return db.read('/hello/world') + }) + + test('it should have a string representation', () => { + expect(db.toString()).not.toHaveLength(0) + }) + + test('it should write and then read data', async () => { + const path = '/hello/world' const toWrite = { - secret: "stuff" - }; - await db.write(path, toWrite); - const s = await db.read(path); - expect(s).toBe(toWrite); - }); - - test("it should write and read data from the root", async () => { + secret: 'stuff' + } + await db.write(path, toWrite) + const s = await db.read(path) + expect(s).toBe(toWrite) + }) + + test('it should write and read data from the root', async () => { const toWrite = { my: { big: { - fun: "data" + fun: 'data' } } - }; - await db.write("/", toWrite); - const s = await db.read("/"); - expect(s).toEqual(toWrite); - }); - - test("it should remove data", async () => { - const path = "/this/is/fun"; + } + await db.write('/', toWrite) + const s = await db.read('/') + expect(s).toEqual(toWrite) + }) + + test('it should remove data', async () => { + const path = '/this/is/fun' await db.write(path, { foxNews: { - stories: ["AOC", "aoc", "aOC", "aOc"] + stories: ['AOC', 'aoc', 'aOC', 'aOc'] } - }); - await db.remove(path); - const s = await db.read(path); - expect(s).toBeNull(); - }); + }) + await db.remove(path) + const s = await db.read(path) + expect(s).toBeNull() + }) - test("it should remove data at path & leave other data", async () => { - const path = "/this/is/fun"; + test('it should remove data at path & leave other data', async () => { + const path = '/this/is/fun' const buzzFeed = { - stories: ["hack your dogs brain in 4 easy steps"] - }; + stories: ['hack your dogs brain in 4 easy steps'] + } await db.write(path, { foxNews: { - stories: ["AOC", "aoc", "aOC", "aOc"] + stories: ['AOC', 'aoc', 'aOC', 'aOc'] }, buzzFeed - }); - const appendedPath = `${path}/foxNews`; - await db.remove(appendedPath); - const buzzFeedOut = await db.read(`${path}/buzzFeed`); - const foxOut = await db.read(appendedPath); - expect(buzzFeedOut).toEqual(buzzFeed); - expect(foxOut).toBeNull(); - }); - - test("it should be empty after a flush", async () => { - await db.write("/hello/world", { - my: "object" - }); - await db.flush(); - expect(db.toString()).toHaveLength(2); // empty object - }); - - test("it should read null on an empty node", async () => { - const s = await db.read("/any/path/should/work"); - expect(s).toBeNull(); - }); - - test("it should not change on read", async () => { - await db.write("/my/data/lives/here", { + }) + const appendedPath = `${path}/foxNews` + await db.remove(appendedPath) + const buzzFeedOut = await db.read(`${path}/buzzFeed`) + const foxOut = await db.read(appendedPath) + expect(buzzFeedOut).toEqual(buzzFeed) + expect(foxOut).toBeNull() + }) + + test('it should be empty after a flush', async () => { + await db.write('/hello/world', { + my: 'object' + }) + await db.flush() + expect(db.toString()).toHaveLength(2) // empty object + }) + + test('it should read null on an empty node', async () => { + const s = await db.read('/any/path/should/work') + expect(s).toBeNull() + }) + + test('it should not change on read', async () => { + await db.write('/my/data/lives/here', { whiteHouseDinner: `America's Finest McDouble's` - }); - const before = db.toString(); - const s = await db.read("/no/data/here/silly"); - expect(s).toBeNull(); - expect(before).toEqual(db.toString()); - }); - - test("it should throw an error if max size is exceeded", async () => { + }) + const before = db.toString() + const s = await db.read('/no/data/here/silly') + expect(s).toBeNull() + expect(before).toEqual(db.toString()) + }) + + test('it should throw an error if max size is exceeded', async () => { try { - await db.write("/big/data", { + await db.write('/big/data', { bigObj: generateString(maxDbSizeMB * 1024 ** 2) - }); - throw new Error("Did not throw exception"); + }) + throw new Error('Did not throw exception') } catch (e) { - expect(e.code).toBe(NaiveErrorCode.OUT_OF_SPACE); + expect(e.code).toBe(NaiveErrorCode.OUT_OF_SPACE) } - }); -}); + }) +}) diff --git a/core/src/database.ts b/core/src/database.ts index 3b19f8e..e4083b1 100644 --- a/core/src/database.ts +++ b/core/src/database.ts @@ -1,22 +1,22 @@ -import { writeFile, readFile } from "fs"; -import { promisify } from "util"; +import { writeFile, readFile } from 'fs' +import { promisify } from 'util' -import { DatabaseInterface } from "./database.model"; -import { Context } from "./context.model"; -import { NaiveError, NaiveErrorCode as e } from "../../lib/error.model"; -import { ChangeHandlers } from "./change-handlers.model"; +import { DatabaseInterface } from './database.model' +import { Context } from './context.model' +import { NaiveError, NaiveErrorCode as e } from '../../lib/error.model' +import { ChangeHandlers } from './change-handlers.model' -import { last, getKey } from "../../lib/util"; +import { last, getKey } from '../../lib/util' /** * Split path using "/" as a delimiter */ -const splitPath = (path: string): string[] => path.split("/").filter(k => k); +const splitPath = (path: string): string[] => path.split('/').filter(k => k) /** * Identify if a path is a root node */ -const isRootNode = (path: string): boolean => path === "/" || path === ""; +const isRootNode = (path: string): boolean => path === '/' || path === '' /** * Check if path1 matches path2, @@ -26,19 +26,19 @@ const isRootNode = (path: string): boolean => path === "/" || path === ""; */ const isChildOrMatch = (child: string, parent: string) => { // console.log(`child: ${child}, parent ${parent}`); - if (child === parent || parent === "/") return true; - const parentTokens = parent.split("/").filter((i: string) => i.length); - return parentTokens.every((t, i) => child.split("/")[i] === t); -}; + if (child === parent || parent === '/') return true + const parentTokens = parent.split('/').filter((i: string) => i.length) + return parentTokens.every((t, i) => child.split('/')[i] === t) +} -const write = promisify(writeFile); -const read = promisify(readFile); +const write = promisify(writeFile) +const read = promisify(readFile) export const DEFAULT_CTX = { logger: console.log, cachePath: `${__dirname}/db.json`, maxDbSizeMB: 6 -}; +} /** * Implementation of NoSQL DB that uses paths and objects. @@ -54,22 +54,22 @@ export class Database implements DatabaseInterface { /** * In memory buffer to read/write data */ - private buff: any = {}; + private buff: any = {} /** * An array of callback functions that are alerted * when the database changes (mostly want to use this * for some type of pubsub functionality on-top) */ - private changeHandlers: ChangeHandlers = {}; + private changeHandlers: ChangeHandlers = {} constructor(private ctx: Context = DEFAULT_CTX) {} async init(): Promise<void> { try { - const buff = await read(this.ctx.cachePath); - this.buff = JSON.parse(buff.toString()); + const buff = await read(this.ctx.cachePath) + this.buff = JSON.parse(buff.toString()) } catch (e) { - this.ctx.logger("Failed to init database, using empty object"); - this.ctx.logger(e); + this.ctx.logger('Failed to init database, using empty object') + this.ctx.logger(e) } } @@ -78,57 +78,57 @@ export class Database implements DatabaseInterface { // (which obviously becomes a bad idea at some point) async read(path: string): Promise<Object> { // root node case - if (isRootNode(path)) return this.buff; - const pathParts = splitPath(path); - return this.resolve(pathParts); + if (isRootNode(path)) return this.buff + const pathParts = splitPath(path) + return this.resolve(pathParts) } async write(path: string, toWrite: any): Promise<void> { if (isRootNode(path)) { - this.buff = toWrite; + this.buff = toWrite } else { - const pathParts = splitPath(path); - const writeTo = this.resolve(pathParts, false, 1); - writeTo[last(pathParts)] = toWrite; + const pathParts = splitPath(path) + const writeTo = this.resolve(pathParts, false, 1) + writeTo[last(pathParts)] = toWrite } - await this.serialize(); + await this.serialize() // alert everyone of our new change - await this.runChangeHandlers(path, toWrite); + await this.runChangeHandlers(path, toWrite) } private async runChangeHandlers(path: string, change: any): Promise<void> { - const handlers = Object.values(this.changeHandlers); + const handlers = Object.values(this.changeHandlers) for (let handler of handlers) { if (isChildOrMatch(path, handler.path)) { - await handler.callback(change); + await handler.callback(change) } } } subscribe(path: string, callback: (e: any) => Promise<any>): () => any { - const key = getKey("subscriber"); + const key = getKey('subscriber') this.changeHandlers[key] = { path, callback - }; - return () => this.unsubscribe(key); + } + return () => this.unsubscribe(key) } private unsubscribe(key: string) { - delete this.changeHandlers[key]; + delete this.changeHandlers[key] } remove(path: string): Promise<void> { - return this.write(path, null); + return this.write(path, null) } async flush(): Promise<void> { - this.buff = {}; - await this.serialize(); + this.buff = {} + await this.serialize() } toString() { - return JSON.stringify(this.buff); + return JSON.stringify(this.buff) } /** @@ -148,41 +148,41 @@ export class Database implements DatabaseInterface { isRead: boolean = true, level: number = 0 ): any { - const [firstPart] = pathParts; - if (isRootNode(firstPart)) return this.buff; - const n = pathParts.length - level; + const [firstPart] = pathParts + if (isRootNode(firstPart)) return this.buff + const n = pathParts.length - level // TODO avoid having to pull // this specific case out if (n === 0) { if (!this.buff[firstPart]) { - this.buff[firstPart] = {}; - return this.buff[firstPart]; + this.buff[firstPart] = {} + return this.buff[firstPart] } } // start at the root of our buffer - let lastNode = this.buff; - let node; + let lastNode = this.buff + let node for (let i = 0; i < n; i++) { - const part: string = pathParts[i]; + const part: string = pathParts[i] // handle null node if (!lastNode[part]) { // if we're reading from the object // we want to stop as soon // as we hit a null node if (isRead) { - return null; + return null } // but if we're writing and the node is missing, // we should make it and continue else { - lastNode[part] = {}; + lastNode[part] = {} } } // traverse to the next node - node = lastNode[part]; - lastNode = node; + node = lastNode[part] + lastNode = node } - return node; + return node } /** @@ -195,12 +195,12 @@ export class Database implements DatabaseInterface { * Throws OUT_OF_SPACE */ private serialize(): Promise<void> { - if (!this.hasSpace()) throw new NaiveError(e.OUT_OF_SPACE); - return write(this.ctx.cachePath, this.toString()); + if (!this.hasSpace()) throw new NaiveError(e.OUT_OF_SPACE) + return write(this.ctx.cachePath, this.toString()) } private hasSpace(): boolean { // convert from MB to B - return this.toString().length <= this.ctx.maxDbSizeMB * 1024 ** 2; + return this.toString().length <= this.ctx.maxDbSizeMB * 1024 ** 2 } } diff --git a/core/src/index.ts b/core/src/index.ts index 0874475..ff4b500 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,3 +1,3 @@ -export { Database } from "./database"; -export { Context } from "./context.model"; -export { DatabaseInterface } from "./database.model"; +export { Database } from './database' +export { Context } from './context.model' +export { DatabaseInterface } from './database.model' |