Merge pull request #4 from bwindels/bwindels/lumia-fixes

Make what we have so far work on Lumia 950
This commit is contained in:
Bruno Windels 2019-06-26 20:27:26 +00:00 committed by GitHub
commit 56ae6670be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 827 additions and 104 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
node_modules node_modules
bundle.js

43
index-build-debug.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="application-name" content="Brawl Chat"/>
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css">
</head>
<body>
<script type="text/javascript" src="bundle.js"></script>
<script type="text/javascript">
window.DEBUG = true;
let buf = "";
console.error = (...params) => {
const lastLines = "...\n" + buf.split("\n").slice(-10).join("\n");
// buf = buf + "ERR " + params.join(" ") + "\n";
// const location = new Error().stack.split("\n")[2];
alert(params.join(" ") +"\n...\n" + lastLines);
};
console.log = console.info = console.warn = (...params) => {
buf = buf + params.join(" ") + "\n";
};
main(document.body);
setTimeout(() => {
const showlogs = document.getElementById("showlogs");
showlogs.addEventListener("click", () => {
const lastLines = "...\n" + buf.split("\n").slice(-20).join("\n");
alert(lastLines);
}, true);
showlogs.innerText = "Show last 20 log lines";
}, 1000);
// (async () => {
// try {
// const js = await (await fetch("bundle.js")).text();
// eval(js+";main(document.body);");
// } catch(err) {
// alert(err.message);
// }
// })();
</script>
</body>
</html>

13
index-build.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css">
</head>
<body>
<script type="text/javascript" src="bundle.js"></script>
<script type="text/javascript">
main(document.body);
</script>
</body>
</html>

View File

@ -8,7 +8,8 @@
}, },
"scripts": { "scripts": {
"test": "node_modules/.bin/impunity --entryPoint src/main.js --force-esm", "test": "node_modules/.bin/impunity --entryPoint src/main.js --force-esm",
"start": "node scripts/serve-local.js" "start": "node scripts/serve-local.js",
"build": "node_modules/rollup/bin/rollup -c"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -23,6 +24,7 @@
"devDependencies": { "devDependencies": {
"finalhandler": "^1.1.1", "finalhandler": "^1.1.1",
"impunity": "^0.0.7", "impunity": "^0.0.7",
"rollup": "^1.15.6",
"serve-static": "^1.13.2" "serve-static": "^1.13.2"
} }
} }

30
prototypes/base256.html Normal file
View File

@ -0,0 +1,30 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script type="text/javascript">
function encodeNumber(n) {
const a = (n & 0xFFFF);
const b = (n & 0xFFFF0000) >> 16;
const c = (n & 0xFFFF00000000) >> 32;
const d = (n & 0xFFFF000000000000) >> 48;
return String.fromCharCode(a, b, c, d);
}
function printN(n) {
//document.write(`<p>fn(${n}) = ${encodeNumber(n)}</p>`);
console.log(n, encodeNumber(n));
}
printN(0);
printN(9);
printN(1000);
printN(Number.MAX_SAFE_INTEGER);
</script>
</body>
</html>

View File

@ -0,0 +1,110 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
pre {
font-family: monospace;
display: block;
white-space: pre;
font-size: 2em;
}
</style>
</head>
<body>
<script type="text/javascript">
const MIN = 0;
const MAX = 0xFFFFFFFF;
function zeropad(a, n) {
return "0".repeat(n - a.length) + a;
}
// const encodeNumber = n => zeropad((n >>> 0).toString(16), 8);
function encodeNumber(n) {
const a = (n & 0xFFFF0000) >>> 16;
const b = (n & 0xFFFF) >>> 0;
//return zeropad(a.toString(16), 4) + zeropad(b.toString(16), 4);
return String.fromCharCode(a, b);
}
function decodeNumber(s) {
const a = s.charCodeAt(0);
const b = s.charCodeAt(1);
//return `${a.toString(16)} ${b}`;
return ((a << 16) | b) >>> 0;
}
function formatArg(a) {
if (typeof a === "string") {
return `"${a}"`;
}
if (Array.isArray(a)) {
return `[${a.map(formatArg)}]`;
}
return a+"";
}
function cmp(a, b) {
let value;
try {
const result = indexedDB.cmp(encodeNumber(a), encodeNumber(b));
if (result < 0) {
value = "a < b";
} else if (result === 0) {
value = "a = b";
} else if (result > 0) {
value = "a > b";
}
} catch(err) {
value = err.message;
}
return `cmp(${formatArg(a)} as ${formatArg(encodeNumber(a))},\n ${formatArg(b)} as ${formatArg(encodeNumber(b))}): ${value}`;
}
try {
const tests = [
// see https://stackoverflow.com/questions/28413947/space-efficient-way-to-encode-numbers-as-sortable-strings
// need to encode numbers with base 256 and zero padded at start
// should still fit in 8 bytes then?
(cmp) => cmp(9, 100000),
(cmp) => cmp(1, 2),
(cmp) => cmp(MIN, 1),
(cmp) => cmp(MIN, MIN),
(cmp) => cmp(MIN, MAX),
(cmp) => cmp(MAX >>> 1, MAX),
(cmp) => cmp(0x7fffffff, 0xffff7fff),
(cmp) => cmp(MAX, MAX),
(cmp) => cmp(MAX - 1, MAX),
];
for (const fn of tests) {
const txt = document.createTextNode(fn(cmp));
const p = document.createElement("pre");
p.appendChild(txt);
document.body.appendChild(p);
}
} catch(err) {
alert(err.message);
}
let prev;
for (let i = MIN; i <= MAX; i += 100) {
if (decodeNumber(encodeNumber(i)) !== i) {
document.write(`<p>${i} decodes back to ${decodeNumber(encodeNumber(i))}</p>`);
break;
}
if (typeof prev === "number") {
if (indexedDB.cmp(encodeNumber(prev), encodeNumber(i)) >= 0) {
document.write(`<p>${i} <= ${prev}</p>`);
break;
}
}
prev = i;
}
document.write(`<p>check from ${MIN} to ${prev}</p>`);
</script>
</body>
</html>

76
prototypes/idb-cmp.html Normal file
View File

@ -0,0 +1,76 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
pre {
font-family: "courier";
display: block;
white-space: pre;
}
</style>
</head>
<body>
<script type="text/javascript">
function encodeNumber(n) {
const a = (n & 0xFFFF);
const b = (n & 0xFFFF0000) >> 16;
const c = (n & 0xFFFF00000000) >> 32;
const d = (n & 0xFFFF000000000000) >> 48;
return String.fromCharCode(a, b, c, d);
}
function formatArg(a) {
if (typeof a === "string") {
return `"${a}"`;
}
if (Array.isArray(a)) {
return `[${a.map(formatArg)}]`;
}
return a+"";
}
function cmp(a, b) {
let value;
try {
const result = indexedDB.cmp(encodeNumber(a), encodeNumber(b));
if (result < 0) {
value = "a < b";
} else if (result === 0) {
value = "a = b";
} else if (result > 0) {
value = "a > b";
}
} catch(err) {
value = err.message;
}
return `cmp(${formatArg(a)},\n ${formatArg(b)}): ${value}`;
}
try {
const tests = [
(cmp) => cmp(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
(cmp) => cmp([Number.MIN_SAFE_INTEGER], [Number.MAX_SAFE_INTEGER]),
// see https://stackoverflow.com/questions/28413947/space-efficient-way-to-encode-numbers-as-sortable-strings
// need to encode numbers with base 256 and zero padded at start
// should still fit in 8 bytes then?
(cmp) => cmp("foo-9", "foo-10000"),
(cmp) => cmp("foo-\u0000", "foo-\uFFFF"),
(cmp) => cmp("foo-\u0000", "foo-0"),
(cmp) => cmp("foo-" + Number.MAX_SAFE_INTEGER, "foo-\uFFFF"),
(cmp) => cmp("!abc:host.tld,"+Number.MIN_SAFE_INTEGER, "!abc:host.tld,"+(Number.MIN_SAFE_INTEGER + 1)),
(cmp) => cmp("!abc:host.tld,"+0, "!abc:host.tld,"+(Number.MAX_SAFE_INTEGER)),
(cmp) => cmp("!abc:host.tld,"+Math.floor(Number.MAX_SAFE_INTEGER / 2), "!abc:host.tld,"+(Number.MAX_SAFE_INTEGER)),
];
for (const fn of tests) {
const txt = document.createTextNode(fn(cmp));
const p = document.createElement("pre");
p.appendChild(txt);
document.body.appendChild(p);
}
} catch(err) {
alert(err.message);
}
</script>
</body>
</html>

View File

@ -2,6 +2,11 @@
<head><meta charset="utf-8"></head> <head><meta charset="utf-8"></head>
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
console.log = (...params) => {
document.write(params.join(" ")+"<br>");
};
function reqAsPromise(req) { function reqAsPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req); req.onsuccess = () => resolve(req);

View File

@ -0,0 +1,75 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script type="text/javascript">
function reqAsPromise(req) {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req);
req.onerror = (err) => reject(err);
});
}
class Storage {
constructor(databaseName) {
this._databaseName = databaseName;
this._database = null;
}
async open() {
const req = window.indexedDB.open(this._databaseName);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
this._createStores(db, oldVersion);
};
await reqAsPromise(req);
this._database = req.result;
}
_createStores(db) {
db.createObjectStore("files", {keyPath: ["idName"]});
}
async storeFoo(id, name) {
const tx = this._database.transaction(["files"], "readwrite");
const store = tx.objectStore("files");
await reqAsPromise(store.add(value(id, name)));
}
}
function value(id, name) {
return {idName: key(id, name)};
}
function key(id, name) {
return id+","+name;
}
async function main() {
let storage = new Storage("idb-multi-key2");
try {
await storage.open();
await storage.storeFoo(5, "foo");
await storage.storeFoo(6, "bar");
alert("all good");
} catch(err) {
alert(err.message);
}
try {
const result = indexedDB.cmp(key(5, "foo"), key(6, "bar"));
//IDBKeyRange.bound(["aaa", "111"],["zzz", "999"], false, false);
alert("all good: " + result);
} catch (err) {
alert(`IDBKeyRange.bound: ${err.message}`);
}
}
main();
</script>
</body>
</html>

8
rollup.config.js Normal file
View File

@ -0,0 +1,8 @@
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'iife',
name: 'main'
}
};

5
src/Platform.js Normal file
View File

@ -0,0 +1,5 @@
// #ifdef PLATFORM_GNOME
// export {default} from "./ui/gnome/GnomePlatform.js";
// #else
export {default} from "./ui/web/WebPlatform.js";
// #endif

View File

@ -5,10 +5,10 @@ import Sync from "./matrix/sync.js";
import SessionView from "./ui/web/session/SessionView.js"; import SessionView from "./ui/web/session/SessionView.js";
import SessionViewModel from "./domain/session/SessionViewModel.js"; import SessionViewModel from "./domain/session/SessionViewModel.js";
const HOST = "localhost"; const HOST = "192.168.2.108";
const HOMESERVER = `http://${HOST}:8008`; const HOMESERVER = `http://${HOST}:8008`;
const USERNAME = "bruno1"; const USERNAME = "bruno1";
const USER_ID = `@${USERNAME}:${HOST}`; const USER_ID = `@${USERNAME}:localhost`;
const PASSWORD = "testtest"; const PASSWORD = "testtest";
function getSessionInfo(userId) { function getSessionInfo(userId) {
@ -50,7 +50,6 @@ function showSession(container, session, sync) {
container.appendChild(view.mount()); container.appendChild(view.mount());
} }
// eslint-disable-next-line no-unused-vars
export default async function main(container) { export default async function main(container) {
try { try {
let sessionInfo = getSessionInfo(USER_ID); let sessionInfo = getSessionInfo(USER_ID);

View File

@ -5,9 +5,6 @@ export class HomeServerError extends Error {
} }
} }
export class StorageError extends Error {
}
export class RequestAbortError extends Error { export class RequestAbortError extends Error {
} }

View File

@ -6,8 +6,21 @@ import {
class RequestWrapper { class RequestWrapper {
constructor(promise, controller) { constructor(promise, controller) {
this._promise = promise; if (!controller) {
this._controller = controller; const abortPromise = new Promise((_, reject) => {
this._controller = {
abort() {
const err = new Error("fetch request aborted");
err.name = "AbortError";
reject(err);
}
};
});
this._promise = Promise.race([promise, abortPromise]);
} else {
this._promise = promise;
this._controller = controller;
}
} }
abort() { abort() {
@ -47,13 +60,13 @@ export default class HomeServerApi {
headers.append("Content-Type", "application/json"); headers.append("Content-Type", "application/json");
bodyString = JSON.stringify(body); bodyString = JSON.stringify(body);
} }
const controller = new AbortController(); const controller = typeof AbortController === "function" ? new AbortController() : null;
// TODO: set authenticated headers with second arguments, cache them // TODO: set authenticated headers with second arguments, cache them
let promise = fetch(url, { let promise = fetch(url, {
method, method,
headers, headers,
body: bodyString, body: bodyString,
signal: controller.signal signal: controller && controller.signal
}); });
promise = promise.then(async (response) => { promise = promise.then(async (response) => {
if (response.ok) { if (response.ok) {

View File

@ -1,7 +1,4 @@
const DEFAULT_LIVE_FRAGMENT_ID = 0; import Platform from "../../../Platform.js";
const MIN_EVENT_INDEX = Number.MIN_SAFE_INTEGER + 1;
const MAX_EVENT_INDEX = Number.MAX_SAFE_INTEGER - 1;
const MID_EVENT_INDEX = 0;
// key for events in the timelineEvents store // key for events in the timelineEvents store
export default class EventKey { export default class EventKey {
@ -12,7 +9,7 @@ export default class EventKey {
nextFragmentKey() { nextFragmentKey() {
// could take MIN_EVENT_INDEX here if it can't be paged back // could take MIN_EVENT_INDEX here if it can't be paged back
return new EventKey(this.fragmentId + 1, MID_EVENT_INDEX); return new EventKey(this.fragmentId + 1, Platform.middleStorageKey);
} }
nextKeyForDirection(direction) { nextKeyForDirection(direction) {
@ -32,15 +29,15 @@ export default class EventKey {
} }
static get maxKey() { static get maxKey() {
return new EventKey(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); return new EventKey(Platform.maxStorageKey, Platform.maxStorageKey);
} }
static get minKey() { static get minKey() {
return new EventKey(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); return new EventKey(Platform.minStorageKey, Platform.minStorageKey);
} }
static get defaultLiveKey() { static get defaultLiveKey() {
return new EventKey(DEFAULT_LIVE_FRAGMENT_ID, MID_EVENT_INDEX); return new EventKey(Platform.minStorageKey, Platform.middleStorageKey);
} }
toString() { toString() {

View File

@ -1,6 +1,7 @@
import BaseEntry from "./BaseEntry.js"; import BaseEntry from "./BaseEntry.js";
import Direction from "../Direction.js"; import Direction from "../Direction.js";
import {isValidFragmentId} from "../common.js"; import {isValidFragmentId} from "../common.js";
import Platform from "../../../../Platform.js";
export default class FragmentBoundaryEntry extends BaseEntry { export default class FragmentBoundaryEntry extends BaseEntry {
constructor(fragment, isFragmentStart, fragmentIdComparer) { constructor(fragment, isFragmentStart, fragmentIdComparer) {
@ -36,9 +37,9 @@ export default class FragmentBoundaryEntry extends BaseEntry {
get entryIndex() { get entryIndex() {
if (this.started) { if (this.started) {
return Number.MIN_SAFE_INTEGER; return Platform.minStorageKey;
} else { } else {
return Number.MAX_SAFE_INTEGER; return Platform.maxStorageKey;
} }
} }

View File

@ -1,5 +1,4 @@
import {directionalConcat, directionalAppend} from "./common.js"; import {directionalConcat, directionalAppend} from "./common.js";
import EventKey from "../EventKey.js";
import Direction from "../Direction.js"; import Direction from "../Direction.js";
import EventEntry from "../entries/EventEntry.js"; import EventEntry from "../entries/EventEntry.js";
import FragmentBoundaryEntry from "../entries/FragmentBoundaryEntry.js"; import FragmentBoundaryEntry from "../entries/FragmentBoundaryEntry.js";

View File

@ -4,3 +4,17 @@ export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
nameMap[name] = name; nameMap[name] = name;
return nameMap; return nameMap;
}, {})); }, {}));
export class StorageError extends Error {
constructor(message, cause) {
let fullMessage = message;
if (cause) {
fullMessage += ": ";
if (cause.name) {
fullMessage += `(${cause.name}) `;
}
fullMessage += cause.message;
}
super(fullMessage);
}
}

View File

@ -12,18 +12,14 @@ function createStores(db) {
db.createObjectStore("roomSummary", {keyPath: "roomId"}); db.createObjectStore("roomSummary", {keyPath: "roomId"});
// need index to find live fragment? prooobably ok without for now // need index to find live fragment? prooobably ok without for now
db.createObjectStore("timelineFragments", {keyPath: ["roomId", "id"]}); //key = room_id | fragment_id
const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: ["roomId", "fragmentId", "eventIndex"]}); db.createObjectStore("timelineFragments", {keyPath: "key"});
timelineEvents.createIndex("byEventId", [ //key = room_id | fragment_id | event_index
"roomId", const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: "key"});
"event.event_id" //eventIdKey = room_id | event_id
], {unique: true}); timelineEvents.createIndex("byEventId", "eventIdKey", {unique: true});
//key = room_id | event.type | event.state_key,
db.createObjectStore("roomState", {keyPath: [ db.createObjectStore("roomState", {keyPath: "key"});
"roomId",
"event.type",
"event.state_key"
]});
// const roomMembers = db.createObjectStore("roomMembers", {keyPath: [ // const roomMembers = db.createObjectStore("roomMembers", {keyPath: [
// "event.room_id", // "event.room_id",

View File

@ -5,6 +5,18 @@ export default class QueryTarget {
this._target = target; this._target = target;
} }
_openCursor(range, direction) {
if (range && direction) {
return this._target.openCursor(range, direction);
} else if (range) {
return this._target.openCursor(range);
} else if (direction) {
return this._target.openCursor(null, direction);
} else {
return this._target.openCursor();
}
}
get(key) { get(key) {
return reqAsPromise(this._target.get(key)); return reqAsPromise(this._target.get(key));
} }
@ -34,7 +46,7 @@ export default class QueryTarget {
} }
async selectAll(range, direction) { async selectAll(range, direction) {
const cursor = this._target.openCursor(range, direction); const cursor = this._openCursor(range, direction);
const results = []; const results = [];
await iterateCursor(cursor, (value) => { await iterateCursor(cursor, (value) => {
results.push(value); results.push(value);
@ -97,7 +109,7 @@ export default class QueryTarget {
_reduce(range, reducer, initialValue, direction) { _reduce(range, reducer, initialValue, direction) {
let reducedValue = initialValue; let reducedValue = initialValue;
const cursor = this._target.openCursor(range, direction); const cursor = this._openCursor(range, direction);
return iterateCursor(cursor, (value) => { return iterateCursor(cursor, (value) => {
reducedValue = reducer(reducedValue, value); reducedValue = reducer(reducedValue, value);
return {done: false}; return {done: false};
@ -111,7 +123,7 @@ export default class QueryTarget {
} }
async _selectWhile(range, predicate, direction) { async _selectWhile(range, predicate, direction) {
const cursor = this._target.openCursor(range, direction); const cursor = this._openCursor(range, direction);
const results = []; const results = [];
await iterateCursor(cursor, (value) => { await iterateCursor(cursor, (value) => {
results.push(value); results.push(value);
@ -121,7 +133,7 @@ export default class QueryTarget {
} }
async _find(range, predicate, direction) { async _find(range, predicate, direction) {
const cursor = this._target.openCursor(range, direction); const cursor = this._openCursor(range, direction);
let result; let result;
const found = await iterateCursor(cursor, (value) => { const found = await iterateCursor(cursor, (value) => {
const found = predicate(value); const found = predicate(value);

View File

@ -1,5 +1,5 @@
import Transaction from "./transaction.js"; import Transaction from "./transaction.js";
import { STORE_NAMES } from "../common.js"; import { STORE_NAMES, StorageError } from "../common.js";
export default class Storage { export default class Storage {
constructor(idbDatabase) { constructor(idbDatabase) {
@ -14,19 +14,27 @@ export default class Storage {
_validateStoreNames(storeNames) { _validateStoreNames(storeNames) {
const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name)); const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name));
if (idx !== -1) { if (idx !== -1) {
throw new Error(`Tried to open a transaction for unknown store ${storeNames[idx]}`); throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`);
} }
} }
async readTxn(storeNames) { async readTxn(storeNames) {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
const txn = this._db.transaction(storeNames, "readonly"); try {
return new Transaction(txn, storeNames); const txn = this._db.transaction(storeNames, "readonly");
return new Transaction(txn, storeNames);
} catch(err) {
throw new StorageError("readTxn failed", err);
}
} }
async readWriteTxn(storeNames) { async readWriteTxn(storeNames) {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
const txn = this._db.transaction(storeNames, "readwrite"); try {
return new Transaction(txn, storeNames); const txn = this._db.transaction(storeNames, "readwrite");
return new Transaction(txn, storeNames);
} catch(err) {
throw new StorageError("readWriteTxn failed", err);
}
} }
} }

View File

@ -1,9 +1,64 @@
import QueryTarget from "./query-target.js"; import QueryTarget from "./query-target.js";
import { reqAsPromise } from "./utils.js"; import { reqAsPromise } from "./utils.js";
import { StorageError } from "../common.js";
class QueryTargetWrapper {
constructor(qt) {
this._qt = qt;
}
openKeyCursor(...params) {
try {
return this._qt.openKeyCursor(...params);
} catch(err) {
throw new StorageError("openKeyCursor failed", err);
}
}
openCursor(...params) {
try {
return this._qt.openCursor(...params);
} catch(err) {
throw new StorageError("openCursor failed", err);
}
}
put(...params) {
try {
return this._qt.put(...params);
} catch(err) {
throw new StorageError("put failed", err);
}
}
add(...params) {
try {
return this._qt.add(...params);
} catch(err) {
throw new StorageError("add failed", err);
}
}
get(...params) {
try {
return this._qt.get(...params);
} catch(err) {
throw new StorageError("get failed", err);
}
}
index(...params) {
try {
return this._qt.index(...params);
} catch(err) {
throw new StorageError("index failed", err);
}
}
}
export default class Store extends QueryTarget { export default class Store extends QueryTarget {
constructor(idbStore) { constructor(idbStore) {
super(idbStore); super(new QueryTargetWrapper(idbStore));
} }
get _idbStore() { get _idbStore() {
@ -11,7 +66,7 @@ export default class Store extends QueryTarget {
} }
index(indexName) { index(indexName) {
return new QueryTarget(this._idbStore.index(indexName)); return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName)));
} }
put(value) { put(value) {
@ -21,4 +76,4 @@ export default class Store extends QueryTarget {
add(value) { add(value) {
return reqAsPromise(this._idbStore.add(value)); return reqAsPromise(this._idbStore.add(value));
} }
} }

View File

@ -12,6 +12,8 @@ export default class RoomStateStore {
} }
async setStateEvent(roomId, event) { async setStateEvent(roomId, event) {
return this._roomStateStore.put({roomId, event}); const key = `${roomId}|${event.type}|${event.state_key}`;
const entry = {roomId, event, key};
return this._roomStateStore.put(entry);
} }
} }

View File

@ -1,4 +1,25 @@
import EventKey from "../../../room/timeline/EventKey.js"; import EventKey from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js";
import Platform from "../../../../Platform.js";
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
function encodeUint32(n) {
const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex;
}
function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
}
function encodeEventIdKey(roomId, eventId) {
return `${roomId}|${eventId}`;
}
function decodeEventIdKey(eventIdKey) {
const [roomId, eventId] = eventIdKey.split("|");
return {roomId, eventId};
}
class Range { class Range {
constructor(only, lower, upper, lowerOpen, upperOpen) { constructor(only, lower, upper, lowerOpen, upperOpen) {
@ -10,38 +31,42 @@ class Range {
} }
asIDBKeyRange(roomId) { asIDBKeyRange(roomId) {
// only try {
if (this._only) { // only
return IDBKeyRange.only([roomId, this._only.fragmentId, this._only.eventIndex]); if (this._only) {
} return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
// lowerBound }
// also bound as we don't want to move into another roomId // lowerBound
if (this._lower && !this._upper) { // also bound as we don't want to move into another roomId
return IDBKeyRange.bound( if (this._lower && !this._upper) {
[roomId, this._lower.fragmentId, this._lower.eventIndex], return IDBKeyRange.bound(
[roomId, this._lower.fragmentId, EventKey.maxKey.eventIndex], encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
this._lowerOpen, encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
false this._lowerOpen,
); false
} );
// upperBound }
// also bound as we don't want to move into another roomId // upperBound
if (!this._lower && this._upper) { // also bound as we don't want to move into another roomId
return IDBKeyRange.bound( if (!this._lower && this._upper) {
[roomId, this._upper.fragmentId, EventKey.minKey.eventIndex], return IDBKeyRange.bound(
[roomId, this._upper.fragmentId, this._upper.eventIndex], encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey),
false, encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._upperOpen false,
); this._upperOpen
} );
// bound }
if (this._lower && this._upper) { // bound
return IDBKeyRange.bound( if (this._lower && this._upper) {
[roomId, this._lower.fragmentId, this._lower.eventIndex], return IDBKeyRange.bound(
[roomId, this._upper.fragmentId, this._upper.eventIndex], encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
this._lowerOpen, encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._upperOpen this._lowerOpen,
); this._upperOpen
);
}
} catch(err) {
throw new StorageError(`IDBKeyRange failed with data: ` + JSON.stringify(this), err);
} }
} }
} }
@ -169,7 +194,7 @@ export default class TimelineEventStore {
// also passing them in chronological order makes sense as that's how we'll receive them almost always. // also passing them in chronological order makes sense as that's how we'll receive them almost always.
async findFirstOccurringEventId(roomId, eventIds) { async findFirstOccurringEventId(roomId, eventIds) {
const byEventId = this._timelineStore.index("byEventId"); const byEventId = this._timelineStore.index("byEventId");
const keys = eventIds.map(eventId => [roomId, eventId]); const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
const results = new Array(keys.length); const results = new Array(keys.length);
let firstFoundKey; let firstFoundKey;
@ -190,8 +215,7 @@ export default class TimelineEventStore {
firstFoundKey = firstFoundAndPrecedingResolved(); firstFoundKey = firstFoundAndPrecedingResolved();
return !!firstFoundKey; return !!firstFoundKey;
}); });
// key of index is [roomId, eventId], so pick out eventId return firstFoundKey && decodeEventIdKey(firstFoundKey).eventId;
return firstFoundKey && firstFoundKey[1];
} }
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown. /** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown.
@ -200,6 +224,8 @@ export default class TimelineEventStore {
* @throws {StorageError} ... * @throws {StorageError} ...
*/ */
insert(entry) { insert(entry) {
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
// TODO: map error? or in idb/store? // TODO: map error? or in idb/store?
return this._timelineStore.add(entry); return this._timelineStore.add(entry);
} }
@ -214,7 +240,7 @@ export default class TimelineEventStore {
} }
get(roomId, eventKey) { get(roomId, eventKey) {
return this._timelineStore.get([roomId, eventKey.fragmentId, eventKey.eventIndex]); return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex));
} }
// returns the entries as well!! (or not always needed? I guess not always needed, so extra method) // returns the entries as well!! (or not always needed? I guess not always needed, so extra method)
removeRange(roomId, range) { removeRange(roomId, range) {
@ -223,6 +249,6 @@ export default class TimelineEventStore {
} }
getByEventId(roomId, eventId) { getByEventId(roomId, eventId) {
return this._timelineStore.index("byEventId").get([roomId, eventId]); return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
} }
} }

View File

@ -1,13 +1,26 @@
import { StorageError } from "../../common.js";
import Platform from "../../../../Platform.js";
function encodeKey(roomId, fragmentId) {
let fragmentIdHex = fragmentId.toString(16);
fragmentIdHex = "0".repeat(8 - fragmentIdHex.length) + fragmentIdHex;
return `${roomId}|${fragmentIdHex}`;
}
export default class RoomFragmentStore { export default class RoomFragmentStore {
constructor(store) { constructor(store) {
this._store = store; this._store = store;
} }
_allRange(roomId) { _allRange(roomId) {
return IDBKeyRange.bound( try {
[roomId, Number.MIN_SAFE_INTEGER], return IDBKeyRange.bound(
[roomId, Number.MAX_SAFE_INTEGER] encodeKey(roomId, Platform.minStorageKey),
); encodeKey(roomId, Platform.maxStorageKey)
);
} catch (err) {
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);
}
} }
all(roomId) { all(roomId) {
@ -33,6 +46,7 @@ export default class RoomFragmentStore {
// depends if we want to do anything smart with fragment ids, // depends if we want to do anything smart with fragment ids,
// like give them meaning depending on range. not for now probably ... // like give them meaning depending on range. not for now probably ...
add(fragment) { add(fragment) {
fragment.key = encodeKey(fragment.roomId, fragment.id);
return this._store.add(fragment); return this._store.add(fragment);
} }
@ -41,6 +55,6 @@ export default class RoomFragmentStore {
} }
get(roomId, fragmentId) { get(roomId, fragmentId) {
return this._store.get([roomId, fragmentId]); return this._store.get(encodeKey(roomId, fragmentId));
} }
} }

View File

@ -1,4 +1,5 @@
import {txnAsPromise} from "./utils.js"; import {txnAsPromise} from "./utils.js";
import {StorageError} from "../common.js";
import Store from "./store.js"; import Store from "./store.js";
import SessionStore from "./stores/SessionStore.js"; import SessionStore from "./stores/SessionStore.js";
import RoomSummaryStore from "./stores/RoomSummaryStore.js"; import RoomSummaryStore from "./stores/RoomSummaryStore.js";
@ -21,7 +22,7 @@ export default class Transaction {
_idbStore(name) { _idbStore(name) {
if (!this._allowedStoreNames.includes(name)) { if (!this._allowedStoreNames.includes(name)) {
// more specific error? this is a bug, so maybe not ... // more specific error? this is a bug, so maybe not ...
throw new Error(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
} }
return new Store(this._txn.objectStore(name)); return new Store(this._txn.objectStore(name));
} }

View File

@ -1,3 +1,5 @@
import { StorageError } from "../common.js";
export function openDatabase(name, createObjectStore, version) { export function openDatabase(name, createObjectStore, version) {
const req = window.indexedDB.open(name, version); const req = window.indexedDB.open(name, version);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev) => {
@ -8,17 +10,21 @@ export function openDatabase(name, createObjectStore, version) {
return reqAsPromise(req); return reqAsPromise(req);
} }
function wrapError(err) {
return new StorageError(`wrapped DOMException`, err);
}
export function reqAsPromise(req) { export function reqAsPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => resolve(event.target.result)); req.addEventListener("success", event => resolve(event.target.result));
req.addEventListener("error", event => reject(event.target.error)); req.addEventListener("error", event => reject(wrapError(event.target.error)));
}); });
} }
export function txnAsPromise(txn) { export function txnAsPromise(txn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", resolve); txn.addEventListener("complete", resolve);
txn.addEventListener("abort", reject); txn.addEventListener("abort", e => reject(wrapError(e)));
}); });
} }
@ -38,10 +44,14 @@ export function iterateCursor(cursor, processValue) {
const {done, jumpTo} = processValue(cursor.value, cursor.key); const {done, jumpTo} = processValue(cursor.value, cursor.key);
if (done) { if (done) {
resolve(true); resolve(true);
} else { } else if(jumpTo) {
cursor.continue(jumpTo); cursor.continue(jumpTo);
} else {
cursor.continue();
} }
}; };
}).catch(err => {
throw new StorageError("iterateCursor failed", err);
}); });
} }
@ -97,7 +107,7 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
} }
}); });
if (!matched) { if (!matched) {
throw new Error("Value not found"); throw new StorageError("Value not found");
} }
return match; return match;
} }

View File

@ -1,8 +1,4 @@
import { import {RequestAbortError} from "./error.js";
RequestAbortError,
HomeServerError,
StorageError
} from "./error.js";
import EventEmitter from "../EventEmitter.js"; import EventEmitter from "../EventEmitter.js";
const INCREMENTAL_TIMEOUT = 30000; const INCREMENTAL_TIMEOUT = 30000;
@ -111,7 +107,8 @@ export default class Sync extends EventEmitter {
await syncTxn.complete(); await syncTxn.complete();
console.info("syncTxn committed!!"); console.info("syncTxn committed!!");
} catch (err) { } catch (err) {
throw new StorageError("unable to commit sync tranaction", err); console.error("unable to commit sync tranaction", err.message);
throw err;
} }
// emit room related events after txn has been closed // emit room related events after txn has been closed
for(let {room, changes} of roomChanges) { for(let {room, changes} of roomChanges) {

16
src/ui/web/WebPlatform.js Normal file
View File

@ -0,0 +1,16 @@
export default {
get minStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0;
},
get middleStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0x7FFFFFFF;
},
get maxStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0xFFFFFFFF;
},
}

View File

@ -11,7 +11,8 @@ export default class SyncStatusBar extends TemplateView {
"SyncStatusBar_shown": true, "SyncStatusBar_shown": true,
}}, [ }}, [
vm => vm.status, vm => vm.status,
t.if(vm => !vm.isSyncing, t => t.button({onClick: () => vm.trySync()}, "Try syncing")) t.if(vm => !vm.isSyncing, t => t.button({onClick: () => vm.trySync()}, "Try syncing")),
window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : ""
]); ]);
} }
} }

197
yarn.lock Normal file
View File

@ -0,0 +1,197 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/node@^12.0.8":
version "12.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.8.tgz#551466be11b2adc3f3d47156758f610bd9f6b1d8"
integrity sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==
acorn@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
colors@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==
commander@^2.19.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
finalhandler@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
impunity@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/impunity/-/impunity-0.0.7.tgz#15b8aaafcc98dcf3a4b5bcfa0beea4eea63d760f"
integrity sha512-+DhzXSWrzqI1KNroKt3y1LkLTn/aoJpt4DzxWN+hair+Jfb+iJAbTEsSFkYUG7kASP9TF9GvI0hIBUul6PjpKg==
dependencies:
colors "^1.3.3"
commander "^2.19.0"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
rollup@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.15.6.tgz#caf0ed28d2d78e3a59c1398e5a3695fb600a0ef0"
integrity sha512-s3Vn3QJQ5YVFfIG4nXoG9VdL1I37IZsft+4ZyeBhxE0df1kCFz9e+4bEAbR4mKH3pvBO9e9xjdxWPhhIp0r9ow==
dependencies:
"@types/estree" "0.0.39"
"@types/node" "^12.0.8"
acorn "^6.1.1"
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@^1.13.2:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=