Merge pull request #583 from vector-im/ts-conversion-logging

Convert src/logging to typescript
This commit is contained in:
Bruno Windels 2021-11-17 14:37:42 +01:00 committed by GitHub
commit efccc1e19e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 464 additions and 316 deletions

View file

@ -184,7 +184,7 @@ import {Clock as MockClock} from "../../../../mocks/Clock.js";
import {createMockStorage} from "../../../../mocks/Storage"; import {createMockStorage} from "../../../../mocks/Storage";
import {ListObserver} from "../../../../mocks/ListObserver.js"; import {ListObserver} from "../../../../mocks/ListObserver.js";
import {createEvent, withTextBody, withContent} from "../../../../mocks/event.js"; import {createEvent, withTextBody, withContent} from "../../../../mocks/event.js";
import {NullLogItem, NullLogger} from "../../../../logging/NullLogger.js"; import {NullLogItem, NullLogger} from "../../../../logging/NullLogger";
import {HomeServer as MockHomeServer} from "../../../../mocks/HomeServer.js"; import {HomeServer as MockHomeServer} from "../../../../mocks/HomeServer.js";
// other imports // other imports
import {BaseMessageTile} from "./tiles/BaseMessageTile.js"; import {BaseMessageTile} from "./tiles/BaseMessageTile.js";

View file

@ -15,23 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {LogItem} from "./LogItem.js"; import {LogItem} from "./LogItem";
import {LogLevel, LogFilter} from "./LogFilter.js"; import {LogLevel, LogFilter} from "./LogFilter";
import type {ILogger, ILogExport, FilterCreator, LabelOrValues, LogCallback, ILogItem} from "./types";
import type {Platform} from "../platform/web/Platform.js";
export abstract class BaseLogger implements ILogger {
protected _openItems: Set<LogItem> = new Set();
protected _platform: Platform;
export class BaseLogger {
constructor({platform}) { constructor({platform}) {
this._openItems = new Set();
this._platform = platform; this._platform = platform;
} }
log(labelOrValues, logLevel = LogLevel.Info) { log(labelOrValues: LabelOrValues, logLevel: LogLevel = LogLevel.Info): void {
const item = new LogItem(labelOrValues, logLevel, null, this); const item = new LogItem(labelOrValues, logLevel, this);
item._end = item._start; item.end = item.start;
this._persistItem(item, null, false); this._persistItem(item, undefined, false);
} }
/** if item is a log item, wrap the callback in a child of it, otherwise start a new root log item. */ /** if item is a log item, wrap the callback in a child of it, otherwise start a new root log item. */
wrapOrRun(item, labelOrValues, callback, logLevel = null, filterCreator = null) { wrapOrRun<T>(item: ILogItem | undefined, labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T {
if (item) { if (item) {
return item.wrap(labelOrValues, callback, logLevel, filterCreator); return item.wrap(labelOrValues, callback, logLevel, filterCreator);
} else { } else {
@ -43,28 +47,31 @@ export class BaseLogger {
where the (async) result or errors are not propagated but still logged. where the (async) result or errors are not propagated but still logged.
Useful to pair with LogItem.refDetached. Useful to pair with LogItem.refDetached.
@return {LogItem} the log item added, useful to pass to LogItem.refDetached */ @return {ILogItem} the log item added, useful to pass to LogItem.refDetached */
runDetached(labelOrValues, callback, logLevel = null, filterCreator = null) { runDetached<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem {
if (logLevel === null) { if (!logLevel) {
logLevel = LogLevel.Info; logLevel = LogLevel.Info;
} }
const item = new LogItem(labelOrValues, logLevel, null, this); const item = new LogItem(labelOrValues, logLevel, this);
this._run(item, callback, logLevel, filterCreator, false /* don't throw, nobody is awaiting */); this._run(item, callback, logLevel, false /* don't throw, nobody is awaiting */, filterCreator);
return item; return item;
} }
/** run a callback wrapped in a log operation. /** run a callback wrapped in a log operation.
Errors and duration are transparently logged, also for async operations. Errors and duration are transparently logged, also for async operations.
Whatever the callback returns is returned here. */ Whatever the callback returns is returned here. */
run(labelOrValues, callback, logLevel = null, filterCreator = null) { run<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T {
if (logLevel === null) { if (logLevel === undefined) {
logLevel = LogLevel.Info; logLevel = LogLevel.Info;
} }
const item = new LogItem(labelOrValues, logLevel, null, this); const item = new LogItem(labelOrValues, logLevel, this);
return this._run(item, callback, logLevel, filterCreator, true); return this._run(item, callback, logLevel, true, filterCreator);
} }
_run(item, callback, logLevel, filterCreator, shouldThrow) { _run<T>(item: LogItem, callback: LogCallback<T>, logLevel: LogLevel, wantResult: true, filterCreator?: FilterCreator): T;
// we don't return if we don't throw, as we don't have anything to return when an error is caught but swallowed for the fire-and-forget case.
_run<T>(item: LogItem, callback: LogCallback<T>, logLevel: LogLevel, wantResult: false, filterCreator?: FilterCreator): void;
_run<T>(item: LogItem, callback: LogCallback<T>, logLevel: LogLevel, wantResult: boolean, filterCreator?: FilterCreator): T | void {
this._openItems.add(item); this._openItems.add(item);
const finishItem = () => { const finishItem = () => {
@ -88,24 +95,29 @@ export class BaseLogger {
}; };
try { try {
const result = item.run(callback); let result = item.run(callback);
if (result instanceof Promise) { if (result instanceof Promise) {
return result.then(promiseResult => { result = result.then(promiseResult => {
finishItem(); finishItem();
return promiseResult; return promiseResult;
}, err => { }, err => {
finishItem(); finishItem();
if (shouldThrow) { if (wantResult) {
throw err; throw err;
} }
}); }) as unknown as T;
if (wantResult) {
return result;
}
} else { } else {
finishItem(); finishItem();
if(wantResult) {
return result; return result;
} }
}
} catch (err) { } catch (err) {
finishItem(); finishItem();
if (shouldThrow) { if (wantResult) {
throw err; throw err;
} }
} }
@ -127,24 +139,20 @@ export class BaseLogger {
this._openItems.clear(); this._openItems.clear();
} }
_persistItem() { abstract _persistItem(item: LogItem, filter?: LogFilter, forced?: boolean): void;
throw new Error("not implemented");
}
async export() { abstract export(): Promise<ILogExport | undefined>;
throw new Error("not implemented");
}
// expose log level without needing // expose log level without needing
get level() { get level(): typeof LogLevel {
return LogLevel; return LogLevel;
} }
_now() { _now(): number {
return this._platform.clock.now(); return this._platform.clock.now();
} }
_createRefId() { _createRefId(): number {
return Math.round(this._platform.random() * Number.MAX_SAFE_INTEGER); return Math.round(this._platform.random() * Number.MAX_SAFE_INTEGER);
} }
} }

View file

@ -13,32 +13,35 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseLogger} from "./BaseLogger.js"; import {BaseLogger} from "./BaseLogger";
import {LogItem} from "./LogItem";
import type {ILogItem, LogItemValues, ILogExport} from "./types";
export class ConsoleLogger extends BaseLogger { export class ConsoleLogger extends BaseLogger {
_persistItem(item) { _persistItem(item: LogItem): void {
printToConsole(item); printToConsole(item);
} }
async export(): Promise<ILogExport | undefined> {
return undefined;
}
} }
const excludedKeysFromTable = ["l", "id"]; const excludedKeysFromTable = ["l", "id"];
function filterValues(values) { function filterValues(values: LogItemValues): LogItemValues | null {
if (!values) {
return null;
}
return Object.entries(values) return Object.entries(values)
.filter(([key]) => !excludedKeysFromTable.includes(key)) .filter(([key]) => !excludedKeysFromTable.includes(key))
.reduce((obj, [key, value]) => { .reduce((obj: LogItemValues, [key, value]) => {
obj = obj || {}; obj = obj || {};
obj[key] = value; obj[key] = value;
return obj; return obj;
}, null); }, null);
} }
function printToConsole(item) { function printToConsole(item: LogItem): void {
const label = `${itemCaption(item)} (${item.duration}ms)`; const label = `${itemCaption(item)} (${item.duration}ms)`;
const filteredValues = filterValues(item._values); const filteredValues = filterValues(item.values);
const shouldGroup = item._children || filteredValues; const shouldGroup = item.children || filteredValues;
if (shouldGroup) { if (shouldGroup) {
if (item.error) { if (item.error) {
console.group(label); console.group(label);
@ -58,8 +61,8 @@ function printToConsole(item) {
if (filteredValues) { if (filteredValues) {
console.table(filteredValues); console.table(filteredValues);
} }
if (item._children) { if (item.children) {
for(const c of item._children) { for(const c of item.children) {
printToConsole(c); printToConsole(c);
} }
} }
@ -68,18 +71,18 @@ function printToConsole(item) {
} }
} }
function itemCaption(item) { function itemCaption(item: ILogItem): string {
if (item._values.t === "network") { if (item.values.t === "network") {
return `${item._values.method} ${item._values.url}`; return `${item.values.method} ${item.values.url}`;
} else if (item._values.l && typeof item._values.id !== "undefined") { } else if (item.values.l && typeof item.values.id !== "undefined") {
return `${item._values.l} ${item._values.id}`; return `${item.values.l} ${item.values.id}`;
} else if (item._values.l && typeof item._values.status !== "undefined") { } else if (item.values.l && typeof item.values.status !== "undefined") {
return `${item._values.l} (${item._values.status})`; return `${item.values.l} (${item.values.status})`;
} else if (item._values.l && item.error) { } else if (item.values.l && item.error) {
return `${item._values.l} failed`; return `${item.values.l} failed`;
} else if (typeof item._values.ref !== "undefined") { } else if (typeof item.values.ref !== "undefined") {
return `ref ${item._values.ref}`; return `ref ${item.values.ref}`;
} else { } else {
return item._values.l || item._values.type; return item.values.l || item.values.type;
} }
} }

View file

@ -22,10 +22,25 @@ import {
iterateCursor, iterateCursor,
fetchResults, fetchResults,
} from "../matrix/storage/idb/utils"; } from "../matrix/storage/idb/utils";
import {BaseLogger} from "./BaseLogger.js"; import {BaseLogger} from "./BaseLogger";
import type {Interval} from "../platform/web/dom/Clock";
import type {Platform} from "../platform/web/Platform.js";
import type {BlobHandle} from "../platform/web/dom/BlobHandle.js";
import type {ILogItem, ILogExport} from "./types";
import type {LogFilter} from "./LogFilter";
type QueuedItem = {
json: string;
id?: number;
}
export class IDBLogger extends BaseLogger { export class IDBLogger extends BaseLogger {
constructor(options) { private readonly _name: string;
private readonly _limit: number;
private readonly _flushInterval: Interval;
private _queuedItems: QueuedItem[];
constructor(options: {name: string, flushInterval?: number, limit?: number, platform: Platform}) {
super(options); super(options);
const {name, flushInterval = 60 * 1000, limit = 3000} = options; const {name, flushInterval = 60 * 1000, limit = 3000} = options;
this._name = name; this._name = name;
@ -36,18 +51,19 @@ export class IDBLogger extends BaseLogger {
this._flushInterval = this._platform.clock.createInterval(() => this._tryFlush(), flushInterval); this._flushInterval = this._platform.clock.createInterval(() => this._tryFlush(), flushInterval);
} }
dispose() { // TODO: move dispose to ILogger, listen to pagehide elsewhere and call dispose from there, which calls _finishAllAndFlush
dispose(): void {
window.removeEventListener("pagehide", this, false); window.removeEventListener("pagehide", this, false);
this._flushInterval.dispose(); this._flushInterval.dispose();
} }
handleEvent(evt) { handleEvent(evt: Event): void {
if (evt.type === "pagehide") { if (evt.type === "pagehide") {
this._finishAllAndFlush(); this._finishAllAndFlush();
} }
} }
async _tryFlush() { async _tryFlush(): Promise<void> {
const db = await this._openDB(); const db = await this._openDB();
try { try {
const txn = db.transaction(["logs"], "readwrite"); const txn = db.transaction(["logs"], "readwrite");
@ -77,13 +93,13 @@ export class IDBLogger extends BaseLogger {
} }
} }
_finishAllAndFlush() { _finishAllAndFlush(): void {
this._finishOpenItems(); this._finishOpenItems();
this.log({l: "pagehide, closing logs", t: "navigation"}); this.log({l: "pagehide, closing logs", t: "navigation"});
this._persistQueuedItems(this._queuedItems); this._persistQueuedItems(this._queuedItems);
} }
_loadQueuedItems() { _loadQueuedItems(): QueuedItem[] {
const key = `${this._name}_queuedItems`; const key = `${this._name}_queuedItems`;
try { try {
const json = window.localStorage.getItem(key); const json = window.localStorage.getItem(key);
@ -97,18 +113,18 @@ export class IDBLogger extends BaseLogger {
return []; return [];
} }
_openDB() { _openDB(): Promise<IDBDatabase> {
return openDatabase(this._name, db => db.createObjectStore("logs", {keyPath: "id", autoIncrement: true}), 1); return openDatabase(this._name, db => db.createObjectStore("logs", {keyPath: "id", autoIncrement: true}), 1);
} }
_persistItem(logItem, filter, forced) { _persistItem(logItem: ILogItem, filter: LogFilter, forced: boolean): void {
const serializedItem = logItem.serialize(filter, forced); const serializedItem = logItem.serialize(filter, undefined, forced);
this._queuedItems.push({ this._queuedItems.push({
json: JSON.stringify(serializedItem) json: JSON.stringify(serializedItem)
}); });
} }
_persistQueuedItems(items) { _persistQueuedItems(items: QueuedItem[]): void {
try { try {
window.localStorage.setItem(`${this._name}_queuedItems`, JSON.stringify(items)); window.localStorage.setItem(`${this._name}_queuedItems`, JSON.stringify(items));
} catch (e) { } catch (e) {
@ -116,12 +132,12 @@ export class IDBLogger extends BaseLogger {
} }
} }
async export() { async export(): Promise<ILogExport> {
const db = await this._openDB(); const db = await this._openDB();
try { try {
const txn = db.transaction(["logs"], "readonly"); const txn = db.transaction(["logs"], "readonly");
const logs = txn.objectStore("logs"); const logs = txn.objectStore("logs");
const storedItems = await fetchResults(logs.openCursor(), () => false); const storedItems: QueuedItem[] = await fetchResults(logs.openCursor(), () => false);
const allItems = storedItems.concat(this._queuedItems); const allItems = storedItems.concat(this._queuedItems);
return new IDBLogExport(allItems, this, this._platform); return new IDBLogExport(allItems, this, this._platform);
} finally { } finally {
@ -131,19 +147,22 @@ export class IDBLogger extends BaseLogger {
} }
} }
async _removeItems(items) { async _removeItems(items: QueuedItem[]): Promise<void> {
const db = await this._openDB(); const db = await this._openDB();
try { try {
const txn = db.transaction(["logs"], "readwrite"); const txn = db.transaction(["logs"], "readwrite");
const logs = txn.objectStore("logs"); const logs = txn.objectStore("logs");
for (const item of items) { for (const item of items) {
const queuedIdx = this._queuedItems.findIndex(i => i.id === item.id); if (typeof item.id === "number") {
if (queuedIdx === -1) {
logs.delete(item.id); logs.delete(item.id);
} else { } else {
// assume the (non-persisted) object in each array will be the same
const queuedIdx = this._queuedItems.indexOf(item);
if (queuedIdx === -1) {
this._queuedItems.splice(queuedIdx, 1); this._queuedItems.splice(queuedIdx, 1);
} }
} }
}
await txnAsPromise(txn); await txnAsPromise(txn);
} finally { } finally {
try { try {
@ -153,33 +172,37 @@ export class IDBLogger extends BaseLogger {
} }
} }
class IDBLogExport { class IDBLogExport implements ILogExport {
constructor(items, logger, platform) { private readonly _items: QueuedItem[];
private readonly _logger: IDBLogger;
private readonly _platform: Platform;
constructor(items: QueuedItem[], logger: IDBLogger, platform: Platform) {
this._items = items; this._items = items;
this._logger = logger; this._logger = logger;
this._platform = platform; this._platform = platform;
} }
get count() { get count(): number {
return this._items.length; return this._items.length;
} }
/** /**
* @return {Promise} * @return {Promise}
*/ */
removeFromStore() { removeFromStore(): Promise<void> {
return this._logger._removeItems(this._items); return this._logger._removeItems(this._items);
} }
asBlob() { asBlob(): BlobHandle {
const log = { const log = {
formatVersion: 1, formatVersion: 1,
appVersion: this._platform.updateService?.version, appVersion: this._platform.updateService?.version,
items: this._items.map(i => JSON.parse(i.json)) items: this._items.map(i => JSON.parse(i.json))
}; };
const json = JSON.stringify(log); const json = JSON.stringify(log);
const buffer = this._platform.encoding.utf8.encode(json); const buffer: Uint8Array = this._platform.encoding.utf8.encode(json);
const blob = this._platform.createBlob(buffer, "application/json"); const blob: BlobHandle = this._platform.createBlob(buffer, "application/json");
return blob; return blob;
} }
} }

View file

@ -14,31 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const LogLevel = { import type {ILogItem, ISerializedItem} from "./types";
All: 1,
Debug: 2, export enum LogLevel {
Detail: 3, All = 1,
Info: 4, Debug,
Warn: 5, Detail,
Error: 6, Info,
Fatal: 7, Warn,
Off: 8, Error,
Fatal,
Off
} }
export class LogFilter { export class LogFilter {
constructor(parentFilter) { private _min?: LogLevel;
private _parentFilter?: LogFilter;
constructor(parentFilter?: LogFilter) {
this._parentFilter = parentFilter; this._parentFilter = parentFilter;
this._min = null;
} }
filter(item, children) { filter(item: ILogItem, children: ISerializedItem[] | null): boolean {
if (this._parentFilter) { if (this._parentFilter) {
if (!this._parentFilter.filter(item, children)) { if (!this._parentFilter.filter(item, children)) {
return false; return false;
} }
} }
// neither our children or us have a loglevel high enough, filter out. // neither our children or us have a loglevel high enough, filter out.
if (this._min !== null && !Array.isArray(children) && item.logLevel < this._min) { if (this._min !== undefined && !Array.isArray(children) && item.logLevel < this._min) {
return false; return false;
} else { } else {
return true; return true;
@ -46,7 +50,7 @@ export class LogFilter {
} }
/* methods to build the filter */ /* methods to build the filter */
minLevel(logLevel) { minLevel(logLevel: LogLevel): LogFilter {
this._min = logLevel; this._min = logLevel;
return this; return this;
} }

View file

@ -15,39 +15,47 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {LogLevel, LogFilter} from "./LogFilter.js"; import {LogLevel, LogFilter} from "./LogFilter";
import type {BaseLogger} from "./BaseLogger";
import type {ISerializedItem, ILogItem, LogItemValues, LabelOrValues, FilterCreator, LogCallback} from "./types";
export class LogItem { export class LogItem implements ILogItem {
constructor(labelOrValues, logLevel, filterCreator, logger) { public readonly start: number;
public logLevel: LogLevel;
public error?: Error;
public end?: number;
private _values: LogItemValues;
private _logger: BaseLogger;
private _filterCreator?: FilterCreator;
private _children?: Array<LogItem>;
constructor(labelOrValues: LabelOrValues, logLevel: LogLevel, logger: BaseLogger, filterCreator?: FilterCreator) {
this._logger = logger; this._logger = logger;
this._start = logger._now(); this.start = logger._now();
this._end = null;
// (l)abel // (l)abel
this._values = typeof labelOrValues === "string" ? {l: labelOrValues} : labelOrValues; this._values = typeof labelOrValues === "string" ? {l: labelOrValues} : labelOrValues;
this.error = null;
this.logLevel = logLevel; this.logLevel = logLevel;
this._children = null;
this._filterCreator = filterCreator; this._filterCreator = filterCreator;
} }
/** start a new root log item and run it detached mode, see BaseLogger.runDetached */ /** start a new root log item and run it detached mode, see BaseLogger.runDetached */
runDetached(labelOrValues, callback, logLevel, filterCreator) { runDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem {
return this._logger.runDetached(labelOrValues, callback, logLevel, filterCreator); return this._logger.runDetached(labelOrValues, callback, logLevel, filterCreator);
} }
/** start a new detached root log item and log a reference to it from this item */ /** start a new detached root log item and log a reference to it from this item */
wrapDetached(labelOrValues, callback, logLevel, filterCreator) { wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): void {
this.refDetached(this.runDetached(labelOrValues, callback, logLevel, filterCreator)); this.refDetached(this.runDetached(labelOrValues, callback, logLevel, filterCreator));
} }
/** logs a reference to a different log item, usually obtained from runDetached. /** logs a reference to a different log item, usually obtained from runDetached.
This is useful if the referenced operation can't be awaited. */ This is useful if the referenced operation can't be awaited. */
refDetached(logItem, logLevel = null) { refDetached(logItem: ILogItem, logLevel?: LogLevel): void {
logItem.ensureRefId(); logItem.ensureRefId();
return this.log({ref: logItem._values.refId}, logLevel); this.log({ref: logItem.values.refId}, logLevel);
} }
ensureRefId() { ensureRefId(): void {
if (!this._values.refId) { if (!this._values.refId) {
this.set("refId", this._logger._createRefId()); this.set("refId", this._logger._createRefId());
} }
@ -56,29 +64,33 @@ export class LogItem {
/** /**
* Creates a new child item and runs it in `callback`. * Creates a new child item and runs it in `callback`.
*/ */
wrap(labelOrValues, callback, logLevel = null, filterCreator = null) { wrap<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T {
const item = this.child(labelOrValues, logLevel, filterCreator); const item = this.child(labelOrValues, logLevel, filterCreator);
return item.run(callback); return item.run(callback);
} }
get duration() { get duration(): number | undefined {
if (this._end) { if (this.end) {
return this._end - this._start; return this.end - this.start;
} else { } else {
return null; return undefined;
} }
} }
durationWithoutType(type) { durationWithoutType(type: string): number | undefined {
return this.duration - this.durationOfType(type); const durationOfType = this.durationOfType(type);
if (this.duration && durationOfType) {
return this.duration - durationOfType;
}
} }
durationOfType(type) { durationOfType(type: string): number | undefined {
if (this._values.t === type) { if (this._values.t === type) {
return this.duration; return this.duration;
} else if (this._children) { } else if (this._children) {
return this._children.reduce((sum, c) => { return this._children.reduce((sum, c) => {
return sum + c.durationOfType(type); const duration = c.durationOfType(type);
return sum + (duration ?? 0);
}, 0); }, 0);
} else { } else {
return 0; return 0;
@ -91,12 +103,12 @@ export class LogItem {
* *
* Hence, the child item is not returned. * Hence, the child item is not returned.
*/ */
log(labelOrValues, logLevel = null) { log(labelOrValues: LabelOrValues, logLevel?: LogLevel): void {
const item = this.child(labelOrValues, logLevel, null); const item = this.child(labelOrValues, logLevel);
item._end = item._start; item.end = item.start;
} }
set(key, value) { set(key: string | object, value?: unknown): void {
if(typeof key === "object") { if(typeof key === "object") {
const values = key; const values = key;
Object.assign(this._values, values); Object.assign(this._values, values);
@ -105,7 +117,7 @@ export class LogItem {
} }
} }
serialize(filter, parentStartTime = null, forced) { serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined {
if (this._filterCreator) { if (this._filterCreator) {
try { try {
filter = this._filterCreator(new LogFilter(filter), this); filter = this._filterCreator(new LogFilter(filter), this);
@ -113,10 +125,10 @@ export class LogItem {
console.error("Error creating log filter", err); console.error("Error creating log filter", err);
} }
} }
let children; let children: Array<ISerializedItem> | null = null;
if (this._children !== null) { if (this._children) {
children = this._children.reduce((array, c) => { children = this._children.reduce((array: Array<ISerializedItem>, c) => {
const s = c.serialize(filter, this._start, false); const s = c.serialize(filter, this.start, false);
if (s) { if (s) {
if (array === null) { if (array === null) {
array = []; array = [];
@ -127,12 +139,12 @@ export class LogItem {
}, null); }, null);
} }
if (filter && !filter.filter(this, children)) { if (filter && !filter.filter(this, children)) {
return null; return;
} }
// in (v)alues, (l)abel and (t)ype are also reserved. // in (v)alues, (l)abel and (t)ype are also reserved.
const item = { const item: ISerializedItem = {
// (s)tart // (s)tart
s: parentStartTime === null ? this._start : this._start - parentStartTime, s: typeof parentStartTime === "number" ? this.start - parentStartTime : this.start,
// (d)uration // (d)uration
d: this.duration, d: this.duration,
// (v)alues // (v)alues
@ -171,20 +183,19 @@ export class LogItem {
* @param {Function} callback [description] * @param {Function} callback [description]
* @return {[type]} [description] * @return {[type]} [description]
*/ */
run(callback) { run<T>(callback: LogCallback<T>): T {
if (this._end !== null) { if (this.end !== undefined) {
console.trace("log item is finished, additional logs will likely not be recorded"); console.trace("log item is finished, additional logs will likely not be recorded");
} }
let result;
try { try {
result = callback(this); const result = callback(this);
if (result instanceof Promise) { if (result instanceof Promise) {
return result.then(promiseResult => { return result.then(promiseResult => {
this.finish(); this.finish();
return promiseResult; return promiseResult;
}, err => { }, err => {
throw this.catch(err); throw this.catch(err);
}); }) as unknown as T;
} else { } else {
this.finish(); this.finish();
return result; return result;
@ -198,45 +209,53 @@ export class LogItem {
* finished the item, recording the end time. After finishing, an item can't be modified anymore as it will be persisted. * finished the item, recording the end time. After finishing, an item can't be modified anymore as it will be persisted.
* @internal shouldn't typically be called by hand. allows to force finish if a promise is still running when closing the app * @internal shouldn't typically be called by hand. allows to force finish if a promise is still running when closing the app
*/ */
finish() { finish(): void {
if (this._end === null) { if (this.end === undefined) {
if (this._children !== null) { if (this._children) {
for(const c of this._children) { for(const c of this._children) {
c.finish(); c.finish();
} }
} }
this._end = this._logger._now(); this.end = this._logger._now();
} }
} }
// expose log level without needing import everywhere // expose log level without needing import everywhere
get level() { get level(): typeof LogLevel {
return LogLevel; return LogLevel;
} }
catch(err) { catch(err: Error): Error {
this.error = err; this.error = err;
this.logLevel = LogLevel.Error; this.logLevel = LogLevel.Error;
this.finish(); this.finish();
return err; return err;
} }
child(labelOrValues, logLevel, filterCreator) { child(labelOrValues: LabelOrValues, logLevel?: LogLevel, filterCreator?: FilterCreator): LogItem {
if (this._end !== null) { if (this.end) {
console.trace("log item is finished, additional logs will likely not be recorded"); console.trace("log item is finished, additional logs will likely not be recorded");
} }
if (!logLevel) { if (!logLevel) {
logLevel = this.logLevel || LogLevel.Info; logLevel = this.logLevel || LogLevel.Info;
} }
const item = new LogItem(labelOrValues, logLevel, filterCreator, this._logger); const item = new LogItem(labelOrValues, logLevel, this._logger, filterCreator);
if (this._children === null) { if (!this._children) {
this._children = []; this._children = [];
} }
this._children.push(item); this._children.push(item);
return item; return item;
} }
get logger() { get logger(): BaseLogger {
return this._logger; return this._logger;
} }
get values(): LogItemValues {
return this._values;
}
get children(): Array<LogItem> | undefined {
return this._children;
}
} }

View file

@ -1,99 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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 {LogLevel} from "./LogFilter.js";
function noop () {}
export class NullLogger {
constructor() {
this.item = new NullLogItem(this);
}
log() {}
run(_, callback) {
return callback(this.item);
}
wrapOrRun(item, _, callback) {
if (item) {
return item.wrap(null, callback);
} else {
return this.run(null, callback);
}
}
runDetached(_, callback) {
new Promise(r => r(callback(this.item))).then(noop, noop);
}
async export() {
return null;
}
get level() {
return LogLevel;
}
}
export class NullLogItem {
constructor(logger) {
this.logger = logger;
}
wrap(_, callback) {
return callback(this);
}
log() {}
set() {}
runDetached(_, callback) {
new Promise(r => r(callback(this))).then(noop, noop);
}
wrapDetached(_, callback) {
return this.refDetached(null, callback);
}
run(callback) {
return callback(this);
}
refDetached() {}
ensureRefId() {}
get level() {
return LogLevel;
}
get duration() {
return 0;
}
catch(err) {
return err;
}
child() {
return this;
}
finish() {}
}
export const Instance = new NullLogger();

106
src/logging/NullLogger.ts Normal file
View file

@ -0,0 +1,106 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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 {LogLevel} from "./LogFilter";
import type {ILogger, ILogExport, ILogItem, LabelOrValues, LogCallback, LogItemValues} from "./types";
function noop (): void {}
export class NullLogger implements ILogger {
public readonly item: ILogItem = new NullLogItem(this);
log(): void {}
run<T>(_, callback: LogCallback<T>): T {
return callback(this.item);
}
wrapOrRun<T>(item: ILogItem | undefined, _, callback: LogCallback<T>): T {
if (item) {
return item.wrap(_, callback);
} else {
return this.run(_, callback);
}
}
runDetached(_, callback): ILogItem {
new Promise(r => r(callback(this.item))).then(noop, noop);
return this.item;
}
async export(): Promise<ILogExport | undefined> {
return undefined;
}
get level(): typeof LogLevel {
return LogLevel;
}
}
export class NullLogItem implements ILogItem {
public readonly logger: NullLogger;
public readonly logLevel: LogLevel;
public children?: Array<ILogItem>;
public values: LogItemValues;
public error?: Error;
constructor(logger: NullLogger) {
this.logger = logger;
}
wrap<T>(_: LabelOrValues, callback: LogCallback<T>): T {
return callback(this);
}
log(): void {}
set(): void {}
runDetached(_: LabelOrValues, callback: LogCallback<unknown>): ILogItem {
new Promise(r => r(callback(this))).then(noop, noop);
return this;
}
wrapDetached(_: LabelOrValues, _callback: LogCallback<unknown>): void {
return this.refDetached();
}
refDetached(): void {}
ensureRefId(): void {}
get level(): typeof LogLevel {
return LogLevel;
}
get duration(): 0 {
return 0;
}
catch(err: Error): Error {
return err;
}
child(): ILogItem {
return this;
}
finish(): void {}
serialize(): undefined {
return undefined;
}
}
export const Instance = new NullLogger();

82
src/logging/types.ts Normal file
View file

@ -0,0 +1,82 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2021 The Matrix.org Foundation C.I.C.
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 {LogLevel, LogFilter} from "./LogFilter";
import type {BaseLogger} from "./BaseLogger";
import type {BlobHandle} from "../platform/web/dom/BlobHandle.js";
export interface ISerializedItem {
s: number;
d?: number;
v: LogItemValues;
l: LogLevel;
e?: {
stack?: string;
name: string;
message: string;
};
f?: boolean;
c?: Array<ISerializedItem>;
};
export interface ILogItem {
logLevel: LogLevel;
error?: Error;
readonly logger: ILogger;
readonly level: typeof LogLevel;
readonly end?: number;
readonly start?: number;
readonly values: LogItemValues;
wrap<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T;
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): void;
set(key: string | object, value: unknown): void;
runDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): void;
refDetached(logItem: ILogItem, logLevel?: LogLevel): void;
ensureRefId(): void;
catch(err: Error): Error;
serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined;
}
export interface ILogger {
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): void;
wrapOrRun<T>(item: ILogItem | undefined, labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T;
runDetached<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
run<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T;
export(): Promise<ILogExport | undefined>;
get level(): typeof LogLevel;
}
export interface ILogExport {
get count(): number;
removeFromStore(): Promise<void>;
asBlob(): BlobHandle;
}
export type LogItemValues = {
l?: string;
t?: string;
id?: unknown;
status?: string | number;
refId?: number;
ref?: number;
[key: string]: any
}
export type LabelOrValues = string | LogItemValues;
export type FilterCreator = ((filter: LogFilter, item: ILogItem) => LogFilter);
export type LogCallback<T> = (item: ILogItem) => T;

View file

@ -1,16 +0,0 @@
// these are helper functions if you can't assume you always have a log item (e.g. some code paths call with one set, others don't)
// if you know you always have a log item, better to use the methods on the log item than these utility functions.
import {Instance as NullLoggerInstance} from "./NullLogger.js";
export function wrapOrRunNullLogger(logItem, labelOrValues, callback, logLevel = null, filterCreator = null) {
if (logItem) {
return logItem.wrap(logItem, labelOrValues, callback, logLevel, filterCreator);
} else {
return NullLoggerInstance.run(null, callback);
}
}
export function ensureLogItem(logItem) {
return logItem || NullLoggerInstance.item;
}

18
src/logging/utils.ts Normal file
View file

@ -0,0 +1,18 @@
// these are helper functions if you can't assume you always have a log item (e.g. some code paths call with one set, others don't)
// if you know you always have a log item, better to use the methods on the log item than these utility functions.
import {Instance as NullLoggerInstance} from "./NullLogger";
import type {FilterCreator, ILogItem, LabelOrValues, LogCallback} from "./types";
import {LogLevel} from "./LogFilter";
export function wrapOrRunNullLogger<T>(logItem: ILogItem | undefined, labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T | Promise<T> {
if (logItem) {
return logItem.wrap(labelOrValues, callback, logLevel, filterCreator);
} else {
return NullLoggerInstance.run(null, callback);
}
}
export function ensureLogItem(logItem: ILogItem): ILogItem {
return logItem || NullLoggerInstance.item;
}

View file

@ -26,7 +26,7 @@ import type {OlmWorker} from "../OlmWorker";
import type {Transaction} from "../../storage/idb/Transaction"; import type {Transaction} from "../../storage/idb/Transaction";
import type {TimelineEvent} from "../../storage/types"; import type {TimelineEvent} from "../../storage/types";
import type {DecryptionResult} from "../DecryptionResult"; import type {DecryptionResult} from "../DecryptionResult";
import type {LogItem} from "../../../logging/LogItem"; import type {ILogItem} from "../../../logging/types";
export class Decryption { export class Decryption {
private keyLoader: KeyLoader; private keyLoader: KeyLoader;
@ -136,7 +136,7 @@ export class Decryption {
* Extracts room keys from decrypted device messages. * Extracts room keys from decrypted device messages.
* The key won't be persisted yet, you need to call RoomKey.write for that. * The key won't be persisted yet, you need to call RoomKey.write for that.
*/ */
roomKeysFromDeviceMessages(decryptionResults: DecryptionResult[], log: LogItem): IncomingRoomKey[] { roomKeysFromDeviceMessages(decryptionResults: DecryptionResult[], log: ILogItem): IncomingRoomKey[] {
const keys: IncomingRoomKey[] = []; const keys: IncomingRoomKey[] = [];
for (const dr of decryptionResults) { for (const dr of decryptionResults) {
if (dr.event?.type !== "m.room_key" || dr.event.content?.algorithm !== MEGOLM_ALGORITHM) { if (dr.event?.type !== "m.room_key" || dr.event.content?.algorithm !== MEGOLM_ALGORITHM) {

View file

@ -27,7 +27,7 @@ import {Heroes} from "./members/Heroes.js";
import {EventEntry} from "./timeline/entries/EventEntry.js"; import {EventEntry} from "./timeline/entries/EventEntry.js";
import {ObservedEventMap} from "./ObservedEventMap.js"; import {ObservedEventMap} from "./ObservedEventMap.js";
import {DecryptionSource} from "../e2ee/common.js"; import {DecryptionSource} from "../e2ee/common.js";
import {ensureLogItem} from "../../logging/utils.js"; import {ensureLogItem} from "../../logging/utils";
import {PowerLevels} from "./PowerLevels.js"; import {PowerLevels} from "./PowerLevels.js";
import {RetainedObservableValue} from "../../observable/ObservableValue"; import {RetainedObservableValue} from "../../observable/ObservableValue";

View file

@ -244,7 +244,7 @@ export class Invite extends EventEmitter {
} }
} }
import {NullLogItem} from "../../logging/NullLogger.js"; import {NullLogItem} from "../../logging/NullLogger";
import {Clock as MockClock} from "../../mocks/Clock.js"; import {Clock as MockClock} from "../../mocks/Clock.js";
import {default as roomInviteFixture} from "../../fixtures/matrix/invites/room.js"; import {default as roomInviteFixture} from "../../fixtures/matrix/invites/room.js";
import {default as dmInviteFixture} from "../../fixtures/matrix/invites/dm.js"; import {default as dmInviteFixture} from "../../fixtures/matrix/invites/dm.js";

View file

@ -353,7 +353,7 @@ export class SendQueue {
import {HomeServer as MockHomeServer} from "../../../mocks/HomeServer.js"; import {HomeServer as MockHomeServer} from "../../../mocks/HomeServer.js";
import {createMockStorage} from "../../../mocks/Storage"; import {createMockStorage} from "../../../mocks/Storage";
import {ListObserver} from "../../../mocks/ListObserver.js"; import {ListObserver} from "../../../mocks/ListObserver.js";
import {NullLogger, NullLogItem} from "../../../logging/NullLogger.js"; import {NullLogger, NullLogItem} from "../../../logging/NullLogger";
import {createEvent, withTextBody, withTxnId} from "../../../mocks/event.js"; import {createEvent, withTextBody, withTxnId} from "../../../mocks/event.js";
import {poll} from "../../../mocks/poll.js"; import {poll} from "../../../mocks/poll.js";
import {createAnnotation} from "../timeline/relations.js"; import {createAnnotation} from "../timeline/relations.js";

View file

@ -346,7 +346,7 @@ import {Clock as MockClock} from "../../../mocks/Clock.js";
import {createMockStorage} from "../../../mocks/Storage"; import {createMockStorage} from "../../../mocks/Storage";
import {ListObserver} from "../../../mocks/ListObserver.js"; import {ListObserver} from "../../../mocks/ListObserver.js";
import {createEvent, withTextBody, withContent, withSender} from "../../../mocks/event.js"; import {createEvent, withTextBody, withContent, withSender} from "../../../mocks/event.js";
import {NullLogItem} from "../../../logging/NullLogger.js"; import {NullLogItem} from "../../../logging/NullLogger";
import {EventEntry} from "./entries/EventEntry.js"; import {EventEntry} from "./entries/EventEntry.js";
import {User} from "../../User.js"; import {User} from "../../User.js";
import {PendingEvent} from "../sending/PendingEvent.js"; import {PendingEvent} from "../sending/PendingEvent.js";

View file

@ -205,7 +205,7 @@ import {FragmentIdComparer} from "../FragmentIdComparer.js";
import {RelationWriter} from "./RelationWriter.js"; import {RelationWriter} from "./RelationWriter.js";
import {createMockStorage} from "../../../../mocks/Storage"; import {createMockStorage} from "../../../../mocks/Storage";
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
import {NullLogItem} from "../../../../logging/NullLogger.js"; import {NullLogItem} from "../../../../logging/NullLogger";
import {TimelineMock, eventIds, eventId} from "../../../../mocks/TimelineMock.ts"; import {TimelineMock, eventIds, eventId} from "../../../../mocks/TimelineMock.ts";
import {SyncWriter} from "./SyncWriter.js"; import {SyncWriter} from "./SyncWriter.js";
import {MemberWriter} from "./MemberWriter.js"; import {MemberWriter} from "./MemberWriter.js";

View file

@ -257,7 +257,7 @@ import {createMockStorage} from "../../../../mocks/Storage";
import {createEvent, withTextBody, withRedacts, withContent} from "../../../../mocks/event.js"; import {createEvent, withTextBody, withRedacts, withContent} from "../../../../mocks/event.js";
import {createAnnotation} from "../relations.js"; import {createAnnotation} from "../relations.js";
import {FragmentIdComparer} from "../FragmentIdComparer.js"; import {FragmentIdComparer} from "../FragmentIdComparer.js";
import {NullLogItem} from "../../../../logging/NullLogger.js"; import {NullLogItem} from "../../../../logging/NullLogger";
export function tests() { export function tests() {
const fragmentIdComparer = new FragmentIdComparer([]); const fragmentIdComparer = new FragmentIdComparer([]);

View file

@ -258,7 +258,7 @@ export class SyncWriter {
import {createMockStorage} from "../../../../mocks/Storage"; import {createMockStorage} from "../../../../mocks/Storage";
import {createEvent, withTextBody} from "../../../../mocks/event.js"; import {createEvent, withTextBody} from "../../../../mocks/event.js";
import {Instance as nullLogger} from "../../../../logging/NullLogger.js"; import {Instance as nullLogger} from "../../../../logging/NullLogger";
export function tests() { export function tests() {
const roomId = "!abc:hs.tld"; const roomId = "!abc:hs.tld";
return { return {

View file

@ -16,7 +16,7 @@ limitations under the License.
import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils"; import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils";
import {StorageError} from "../common"; import {StorageError} from "../common";
import {LogItem} from "../../../logging/LogItem.js"; import {ILogItem} from "../../../logging/types";
import {IDBKey} from "./Transaction"; import {IDBKey} from "./Transaction";
// this is the part of the Transaction class API that is used here and in the Store subclass, // this is the part of the Transaction class API that is used here and in the Store subclass,
@ -25,7 +25,7 @@ export interface ITransaction {
idbFactory: IDBFactory; idbFactory: IDBFactory;
IDBKeyRange: typeof IDBKeyRange; IDBKeyRange: typeof IDBKeyRange;
databaseName: string; databaseName: string;
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined); addWriteError(error: StorageError, refItem: ILogItem | undefined, operationName: string, keys: IDBKey[] | undefined);
} }
type Reducer<A,B> = (acc: B, val: A) => B type Reducer<A,B> = (acc: B, val: A) => B
@ -277,7 +277,7 @@ export function tests() {
class MockTransaction extends MockIDBImpl { class MockTransaction extends MockIDBImpl {
get databaseName(): string { return "mockdb"; } get databaseName(): string { return "mockdb"; }
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined) {} addWriteError(error: StorageError, refItem: ILogItem | undefined, operationName: string, keys: IDBKey[] | undefined) {}
} }
interface TestEntry { interface TestEntry {

View file

@ -18,7 +18,7 @@ import {IDOMStorage} from "./types";
import {Transaction} from "./Transaction"; import {Transaction} from "./Transaction";
import { STORE_NAMES, StoreNames, StorageError } from "../common"; import { STORE_NAMES, StoreNames, StorageError } from "../common";
import { reqAsPromise } from "./utils"; import { reqAsPromise } from "./utils";
import { BaseLogger } from "../../../logging/BaseLogger.js"; import { ILogger } from "../../../logging/types";
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
@ -26,13 +26,13 @@ export class Storage {
private _db: IDBDatabase; private _db: IDBDatabase;
private _hasWebkitEarlyCloseTxnBug: boolean; private _hasWebkitEarlyCloseTxnBug: boolean;
readonly logger: BaseLogger; readonly logger: ILogger;
readonly idbFactory: IDBFactory readonly idbFactory: IDBFactory
readonly IDBKeyRange: typeof IDBKeyRange; readonly IDBKeyRange: typeof IDBKeyRange;
readonly storeNames: typeof StoreNames; readonly storeNames: typeof StoreNames;
readonly localStorage: IDOMStorage; readonly localStorage: IDOMStorage;
constructor(idbDatabase: IDBDatabase, idbFactory: IDBFactory, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean, localStorage: IDOMStorage, logger: BaseLogger) { constructor(idbDatabase: IDBDatabase, idbFactory: IDBFactory, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean, localStorage: IDOMStorage, logger: ILogger) {
this._db = idbDatabase; this._db = idbDatabase;
this.idbFactory = idbFactory; this.idbFactory = idbFactory;
this.IDBKeyRange = _IDBKeyRange; this.IDBKeyRange = _IDBKeyRange;

View file

@ -20,11 +20,10 @@ import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession, Export } from "./export"; import { exportSession, importSession, Export } from "./export";
import { schema } from "./schema"; import { schema } from "./schema";
import { detectWebkitEarlyCloseTxnBug } from "./quirks"; import { detectWebkitEarlyCloseTxnBug } from "./quirks";
import { BaseLogger } from "../../../logging/BaseLogger.js"; import { ILogItem } from "../../../logging/types";
import { LogItem } from "../../../logging/LogItem.js";
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: LogItem) { const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: ILogItem) {
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, localStorage, log); const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, localStorage, log);
return openDatabase(sessionName(sessionId), create, schema.length, idbFactory); return openDatabase(sessionName(sessionId), create, schema.length, idbFactory);
} }
@ -63,7 +62,7 @@ export class StorageFactory {
this._localStorage = localStorage; this._localStorage = localStorage;
} }
async create(sessionId: string, log: LogItem): Promise<Storage> { async create(sessionId: string, log: ILogItem): Promise<Storage> {
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
requestPersistedStorage().then(persisted => { requestPersistedStorage().then(persisted => {
// Firefox lies here though, and returns true even if the user denied the request // Firefox lies here though, and returns true even if the user denied the request
@ -83,23 +82,25 @@ export class StorageFactory {
return reqAsPromise(req); return reqAsPromise(req);
} }
async export(sessionId: string, log: LogItem): Promise<Export> { async export(sessionId: string, log: ILogItem): Promise<Export> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log);
return await exportSession(db); return await exportSession(db);
} }
async import(sessionId: string, data: Export, log: LogItem): Promise<void> { async import(sessionId: string, data: Export, log: ILogItem): Promise<void> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log);
return await importSession(db, data); return await importSession(db, data);
} }
} }
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, localStorage: IDOMStorage, log: LogItem): Promise<void> { async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, localStorage: IDOMStorage, log: ILogItem): Promise<void> {
const startIdx = oldVersion || 0; const startIdx = oldVersion || 0;
return log.wrap({l: "storage migration", oldVersion, version}, async log => { return log.wrap(
for(let i = startIdx; i < version; ++i) { { l: "storage migration", oldVersion, version },
async (log) => {
for (let i = startIdx; i < version; ++i) {
const migrationFunc = schema[i]; const migrationFunc = schema[i];
await log.wrap(`v${i + 1}`, log => migrationFunc(db, txn, localStorage, log)); await log.wrap(`v${i + 1}`, (log) => migrationFunc(db, txn, localStorage, log));
} }
}); });
} }

View file

@ -18,7 +18,7 @@ import {QueryTarget, IDBQuery, ITransaction} from "./QueryTarget";
import {IDBRequestError, IDBRequestAttemptError} from "./error"; import {IDBRequestError, IDBRequestAttemptError} from "./error";
import {reqAsPromise} from "./utils"; import {reqAsPromise} from "./utils";
import {Transaction, IDBKey} from "./Transaction"; import {Transaction, IDBKey} from "./Transaction";
import {LogItem} from "../../../logging/LogItem.js"; import {ILogItem} from "../../../logging/types";
const LOG_REQUESTS = false; const LOG_REQUESTS = false;
@ -145,7 +145,7 @@ export class Store<T> extends QueryTarget<T> {
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)), this._transaction); return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)), this._transaction);
} }
put(value: T, log?: LogItem): void { put(value: T, log?: ILogItem): void {
// If this request fails, the error will bubble up to the transaction and abort it, // If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this // which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it. // request and await it.
@ -160,13 +160,13 @@ export class Store<T> extends QueryTarget<T> {
this._prepareErrorLog(request, log, "put", undefined, value); this._prepareErrorLog(request, log, "put", undefined, value);
} }
add(value: T, log?: LogItem): void { add(value: T, log?: ILogItem): void {
// ok to not monitor result of request, see comment in `put`. // ok to not monitor result of request, see comment in `put`.
const request = this._idbStore.add(value); const request = this._idbStore.add(value);
this._prepareErrorLog(request, log, "add", undefined, value); this._prepareErrorLog(request, log, "add", undefined, value);
} }
async tryAdd(value: T, log: LogItem): Promise<boolean> { async tryAdd(value: T, log: ILogItem): Promise<boolean> {
try { try {
await reqAsPromise(this._idbStore.add(value)); await reqAsPromise(this._idbStore.add(value));
return true; return true;
@ -181,13 +181,13 @@ export class Store<T> extends QueryTarget<T> {
} }
} }
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange, log?: LogItem): void { delete(keyOrKeyRange: IDBValidKey | IDBKeyRange, log?: ILogItem): void {
// ok to not monitor result of request, see comment in `put`. // ok to not monitor result of request, see comment in `put`.
const request = this._idbStore.delete(keyOrKeyRange); const request = this._idbStore.delete(keyOrKeyRange);
this._prepareErrorLog(request, log, "delete", keyOrKeyRange, undefined); this._prepareErrorLog(request, log, "delete", keyOrKeyRange, undefined);
} }
private _prepareErrorLog(request: IDBRequest, log: LogItem | undefined, operationName: string, key: IDBKey | undefined, value: T | undefined) { private _prepareErrorLog(request: IDBRequest, log: ILogItem | undefined, operationName: string, key: IDBKey | undefined, value: T | undefined) {
if (log) { if (log) {
log.ensureRefId(); log.ensureRefId();
} }

View file

@ -36,15 +36,14 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
import {OperationStore} from "./stores/OperationStore"; import {OperationStore} from "./stores/OperationStore";
import {AccountDataStore} from "./stores/AccountDataStore"; import {AccountDataStore} from "./stores/AccountDataStore";
import {LogItem} from "../../../logging/LogItem.js"; import type {ILogger, ILogItem} from "../../../logging/types";
import {BaseLogger} from "../../../logging/BaseLogger.js";
export type IDBKey = IDBValidKey | IDBKeyRange; export type IDBKey = IDBValidKey | IDBKeyRange;
class WriteErrorInfo { class WriteErrorInfo {
constructor( constructor(
public readonly error: StorageError, public readonly error: StorageError,
public readonly refItem: LogItem | undefined, public readonly refItem: ILogItem | undefined,
public readonly operationName: string, public readonly operationName: string,
public readonly keys: IDBKey[] | undefined, public readonly keys: IDBKey[] | undefined,
) {} ) {}
@ -77,7 +76,7 @@ export class Transaction {
return this._storage.databaseName; return this._storage.databaseName;
} }
get logger(): BaseLogger { get logger(): ILogger {
return this._storage.logger; return this._storage.logger;
} }
@ -169,7 +168,7 @@ export class Transaction {
return this._store(StoreNames.accountData, idbStore => new AccountDataStore(idbStore)); return this._store(StoreNames.accountData, idbStore => new AccountDataStore(idbStore));
} }
async complete(log?: LogItem): Promise<void> { async complete(log?: ILogItem): Promise<void> {
try { try {
await txnAsPromise(this._txn); await txnAsPromise(this._txn);
} catch (err) { } catch (err) {
@ -190,7 +189,7 @@ export class Transaction {
return error; return error;
} }
abort(log?: LogItem): void { abort(log?: ILogItem): void {
// TODO: should we wrap the exception in a StorageError? // TODO: should we wrap the exception in a StorageError?
try { try {
this._txn.abort(); this._txn.abort();
@ -202,14 +201,14 @@ export class Transaction {
} }
} }
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined) { addWriteError(error: StorageError, refItem: ILogItem | undefined, operationName: string, keys: IDBKey[] | undefined) {
// don't log subsequent `AbortError`s // don't log subsequent `AbortError`s
if (error.errcode !== "AbortError" || this._writeErrors.length === 0) { if (error.errcode !== "AbortError" || this._writeErrors.length === 0) {
this._writeErrors.push(new WriteErrorInfo(error, refItem, operationName, keys)); this._writeErrors.push(new WriteErrorInfo(error, refItem, operationName, keys));
} }
} }
private _logWriteErrors(parentItem: LogItem | undefined) { private _logWriteErrors(parentItem: ILogItem | undefined) {
const callback = errorGroupItem => { const callback = errorGroupItem => {
// we don't have context when there is no parentItem, so at least log stores // we don't have context when there is no parentItem, so at least log stores
if (!parentItem) { if (!parentItem) {

View file

@ -11,10 +11,10 @@ import {SessionStore} from "./stores/SessionStore";
import {Store} from "./Store"; import {Store} from "./Store";
import {encodeScopeTypeKey} from "./stores/OperationStore"; import {encodeScopeTypeKey} from "./stores/OperationStore";
import {MAX_UNICODE} from "./stores/common"; import {MAX_UNICODE} from "./stores/common";
import {LogItem} from "../../../logging/LogItem.js"; import {ILogItem} from "../../../logging/types";
export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) => Promise<void> | void; export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) => Promise<void> | void;
// FUNCTIONS SHOULD ONLY BE APPENDED!! // FUNCTIONS SHOULD ONLY BE APPENDED!!
// the index in the array is the database version // the index in the array is the database version
export const schema: MigrationFunc[] = [ export const schema: MigrationFunc[] = [
@ -166,7 +166,7 @@ function createTimelineRelationsStore(db: IDBDatabase) : void {
} }
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470) //v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
async function fixMissingRoomsInUserIdentities(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) { async function fixMissingRoomsInUserIdentities(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) {
const roomSummaryStore = txn.objectStore("roomSummary"); const roomSummaryStore = txn.objectStore("roomSummary");
const trackedRoomIds: string[] = []; const trackedRoomIds: string[] = [];
await iterateCursor<SummaryData>(roomSummaryStore.openCursor(), roomSummary => { await iterateCursor<SummaryData>(roomSummaryStore.openCursor(), roomSummary => {
@ -220,7 +220,7 @@ async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) {
} }
} }
// v13 // v13
async function backupAndRestoreE2EEAccountToLocalStorage(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) { async function backupAndRestoreE2EEAccountToLocalStorage(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) {
const session = txn.objectStore("session"); const session = txn.objectStore("session");
// the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction), // the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction),
// the only thing we should need here is the databaseName though, so we mock it out. // the only thing we should need here is the databaseName though, so we mock it out.

View file

@ -16,8 +16,8 @@ limitations under the License.
import {Store} from "../Store"; import {Store} from "../Store";
import {IDOMStorage} from "../types"; import {IDOMStorage} from "../types";
import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common.js";
import {LogItem} from "../../../../logging/LogItem.js";
import {parse, stringify} from "../../../../utils/typedJSON"; import {parse, stringify} from "../../../../utils/typedJSON";
import type {ILogItem} from "../../../../logging/types";
export interface SessionEntry { export interface SessionEntry {
key: string; key: string;
@ -64,7 +64,7 @@ export class SessionStore {
}); });
} }
async tryRestoreE2EEIdentityFromLocalStorage(log: LogItem): Promise<boolean> { async tryRestoreE2EEIdentityFromLocalStorage(log: ILogItem): Promise<boolean> {
let success = false; let success = false;
const lsPrefix = this._localStorageKeyPrefix; const lsPrefix = this._localStorageKeyPrefix;
const prefix = lsPrefix + SESSION_E2EE_KEY_PREFIX; const prefix = lsPrefix + SESSION_E2EE_KEY_PREFIX;

View file

@ -20,7 +20,7 @@ import { encodeUint32, decodeUint32 } from "../utils";
import {KeyLimits} from "../../common"; import {KeyLimits} from "../../common";
import {Store} from "../Store"; import {Store} from "../Store";
import {TimelineEvent, StateEvent} from "../../types"; import {TimelineEvent, StateEvent} from "../../types";
import {LogItem} from "../../../../logging/LogItem.js"; import {ILogItem} from "../../../../logging/types";
interface Annotation { interface Annotation {
count: number; count: number;
@ -286,7 +286,7 @@ export class TimelineEventStore {
* *
* Returns if the event was not yet known and the entry was written. * Returns if the event was not yet known and the entry was written.
*/ */
tryInsert(entry: TimelineEventEntry, log: LogItem): Promise<boolean> { tryInsert(entry: TimelineEventEntry, log: ILogItem): Promise<boolean> {
(entry as TimelineEventStorageEntry).key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex); (entry as TimelineEventStorageEntry).key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
(entry as TimelineEventStorageEntry).eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id); (entry as TimelineEventStorageEntry).eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
return this._timelineStore.tryAdd(entry as TimelineEventStorageEntry, log); return this._timelineStore.tryAdd(entry as TimelineEventStorageEntry, log);
@ -320,7 +320,7 @@ export class TimelineEventStore {
import {createMockStorage} from "../../../../mocks/Storage"; import {createMockStorage} from "../../../../mocks/Storage";
import {createEvent, withTextBody} from "../../../../mocks/event.js"; import {createEvent, withTextBody} from "../../../../mocks/event.js";
import {createEventEntry} from "../../../room/timeline/persistence/common.js"; import {createEventEntry} from "../../../room/timeline/persistence/common.js";
import {Instance as logItem} from "../../../../logging/NullLogger.js"; import {Instance as nullLogger} from "../../../../logging/NullLogger";
export function tests() { export function tests() {
@ -368,7 +368,7 @@ export function tests() {
let eventKey = EventKey.defaultFragmentKey(109); let eventKey = EventKey.defaultFragmentKey(109);
for (const insertedId of insertedIds) { for (const insertedId of insertedIds) {
const entry = createEventEntry(eventKey.nextKey(), roomId, createEventWithId(insertedId)); const entry = createEventEntry(eventKey.nextKey(), roomId, createEventWithId(insertedId));
assert(await txn.timelineEvents.tryInsert(entry, logItem)); assert(await txn.timelineEvents.tryInsert(entry, nullLogger.item));
eventKey = eventKey.nextKey(); eventKey = eventKey.nextKey();
} }
const eventKeyMap = await txn.timelineEvents.getEventKeysForIds(roomId, checkedIds); const eventKeyMap = await txn.timelineEvents.getEventKeysForIds(roomId, checkedIds);

View file

@ -18,7 +18,7 @@ import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
import {StorageFactory} from "../matrix/storage/idb/StorageFactory"; import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
import {IDOMStorage} from "../matrix/storage/idb/types"; import {IDOMStorage} from "../matrix/storage/idb/types";
import {Storage} from "../matrix/storage/idb/Storage"; import {Storage} from "../matrix/storage/idb/Storage";
import {Instance as nullLogger} from "../logging/NullLogger.js"; import {Instance as nullLogger} from "../logging/NullLogger";
import {openDatabase, CreateObjectStore} from "../matrix/storage/idb/utils"; import {openDatabase, CreateObjectStore} from "../matrix/storage/idb/utils";
export function createMockStorage(): Promise<Storage> { export function createMockStorage(): Promise<Storage> {

View file

@ -21,8 +21,8 @@ import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionI
import {SettingsStorage} from "./dom/SettingsStorage.js"; import {SettingsStorage} from "./dom/SettingsStorage.js";
import {Encoding} from "./utils/Encoding.js"; import {Encoding} from "./utils/Encoding.js";
import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js"; import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js";
import {IDBLogger} from "../../logging/IDBLogger.js"; import {IDBLogger} from "../../logging/IDBLogger";
import {ConsoleLogger} from "../../logging/ConsoleLogger.js"; import {ConsoleLogger} from "../../logging/ConsoleLogger";
import {RootView} from "./ui/RootView.js"; import {RootView} from "./ui/RootView.js";
import {Clock} from "./dom/Clock.js"; import {Clock} from "./dom/Clock.js";
import {ServiceWorkerHandler} from "./dom/ServiceWorkerHandler.js"; import {ServiceWorkerHandler} from "./dom/ServiceWorkerHandler.js";