WIP
This commit is contained in:
parent
e49639fda2
commit
89599e9f87
5 changed files with 343 additions and 11 deletions
|
@ -30,8 +30,8 @@ export class ViewModel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
childOptions(explicitOptions) {
|
childOptions(explicitOptions) {
|
||||||
const {navigation, urlCreator, platform} = this._options;
|
const {navigation, urlCreator, platform, logger} = this._options;
|
||||||
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
|
return Object.assign({navigation, urlCreator, platform, logger}, explicitOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// makes it easier to pass through dependencies of a sub-view model
|
// makes it easier to pass through dependencies of a sub-view model
|
||||||
|
@ -78,14 +78,9 @@ export class ViewModel extends EventEmitter {
|
||||||
// we probably are, if we're using routing with a url, we could just refresh.
|
// we probably are, if we're using routing with a url, we could just refresh.
|
||||||
i18n(parts, ...expr) {
|
i18n(parts, ...expr) {
|
||||||
// just concat for now
|
// just concat for now
|
||||||
let result = "";
|
return parts.reduce((all, p, i) => {
|
||||||
for (let i = 0; i < parts.length; ++i) {
|
return all + p + expr[i];
|
||||||
result = result + parts[i];
|
});
|
||||||
if (i < expr.length) {
|
|
||||||
result = result + expr[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOptions(options) {
|
updateOptions(options) {
|
||||||
|
@ -108,6 +103,10 @@ export class ViewModel extends EventEmitter {
|
||||||
return this._options.platform.clock;
|
return this._options.platform.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get logger() {
|
||||||
|
return this._options.logger;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The url router, only meant to be used to create urls with from view models.
|
* The url router, only meant to be used to create urls with from view models.
|
||||||
* @return {URLRouter}
|
* @return {URLRouter}
|
||||||
|
|
188
src/logging/IDBLogger.js
Normal file
188
src/logging/IDBLogger.js
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {openDatabase, txnAsPromise, reqAsPromise, iterateCursor, fetchResults} from "../matrix/storage/idb/utils.js";
|
||||||
|
import {LogItem} from "./LogItem.js";
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor(clock) {
|
||||||
|
this._openItems = new Set();
|
||||||
|
this._clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
decend(label, callback, logLevel) {
|
||||||
|
const item = new LogItem(label, this, logLevel, this._clock);
|
||||||
|
|
||||||
|
const failItem = (err) => {
|
||||||
|
item.catch(err);
|
||||||
|
finishItem();
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishItem = () => {
|
||||||
|
item.finish();
|
||||||
|
this._persistItem(item);
|
||||||
|
this._openItems.remove(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = callback(item);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
result = result.then(promiseResult => {
|
||||||
|
finishItem();
|
||||||
|
return promiseResult;
|
||||||
|
}, failItem);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
failItem(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistItem(item) {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractItems() {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUint64(n) {
|
||||||
|
const hex = n.toString(16);
|
||||||
|
return "0".repeat(16 - hex.length) + hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IDBLogger extends Logger {
|
||||||
|
constructor({name, clock, utf8, flushInterval = 2 * 60 * 1000, limit = 1000}) {
|
||||||
|
super(clock);
|
||||||
|
this._utf8 = utf8;
|
||||||
|
this._name = name;
|
||||||
|
this._limit = limit;
|
||||||
|
// does not get loaded from idb on startup as we only use it to
|
||||||
|
// differentiate between two items with the same start time
|
||||||
|
this._itemCounter = 0;
|
||||||
|
this._queuedItems = this._loadQueuedItems();
|
||||||
|
window.addEventListener("pagehide", this, false);
|
||||||
|
this._flushInterval = this._clock.createInterval(() => this._tryFlush(), flushInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
window.removeEventListener("pagehide", this, false);
|
||||||
|
this._flushInterval.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(evt) {
|
||||||
|
if (evt.type === "pagehide") {
|
||||||
|
this._finishAllAndFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _tryFlush() {
|
||||||
|
const db = await this._openDB();
|
||||||
|
try {
|
||||||
|
const txn = this.db.transaction(["logs"], "readwrite");
|
||||||
|
const logs = txn.objectStore("logs");
|
||||||
|
const amount = this._queuedItems.length;
|
||||||
|
for(const i of this._queuedItems) {
|
||||||
|
logs.add(i);
|
||||||
|
}
|
||||||
|
// trim logs if needed
|
||||||
|
const itemCount = await reqAsPromise(logs.count());
|
||||||
|
if (itemCount > this._limit) {
|
||||||
|
let currentCount = itemCount;
|
||||||
|
await iterateCursor(logs.openCursor(), (_, __, cursor) => {
|
||||||
|
cursor.delete();
|
||||||
|
currentCount -= 1;
|
||||||
|
return {done: currentCount <= this._limit};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await txnAsPromise(txn);
|
||||||
|
this._queuedItems.splice(0, amount);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
db.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_finishAllAndFlush() {
|
||||||
|
for (const openItem of this._openItems) {
|
||||||
|
openItem.finish();
|
||||||
|
this._persistItem(openItem);
|
||||||
|
}
|
||||||
|
this._openItems.clear();
|
||||||
|
this._persistQueuedItems(this._queuedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadQueuedItems() {
|
||||||
|
const key = `${this._name}_queuedItems`;
|
||||||
|
const json = window.localStorage.getItem(key);
|
||||||
|
if (json) {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
return JSON.parse(json);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_openDB() {
|
||||||
|
return openDatabase(this._name, db => db.createObjectStore("logs", {keyPath: "id"}), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistItem(item) {
|
||||||
|
this._itemCounter += 1;
|
||||||
|
this._queuedItems.push({
|
||||||
|
id: `${encodeUint64(item.start)}:${this._itemCounter}`,
|
||||||
|
// store as buffer so parsing overhead is lower
|
||||||
|
content: this._utf8.encode(JSON.stringify(item.serialize()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistQueuedItems(items) {
|
||||||
|
window.localStorage.setItem(`${this._name}_queuedItems`, JSON.stringify(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
// should we actually delete items and just not rely on trimming for it not to grow too large?
|
||||||
|
// just worried that people will create a file, not do anything with it, hit export logs again and then
|
||||||
|
// send a mostly empty file. we could just not delete but in _tryFlush (and perhaps make trimming delete a few more than needed to be below limit)
|
||||||
|
// how does eleweb handle this?
|
||||||
|
//
|
||||||
|
// both deletes and reads items from store
|
||||||
|
async extractItems() {
|
||||||
|
const db = this._openDB();
|
||||||
|
try {
|
||||||
|
const queuedItems = this._queuedItems.slice();
|
||||||
|
const txn = this.db.transaction(["logs"], "readwrite");
|
||||||
|
const logs = txn.objectStore("logs");
|
||||||
|
const items = await fetchResults(logs.openCursor(), () => false);
|
||||||
|
// we know we have read all the items as we're doing this in a txn
|
||||||
|
logs.clear();
|
||||||
|
await txnAsPromise(txn);
|
||||||
|
// once the transaction is complete, remove the queued items
|
||||||
|
this._queuedItems.splice(0, queuedItems.length);
|
||||||
|
const sortedItems = items.concat(queuedItems).sort((a, b) => {
|
||||||
|
return a.id > b.id;
|
||||||
|
}).map(i => {
|
||||||
|
|
||||||
|
});
|
||||||
|
return sortedItems;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
db.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
src/logging/LogItem.js
Normal file
94
src/logging/LogItem.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class LogItem {
|
||||||
|
constructor(label, parent, logLevel, clock) {
|
||||||
|
this._clock = clock;
|
||||||
|
this._start = clock.now();
|
||||||
|
this._end = null;
|
||||||
|
this._values = {label};
|
||||||
|
this._parent = parent;
|
||||||
|
this._error = null;
|
||||||
|
this._children = [];
|
||||||
|
this._logLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should this handle errors in the same way as logger.descend/start?
|
||||||
|
descend(label, logLevel = this._logLevel) {
|
||||||
|
if (this._end !== null) {
|
||||||
|
throw new Error("can't descend on finished item");
|
||||||
|
}
|
||||||
|
const item = new LogItem(label, logLevel);
|
||||||
|
this._children.push(item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
if(typeof key === "object") {
|
||||||
|
const values = key;
|
||||||
|
Object.assign(this._values, values);
|
||||||
|
} else {
|
||||||
|
this._values[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: where will this be called? from wrapAsync?
|
||||||
|
finish() {
|
||||||
|
if (this._end === null) {
|
||||||
|
for(const c of this._children) {
|
||||||
|
c.finish();
|
||||||
|
}
|
||||||
|
this._end = this._clock.now();
|
||||||
|
if (this._parent) {
|
||||||
|
this._parent._closeChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch(err) {
|
||||||
|
this._error = err;
|
||||||
|
console.error(`log item ${this.values.label} failed: ${err.message}:\n${err.stack}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
let error;
|
||||||
|
if (this._error) {
|
||||||
|
error = {
|
||||||
|
message: this._error.message,
|
||||||
|
stack: this._error.stack,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start: this._start,
|
||||||
|
end: this._end,
|
||||||
|
values: this._values,
|
||||||
|
error,
|
||||||
|
children: this._children.map(c => c.serialize()),
|
||||||
|
logLevel: this._logLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async wrapAsync(fn) {
|
||||||
|
try {
|
||||||
|
const ret = await fn(this);
|
||||||
|
this.finish();
|
||||||
|
return ret;
|
||||||
|
} catch (err) {
|
||||||
|
this.fail(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/logging/Logger.js
Normal file
51
src/logging/Logger.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {LogItem} from "./LogItem.js";
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
constructor(persister) {
|
||||||
|
this._openItems = [];
|
||||||
|
this._persister = persister;
|
||||||
|
this._closing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
const items = this._persister.loadTempItems();
|
||||||
|
return this._persister.persistItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
start(label, logLevel) {
|
||||||
|
const item = new LogItem(label, logLevel, this);
|
||||||
|
this._openItems.push(item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeChild(item) {
|
||||||
|
if (!this._closing) {
|
||||||
|
this._persister.persistItems([item.serialize()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._closing = true;
|
||||||
|
for(const i of this._openItems) {
|
||||||
|
i.finish();
|
||||||
|
}
|
||||||
|
this._closing = false;
|
||||||
|
this._persister.persistTempItems(this._openItems.map(i => i.serialize()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,7 +110,7 @@ export function iterateCursor(cursorRequest, processValue) {
|
||||||
needsSyncPromise && Promise._flush && Promise._flush();
|
needsSyncPromise && Promise._flush && Promise._flush();
|
||||||
return; // end of results
|
return; // end of results
|
||||||
}
|
}
|
||||||
const result = processValue(cursor.value, cursor.key);
|
const result = processValue(cursor.value, cursor.key, cursor);
|
||||||
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
|
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
|
||||||
const done = result?.done;
|
const done = result?.done;
|
||||||
const jumpTo = result?.jumpTo;
|
const jumpTo = result?.jumpTo;
|
||||||
|
|
Reference in a new issue