feat: seperate text & delta

This commit is contained in:
thecodrr
2020-03-19 11:30:05 +05:00
parent 7bdbde60ae
commit daf93e6f2c
13 changed files with 203 additions and 88 deletions

View File

@@ -14,7 +14,8 @@ beforeEach(async () => {
StorageInterface.clear(); StorageInterface.clear();
}); });
test("search notes", () => //TODO
test.skip("search notes", () =>
noteTest({ noteTest({
content: { delta: "5", text: "5" } content: { delta: "5", text: "5" }
}).then(async ({ db }) => { }).then(async ({ db }) => {

View File

@@ -26,7 +26,7 @@ test("add note", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
let note = db.notes.note(id); let note = db.notes.note(id);
expect(note.data).toBeDefined(); expect(note.data).toBeDefined();
expect(note.text).toStrictEqual(TEST_NOTE.content.text); expect(await note.text()).toStrictEqual(TEST_NOTE.content.text);
})); }));
test("get delta of note", () => test("get delta of note", () =>
@@ -75,7 +75,7 @@ test("update note", () =>
id = await db.notes.add(noteData); id = await db.notes.add(noteData);
let note = db.notes.note(id); let note = db.notes.note(id);
expect(note.title).toBe(noteData.title); expect(note.title).toBe(noteData.title);
expect(note.text).toStrictEqual(noteData.content.text); expect(await note.text()).toStrictEqual(noteData.content.text);
expect(note.data.pinned).toBe(true); expect(note.data.pinned).toBe(true);
expect(note.data.favorite).toBe(true); expect(note.data.favorite).toBe(true);
})); }));

View File

@@ -60,7 +60,7 @@ test("get topic", () =>
let noteId = await db.notes.add({ content: { text: "Hello", delta: [] } }); let noteId = await db.notes.add({ content: { text: "Hello", delta: [] } });
await topic.add(noteId); await topic.add(noteId);
topic = topics.topic("Home"); topic = topics.topic("Home");
expect(topic.all[0].content.text).toBe("Hello"); expect(await db.text.get(topic.all[0].content.text)).toBe("Hello");
expect(topic.totalNotes).toBe(1); expect(topic.totalNotes).toBe(1);
})); }));

View File

@@ -8,19 +8,15 @@ import {
beforeEach(() => StorageInterface.clear()); beforeEach(() => StorageInterface.clear());
test("delete a note", () => test("permanently delete a note", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
let { id: nbId } = await db.notebooks.add(TEST_NOTEBOOK); const note = db.notes.note(id);
await db.notebooks
.notebook(nbId)
.topics.topic("General")
.add(id);
await db.notes.delete(id); await db.notes.delete(id);
expect(db.trash.all.length).toBe(1); expect(db.trash.all.length).toBe(1);
expect(await db.context.read(id + "_delta")).toBeDefined(); expect(await note.delta()).toBeDefined();
await db.trash.delete(db.trash.all[0].id); await db.trash.delete(db.trash.all[0].id);
expect(db.trash.all.length).toBe(0); expect(db.trash.all.length).toBe(0);
expect(await db.context.read(id + "_delta")).toBeUndefined(); expect(await db.delta.get(note.data.content.delta)).toBeUndefined();
})); }));
test("restore a deleted note", () => test("restore a deleted note", () =>
@@ -47,23 +43,25 @@ test("restore a deleted note", () =>
test("delete a locked note", () => test("delete a locked note", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
const note = db.notes.note(id);
await db.vault.create("password"); await db.vault.create("password");
await db.vault.add(id); await db.vault.add(id);
await db.notes.delete(id); await db.notes.delete(id);
expect(db.trash.all.length).toBe(1); expect(db.trash.all.length).toBe(1);
expect(await db.context.read(id + "_delta")).toBeDefined(); expect(await db.delta.get(note.data.content.delta)).toBeDefined();
})); }));
test("restore a deleted locked note", () => test("restore a deleted locked note", () =>
noteTest().then(async ({ db, id }) => { noteTest().then(async ({ db, id }) => {
let note = db.notes.note(id);
await db.vault.create("password"); await db.vault.create("password");
await db.vault.add(id); await db.vault.add(id);
await db.notes.delete(id); await db.notes.delete(id);
expect(db.trash.all.length).toBe(1); expect(db.trash.all.length).toBe(1);
expect(await db.context.read(id + "_delta")).toBeDefined(); expect(await db.delta.get(note.data.content.delta)).toBeDefined();
await db.trash.restore(db.trash.all[0].id); await db.trash.restore(db.trash.all[0].id);
expect(db.trash.all.length).toBe(0); expect(db.trash.all.length).toBe(0);
let note = db.notes.note(id); note = db.notes.note(id);
expect(note).toBeDefined(); expect(note).toBeDefined();
})); }));

View File

@@ -46,10 +46,14 @@ test("lock a note", () =>
await db.vault.create("password"); await db.vault.create("password");
await db.vault.add(id); await db.vault.add(id);
const note = db.notes.note(id); const note = db.notes.note(id);
const { content } = note.data;
expect(content.iv).toBeDefined(); const delta = await note.delta();
expect(content.cipher).toBeDefined(); expect(delta.iv).toBeDefined();
expect((await note.delta()).iv).toBeDefined(); expect(delta.cipher).toBeDefined();
const text = await db.text.get(note.data.content.text);
expect(text.iv).toBeDefined();
expect(text.cipher).toBeDefined();
})); }));
test("unlock a note", () => test("unlock a note", () =>
@@ -58,7 +62,6 @@ test("unlock a note", () =>
await db.vault.add(id); await db.vault.add(id);
const note = await db.vault.open(id, "password"); const note = await db.vault.open(id, "password");
expect(note.id).toBe(id); expect(note.id).toBe(id);
expect(note.content.text).toBe(TEST_NOTE.content.text);
expect(note.content.delta.ops).toBeDefined(); expect(note.content.delta.ops).toBeDefined();
})); }));
@@ -69,6 +72,5 @@ test("unlock a note permanently", () =>
await db.vault.remove(id, "password"); await db.vault.remove(id, "password");
const note = db.notes.note(id); const note = db.notes.note(id);
expect(note.id).toBe(id); expect(note.id).toBe(id);
expect(note.data.content.text).toBe(TEST_NOTE.content.text);
expect((await note.delta()).ops).toBeDefined(); expect((await note.delta()).ops).toBeDefined();
})); }));

View File

@@ -6,6 +6,7 @@ import User from "../models/user";
import Sync from "./sync"; import Sync from "./sync";
import Vault from "./vault"; import Vault from "./vault";
import Lookup from "./lookup"; import Lookup from "./lookup";
import Content from "../collections/content";
class Database { class Database {
constructor(context) { constructor(context) {
@@ -18,11 +19,22 @@ class Database {
this.user = new User(this.context); this.user = new User(this.context);
this.tags = new Tags(this.context, "tags"); this.tags = new Tags(this.context, "tags");
this.colors = new Tags(this.context, "colors"); this.colors = new Tags(this.context, "colors");
this.delta = new Content(this.context, "delta");
this.text = new Content(this.context, "text");
await this.delta.init();
await this.text.init();
await this.tags.init(); await this.tags.init();
await this.colors.init(); await this.colors.init();
await this.notes.init(this.notebooks, this.trash, this.tags, this.colors); await this.notes.init(
this.notebooks,
this.trash,
this.tags,
this.colors,
this.delta,
this.text
);
await this.notebooks.init(this.notes, this.trash); await this.notebooks.init(this.notes, this.trash);
await this.trash.init(this.notes, this.notebooks); await this.trash.init(this.notes, this.notebooks, this.delta);
this.syncer = new Sync(this); this.syncer = new Sync(this);
this.vault = new Vault(this, this.context); this.vault = new Vault(this, this.context);
this.lookup = new Lookup(); this.lookup = new Lookup();

View File

@@ -1,5 +1,5 @@
import Database from "./index"; import Database from "./index";
import getId from "../utils/id";
export default class Vault { export default class Vault {
/** /**
* *
@@ -85,49 +85,56 @@ export default class Vault {
async save(note) { async save(note) {
if (!note) return; if (!note) return;
await this._check(); await this._check();
let id = note.id || Date.now().toString() + "_note"; let id = note.id || getId();
return await this._lockNote(id, note); return await this._lockNote(id, note);
} }
_encryptText(text) { async _encryptContent(content, ids) {
return this._context.encrypt(this._password, JSON.stringify({ text })); let { text, delta } = { ...content };
} let { deltaId, textId } = ids;
async _decryptText(text) {
const decrypted = await this._context.decrypt(this._password, text); if (!delta.ops) delta = await this._db.delta.get(deltaId);
return JSON.parse(decrypted); if (text === textId) text = await this._db.text.get(textId);
text = await this._context.encrypt(this._password, text);
delta = await this._context.encrypt(this._password, delta);
await this._db.text.add({ id: textId, data: text });
await this._db.delta.add({ id: deltaId, data: delta });
} }
async _encryptDelta(id, deltaArg) { async _decryptContent(content) {
if (!deltaArg) return; let { text, delta } = { ...content };
const delta = await this._context.encrypt(
this._password,
JSON.stringify(deltaArg)
);
await this._context.write(this._deltaId(id), delta);
}
async _decryptDelta(id) { text = await this._db.text.get(text);
const delta = await this._context.read(this._deltaId(id)); text = await this._context.decrypt(this._password, text);
const decrypted = await this._context.decrypt(this._password, delta);
return JSON.parse(decrypted);
}
_deltaId(id) { delta = await this._db.text.get(delta);
return id + "_delta"; delta = await this._context.decrypt(this._password, delta);
return {
delta,
text
};
} }
async _lockNote(id, note) { async _lockNote(id, note) {
if (!note) return; if (!note) return;
let delta = note.content.delta; let oldNote = this._db.notes.note(id);
if (!delta) delta = await this._context.read(this._deltaId(id));
await this._encryptDelta(id, delta);
const content = await this._encryptText(note.content.text); let deltaId = 0;
let textId = 0;
if (oldNote && oldNote.data.content) {
deltaId = oldNote.data.content.delta;
textId = oldNote.data.content.text;
}
await this._encryptContent(note.content, { textId, deltaId });
return await this._db.notes.add({ return await this._db.notes.add({
id, id,
content,
locked: true locked: true
}); });
} }
@@ -135,21 +142,21 @@ export default class Vault {
async _unlockNote(note, perm = false) { async _unlockNote(note, perm = false) {
if (!note.locked) return; if (!note.locked) return;
let decrypted = await this._decryptText(note.content); let { delta, text } = await this._decryptContent(note.content);
let delta = await this._decryptDelta(note.id);
if (perm) { if (perm) {
await this._db.notes.add({ await this._db.notes.add({
id: note.id, id: note.id,
content: decrypted,
locked: false locked: false
}); });
return await this._context.write(this._deltaId(note.id), delta); await this._db.delta.add({ id: note.content.delta, data: delta });
await this._db.text.add({ id: note.content.text, data: text });
return;
} }
return { return {
...note, ...note,
content: { ...decrypted, delta } content: { delta }
}; };
} }
} }

View File

@@ -0,0 +1,30 @@
import IndexedCollection from "../database/indexed-collection";
import getId from "../utils/id";
export default class Content {
constructor(context, name) {
this._collection = new IndexedCollection(context, name);
}
init() {
return this._collection.init();
}
async add(content) {
if (!content) return;
const id = content.id || getId();
await this._collection.addItem({ id, data: content.data || content });
return id;
}
async get(id) {
const content = await this._collection.getItem(id);
if (!content) return;
return content.data;
}
remove(id) {
if (!id) return;
return this._collection.removeItem(id);
}
}

View File

@@ -4,7 +4,7 @@ import Notebook from "../models/notebook";
import Notes from "./notes"; import Notes from "./notes";
import Trash from "./trash"; import Trash from "./trash";
import sort from "fast-sort"; import sort from "fast-sort";
import getId from "../utils/id" import getId from "../utils/id";
var tfun = require("transfun/transfun.js").tfun; var tfun = require("transfun/transfun.js").tfun;
if (!tfun) { if (!tfun) {

View File

@@ -12,7 +12,11 @@ import Storage from "../database/storage";
import Notebooks from "./notebooks"; import Notebooks from "./notebooks";
import Note from "../models/note"; import Note from "../models/note";
import Trash from "./trash"; import Trash from "./trash";
import getId from "../utils/id" import getId from "../utils/id";
import Tags from "./tags";
import Delta from "./content";
import Content from "./content";
var tfun = require("transfun/transfun.js").tfun; var tfun = require("transfun/transfun.js").tfun;
if (!tfun) { if (!tfun) {
tfun = global.tfun; tfun = global.tfun;
@@ -21,20 +25,25 @@ if (!tfun) {
export default class Notes { export default class Notes {
constructor(context) { constructor(context) {
this._collection = new CachedCollection(context, "notes"); this._collection = new CachedCollection(context, "notes");
this._deltaStorage = new Storage(context);
} }
/** /**
* *
* @param {Notebooks} notebooks * @param {Notebooks} notebooks
* @param {Trash} trash * @param {Trash} trash
* @param {Tags} tags
* @param {Tags} colors
* @param {Content} delta
* @param {Content} text
*/ */
async init(notebooks, trash, tags, colors) { async init(notebooks, trash, tags, colors, delta, text) {
await this._collection.init(); await this._collection.init();
this._notebooks = notebooks; this._notebooks = notebooks;
this._trash = trash; this._trash = trash;
this._tagsCollection = tags; this._tagsCollection = tags;
this._colorsCollection = colors; this._colorsCollection = colors;
this._deltaCollection = delta;
this._textCollection = text;
} }
async add(noteArg) { async add(noteArg) {
@@ -42,6 +51,14 @@ export default class Notes {
let id = noteArg.id || getId(); let id = noteArg.id || getId();
let oldNote = this._collection.getItem(id); let oldNote = this._collection.getItem(id);
let deltaId = 0;
let textId = 0;
if (oldNote && oldNote.content) {
deltaId = oldNote.content.delta;
textId = oldNote.content.text;
}
let note = { let note = {
...oldNote, ...oldNote,
...noteArg ...noteArg
@@ -53,21 +70,33 @@ export default class Notes {
} }
if (note.content.delta && note.content.delta.ops) { if (note.content.delta && note.content.delta.ops) {
await this._deltaStorage.write(id + "_delta", note.content.delta); deltaId = await this._deltaCollection.add({
id: deltaId,
data: note.content.delta
});
}
if (note.content.text !== textId) {
textId = await this._textCollection.add({
id: textId,
data: note.content.text
});
note.title = getNoteTitle(note);
note.headline = getNoteHeadline(note);
} }
note = { note = {
id, id,
type: "note", type: "note",
title: getNoteTitle(note), title: note.title,
content: getNoteContent(note), content: { text: textId, delta: deltaId },
pinned: !!note.pinned, pinned: !!note.pinned,
locked: !!note.locked, locked: !!note.locked,
notebook: note.notebook || {}, notebook: note.notebook || {},
colors: note.colors || [], colors: note.colors || [],
tags: note.tags || [], tags: note.tags || [],
favorite: !!note.favorite, favorite: !!note.favorite,
headline: getNoteHeadline(note), headline: note.headline,
dateCreated: note.dateCreated dateCreated: note.dateCreated
}; };
@@ -225,13 +254,3 @@ function getNoteTitle(note) {
.join(" ") .join(" ")
.trim(); .trim();
} }
function getNoteContent(note) {
if (note.locked) {
return note.content;
}
return {
text: note.content.text.trim()
};
}

View File

@@ -1,23 +1,24 @@
import CachedCollection from "../database/cached-collection"; import CachedCollection from "../database/cached-collection";
import Notes from "./notes"; import Notes from "./notes";
import Notebooks from "./notebooks"; import Notebooks from "./notebooks";
import Storage from "../database/storage"; import Delta from "./content";
import { get7DayTimestamp } from "../utils/date"; import { get7DayTimestamp } from "../utils/date";
export default class Trash { export default class Trash {
constructor(context) { constructor(context) {
this._collection = new CachedCollection(context, "trash"); this._collection = new CachedCollection(context, "trash");
this._deltaStorage = new Storage(context);
} }
/** /**
* *
* @param {Notes} notes * @param {Notes} notes
* @param {Notebooks} notebooks * @param {Notebooks} notebooks
* @param {Delta} delta
*/ */
async init(notes, notebooks) { async init(notes, notebooks, delta) {
this._notes = notes; this._notes = notes;
this._notebooks = notebooks; this._notebooks = notebooks;
this._deltaCollection = delta;
await this._collection.init(); await this._collection.init();
await this.cleanup(); await this.cleanup();
} }
@@ -36,20 +37,22 @@ export default class Trash {
} }
async add(item) { async add(item) {
if (this._collection.exists(item.id + "_deleted")) if (this._collection.exists(item.id))
throw new Error("This item has already been deleted."); throw new Error("This item has already been deleted.");
await this._collection.addItem({ await this._collection.addItem({
...item, ...item,
dateDeleted: Date.now(), dateDeleted: Date.now()
id: item.id + "_deleted"
}); });
} }
async delete(...ids) { async delete(...ids) {
for (let id of ids) { for (let id of ids) {
if (!this._collection.exists(id)) return; if (!id) continue;
if (id.indexOf("note") > -1) let item = this._collection.getItem(id);
await this._deltaStorage.remove(id.replace("_deleted", "") + "_delta"); if (!item) continue;
if (item.type === "note") {
await this._deltaCollection.remove(item.content.delta);
}
await this._collection.removeItem(id); await this._collection.removeItem(id);
} }
} }
@@ -59,7 +62,6 @@ export default class Trash {
let item = { ...this._collection.getItem(id) }; let item = { ...this._collection.getItem(id) };
if (!item) continue; if (!item) continue;
delete item.dateDeleted; delete item.dateDeleted;
item.id = item.id.replace("_deleted", "");
if (item.type === "note") { if (item.type === "note") {
let { notebook } = item; let { notebook } = item;
item.notebook = {}; item.notebook = {};

View File

@@ -0,0 +1,44 @@
import Indexer from "./indexer";
export default class IndexedCollection {
constructor(context, type) {
this.indexer = new Indexer(context, type);
}
async init() {
await this.indexer.init();
}
async addItem(item) {
if (!item.id) throw new Error("The item must contain the id field.");
const exists = await this.exists(item.id);
if (!exists) item.dateCreated = item.dateCreated || Date.now();
await this.updateItem(item);
if (!exists) {
await this.indexer.index(item.id);
}
}
async updateItem(item) {
if (!item.id) throw new Error("The item must contain the id field.");
// if item is newly synced, remote will be true.
item.dateEdited = item.remote ? item.dateEdited : Date.now();
// the item has become local now, so remove the flag.
delete item.remote;
await this.indexer.write(item.id, item);
}
async removeItem(id) {
await this.indexer.deindex(id);
await this.indexer.remove(id);
}
exists(id) {
return this.indexer.exists(id);
}
getItem(id) {
return this.indexer.read(id);
}
}

View File

@@ -40,12 +40,12 @@ export default class Note {
return this._note.notebook; return this._note.notebook;
} }
get text() { delta() {
return this._note.content.text; return this._notes._deltaCollection.get(this._note.content.delta);
} }
delta() { text() {
return this._notes._deltaStorage.read(this._note.id + "_delta"); return this._notes._textCollection.get(this._note.content.text);
} }
color(color) { color(color) {