Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve debugging experience #1879

Merged
merged 4 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/classes/dexie/dexie-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export function dexieOpen (db: Dexie) {
return state.dbReadyPromise.then<Dexie>(() => state.dbOpenError ?
rejection (state.dbOpenError) :
db);
Debug.debug && (state.openCanceller._stackHolder = Debug.getErrorWithStack()); // Let stacks point to when open() was called rather than where new Dexie() was called.
state.isBeingOpened = true;
state.dbOpenError = null;
state.openComplete = false;
Expand Down
15 changes: 13 additions & 2 deletions src/classes/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ export class Table implements ITable<any, IndexableType> {
{
const trans: Transaction = this._tx || PSD.trans;
const tableName = this.name;
// @ts-ignore: Use Chrome's Async Stack Tagging API to allow tracing and simplify debugging for dexie users.
const task = debug && typeof console !== 'undefined' && console.createTask && console.createTask(`Dexie: ${mode === 'readonly' ? 'read' : 'write' } ${this.name}`);

function checkTableInTransaction(resolve, reject, trans: Transaction) {
if (!trans.schema[tableName])
throw new exceptions.NotFound("Table " + tableName + " not part of transaction");
return fn(trans.idbtrans, trans);
return fn(trans.idbtrans, trans) as Promise<any>;
}
// Surround all in a microtick scope.
// Reason: Browsers (modern Safari + older others)
Expand All @@ -60,11 +62,19 @@ export class Table implements ITable<any, IndexableType> {
// in native engine.
const wasRootExec = beginMicroTickScope();
try {
return trans && trans.db._novip === this.db._novip ?
let p = trans && trans.db._novip === this.db._novip ?
trans === PSD.trans ?
trans._promise(mode, checkTableInTransaction, writeLocked) :
newScope(() => trans._promise(mode, checkTableInTransaction, writeLocked), { trans: trans, transless: PSD.transless || PSD }) :
tempTransaction(this.db, mode, [this.name], checkTableInTransaction);
if (task) { // Dexie.debug = true so we trace errors
p._consoleTask = task;
p = p.catch(err => {
console.trace(err);
return rejection(err);
});
}
return p;
} finally {
if (wasRootExec) endMicroTickScope();
}
Expand All @@ -78,6 +88,7 @@ export class Table implements ITable<any, IndexableType> {
get(keyOrCrit, cb?) {
if (keyOrCrit && keyOrCrit.constructor === Object)
return this.where(keyOrCrit as { [key: string]: IndexableType }).first(cb);
if (keyOrCrit == null) return rejection(new exceptions.Type(`Invalid argument to Table.get()`));

return this._trans('readonly', (trans) => {
return this.core.get({trans, key: keyOrCrit})
Expand Down
11 changes: 0 additions & 11 deletions src/errors/errors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { derive, setProp } from '../functions/utils';
import { getErrorWithStack, prettyStack } from '../helpers/debug';

var dexieErrorNames = [
'Modify',
Expand Down Expand Up @@ -56,18 +55,11 @@ export function DexieError (name, msg) {
// 2. It doesn't give us much in this case.
// 3. It would require sub classes to call super(), which
// is not needed when deriving from Error.
this._e = getErrorWithStack();
this.name = name;
this.message = msg;
}

derive(DexieError).from(Error).extend({
stack: {
get: function() {
return this._stack ||
(this._stack = this.name + ": " + this.message + prettyStack(this._e, 2));
}
},
toString: function(){ return this.name + ": " + this.message; }
});

Expand All @@ -83,7 +75,6 @@ function getMultiErrorMessage (msg, failures) {
// Specific constructor because it contains members failures and failedKeys.
//
export function ModifyError (msg, failures, successCount, failedKeys) {
this._e = getErrorWithStack();
this.failures = failures;
this.failedKeys = failedKeys;
this.successCount = successCount;
Expand All @@ -92,7 +83,6 @@ export function ModifyError (msg, failures, successCount, failedKeys) {
derive(ModifyError).from(DexieError);

export function BulkError (msg, failures) {
this._e = getErrorWithStack();
this.name = "BulkError";
this.failures = Object.keys(failures).map(pos => failures[pos]);
this.failuresByPos = failures;
Expand Down Expand Up @@ -122,7 +112,6 @@ export var exceptions = errorList.reduce((obj,name)=>{
// 'eval-evil'.
var fullName = name + "Error";
function DexieError (msgOrInner, inner){
this._e = getErrorWithStack();
this.name = fullName;
if (!msgOrInner) {
this.message = defaultTexts[name] || fullName;
Expand Down
37 changes: 1 addition & 36 deletions src/helpers/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,11 @@ export var debug = typeof location !== 'undefined' &&

export function setDebug(value, filter) {
debug = value;
libraryFilter = filter;
}

export var libraryFilter = () => true;

export const NEEDS_THROW_FOR_STACK = !new Error("").stack;

export function getErrorWithStack() {
"use strict";
if (NEEDS_THROW_FOR_STACK) try {
// Doing something naughty in strict mode here to trigger a specific error
// that can be explicitely ignored in debugger's exception settings.
// If we'd just throw new Error() here, IE's debugger's exception settings
// will just consider it as "exception thrown by javascript code" which is
// something you wouldn't want it to ignore.
getErrorWithStack.arguments;
throw new Error(); // Fallback if above line don't throw.
} catch(e) {
return e;
}
return new Error();
}

export function prettyStack(exception, numIgnoredFrames) {
var stack = exception.stack;
if (!stack) return "";
numIgnoredFrames = (numIgnoredFrames || 0);
if (stack.indexOf(exception.name) === 0)
numIgnoredFrames += (exception.name + exception.message).split('\n').length;
return stack.split('\n')
.slice(numIgnoredFrames)
.filter(libraryFilter)
.map(frame => "\n" + frame)
.join('');
}

// TODO: Replace this in favor of a decorator instead.
export function deprecated<T> (what: string, fn: (...args)=>T) {
return function () {
console.warn(`${what} is deprecated. See https://dexie.org/docs/Deprecations. ${prettyStack(getErrorWithStack(), 1)}`);
console.warn(`${what} is deprecated. See https://dexie.org/docs/Deprecations}`);
return fn.apply(this, arguments);
} as (...args)=>T
}
109 changes: 7 additions & 102 deletions src/helpers/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { _global } from '../globals/global';
import {tryCatch, props, setProp,
getPropertyDescriptor, getArrayOf, extend, getProto} from '../functions/utils';
import {nop, callBoth, mirror} from '../functions/chaining-functions';
import {debug, prettyStack, getErrorWithStack} from './debug';
import {debug} from './debug';
import {exceptions} from '../errors';

//
Expand All @@ -20,7 +20,6 @@ import {exceptions} from '../errors';
// another strategy now that simplifies everything a lot: to always execute callbacks in a new micro-task, but have an own micro-task
// engine that is indexedDB compliant across all browsers.
// Promise class has also been optimized a lot with inspiration from bluebird - to avoid closures as much as possible.
// Also with inspiration from bluebird, asyncronic stacks in debug mode.
//
// Specific non-standard features of this Promise class:
// * Custom zone support (a.k.a. PSD) with ability to keep zones also when using native promises as well as
Expand All @@ -36,11 +35,7 @@ import {exceptions} from '../errors';
// Used in Promise constructor to emulate a private constructor.
var INTERNAL = {};

// Async stacks (long stacks) must not grow infinitely.
const
LONG_STACKS_CLIP_LIMIT = 100,
// When calling error.stack or promise.stack, limit the number of asyncronic stacks to print out.
MAX_LONG_STACKS = 20,
ZONE_ECHO_LIMIT = 100,
[resolvedNativePromise, nativePromiseProto, resolvedGlobalPromise] = typeof Promise === 'undefined' ?
[] :
Expand All @@ -61,8 +56,6 @@ const
export const NativePromise = resolvedNativePromise && resolvedNativePromise.constructor;
const patchGlobalPromise = !!resolvedGlobalPromise;

var stack_being_generated = false;

/* The default function used only for the very first promise in a promise chain.
As soon as then promise is resolved or rejected, all next tasks will be executed in micro ticks
emulated in this module. For indexedDB compatibility, this means that every method needs to
Expand Down Expand Up @@ -91,7 +84,6 @@ var isOutsideMicroTick = true, // True when NOT in a virtual microTick.
needsNewPhysicalTick = true, // True when a push to microtickQueue must also schedulePhysicalTick()
unhandledErrors = [], // Rejected promises that has occured. Used for triggering 'unhandledrejection'.
rejectingErrors = [], // Tracks if errors are being re-rejected during onRejected callback.
currentFulfiller = null,
rejectionMapper = mirror; // Remove in next major when removing error mapping of DOMErrors and DOMExceptions

export var globalPSD = {
Expand Down Expand Up @@ -124,12 +116,6 @@ export default function DexiePromise(fn) {
this._lib = false;
// Current async scope
var psd = (this._PSD = PSD);

if (debug) {
this._stackHolder = getErrorWithStack();
this._prev = null;
this._numPrev = 0; // Number of previous promises (for long stacks)
}

if (typeof fn !== 'function') {
if (fn !== INTERNAL) throw new TypeError('Not a function');
Expand Down Expand Up @@ -164,7 +150,7 @@ const thenProp = {
reject,
psd));
});
debug && linkToPreviousPromise(rv, this);
if (this._consoleTask) rv._consoleTask = this._consoleTask;
return rv;
}

Expand Down Expand Up @@ -218,21 +204,6 @@ props(DexiePromise.prototype, {
});
},

stack: {
get: function() {
if (this._stack) return this._stack;
try {
stack_being_generated = true;
var stacks = getStack (this, [], MAX_LONG_STACKS);
var stack = stacks.join("\nFrom previous: ");
if (this._state !== null) this._stack = stack; // Stack may be updated on reject.
return stack;
} finally {
stack_being_generated = false;
}
}
},

timeout: function (ms, msg) {
return ms < Infinity ?
new DexiePromise((resolve, reject) => {
Expand Down Expand Up @@ -278,7 +249,6 @@ props (DexiePromise, {
value.then(resolve, reject);
});
var rv = new DexiePromise(INTERNAL, true, value);
linkToPreviousPromise(rv, currentFulfiller);
return rv;
},

Expand Down Expand Up @@ -402,18 +372,6 @@ function handleRejection (promise, reason) {
reason = rejectionMapper(reason);
promise._state = false;
promise._value = reason;
debug && reason !== null && typeof reason === 'object' && !reason._promise && tryCatch(()=>{
var origProp = getPropertyDescriptor(reason, "stack");
reason._promise = promise;
setProp(reason, "stack", {
get: () =>
stack_being_generated ?
origProp && (origProp.get ?
origProp.get.apply(reason) :
origProp.value) :
promise.stack
});
});
// Add the failure to a list of possibly uncaught errors
addPossiblyUnhandledError(promise);
propagateAllListeners(promise);
Expand Down Expand Up @@ -460,70 +418,25 @@ function propagateToListener(promise, listener) {

function callListener (cb, promise, listener) {
try {
// Set static variable currentFulfiller to the promise that is being fullfilled,
// so that we connect the chain of promises (for long stacks support)
currentFulfiller = promise;

// Call callback and resolve our listener with it's return value.
var ret, value = promise._value;

if (promise._state) {
// cb is onResolved
ret = cb (value);
} else {
// cb is onRejected
if (rejectingErrors.length) rejectingErrors = [];
ret = cb(value);
if (rejectingErrors.indexOf(value) === -1)
markErrorAsHandled(promise); // Callback didnt do Promise.reject(err) nor reject(err) onto another promise.
if (!promise._state && rejectingErrors.length) rejectingErrors = [];
// cb is onResolved
ret = debug && promise._consoleTask ? promise._consoleTask.run(()=>cb (value)) : cb (value);
if (!promise._state && rejectingErrors.indexOf(value) === -1) {
markErrorAsHandled(promise); // Callback didnt do Promise.reject(err) nor reject(err) onto another promise.
}
listener.resolve(ret);
} catch (e) {
// Exception thrown in callback. Reject our listener.
listener.reject(e);
} finally {
// Restore env and currentFulfiller.
currentFulfiller = null;
if (--numScheduledCalls === 0) finalizePhysicalTick();
--listener.psd.ref || listener.psd.finalize();
}
}

function getStack (promise, stacks, limit) {
if (stacks.length === limit) return stacks;
var stack = "";
if (promise._state === false) {
var failure = promise._value,
errorName,
message;

if (failure != null) {
errorName = failure.name || "Error";
message = failure.message || failure;
stack = prettyStack(failure, 0);
} else {
errorName = failure; // If error is undefined or null, show that.
message = "";
}
stacks.push(errorName + (message ? ": " + message : "") + stack);
}
if (debug) {
stack = prettyStack(promise._stackHolder, 2);
if (stack && stacks.indexOf(stack) === -1) stacks.push(stack);
if (promise._prev) getStack(promise._prev, stacks, limit);
}
return stacks;
}

function linkToPreviousPromise(promise, prev) {
// Support long stacks by linking to previous completed promise.
var numPrev = prev ? prev._numPrev + 1 : 0;
if (numPrev < LONG_STACKS_CLIP_LIMIT) { // Prohibit infinite Promise loops to get an infinite long memory consuming "tail".
promise._prev = prev;
promise._numPrev = numPrev;
}
}

/* The callback to schedule with queueMicrotask().
It runs a virtual microtick and executes any callback registered in microtickQueue.
*/
Expand Down Expand Up @@ -813,14 +726,6 @@ function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) {
};
}

function getPatchedPromiseThen (origThen, zone) {
return function (onResolved, onRejected) {
return origThen.call(this,
nativeAwaitCompatibleWrap(onResolved, zone),
nativeAwaitCompatibleWrap(onRejected, zone));
};
}

/** Execute callback in global context */
export function execInGlobalContext(cb) {
if (Promise === NativePromise && task.echoes === 0) {
Expand Down
11 changes: 1 addition & 10 deletions test/dexie-unittest-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@ config.urlConfig.push(/*{
id: "dontoptimize",
label: "Dont optimize tests",
tooltip: "Always delete and recreate the DB between each test"
}, {
id: "longstacks",
label: "Long async stacks",
tooltip: "Set Dexie.debug=true, turning on long async stacks on all" +
" errors (Actually we use Dexie.debug='dexie' so that frames from" +
" dexie.js are also included)"
});

Dexie.debug = window.location.search.indexOf('longstacks') !== -1 ? 'dexie' : false;
if (window.location.search.indexOf('longstacks=tests') !== -1) Dexie.debug = true; // Don't include stuff from dexie.js.
});

var no_optimize = window.no_optimize || window.location.search.indexOf('dontoptimize') !== -1;

Expand Down
3 changes: 0 additions & 3 deletions test/run-unit-tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="babel-polyfill/polyfill.min.js"></script>
<!-- <script src="https://unpkg.com/zone.js/dist/zone.js"></script> -->
<script src="../node_modules/regenerator-runtime/runtime.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script> -->
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
<script src="../dist/dexie.js"></script>
Expand Down