2021-02-11 21:07:18 +01:00
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
2021-03-01 15:01:43 +01:00
|
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
2021-02-11 21:07:18 +01:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-02-12 13:04:05 +01:00
|
|
|
import {
|
|
|
|
openDatabase,
|
|
|
|
txnAsPromise,
|
|
|
|
reqAsPromise,
|
|
|
|
iterateCursor,
|
|
|
|
fetchResults,
|
|
|
|
} from "../matrix/storage/idb/utils.js";
|
|
|
|
import {BaseLogger} from "./BaseLogger.js";
|
|
|
|
|
|
|
|
export class IDBLogger extends BaseLogger {
|
2021-02-12 16:08:07 +01:00
|
|
|
constructor(options) {
|
|
|
|
super(options);
|
2021-02-16 17:45:47 +01:00
|
|
|
const {name, flushInterval = 60 * 1000, limit = 3000} = options;
|
2021-02-11 21:07:18 +01:00
|
|
|
this._name = name;
|
|
|
|
this._limit = limit;
|
|
|
|
this._queuedItems = this._loadQueuedItems();
|
2021-02-12 13:04:05 +01:00
|
|
|
// TODO: also listen for unload just in case sync keeps on running after pagehide is fired?
|
2021-02-11 21:07:18 +01:00
|
|
|
window.addEventListener("pagehide", this, false);
|
2021-02-12 18:05:39 +01:00
|
|
|
this._flushInterval = this._platform.clock.createInterval(() => this._tryFlush(), flushInterval);
|
2021-02-11 21:07:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2021-02-12 18:05:39 +01:00
|
|
|
const txn = db.transaction(["logs"], "readwrite");
|
2021-02-11 21:07:18 +01:00
|
|
|
const logs = txn.objectStore("logs");
|
|
|
|
const amount = this._queuedItems.length;
|
|
|
|
for(const i of this._queuedItems) {
|
|
|
|
logs.add(i);
|
|
|
|
}
|
|
|
|
const itemCount = await reqAsPromise(logs.count());
|
|
|
|
if (itemCount > this._limit) {
|
2021-02-16 17:45:19 +01:00
|
|
|
// delete an extra 10% so we don't need to delete every time we flush
|
|
|
|
let deleteAmount = (itemCount - this._limit) + Math.round(0.1 * this._limit);
|
2021-02-11 21:07:18 +01:00
|
|
|
await iterateCursor(logs.openCursor(), (_, __, cursor) => {
|
|
|
|
cursor.delete();
|
2021-02-16 17:45:19 +01:00
|
|
|
deleteAmount -= 1;
|
|
|
|
return {done: deleteAmount === 0};
|
2021-02-11 21:07:18 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
await txnAsPromise(txn);
|
|
|
|
this._queuedItems.splice(0, amount);
|
2021-02-16 17:45:19 +01:00
|
|
|
} catch (err) {
|
2021-02-16 18:36:41 +01:00
|
|
|
console.error("Could not flush logs", err);
|
2021-02-11 21:07:18 +01:00
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
db.close();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_finishAllAndFlush() {
|
2021-02-12 14:05:51 +01:00
|
|
|
this._finishOpenItems();
|
2021-02-18 19:46:40 +01:00
|
|
|
this.log({l: "pagehide, closing logs", t: "navigation"});
|
2021-02-11 21:07:18 +01:00
|
|
|
this._persistQueuedItems(this._queuedItems);
|
|
|
|
}
|
|
|
|
|
|
|
|
_loadQueuedItems() {
|
|
|
|
const key = `${this._name}_queuedItems`;
|
2021-02-12 13:04:05 +01:00
|
|
|
try {
|
|
|
|
const json = window.localStorage.getItem(key);
|
|
|
|
if (json) {
|
|
|
|
window.localStorage.removeItem(key);
|
|
|
|
return JSON.parse(json);
|
|
|
|
}
|
2021-02-16 19:57:45 +01:00
|
|
|
} catch (err) {
|
|
|
|
console.error("Could not load queued log items", err);
|
|
|
|
}
|
2021-02-11 21:07:18 +01:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
_openDB() {
|
2021-02-22 10:46:40 +01:00
|
|
|
return openDatabase(this._name, db => db.createObjectStore("logs", {keyPath: "id", autoIncrement: true}), 1);
|
2021-02-11 21:07:18 +01:00
|
|
|
}
|
|
|
|
|
2021-03-01 15:01:43 +01:00
|
|
|
_persistItem(logItem, filter, forced) {
|
|
|
|
const serializedItem = logItem.serialize(filter, forced);
|
2021-02-11 21:07:18 +01:00
|
|
|
this._queuedItems.push({
|
2021-02-16 15:08:08 +01:00
|
|
|
json: JSON.stringify(serializedItem)
|
2021-02-11 21:07:18 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
_persistQueuedItems(items) {
|
2021-02-12 13:04:05 +01:00
|
|
|
try {
|
|
|
|
window.localStorage.setItem(`${this._name}_queuedItems`, JSON.stringify(items));
|
|
|
|
} catch (e) {
|
2021-02-16 18:36:41 +01:00
|
|
|
console.error("Could not persist queued log items in localStorage, they will likely be lost", e);
|
2021-02-12 13:04:05 +01:00
|
|
|
}
|
2021-02-11 21:07:18 +01:00
|
|
|
}
|
|
|
|
|
2021-02-12 13:04:05 +01:00
|
|
|
async export() {
|
2021-02-12 18:05:39 +01:00
|
|
|
const db = await this._openDB();
|
2021-02-11 21:07:18 +01:00
|
|
|
try {
|
2021-02-12 18:05:39 +01:00
|
|
|
const txn = db.transaction(["logs"], "readonly");
|
2021-02-11 21:07:18 +01:00
|
|
|
const logs = txn.objectStore("logs");
|
2021-02-16 11:37:48 +01:00
|
|
|
const storedItems = await fetchResults(logs.openCursor(), () => false);
|
|
|
|
const allItems = storedItems.concat(this._queuedItems);
|
2021-02-22 10:46:40 +01:00
|
|
|
return new IDBLogExport(allItems, this, this._platform);
|
2021-02-11 21:07:18 +01:00
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
db.close();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
2021-02-12 13:04:05 +01:00
|
|
|
|
|
|
|
async _removeItems(items) {
|
2021-02-12 18:05:39 +01:00
|
|
|
const db = await this._openDB();
|
2021-02-12 13:04:05 +01:00
|
|
|
try {
|
2021-02-12 18:05:39 +01:00
|
|
|
const txn = db.transaction(["logs"], "readwrite");
|
2021-02-12 13:04:05 +01:00
|
|
|
const logs = txn.objectStore("logs");
|
|
|
|
for (const item of items) {
|
|
|
|
const queuedIdx = this._queuedItems.findIndex(i => i.id === item.id);
|
|
|
|
if (queuedIdx === -1) {
|
|
|
|
logs.delete(item.id);
|
|
|
|
} else {
|
|
|
|
this._queuedItems.splice(queuedIdx, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await txnAsPromise(txn);
|
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
db.close();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class IDBLogExport {
|
|
|
|
constructor(items, logger, platform) {
|
|
|
|
this._items = items;
|
|
|
|
this._logger = logger;
|
|
|
|
this._platform = platform;
|
|
|
|
}
|
2021-02-16 20:00:08 +01:00
|
|
|
|
2021-02-12 16:08:28 +01:00
|
|
|
get count() {
|
|
|
|
return this._items.length;
|
|
|
|
}
|
|
|
|
|
2021-02-12 13:04:05 +01:00
|
|
|
/**
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
|
|
|
removeFromStore() {
|
|
|
|
return this._logger._removeItems(this._items);
|
|
|
|
}
|
|
|
|
|
|
|
|
asBlob() {
|
|
|
|
const log = {
|
2021-02-17 17:14:24 +01:00
|
|
|
formatVersion: 1,
|
|
|
|
appVersion: this._platform.updateService?.version,
|
2021-02-16 15:08:08 +01:00
|
|
|
items: this._items.map(i => JSON.parse(i.json))
|
2021-02-12 13:04:05 +01:00
|
|
|
};
|
|
|
|
const json = JSON.stringify(log);
|
2021-02-12 16:08:07 +01:00
|
|
|
const buffer = this._platform.encoding.utf8.encode(json);
|
2021-02-12 13:04:05 +01:00
|
|
|
const blob = this._platform.createBlob(buffer, "application/json");
|
|
|
|
return blob;
|
|
|
|
}
|
2021-02-11 21:07:18 +01:00
|
|
|
}
|