UNPKG

pear-api

Version:

Pear API Base & Integration Module

275 lines (235 loc) 8.13 kB
'use strict' const fs = require('fs') const { spawn } = require('child_process') const { isWindows, isBare } = require('which-runtime') const { command } = require('paparam') const Pipe = isBare ? require('bare-pipe') : class Pipe extends require('net').Socket { constructor (fd) { super({ fd }) } } const { RUNTIME } = require('./constants') const rundef = require('./cmd/run') const pear = require('./cmd') const onteardown = global.Bare ? require('./teardown') : noop const program = global.Bare || global.process const kIPC = Symbol('ipc') const { ERR_INVALID_INPUT } = require('./errors') let COMPAT = false class API { #ipc = null #state = null #unloading = null #teardown = null #teardowns = [] #onteardown = null #refs = 0 #exitCode = 0 #pipe = null config = null argv = program.argv pid = program.pid static RTI = global.Pear?.constructor.RTI ?? null static IPC = kIPC static RUNTIME = RUNTIME static RUNTIME_ARGV = [] static set COMPAT (compat) { if (compat) Pear.config.tier = Pear.config.key ? 'production' : 'dev' return (COMPAT = compat) } static get COMPAT () { return COMPAT } constructor (ipc, state, { teardown = onteardown } = {}) { this.#ipc = ipc this.#state = state this.#refs = 0 this.#teardown = new Promise((resolve) => { this.#unloading = resolve }) this.#onteardown = teardown this.key = this.#state.key ? (this.#state.key.type === 'Buffer' ? Buffer.from(this.#state.key.data) : this.#state.key) : null this.config = state.config this.#onteardown(() => this.#unload()) this.#ipc.unref() } get [kIPC] () { return this.#ipc } get worker () { if (!this.constructor.COMPAT) console.error('[ DEPRECATED ] Pear.worker is deprecated and will be removed') const api = this return new class DeprecatedWorker { pipe () { if (!this.constructor.COMPAT) console.error('[ DEPRECATED ] Pear.worker.pipe() is now Pear.pipe') return api.pipe } run (...args) { if (!this.constructor.COMPAT) console.error('[ DEPRECATED ] Pear.worker.run() is now Pear.run()') return api.run(...args) } }() } #ref () { this.#refs++ if (this.#refs === 1) { this.#ipc.ref() } } #unref () { this.#refs-- if (this.#refs === 0) { this.#ipc.unref() } } async #unload () { this.#unloading() this.#teardowns.sort((a, b) => a.position - b.position) for (const teardown of this.#teardowns) this.#teardown = this.#teardown.then(teardown.fn) const MAX_TEARDOWN_WAIT = 15000 let timeout = null let timedout = false let rejected = null const countdown = new Promise((resolve) => { timeout = setTimeout(() => { timedout = true resolve() }, MAX_TEARDOWN_WAIT) }) this.#teardown.finally(() => { clearTimeout(timeout) }) await Promise.race([this.#teardown, countdown]).catch((err) => { rejected = err }) if (timedout || rejected) { if (timedout) console.error(`Max teardown wait reached after ${MAX_TEARDOWN_WAIT} ms. Exiting...`) if (rejected) console.error(`${rejected}. User teardown threw. Exiting...`) if (global.Bare) { global.Bare.exit() } else { const electron = require('electron') electron.ipcRenderer.send('app-exit') // graceful electron shutdown } } } async #reftrack (promise) { this.#ref() try { return await promise } finally { this.#unref() } } get pipe () { if (this.#pipe !== null) return this.#pipe const fd = 3 try { const hasPipe = isWindows ? fs.fstatSync(fd).isFIFO() : fs.fstatSync(fd).isSocket() if (hasPipe === false) return null } catch { return null } const pipe = new Pipe(fd) pipe.on('end', () => { this.#onteardown(() => pipe.end(), Number.MAX_SAFE_INTEGER) }) this.#pipe = pipe pipe.once('close', () => { this.#onteardown(() => program.exit(), Number.MAX_SAFE_INTEGER) }) return pipe } run (link, args = []) { const { RUNTIME, RUNTIME_ARGV, RTI } = this.constructor const argv = pear(program.argv.slice(1)).rest const parser = command('run', ...rundef) const cmd = parser.parse(argv, { sync: true }) const inject = [link] if (!cmd.flags.trusted) inject.unshift('--trusted') if (RTI.startId) inject.unshift('--parent', RTI.startId) argv.splice(cmd.indices.args.link, 1, ...inject) argv.unshift('run') let linksIndex = cmd.indices.flags.links const linksElements = linksIndex > 0 ? (cmd.flags.links === argv[linksIndex]) ? 2 : 1 : 0 if (cmd.indices.flags.startId > 0) { argv.splice(cmd.indices.flags.startId, 1) if (linksIndex > cmd.indices.flags.startId) linksIndex -= linksElements } if (linksIndex > 0) argv.splice(linksIndex, linksElements) const sp = spawn(RUNTIME, [...RUNTIME_ARGV, ...argv, ...args], { stdio: ['inherit', 'inherit', 'inherit', 'overlapped'], windowsHide: true }) this.#ref() sp.once('exit', (exitCode) => { if (exitCode !== 0) pipe.emit('crash', { exitCode }) this.#unref() }) const pipe = sp.stdio[3] return pipe } message = (msg) => this.#reftrack(this.#ipc.message(msg)) messages = (pattern, listener) => { if (typeof pattern === 'function') { listener = pattern pattern = {} } this.#ref() const subscriber = this.#ipc.messages(pattern) subscriber.on('close', () => this.#unref()) if (typeof listener === 'function') subscriber.on('data', listener) return subscriber } checkpoint = (state) => { this.config.checkpoint = state return this.#reftrack(this.#ipc.checkpoint(state)) } versions = () => this.#reftrack(this.#ipc.versions()) updated = () => this.#reftrack(this.#ipc.updated()) get = (key, opts = {}) => this.#reftrack(this.#ipc.get({ key, ...opts })) exists = (key) => this.#reftrack(this.#ipc.exists({ key })) compare = (keyA, keyB) => this.#reftrack(this.#ipc.compare({ keyA, keyB })) restart = async (opts = {}) => { if (this.#state.ui === null) throw new Error('Pear.restart is not supported for terminal apps') return this.#reftrack(this.#ipc.restart(opts)) } reload = async (opts = {}) => { if (this.#state.ui === null) throw new Error('Pear.reload is not supported for terminal apps') if (opts.platform) throw new Error('Platform Pear.reload is not supported for desktop apps') global.location.reload() } updates = (listener) => this.messages({ type: 'pear/updates' }, listener) wakeups = (listener) => this.messages({ type: 'pear/wakeup' }, listener) teardown = (fn = () => {}, position = 0) => { if (typeof fn !== 'function') throw ERR_INVALID_INPUT('teardown expects function') const isValidPosition = Number.isInteger(position) || position === Infinity || position === -Infinity if (!isValidPosition) throw ERR_INVALID_INPUT('teardown position must be integer') this.#teardowns.push({ fn, position }) } exit = (code) => program.exit(code) set exitCode (code) { this.#exitCode = code } get exitCode () { return this.#exitCode } dump = (link, opts = {}) => { this.#ref() const stream = this.#ipc.dump({ ...opts, link }) stream.on('close', () => this.#unref()) return stream } stage = (link, opts = {}) => { this.#ref() const stream = this.#ipc.stage({ ...opts, link }) stream.on('close', () => this.#unref()) return stream } release = (link, opts = {}) => { this.#ref() const stream = this.#ipc.release({ ...opts, link }) stream.on('close', () => this.#unref()) return stream } info = (link, opts = {}) => { this.#ref() const stream = this.#ipc.info({ ...opts, link }) stream.on('close', () => this.#unref()) return stream } seed = (link, opts = {}) => { this.#ref() const stream = this.#ipc.seed({ ...opts, link }) stream.on('close', () => { this.#unref() }) return stream } } function noop () {} module.exports = API