@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
920 lines (806 loc) • 21.9 kB
JavaScript
import InternalSymbols from './symbols.js'
import { createHook } from '../async/hooks.js'
import { murmur3 } from '../crypto.js'
import { Buffer } from '../buffer.js'
import path from '../path.js'
let isAsyncContext = false
const asyncContexts = new Set([
'Promise',
'Timeout',
'Interval',
'Immediate',
'Microtask'
])
const hook = createHook({
before (asyncId, type) {
if (asyncContexts.has(type)) {
isAsyncContext = true
}
},
after (asyncId, type) {
if (asyncContexts.has(type)) {
isAsyncContext = false
}
}
})
hook.enable()
/**
* @typedef {{
* sourceURL: string | null,
* symbol: string,
* column: number | undefined,
* line: number | undefined,
* native: boolean
* }} ParsedStackFrame
*/
/**
* A container for location data related to a `StackFrame`
*/
export class StackFrameLocation {
/**
* Creates a `StackFrameLocation` from JSON input.
* @param {object=} json
* @return {StackFrameLocation}
*/
static from (json) {
const location = new this()
if (Number.isFinite(json?.lineNumber)) {
location.lineNumber = json.lineNumber
}
if (Number.isFinite(json?.columnNumber)) {
location.columnNumber = json.columnNumber
}
if (json?.sourceURL && URL.canParse(json.sourceURL)) {
location.sourceURL = new URL(json.sourceURL).href
}
if (json?.isNative === true) {
location.isNative = true
}
return location
}
/**
* The line number of the location of the stack frame, if available.
* @type {number | undefined}
*/
lineNumber
/**
* The column number of the location of the stack frame, if available.
* @type {number | undefined}
*/
columnNumber
/**
* The source URL of the location of the stack frame, if available. This value
* may be `null`.
* @type {string?}
*/
sourceURL = null
/**
* `true` if the stack frame location is in native location, otherwise
* this value `false` (default).
* @type
*/
isNative = false
/**
* Converts this `StackFrameLocation` to a JSON object.
* @ignore
* @return {{
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* }}
*/
toJSON () {
return {
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
sourceURL: this.sourceURL,
isNative: this.isNative
}
}
/**
* Serializes this `StackFrameLocation`, suitable for `postMessage()` transfers.
* @ignore
* @return {{
* __type__: 'StackFrameLocation',
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* }}
*/
[InternalSymbols.serialize] () {
return { __type__: 'StackFrameLocation', ...this.toJSON() }
}
}
/**
* A stack frame container related to a `CallSite`.
*/
export class StackFrame {
/**
* Parses a raw stack frame string into structured data.
* @param {string} rawStackFrame
* @return {ParsedStackFrame}
*/
static parse (rawStackFrame) {
const parsed = {
sourceURL: null,
symbol: '<anonymous>',
column: undefined,
line: undefined
}
const parts = rawStackFrame.split('@')
const symbol = parts.shift()
const location = parts.shift()
if (symbol) {
parsed.symbol = symbol
}
if (location === '[native code]') {
parsed.native = true
} else if (location && URL.canParse(location)) {
const url = new URL(location)
const [pathname, lineno, columnno] = url.pathname.split(':')
const line = parseInt(lineno)
const column = parseInt(columnno)
if (Number.isFinite(line)) {
parsed.line = line
}
if (Number.isFinite(column)) {
parsed.column = column
}
parsed.sourceURL = new URL(pathname + url.search, url.origin).href
}
return parsed
}
/**
* Creates a new `StackFrame` from an `Error` and raw stack frame
* source `rawStackFrame`.
* @param {Error} error
* @param {string} rawStackFrame
* @return {StackFrame}
*/
static from (error, rawStackFrame) {
const parsed = this.parse(rawStackFrame)
return new this(error, parsed, rawStackFrame)
}
/**
* The stack frame location data.
* @type {StackFrameLocation}
*/
location = new StackFrameLocation()
/**
* The `Error` associated with this `StackFrame` instance.
* @type {Error?}
*/
error = null
/**
* The name of the function where the stack frame is located.
* @type {string?}
*/
symbol = null
/**
* The raw stack frame source string.
* @type {string?}
*/
source = null
/**
* `StackFrame` class constructor.
* @param {Error} error
* @param {ParsedStackFrame=} [frame]
* @param {string=} [source]
*/
constructor (error, frame = null, source = null) {
if (error instanceof Error) {
this.error = error
}
if (typeof source === 'string') {
this.source = source
}
if (Number.isFinite(frame?.line)) {
this.location.lineNumber = frame.line
}
if (Number.isFinite(frame?.column)) {
this.location.columnNumber = frame.column
}
if (typeof frame?.sourceURL === 'string' && URL.canParse(frame.sourceURL)) {
this.location.sourceURL = frame.sourceURL
}
if (typeof frame?.symbol === 'string') {
this.symbol = frame.symbol
}
if (frame?.native === true) {
this.location.isNative = true
}
}
/**
* Converts this `StackFrameLocation` to a JSON object.
* @ignore
* @return {{
* location: {
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }}
*/
toJSON () {
return {
location: this.location.toJSON(),
isNative: this.isNative,
symbol: this.symbol,
source: this.source,
error: this.error === null
? null
: {
message: this.error.message ?? '',
name: this.error.name ?? '',
stack: String(this.error[CallSite.StackSourceSymbol] ?? this.error.stack ?? '')
}
}
}
/**
* Serializes this `StackFrame`, suitable for `postMessage()` transfers.
* @ignore
* @return {{
* __type__: 'StackFrame',
* location: {
* __type__: 'StackFrameLocation',
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }}
*/
[InternalSymbols.serialize] () {
return {
__type__: 'StackFrame',
...this.toJSON(),
location: this.location[InternalSymbols.serialize]()
}
}
}
// private symbol for `CallSiteList` previous reference
const $previous = Symbol('previous')
/**
* A v8 compatible interface and container for call site information.
*/
export class CallSite {
/**
* An internal symbol used to refer to the index of a promise in
* `Promise.all` or `Promise.any` function call site.
* @ignore
* @type {symbol}
*/
static PromiseElementIndexSymbol = Symbol.for('socket.runtime.CallSite.PromiseElementIndex')
/**
* An internal symbol used to indicate that a call site is in a `Promise.all`
* function call.
* @ignore
* @type {symbol}
*/
static PromiseAllSymbol = Symbol.for('socket.runtime.CallSite.PromiseAll')
/**
* An internal symbol used to indicate that a call site is in a `Promise.any`
* function call.
* @ignore
* @type {symbol}
*/
static PromiseAnySymbol = Symbol.for('socket.runtime.CallSite.PromiseAny')
/**
* An internal source symbol used to store the original `Error` stack source.
* @ignore
* @type {symbol}
*/
static StackSourceSymbol = Symbol.for('socket.runtime.CallSite.StackSource')
#error = null
#frame = null
#previous = null
/**
* `CallSite` class constructor
* @param {Error} error
* @param {string} rawStackFrame
* @param {CallSite=} previous
*/
constructor (error, rawStackFrame, previous = null) {
this.#error = error
this.#frame = StackFrame.from(error, rawStackFrame)
if (previous !== null && previous instanceof CallSite) {
this.#previous = previous
}
}
/**
* Private accessor to "friend class" `CallSiteList`.
* @ignore
*/
get [$previous] () { return this.#previous }
set [$previous] (previous) {
if (previous === null || previous instanceof CallSite) {
this.#previous = previous
}
}
/**
* The `Error` associated with the call site.
* @type {Error}
*/
get error () {
return this.#error
}
/**
* The previous `CallSite` instance, if available.
* @type {CallSite?}
*/
get previous () {
return this.#previous
}
/**
* A reference to the `StackFrame` data.
* @type {StackFrame}
*/
get frame () {
return this.#frame
}
/**
* This function _ALWAYS__ returns `globalThis` as `this` cannot be determined.
* @return {object}
*/
getThis () {
// not supported
return globalThis
}
/**
* This function _ALWAYS__ returns `null` as the type name of `this`
* cannot be determined.
* @return {null}
*/
getTypeName () {
// not supported
return null
}
/**
* This function _ALWAYS__ returns `undefined` as the current function
* reference cannot be determined.
* @return {undefined}
*/
getFunction () {
// not supported
return undefined
}
/**
* Returns the name of the function in at the call site, if available.
* @return {string|undefined}
*/
getFunctionName () {
const symbol = this.#frame.symbol
if (symbol === 'global code' || symbol === 'module code' || symbol === 'eval code') {
return undefined
}
return symbol
}
/**
* An alias to `getFunctionName()
* @return {string}
*/
getMethodName () {
return this.getFunctionName()
}
/**
* Get the filename of the call site location, if available, otherwise this
* function returns 'unknown location'.
* @return {string}
*/
getFileName () {
if (this.#frame.location.sourceURL) {
const root = new URL('../../', import.meta.url || globalThis.location.href).pathname
let filename = new URL(this.#frame.location.sourceURL).pathname.replace(root, '')
if (/\/socket\//.test(filename)) {
filename = filename.replace('socket/', 'socket:').replace(/.js$/, '')
return filename
}
return path.basename(new URL(this.#frame.location.sourceURL).pathname)
}
return 'unknown location'
}
/**
* Returns the location source URL defaulting to the global location.
* @return {string}
*/
getScriptNameOrSourceURL () {
const url = new URL(this.#frame.location.sourceURL ?? globalThis.location.href)
let filename = url.pathname.replace(url.pathname, '')
if (/\/socket\//.test(filename)) {
filename = filename.replace('socket/', 'socket:').replace(/.js$/, '')
return filename
}
return url.href
}
/**
* Returns a hash value of the source URL return by `getScriptNameOrSourceURL()`
* @return {string}
*/
getScriptHash () {
return Buffer.from(String(murmur3(this.getScriptNameOrSourceURL()))).toString('hex')
}
/**
* Returns the line number of the call site location.
* This value may be `undefined`.
* @return {number|undefined}
*/
getLineNumber () {
return this.#frame.lineNumber
}
/**
* @ignore
* @return {number}
*/
getPosition () {
return 0
}
/**
* Attempts to get an "enclosing" line number, potentially the previous
* line number of the call site
* @param {number|undefined}
*/
getEnclosingLineNumber () {
if (this.#previous) {
const previousSourceURL = this.#previous.getScriptNameOrSourceURL()
if (previousSourceURL && previousSourceURL === this.getScriptNameOrSourceURL()) {
return this.#previous.getLineNumber()
}
}
}
/**
* Returns the column number of the call site location.
* This value may be `undefined`.
* @return {number|undefined}
*/
getColumnNumber () {
return this.#frame.columnNumber
}
/**
* Attempts to get an "enclosing" column number, potentially the previous
* line number of the call site
* @param {number|undefined}
*/
getEnclosingColumnNumber () {
if (this.#previous) {
const previousSourceURL = this.#previous.getScriptNameOrSourceURL()
if (previousSourceURL && previousSourceURL === this.getScriptNameOrSourceURL()) {
return this.#previous.getColumnNumber()
}
}
}
/**
* Gets the origin of where `eval()` was called if this call site function
* originated from a call to `eval()`. This function may return `undefined`.
* @return {string|undefined}
*/
getEvalOrigin () {
let current = this
while (current) {
if (current.frame.symbol === 'eval' && current.frame.location.isNative) {
const previous = current.previous
if (previous) {
return previous.location.sourceURL
}
}
current = this.previous
}
}
/**
* This function _ALWAYS__ returns `false` as `this` cannot be determined so
* "top level" detection is not possible.
* @return {boolean}
*/
isTopLevel () {
return false
}
/**
* Returns `true` if this call site originated from a call to `eval()`.
* @return {boolean}
*/
isEval () {
let current = this
while (current) {
if (current.frame.symbol === 'eval' || current.frame.symbol === 'eval code') {
return true
}
current = this.previous
}
return false
}
/**
* Returns `true` if the call site is in a native location, otherwise `false`.
* @return {boolean}
*/
isNative () {
return this.#frame.location.isNative
}
/**
* This function _ALWAYS_ returns `false` as constructor detection
* is not possible.
* @return {boolean}
*/
isConstructor () {
// not supported
return false
}
/**
* Returns `true` if the call site is in async context, otherwise `false`.
* @return {boolean}
*/
isAsync () {
return isAsyncContext
}
/**
* Returns `true` if the call site is in a `Promise.all()` function call,
* otherwise `false.
* @return {boolean}
*/
isPromiseAll () {
return this.#error[CallSite.PromiseAllSymbol] === true
}
/**
* Gets the index of the promise element that was followed in a
* `Promise.all()` or `Promise.any()` function call. If not available, then
* this function returns `null`.
* @return {number|null}
*/
getPromiseIndex () {
return this.#error[CallSite.PromiseElementIndexSymbol] ?? null
}
/**
* Converts this call site to a string.
* @return {string}
*/
toString () {
const { symbol, location } = this.#frame
const output = [symbol]
if (location.sourceURL) {
const pathname = new URL(location.sourceURL).pathname
const root = new URL('../../', import.meta.url || globalThis.location.href).pathname
let filename = pathname.replace(root, '')
if (/\/?socket\//.test(filename)) {
filename = filename.replace('socket/', 'socket:').replace(/.js$/, '')
}
if (location.lineNumber && location.columnNumber) {
output.push(`(${filename}:${location.lineNumber}:${location.columnNumber})`)
} else if (location.lineNumber) {
output.push(`(${filename}:${location.lineNumber})`)
} else {
output.push(`${filename}`)
}
}
return output.filter(Boolean).join(' ')
}
/**
* Converts this `CallSite` to a JSON object.
* @ignore
* @return {{
* frame: {
* location: {
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }
* }}
*/
toJSON () {
return {
frame: this.#frame.toJSON()
}
}
/**
* Serializes this `CallSite`, suitable for `postMessage()` transfers.
* @ignore
* @return {{
* __type__: 'CallSite',
* frame: {
* __type__: 'StackFrame',
* location: {
* __type__: 'StackFrameLocation',
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }
* }}
*/
[InternalSymbols.serialize] () {
return {
__type__: 'CallSite',
frame: this.#frame[InternalSymbols.serialize]()
}
}
}
/**
* An array based list container for `CallSite` instances.
*/
export class CallSiteList extends Array {
/**
* Creates a `CallSiteList` instance from `Error` input.
* @param {Error} error
* @param {string} source
* @return {CallSiteList}
*/
static from (error, source) {
const callsites = new this(
error,
source.split('\n').slice(0, Error.stackTraceLimit)
)
for (const source of callsites.sources.reverse()) {
callsites.unshift(new CallSite(error, source, callsites[0]))
}
return callsites
}
#error = null
#sources = []
/**
* `CallSiteList` class constructor.
* @param {Error} error
* @param {string[]=} [sources]
*/
constructor (error, sources = null) {
super()
this.#error = error
if (Array.isArray(sources)) {
this.#sources = Array.from(sources) // copy
} else if (typeof error.stack === 'string') {
this.#sources = error.stack.split('\n')
} else if (typeof error[CallSiteList.StackSourceSymbol] === 'string') {
this.#sources = error[CallSiteList.StackSourceSymbol].split('\n')
}
}
/**
* A reference to the `Error` for this `CallSiteList` instance.
* @type {Error}
*/
get error () {
return this.#error
}
/**
* An array of stack frame source strings.
* @type {string[]}
*/
get sources () {
return this.#sources
}
/**
* The original stack string derived from the sources.
* @type {string}
*/
get stack () {
return this.#sources.join('\n')
}
/**
* Adds `CallSite` instances to the top of the list, linking previous
* instances to the next one.
* @param {...CallSite} callsites
* @return {number}
*/
unshift (...callsites) {
for (const callsite of callsites) {
if (callsite instanceof CallSite) {
callsite[$previous] = this[0]
super.unshift(callsite)
}
}
return this.length
}
/**
* A no-op function as `CallSite` instances cannot be added to the end
* of the list.
* @return {number}
*/
push () {
// no-op
return this.length
}
/**
* Pops a `CallSite` off the end of the list.
* @return {CallSite|undefined}
*/
pop () {
const value = super.pop()
if (this.length >= 1) {
this[this.length - 1][$previous] = null
}
return value
}
/**
* Converts the `CallSiteList` to a string combining the error name, message,
* and callsite stack information into a friendly string.
* @return {string}
*/
toString () {
const stack = this.map((callsite) => ` at ${callsite.toString()}`)
if (this.#error.name && this.#error.message) {
return [`${this.#error.name}: ${this.#error.message}`].concat(stack).join('\n')
} else if (this.#error.name) {
return [this.#error.name].concat(stack).join('\n')
} else {
return stack.join('\n')
}
}
/**
* Converts this `CallSiteList` to a JSON object.
* @return {{
* frame: {
* location: {
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }
* }[]}
*/
toJSON () {
return Array.from(this.map((callsite) => callsite.toJSON()))
}
/**
* Serializes this `CallSiteList`, suitable for `postMessage()` transfers.
* @ignore
* @return {Array<{
* __type__: 'CallSite',
* frame: {
* __type__: 'StackFrame',
* location: {
* __type__: 'StackFrameLocation',
* lineNumber: number | undefined,
* columnNumber: number | undefined,
* sourceURL: string | null,
* isNative: boolean
* },
* isNative: boolean,
* symbol: string | null,
* source: string | null,
* error: { message: string, name: string, stack: string } | null
* }
* }>}
*/
[InternalSymbols.serialize] () {
return this.toJSON()
}
/**
* @ignore
*/
[Symbol.for('socket.runtime.util.inspect.custom')] () {
return this.toString()
}
}
/**
* Creates an ordered and link array of `CallSite` instances from a
* given `Error`.
* @param {Error} error
* @param {string} source
* @return {CallSite[]}
*/
export function createCallSites (error, source) {
return CallSiteList.from(error, source)
}
export default CallSite