@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.
2,002 lines (1,716 loc) • 52 kB
JavaScript
/**
* @module ipc
*
* This is a low-level API that you don't need unless you are implementing
* a library on top of Socket runtime. A Socket app has one or more processes.
*
* When you need to send a message to another window or to the backend, you
* should use the `application` module to get a reference to the window and
* use the `send` method to send a message.
*
* - The `Render` process, is the UI where the HTML, CSS, and JS are run.
* - The `Bridge` process, is the thin layer of code that manages everything.
* - The `Main` process, is for apps that need to run heavier compute jobs. And
* unlike electron it's optional.
*
* The Bridge process manages the Render and Main process, it may also broker
* data between them.
*
* The Binding process uses standard input and output as a way to communicate.
* Data written to the write-end of the pipe is buffered by the OS until it is
* read from the read-end of the pipe.
*
* The IPC protocol uses a simple URI-like scheme. Data is passed as
* ArrayBuffers.
*
* ```
* ipc://command?key1=value1&key2=value2...
* ```
*
* Example usage:
* ```js
* import { send } from 'socket:ipc'
* ```
*/
/* global webkit, chrome, external, reportError */
import {
AbortError,
InternalError,
ErrnoError,
TimeoutError
} from './errors.js'
import {
isArrayLike,
isBufferLike,
isPlainObject,
format,
parseHeaders,
parseJSON
} from './util.js'
import { URL, protocols } from './url.js'
import * as errors from './errors.js'
import { Buffer } from './buffer.js'
import { rand64 } from './crypto.js'
import bookmarks from './fs/bookmarks.js'
import serialize from './internal/serialize.js'
import location from './location.js'
import gc from './gc.js'
let nextSeq = 1
const cache = {}
function initializeXHRIntercept () {
if (typeof globalThis.XMLHttpRequest !== 'function') return
const patched = Symbol.for('socket.runtime.ipc.XMLHttpRequest.patched')
if (globalThis.XMLHttpRequest.prototype[patched]) {
return
}
globalThis.XMLHttpRequest.prototype[patched] = true
const { send, open } = globalThis.XMLHttpRequest.prototype
const encoder = new TextEncoder()
Object.assign(globalThis.XMLHttpRequest.prototype, {
open (method, url, ...args) {
try {
this.readyState = globalThis.XMLHttpRequest.OPENED
} catch (_) {}
this.method = method
this.url = new URL(url, location.origin)
this.seq = this.url.searchParams.get('seq')
return open.call(this, method, url, ...args)
},
async send (body) {
let { method, seq, url } = this
if (
url?.protocol && (
url.protocol === 'ipc:' ||
protocols.handlers.has(url.protocol.slice(0, -1))
)
) {
if (
/put|post|patch/i.test(method) &&
typeof body !== 'undefined'
) {
if (typeof body === 'string') {
body = encoder.encode(body)
}
if (/android/i.test(primordials.platform)) {
if (!seq) {
seq = 'R' + Math.random().toString().slice(2, 8) + 'X'
}
this.setRequestHeader('runtime-xhr-seq', seq)
await postMessage(`ipc://buffer.map?seq=${seq}`, body)
if (!globalThis.window && globalThis.self) {
await new Promise((resolve) => setTimeout(resolve, 200))
}
body = null
}
}
}
return send.call(this, body)
}
})
}
function getErrorClass (type, fallback = null) {
if (typeof globalThis !== 'undefined' && typeof globalThis[type] === 'function') {
// eslint-disable-next-line
return new Function(`return function ${type} () {
const object = Object.create(globalThis['${type}']?.prototype ?? {}, {
message: { enumerable: true, configurable: true, writable: true, value: null },
code: { value: null }
})
return object
}
`)()
}
if (typeof errors[type] === 'function') {
return errors[type]
}
return fallback || Error
}
function getRequestResponseText (request) {
try {
// can throw `InvalidStateError` error
return request?.responseText
} catch (_) {}
return null
}
function getRequestResponse (request, options) {
if (!request) return null
let { responseType } = request
const expectedResponseType = options?.responseType ?? responseType
const desiredResponseType = options?.desiredResponseType ?? expectedResponseType
const headers = Headers.from(request)
let response = null
if (expectedResponseType && responseType !== expectedResponseType) {
return null
}
if (!responseType) {
responseType = desiredResponseType
}
if (!responseType || responseType === 'text') {
// The `responseText` could be an accessor which could throw an
// `InvalidStateError` error when accessed when `responseType` is not empty
// empty or 'text'
// - see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText#exceptions
const responseText = getRequestResponseText(request)
if (responseText) {
response = responseText
// maybe json for unspecified response types
if (!responseType) {
const json = parseJSON(response)
if (json) {
response = json
}
}
}
}
if (responseType === 'json') {
response = parseJSON(request.response)
}
if (responseType === 'arraybuffer') {
if (
request.response instanceof ArrayBuffer ||
typeof request.response === 'string' ||
isBufferLike(request.response)
) {
const contentLength = parseInt(headers.get('content-length'))
response = Buffer.from(request.response)
if (contentLength) {
response = response.slice(0, contentLength)
}
}
// maybe json in buffered response
const json = parseJSON(response)
if ((isPlainObject(json?.data) && json?.source) || isPlainObject(json?.err)) {
response = json
}
}
// try using `statusText` for stubbed response
if (!response) {
// defaults:
// `400...499: errors.NotAllowedError`
// `500...599: errors.InternalError`
const statusCodeToErrorCode = {
400: errors.BadRequestError,
403: errors.InvalidAccessError,
404: errors.NotFoundError,
408: errors.TimeoutError,
501: errors.NotSupportedError,
502: errors.NetworkError,
504: errors.TimeoutError
}
const { status, responseURL, statusText } = request
// @ts-ignore
const message = Message.from(responseURL)
const source = message.command
if (status >= 100 && status < 400) {
const data = { status: statusText }
return { source, data }
} else if (status >= 400 && status < 499) {
const ErrorType = statusCodeToErrorCode[status] || errors.NotAllowedError
const err = new ErrorType(statusText || status)
err.url = responseURL
return { source, err }
} else if (status >= 500 && status < 599) {
const ErrorType = statusCodeToErrorCode[status] || errors.InternalError
const err = new ErrorType(statusText || status)
err.url = responseURL
return { source, err }
}
}
return response
}
function getFileSystemBookmarkName (options) {
const names = [
options.params.get('src'),
options.params.get('path'),
options.params.get('value')
]
return names.find(Boolean) ?? null
}
function isFileSystemBookmark (options) {
const names = [
options.params.get('src'),
options.params.get('path'),
options.params.get('value')
].filter(Boolean)
if (names.some((name) => bookmarks.temporary.has(name))) {
return true
}
for (const [, fd] of bookmarks.temporary.entries()) {
if (fd === options.params.get('id') || fd === options.params.get('fd')) {
return true
}
}
return false
}
export function maybeMakeError (error, caller) {
const errors = {
AbortError: getErrorClass('AbortError'),
AggregateError: getErrorClass('AggregateError'),
EncodingError: getErrorClass('EncodingError'),
GeolocationPositionError: getErrorClass('GeolocationPositionError'),
IndexSizeError: getErrorClass('IndexSizeError'),
InternalError,
DOMException: getErrorClass('DOMContentLoaded'),
InvalidAccessError: getErrorClass('InvalidAccessError'),
NetworkError: getErrorClass('NetworkError'),
NotAllowedError: getErrorClass('NotAllowedError'),
NotFoundError: getErrorClass('NotFoundError'),
NotSupportedError: getErrorClass('NotSupportedError'),
OperationError: getErrorClass('OperationError'),
RangeError: getErrorClass('RangeError'),
TimeoutError,
TypeError: getErrorClass('TypeError'),
URIError: getErrorClass('URIError')
}
if (!error) {
return null
}
if (error instanceof Error) {
return error
}
error = { ...error }
const type = error.type || 'Error'
const code = error.code
let err = null
delete error.type
if (code && ErrnoError.errno?.constants && -code in ErrnoError.errno.strings) {
err = new ErrnoError(-code)
} else if (type in errors) {
err = new errors[type](error.message || '', error.code)
} else {
for (const E of Object.values(errors)) {
if ((E.code && type === E.code) || (code && code === E.code)) {
err = new E(error.message || '')
break
}
}
}
if (!err) {
err = new Error(error.message || '')
}
// assign extra data to `err` like an error `code`
for (const key in error) {
try {
err[key] = error[key]
} catch (_) {}
}
if (
// @ts-ignore
typeof Error.captureStackTrace === 'function' &&
typeof caller === 'function'
) {
// @ts-ignore
Error.captureStackTrace(err, caller)
}
return err
}
/**
* Represents an OK IPC status.
* @ignore
*/
export const OK = 0
/**
* Represents an ERROR IPC status.
* @ignore
*/
export const ERROR = 1
/**
* Timeout in milliseconds for IPC requests.
* @ignore
*/
export const TIMEOUT = 32 * 1000
/**
* Symbol for the `ipc.debug.enabled` property
* @ignore
*/
export const kDebugEnabled = Symbol.for('socket.runtime.ipc.debug.enabled')
/**
* Parses `seq` as integer value
* @param {string|number} seq
* @param {object=} [options]
* @param {boolean} [options.bigint = false]
* @ignore
*/
export function parseSeq (seq, options) {
const value = String(seq).replace(/^R/i, '').replace(/n$/, '')
return options?.bigint === true ? BigInt(value) : parseInt(value)
}
/**
* If `debug.enabled === true`, then debug output will be printed to console.
* @param {(boolean)} [enable]
* @return {boolean}
* @ignore
*/
export function debug (enable) {
if (enable === true) {
debug.enabled = true
} else if (enable === false) {
debug.enabled = false
}
return debug.enabled
}
/**
* @type {boolean}
*/
debug.enabled = false
Object.defineProperty(debug, 'enabled', {
enumerable: false,
set (value) {
debug[kDebugEnabled] = Boolean(value)
},
get () {
if (debug[kDebugEnabled] === undefined) {
return Boolean(globalThis?.__args?.debug)
}
return debug[kDebugEnabled]
}
})
if (debug.enabled && globalThis.__args?.env?.SOCKET_DEBUG_IPC) {
// eslint-disable-next-line
debug.log = (...args) => void globalThis.console?.log?.(...args)
} else {
debug.log = () => undefined
}
/**
* @ignore
*/
export class Headers extends globalThis.Headers {
/**
* @ignore
*/
static from (input) {
if (input?.headers && typeof input.headers === 'object') {
input = input.headers
}
if (Array.isArray(input) && !Array.isArray(input[0])) {
input = input.join('\n')
} else if (typeof input?.entries === 'function') {
// @ts-ignore
return new this(Array.from(input.entries()))
} else if (isPlainObject(input) || isArrayLike(input)) {
return new this(input)
} else if (typeof input?.getAllResponseHeaders === 'function') {
input = input.getAllResponseHeaders()
} else if (typeof input?.headers?.entries === 'function') {
// @ts-ignore
return new this(Array.from(input.headers.entries()))
}
// @ts-ignore
return new this(parseHeaders(String(input)))
}
/**
* @ignore
*/
get length () {
return Array.from(this.entries()).length
}
/**
* @ignore
*/
toJSON () {
return Object.fromEntries(this.entries())
}
}
/**
* Find transfers for an in worker global `postMessage`
* that is proxied to the main thread.
* @ignore
*/
export function findMessageTransfers (transfers, object) {
if (ArrayBuffer.isView(object)) {
add(object.buffer)
} else if (object instanceof ArrayBuffer) {
add(object)
} else if (Array.isArray(object)) {
for (const value of object) {
findMessageTransfers(transfers, value)
}
} else if (object && typeof object === 'object') {
for (const key in object) {
findMessageTransfers(transfers, object[key])
}
}
return transfers
function add (value) {
if (!transfers.includes(value)) {
transfers.push(value)
}
}
}
/**
* @ignore
*/
export function postMessage (message, ...args) {
if (globalThis?.webkit?.messageHandlers?.external?.postMessage) {
// @ts-ignore
return webkit.messageHandlers.external.postMessage(message, ...args)
} else if (globalThis?.chrome?.webview?.postMessage) {
// @ts-ignore
return chrome.webview.postMessage(message, ...args)
// @ts-ignore
} else if (globalThis?.external?.postMessage) {
// @ts-ignore
return external.postMessage(message, ...args)
} else if (globalThis.postMessage) {
const transfer = []
findMessageTransfers(transfer, args)
// worker
if (globalThis.self && !globalThis.window) {
return globalThis?.postMessage({
__runtime_worker_ipc_request: {
message,
bytes: args[0] ?? null
}
}, { transfer })
} else {
return globalThis.top.postMessage(message, ...args)
}
}
throw new TypeError(
'Could not determine UserMessageHandler.postMessage in globalThis'
)
}
/**
* A container for a IPC message based on a `ipc://` URI scheme.
* @ignore
*/
export class Message extends URL {
/**
* The expected protocol for an IPC message.
* @ignore
*/
static get PROTOCOL () {
return 'ipc:'
}
/**
* Creates a `Message` instance from a variety of input.
* @param {string|URL|Message|Buffer|object} input
* @param {(object|string|URLSearchParams)=} [params]
* @param {(ArrayBuffer|Uint8Array|string)?} [bytes]
* @return {Message}
* @ignore
*/
static from (input, params, bytes = null) {
const protocol = this.PROTOCOL
if (isBufferLike(input)) {
input = Buffer.from(input).toString()
}
if (isBufferLike(params)) {
bytes = Buffer.from(params)
params = null
} else if (bytes) {
bytes = Buffer.from(bytes)
}
if (input instanceof Message) {
const message = new this(String(input))
if (typeof params === 'object') {
const entries = params.entries ? params.entries() : Object.entries(params)
for (const [key, value] of entries) {
message.set(key, value)
}
}
return message
} else if (isPlainObject(input)) {
return new this(
`${input.protocol || protocol}//${input.command}?${new URLSearchParams({ ...input.params, ...params })}`,
bytes
)
}
if (typeof input === 'string' && params) {
return new this(`${protocol}//${input}?${new URLSearchParams(params)}`, bytes)
}
// coerce input into a string
const string = String(input)
if (string.startsWith(`${protocol}//`)) {
return new this(string, bytes)
}
return new this(`${protocol}//${input}`, bytes)
}
/**
* Predicate to determine if `input` is valid for constructing
* a new `Message` instance.
* @param {string|URL|Message|Buffer|object} input
* @return {boolean}
* @ignore
*/
static isValidInput (input) {
const protocol = this.PROTOCOL
const string = String(input)
return (
string.startsWith(`${protocol}//`) &&
string.length > protocol.length + 2
)
}
/**
* @type {Uint8Array?}
* @ignore
*/
bytes = null
/**
* `Message` class constructor.
* @protected
* @param {string|URL} input
* @param {(object|Uint8Array)?} [bytes]
* @ignore
*/
constructor (input, bytes = null) {
super(input)
if (this.protocol !== Message.PROTOCOL) {
throw new TypeError(format(
'Invalid protocol in input. Expected \'%s\' but got \'%s\'',
Message.PROTOCOL, this.protocol
))
}
this.bytes = bytes || null
const properties = Object.getOwnPropertyDescriptors(Message.prototype)
Object.defineProperties(this, {
command: { ...properties.command, enumerable: true },
seq: { ...properties.seq, enumerable: true },
index: { ...properties.index, enumerable: true },
id: { ...properties.id, enumerable: true },
value: { ...properties.value, enumerable: true },
params: { ...properties.params, enumerable: true }
})
}
/**
* Computed IPC message name.
* @type {string}
* @ignore
*/
get command () {
// TODO(jwerle): issue deprecation notice
return this.name
}
/**
* Computed IPC message name.
* @type {string}
* @ignore
*/
get name () {
return this.hostname || this.host || this.pathname.slice(2)
}
/**
* Computed `id` value for the command.
* @type {string}
* @ignore
*/
get id () {
return this.searchParams.get('id') ?? null
}
/**
* Computed `seq` (sequence) value for the command.
* @type {string}
* @ignore
*/
get seq () {
return this.has('seq') ? this.get('seq') : null
}
/**
* Computed message value potentially given in message parameters.
* This value is automatically decoded, but not treated as JSON.
* @type {string}
* @ignore
*/
get value () {
return this.get('value') ?? null
}
/**
* Computed `index` value for the command potentially referring to
* the window index the command is scoped to or originating from. If not
* specified in the message parameters, then this value defaults to `-1`.
* @type {number}
* @ignore
*/
get index () {
const index = this.get('index')
if (index !== undefined) {
const value = parseInt(index)
if (Number.isFinite(value)) {
return value
}
}
return -1
}
/**
* Computed value parsed as JSON. This value is `null` if the value is not present
* or it is invalid JSON.
* @type {object?}
* @ignore
*/
get json () {
return parseJSON(this.value)
}
/**
* Computed readonly object of message parameters.
* @type {object}
* @ignore
*/
get params () {
return Object.fromEntries(this.entries())
}
/**
* Gets unparsed message parameters.
* @type {Array<Array<string>>}
* @ignore
*/
get rawParams () {
return Object.fromEntries(this.searchParams.entries())
}
/**
* Returns computed parameters as entries
* @return {Array<Array<any>>}
* @ignore
*/
entries () {
return Array.from(this.searchParams.entries()).map(([key, value]) => {
return [key, parseJSON(value) || value]
})
}
/**
* Set a parameter `value` by `key`.
* @param {string} key
* @param {any} value
* @ignore
*/
set (key, value) {
if (value && typeof value === 'object') {
value = JSON.stringify(value)
}
return this.searchParams.set(key, value)
}
/**
* Get a parameter value by `key`.
* @param {string} key
* @param {any=} [defaultValue]
* @return {any}
* @ignore
*/
get (key, defaultValue = undefined) {
if (!this.has(key)) {
return defaultValue
}
const value = this.searchParams.get(key)
const json = value && parseJSON(value)
if (json === null || json === undefined) {
return value
}
return json
}
/**
* Delete a parameter by `key`.
* @param {string} key
* @return {boolean}
* @ignore
*/
delete (key) {
if (this.has(key)) {
return this.searchParams.delete(key)
}
return false
}
/**
* Computed parameter keys.
* @return {Array<string>}
* @ignore
*/
keys () {
return Array.from(this.searchParams.keys())
}
/**
* Computed parameter values.
* @return {Array<any>}
* @ignore
*/
values () {
return Array.from(this.searchParams.values()).map(parseJSON)
}
/**
* Predicate to determine if parameter `key` is present in parameters.
* @param {string} key
* @return {boolean}
* @ignore
*/
has (key) {
return this.searchParams.has(key)
}
}
/**
* A result type used internally for handling
* IPC result values from the native layer that are in the form
* of `{ err?, data? }`. The `data` and `err` properties on this
* type of object are in tuple form and be accessed at `[data?,err?]`
* @ignore
*/
export class Result {
/**
* The unique ID for this result.
* @type {string}
* @ignore
*/
id = String(rand64())
/**
* An optional error in the result.
* @type {Error?}
* @ignore
*/
err = null
/**
* Result data if given.
* @type {(string|object|Uint8Array)?}
* @ignore
*/
data = null
/**
* The source of this result.
* @type {string?}
* @ignore
*/
source = null
/**
* Result headers, if given.
* @type {Headers?}
* @ignore
*/
headers = new Headers()
/**
* Creates a `Result` instance from input that may be an object
* like `{ err?, data? }`, an `Error` instance, or just `data`.
* @param {(object|Error|any)?} result
* @param {Error|object} [maybeError]
* @param {string} [maybeSource]
* @param {object|string|Headers} [maybeHeaders]
* @return {Result}
* @ignore
*/
static from (result, maybeError = null, maybeSource = null, maybeHeaders = null) {
if (result instanceof Result) {
if (!result.source && maybeSource) {
result.source = maybeSource
}
if (!result.err && maybeError) {
result.err = maybeError
}
if (!result.headers && maybeHeaders) {
result.headers = maybeHeaders
}
return result
}
if (result instanceof Error) {
result = { err: result }
}
if (!maybeSource && typeof maybeError === 'string') {
maybeSource = maybeError
maybeError = null
}
const id = result?.id || null
const err = maybeMakeError(result?.err || maybeError || null, Result.from)
const data = !err && result?.data !== null && result?.data !== undefined
? result.data
: (!err && !id && !result?.source ? result?.err ?? result : null)
const source = result?.source || maybeSource || null
const headers = result?.headers || maybeHeaders || null
return new this(id, err, data, source, headers)
}
/**
* `Result` class constructor.
* @private
* @param {string?} [id = null]
* @param {Error?} [err = null]
* @param {object?} [data = null]
* @param {string?} [source = null]
* @param {(object|string|Headers)?} [headers = null]
* @ignore
*/
constructor (id, err, data, source, headers) {
this.id = typeof id !== 'undefined' ? id : this.id
this.err = typeof err !== 'undefined' ? err : this.err
this.data = typeof data !== 'undefined' ? data : this.data
this.source = typeof source === 'string' && source.length > 0
? source
: this.source
this.headers = headers ? Headers.from(headers) : null
Object.defineProperty(this, 0, {
get: () => this.err,
enumerable: false,
configurable: false
})
Object.defineProperty(this, 1, {
get: () => this.data,
enumerable: false,
configurable: false
})
Object.defineProperty(this, 2, {
get: () => this.source,
enumerable: false,
configurable: false
})
Object.defineProperty(this, 3, {
get: () => this.headers,
enumerable: false,
configurable: false
})
}
/**
* Computed result length.
* @ignore
*/
get length () {
return Array.from(this).length
}
/**
* Generator for an `Iterable` interface over this instance.
* @ignore
*/
* [Symbol.iterator] () {
if (this.err !== undefined) yield this.err
if (this.data !== undefined) yield this.data
if (this.source !== undefined) yield this.source
if (this.headers !== undefined) yield this.headers
}
/**
* @ignore
*/
toJSON () {
return {
headers: this.headers ? this.headers.toJSON() : null,
source: this.source ?? null,
data: this.data ?? null,
err: this.err && {
// @ts-ignore
message: this.err.message ?? '',
// @ts-ignore
name: this.err.name ?? '',
// @ts-ignore
type: this.err.type ?? '',
// @ts-ignore
code: this.err.code ?? '',
...this.err
}
}
}
}
/**
* Waits for the native IPC layer to be ready and exposed on the
* global window object.
* @ignore
*/
export async function ready () {
const startReady = Date.now()
return await new Promise((resolve, reject) => {
return loop()
function loop () {
// this can hang on android. Give it some time because emulators can be slow.
if (Date.now() - startReady > 10000) {
reject(new Error('Failed to resolve globalThis.__args'))
} else if (globalThis.__args) {
// @ts-ignore
queueMicrotask(() => resolve())
} else {
queueMicrotask(loop)
}
}
})
}
const { toString } = Object.prototype
export class IPCSearchParams extends URLSearchParams {
constructor (params, nonce = null) {
let value
if (params !== undefined && toString.call(params) !== '[object Object]') {
value = params
params = null
}
super({
...params,
index: globalThis.__args?.index ?? 0,
seq: params?.seq ?? ('R' + nextSeq++)
})
if (value !== undefined) {
this.set('value', value)
}
if (nonce) {
this.set('nonce', nonce)
}
if (globalThis.RUNTIME_WORKER_ID) {
this.set('runtime-worker-id', globalThis.RUNTIME_WORKER_ID)
}
if (globalThis.RUNTIME_WORKER_LOCATION) {
this.set('runtime-worker-location', globalThis.RUNTIME_WORKER_LOCATION)
}
const runtimeFrameSource = globalThis.document
// @ts-ignore
? globalThis.document.querySelector('meta[name=runtime-frame-source]')?.content
: ''
// @ts-ignore
if (globalThis.top && globalThis.top !== globalThis) {
this.set('runtime-frame-type', 'nested')
} else if (!globalThis.window && globalThis.self === globalThis) {
this.set('runtime-frame-type', 'worker')
if (
globalThis.isServiceWorkerScope ||
(globalThis.clients && globalThis.FetchEvent) ||
globalThis.RUNTIME_WORKER_TYPE === 'serviceWorker'
) {
this.set('runtime-worker-type', 'serviceworker')
} else {
this.set('runtime-worker-type', 'worker')
}
} else {
this.set('runtime-frame-type', 'top-level')
}
if (runtimeFrameSource) {
this.set('runtime-frame-source', runtimeFrameSource)
}
}
toString () {
return super.toString().replace(/\+/g, '%20')
}
}
/**
* Sends a synchronous IPC command over XHR returning a `Result`
* upon success or error.
* @param {string} command
* @param {any?} [value]
* @param {object?} [options]
* @return {Result}
* @ignore
*/
export function sendSync (command, value = '', options = null, buffer = null) {
if (!globalThis.XMLHttpRequest) {
const err = new Error('XMLHttpRequest is not supported in environment')
return Result.from(err)
}
if (options?.cache === true && cache[command]) {
return cache[command]
}
const params = new IPCSearchParams(value, Date.now())
params.set('__sync__', 'true')
const uri = `ipc://${command}?${params}`
if (
typeof globalThis.__global_ipc_extension_handler === 'function' &&
(options?.useExtensionIPCIfAvailable || command.startsWith('fs.'))
) {
// eslint-disable-next-line
do {
if (command.startsWith('fs.')) {
if (isFileSystemBookmark({ params })) {
break
}
}
let response = null
try {
response = globalThis.__global_ipc_extension_handler(uri)
} catch (err) {
return Result.from(null, err)
}
if (typeof response === 'string') {
try {
response = JSON.parse(response)
} catch {}
}
return Result.from(response, null, command)
} while (0)
}
const request = new globalThis.XMLHttpRequest()
if (debug.enabled) {
debug.log('ipc.sendSync: %s', uri)
}
if (options?.responseType && typeof primordials !== 'undefined') {
if (!(/android/i.test(primordials.platform) && globalThis.document)) {
// @ts-ignore
request.responseType = options.responseType
}
}
if (buffer) {
request.open('POST', uri, false)
request.send(buffer)
} else {
request.open('GET', uri, false)
request.send()
}
const response = getRequestResponse(request, options)
const headers = request.getAllResponseHeaders()
const result = Result.from(response, null, command, headers)
if (debug.enabled) {
debug.log('ipc.sendSync: (resolved)', command, result)
}
if (options?.cache === true) {
cache[command] = result
}
if (command.startsWith('fs.') && isFileSystemBookmark({ params })) {
if (!result.err) {
const id = result.data?.id ?? result.data?.fd
const name = getFileSystemBookmarkName({ params })
if (id && name) {
bookmarks.temporary.set(name, id)
}
}
}
return result
}
/**
* Emit event to be dispatched on `window` object.
* @param {string} name
* @param {any} value
* @param {EventTarget=} [target = window]
* @param {Object=} options
*/
export async function emit (name, value, target, options) {
let detail = value
await ready()
if (debug.enabled) {
debug.log('ipc.emit:', name, value, target, options)
}
if (typeof value === 'string') {
try {
detail = decodeURIComponent(value)
detail = JSON.parse(detail)
} catch (err) {
// consider okay here because if detail is defined then
// `decodeURIComponent(value)` was successful and `JSON.parse(value)`
// was not: there could be bad/unsupported unicode in `value`
if (!detail) {
console.error(`${err.message} (${value})`)
return
}
}
}
const event = new globalThis.CustomEvent(name, { detail, ...options })
if (target) {
target.dispatchEvent(event)
} else {
globalThis.dispatchEvent(event)
}
}
/**
* Resolves a request by `seq` with possible value.
* @param {string} seq
* @param {any} value
* @ignore
*/
export async function resolve (seq, value) {
await ready()
if (debug.enabled) {
debug.log('ipc.resolve:', seq, value)
}
const index = globalThis.__args?.index || 0
const eventName = `resolve-${index}-${seq}`
const event = new globalThis.CustomEvent(eventName, { detail: value })
globalThis.dispatchEvent(event)
}
/**
* Sends an async IPC command request with parameters.
* @param {string} command
* @param {any=} value
* @param {object=} [options]
* @param {boolean=} [options.cache=false]
* @param {boolean=} [options.bytes=false]
* @return {Promise<Result>}
*/
export async function send (command, value, options = null) {
await ready()
if (options?.cache === true && cache[command]) {
return cache[command]
}
if (debug.enabled) {
debug.log('ipc.send:', command, value)
}
const params = new IPCSearchParams(value)
const uri = `ipc://${command}?${params}`
if (
typeof globalThis.__global_ipc_extension_handler === 'function' &&
(options?.useExtensionIPCIfAvailable || command.startsWith('fs.'))
) {
// eslint-disable-next-line
do {
if (command.startsWith('fs.')) {
if (isFileSystemBookmark({ params })) {
break
}
}
let response = null
try {
response = await globalThis.__global_ipc_extension_handler(uri)
} catch (err) {
return Result.from(null, err)
}
if (typeof response === 'string') {
try {
response = JSON.parse(response)
} catch {}
}
return Result.from(response, null, command)
} while (0)
}
if (options?.bytes) {
postMessage(uri, options.bytes)
} else {
postMessage(uri)
}
return await new Promise((resolve) => {
const event = `resolve-${params.get('index')}-${params.get('seq')}`
globalThis.addEventListener(event, onresolve, { once: true })
function onresolve (event) {
const result = Result.from(event.detail, null, command)
if (debug.enabled) {
debug.log('ipc.send: (resolved)', command, result)
}
if (options?.cache === true) {
cache[command] = result
}
if (command.startsWith('fs.') && isFileSystemBookmark({ params })) {
if (!result.err) {
const id = result.data?.id ?? result.data?.fd
const name = getFileSystemBookmarkName({ params })
if (id && name) {
bookmarks.temporary.set(name, id)
}
}
}
resolve(result)
}
})
}
/**
* Sends an async IPC command request with parameters and buffered bytes.
* @param {string} command
* @param {any=} value
* @param {(Buffer|Uint8Array|ArrayBuffer|string|Array)=} buffer
* @param {object=} options
* @ignore
*/
export async function write (command, value, buffer, options) {
if (!globalThis.XMLHttpRequest) {
const err = new Error('XMLHttpRequest is not supported in environment')
return Result.from(err)
}
await ready()
const params = new IPCSearchParams(value, Date.now())
const uri = `ipc://${command}?${params}`
if (
typeof globalThis.__global_ipc_extension_handler === 'function' &&
(options?.useExtensionIPCIfAvailable || command.startsWith('fs.'))
) {
// eslint-disable-next-line
do {
if (command.startsWith('fs.')) {
if (isFileSystemBookmark({ params })) {
break
}
}
let response = null
try {
response = await globalThis.__global_ipc_extension_handler(uri, buffer)
} catch (err) {
return Result.from(null, err)
}
if (typeof response === 'string') {
try {
response = JSON.parse(response)
} catch {}
}
return Result.from(response, null, command)
} while (0)
}
const signal = options?.signal
const request = new globalThis.XMLHttpRequest()
let resolved = false
let aborted = false
let timeout = null
if (signal) {
if (signal.aborted) {
return Result.from(null, new AbortError(signal), command)
}
signal.addEventListener('abort', () => {
if (!aborted && !resolved) {
aborted = true
request.abort()
}
})
}
request.responseType = options?.responseType ?? ''
request.open('POST', uri, true)
await request.send(buffer || null)
if (debug.enabled) {
debug.log('ipc.write:', uri, buffer || null)
}
return await new Promise((resolve) => {
if (options?.timeout) {
timeout = setTimeout(() => {
resolve(Result.from(null, new TimeoutError('ipc.write timedout'), command))
request.abort()
}, typeof options.timeout === 'number' ? options.timeout : TIMEOUT)
}
request.onabort = () => {
aborted = true
if (options?.timeout) {
clearTimeout(timeout)
}
resolve(Result.from(null, new AbortError(signal), command))
}
request.onreadystatechange = () => {
if (aborted) {
return
}
if (request.readyState === globalThis.XMLHttpRequest.DONE) {
resolved = true
clearTimeout(timeout)
const response = getRequestResponse(request, options)
const headers = request.getAllResponseHeaders()
const result = Result.from(response, null, command, headers)
if (debug.enabled) {
debug.log('ipc.write: (resolved)', command, result)
}
return resolve(result)
}
}
request.onerror = () => {
const headers = request.getAllResponseHeaders()
const err = new Error(getRequestResponseText(request) || '')
resolved = true
clearTimeout(timeout)
resolve(Result.from(null, err, command, headers))
}
})
}
/**
* Sends an async IPC command request with parameters requesting a response
* with buffered bytes.
* @param {string} command
* @param {any=} value
* @param {object=} options
* @ignore
*/
export async function request (command, value, options) {
if (!globalThis.XMLHttpRequest) {
const err = new Error('XMLHttpRequest is not supported in environment')
return Result.from(err)
}
if (options?.cache === true && cache[command]) {
return cache[command]
}
await ready()
const params = new IPCSearchParams(value, Date.now())
const uri = `ipc://${command}?${params}`
if (
typeof globalThis.__global_ipc_extension_handler === 'function' &&
(options?.useExtensionIPCIfAvailable || command.startsWith('fs.'))
) {
// eslint-disable-next-line
do {
if (command.startsWith('fs.')) {
if (isFileSystemBookmark({ params })) {
break
}
}
let response = null
try {
response = await globalThis.__global_ipc_extension_handler(uri)
} catch (err) {
return Result.from(null, err)
}
if (typeof response === 'string') {
try {
response = JSON.parse(response)
} catch {}
}
return Result.from(response, null, command)
} while (0)
}
const signal = options?.signal
const request = new globalThis.XMLHttpRequest()
let resolved = false
let aborted = false
let timeout = null
if (signal) {
if (signal.aborted) {
return Result.from(null, new AbortError(signal), command)
}
signal.addEventListener('abort', () => {
if (!aborted && !resolved) {
aborted = true
request.abort()
}
})
}
request.responseType = options?.responseType ?? ''
request.open('GET', uri)
request.send(null)
if (debug.enabled) {
debug.log('ipc.request:', uri)
}
return await new Promise((resolve) => {
if (options?.timeout) {
timeout = setTimeout(() => {
resolve(Result.from(null, new TimeoutError('ipc.request timedout'), command))
request.abort()
}, typeof options.timeout === 'number' ? options.timeout : TIMEOUT)
}
request.onabort = () => {
aborted = true
if (options?.timeout) {
clearTimeout(timeout)
}
resolve(Result.from(null, new AbortError(signal), command))
}
request.onreadystatechange = () => {
if (aborted) {
return
}
if (request.readyState === globalThis.XMLHttpRequest.DONE) {
resolved = true
clearTimeout(timeout)
const response = getRequestResponse(request, options)
const headers = request.getAllResponseHeaders()
const result = Result.from(response, null, command, headers)
if (debug.enabled) {
debug.log('ipc.request: (resolved)', command, result)
}
if (options?.cache === true) {
cache[command] = result
}
if (command.startsWith('fs.') && isFileSystemBookmark({ params })) {
if (!result.err) {
const id = result.data?.id ?? result.data?.fd
const name = getFileSystemBookmarkName({ params })
if (id && name) {
bookmarks.temporary.set(name, id)
}
}
}
return resolve(result)
}
}
request.onerror = () => {
const headers = request.getAllResponseHeaders()
const err = new Error(getRequestResponseText(request))
resolved = true
clearTimeout(timeout)
resolve(Result.from(null, err, command, headers))
}
})
}
/**
* Factory for creating a proxy based IPC API.
* @param {string} domain
* @param {(function|object)=} ctx
* @param {string=} [ctx.default]
* @return {Proxy}
* @ignore
*/
export function createBinding (domain, ctx) {
const dispatchable = {
emit,
ready,
resolve,
request,
send,
sendSync,
write
}
if (domain && typeof domain === 'object') {
ctx = domain
domain = null
}
if (typeof ctx !== 'function') {
ctx = Object.assign(function () {}, ctx)
}
ctx.chain = new Set()
ctx.chain.add(domain)
const proxy = new Proxy(ctx, {
apply (target, _, args) {
const chain = Array.from(ctx.chain)
const path = chain.filter(Boolean).filter((v) => typeof v === 'string').join('.')
const method = (ctx[path]?.method || ctx[path]) || ctx.default || 'send'
ctx.chain = new Set()
ctx.chain.add(domain)
return dispatchable[method](path, ...args)
},
get (target, key, ...args) {
const value = Reflect.get(target, key, ...args)
if (value !== undefined) return value
if (
key === 'inspect' ||
key === '__proto__' ||
key === 'constructor' ||
key in Promise.prototype ||
key in Function.prototype
) {
return
}
ctx.chain.add(key)
return new Proxy(ctx, this)
}
})
return proxy
}
// TODO(@chicoxyzzy): generate the primordials file during the build
// We need to set primordials here because we are using the
// `sendSync` method. This is a hack to get around the fact
// that we can't use cyclic imports with a sync call.
/**
* @ignore
*/
export const primordials = sendSync('platform.primordials')?.data || {}
// remove trailing slash on windows
if (primordials.cwd) {
primordials.cwd = primordials.cwd.replace(/\\$/, '')
}
if (
globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__ &&
typeof globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__ === 'object'
) {
Object.assign(primordials, globalThis.__RUNTIME_PRIMORDIAL_OVERRIDES__)
}
Object.freeze(primordials)
initializeXHRIntercept()
if (typeof globalThis?.window !== 'undefined') {
if (globalThis.document.readyState === 'complete') {
queueMicrotask(async () => {
try {
await send('platform.event', 'domcontentloaded')
} catch (err) {
reportError(err)
}
})
} else {
globalThis.document.addEventListener('DOMContentLoaded', () => {
queueMicrotask(async () => {
try {
await send('platform.event', 'domcontentloaded')
} catch (err) {
reportError(err)
}
})
})
}
}
export function inflateIPCMessageTransfers (object, types = new Map()) {
if (ArrayBuffer.isView(object)) {
return object
} else if (Array.isArray(object)) {
for (let i = 0; i < object.length; ++i) {
object[i] = inflateIPCMessageTransfers(object[i], types)
}
} else if (object && typeof object === 'object') {
if ('__type__' in object && types.has(object.__type__)) {
const Type = types.get(object.__type__)
if (typeof Type === 'function') {
if (typeof Type.from === 'function') {
return Type.from(object)
} else {
return new Type(object)
}
}
}
if (object.__type__ === 'IPCMessagePort' && object.id) {
return IPCMessagePort.create(object)
} else {
for (const key in object) {
const value = object[key]
object[key] = inflateIPCMessageTransfers(value, types)
}
}
}
return object
}
export function findIPCMessageTransfers (transfers, object) {
if (ArrayBuffer.isView(object)) {
add(object.buffer)
} else if (object instanceof ArrayBuffer) {
add(object)
} else if (Array.isArray(object)) {
for (let i = 0; i < object.length; ++i) {
object[i] = findIPCMessageTransfers(transfers, object[i])
}
} else if (object && typeof object === 'object') {
if (
object instanceof MessagePort || (
typeof object.postMessage === 'function' &&
Object.getPrototypeOf(object).constructor.name === 'MessagePort'
)
) {
const port = IPCMessagePort.create(object)
object.addEventListener('message', function onMessage (event) {
if (port.closed === true) {
port.onmessage = null
event.preventDefault()
event.stopImmediatePropagation()
object.removeEventListener('message', onMessage)
return false
}
port.dispatchEvent(new MessageEvent('message', event))
})
port.onmessage = (event) => {
if (port.closed === true) {
port.onmessage = null
event.preventDefault()
event.stopImmediatePropagation()
return false
}
const transfers = new Set()
findIPCMessageTransfers(transfers, event.data)
object.postMessage(event.data, {
transfer: Array.from(transfers)
})
}
add(port)
return port
} else {
for (const key in object) {
object[key] = findIPCMessageTransfers(transfers, object[key])
}
}
}
return object
function add (value) {
if (
value &&
!transfers.has(value) &&
!(Symbol.for('socket.runtime.serialize') in value)
) {
transfers.add(value)
}
}
}
export const ports = new Map()
export class IPCMessagePort extends MessagePort {
static from (options = null) {
return this.create(options)
}
static create (options = null) {
const id = String(options?.id ?? rand64())
const port = Object.create(this.prototype)
const token = String(rand64())
const channel = typeof options?.channel === 'string'
? new BroadcastChannel(options.channel)
: options.channel ?? new BroadcastChannel(id)
port[Symbol.for('socket.runtime.IPCMessagePort.id')] = id
ports.set(id, Object.create(null, {
id: { writable: true, value: id },
token: { writable: false, value: token },
closed: { writable: true, value: false },
started: { writable: true, value: false },
channel: { writable: true, value: channel },
onmessage: { writable: true, value: null },
onmessageerror: { writable: true, value: null },
eventTarget: { writable: true, value: new EventTarget() }
}))
channel.addEventListener('message', function onMessage (event) {
const state = ports.get(id)
if (!state || state?.closed === true) {
event.preventDefault()
event.stopImmediatePropagation()
channel.removeEventListener('message', onMessage)
return false
}
if (state?.started && event.data?.token !== state.token) {
port.dispatchEvent(new MessageEvent('message', {
...event,
data: event.data?.data
}))
}
})
gc.ref(port)
return port
}
get id () {
return this[Symbol.for('socket.runtime.IPCMessagePort.id')] ?? null
}
get started () {
if (!ports.has(this.id)) {
return false
}
return ports.get(this.id)?.started ?? false
}
get closed () {
if (!ports.has(this.id)) {
return true
}
return ports.get(this.id)?.closed ?? false
}
get onmessage () {
return ports.get(this.id)?.onmessage ?? null
}
set onmessage (onmessage) {
const port = ports.get(this.id)
if (!port) {
return
}
if (typeof this.onmessage === 'function') {
this.removeEventListener('message', this.onmessage)
port.onmessage = null
}
if (typeof onmessage === 'function') {
this.addEventListener('message', onmessage)
port.onmessage = onmessage
if (!port.started) {
this.start()
}
}
}
get onmessageerror () {
return ports.get(this.id)?.onmessageerror ?? null
}
set onmessageerror (onmessageerror) {
const port = ports.get(this.id)
if (!port) {
return
}
if (typeof this.onmessageerror === 'function') {
this.removeEventListener('messageerror', this.onmessageerror)
port.onmessageerror = null
}
if (typeof onmessageerror === 'function') {
this.addEventListener('messageerror', onmessageerror)
port.onmessageerror = onmessageerror
}
}
start () {
const port = ports.get(this.id)
if (port) {
port.started = true
}
}
close (purge = true) {
const port = ports.get(this.id)
if (port) {
port.closed = true
}
if (purge) {
ports.delete(this.id)
}
}
postMessage (message, optionsOrTransferList) {
const port = ports.get(this.id)
const options = { transfer: [] }
if (!port) {
return
}
if (Array.isArray(optionsOrTransferList)) {
options.transfer.push(...optionsOrTransferList)
} else if (Array.isArray(optionsOrTransferList?.transfer)) {
options.transfer.push(...optionsOrTransferList.transfer)
}
const transfers = new Set(options.transfer)
const handle = this[Symbol.for('socket.runtime.ipc.MessagePort.handlePostMessage')]
const serializedMessage = serialize(findIPCMessageTransfers(transfers, message))
options.transfer = Array.from(transfers)
if (typeof handle === 'function') {
if (handle.call(this, serializedMessage, options) === false) {
return
}
}
port.channel.postMessage({
token: port.token,
data: serializedMessage
}, options)
}
addEventListener (...args) {
const eventTarget = ports.get(this.id)?.eventTarget
if (eventTarget) {
return eventTarget.addEventListener(...args)
}
return false
}
removeEventListener (...args) {
const eventTarget = ports.get(this.id)?.eventT