Skip to content

Commit

Permalink
Closes #24 Write prompt state cache to local level DB
Browse files Browse the repository at this point in the history
  • Loading branch information
andymatuschak committed Apr 24, 2020
1 parent c60da12 commit c0b8392
Show file tree
Hide file tree
Showing 28 changed files with 1,330 additions and 632 deletions.
16 changes: 16 additions & 0 deletions @types/level-auto-index/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare module "level-auto-index" {
import { LevelUp } from "levelup";
function AutoIndex<T>(
db: LevelUp,
idb: LevelUp,
reduce: (value: T) => string,
opts: unknown,
): AutoIndex.IndexedDB;

export = AutoIndex;
namespace AutoIndex {
interface IndexedDB {
get(key: string, opts: unknown): Promise<unknown>;
}
}
}
6 changes: 5 additions & 1 deletion firebase-react-native-persistence-shim/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export default function shimFirebasePersistence(databaseBasePath) {
isShimmed = true;

window.openDatabase = SQLite.openDatabase;
setGlobalVars(window, { checkOrigin: false, databaseBasePath });
setGlobalVars(window, {
checkOrigin: false,
databaseBasePath,
useSQLiteIndexes: true,
});

window.__localStorageStore = {};
window.localStorage = {
Expand Down
9 changes: 8 additions & 1 deletion metabook-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
},
"dependencies": {
"@tradle/react-native-http": "^2.0.1",
"@types/subleveldown": "^4.1.0",
"abstract-leveldown": "^6.3.0",
"assert": "^1.5.0",
"base-64": "^0.1.0",
"browserify-zlib": "^0.1.4",
Expand All @@ -31,8 +33,11 @@
"firebase-react-native-persistence-shim": "0.0.1",
"https-browserify": "0.0.1",
"inherits": "^2.0.4",
"level-auto-index": "^2.0.0",
"level-js": "^5.0.2",
"levelup": "^4.4.0",
"lexicographic-integer": "^1.1.0",
"lodash.isequal": "^4.5.0",
"metabook-client": "0.0.1",
"metabook-core": "0.0.1",
"metabook-sample-data": "0.0.1",
Expand All @@ -49,8 +54,8 @@
"react-native-unimodules": "^0.9.0",
"react-native-web": "~0.11.7",
"readable-stream": "^1.0.33",
"stream-browserify": "^1.0.0",
"string_decoder": "^0.10.31",
"subleveldown": "andymatuschak/subleveldown",
"timers-browserify": "^1.4.2",
"tty-browserify": "0.0.0",
"url": "^0.11.0",
Expand All @@ -64,6 +69,8 @@
"@types/jest": "^25.2.1",
"@types/level-js": "^4.0.1",
"@types/levelup": "^4.3.0",
"@types/lexicographic-integer": "^1.1.0",
"@types/lodash.isequal": "^4.5.5",
"@types/react": "~16.9.0",
"@types/react-native": "~0.60.23",
"babel-core": "^6.26.3",
Expand Down
107 changes: 76 additions & 31 deletions metabook-app/src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ import {
MetabookFirebaseUserClient,
} from "metabook-client";
import {
getActionLogFromPromptActionLog,
getIDForPrompt,
getIDForPromptTask,
getNextTaskParameters,
PromptTask,
repetitionActionLogType,
} from "metabook-core";
import { ReviewArea, ReviewAreaProps } from "metabook-ui";
import { ReviewArea, ReviewAreaProps, ReviewItem } from "metabook-ui";
import colors from "metabook-ui/dist/styles/colors";
import "node-libs-react-native/globals";
import typography from "metabook-ui/dist/styles/typography";
import React, { useCallback, useState } from "react";
import { View, Text } from "react-native";
import DataRecordCache from "./dataRecordCache";
import DataRecordClient from "./dataRecordClient";
import { getFirebaseApp } from "./firebase";
import "node-libs-react-native/globals";
import React, { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import {
enableFirebasePersistence,
getFirebaseApp,
PersistenceStatus,
} from "./firebase";
import DataRecordCache from "./model/dataRecordCache";
import DataRecordClient from "./model/dataRecordClient";
import PromptStateClient from "./model/promptStateClient";
import PromptStateStore from "./model/promptStateStore";
import { useReviewItems } from "./useReviewItems";

async function cacheWriteHandler(name: string, data: Buffer): Promise<string> {
Expand All @@ -43,35 +48,76 @@ async function fileExistsAtURL(url: string): Promise<boolean> {
return info.exists;
}

function usePersistenceStatus() {
const [persistenceStatus, setPersistenceStatus] = useState<PersistenceStatus>(
"pending",
);

useEffect(() => {
let hasUnmounted = false;

function safeSetPersistenceStatus(newStatus: PersistenceStatus) {
if (!hasUnmounted) {
setPersistenceStatus(newStatus);
}
}

enableFirebasePersistence()
.then(() => safeSetPersistenceStatus("enabled"))
.catch(() => safeSetPersistenceStatus("unavailable"));

return () => {
hasUnmounted = true;
};
}, []);

return persistenceStatus;
}

export default function App() {
const [{ userClient, dataRecordClient }] = useState(() => {
const firebaseApp = getFirebaseApp();
const dataClient = new MetabookFirebaseDataClient(
firebaseApp,
firebaseApp.functions(),
);
const dataCache = new DataRecordCache();
return {
userClient: new MetabookFirebaseUserClient(
const persistenceStatus = usePersistenceStatus();
const [
promptStateClient,
setPromptStateClient,
] = useState<PromptStateClient | null>(null);
const [
dataRecordClient,
setDataRecordClient,
] = useState<DataRecordClient | null>(null);

useEffect(() => {
if (persistenceStatus === "enabled") {
const firebaseApp = getFirebaseApp();
const userClient = new MetabookFirebaseUserClient(
firebaseApp.firestore(),
"x5EWk2UT56URxbfrl7djoxwxiqH2",
),
dataRecordClient: new DataRecordClient(dataClient, dataCache, {
writeFile: cacheWriteHandler,
fileExistsAtURL,
}),
};
});
);
setPromptStateClient(
new PromptStateClient(userClient, new PromptStateStore()),
);
const dataClient = new MetabookFirebaseDataClient(
firebaseApp,
firebaseApp.functions(),
);
const dataCache = new DataRecordCache();
setDataRecordClient(
new DataRecordClient(dataClient, dataCache, {
writeFile: cacheWriteHandler,
fileExistsAtURL,
}),
);
}
}, [persistenceStatus]);

const items = useReviewItems(userClient, dataRecordClient);
const items = useReviewItems(promptStateClient, dataRecordClient);

const onMark = useCallback<ReviewAreaProps["onMark"]>(
async (marking) => {
console.log("[Performance] Mark prompt", Date.now() / 1000.0);

userClient
.recordActionLogs([
getActionLogFromPromptActionLog({
promptStateClient!
.recordPromptActionLogs([
{
actionLogType: repetitionActionLogType,
parentActionLogIDs:
marking.reviewItem.promptState?.headActionLogIDs ?? [],
Expand All @@ -87,10 +133,9 @@ export default function App() {
marking.reviewItem.prompt,
marking.reviewItem.promptState?.lastReviewTaskParameters ?? null,
),
}),
},
])
.then(() => {
console.log("Committed", marking.reviewItem.prompt);
console.log(
"[Performance] Log committed to server",
Date.now() / 1000.0,
Expand All @@ -100,7 +145,7 @@ export default function App() {
console.error("Couldn't commit", marking.reviewItem.prompt, error);
});
},
[userClient],
[promptStateClient],
);

console.log("[Performance] Render", Date.now() / 1000.0);
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,42 @@ import {
Prompt,
PromptID,
} from "metabook-core";
import { getJSONRecord, saveJSONRecord } from "./levelDBUtil";

export default class DataRecordCache {
private db: levelup.LevelUp;
constructor(cacheName = "DataRecordCache") {
this.db = LevelUp(LevelJS(cacheName));
}

private saveRecord(key: string, value: unknown): Promise<void> {
return this.db.put(key, JSON.stringify(value));
}

private async getRecord<R>(key: string): Promise<R | null> {
const recordString = await this.db
.get(key)
.catch((error) => (error.notFound ? null : Promise.reject(error)));
if (recordString) {
return JSON.parse(recordString) as R;
} else {
return null;
}
}

async savePrompt(id: PromptID, prompt: Prompt): Promise<void> {
await this.saveRecord(id, prompt);
await saveJSONRecord(this.db, id, prompt);
}

async getPrompt(id: PromptID): Promise<Prompt | null> {
return this.getRecord(id);
const result = await getJSONRecord(this.db, id);
return (result?.record as Prompt) ?? null;
}

async saveAttachmentURLReference(
id: AttachmentID,
reference: AttachmentURLReference,
): Promise<void> {
await this.saveRecord(id, reference);
await saveJSONRecord(this.db, id, reference);
}

async getAttachmentURLReference(
id: AttachmentID,
): Promise<AttachmentURLReference | null> {
return this.getRecord(id);
const result = await getJSONRecord(this.db, id);
return (result?.record as AttachmentURLReference) ?? null;
}

async clear() {
return this.db.clear();
async clear(): Promise<void> {
await this.db.clear();
}

async close() {
return this.db.close();
async close(): Promise<void> {
await this.db.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ class MockDataClient implements MetabookDataClient {
}

let cache: DataRecordCache;
let client: DataRecordClient;
const testBasicPromptID = getIDForPrompt(testBasicPrompt);
beforeEach(() => {
cache = new DataRecordCache();
Expand Down
File renamed without changes.
23 changes: 23 additions & 0 deletions metabook-app/src/model/levelDBUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LevelUp } from "levelup";

export async function saveJSONRecord(
db: LevelUp,
key: string,
value: unknown,
): Promise<void> {
await db.put(key, JSON.stringify(value));
}

export async function getJSONRecord<T>(
db: LevelUp,
key: string,
): Promise<{ record: T } | null> {
const recordString = await db
.get(key)
.catch((error) => (error.notFound ? null : Promise.reject(error)));
if (recordString) {
return { record: JSON.parse(recordString) };
} else {
return null;
}
}
Loading

0 comments on commit c0b8392

Please sign in to comment.