Skip to content

Instantly share code, notes, and snippets.

@axefrog
Last active January 17, 2017 04:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save axefrog/3a0494b196fd596bad5ebbc47e95dcb0 to your computer and use it in GitHub Desktop.
Save axefrog/3a0494b196fd596bad5ebbc47e95dcb0 to your computer and use it in GitHub Desktop.
Full-featured console-based log/trace debugger for most.js, borrowing ideas from CQRS.
import Immutable from 'immutable';
import formatDate from 'date-fns/format';
import immutableDiff from 'immutablediff';
/* eslint-disable no-underscore-dangle */
var colors = {
0: ['white', '#7fbad8', '#0075b2'],
1: ['white', '#91a0ce', '#24429e'],
2: ['white', '#ab86e0', '#570ec1'],
3: ['white', '#c693cb', '#8d2798'],
4: ['white', '#e17fa2', '#c30045'],
5: ['white', '#ee8c7f', '#de1900'],
6: ['white', '#eeb27f', '#de6500'],
7: ['black', '#6f4900', '#de9200'],
8: ['black', '#6f5f00', '#debe00'],
9: ['black', '#6c7200', '#d9e400'],
10: ['white', '#b8e08d', '#72c11b'],
11: ['white', '#94d4a9', '#2aaa54'],
12: ['black', '#797a7a', '#f2f4f4'],
13: ['black', '#333339', '#676773'],
main: i => colors[i][2],
inverse: i => colors[i][0],
mid: i => colors[i][1],
};
function colorText(i, mid = false) {
return `color: ${mid ? colors.mid(i) : colors.main(i)}`;
}
function colorFill(i, mid = false) {
return `color: ${mid ? colors.mid(i) : colors.inverse(i)}; background-color: ${colors.main(i)}`;
}
function colorFillInv(i, mid = false) {
return `color: ${mid ? colors.mid(i) : colors.main(i)}; background-color: ${colors.inverse(i)}`;
}
const dimTextStyle = colorText(13);
const colorsByEventType = {
construct: 13,
run: 11,
event: 12,
end: 9,
error: 5,
dispose: 3
};
const typeLabels = {
'construct': 'CONSTRUCT',
'run': ' RUN ',
'event': ' EVENT ',
'end': ' END ',
'error': ' ERROR ',
'dispose': ' DISPOSE ',
};
const instanceIdMap = {};
function hashString(str) {
var hash = 5381, i = str.length;
while(i) hash = (hash * 33) ^ str.charCodeAt(--i);
return hash >>> 0;
}
// function hashString(s) {
// let n = 0;
// for(let i = 0; i < s.length; i++) {
// n += s.charCodeAt(i);
// }
// return n;
// }
function safe(id) {
return typeof id === 'symbol'
? id.toString().substr(7, id.toString().length - 8)
: id;
}
const colorWheelSize = 12;
function chooseStyle(id) {
var number;
if(id === null || id === void 0) number = 0;
else if(typeof id !== 'number') number = hashString(safe(id));
else number = id;
var index = number % colorWheelSize;
var isMid = number % (colorWheelSize*2) > colorWheelSize;
var isInverse = number % (colorWheelSize*4) > colorWheelSize*2;
return isInverse ? colorFillInv(index, isMid) : colorFill(index, isMid);
}
var startTime = Date.now();
function fmtTime(t) {
const time = t === void 0 ? Date.now() : t;
return formatDate(time - startTime, 'mm:ss.SSS');
}
function nextInstanceId(hash) {
if(!hash) return void 0;
return [instanceIdMap[hash] = (instanceIdMap[hash] || 0) + 1, Date.now()];
}
function isImmutableCollection(x) {
return x ? x instanceof Immutable.Collection : false;
}
function isImmutableClass(x) {
return x && typeof x === 'object' && isImmutableCollection(x._state) && x._state.has('$version');
}
function versionOf(cls) {
const version = cls.version;
return typeof version === 'object' ? version.number : version || 0;
}
const undefinedType = Symbol('DataType: Undefined');
const nullType = Symbol('DataType: Null');
const objectType = Symbol('DataType: Object');
const arrayType = Symbol('DataType: Array');
const dateType = Symbol('DataType: Date');
const immutableCollectionType = Symbol('DataType: Immutable Collection');
const immutableClassType = Symbol('DataType: Immutable Class');
const stringType = Symbol('DataType: String');
const numberType = Symbol('DataType: Number');
const booleanType = Symbol('DataType: Boolean');
const functionType = Symbol('DataType: Function');
const symbolType = Symbol('DataType: Symbol');
function determineType(x) {
if(x === void 0) return undefinedType;
if(x === null) return nullType;
if(typeof x === 'object') {
if(x instanceof Date) return dateType;
if(isImmutableClass(x)) return immutableClassType;
if(isImmutableCollection(x)) return immutableCollectionType;
if(Array.isArray(x)) return arrayType;
return objectType;
}
if(typeof x === 'string') return stringType;
if(typeof x === 'number') return numberType;
if(typeof x === 'function') return functionType;
if(typeof x === 'boolean') return booleanType;
if(typeof x === 'symbol') return symbolType;
console.warn('Unable to emit debug information for unexpected type:', x, typeof x);
return objectType;
}
function describeType(type, x) {
switch(type) {
case undefinedType: return 'undefined';
case nullType: return 'null';
case objectType: return x.constructor.name || 'UnnamedType';
case arrayType: return `Array[${x.length}]`;
case dateType: return `Date(${formatDate(x)})`;
case immutableClassType: return `${x.constructor.name}(V:${versionOf(x)})`;
case stringType: return `String(${x.length})`;
case numberType: return `Number(${x})`;
case functionType: return `Function(${x.name})`;
case booleanType: return `Bool(${x})`;
case symbolType: return x.toString();
default: return 'Unknown';
}
}
function describe(value, abbreviate) {
const type = determineType(value);
const descr = abbreviate ? abbreviate(value) : describeType(type, value);
return {value, type, descr};
}
function isNullOrUndefined(type) {
return type === nullType || type === undefinedType;
}
function padStr(str, len) {
const pad = len - str.length;
if(pad <= 0) return str;
const left = Math.floor(pad/2);
const right = pad - left;
const a = new Array(left + 1).join(' ');
const b = new Array(right + 1).join(' ');
return a + str + b;
}
class MessageBuilder
{
constructor() {
this.message = [];
this.args = [];
this._method = console.log;
this._append(fmtTime(), {style: colorFill(13), pad: true});
}
static create() {
return new MessageBuilder();
}
_tokenize(str, {style, prefix = '', suffix = '', pad = false} = {}) {
const delimiter = style ? '%c' : '';
const padLeft = pad === true || pad === 'left' ? ' ' : '';
const padRight = pad === true || pad === 'right' ? ' ' : '';
const token = `${prefix}${delimiter}${padLeft}${str}${padRight}${delimiter}${suffix}`;
const args = style ? [style, ''] : [];
return [token, args];
}
_appendFinal(msg, args) {
this.message.push(msg);
this.args.push(...args);
}
_append() {
const [msg, args] = this._tokenize.apply(this, arguments);
this._appendFinal(msg, args);
}
_mergeTokens(tokens) {
return tokens.reduce((a, token) => [a[0] + token[0], a[1].concat(token[1])], ['', []]);
}
_appendTokens(tokens) {
const [msg, args] = this._mergeTokens(tokens);
this._appendFinal(msg, args);
}
_tokenizeDescr(x, disposition) {
var style;
switch(disposition) {
case 'good': style = colorText(10); break;
case 'bad':
style = colorText(5);
this._method = console.warn;
break;
case 'warn':
if(this._method !== console.warn) {
this._method = console.debug;
}
style = colorText(7); break;
case 'flag':
if(this._method !== console.warn) {
this._method = console.debug;
}
style = colorText(9); break;
case 'minor': style = dimTextStyle; break;
default: style = colorText(12); break;
}
return this._tokenize(x.descr, {style});
}
setType(type) {
this._append(typeLabels[type], {style: colorFill(colorsByEventType[type]), pad: true});
return this;
}
setSource(source) {
const name = source.constructor.name;
this._append(padStr(name, 18), {style: chooseStyle(name), pad: true});
return this;
}
_appendInstance(type, id, t) {
this._appendTokens([
this._tokenize(`${type}:`, {style: colorFill(1), pad: 'left'}),
this._tokenize(id, {style: chooseStyle(id)}),
this._tokenize(fmtTime(t), {style: colorFill(1, true), pad: true})
]);
}
setSourceInstance([id, t]) {
this._appendInstance('SRC', id, t);
return this;
}
setSinkInstance([id, t]) {
this._appendInstance('SNK', id, t);
return this;
}
setName(name) {
if(name !== void 0) {
this._append(name, {style: colorFillInv(12), pad: true});
}
return this;
}
setCount(count) {
this._append(count, {style: colorText(12), prefix: '[#', suffix: ']'});
return this;
}
setAbbreviatedValue(showPreviousType, current, previous, dispositions) {
this._a = previous;
this._b = current;
const aIsNil = isNullOrUndefined(previous.type);
const bIsNil = isNullOrUndefined(current.type);
const aIsClass = previous.type === immutableClassType;
const bIsClass = current.type === immutableClassType;
let aDisposition, bDisposition;
if(aIsClass || bIsClass) {
if(aIsClass && bIsClass) {
const va = versionOf(previous.value);
const vb = versionOf(current.value);
const disposition = vb === va + 1 ? 'good'
: vb > va ? 'warn'
: vb === va ? 'minor'
: 'bad';
aDisposition = bDisposition = disposition;
}
else {
aDisposition = aIsClass ? bIsNil ? 'minor' : 'bad' : aIsNil ? 'minor' : 'bad';
bDisposition = bIsClass ? aIsNil ? null : 'bad' : bIsNil ? 'minor' : 'bad';
}
}
else {
aDisposition = aIsNil ? 'minor' : bIsNil || previous.type === current.type ? 'minor' : 'bad';
bDisposition = bIsNil ? 'minor' : aIsNil || previous.type === current.type ? null : 'bad';
}
if(dispositions) {
if(Array.isArray(dispositions)) {
aDisposition = dispositions[0] || aDisposition;
bDisposition = dispositions[1] || bDisposition;
}
else {
aDisposition = dispositions || aDisposition;
bDisposition = dispositions || bDisposition;
}
}
const tokens = [this._tokenize('[')];
if(showPreviousType) {
tokens.push(this._tokenizeDescr(previous, aDisposition));
tokens.push(this._tokenize(' => ', {style: colorText(13)}));
}
tokens.push(this._tokenizeDescr(current, bDisposition));
tokens.push(this._tokenize(']'));
this._appendTokens(tokens);
return this;
}
includeValueInspector() {
this.args.push(new InspectableValue(this._a, this._b));
}
write(extraValues) {
const args = [this.message.join(' ')].concat(this.args);
if(extraValues.length) {
args.push(...extraValues);
}
this._method.apply(console, args);
}
};
const globalTraceId = Symbol('Global Trace');
const traces = {};
function initContext(trace) {
if(trace._context) {
trace.context = trace.options.init ? trace.options.init() : trace._context;
delete trace._context;
}
return trace;
}
function processTrace(eventContext, trace) {
const type = eventContext.eventType;
const options = trace.options;
if(options['pre']) {
const result = options['pre'](eventContext);
if(typeof result === 'boolean') return result;
}
if(options[type]) {
const result = options[type](eventContext);
if(typeof result === 'boolean') return result;
}
switch(eventContext.eventType) {
case 'event':
case 'end':
case 'error':
if(options['events']) {
const result = options['events'](eventContext);
if(typeof result === 'boolean') return result;
}
break;
case 'construct':
case 'run':
case 'dispose':
if(options['lifecycle']) {
const result = options['lifecycle'](eventContext);
if(typeof result === 'boolean') return result;
}
break;
}
if(options['*']) {
const result = options['*'](eventContext);
if(typeof result === 'boolean') return result;
}
}
function option(name, sources) {
const trace = sources.find(source => source && source.options && name in source.options);
return trace && trace.options[name];
}
function processUnionedTrace(sources, eventContext) {
let cancel = void 0;
const silent = valueOrDefault(option('silent', sources), false);
for(let trace of sources) {
if(trace) {
const result = processTrace(eventContext, trace);
if(typeof result === 'boolean' && cancel === void 0) cancel = result;
if(silent !== (cancel === false)) return false;
}
}
return true;
}
function traceUnion(localTrace, namedTrace, globalTrace) {
const sources = [
localTrace,
namedTrace && initContext(namedTrace),
globalTrace && initContext(globalTrace)
];
return new Proxy({}, {
get(target, name) {
switch(name) {
case 'local': return sources[0];
case 'named': return sources[1];
case 'global': return sources[2];
case 'execute': return context => processUnionedTrace(sources, context);
default:
return name in target ? target[name] : target[name] = option(name, sources);
}
}
});
}
function valueOrDefault(value, defaultValue) {
return value === void 0 ? defaultValue : value;
}
class Logger
{
constructor(options) {
this.options = options;
}
clone(options) {
return new Logger(Object.assign({}, this.options, options));
}
write(eventType, value) {
const [hasTrace, trace] = (() => {
const localTrace = this.options.trace && {context:{}, options: this.options.trace};
const namedTrace = this.options.name && traces[this.options.name];
const globalTrace = traces[globalTraceId];
const hasTrace = localTrace || namedTrace || globalTrace;
return [hasTrace, traceUnion(localTrace, namedTrace, globalTrace)];
})();
const isSinkEvent = arguments.length === 2;
const currentValue = isSinkEvent && describe(value, value !== void 0 && trace.abbreviate);
let previousValue;
const extraValues = [];
if(isSinkEvent) {
this._count = (this._count || 0) + 1;
previousValue = this._previousValue || describe(void 0);
this._previousValue = currentValue;
}
const showPreviousType = valueOrDefault(trace.showPreviousType, true);
const getDisposition = trace.disposition;
if(hasTrace) {
const contextType = isSinkEvent ? EventContext : DebugContext;
const eventContext = new contextType(eventType, this.options, extraValues, trace, currentValue, previousValue, this._count);
if(!trace.execute(eventContext)) return;
}
const msg = MessageBuilder.create()
.setType(eventType)
.setSource(this.options.source)
.setSourceInstance(this.options.sourceInstanceId);
if('sinkInstanceId' in this.options) {
msg.setSinkInstance(this.options.sinkInstanceId);
if(isSinkEvent) {
msg.setCount(this._count);
}
};
if(this.options.name) {
msg.setName(this.options.name);
}
if(isSinkEvent) {
const dispositions = getDisposition && getDisposition(currentValue.value, previousValue.value);
msg.setAbbreviatedValue(showPreviousType, currentValue, previousValue, dispositions);
if(trace.inspect) {
msg.includeValueInspector();
}
}
msg.write(extraValues);
}
};
const globalContext = {};
class DebugContext
{
constructor(eventType, options, extraValues, trace) {
this._eventType = eventType;
this._options = options;
this._values = extraValues;
this._trace = trace;
}
get eventType() { return this._eventType; }
get name() { return this._options.name; }
get localContext() { return this._trace.local && this._trace.local.context; }
get traceContext() { return this._trace.named && this._trace.named.context; }
get globalContext() { return this._trace.global && this._trace.global.context; }
get src() {
return this._src || (this._src = {
id: this._options.sourceInstanceId[0],
name: this._options.source.constructor.name,
context: this._options.sourceContext,
ref: this._options.source
});
}
get snk() {
return this._options.sink && (this._snk || (this._snk = {
id: this._options.sinkInstanceId[0],
name: this._options.sink.constructor.name,
context: this._options.sinkContext,
ref: this._options.sink
}));
}
log(...x) { this._values.push(...x); }
}
class EventContext extends DebugContext
{
constructor(eventType, options, extraValues, trace, current, previous, count) {
super(eventType, options, extraValues, trace);
this._current = current;
this._previous = previous;
this._count = count;
}
get diff() {
if('_diff' in this) return this._diff;
const a = this._previous;
const b = this._current;
if(a.type === immutableClassType && b.type === immutableClassType) {
return this._diff = immutableDiff(a.value.state, b.value.state).toJS();
}
return this._diff = void 0;
}
get count() { return this._count; }
get value() { return this._current.value; }
get previous() { return this._previous.value; }
}
function summarizeValue(value) {
const x = describe(value);
return describeType(x.type, x.value);
}
function refineImmutableDiff(diff) {
const result = diff
.map(change => {
if(change.path.endsWith('$version') && change.op !== 'remove') {
return `VERSION (#${change.value}) at ${change.path.substr(0, change.path.length - 9)||'/'}`;
}
switch(change.op) {
case 'add': return `ADD (${summarizeValue(change.value)}) at ${change.path}`;
case 'replace': return `UPDATE (${summarizeValue(change.value)}) at ${change.path}`;
case 'remove': return 'DELETE: ' + change.path;
default: return change;
}
})
.filter(x => x);
return result && result.length ? result.length === 1 ? result[0] : result : 'NO CHANGES';
}
function InspectableValue(a, b) {
let _diff;
let hasDiff;
if(a.type === immutableClassType) {
if(b.type === immutableClassType) {
Object.defineProperty(this, 'diff', {
get() {
if(!_diff) _diff = immutableDiff(a.value.state, b.value.state).toJS();
return _diff;
}
});
hasDiff = true;
}
let _version;
Object.defineProperty(this, 'version', {
get() {
if(!_version) {
if(typeof a.value.version === 'number') {
_version = a.value.version;
}
else {
_version = {
number: a.value.version.number,
updates: a.value.version.updates.map(u => u.type + ': [' + u.args.join(', ') + ']')
};
}
}
return _version;
}
});
}
else if(a.type === immutableCollectionType && b.type === immutableCollectionType) {
Object.defineProperty(this, 'diff', {
get() {
if(!_diff) _diff = immutableDiff(a.value, b.value).toJS();
return _diff;
}
});
hasDiff = true;
}
if(hasDiff) {
Object.defineProperty(this, 'summary', {
get() { return refineImmutableDiff(this.diff); }
});
}
Object.defineProperty(this, 'previous', {
get() { return a.value; }
});
Object.defineProperty(this, 'current', {
get() { return b.value; }
});
}
class DebugSource
{
constructor(source, name, trace) {
this.source = source;
const stackStr = new Error().stack.toString().split(/\n/g).slice(4, 7).join('\n');
const hash = hashString((name ? name + ';' : '') + stackStr);
const sourceInstanceId = nextInstanceId(hash);
this.logger = new Logger({
name,
source,
sourceInstanceId,
sourceContext: {},
trace
});
this.logger.write('construct');
}
run(sink, scheduler) {
const name = this.name;
const sinkInstanceId = nextInstanceId(`sink.${this.logger.options.sourceInstanceId}` + name);
const debugSink = new DebugSink(sink, this.logger.clone({
sink,
sinkInstanceId,
sinkContext: {},
}));
debugSink.logger.write('run');
const disposable = this.source.run(debugSink, scheduler);
return {
dispose() {
debugSink.logger.write('dispose');
disposable.dispose();
}
};
}
}
class DebugSink
{
constructor(sink, msg) {
this.sink = sink;
this.logger = msg;
}
event(t, x) {
this.logger.write('event', x);
this.sink.event(t, x);
}
end(t, x) {
this.logger.write('end', x);
this.sink.end(t, x);
}
error(t, e) {
this.logger.write('error', e);
this.sink.error(t, e);
}
}
function debug(name, trace) {
if(!trace && typeof name === 'object') {
trace = name;
name = void 0;
}
if(name === null) name = void 0;
if(trace === null) trace = void 0;
return stream => new stream.constructor(new DebugSource(stream.source, name, trace));
}
debug.trace = function registerDebugTrace(id, options) {
let context;
if(arguments.length === 1) {
options = id;
id = globalTraceId;
context = globalContext;
}
else {
context = {};
}
traces[id] = {_context: context, options};
return debug;
};
export default debug;
/* eslint-enable no-underscore-dangle */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment