const coalesce = require('extant')
Return the first non-null
like parameter.
const coalesce = require('extant')
Deep differences.
const Keyify = require('keyify')
Weak map of instances to construction material used for de-duplication and reporting to report the errors of our errors. Errors in JavaScript are simple objects and utilities that encounter them will do things like print their properties to console so protected status of protected properties is likely to be violated.
const Instances = new WeakMap
const Prototypes = new WeakMap
Parse the file and line number from a Node.js stack trace.
const location = require('./location')
sprintf
supports named parameters so we can use our parameters object to
fill in the sprintf
place-holders.
const sprintf = require('sprintf-js').sprintf
Used to assert that the constructor is only ever called from a generated derived Interrupt class.
const PROTECTED = Symbol('PROTECTED')
The value of the type
property for the options object to the constructor.
Use for disambiguation when currying assert
.
const OPTIONS = Symbol('OPTIONS')
This is a place-holder object for the nested exception when we generate exceptions to audit assertions and guarded functions.
const AUDIT = new Error('example')
Generate the message for the Goole V8 exception. The message is specially
formatted to appear integrated with the stack trace from error.stack
which
includes the message in the stack trace.
function context (options, instance, stack = true) {
Attempt to use the options message as a sprintf
format. Use the message
as is if sprintf
fails.
let message = instance.message = options.message
try {
message = instance.message = sprintf(options.message, options)
} catch (error) {
instance.errors.push({
code: Interrupt.Error.SPRINTF_ERROR,
format: options.format,
properties: options.properties,
error: error
})
}
The enumerable properties, if any, of the object using our special JSON.
if (Object.keys(instance.displayed).length != 0) {
message += '\n\n' + Interrupt.JSON.stringify(instance.displayed)
}
TODO Without context messages we have more space. We could, if the
type is not an Error, serialize the cause as JSON. Parsing would be a
matter of detecting if it is an error, if not it is going to be JSON.
JSON will not look like an error, perhaps just plain null
would be
confusing, but I doubt it.
if (options.errors.length) {
for (let i = 0, I = options.errors.length; i < I; i++) {
const error = options.errors[i]
const text = error instanceof Error ? Interrupt.stringify(error) : error.toString()
const indented = text.replace(/^/gm, ' ')
message += '\n\ncause:\n\n' + indented
}
}
A header for the stack trace unless the stack trace has been suppressed.
if (stack && (options.$stack == null || options.$stack != 0)) {
message += '\n\nstack:\n'
}
return message
}
A utility to merge two or more objects preserving their descriptor traits.
function combine (...vargs) {
const properties = {}
for (const object of vargs) {
for (const property of Object.getOwnPropertyNames(object)) {
properties[property] = Object.getOwnPropertyDescriptor(object, property)
}
}
return Object.defineProperties({}, properties)
}
Get an object from a tree of objects object
using the given array of
indexes in the given path
. Used by our specialized JSON to generate and
resolve references.
function get (object, path) {
let iterator = object
for (const part of path) {
iterator = iterator[part]
}
return iterator
}
class Collector {
constructor () {
this._lines = []
}
push (line) {
this._lines.push(line)
}
end () {
if (this._lines[this._lines.length - 1] === '') {
this._lines.pop()
}
return this._lines.join('\n')
}
}
An assert internal to Interrupt that will not get audited.
function assert (condition, ...vargs) {
if (! condition) {
throw new Interrupt.Error(Interrupt.Error.options.apply(Interrupt.Error, [{ $callee: assert }].concat(vargs)))
}
}
The Interrupt class extends Error
using class ES6 extension.
class Interrupt extends Error {
Private static initializer. We are committed to Node.js 12 or greater.
static #initializer = (function () {
Prototypes.set(Interrupt, {
is: new Map, Super: { Codes: {}, Aliases: {} }
})
} ())
TODO Maybe a set of common symbols mapped to the existing Node.js
error types? No, the ability to specify a symbol, but it must be unique,
and we can put those types in Interrupt.Error
.
The Interrupt.Error
class is itself an interrupt defined error.
static Error = Interrupt.create('Interrupt.Error', {
TODO Rename.
INVALID_CODE: 'code is already a property of the superclass',
UNKNOWN_CODE: 'unknown code',
INVALID_CODE_TYPE: 'invalid code type',
INVALID_ACCESS: 'constructor is not a public interface',
PARSE_ERROR: null,
SPRINTF_ERROR: null,
NULL_ARGUMENT: 'null argument given to exception constructor',
INVALID_PROPERTY_NAME: 'invalid property name',
INVALID_PROPERTY_TYPE: 'invalid property type',
INVALID_PROPERTY_NAME: 'invalid property name',
DEFERRED_CONSTRUCTOR_INVALID_RETURN: null,
DEFERRED_CONSTRUCTOR_NOT_CALLED: null
}, function ({ Codes }) {
return {
SUPER_PROTOTYPE_MISSING: {
code: Codes.INVALID_CODE.symbol,
message: 'attempt to define alias of non-existant code or alias'
},
SYMBOLS_NOT_ALLOWED: {
code: Codes.INVALID_CODE.symbol,
message: 'code alias definitions cannot define a `symbol` property'
}
}
})
static explode (error) {
const preamble = error.message == ''
? `${error.name}`
: `${error.name}: ${error.message}`
if (
error.name == null ||
error.message == null ||
error.stack == null ||
error.stack.indexOf(preamble) != 0 ||
!RE.identifier.test(error.name)
) {
return [{
constructor: error.constructor.name,
error: {
name: error.name,
message: error.message,
stack: error.stack == null ? null : unstacker.parse(error.stack),
properties: { ...error }
}
}]
}
const stack = error.stack[preamble.length] == '\n'
? error.stack.substring(preamble.length + 1)
: error.stack.substring(preamble.length)
return {
name: error.name,
message: error.message,
properties: { ...error },
stack: stack
}
}
static stringify (error) {
if (error instanceof Interrupt) {
return error.stack
}
const exploded = Interrupt.explode(error)
if (Array.isArray(exploded)) {
return Interrupt.JSON.stringify(exploded[0])
}
if (exploded.message == '' && Object.keys(exploded.properties).length == 0) {
return error.stack
}
const message = error.message.split('\n')
for (let i = 1, I = message.length; i < I; i++) {
message[i] = ` ${message[i]}`
}
const title = exploded.message == ''
? `${exploded.name}`
: `${exploded.name}: ${message.join('\n')}`
const header = Object.keys(exploded.properties).length == 0
? title
: `${title}\n\n${Interrupt.JSON.stringify(exploded.properties)}`
if (exploded.stack.length == 0) {
return header
}
return `${header}\n\nstack:\n\n${exploded.stack}`
}
static Parser = class Parser {
constructor (scan = false) {
this._scannable = scan
this._scanning = scan
this._mode = scan ? 'scan' : 'exception'
this._collector = null
this._depth = 0
this._position = { line: 1 }
}
static _DEDENT = {
'message': 1,
'properties': 0,
'stack': 0
}
TODO Need to be regex so we can detect a naked error with no
message and a stack that starts with ' at'
.
static _START = {
'properties': '{',
'stack': 'stack:',
'errors': 'cause:'
}
static _TRANSITION = {
'message': [ 'properties', 'errors', 'stack' ],
'properties': [ 'errors', 'stack' ],
'errors': [ 'errors', 'stack' ],
'stack': [],
'object': []
}
static _INCLUDE = {
'message': false,
'properties': true,
'errors': false,
'stack': 'false'
}
_complete () {
switch (this._mode) {
case 'message': {
this._node.message = this._collector.end()
this._collector.length = 0
}
break
case 'properties': {
this._node.properties = Interrupt.JSON.parse(this._collector.end())
}
break
case 'stack': {
this._node.stack = unstacker.parse(this._collector.end())
}
break
case 'errors': {
this._node.errors.push(this._collector.end())
}
break
case 'object': {
this._node = Interrupt.JSON.parse(this._collector.end())
}
break
}
}
_transition (source) {
MODES: for (const mode of Interrupt.Parser._TRANSITION[this._mode]) {
if (source.trimRight() === Interrupt.Parser._START[mode]) {
this._complete()
switch (this._mode = mode) {
case 'errors': {
this._collector = new Interrupt.Parser
this._collector._mode = 'cause'
}
break
default: {
this._collector = new Collector
}
break
}
return Interrupt.Parser._INCLUDE[this._mode]
}
}
return true
}
_exception (line) {
const $ = RE.exceptionStart.exec(line)
if ($ != null) {
const [ , space, className, separator, message ] = $
this._depth = space.length
this._collector = new Collector
this._node = {
className: className,
message: null,
properties: {},
errors: [],
_errors: [],
stack: []
}
if (separator != null) {
this._collector.push(message)
}
this._mode = 'message'
return true
}
return false
}
push(line) {
switch (this._mode) {
case 'exception': {
this._position = { line: 1, text: line }
const dedented = dedent(line, this._depth, this._position)
const $ = RE.exceptionStart.exec(line)
assert($ != null, 'PARSE_ERROR', this._position)
const [ , space, className, separator, message ] = $
this._depth = space.length
this._collector = new Collector
this._node = {
className: className,
message: null,
properties: {},
errors: [],
_errors: [],
stack: []
}
if (separator != null) {
this._collector.push(message)
}
this._mode = 'message'
}
break
case 'cause': {
if (/\S+/.test(line) && ! this._exception(line)) {
console.log('OH, NO!')
this._collector = new Collector
this._collector.push(line)
this._mode = 'object'
}
}
break
default: {
this._position.line++
this._position.text = line
const dedented = dedent(line, this._depth, this._position)
if (this._transition(dedented, 'properties', 'cause', 'stack')) {
this._collector.push(dedent(dedented, this._mode == 'message' ? 1 : 0, this._position))
}
}
break
}
}
end () {
this._complete()
return this._node
}
}
We implement custom JSON serialization that supports circular references because we don’t want to raise an exception on bad JSON because JSON serialization is used for printing out the properties on the error path. We don’t want to raise an exception on bad JSON and we don’t want to neglect to say as much as we can about the properties we’ve been given.
static JSON = {
Stringify visits each object in the object to look for duplicate
objects and mark them for reference construction in the replacer. It
does not create a copy of the object because we want
JSON.stringify()
is to resolve the .toJSON()
conversions.
stringify (object) {
const seen = new Map
const replacements = new Map
function visit (path, value) {
switch (typeof value) {
case 'object': {
if (value != null) {
const reference = seen.get(value)
if (reference != null) {
replacements.set(value, '_reference')
} else {
seen.set(value, path)
if (Array.isArray(value)) {
const array = []
if (
typeof value[0] == 'string' &&
/^_reference|_array|_undefined$/.test(value[0])
) {
replacements.set(value, [ '_array' ].concat(value))
}
} else if (value instanceof Error && ! (value instanceof Interrupt && value === object)) {
const error = { message: value.message }
for (const key in value) {
error[key] = value[key]
}
replacements.set(value, error)
} else {
for (const property in value) {
visit(path.concat(property), value[property])
}
}
}
}
}
default:
return value
}
}
const referenced = visit([], object)
return JSON.stringify(referenced, function (index, value) {
if (typeof value === 'undefined') {
return [ '_undefined' ]
}
if (typeof value === 'function' || typeof value === 'symbol') {
return value.toString()
}
if (typeof value == 'object' && value != null) {
const replacement = replacements.get(value)
if (replacement != null) {
if (replacement === '_reference') {
const path = seen.get(value)
const origin = {
object: get(object, path.slice(0, path.length - 1)),
index: path[path.length - 1]
}
if (origin.object === this && origin.index === index) {
return value
}
return [ '_reference', path ]
}
return replacement
}
}
return value
}, 4)
},
Parse converts our escaped Array
and undefined
place holders and
builds an array of references in the reviver. It resolve the
references after parsing so that any referenced arrays are already
converted.
parse (json) {
const references = []
const parsed = [ JSON.parse(json) ]
function visit (object, index, value) {
if (typeof value == 'object' && value != null) {
if (Array.isArray(value)) {
switch (value[0]) {
case '_reference':
references.push({ object, index, path: value[1] })
break
case '_undefined':
object[index] = void 0
break
case '_array':
value.shift()
default:
for (let i = 0, I = value.length; i < I; i++) {
visit(value, i, value[i])
}
}
} else {
for (const property in value) {
visit(value, property, value[property])
}
}
}
}
visit(parsed, 0, parsed[0])
for (const { object, index, path } of references) {
object[index] = get(parsed[0], path)
}
return parsed[0]
}
}
static Code (object) {
return Object.defineProperties({}, {
code: { value: object.code, enumerable: true },
symbol: { value: object.symbol, enumerable: false }
})
}
This constructor is only called by derived class and should not be called
by the user. An argument could be made that we accommodate the user that
hasn’t read the documentation because they could be calling this in
production having never tested an exceptional branch of their code, but
they could just as easily have misspelled Interrupt
. Basically, we’re
not going to be as accommodating as all that.
constructor (Protected, Class, Prototype, vargs) {
We can’t use Interrupt.Error.assert
because auditing will make us
blow the stack.
assert(PROTECTED === Protected, 'INVALID_ACCESS')
When called with no arguments we call our super constructor with no
arguments to eventually call Error
with no argments to create an
empty error.
const args = Class.options.apply(Class, vargs)
const prototypes = [ Prototype.prototypes[args.code] || { code: null } ]
const code = prototypes[0].code
if (prototypes[0].code != null && prototypes[0].code != args.code) {
let superPrototype = prototypes[0], superCode
do {
superCode = superPrototype.code
superPrototype = Prototype.prototypes[superCode]
prototypes.unshift(superPrototype)
} while (superPrototype.code != superCode)
}
prototypes.push(args, { code: prototypes[prototypes.length - 1].code })
const options = Class.options.apply(Class, prototypes)
const properties = {
name: {
TODO Class.name?
value: Prototype.name,
enumerable: false,
writable: false
},
errors: {
value: options.errors,
enumerable: false,
writable: false
}
}
if (options.code) {
properties.code = {
value: options.code,
enumerable: true,
writable: false
}
properties.symbol = {
value: Prototype.prototypes[options.code].symbol,
enumerable: false,
writable: false
}
}
for (const property of Object.getOwnPropertyNames(options)) {
if (property[0] != '_' && property[0] != '$' && !/^(?:name|message|stack|symbol)$/.test(property) && !(property in properties)) {
properties[property] = Object.getOwnPropertyDescriptor(options, property)
}
}
const instance = { message: null, errors: options.$errors, options, displayed: {} }
for (const property in properties) {
if (properties[property].enumerable) {
instance.displayed[property] = properties[property].value
}
}
const stackTraceLimit = Error.stackTraceLimit
if (options.$stack != null) {
Error.stackTraceLimit = options.$stack
}
TODO Display internal errors.
if (
options.code == null &&
options.message == null &&
options.errors.length == 0 &&
Object.keys(options).filter(name => !/^\$|^#|^errors$/.test(name)).length == 0
) {
super()
} else {
super(context(options, instance))
}
Instances.set(this, instance)
const invisible = {}
for (const property in properties) {
invisible[property] = { ...properties[property], enumerable: false }
}
invisible.properties = {
value: Object.defineProperties({}, properties),
enumerable: false,
writable: false,
configurable: false
}
Object.defineProperties(this, invisible)
FYI It is faster to use Error.captureStackTrace
again than
it is to try to strip the stack frames created by Error
using a regular expression or string manipulation. You know
because you tried. Years later: Thanks for reminding me, I keep
coming back to experiment with it.
if (options.$callee != null) {
Error.captureStackTrace(this, options.$callee)
}
Error.stackTraceLimit = stackTraceLimit
}
Our toString
representation mirrors that of Node.js. We remove the
context and headings from the message
used to generate stack
.
toString () {
const instance = Instances.get(this)
if (instance.message == null) {
return this.name
}
return `${this.name}: ${instance.message}`
}
static get OPTIONS () {
return OPTIONS
}
static get CURRY () {
return { $type: OPTIONS }
}
static get auditing () {
return typeof this.audit == 'function'
}
TODO Wouldn’t it be nice to have some sort of way to specify properties by code? Like which subsystem or a severity?
static create (name, ...vargs) {
const SuperClass = typeof vargs[0] == 'function' ? vargs.shift() : Interrupt
if (Interrupt.Error != null) {
assert(SuperClass === Interrupt || SuperClass.prototype instanceof Interrupt, 'INVALID_SUPER_CLASS', SuperClass.name)
}
const Class = class extends SuperClass {
constructor (...vargs) {
if (vargs[0] === PROTECTED) {
super(...vargs)
} else {
super(PROTECTED, Class, Prototype, vargs)
}
}
static get codes () {
return Object.keys(Prototype.codes)
}
static code (code) {
return Prototype.codes[code]
}
static options (...vargs) {
function attr (value) {
return { value: value, enumerable: true, writable: true, configurable: true }
}
const options = {
$type: attr(OPTIONS),
$errors: attr([]),
errors: attr([]),
$pokers: attr([]),
$stack: attr(null),
$callee: attr(null)
}
while (vargs.length != 0) {
const argument = vargs.shift()
switch (typeof argument) {
case 'string': {
TODO Keep expecting us to use the first code that is set, but we don’t do that, do we?
if (Prototype.prototypes[argument] != null) {
options.code = attr(argument)
} else {
options.message = attr(argument)
}
}
break
case 'symbol': {
TODO Wondering about code overrides, should they be allowed? Or do we accept the first code that is set?
const code = Prototype.symbols.get(argument)
if (code != null) {
options.code = attr(code)
}
}
break
case 'number': {
if ((Number.isInteger(argument) || argument == Infinity) && argument >= 0) {
options.$stack = attr(argument)
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE')))
}
}
break
case 'object': {
if (argument == null) {
TODO code = { text, symbol } // name? label? identifier? id? string?
options.$errors.push(combine(Interrupt.Error.codes('NULL_ARGUMENT')))
} else if (argument instanceof Error) {
options.errors.value.push(argument)
} else if (Array.isArray(argument)) {
options.errors.value.push.apply(options.errors.value, argument)
} else {
for (const property of Object.getOwnPropertyNames(argument)) {
switch (property) {
case '$type': {
if (argument[property] !== OPTIONS) {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case '$vargs': {
if (argument[property] == null) {
} else if (Array.isArray(argument[property])) {
vargs.unshift.apply(vargs, argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case '$pokers': {
if (argument[property] == null) {
} else if (typeof argument[property] == 'function') {
options.$pokers.value.push(argument[property])
} else if (
Array.isArray(argument[property]) &&
argument[property].every(element => typeof element == 'function')
) {
options[property].value.push.apply(options[property].value, argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case '$errors': {
if (
Array.isArray(argument[property]) &&
argument[property].every(error => {
return Interrupt.Error[error.code] === error.symbol
})
) {
options.$errors.value.push.apply(options.$errors.value, argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case '$stack': {
const stack = argument[property]
if (stack == null) {
} else if ((Number.isInteger(stack) || stack == Infinity) && stack >= 0) {
options.$stack = attr(stack)
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case 'stack':
case 'name': {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_NAME'), { property }))
}
break
case 'errors': {
if (Array.isArray(argument[property])) {
options.errors.value.push.apply(options.errors.value, argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case 'message': {
if (argument[property] == null) {
} else if (typeof argument[property] === 'string') {
options.message = attr(argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case 'code': {
TODO Convert symbol
to string
.
if (argument[property] == null) {
} else if (typeof argument[property] === 'string') {
if (Prototype.prototypes[argument[property]]) {
options.code = attr(argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('UNKNOWN_CODE'), {
property: property,
value: argument[property]
}))
}
} else {
options.$errors.value.push(combine(Interrupt.Error.code('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case 'symbol': {
if (argument[property] == null) {
} else if (typeof argument[property] == 'symbol') {
const code = Prototype.symbols.get(argument[property])
if (code != null) {
options.code = attr(code)
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('UNKNOWN_CODE'), {
property: property,
value: argument[property]
}))
}
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
case '$callee': {
if (argument[property] == null) {
} else if (typeof argument[property] == 'function') {
options[property] = attr(argument[property])
} else {
options.$errors.value.push(combine(Interrupt.Error.codes('INVALID_PROPERTY_TYPE'), { property }))
}
}
break
default: {
if (!RE.identifier.test(property)) {
options.$errors.value.push(combine(Interrupt.Error.code('INVALID_PROPERTY_NAME'), { property }))
} else {
options[property] = Object.getOwnPropertyDescriptor(argument, property)
}
}
break
}
}
}
}
}
}
return Object.defineProperties({}, options)
}
static create (options, vargs, ...callees) {
return construct(options, vargs, callees[0], callees[1])
}
static assert (...vargs) {
return _assert(Class.assert, {}, vargs)
}
static invoke (...vargs) {
return _invoke(Class.invoke, {}, vargs)
}
static callback (...vargs) {
return _callback(Class.callback, {}, vargs)
}
static resolve (...vargs) {
return _resolver(Class.resolve, {}, vargs)
}
}
function _construct (options, vargs, callees) {
debugger
const prelimary = vargs.length > 0 && typeof vargs[vargs.length - 1] == 'function'
? Class.options(options, { $vargs: vargs }, { $pokers: vargs.pop() })
: Class.options(options, { $vargs: vargs })
if (prelimary.$pokers.length != 0) {
let error = null
const $ = function () { error = new Class(merged) }
const merged = Class.options({ $callee: $ }, prelimary)
const pokers = merged.$pokers.slice()
let previous = $
while (pokers.length != 0) {
previous = function (caller, callee) {
return caller.bind(null, callee)
} (pokers.shift(), previous)
}
previous()
if (error == null) {
const error = new Class(merged)
const instance = Instances.get(error)
instance.errors.push({
code: Interrupt.Error.DEFERRED_CONSTRUCTOR_NOT_CALLED
})
return error
}
return error
} else {
return new Class(Class.options({ $callee: callees[0] }, prelimary))
}
}
function construct (options, vargs, ...callees) {
const error = _construct(options, vargs, callees)
if (Interrupt.auditing) {
Interrupt.audit(error, Instances.get(error).errors)
}
return error
}
function _assert (callee, options, vargs) {
if (typeof vargs[0] === 'object' && vargs[0] != null && vargs[0].$type === OPTIONS) {
const curried = Class.options(options, { $vargs: vargs })
return function assert (...vargs) {
return _assert(assert, curried, vargs)
}
} else if (!vargs[0]) {
vargs.shift()
throw construct(options, vargs, callee, callee)
} else if (Interrupt.auditing) {
construct(options, vargs, callee, callee)
}
}
function _invoke (callee, options, vargs) {
if (typeof vargs[0] == 'function') {
const f = vargs.shift()
try {
const result = f()
if (Interrupt.auditing) {
construct(Class.options(options, { errors: [ AUDIT ] }), vargs, callee, callee)
}
return result
} catch (error) {
throw construct(Class.options(options, { errors: [ error ] }), vargs, callee, callee)
}
}
const curried = Class.options(options, { $vargs: vargs })
return function invoker (...vargs) {
return _invoke(invoker, curried, vargs)
}
}
TOODO You cannot curry a poker function.
function _callback (callee, options, vargs) {
if (typeof vargs[vargs.length - 1] == 'function') {
const callback = vargs.pop()
return function (...response) {
if (response[0] == null) {
if (Interrupt.auditing) {
construct(Class.options(options, { errors: [ AUDIT ] }), vargs)
}
const poker = typeof vargs[vargs.length - 1] == 'function' ? vargs.pop() : null
const merged = Class.options(options, { $vargs: vargs }, { $pokers: poker })
callback.apply(null, response.concat({ $pokers: merged.$pokers }))
} else {
callback(construct(Class.options(options, { errors: [ response[0] ] }), vargs))
}
}
}
const merged = Class.options(options, { $vargs: vargs })
return function wrapper (...vargs) {
return _callback(wrapper, merged, vargs)
}
}
async function resolve (callee, f, options, vargs) {
try {
if (typeof f == 'function') {
f = f()
}
const result = await f
TODO No, I don’t want to do this merge every time. Yes, just go ahead and have the test condition here.
if (Interrupt.auditing) {
construct(Class.options(options, { errors: [ AUDIT ] }), vargs, callee)
}
return result
} catch (error) {
throw construct(Class.options(options, { errors: [ error ] }), vargs, callee)
}
}
function _resolver (callee, options, vargs) {
if (
typeof vargs[0] == 'function' ||
(
typeof vargs[0] == 'object' &&
vargs[0] != null &&
typeof vargs[0].then == 'function'
)
) {
return resolve(callee, vargs.shift(), options, vargs)
}
const merged = Class.options(options, { $vargs: vargs })
return function resolver (...vargs) {
return _resolver(resolver, merged, vargs)
}
}
Object.defineProperty(Class, 'name', { value: name })
We have an prototypical state of an exception that we do not want to store in the class and we definitely do not want to expose it publicly.
Running out of names, must tidy.
const SuperPrototype = Prototypes.get(SuperClass)
const Prototype = {
name: name,
is: new Map,
symbols: new Map,
codes: {},
prototypes: {},
Super: { Codes: {}, Aliases: {} }
}
Prototypes.set(Class, Prototype)
Detect duplicate declarations.
const duplicates = new Set
while (vargs.length != 0) {
const codes = vargs.shift()
switch (typeof codes) {
Define a code with no default properties.
case 'string': {
const object = {}
object[codes] = null
vargs.unshift(object)
continue
}
Invoke a function that will return further code definitions.
case 'function': {
vargs.unshift(codes({ Codes: Prototype.Super.Codes, Super: SuperPrototype.Super }))
continue
}
If an array, unshift the definitions onto our argument list, otherwise fall through to object processing.
case 'object': {
assert(codes != null, 'INVALID_ARGUMENT')
if (Array.isArray(codes)) {
vargs.unshift.apply(vargs, codes)
continue
}
if (SuperPrototype.is.has(codes)) {
const object = {}
object[codes.code] = SuperPrototype.prototypes[codes.code]
vargs.unshift(object)
continue
}
for (const code in codes) {
Duplicate declaration detection. TODO Better error.
assert(!duplicates.has(code), 'DUPLICATE_CODE', { code })
duplicates.add(code)
Use an existing code symbol from the super class if one exists, otherwise create a new symbol.
const object = function () {
switch (typeof codes[code]) {
case 'symbol': {
TODO This is new, what about it?
return { code, symbol: codes[code] }
}
case 'string': {
return { message: codes[code] }
}
case 'object': {
if (codes[code] == null) {
return {}
}
return codes[code]
}
default:
throw new Error
}
} ()
Goes here.
const prototype = function () {
TODO Kind of broken. What if the user uses a key other than
the existing code? Turn is
into map and use the existing code,
I guess.
if (SuperPrototype.is.has(object)) {
return object
}
if (object == null) {
return { code }
}
switch (typeof coalesce(object.code)) {
case 'symbol': {
Create an alias of the specified code
. When creating an alias,
specifying a symbol
is not allowed.
const code = Prototype.symbols.get(object.code)
assert(code != null, 'INVALID_CODE')
return combine(object, { code: code })
}
case 'string': {
If the code in the object matches the used as the key, that’s exactly the form we use for the prototype, otherwise we’re creating an alias.
if (object.code == code) {
return object
}
assert(Prototype.prototypes[object.code] != null, 'INVALID_CODE')
return object
}
case 'object': {
No alias, set the code to key from the set of aliases.
if (object.code == null) {
return combine(object, { code })
}
Define an alias extending on the given code or alias.
const superSuperCode = SuperPrototype.is.get(object.code)
if (superSuperCode != null) {
if (superSuperCode === object.code.code) {
assert(superSuperCode == code, 'INVALID_CODE')
return combine(object.code, object, { code: code })
} else {
return combine(object.code, object, { code: code, symbol: null })
}
}
const superCode = Prototype.is.get(object.code)
assert(superCode != null, 'INVALID_CODE')
Must be an alias.
return combine(object.code, object, { symbol: null, code: object.code.code })
}
default:
throw new Error('INVALID_CODE')
}
} ()
if (prototype.message == null) {
prototype.message = prototype.code
}
if (prototype.code == code) {
if (prototype.symbol == null) {
prototype.symbol = Symbol(code)
}
Create a property to hold the symbol in the class.
Object.defineProperty(Class, code, { value: prototype.symbol })
Our internal tracking of symbols.
Prototype.symbols.set(prototype.symbol, code)
Prototype.codes[code] = Object.defineProperties({}, {
code: { value: code, enumerable: true },
symbol: { value: prototype.symbol }
})
Prototype.Super.Codes[code] = prototype
} else {
assert(Prototype.prototypes[prototype.code], 'SUPER_PROTOTYPE_MISSING', {
definition: { superCode: prototype.code, code: code }
})
assert(prototype.symbol == null, 'SYMBOLS_NOT_ALLOWED', {
definition: { symbol: String(prototype.symbol), superCode: prototype.code, code: code }
})
Prototype.Super.Aliases[code] = prototype
}
Prototype.prototypes[code] = prototype
Prototype.is.set(prototype, code)
}
}
break
default:
throw new Interrupt.Error('INVALID_ARGUMENT')
}
}
return Class
}
Get just the message for the given Interrupt error.
If the error is not an Interrupt error return the message
property of
the error or null
if the property is not defined.
static message (error) {
const instance = Instances.get(error)
if (instance != null) {
return instance.message
}
return coalesce(error.message)
}
static parse (stack) {
const parser = new Interrupt.Parser
for (const line of stack.split('\n')) {
parser.push(line)
}
parser.end()
return parser._node
}
static dedup (error, keyify = (_, file, line) => [ file, line ]) {
const seen = {}
let id = 0
function treeify (parent, error) {
const [ file, line ] = location(error.stack)
const key = keyify(error, file, line)
if (error instanceof Interrupt) {
const node = {
parent: parent,
duplicated: false,
duplicates: new Set,
id: id++,
key: key,
stringified: Keyify.stringify(key),
context: {}, // **TODO** Legacy, dubious.
error: error,
errors: null
}
node.errors = error.errors.map((cause, index) => {
return treeify(node, cause)
})
return node
}
return {
parent: parent,
duplicated: false,
id: id++,
key: key,
duplicates: new Set,
stringified: Keyify.stringify(key),
error: error,
context: {},
errors: null
}
}
const leaves = {}
function leafify (node) {
if (node.errors != null && node.errors.length != 0) {
for (const cause of node.errors) {
leafify(cause)
}
} else {
const key = node.stringified
if (leaves[key] == null) {
leaves[key] = []
}
leaves[key].push(node)
}
}
function compare (left, right) {
if (left.stringified != right.stringified) {
return false
}
if (left.errors == null && right.errors == null) {
return true
}
if (left.errors.length != right.errors.length) {
return false
}
const errors = right.errors.slice(0)
CAUSES: for (const cause of left.errors) {
for (let i = 0; i < errors.length; i++) {
if (compare(cause, errors[i])) {
errors.splice(i, 1)
continue CAUSES
}
}
return false
}
return true
}
function mark (node) {
if (node.duplicated) {
return
}
if (node.errors != null && node.errors.length != 0) {
for (const cause of node.errors) {
mark(cause)
}
} else {
for (const other of leaves[node.stringified]) {
if (other === node) {
continue
}
const iterator = {
self: node,
other: other
}
const departure = {
self: null,
other: null
}
while (
iterator.self.parent != null &&
iterator.other.parent != null &&
iterator.self !== iterator.other &&
iterator.self.parent.stringified == iterator.other.parent.stringified
) {
departure.self = iterator.self
departure.other = iterator.other
iterator.self = iterator.self.parent
iterator.other = iterator.other.parent
}
if (departure.self != null) {
if (compare(departure.self, departure.other)) {
departure.self.duplicates.add(departure.other.id)
departure.other.duplicated = true
}
}
}
}
}
function format (node) {
if (node.errors == null || node.errors.length == 0) {
const repeated = node.duplicates.size + 1
const context = node.context
if (repeated != 1) {
context.repeated = repeated
}
const text = (node.error instanceof Error)
? coalesce(node.error.stack, node.error.message)
: node.error.toString()
if (Object.keys(context).length != 0) {
const contextualized = Interrupt.JSON.stringify(context)
return `${contextualized}\n\n${text}`
}
return text
}
const errors = []
for (const cause of node.errors) {
const formatted = format(cause)
const indented = formatted.replace(/^/gm, ' ')
errors.push(`\ncause:\n\n${indented}\n`)
}
const repeated = node.duplicates.size + 1
const instance = Instances.get(node.error)
const properties = { ...instance.options.properties }
if (repeated != 1) {
properties.repeated = repeated
}
const stack = node.error.stack.replace(/[\s\S]*^stack:$/m, 'stack:')
if (Object.keys(properties).length != 0) {
const contextualized = Interrupt.JSON.stringify(properties)
return `${node.error.name}: ${instance.message}\n\n${properties}\n\n${errors.join('')}\n\n${stack}`
}
return `${node.error.name}: ${instance.message}\n\n${errors.join('')}\n\n${stack}`
}
/*
function print (indent, extract, node) {
console.log(`${indent}${util.inspect(extract(node), { depth: null, breakLength: Infinity })}`)
if (node.errors != null && node.errors.length != 0) {
for (const cause of node.errors) {
print(` ${indent}`, extract, cause)
}
}
}*/
function trim (node, parent) {
if (parent != null) {
parent.errors = parent.errors.filter(sibling => ! node.duplicates.has(sibling.id))
}
if (node.errors != null && node.errors.length != 0) {
let i = 0
while (node.errors.length != i) {
trim(node.errors[i++], node)
}
}
}
const tree = treeify(null, error)
leafify(tree)
mark(tree)
print(‘’, $ => [ $.id, 1 + $.duplicates.size ], tree)
trim(tree, null)
print(‘’, $ => [ $.id, 1 + $.duplicates.size ], tree)
return format(tree)
}
}
A valid JavaScript identifier. Taken from this gist and used as a string for inclusion into other regexen.
const identifier = require('./identifier.json')
const RE = {
identifier: new RegExp(`^${identifier}$`),
exceptionStart: new RegExp(`^(\\s*)(${identifier}(?:\.${identifier})*)(:)\\s([\\s\\S]*)`, 'm')
}
const unstacker = require('stacktrace-parser')
const Dedents = new Map
function dedent (line, depth, position) {
if (depth == 0 || line.length == 0) {
return line
}
const dedenter = Dedents.get(depth)
if (dedenter == null) {
Dedents.set(depth, new RegExp(`^ {${depth}}(.*)$`))
return dedent(line, depth, position)
}
const $ = dedenter.exec(line)
assert($ != null, 'PARSE_ERROR', position)
return $[1]
}
module.exports = Interrupt