From 8a247c4c1e7d2d34e8e3593d823464ab15948329 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 19:04:46 +1200 Subject: [PATCH 1/9] tests needed for p2p --- test/src/dgram.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++ test/src/index.js | 1 + test/src/os.js | 21 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 test/src/dgram.js diff --git a/test/src/dgram.js b/test/src/dgram.js new file mode 100644 index 00000000..e9c4a4b3 --- /dev/null +++ b/test/src/dgram.js @@ -0,0 +1,52 @@ + +import * as dgram from '@socketsupply/io/dgram' +//import dgram from 'dgram' //uncomment to tests the tests, should pass running node +import { test } from 'tapzero' + +test('bind a dgram port and send a packet to it', function (t) { + var resolve + var socket = dgram.createSocket('udp4'), socket2 +// var p = ~~(Math.random()*0xffff) + socket.on('message', function (b) { + t.equal(b.toString(), 'hello!') + socket.close(function () {}) + resolve() + }) + t.equal(socket.bind(), socket, 'bind returns this') //any port + + socket.on('listening', function () { + var addr = socket.address() + var p = addr.port + t.equal(addr.address, '0.0.0.0') + t.ok(Number.isInteger(addr.port)) + socket.send(Buffer.from('hello!'), p, '127.0.0.1') //any port + }) + return new Promise(_resolve => resolve = _resolve) +}) + +test('error if port is already bound', function (t) { + var resolve + var socket = dgram.createSocket('udp4') + var socket2 = dgram.createSocket('udp4') + var received = [] + socket.on('message', function (b) { + console.log('receive:', b) + socket.close(function () {}) + resolve() + }) + t.equal(socket.bind(), socket, 'bind returns this') //any port + + socket.on('listening', function () { + var addr = socket.address() + var p = addr.port + t.equal(addr.address, '0.0.0.0') + t.ok(Number.isInteger(addr.port)) + socket2.on('error', function (err) { + t.equal(err.code, 'EADDRINUSE', 'address in use err') + socket.close() + resolve() + }) + t.equal(socket2.bind(p), socket2) + }) + return new Promise(_resolve => resolve = _resolve) +}) \ No newline at end of file diff --git a/test/src/index.js b/test/src/index.js index 97f28548..76cc7e38 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -25,3 +25,4 @@ setTimeout(function poll () { import './console.js' import './fs.js' import './os.js' +import './dgram.js' \ No newline at end of file diff --git a/test/src/os.js b/test/src/os.js index 58941a3f..3d022c43 100644 --- a/test/src/os.js +++ b/test/src/os.js @@ -1,4 +1,5 @@ import * as os from '@socketsupply/io/os' +//import os from 'os' //uncomment to tests the tests, should pass running node import { test } from 'tapzero' test('os.arch()', (t) => { @@ -14,6 +15,26 @@ test('os.type()', (t) => { }) test('os.networkInterfaces()', (t) => { + var int = os.networkInterfaces() + function isAddress (i) { + console.log(i) + if(i.family === 4) { + t.ok(/\d+\.\d+\.\d+\.\d+/.test(i.address)) + } + else t.equal(i.family, 6) + + t.ok(i.netmask, 'has netmask') + t.ok(i.mac, 'has mac address') + t.equal(typeof i.internal, 'boolean') + t.ok(i.cidr, 'has cidr') + } + + t.ok(Array.isArray(int.lo)) + var interfaces = Object.keys(int).length + t.ok(interfaces >= 2, 'network interfaces has at least two keys, loopback + wifi, was:'+interfaces) + for(var intf in int) { + int[intf].forEach(isAddress) + } }) test('os.EOL', (t) => { From 7277256a23143086e18368fd3d393d425bca9954 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 21:24:29 +1200 Subject: [PATCH 2/9] refactor to make tests runnable in node.js --- dgram.js | 555 +------------------------------- fs.js | 6 +- index.js | 2 +- os.js | 184 +---------- package.json | 6 + process.js | 36 +-- sdk/dgram.js | 555 ++++++++++++++++++++++++++++++++ sdk/fs.js | 6 + {fs => sdk/fs}/binding.js | 0 {fs => sdk/fs}/constants.js | 73 ++++- {fs => sdk/fs}/dir.js | 0 {fs => sdk/fs}/fds.js | 0 {fs => sdk/fs}/flags.js | 0 {fs => sdk/fs}/handle.js | 8 +- {fs => sdk/fs}/index.js | 10 +- {fs => sdk/fs}/promises.js | 0 {fs => sdk/fs}/stats.js | 0 {fs => sdk/fs}/stream.js | 2 +- sdk/ipc.js | 618 ++++++++++++++++++++++++++++++++++++ sdk/os.js | 184 +++++++++++ sdk/process.js | 35 ++ test/src/dgram.js | 2 +- test/src/fs/flags.js | 2 +- test/src/fs/index.js | 15 +- test/src/index.js | 6 +- test/src/os.js | 4 +- 26 files changed, 1502 insertions(+), 807 deletions(-) create mode 100644 sdk/dgram.js create mode 100644 sdk/fs.js rename {fs => sdk/fs}/binding.js (100%) rename {fs => sdk/fs}/constants.js (63%) rename {fs => sdk/fs}/dir.js (100%) rename {fs => sdk/fs}/fds.js (100%) rename {fs => sdk/fs}/flags.js (100%) rename {fs => sdk/fs}/handle.js (99%) rename {fs => sdk/fs}/index.js (98%) rename {fs => sdk/fs}/promises.js (100%) rename {fs => sdk/fs}/stats.js (100%) rename {fs => sdk/fs}/stream.js (99%) create mode 100644 sdk/ipc.js create mode 100644 sdk/os.js create mode 100644 sdk/process.js diff --git a/dgram.js b/dgram.js index d00ac27b..a0e62708 100644 --- a/dgram.js +++ b/dgram.js @@ -1,555 +1,2 @@ -/** - * @module Dgram - * - * This module provides an implementation of UDP datagram sockets. It does - * not (yet) provide any of the multicast methods or properties. - */ -import { Buffer } from 'buffer' - -import { EventEmitter } from './events.js' -import { isIPv4 } from './net.js' -import * as dns from './dns.js' -import * as ipc from './ipc.js' -import { rand64, isArrayBufferView } from './util.js' - -const fixBufferList = list => { - const newlist = new Array(list.length) - - for (let i = 0, l = list.length; i < l; i++) { - const buf = list[i] - - if (typeof buf === 'string') { - newlist[i] = Buffer.from(buf) - } else if (!isArrayBufferView(buf)) { - return null - } else { - newlist[i] = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) - } - } - - return newlist -} - -/* - * New instances of dgram.Socket are created using dgram.createSocket(). - * The new keyword is not to be used to create dgram.Socket instances. - */ -export class Socket extends EventEmitter { - constructor (options) { - super() - - this.serverId = rand64() - this.clientId = rand64() - this.type = options.type || 'udp4' - - this.state = { - recvBufferSize: options.recvBufferSize, - sendBufferSize: options.sendBufferSize, - connectState: 2, - reuseAddr: options.reuseAddr, - ipv6Only: options.ipv6Only - } - - this.connect() - } - - _getSockData ({ id }) { - return ipc.sendSync('udpGetSockName', { id: id || this.clientId }) - } - - async _getPeerData () { - const { err, data } = await ipc.send('udpGetPeerName', { - id: this.clientId - }) - - if (err) return { err } - return { data } - } - - async _recvStart () { - return await ipc.write('udpReadStart', { serverId: this.serverId }) - } - - /** - * Listen for datagram messages on a named port and optional address - * If address is not specified, the operating system will attempt to - * listen on all addresses. Once binding is complete, a 'listening' - * event is emitted and the optional callback function is called. - * - * If binding fails, an 'error' event is emitted. - * - * @param {number} port - The port to to listen for messages on - * @param {string} address - The address to bind to (0.0.0.0) - * @param {function} callback - With no parameters. Called when binding is complete. - */ - bind (arg1, arg2, cb) { - let options = {} - - if (typeof arg2 === 'function') { - cb = arg2 - options.address = undefined - } - - if (typeof arg1 === 'number') { - options.port = arg1 - options.address = arg2 - } else if (typeof arg1 === 'object') { - options = { ...arg1 } - } else { - throw new Error('invalid arguments') - } - - function removeListeners () { - this.removeListener('close', removeListeners) - this.removeListener('error', removeListeners) - } - - function onListening () { - Function.prototype.call(removeListeners, this) - if (cb) Function.prototype.call(cb, this) - } - - this.on('error', removeListeners) - this.on('listening', onListening) - - if (!options.address) { - if (this.type === 'udp4') { - options.address = '0.0.0.0' - } else { - options.address = '::' - } - } else if (!isIPv4(options.address)) { - // fire off a dns lookup, listening or error will be emitted in response - ipc.write('dnsLookup', { - hostname: options.address, - serverId: this.serverId, - seq: -1 - }) - } - - const { err: errBind, data } = ipc.sendSync('udpBind', { - serverId: this.serverId, - address: options.address || "", - port: options.port || 0, - reuseAddr: options.reuseAddr ? "true" : "false", - ipv6Only: options.ipv6Only ? "true" : "false" - }) - - if (errBind) { - this.emit('error', errBind) - return { err: errBind } - } - - this._address = options.address - this._port = options.port - this._family = isIPv4(options.address) ? 'IPv4' : 'IPv6' - - const listener = e => { - const { data: buffer, params } = e.detail - const { err, data } = params - - if (err && err.serverId === this.serverId) { - return this.emit('error', err) - } - - if (!data || BigInt(data.serverId) !== this.serverId) return - - if (data.source === 'dnsLookup') { - this._address = data.params.ip - return this.emit('listenting') - } - - if (data.source === 'udpReadStart') { - this.emit('message', buffer, { - address: params.data.ip, - port: params.data.port, - family: isIPv4(params.data.ip) - }) - } - - if (data.EOF) { - window.removeListener('data', listener) - } - } - - // subscribe this socket to the firehose - window.addEventListener('data', listener) - - this._recvStart() - - if (cb) cb(null) - return this - } - - /** - * Associates the dgram.Socket to a remote address and port. Every message sent - * by this handle is automatically sent to that destination. Also, the socket - * will only receive messages from that remote peer. Trying to call connect() - * on an already connected socket will result in an ERR_SOCKET_DGRAM_IS_CONNECTED - * exception. If address is not provided, '127.0.0.1' (for udp4 sockets) or '::1' - * (for udp6 sockets) will be used by default. Once the connection is complete, - * a 'connect' event is emitted and the optional callback function is called. - * In case of failure, the callback is called or, failing this, an 'error' event - * is emitted. - * - * @param {number} port - Port the client should connect to. - * @param {string?} host - Host the client should connect to. - * @param {function?} connectListener - Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. - */ - async connect (arg1, arg2, cb) { - if (this.clientId) { - const err = new Error('already connected') - if (cb) return cb(err) - return { err } - } - - const port = arg1 - let address = arg2 - - if (typeof arg2 === 'function') { - cb = arg2 - address = undefined - } - - const { - err: errBind - } = this.bind({ port: 0 }, null) - - if (errBind) { - if (cb) return cb(errBind) - return { err: errBind } - } - - this.once('connect', cb) - - const { - err: errLookup, - data: dataLookup - } = await dns.lookup(address) - - if (errLookup) { - this.emit('error', errLookup) - return { err: errLookup } - } - - const { - err: errConnect, - dataConnect - } = await ipc.send('udpConnect', { - ip: dataLookup.ip, - port: port || 0 - }) - - if (errConnect) { - this.emit('error', errConnect) - return { err: errConnect } - } - - this.state.connectState = 2 - - this.emit('connect') - - // TODO udpConnect could return the peer data instead of putting it - // into a different call and we could shave off a bit of time here. - const { - err: errGetPeerData, - data: dataPeerData - } = await this._getPeerData({ clientId: dataConnect.clientId }) - - if (errGetPeerData) { - this.emit('error', errGetPeerData) - return { err: errGetPeerData } - } - - this._remoteAddress = dataPeerData.address - this._remotePort = dataPeerData.port - this._remoteFamily = dataPeerData.family - - if (cb) cb(null) - return {} - } - - async disconnect () { - const { err: errConnect } = await ipc.send('udpDisconnect', { - ip: this._remoteAddress, - port: this._remotePort || 0 - }) - - if (errConnect) { - this.emit('error', errConnect) - return { err: errConnect } - } - - if (this.connectedState === 2) { - this.emit('close') - } - - return {} - } - - /* - * Broadcasts a datagram on the socket. For connectionless sockets, the - * destination port and address must be specified. Connected sockets, on the - * other hand, will use their associated remote endpoint, so the port and - * address arguments must not be set. - * - * The msg argument contains the message to be sent. Depending on its type, - * different behavior can apply. If msg is a Buffer, any TypedArray or a - * DataView, the offset and length specify the offset within the Buffer where - * the message begins and the number of bytes in the message, respectively. - * If msg is a String, then it is automatically converted to a Buffer with - * 'utf8' encoding. With messages that contain multi-byte characters, offset - * and length will be calculated with respect to byte length and not the - * character position. If msg is an array, offset and length must not be - * specified. - * - * The address argument is a string. If the value of address is a host name, - * DNS will be used to resolve the address of the host. If address is not - * provided or otherwise nullish, '127.0.0.1' (for udp4 sockets) or '::1' - * (for udp6 sockets) will be used by default. - * - * If the socket has not been previously bound with a call to bind, the socket - * is assigned a random port number and is bound to the "all interfaces" - * address ('0.0.0.0' for udp4 sockets, '::0' for udp6 sockets.) - * - * An optional callback function may be specified to as a way of reporting DNS - * errors or for determining when it is safe to reuse the buf object. DNS - * lookups delay the time to send for at least one tick of the Node.js event - * loop. - * - * The only way to know for sure that the datagram has been sent is by using a - * callback. If an error occurs and a callback is given, the error will be - * passed as the first argument to the callback. If a callback is not given, - * the error is emitted as an 'error' event on the socket object. - * - * Offset and length are optional but both must be set if either are used. - * They are supported only when the first argument is a Buffer, a TypedArray, - * or a DataView. - * - * @param {ArrayBuffer} buffer - An array buffer of data to send - * - */ - async send (buffer, ...args) { - let offset, length, port, address, cb - const connected = this.state.connectState === 2 - - if (typeof buffer === 'string') { - buffer = Buffer.from(buffer) - } - - const index = args.findIndex(arg => typeof arg === 'function') - if (index > -1) cb = args.pop() - - if (typeof args[2] === 'number') { - [offset, length, port, address] = args.slice(0, index) - - if (connected && (port || address)) { - throw new Error('Already connected') - } - - buffer = Buffer.from(buffer.buffer, buffer.byteOffset + offset, length) - } else { - [port, address] = args - } - - let list - - if (!Array.isArray(buffer)) { - if (typeof buffer === 'string') { - list = [Buffer.from(buffer)] - } else if (!isArrayBufferView(buffer)) { - // throw new Error('Invalid buffer') - list = Buffer.from(buffer) - } else { - list = [buffer] - } - } else if (!(list = fixBufferList(buffer))) { - throw new Error('Invalid buffer') - } - - // @XXX(jwerle): @heapwolf why is this happening in a `send()` call? - // - // @jwerle it's from the node.js source code - https://github.com/nodejs/node/blob/main/lib/dgram.js#L645 - // but it's missing a check to see if the instance is unbound (state.bindState === BIND_STATE_UNBOUND) - /* - const { err: errBind } = this.bind({ port: 0 }, null) - - if (errBind) { - if (cb) return cb(errBind) - return { err: errBind } - } - */ - - if (list.length === 0) { - list.push(Buffer.alloc(0)) - } - - if (!connected && !isIPv4(address)) { - throw new Error('Currently dns lookup on send is not supported') - } - - const { err: errSend } = await ipc.write('udpSend', { - ephemeral: !this.clientId, - clientId: this.clientId || rand64(), - serverId: this.serverId || 0, - address, - port - }, list) - - if (errSend) { - if (cb) return cb(errSend) - return { err: errSend } - } - - if (cb) cb(null) - return {} - } - - /** - * Close the underlying socket and stop listening for data on it. If a - * callback is provided, it is added as a listener for the 'close' event. - * - * @param {function} callback - Called when the connection is completed or on error. - * - */ - async close (cb) { - if (typeof cb === 'function') { - this.on('close', cb) - } - - const { err } = await ipc.send('udpClose', { - id: this.clientId - }) - - if (err && cb) return cb(err) - if (err) return { err } - - this.emit('close') - return {} - } - - /** - * - * Returns an object containing the address information for a socket. For - * UDP sockets, this object will contain address, family, and port properties. - * - * This method throws EBADF if called on an unbound socket. - * @returns {Object} socketInfo - Information about the local socket - * @returns {string} socketInfo.address - The IP address of the socket - * @returns {ip} socketInfo.ip - The IP address of the socket - * @returns {string} socketInfo.port - The port of the socket - * @returns {string} socketInfo.family - The IP family of the socket - */ - address () { - return { - address: this._address, - ip: this._address, - port: this._port, - family: this._family - } - } - - /** - * Returns an object containing the address, family, and port of the remote - * endpoint. This method throws an ERR_SOCKET_DGRAM_NOT_CONNECTED exception - * if the socket is not connected. - * - * @returns {Object} socketInfo - Information about the remote socket - * @returns {string} socketInfo.remoteAddress - The IP address of the socket - * @returns {ip} socketInfo.remoteIp - The IP address of the socket - * @returns {string} socketInfo.remotePort - The port of the socket - * @returns {string} socketInfo.remoteFamily - The IP family of the socket - */ - remoteAddress () { - return { - remoteIp: this._remoteAddress, - remoteAddress: this._remoteAddress, - remotePort: this._remotePort, - remoteFamily: this._remoteFamily - } - } - - // - // Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in - // bytes. - // - setRecvBufferSize (size) { - this.state.recvBufferSize = size - } - - // - // Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in - // bytes. - // - setSendBufferSize (size) { - this.state.sendBufferSize = size - } - - getRecvBufferSize () { - return this.state.recvBufferSize - } - - getSendBufferSize () { - return this.state.sendBufferSize - } - - // - // For now wer aren't going to implement any of the multicast options, - // mainly because 1. we don't need it in hyper and 2. if a user wants - // to deploy their app to the app store, they will need to request the - // multicast entitlement from apple. If someone really wants this they - // can implement it. - // - setBroadcast () { - throw new Error('not implemented') - } - - setTTL () { - throw new Error('not implemented') - } - - setMulticastTTL () { - throw new Error('not implemented') - } - - setMulticastLoopback () { - throw new Error('not implemented') - } - - setMulticastMembership () { - throw new Error('not implemented') - } - - setMulticastInterface () { - throw new Error('not implemented') - } - - addMembership () { - throw new Error('not implemented') - } - - dropMembership () { - throw new Error('not implemented') - } - - addSourceSpecificMembership () { - throw new Error('not implemented') - } - - dropSourceSpecificMembership () { - throw new Error('not implemented') - } - - ref () { - return this - } - - unref () { - return this - } -} - -export const createSocket = (type, listener) => { - return new Socket({ type, listener }) -} +export * from 'dgram' \ No newline at end of file diff --git a/fs.js b/fs.js index cdae450e..36604ec5 100644 --- a/fs.js +++ b/fs.js @@ -1,5 +1 @@ -/** - * @notice This is a rexports of `fs/index.js` so consumers will - * need to only `import * as fs from '@socketsupply/io/fs.js'` - */ -export * from './fs/index.js' +export * from 'fs' \ No newline at end of file diff --git a/index.js b/index.js index 8a6f2801..b18352b6 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,6 @@ export * as ipc from './ipc.js' export * as net from './net.js' export * as os from './os.js' export * as crypto from './crypto.js' -export { default as process } from './process.js' +export * as process from './process.js' export * as stream from './stream.js' export * as util from './util.js' diff --git a/os.js b/os.js index a378f0f6..68125506 100644 --- a/os.js +++ b/os.js @@ -1,184 +1,2 @@ -/** - * @module OS - * - * This module provides normalized system information from all the major - * operating systems. - */ -import { toProperCase } from './util.js' -import * as ipc from './ipc.js' - -const UNKNOWN = 'unknown' - -const cache = { - arch: UNKNOWN, - type: UNKNOWN, - platform: UNKNOWN -} - -export function arch () { - let value = UNKNOWN - - if (cache.arch !== UNKNOWN) { - return cache.arch - } - - if (typeof window !== 'object') { - if (typeof process === 'object' && typeof process.arch === 'string') { - return process.arch - } - } - - if (typeof window === 'object') { - value = ( - window.process?.arch || - ipc.sendSync('getPlatformArch')?.data || - UNKNOWN - ) - } - - if (value === 'arm64') { - return value - } - - cache.arch = value - .replace('x86_64', 'x64') - .replace('x86', 'ia32') - .replace(/arm.*/, 'arm') - - return cache.arch -} - -export function networkInterfaces () { - const { ipv4, ipv6 } = ipc.sendSync('getNetworkInterfaces')?.data || {} - const interfaces = {} - - for (const type in ipv4) { - const address = ipv4[type] - const family = 'IPv4' - - let internal = false - let netmask = '255.255.255.0' - let cidr = `${address}/24` - let mac = null - - if (address === '127.0.0.1' || address === '0.0.0.0') { - internal = true - mac = '00:00:00:00:00:00' - - if (address === '127.0.0.1') { - cidr = '127.0.0.1/8' - netmask = '255.0.0.0' - } else { - cidr = '0.0.0.0/0' - netmask = '0.0.0.0' - } - } - - interfaces[type] = interfaces[type] || [] - interfaces[type].push({ - address, - netmask, - internal, - family, - cidr, - mac - }) - } - - for (const type in ipv6) { - const address = ipv6[type] - const family = 'IPv6' - - let internal = false - let netmask = 'ffff:ffff:ffff:ffff::' - let cidr = `${address}/64` - let mac = null - - if (address === '::1') { - internal = true - netmask = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' - cidr = '::1/128' - mac = '00:00:00:00:00:00' - } - - interfaces[type] = interfaces[type] || [] - interfaces[type].push({ - address, - netmask, - internal, - family, - cidr, - mac - }) - } - - return interfaces -} - -export function platform () { - let value = UNKNOWN - - if (cache.platform !== UNKNOWN) { - return cache.platform - } - - if (typeof window !== 'object') { - if (typeof process === 'object' && typeof process.platform === 'string') { - return process.platform - } - } - - if (typeof window === 'object') { - value = ( - window.process?.os || - ipc.sendSync('getPlatformOS')?.data || - window.process?.platform || - UNKNOWN - ) - } - - cache.platform = value.replace(/^mac/i, 'darwin') - - return cache.platform -} - -export function type () { - let value = 'unknown' - - if (cache.type !== UNKNOWN) { - return cache.type - } - - if (typeof window !== 'object') { - switch (platform()) { - case 'linux': return 'Linux' - case 'mac': case 'darwin': return 'Darwin' - case 'win32': return 'Windows' // Windows_NT? - } - } - - if (typeof window == 'object') { - value = ( - window.process?.platform || - ipc.sendSync('getPlatformType')?.data || - UNKNOWN - ) - } - - if (value !== UNKNOWN) { - value = toProperCase(value) - } - - cache.type = value - - return cache.type -} - -export const EOL = (() => { - if (/^win/i.test(type())) { - return '\r\n' - } - - return '\n' -})() +export * from 'os' diff --git a/package.json b/package.json index 389fdfb6..2b070d4b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,12 @@ "esbuild": "0.14.49", "tapzero": "*" }, + "browser": { + "fs": "./sdk/fs.js", + "os": "./sdk/os.js", + "dgram": "./sdk/dgram.js", + "process": "./sdk/process.js" + }, "dependencies": { "buffer": "6.0.3" } diff --git a/process.js b/process.js index 509d7cef..6d039b07 100644 --- a/process.js +++ b/process.js @@ -1,35 +1 @@ -import { EventEmitter } from './events.js' -import { send } from './ipc.js' - -let didEmitExitEvent = false - -export function homedir () { - process.env.HOME || '' -} - -export function exit (code) { - if (!didEmitExitEvent) { - didEmitExitEvent = true - queueMicrotask(() => process.emit('exit', code)) - } - - send('exit', { value: code || 0 }) -} - -const parent = typeof window === 'object' ? window : globalThis -const isNode = parent?.process?.versions?.node -const process = isNode - ? globalThis.process - : Object.create(null, Object.getOwnPropertyDescriptors({ - ...EventEmitter.prototype, - homedir, - argv0: parent?.process?.argv?.[0], - exit, - ...parent?.process, - })) - -if (!isNode) { - EventEmitter.call(process) -} - -export default process +export * from 'process' \ No newline at end of file diff --git a/sdk/dgram.js b/sdk/dgram.js new file mode 100644 index 00000000..e7984b8b --- /dev/null +++ b/sdk/dgram.js @@ -0,0 +1,555 @@ +/** + * @module Dgram + * + * This module provides an implementation of UDP datagram sockets. It does + * not (yet) provide any of the multicast methods or properties. + */ + +import { Buffer } from 'buffer' + +import { EventEmitter } from '../events.js' +import { isIPv4 } from '../net.js' +import * as dns from '../dns.js' +import * as ipc from '../ipc.js' +import { rand64, isArrayBufferView } from '../util.js' + +const fixBufferList = list => { + const newlist = new Array(list.length) + + for (let i = 0, l = list.length; i < l; i++) { + const buf = list[i] + + if (typeof buf === 'string') { + newlist[i] = Buffer.from(buf) + } else if (!isArrayBufferView(buf)) { + return null + } else { + newlist[i] = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) + } + } + + return newlist +} + +/* + * New instances of dgram.Socket are created using dgram.createSocket(). + * The new keyword is not to be used to create dgram.Socket instances. + */ +export class Socket extends EventEmitter { + constructor (options) { + super() + + this.serverId = rand64() + this.clientId = rand64() + this.type = options.type || 'udp4' + + this.state = { + recvBufferSize: options.recvBufferSize, + sendBufferSize: options.sendBufferSize, + connectState: 2, + reuseAddr: options.reuseAddr, + ipv6Only: options.ipv6Only + } + + this.connect() + } + + _getSockData ({ id }) { + return ipc.sendSync('udpGetSockName', { id: id || this.clientId }) + } + + async _getPeerData () { + const { err, data } = await ipc.send('udpGetPeerName', { + id: this.clientId + }) + + if (err) return { err } + return { data } + } + + async _recvStart () { + return await ipc.write('udpReadStart', { serverId: this.serverId }) + } + + /** + * Listen for datagram messages on a named port and optional address + * If address is not specified, the operating system will attempt to + * listen on all addresses. Once binding is complete, a 'listening' + * event is emitted and the optional callback function is called. + * + * If binding fails, an 'error' event is emitted. + * + * @param {number} port - The port to to listen for messages on + * @param {string} address - The address to bind to (0.0.0.0) + * @param {function} callback - With no parameters. Called when binding is complete. + */ + bind (arg1, arg2, cb) { + let options = {} + + if (typeof arg2 === 'function') { + cb = arg2 + options.address = undefined + } + + if (typeof arg1 === 'number') { + options.port = arg1 + options.address = arg2 + } else if (typeof arg1 === 'object') { + options = { ...arg1 } + } else { + throw new Error('invalid arguments') + } + + function removeListeners () { + this.removeListener('close', removeListeners) + this.removeListener('error', removeListeners) + } + + function onListening () { + Function.prototype.call(removeListeners, this) + if (cb) Function.prototype.call(cb, this) + } + + this.on('error', removeListeners) + this.on('listening', onListening) + + if (!options.address) { + if (this.type === 'udp4') { + options.address = '0.0.0.0' + } else { + options.address = '::' + } + } else if (!isIPv4(options.address)) { + // fire off a dns lookup, listening or error will be emitted in response + ipc.write('dnsLookup', { + hostname: options.address, + serverId: this.serverId, + seq: -1 + }) + } + + const { err: errBind, data } = ipc.sendSync('udpBind', { + serverId: this.serverId, + address: options.address || "", + port: options.port || 0, + reuseAddr: options.reuseAddr ? "true" : "false", + ipv6Only: options.ipv6Only ? "true" : "false" + }) + + if (errBind) { + this.emit('error', errBind) + return { err: errBind } + } + + this._address = options.address + this._port = options.port + this._family = isIPv4(options.address) ? 'IPv4' : 'IPv6' + + const listener = e => { + const { data: buffer, params } = e.detail + const { err, data } = params + + if (err && err.serverId === this.serverId) { + return this.emit('error', err) + } + + if (!data || BigInt(data.serverId) !== this.serverId) return + + if (data.source === 'dnsLookup') { + this._address = data.params.ip + return this.emit('listenting') + } + + if (data.source === 'udpReadStart') { + this.emit('message', buffer, { + address: params.data.ip, + port: params.data.port, + family: isIPv4(params.data.ip) + }) + } + + if (data.EOF) { + window.removeListener('data', listener) + } + } + + // subscribe this socket to the firehose + window.addEventListener('data', listener) + + this._recvStart() + + if (cb) cb(null) + return this + } + + /** + * Associates the dgram.Socket to a remote address and port. Every message sent + * by this handle is automatically sent to that destination. Also, the socket + * will only receive messages from that remote peer. Trying to call connect() + * on an already connected socket will result in an ERR_SOCKET_DGRAM_IS_CONNECTED + * exception. If address is not provided, '127.0.0.1' (for udp4 sockets) or '::1' + * (for udp6 sockets) will be used by default. Once the connection is complete, + * a 'connect' event is emitted and the optional callback function is called. + * In case of failure, the callback is called or, failing this, an 'error' event + * is emitted. + * + * @param {number} port - Port the client should connect to. + * @param {string?} host - Host the client should connect to. + * @param {function?} connectListener - Common parameter of socket.connect() methods. Will be added as a listener for the 'connect' event once. + */ + async connect (arg1, arg2, cb) { + if (this.clientId) { + const err = new Error('already connected') + if (cb) return cb(err) + return { err } + } + + const port = arg1 + let address = arg2 + + if (typeof arg2 === 'function') { + cb = arg2 + address = undefined + } + + const { + err: errBind + } = this.bind({ port: 0 }, null) + + if (errBind) { + if (cb) return cb(errBind) + return { err: errBind } + } + + this.once('connect', cb) + + const { + err: errLookup, + data: dataLookup + } = await dns.lookup(address) + + if (errLookup) { + this.emit('error', errLookup) + return { err: errLookup } + } + + const { + err: errConnect, + dataConnect + } = await ipc.send('udpConnect', { + ip: dataLookup.ip, + port: port || 0 + }) + + if (errConnect) { + this.emit('error', errConnect) + return { err: errConnect } + } + + this.state.connectState = 2 + + this.emit('connect') + + // TODO udpConnect could return the peer data instead of putting it + // into a different call and we could shave off a bit of time here. + const { + err: errGetPeerData, + data: dataPeerData + } = await this._getPeerData({ clientId: dataConnect.clientId }) + + if (errGetPeerData) { + this.emit('error', errGetPeerData) + return { err: errGetPeerData } + } + + this._remoteAddress = dataPeerData.address + this._remotePort = dataPeerData.port + this._remoteFamily = dataPeerData.family + + if (cb) cb(null) + return {} + } + + async disconnect () { + const { err: errConnect } = await ipc.send('udpDisconnect', { + ip: this._remoteAddress, + port: this._remotePort || 0 + }) + + if (errConnect) { + this.emit('error', errConnect) + return { err: errConnect } + } + + if (this.connectedState === 2) { + this.emit('close') + } + + return {} + } + + /* + * Broadcasts a datagram on the socket. For connectionless sockets, the + * destination port and address must be specified. Connected sockets, on the + * other hand, will use their associated remote endpoint, so the port and + * address arguments must not be set. + * + * The msg argument contains the message to be sent. Depending on its type, + * different behavior can apply. If msg is a Buffer, any TypedArray or a + * DataView, the offset and length specify the offset within the Buffer where + * the message begins and the number of bytes in the message, respectively. + * If msg is a String, then it is automatically converted to a Buffer with + * 'utf8' encoding. With messages that contain multi-byte characters, offset + * and length will be calculated with respect to byte length and not the + * character position. If msg is an array, offset and length must not be + * specified. + * + * The address argument is a string. If the value of address is a host name, + * DNS will be used to resolve the address of the host. If address is not + * provided or otherwise nullish, '127.0.0.1' (for udp4 sockets) or '::1' + * (for udp6 sockets) will be used by default. + * + * If the socket has not been previously bound with a call to bind, the socket + * is assigned a random port number and is bound to the "all interfaces" + * address ('0.0.0.0' for udp4 sockets, '::0' for udp6 sockets.) + * + * An optional callback function may be specified to as a way of reporting DNS + * errors or for determining when it is safe to reuse the buf object. DNS + * lookups delay the time to send for at least one tick of the Node.js event + * loop. + * + * The only way to know for sure that the datagram has been sent is by using a + * callback. If an error occurs and a callback is given, the error will be + * passed as the first argument to the callback. If a callback is not given, + * the error is emitted as an 'error' event on the socket object. + * + * Offset and length are optional but both must be set if either are used. + * They are supported only when the first argument is a Buffer, a TypedArray, + * or a DataView. + * + * @param {ArrayBuffer} buffer - An array buffer of data to send + * + */ + async send (buffer, ...args) { + let offset, length, port, address, cb + const connected = this.state.connectState === 2 + + if (typeof buffer === 'string') { + buffer = Buffer.from(buffer) + } + + const index = args.findIndex(arg => typeof arg === 'function') + if (index > -1) cb = args.pop() + + if (typeof args[2] === 'number') { + [offset, length, port, address] = args.slice(0, index) + + if (connected && (port || address)) { + throw new Error('Already connected') + } + + buffer = Buffer.from(buffer.buffer, buffer.byteOffset + offset, length) + } else { + [port, address] = args + } + + let list + + if (!Array.isArray(buffer)) { + if (typeof buffer === 'string') { + list = [Buffer.from(buffer)] + } else if (!isArrayBufferView(buffer)) { + // throw new Error('Invalid buffer') + list = Buffer.from(buffer) + } else { + list = [buffer] + } + } else if (!(list = fixBufferList(buffer))) { + throw new Error('Invalid buffer') + } + + // @XXX(jwerle): @heapwolf why is this happening in a `send()` call? + // + // @jwerle it's from the node.js source code - https://github.com/nodejs/node/blob/main/lib/dgram.js#L645 + // but it's missing a check to see if the instance is unbound (state.bindState === BIND_STATE_UNBOUND) + /* + const { err: errBind } = this.bind({ port: 0 }, null) + + if (errBind) { + if (cb) return cb(errBind) + return { err: errBind } + } + */ + + if (list.length === 0) { + list.push(Buffer.alloc(0)) + } + + if (!connected && !isIPv4(address)) { + throw new Error('Currently dns lookup on send is not supported') + } + + const { err: errSend } = await ipc.write('udpSend', { + ephemeral: !this.clientId, + clientId: this.clientId || rand64(), + serverId: this.serverId || 0, + address, + port + }, list) + + if (errSend) { + if (cb) return cb(errSend) + return { err: errSend } + } + + if (cb) cb(null) + return {} + } + + /** + * Close the underlying socket and stop listening for data on it. If a + * callback is provided, it is added as a listener for the 'close' event. + * + * @param {function} callback - Called when the connection is completed or on error. + * + */ + async close (cb) { + if (typeof cb === 'function') { + this.on('close', cb) + } + + const { err } = await ipc.send('udpClose', { + id: this.clientId + }) + + if (err && cb) return cb(err) + if (err) return { err } + + this.emit('close') + return {} + } + + /** + * + * Returns an object containing the address information for a socket. For + * UDP sockets, this object will contain address, family, and port properties. + * + * This method throws EBADF if called on an unbound socket. + * @returns {Object} socketInfo - Information about the local socket + * @returns {string} socketInfo.address - The IP address of the socket + * @returns {ip} socketInfo.ip - The IP address of the socket + * @returns {string} socketInfo.port - The port of the socket + * @returns {string} socketInfo.family - The IP family of the socket + */ + address () { + return { + address: this._address, + ip: this._address, + port: this._port, + family: this._family + } + } + + /** + * Returns an object containing the address, family, and port of the remote + * endpoint. This method throws an ERR_SOCKET_DGRAM_NOT_CONNECTED exception + * if the socket is not connected. + * + * @returns {Object} socketInfo - Information about the remote socket + * @returns {string} socketInfo.remoteAddress - The IP address of the socket + * @returns {ip} socketInfo.remoteIp - The IP address of the socket + * @returns {string} socketInfo.remotePort - The port of the socket + * @returns {string} socketInfo.remoteFamily - The IP family of the socket + */ + remoteAddress () { + return { + remoteIp: this._remoteAddress, + remoteAddress: this._remoteAddress, + remotePort: this._remotePort, + remoteFamily: this._remoteFamily + } + } + + // + // Sets the SO_RCVBUF socket option. Sets the maximum socket receive buffer in + // bytes. + // + setRecvBufferSize (size) { + this.state.recvBufferSize = size + } + + // + // Sets the SO_SNDBUF socket option. Sets the maximum socket send buffer in + // bytes. + // + setSendBufferSize (size) { + this.state.sendBufferSize = size + } + + getRecvBufferSize () { + return this.state.recvBufferSize + } + + getSendBufferSize () { + return this.state.sendBufferSize + } + + // + // For now wer aren't going to implement any of the multicast options, + // mainly because 1. we don't need it in hyper and 2. if a user wants + // to deploy their app to the app store, they will need to request the + // multicast entitlement from apple. If someone really wants this they + // can implement it. + // + setBroadcast () { + throw new Error('not implemented') + } + + setTTL () { + throw new Error('not implemented') + } + + setMulticastTTL () { + throw new Error('not implemented') + } + + setMulticastLoopback () { + throw new Error('not implemented') + } + + setMulticastMembership () { + throw new Error('not implemented') + } + + setMulticastInterface () { + throw new Error('not implemented') + } + + addMembership () { + throw new Error('not implemented') + } + + dropMembership () { + throw new Error('not implemented') + } + + addSourceSpecificMembership () { + throw new Error('not implemented') + } + + dropSourceSpecificMembership () { + throw new Error('not implemented') + } + + ref () { + return this + } + + unref () { + return this + } +} + +export const createSocket = (type, listener) => { + return new Socket({ type, listener }) +} diff --git a/sdk/fs.js b/sdk/fs.js new file mode 100644 index 00000000..c22c5621 --- /dev/null +++ b/sdk/fs.js @@ -0,0 +1,6 @@ +/** + * @notice This is a rexports of `fs/index.js` so consumers will + * need to only `import * as fs from '@socketsupply/io/fs.js'` + */ + +export * from './fs/index.js' diff --git a/fs/binding.js b/sdk/fs/binding.js similarity index 100% rename from fs/binding.js rename to sdk/fs/binding.js diff --git a/fs/constants.js b/sdk/fs/constants.js similarity index 63% rename from fs/constants.js rename to sdk/fs/constants.js index 8d339034..06f76f51 100644 --- a/fs/constants.js +++ b/sdk/fs/constants.js @@ -1,23 +1,87 @@ -import { sendSync } from '../ipc.js' +//import { sendSync } from '../ipc.js' + +//var constants = require('fs').constants +//for(var k in constants) { console.log('export const', k, '=', constants[k]) } + + +export const UV_FS_SYMLINK_DIR = 1 +export const UV_FS_SYMLINK_JUNCTION = 2 +export const O_RDONLY = 0 +export const O_WRONLY = 1 +export const O_RDWR = 2 +export const UV_DIRENT_UNKNOWN = 0 +export const UV_DIRENT_FILE = 1 +export const UV_DIRENT_DIR = 2 +export const UV_DIRENT_LINK = 3 +export const UV_DIRENT_FIFO = 4 +export const UV_DIRENT_SOCKET = 5 +export const UV_DIRENT_CHAR = 6 +export const UV_DIRENT_BLOCK = 7 +export const S_IFMT = 61440 +export const S_IFREG = 32768 +export const S_IFDIR = 16384 +export const S_IFCHR = 8192 +export const S_IFBLK = 24576 +export const S_IFIFO = 4096 +export const S_IFLNK = 40960 +export const S_IFSOCK = 49152 +export const O_CREAT = 64 +export const O_EXCL = 128 +export const UV_FS_O_FILEMAP = 0 +export const O_NOCTTY = 256 +export const O_TRUNC = 512 +export const O_APPEND = 1024 +export const O_DIRECTORY = 65536 +export const O_NOATIME = 262144 +export const O_NOFOLLOW = 131072 +export const O_SYNC = 1052672 +export const O_DSYNC = 4096 +export const O_DIRECT = 16384 +export const O_NONBLOCK = 2048 +export const S_IRWXU = 448 +export const S_IRUSR = 256 +export const S_IWUSR = 128 +export const S_IXUSR = 64 +export const S_IRWXG = 56 +export const S_IRGRP = 32 +export const S_IWGRP = 16 +export const S_IXGRP = 8 +export const S_IRWXO = 7 +export const S_IROTH = 4 +export const S_IWOTH = 2 +export const S_IXOTH = 1 +export const F_OK = 0 +export const R_OK = 4 +export const W_OK = 2 +export const X_OK = 1 +export const UV_FS_COPYFILE_EXCL = 1 +export const COPYFILE_EXCL = 1 +export const UV_FS_COPYFILE_FICLONE = 2 +export const COPYFILE_FICLONE = 2 +export const UV_FS_COPYFILE_FICLONE_FORCE = 4 +export const COPYFILE_FICLONE_FORCE = 4 + + +/* const constants = sendSync('getFSConstants')?.data || {} /** * This flag can be used with uv_fs_copyfile() to return an error if the * destination already exists. - */ + * / export const COPYFILE_EXCL = 0x0001 /** * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. * If copy-on-write is not supported, a fallback copy mechanism is used. - */ + * / export const COPYFILE_FICLONE = 0x0002 /** * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. * If copy-on-write is not supported, an error is returned. - */ + * / export const COPYFILE_FICLONE_FORCE = 0x0004 export const UV_DIRENT_UNKNOWN = constants.UV_DIRENT_UNKNOWN || 0 @@ -76,3 +140,4 @@ export const F_OK = constants.F_OK || 0 export const R_OK = constants.R_OK || 0 export const W_OK = constants.W_OK || 0 export const X_OK = constants.X_OK || 0 +*/ \ No newline at end of file diff --git a/fs/dir.js b/sdk/fs/dir.js similarity index 100% rename from fs/dir.js rename to sdk/fs/dir.js diff --git a/fs/fds.js b/sdk/fs/fds.js similarity index 100% rename from fs/fds.js rename to sdk/fs/fds.js diff --git a/fs/flags.js b/sdk/fs/flags.js similarity index 100% rename from fs/flags.js rename to sdk/fs/flags.js diff --git a/fs/handle.js b/sdk/fs/handle.js similarity index 99% rename from fs/handle.js rename to sdk/fs/handle.js index a49b545f..3af17305 100644 --- a/fs/handle.js +++ b/sdk/fs/handle.js @@ -6,18 +6,18 @@ import { splitBuffer, rand64, clamp -} from '../util.js' +} from '../../util.js' import { ReadStream, WriteStream } from './stream.js' import { normalizeFlags } from './flags.js' -import { EventEmitter } from '../events.js' -import { AbortError } from '../errors.js' +import { EventEmitter } from '../../events.js' +import { AbortError } from '../../errors.js' import { Buffer } from 'buffer' import { Stats } from './stats.js' import { F_OK } from './constants.js' import * as ipc from '../ipc.js' import fds from './fds.js' -import gc from '../gc.js' +import gc from '../../gc.js' export const kOpening = Symbol.for('fs.FileHandle.opening') export const kClosing = Symbol.for('fs.FileHandle.closing') diff --git a/fs/index.js b/sdk/fs/index.js similarity index 98% rename from fs/index.js rename to sdk/fs/index.js index be0e19dd..d86afeae 100644 --- a/fs/index.js +++ b/sdk/fs/index.js @@ -20,16 +20,16 @@ import { Dir, Dirent, sortDirectoryEntries } from './dir.js' import { DirectoryHandle, FileHandle } from './handle.js' import { ReadStream, WriteStream } from './stream.js' -import { isBufferLike, isFunction, promisify } from '../util.js' +import { isBufferLike, isFunction, promisify } from '../../util.js' import * as constants from './constants.js' import * as promises from './promises.js' import { Stats } from './stats.js' import * as binding from './binding.js' -import * as ipc from '../ipc.js' +import * as ipc from '../../ipc.js' import fds from './fds.js' -import gc from '../gc.js' +import gc from '../../gc.js' -export * from './stream.js' +//export * from '../../stream.js' export { default as binding } from './binding.js' function defaultCallback (err) { @@ -550,7 +550,7 @@ export function writeFile (path, data, options, callback) { export function writev (fd, buffers, position, callback) { } -// re-exports +// re-export export { constants, Dir, diff --git a/fs/promises.js b/sdk/fs/promises.js similarity index 100% rename from fs/promises.js rename to sdk/fs/promises.js diff --git a/fs/stats.js b/sdk/fs/stats.js similarity index 100% rename from fs/stats.js rename to sdk/fs/stats.js diff --git a/fs/stream.js b/sdk/fs/stream.js similarity index 99% rename from fs/stream.js rename to sdk/fs/stream.js index 67219bf8..b75d8b0e 100644 --- a/fs/stream.js +++ b/sdk/fs/stream.js @@ -1,4 +1,4 @@ -import { Readable, Writable } from '../stream.js' +import { Readable, Writable } from '../../stream.js' import { Buffer } from 'buffer' export const DEFAULT_STREAM_HIGH_WATER_MARK = 16 * 1024 diff --git a/sdk/ipc.js b/sdk/ipc.js new file mode 100644 index 00000000..a3c55e4f --- /dev/null +++ b/sdk/ipc.js @@ -0,0 +1,618 @@ +/* global window */ + +/* + * @module IPC + * + * There are three important concepts for an application built with the Socket + * SDK. The `Render` process, the `Main` process, and the `Bridge` process. + * + * `IPC` is an acronym for Inter Process Communication. It's the method for + * which these [processes][processes] work together. + * + * The Bridge process handles communication between the Render and Main + * processes. For Desktop apps, the Render process is the user interface, and + * the Main process, which is optional, is strictly for computing and IO. + * + * When an applicaiton starts, the Bridge process will spawn a child process + * if one is specified. + * + * 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. + * + * ```uri + * ipc://command?key1=value1&key2=value2... + * ``` + * + * The query is encoded with `encodeURIComponent`. + * + * Here is a reference [implementation][0] if you would like to use a language + * that does not yet have one. + */ + +import { + AbortError, + InternalError, + TimeoutError +} from '../errors.js' + +import * as errors from '../errors.js' + +function getErrorClass (type, fallback) { + if (typeof window !== 'undefined' && typeof window[type] === 'function') { + return window[type] + } + + if (typeof errors[type] === 'function') { + return errors[type] + } + + return fallback || Error +} + +function maybeMakeError (error, caller) { + const errors = { + AbortError: getErrorClass('AbortError'), + AggregateError: getErrorClass('AggregateError'), + EncodingError: getErrorClass('EncodingError'), + IndexSizeError: getErrorClass('IndexSizeError'), + InternalError: InternalError, + InvalidAccessError: getErrorClass('InvalidAccessError'), + NetworkError: getErrorClass('NetworkError'), + NotAllowedError: getErrorClass('NotAllowedError'), + NotFoundError: getErrorClass('NotFoundError'), + NotSupportedError: getErrorClass('NotSupportedError'), + OperationError: getErrorClass('OperationError'), + RangeError: getErrorClass('RangeError'), + TimeoutError: 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 (type in errors) { + err = new errors[type](error.message || '', error) + } else { + for (const E of Object.values(errors)) { + if ((E.code && type === E.code) || (code && code === E.code)) { + err = new E(error.message || '', error) + } + } + } + + if (!err) { + err = new Error(error.message || '', error) + } + + // assign extra data to `err` like an error `code` + for (const key in error) { + try { + err[key] = error[key] + } catch (err) { + void err + } + } + + if ( + typeof Error.captureStackTrace === 'function' && + typeof caller === 'function' + ) { + Error.captureStackTrace(err, caller) + } + + return err +} + +/** + * Represents an OK IPC status. + */ +export const OK = 0 + +/** + * Represents an ERROR IPC status. + */ +export const ERROR = 1 + +/** + * Timeout in milliseconds for IPC requests. + */ +export const TIMEOUT = 32 * 1000 + +/** + * Symbol for the `ipc.debug.enabled` property + */ +export const kDebugEnabled = Symbol.for('ipc.debug.enabled') + +/** + * Parses `seq` as integer value + * @param {string|number} seq + * @param {?(object)} [options] + * @param {boolean} [options.bigint = false] + */ +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} + */ +export function debug (enable) { + if (enable === true) { + debug.enabled = true + } else if (enable === false) { + debug.enabled = false + } + + return debug.enabled +} + +Object.defineProperty(debug, 'enabled', { + enumerable: false, + set (value) { + debug[kDebugEnabled] = Boolean(value) + }, + + get () { + if (debug[kDebugEnabled] === undefined) { + return typeof window === 'undefined' + ? false + : Boolean(window.process?.debug) + } + + return debug[kDebugEnabled] + } +}) + +/** + * 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?]` + */ +export class Result { + + /** + * Creates a `Result` instance from input that may be an object + * like `{ err?, data? }`, an `Error` instance, or just `data`. + * @param {?(object|Error|mixed)} result + * @return {Result} + */ + static from (result) { + if (result instanceof Result) { + return result + } + + if (result instanceof Error) { + return new this(null, result) + } + + const err = maybeMakeError(result?.err, Result.from) + const data = result?.data !== null && result?.data !== undefined + ? result.data + : (result?.err ? null : result) + + return new this(data, err) + } + + /** + * `Result` class constructor. + * @private + * @param {?(object)} data + * @param {?(Error)} err + */ + constructor (data, err) { + this.data = data || null + this.err = err || null + + Object.defineProperty(this, 0, { + get: () => this.data, + enumerable: false, + configurable: false + }) + + Object.defineProperty(this, 1, { + get: () => this.err, + enumerable: false, + configurable: false + }) + } + + get length () { + if (this.data !== null && this.err !== null) { + return 2 + } else if (this.data !== null || this.err !== null) { + return 1 + } + + return 0 + } + + *[Symbol.iterator]() { + yield this.data + yield this.err + } +} + +/** + * Waits for the native IPC layer to be ready and exposed on the + * global window object. + */ +export async function ready () { + await new Promise((resolve, reject) => { + if (typeof window === 'undefined') { + return reject(new TypeError('Global window object is not defined.')) + } + + return loop() + + function loop () { + if (window._ipc) { + return resolve() + } + + queueMicrotask(loop) + } + }) +} + +/** + * Sends a synchronous IPC command over XHR returning a `Result` + * upon success or error. + * @param {string} command + * @param {?(object|string)} params + * @return {Result} + */ +export function sendSync (command, params) { + if (typeof window === 'undefined') { + if (debug.enabled) { + console.debug('Global window object is not defined') + } + + return {} + } + + const request = new window.XMLHttpRequest() + const index = window.process ? window.process.index : 0 + const seq = window._ipc ? window._ipc.nextSeq++ : 0 + const uri = `ipc://${command}` + + params = new URLSearchParams(params) + params.set('index', index) + params.set('seq', 'R' + seq) + + const query = `?${params}` + + if (debug.enabled) { + console.debug('io.ipc.sendSync: %s', uri + query) + } + + request.open('GET', uri + query, false) + request.send() + + const response = request.response || request.responseText + + try { + return Result.from(JSON.parse(response)) + } catch (err) { + if (debug.enabled) { + console.warn('ipc.sendSync: error:', err.message || err) + } + } + + return Result.from(response) +} + +export async function emit (...args) { + await ready() + + if (debug.enabled) { + console.debug('io.ipc.emit:', ...args) + } + + return await window._ipc.emit(...args) +} + +export async function resolve (...args) { + await ready() + + if (debug.enabled) { + console.debug('io.ipc.resolve:', ...args) + } + + return await window._ipc.resolve(...args) +} + +export async function send (command, ...args) { + await ready() + + if (debug.enabled) { + console.debug('io.ipc.send:', command, ...args) + } + + const response = await window._ipc.send(command, ...args) + const result = Result.from(response) + + if (debug.enabled) { + console.debug('io.ipc.send: (resolved)', command, result) + } + + return result +} + +export async function write (command, params, buffer, options) { + if (typeof window === 'undefined') { + console.warn('Global window object is not defined') + return {} + } + + const signal = options?.signal + const request = new window.XMLHttpRequest() + const index = window.process ? window.process.index : 0 + const seq = window._ipc ? window._ipc.nextSeq++ : 0 + const uri = `ipc://${command}` + + let resolved = false + let aborted = false + let timeout = null + + if (signal) { + if (signal.aborted) { + return Result.from(new AbortError(signal)) + } + + signal.addEventListener('abort', () => { + if (!aborted && !resolved) { + aborted = true + request.abort() + } + }) + } + + params = new URLSearchParams(params) + params.set('index', index) + params.set('seq', 'R' + seq) + + const query = `?${params}` + + request.open('PUT', uri + query, true) + request.send(buffer || null) + + if (debug.enabled) { + console.debug('io.ipc.write:', uri + query, buffer || null) + } + + return await new Promise((resolve) => { + if (options?.timeout) { + timeout = setTimeout(() => { + resolve(Result.from(new TimeoutError('ipc.write timedout'))) + request.abort() + }, typeof options.timeout === 'number' ? options.timeout : TIMEOUT) + } + + request.onabort = () => { + aborted = true + if (options?.timeout) { + clearTimeout(timeout) + } + resolve(Result.from(new AbortError(signal))) + } + + request.onreadystatechange = () => { + if (aborted) { + return + } + + if (request.readyState === window.XMLHttpRequest.DONE) { + resolved = true + clearTimeout(timeout) + + let data = request.response + try { + data = JSON.parse(request.response) + } catch (err) { + if (debug.enabled) { + console.warn(err.message || err) + } + } + + const result = Result.from(data) + + if (debug.enabled) { + console.debug('io.ipc.write: (resolved)', command, result) + } + + return resolve(data) + } + } + + request.onerror = () => { + resolved = true + clearTimeout(timeout) + resolve(Result.from(new Error(request.responseText))) + } + }) +} + +export async function request (command, data, options) { + await ready() + + const signal = options?.signal + const params = { ...data } + + for (const key in params) { + if (params[key] === undefined) { + delete params[key] + } + } + + if (debug.enabled) { + console.debug('io.ipc.request:', command, data) + } + + let aborted = false + let timeout = null + + const parent = typeof window === 'object' ? window : globalThis + const promise = parent._ipc.send(command, params) + + const { seq, index } = promise + const resolved = promise.then((response) => { + cleanup() + + let result = response + + if (result?.data instanceof ArrayBuffer) { + result = Result.from(new Uint8Array(result.data)) + } else { + result = Result.from(result) + } + + if (debug.enabled) { + console.debug('io.ipc.request: (resolved)', command, result) + } + + return result + }) + + const onabort = () => { + aborted = true + cleanup() + resolve(seq, ERROR, { + err: new TimeoutError('ipc.request timedout') + }) + } + + if (signal) { + if (signal.aborted) { + return Result.from(new AbortError(signal)) + } + + signal.addEventListener('abort', onabort) + } + + if (options?.timeout !== false) { + timeout = setTimeout( + onabort, + typeof options?.timeout === 'number' ? options.timeout : TIMEOUT + ) + } + + // handle async resolution from IPC over XHR + parent.addEventListener('data', ondata) + + return Object.assign(resolved, { seq, index }) + + function cleanup () { + window.removeEventListener('data', ondata) + + if (timeout) { + clearTimeout(timeout) + } + } + + function ondata (event) { + if (aborted) { + cleanup() + + return resolve(seq, ERROR, { + err: new AbortError(signal) + }) + } + + if (event.detail?.data) { + const { data, params } = event.detail + if (parseSeq(params.seq) === parseSeq(seq)) { + cleanup() + resolve(seq, OK, { data }) + } + } + } +} + +/** + * Factory for creating a proxy based IPC API. + * @param {string} domain + * @param {?(function|object)} ctx + * @param {?(string)} [ctx.default] + * @return {Proxy} + */ +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) + } + + const proxy = new Proxy(ctx, { + apply (target, bound, args) { + const chain = [...target.chain] + const domain = chain.shift() + const path = chain.join('.') + + target.chain = new Set() + + const method = (ctx[path]?.method || ctx[path]) || ctx.default || 'send' + return dispatchable[method](path, ...args) + }, + + get (target, key, receiver) { + if (key === '__proto__') return null + (ctx.chain ||= new Set()).add(key) + return new Proxy(ctx, this) + } + }) + + if (typeof domain === 'string') { + return proxy[domain] + } + + return domain +} + +export default { + OK, + ERROR, + TIMEOUT, + + createBinding, + debug, + emit, + ready, + resolve, + request, + send, + sendSync, + write +} diff --git a/sdk/os.js b/sdk/os.js new file mode 100644 index 00000000..497e9024 --- /dev/null +++ b/sdk/os.js @@ -0,0 +1,184 @@ +/** + * @module OS + * + * This module provides normalized system information from all the major + * operating systems. + */ + +import { toProperCase } from '../util.js' +import * as ipc from '../ipc.js' + +const UNKNOWN = 'unknown' + +const cache = { + arch: UNKNOWN, + type: UNKNOWN, + platform: UNKNOWN +} + +export function arch () { + let value = UNKNOWN + + if (cache.arch !== UNKNOWN) { + return cache.arch + } + + if (typeof window !== 'object') { + if (typeof process === 'object' && typeof process.arch === 'string') { + return process.arch + } + } + + if (typeof window === 'object') { + value = ( + window.process?.arch || + ipc.sendSync('getPlatformArch')?.data || + UNKNOWN + ) + } + + if (value === 'arm64') { + return value + } + + cache.arch = value + .replace('x86_64', 'x64') + .replace('x86', 'ia32') + .replace(/arm.*/, 'arm') + + return cache.arch +} + +export function networkInterfaces () { + const { ipv4, ipv6 } = ipc.sendSync('getNetworkInterfaces')?.data || {} + const interfaces = {} + + for (const type in ipv4) { + const address = ipv4[type] + const family = 'IPv4' + + let internal = false + let netmask = '255.255.255.0' + let cidr = `${address}/24` + let mac = null + + if (address === '127.0.0.1' || address === '0.0.0.0') { + internal = true + mac = '00:00:00:00:00:00' + + if (address === '127.0.0.1') { + cidr = '127.0.0.1/8' + netmask = '255.0.0.0' + } else { + cidr = '0.0.0.0/0' + netmask = '0.0.0.0' + } + } + + interfaces[type] = interfaces[type] || [] + interfaces[type].push({ + address, + netmask, + internal, + family, + cidr, + mac + }) + } + + for (const type in ipv6) { + const address = ipv6[type] + const family = 'IPv6' + + let internal = false + let netmask = 'ffff:ffff:ffff:ffff::' + let cidr = `${address}/64` + let mac = null + + if (address === '::1') { + internal = true + netmask = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + cidr = '::1/128' + mac = '00:00:00:00:00:00' + } + + interfaces[type] = interfaces[type] || [] + interfaces[type].push({ + address, + netmask, + internal, + family, + cidr, + mac + }) + } + + return interfaces +} + +export function platform () { + let value = UNKNOWN + + if (cache.platform !== UNKNOWN) { + return cache.platform + } + + if (typeof window !== 'object') { + if (typeof process === 'object' && typeof process.platform === 'string') { + return process.platform + } + } + + if (typeof window === 'object') { + value = ( + window.process?.os || + ipc.sendSync('getPlatformOS')?.data || + window.process?.platform || + UNKNOWN + ) + } + + cache.platform = value.replace(/^mac/i, 'darwin') + + return cache.platform +} + +export function type () { + let value = 'unknown' + + if (cache.type !== UNKNOWN) { + return cache.type + } + + if (typeof window !== 'object') { + switch (platform()) { + case 'linux': return 'Linux' + case 'mac': case 'darwin': return 'Darwin' + case 'win32': return 'Windows' // Windows_NT? + } + } + + if (typeof window == 'object') { + value = ( + window.process?.platform || + ipc.sendSync('getPlatformType')?.data || + UNKNOWN + ) + } + + if (value !== UNKNOWN) { + value = toProperCase(value) + } + + cache.type = value + + return cache.type +} + +export const EOL = (() => { + if (/^win/i.test(type())) { + return '\r\n' + } + + return '\n' +})() diff --git a/sdk/process.js b/sdk/process.js new file mode 100644 index 00000000..50d90b3b --- /dev/null +++ b/sdk/process.js @@ -0,0 +1,35 @@ +import { EventEmitter } from '../events.js' +import { send } from '../ipc.js' + +let didEmitExitEvent = false + +export function homedir () { + process.env.HOME || '' +} + +export function exit (code) { + if (!didEmitExitEvent) { + didEmitExitEvent = true + queueMicrotask(() => process.emit('exit', code)) + } + + send('exit', { value: code || 0 }) +} + +const parent = typeof window === 'object' ? window : globalThis +const isNode = parent?.process?.versions?.node +const process = isNode + ? globalThis.process + : Object.create(null, Object.getOwnPropertyDescriptors({ + ...EventEmitter.prototype, + homedir, + argv0: parent?.process?.argv?.[0], + exit, + ...parent?.process, + })) + +if (!isNode) { + EventEmitter.call(process) +} + +export default process diff --git a/test/src/dgram.js b/test/src/dgram.js index e9c4a4b3..33988323 100644 --- a/test/src/dgram.js +++ b/test/src/dgram.js @@ -1,5 +1,5 @@ -import * as dgram from '@socketsupply/io/dgram' +import * as dgram from '@socketsupply/io/dgram.js' //import dgram from 'dgram' //uncomment to tests the tests, should pass running node import { test } from 'tapzero' diff --git a/test/src/fs/flags.js b/test/src/fs/flags.js index 4a733192..6da8c13d 100644 --- a/test/src/fs/flags.js +++ b/test/src/fs/flags.js @@ -1,4 +1,4 @@ -import { normalizeFlags } from '@socketsupply/io/fs/flags.js' +import { normalizeFlags } from '@socketsupply/io/sdk/fs/flags.js' import { test } from 'tapzero' import * as fs from '@socketsupply/io/fs.js' diff --git a/test/src/fs/index.js b/test/src/fs/index.js index d968b74f..d86f7299 100644 --- a/test/src/fs/index.js +++ b/test/src/fs/index.js @@ -1,38 +1,35 @@ import { Buffer } from '@socketsupply/io' -import process from '@socketsupply/io/process.js' +import * as process from '@socketsupply/io/process.js' import * as fs from '@socketsupply/io/fs.js' import { test } from 'tapzero' test('fs.access', async (t) => { await new Promise((resolve, reject) => { - fs.access('fixtures', fs.constants.F_OK, (err, mode) => { + console.log(fs.access.toString()) + fs.access('fixtures', fs.constants.F_OK, (err) => { if (err) { return reject(err) } - t.ok(mode, '(F_OK) fixtures/ directory is accessible') resolve() }) }) await new Promise((resolve, reject) => { - fs.access('fixtures', fs.constants.F_OK | fs.constants.R_OK, (err, mode) => { + fs.access('fixtures', fs.constants.F_OK | fs.constants.R_OK, (err) => { if (err) { return reject(err) } - t.ok(mode, '(F_OK | R_OK) fixtures/ directory is readable') resolve() }) }) await new Promise((resolve, reject) => { - fs.access('fixtures', fs.constants.W_OK, (err, mode) => { + fs.access('fixtures', fs.constants.W_OK, (err) => { if (err) { return reject(err) } - t.ok(mode, '(W_OK) fixtures/ directory is writable') resolve() }) }) await new Promise((resolve, reject) => { - fs.access('fixtures', fs.constants.X_OK, (err, mode) => { + fs.access('fixtures', fs.constants.X_OK, (err) => { if (err) { return reject(err) } - t.ok(mode, '(X_OK) fixtures/ directory is "executable" - can list items') resolve() }) }) diff --git a/test/src/index.js b/test/src/index.js index 76cc7e38..158b36b1 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -1,11 +1,11 @@ -import process from '@socketsupply/io/process' -import ipc from '@socketsupply/io/ipc' +import * as process from '@socketsupply/io/process.js' +//import ipc from '@socketsupply/io/ipc.js' import { GLOBAL_TEST_RUNNER } from 'tapzero' const parent = typeof window === 'object' ? window : globalThis -ipc.debug.enabled = true +//ipc.debug.enabled = true if (typeof parent?.addEventListener === 'function') { parent.addEventListener('error', (err, url, line) => { diff --git a/test/src/os.js b/test/src/os.js index 3d022c43..4167c0ee 100644 --- a/test/src/os.js +++ b/test/src/os.js @@ -1,7 +1,9 @@ -import * as os from '@socketsupply/io/os' +import * as os from '@socketsupply/io/os.js' //import os from 'os' //uncomment to tests the tests, should pass running node import { test } from 'tapzero' +console.log(os) + test('os.arch()', (t) => { t.ok(os.arch(), 'os.arch()') }) From 8beeb73d01c275b60325caa1ac55e837dcbea6ca Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 21:26:00 +1200 Subject: [PATCH 3/9] run tests in node first, to confirm tests make sense --- test/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/package.json b/test/package.json index e74b5b3d..d64300d2 100644 --- a/test/package.json +++ b/test/package.json @@ -2,7 +2,8 @@ "type": "module", "private": true, "scripts": { - "test": "npm run test:desktop", + "test": "npm run test:node && npm run test:desktop", + "test:node": "node src/index.js", "test:desktop": "./scripts/test-desktop.sh", "test:android": "./scripts/test-android.sh", "test:android-emulator": "./scripts/test-android-emulator.sh", From 2c5896893ccfdd71b1ddc97014b6ebd19c467d16 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 23:30:29 +1200 Subject: [PATCH 4/9] send sync returns {err,data} --- test/src/fs/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/src/fs/index.js b/test/src/fs/index.js index 990812b0..0d5ba7e1 100644 --- a/test/src/fs/index.js +++ b/test/src/fs/index.js @@ -6,7 +6,6 @@ import { test } from 'tapzero' test('fs.access', async (t) => { await new Promise((resolve, reject) => { - console.log(fs.access.toString()) fs.access('fixtures', fs.constants.F_OK, (err) => { if (err) { return reject(err) } resolve() From a35086b2f6df05475531382b53e78b930ce4ae98 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 23:31:49 +1200 Subject: [PATCH 5/9] finish merging... --- sdk/dgram.js | 10 +++++----- sdk/os.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/sdk/dgram.js b/sdk/dgram.js index 06b5d921..bddf552b 100644 --- a/sdk/dgram.js +++ b/sdk/dgram.js @@ -1,10 +1,10 @@ import { Buffer } from 'buffer' -import { EventEmitter } from './events.js' -import { isIPv4 } from './net.js' -import * as dns from './dns.js' -import * as ipc from './ipc.js' -import { rand64, isArrayBufferView } from './util.js' +import { EventEmitter } from '../events.js' +import { isIPv4 } from '../net.js' +import * as dns from '../dns.js' +import * as ipc from '../ipc.js' +import { rand64, isArrayBufferView } from '../util.js' const BIND_STATE_UNBOUND = 0 const BIND_STATE_BINDING = 1 diff --git a/sdk/os.js b/sdk/os.js index 497e9024..b425b1af 100644 --- a/sdk/os.js +++ b/sdk/os.js @@ -129,15 +129,21 @@ export function platform () { } } + var {data, err} = ipc.sendSync('getPlatformOS') + + if(err) { + throw new Error('error in getPlatformOS') + } + if (typeof window === 'object') { value = ( window.process?.os || - ipc.sendSync('getPlatformOS')?.data || + data || window.process?.platform || UNKNOWN ) } - + console.log('value', value) cache.platform = value.replace(/^mac/i, 'darwin') return cache.platform From 9992fcec3dd8eae74431a0d1fe03cf0e1a7a9d8d Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 23:38:08 +1200 Subject: [PATCH 6/9] deduplicate node tests, just run the same tests in node --- package.json | 2 +- test/node/dgram.js | 1 - test/node/fs.js | 223 ------------------------- test/node/index.js | 5 - test/node/mock.js | 249 ---------------------------- test/node/net.js | 394 --------------------------------------------- test/node/os.js | 78 --------- 7 files changed, 1 insertion(+), 951 deletions(-) delete mode 100644 test/node/dgram.js delete mode 100644 test/node/fs.js delete mode 100644 test/node/index.js delete mode 100644 test/node/mock.js delete mode 100644 test/node/net.js delete mode 100644 test/node/os.js diff --git a/package.json b/package.json index 12e610aa..8187acaf 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "standardx -v", "readme": "node ./bin/generate-docs.js", "test": "cd test && npm install --silent --no-audit && npm test --silent", - "test:node": "node ./test/node/index.js", + "test:node": "cd test && npm run test:node", "test:android": "cd test && npm install --silent --no-audit && npm run test:android --silent", "test:android-emulator": "cd test && npm install --silent --no-audit && npm run test:android-emulator --silent", "test:clean": "cd test && rm -rf dist", diff --git a/test/node/dgram.js b/test/node/dgram.js deleted file mode 100644 index 70b786d1..00000000 --- a/test/node/dgram.js +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/test/node/fs.js b/test/node/fs.js deleted file mode 100644 index c4a375a8..00000000 --- a/test/node/fs.js +++ /dev/null @@ -1,223 +0,0 @@ -import { promisify } from 'node:util' -import { test } from 'tapzero' - -import mock from './mock.js' - -import { normalizeFlags } from '../../fs/flags.js' -import * as fs from '../../fs/index.js' - -test('FileHandle', async t => { - mock.create(t, 'fsOpen', {}, { err: null, data: {} }) - - const handle = await fs.promises.open('./foo.txt') - t.ok(handle.id.toString().length, 'handle provides an id') -}) - -test('flags', (t) => { - t.ok( - normalizeFlags() === fs.constants.O_RDONLY, - 'undefined === fs.constants.O_RDONLY' - ) - - t.ok( - normalizeFlags(fs.constants.O_WRONLY) === fs.constants.O_WRONLY, - 'fs.constants.O_WRONLY=== fs.constants.O_WRONLY' - ) - - t.throws( - () => normalizeFlags(null), - RegExp('Expecting flags to be a string or number: Got object'), - 'normalizeFlags() throws on null' - ) - - t.throws(() => - normalizeFlags({}), - RegExp('Expecting flags to be a string or number: Got object'), - 'normalizeFlags() throws on object' - ) - - t.throws( - () => normalizeFlags(true), - RegExp('Expecting flags to be a string or number: Got boolean'), - 'normalizeFlags() throws on boolean' - ) - - t.ok( - normalizeFlags('r') === fs.constants.O_RDONLY, - 'r === fs.constants.O_RDONLY' - ) - - t.ok( - normalizeFlags('rs') === fs.constants.O_RDONLY | fs.constants.O_SYNC, - 'rs === fs.constants.O_RDONLY | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('sr') === fs.constants.O_RDONLY | fs.constants.O_SYNC, 'sr === fs.constants.O_RDONLY | fs.constants.O_SYNC') - - t.ok( - normalizeFlags('r+') === fs.constants.O_RDWR, - 'r+ === fs.constants.O_RDWR' - ) - - t.ok( - normalizeFlags('rs+') === fs.constants.O_RDWR | fs.constants.O_SYNC, - 'rs+ === fs.constants.O_RDWR | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('sr+') === fs.constants.O_RDWR | fs.constants.O_SYNC, - 'sr+ === fs.constants.O_RDWR | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('w') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY, - 'w === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY' - ) - - t.ok( - normalizeFlags('wx') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, - 'wx === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('xw') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, - 'xw === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('w+') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR, - 'w+ === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR' - ) - - t.ok( - normalizeFlags('wx+') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, - 'wx+ === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' - ) - t.ok( - normalizeFlags('xw+') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, 'xw+ === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL') - - t.ok( - normalizeFlags('a') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, - 'a === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY' - ) - - t.ok( - normalizeFlags('ax') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, - 'ax === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('xa') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, - 'xa === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('as') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC, - 'as === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('sa') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC, - 'sa === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('a+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR, - 'a+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR' - ) - - t.ok( - normalizeFlags('ax+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, - 'ax+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('xa+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, - 'xa+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' - ) - - t.ok( - normalizeFlags('as+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC, - 'as+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC' - ) - - t.ok( - normalizeFlags('sa+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC, - 'sa+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC' - ) -}) - -test('constants', (t) => { - t.ok( - fs.constants.COPYFILE_EXCL === 0x0001, - 'fs.fs.constants.COPYFILE_EXCL' - ) - - t.ok( - fs.constants.COPYFILE_FICLONE === 0x0002, - 'fs.fs.constants.COPYFILE_FICLONE' - ) - - t.ok( - fs.constants.COPYFILE_FICLONE_FORCE === 0x0004, - 'fs.fs.constants.COPYFILE_FICLONE_FORCE' - ) - - t.ok(typeof fs.constants.O_RDONLY === 'number', 'fs.constants.O_RDONLY') - t.ok(typeof fs.constants.O_WRONLY === 'number', 'fs.constants.O_WRONLY') - t.ok(typeof fs.constants.O_RDWR === 'number', 'fs.constants.O_RDWR') - t.ok(typeof fs.constants.O_APPEND === 'number', 'fs.constants.O_APPEND') - t.ok(typeof fs.constants.O_ASYNC === 'number', 'fs.constants.O_ASYNC') - t.ok(typeof fs.constants.O_CLOEXEC === 'number', 'fs.constants.O_CLOEXEC') - t.ok(typeof fs.constants.O_CREAT === 'number', 'fs.constants.O_CREAT') - t.ok(typeof fs.constants.O_DIRECT === 'number', 'fs.constants.O_DIRECT') - t.ok(typeof fs.constants.O_DIRECTORY === 'number', 'fs.constants.O_DIRECTORY') - t.ok(typeof fs.constants.O_DSYNC === 'number', 'fs.constants.O_DSYNC') - t.ok(typeof fs.constants.O_EXCL === 'number', 'fs.constants.O_EXCL') - t.ok(typeof fs.constants.O_LARGEFILE === 'number', 'fs.constants.O_LARGEFILE') - t.ok(typeof fs.constants.O_NOATIME === 'number', 'fs.constants.O_NOATIME') - t.ok(typeof fs.constants.O_NOCTTY === 'number', 'fs.constants.O_NOCTTY') - t.ok(typeof fs.constants.O_NOFOLLOW === 'number', 'fs.constants.O_NOFOLLOW') - t.ok(typeof fs.constants.O_NONBLOCK === 'number', 'fs.constants.O_NONBLOCK') - t.ok(typeof fs.constants.O_NDELAY === 'number', 'fs.constants.O_NDELAY') - t.ok(typeof fs.constants.O_PATH === 'number', 'fs.constants.O_PATH') - t.ok(typeof fs.constants.O_SYNC === 'number', 'fs.constants.O_SYNC') - t.ok(typeof fs.constants.O_TMPFILE === 'number', 'fs.constants.O_TMPFILE') - t.ok(typeof fs.constants.O_TRUNC === 'number', 'fs.constants.O_TRUNC') - - t.ok(typeof fs.constants.S_IFMT === 'number', 'fs.constants.S_IFMT') - t.ok(typeof fs.constants.S_IFREG === 'number', 'fs.constants.S_IFREG') - t.ok(typeof fs.constants.S_IFDIR === 'number', 'fs.constants.S_IFDIR') - t.ok(typeof fs.constants.S_IFCHR === 'number', 'fs.constants.S_IFCHR') - t.ok(typeof fs.constants.S_IFBLK === 'number', 'fs.constants.S_IFBLK') - t.ok(typeof fs.constants.S_IFIFO === 'number', 'fs.constants.S_IFIFO') - t.ok(typeof fs.constants.S_IFLNK === 'number', 'fs.constants.S_IFLNK') - t.ok(typeof fs.constants.S_IFSOCK === 'number', 'fs.constants.S_IFSOCK') - t.ok(typeof fs.constants.S_IRWXU === 'number', 'fs.constants.S_IRWXU') - t.ok(typeof fs.constants.S_IRUSR === 'number', 'fs.constants.S_IRUSR') - t.ok(typeof fs.constants.S_IWUSR === 'number', 'fs.constants.S_IWUSR') - t.ok(typeof fs.constants.S_IXUSR === 'number', 'fs.constants.S_IXUSR') - t.ok(typeof fs.constants.S_IRWXG === 'number', 'fs.constants.S_IRWXG') - t.ok(typeof fs.constants.S_IRGRP === 'number', 'fs.constants.S_IRGRP') - t.ok(typeof fs.constants.S_IWGRP === 'number', 'fs.constants.S_IWGRP') - t.ok(typeof fs.constants.S_IXGRP === 'number', 'fs.constants.S_IXGRP') - t.ok(typeof fs.constants.S_IRWXO === 'number', 'fs.constants.S_IRWXO') - t.ok(typeof fs.constants.S_IROTH === 'number', 'fs.constants.S_IROTH') - t.ok(typeof fs.constants.S_IWOTH === 'number', 'fs.constants.S_IWOTH') - t.ok(typeof fs.constants.S_IXOTH === 'number', 'fs.constants.S_IXOTH') - - t.ok(typeof fs.constants.F_OK === 'number', 'fs.constants.F_OK') - t.ok(typeof fs.constants.R_OK === 'number', 'fs.constants.R_OK') - t.ok(typeof fs.constants.W_OK === 'number', 'fs.constants.W_OK') - t.ok(typeof fs.constants.X_OK === 'number', 'fs.constants.X_OK') -}) - -test('nodejs.util.promisify.custom', (t) => { - for (const key in fs) { - const value = fs[key] - if (typeof fs.promises[key] === 'function') { - t.equal(promisify(value), fs.promises[key], `promisify(fs.${key})`) - } - } -}) diff --git a/test/node/index.js b/test/node/index.js deleted file mode 100644 index 0c0d8923..00000000 --- a/test/node/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import './mock.js' -import './dgram.js' -import './fs.js' -import './net.js' -import './os.js' diff --git a/test/node/mock.js b/test/node/mock.js deleted file mode 100644 index 5db37d10..00000000 --- a/test/node/mock.js +++ /dev/null @@ -1,249 +0,0 @@ -import { EventEmitter } from 'node:events' -import { webcrypto } from 'node:crypto' -import fs from 'node:fs' -import os from 'node:os' - -Object.assign(globalThis, { - crypto: webcrypto -}) - -export const methods = {} - -const CustomEventDispatched = Symbol.for('CustomEvent.dispatched') - -class XMLHttpRequest { - static get UNSENT () { return 0 } - static get OPENED () { return 1 } - static get HEADERS_RECEIVED () { return 2 } - static get LOADING () { return 3 } - static get DONE () { return 4 } - - constructor () { - this.aborted = false - this.response = null - this.responseText = null - this.readyState = XMLHttpRequest.UNSENT - } - - abort () { - this.aborted = true - if (typeof this.onabort === 'function') { - this.onabort(new Event('abort')) - } - } - - open (method, url, isAsync) { - if (this.aborted) { - return - } - - this.url = url - this.method = method - this.isAsync = isAsync - this.readyState = XMLHttpRequest.OPENED - } - - send (value) { - if (this.aborted) { - return - } - - const { host: name } = new URL(this.url) - let mock - - console.log('call XHR mock:', name, value) - - if (methods[name] && methods[name].length) { - if (Array.isArray(methods[name])) { - mock = methods[name].shift() - - if (methods[name].length === 0) { - delete methods[name] - } - } else { - mock = methods[name] - } - } - - return send(this) - - function updateReadyState (request, value) { - if (request.aborted) { - return - } - - if (typeof request.onreadystatechange === 'function') { - request.readyState = value - request.onreadystatechange(new Event('readystatechange')) - } - } - - function send (request) { - if (!request.isAsync) { - return done(request) - } - - process.nextTick(() => { - updateReadyState(request, XMLHttpRequest.HEADERS_RECEIVED) - }) - - process.nextTick(() => { - updateReadyState(request, XMLHttpRequest.LOADING) - }) - - process.nextTick(() => { - done(request) - }) - } - - function done (request) { - const response = typeof mock === 'function' ? mock(value) : null - - request.response = response - - if (typeof response === 'string') { - request.responseText = response - } else { - request.responseText = JSON.stringify(response) - } - - updateReadyState(request, XMLHttpRequest.DONE) - } - } -} - -class Event { - constructor (type, options) { - if (this.constructor === Event) { - throw new TypeError('Illegal constructor') - } - - this.type = type.toLowerCase() - this.bubbles = options?.bubbles || false - this.composed = options?.composed || false - this.cancelable = options?.cancelable || false - } -} - -class CustomEvent extends Event { - constructor (type, options) { - super(type, options) - this.detail = options?.detail !== undefined ? options.detail : null - this[CustomEventDispatched] = false - } - - initCustomEvent (type, bubbles, cancelable, detail) { - this.type = type - this.detail = detail !== undefined ? detail : null - this.bubbles = bubbles || false - this.cancelable = cancelable || false - } -} - -global.window = Object.assign(new EventEmitter(), { - XMLHttpRequest, - CustomEvent, - Event, - crypto: webcrypto, - - addEventListener (event, fn, opts) { - if (opts?.once) { - this.once(event.toLowerCase(), fn) - } else { - this.on(event.toLowerCase(), fn) - } - }, - - removeEventListener (event, fn) { - this.off(event.toLowerCase(), fn) - }, - - dispatchEvent (event) { - this.emit(event.type.toLowerCase(), event) - }, - - process: { - index: 0, - platform: process.platform - }, - - _ipc: { - nextSeq: 1, - streams: {}, - send (name, value) { - let mock - console.log('call mock:', name, value) - - if (methods[name] === null || methods[name].length === 0) { - throw new Error('unexpected: ' + name + ', ' + JSON.stringify(value)) - } - - if (Array.isArray(methods[name])) { - mock = methods[name].shift() - - if (methods[name].length === 0) { - delete methods[name] - } - } else { - mock = methods[name] - } - return mock(value) - }, - - async resolve (seq, status, value) { - if (typeof value === 'string') { - let didDecodeURIComponent = false - try { - value = decodeURIComponent(value) - didDecodeURIComponent = true - } catch (err) { - console.error(`${err.message} (${value})`) - return - } - - try { - value = JSON.parse(value) - } catch (err) { - if (!didDecodeURIComponent) { - console.error(`${err.message} (${value})`) - return - } - } - } - - if (!window._ipc[seq]) { - console.error('inbound IPC message with unknown sequence:', seq, value) - return - } - - if (status === 0) { - await window._ipc[seq].resolve(value) - } else { - const err = new Error(typeof value === 'string' ? value : JSON.stringify(value)) - await window._ipc[seq].reject(err) - } - - delete window._ipc[seq] - } - } -}) - -export function create (t, name, args, result, isAsync = true) { - methods[name] = methods[name] || [] - - methods[name].push((_args) => { - for (const k in args) { - t.equal(_args[k], args[k], `property: ${k}`) - } - - if (isAsync) { - return Promise.resolve(result) - } - - return result - }) -} - -import * as exports from './mock.js' -export default exports - diff --git a/test/node/net.js b/test/node/net.js deleted file mode 100644 index ebe27b56..00000000 --- a/test/node/net.js +++ /dev/null @@ -1,394 +0,0 @@ -import mock from './mock.js' - -import { test } from 'tapzero' -import { rand64 } from '../../util.js' -import * as net from '../../net.js' - -// createServer, call listen, close server -test('net.createServer', async t => { - return new Promise((resolve) => { - const server = net.createServer(() => { - // no actual connections on this test - }) - const ID = server._serverId - // should not have sent a message yet - mock.create(t, 'tcpCreateServer', - { port: 9000, address: '127.0.0.1' }, - { data: { serverId: ID, port: 9000, address: '127.0.0.1', family: 'IPv4' } } - ) - - // unref does nothing, but returns self - t.equal(server.unref(), server) - - // the default behaviour seems to be to listen on IPv6, - // guessing that probably depends on the system though. - server.listen(9000, '127.0.0.1', function () { - t.deepEqual( - server.address(), - { port: 9000, address: '127.0.0.1', family: 'IPv4' } - ) - - mock.create(t, 'tcpClose', { serverId: ID }, {}) - - server.close(function () { - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }) - }) -}) - -// net.connect returns socket, write data, receive data, end stream - -test('net.connect', async t => { - return new Promise((resolve) => { - const ID = rand64() - const HELLO = 'Hello, World!\n' - - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - - const _stream = net.connect(9000, '127.0.0.1', function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, false) - - const ID = _stream.clientId - - mock.create(t, 'tcpSend', - { clientId: ID, data: HELLO }, - {} - ) - - mock.methods.tcpReadStart = [q => { - t.deepEqual(q, { clientId: ID }) - return (async () => { - return {} - })() - }] - - // using setTimeout here is a sign we don't understand something. - // - setTimeout(() => { - t.deepEqual(mock.methods, {}, 'no uncalled methods') - mock.create(t, 'tcpShutdown', - { clientId: ID }, - {} - ) - mock.create(t, 'tcpClose', - { clientId: ID }, - {} - ) - stream.__write('') - - stream.end() - stream.on('close', () => { - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }, 100) - stream.write(HELLO) - }) - - t.ok(_stream) - }) -}) - -test('net.connect, allowHalfOpen=false', async (t) => { - return new Promise((resolve) => { - const ID = rand64() - let ended = false - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - - const _stream = net.connect(9000, '127.0.0.1', function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, false) - - const ID = _stream.clientId - - stream.on('end', function () { - ended = true - }) - mock.create(t, 'tcpShutdown', - { clientId: ID }, - {} - ) - mock.create(t, 'tcpClose', - { clientId: ID }, - {} - ) - stream.end() - stream.__write('') - - stream.on('close', () => { - t.ok(ended) - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }) - t.ok(_stream) - }) -}) - -test('net.connect allowHalfOpen=true', async (t) => { - return new Promise((resolve) => { - const ID = rand64() - let ended = false - - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - const _stream = net.connect({ - port: 9000, - host: '127.0.0.1', - allowHalfOpen: true - }, function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, true) - - const ID = _stream.clientId - - stream.on('end', function () { - ended = true - stream.end() - }) - - mock.create(t, 'tcpShutdown', - { clientId: ID }, - {} - ) - - mock.create(t, 'tcpClose', - { clientId: ID }, - {} - ) - - stream.__write('') - - stream.on('close', () => { - t.ok(ended) - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }) - t.ok(_stream) - }) -}) - -test('net.connect allowHalfOpen=true, write write write', (t) => { - return new Promise((resolve) => { - const ID = rand64() - const HELLO = 'Hello, World!\n' - let ended = false - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - const _stream = net.connect({ - port: 9000, - host: '127.0.0.1', - allowHalfOpen: true - }, function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, true) - // to just test writes, end the read side immediately - // (by simulated end receive '') - stream.__write('') - - stream.on('end', function () { - ended = true - }) - - const waiting = [] - - const ID = _stream.clientId - - function next (data) { - const p = new Promise((resolve) => { - waiting.push(resolve) - }) - return (args) => { - t.equal(args.clientId, ID) - t.equal(data, args.data) - return p - } - } - - mock.methods.tcpSend = [ - next(HELLO + 1), - next(HELLO + 2), - next(HELLO + 3), - next(HELLO + 4), - next(HELLO + 5), - next(HELLO + 6), - next(HELLO + 7) - ] - - stream.write(HELLO + 1) - stream.write(HELLO + 2) - stream.write(HELLO + 3) - stream.write(HELLO + 4) - stream.write(HELLO + 5) - stream.write(HELLO + 6) - stream.write(HELLO + 7) - - stream.end() - - const int = setInterval(() => { - waiting.shift()({}) - if (!waiting.length) { - clearInterval(int) - } - }, 100) - - mock.create(t, 'tcpShutdown', - { clientId: ID }, - {} - ) - - mock.create(t, 'tcpClose', - { clientId: ID }, - {} - ) - - stream.on('close', () => { - t.ok(ended) - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }) - t.ok(_stream) - }) -}) - -test.skip('net.connect', async (t) => { - return new Promise((resolve) => { - const ID = rand64() - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - - const _stream = net.connect(9000, '127.0.0.1', function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, false) - - const ID = _stream.clientId - - mock.methods.tcpReadStart = [(q) => { - t.deepEqual(q, { clientId: ID }) - return (async () => { - return {} - })() - }] - - // using setTimeout here is a sign we don't understand something. - // - setTimeout(() => { - t.deepEqual(mock.methods, {}, 'no uncalled methods') - mock.create(t, 'tcpShutdown', - { clientId: ID }, - {} - ) - - mock.create(t, 'tcpClose', - { clientId: ID }, - {} - ) - - stream.__write('') - - stream.end() - stream.on('close', () => { - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }) - }, 100) - // stream.write(HELLO) - }) - t.ok(_stream) - }) -}) - -test('net.connect allowHalfOpen=true readStart readStop', (t) => { - return new Promise((resolve) => { - const ID = rand64() - const HELLO = 'Hello, World!\n' - - mock.create(t, 'tcpConnect', - { port: 9000, address: '127.0.0.1' }, - { - data: { - clientId: ID - } - } - ) - - const _stream = net.connect({ - port: 9000, - host: '127.0.0.1', - allowHalfOpen: true - }, function (err, stream) { - t.equal(_stream, stream) - t.equal(err, null) - t.equal(stream.allowHalfOpen, true) - - const ID = _stream.clientId - - // stream.end() - mock.methods.tcpReadStart = [(q) => { - t.deepEqual(q, { clientId: ID }) - return (async () => { - return { data: HELLO } - })() - }] - - // trigger flow? - const fn = () => {} - stream.on('data', fn) - - mock.methods.tcpReadStop = [(q) => { - t.deepEqual(q, { clientId: ID }) - return (async () => { - return {} - })() - }] - - setTimeout(() => { - stream.pause() - t.deepEqual(mock.methods, {}, 'no uncalled methods') - resolve() - }, 1000) - }) - }) -}) diff --git a/test/node/os.js b/test/node/os.js deleted file mode 100644 index 05e9facd..00000000 --- a/test/node/os.js +++ /dev/null @@ -1,78 +0,0 @@ -import { test } from 'tapzero' -import * as nodeos from 'node:os' - -import mock from './mock.js' - -import * as os from '../../os.js' - -test('os.arch()', (t) => { - mock.create(t, 'getPlatformArch', {}, { data: nodeos.arch() }, false) - - t.equal(nodeos.arch(), os.arch()) -}) - -test('os.platform()', (t) => { - mock.create(t, 'getPlatformOS', {}, { data: nodeos.platform() }, false) - t.equal(nodeos.platform(), os.platform()) -}) - -test('os.type()', (t) => { - mock.create(t, 'getPlatformType', {}, { data: nodeos.type() }, false) - t.equal(nodeos.type(), os.type()) -}) - -test('os.networkInterfaces()', (t) => { - const mockedIPCNetworkInterfaces = { - ipv4: { - tun0: '100.200.100.200', - wlan0: '192.168.1.113', - rmnet0: '11.11.11.11', - lo: '127.0.0.1', - local: '0.0.0.0' - }, - ipv6: { - tun0: '2604:cccc:11a:84b2::60:9999', - wlan0: 'ffff::acef:64ff:fe1c:3c77', - dummy0: 'ffff::30c8:c6ff:fe02:123d', - lo: '::1', - local: '::1' - } - } - - const mockedNetworkInterfaces = { - tun0: [ - { address: '100.200.100.200', netmask: '255.255.255.0', internal: false, family: 'IPv4', cidr: '100.200.100.200/24', mac: null }, - { address: '2604:cccc:11a:84b2::60:9999', netmask: 'ffff:ffff:ffff:ffff::', internal: false, family: 'IPv6', cidr: '2604:cccc:11a:84b2::60:9999/64', mac: null } - ], - wlan0: [ - { address: '192.168.1.113', netmask: '255.255.255.0', internal: false, family: 'IPv4', cidr: '192.168.1.113/24', mac: null }, - { address: 'ffff::acef:64ff:fe1c:3c77', netmask: 'ffff:ffff:ffff:ffff::', internal: false, family: 'IPv6', cidr: 'ffff::acef:64ff:fe1c:3c77/64', mac: null } - ], - rmnet0: [ - { address: '11.11.11.11', netmask: '255.255.255.0', internal: false, family: 'IPv4', cidr: '11.11.11.11/24', mac: null } - ], - lo: [ - { address: '127.0.0.1', netmask: '255.0.0.0', internal: true, family: 'IPv4', cidr: '127.0.0.1/8', mac: '00:00:00:00:00:00' }, - { address: '::1', netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', internal: true, family: 'IPv6', cidr: '::1/128', mac: '00:00:00:00:00:00' } - ], - local: [ - { address: '0.0.0.0', netmask: '0.0.0.0', internal: true, family: 'IPv4', cidr: '0.0.0.0/0', mac: '00:00:00:00:00:00' }, - { address: '::1', netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', internal: true, family: 'IPv6', cidr: '::1/128', mac: '00:00:00:00:00:00' } - ], - dummy0: [ - { address: 'ffff::30c8:c6ff:fe02:123d', netmask: 'ffff:ffff:ffff:ffff::', internal: false, family: 'IPv6', cidr: 'ffff::30c8:c6ff:fe02:123d/64', mac: null } - ] - } - - mock.create(t, 'getNetworkInterfaces', {}, { data: mockedIPCNetworkInterfaces }, false) - - t.deepEqual(mockedNetworkInterfaces, os.networkInterfaces()) -}) - -test('os.EOL', (t) => { - if (/windows/i.test(nodeos.type())) { - t.equal(os.EOL, '\r\n') - } else { - t.equal(os.EOL, '\n') - } -}) From 9012d8f6687f3cf5da97640eb08b993f4bee042e Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 23:40:34 +1200 Subject: [PATCH 7/9] install before node tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8187acaf..bb0ab7d8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "standardx -v", "readme": "node ./bin/generate-docs.js", "test": "cd test && npm install --silent --no-audit && npm test --silent", - "test:node": "cd test && npm run test:node", + "test:node": "cd test && npm install --silent --no-audit && npm run test:node", "test:android": "cd test && npm install --silent --no-audit && npm run test:android --silent", "test:android-emulator": "cd test && npm install --silent --no-audit && npm run test:android-emulator --silent", "test:clean": "cd test && rm -rf dist", From ad31ee03623a652512f3ade878b8d5fcb4a52627 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Fri, 12 Aug 2022 23:45:10 +1200 Subject: [PATCH 8/9] network address should have family:4 or 6, not a string --- sdk/os.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/os.js b/sdk/os.js index b425b1af..213e2ce0 100644 --- a/sdk/os.js +++ b/sdk/os.js @@ -55,7 +55,7 @@ export function networkInterfaces () { for (const type in ipv4) { const address = ipv4[type] - const family = 'IPv4' + const family = 4 let internal = false let netmask = '255.255.255.0' @@ -88,7 +88,7 @@ export function networkInterfaces () { for (const type in ipv6) { const address = ipv6[type] - const family = 'IPv6' + const family = 6 let internal = false let netmask = 'ffff:ffff:ffff:ffff::' From 39acb59d0240324426737647c5d8e43e95952897 Mon Sep 17 00:00:00 2001 From: Dominic Tarr Date: Sun, 14 Aug 2022 19:44:02 +1200 Subject: [PATCH 9/9] update to node@18 --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index eb3b0754..9592b1ae 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,7 +12,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 18.x - name: npm install run: npm install - name: npm test