├── .eslintrc ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── .vimrc ├── README.md ├── backend.js ├── bin └── generate-docs.js ├── bluetooth.js ├── bootstrap.js ├── buffer.js ├── console.js ├── crypto.js ├── dgram.js ├── dns.js ├── dns ├── index.js └── promises.js ├── errors.js ├── events.js ├── fs.js ├── fs ├── binding.js ├── constants.js ├── dir.js ├── fds.js ├── flags.js ├── handle.js ├── index.js ├── promises.js ├── stats.js └── stream.js ├── gc.js ├── index.js ├── ipc.js ├── net.js ├── os.js ├── p2p.js ├── package.json ├── path.js ├── path ├── index.js ├── path.js ├── posix.js └── win32.js ├── polyfills.js ├── process.js ├── runtime.js ├── stream.js ├── test ├── build.js ├── fixtures │ ├── bin │ │ └── file │ ├── directory │ │ ├── 0.txt │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── a.txt │ │ ├── b.txt │ │ └── c.txt │ ├── file.js │ ├── file.json │ └── file.txt ├── package.json ├── scripts │ ├── bootstrap-android-emulator.sh │ ├── poll-adb-logcat.sh │ ├── test-android-emulator.sh │ ├── test-android.sh │ ├── test-desktop.sh │ └── test-ios-simulator.sh ├── socket.ini └── src │ ├── backend.js │ ├── backend │ └── backend.js │ ├── crypto.js │ ├── dgram.js │ ├── dns.js │ ├── frontend │ ├── index.html │ ├── index_second_window.html │ ├── index_second_window.js │ └── index_second_window2.html │ ├── fs.js │ ├── fs │ ├── binding.js │ ├── constants.js │ ├── dir.js │ ├── fds.js │ ├── flags.js │ ├── handle.js │ ├── index.js │ ├── promises.js │ ├── stats.js │ └── stream.js │ ├── index.js │ ├── ipc.js │ ├── os.js │ ├── path.js │ ├── process.js │ ├── runtime.js │ ├── test-context.js │ └── util.js └── util.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2020 4 | }, 5 | "env": { 6 | "es6": true 7 | }, 8 | "sourceType": "module" 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | socket-api-tests: 7 | uses: socketsupply/socket-api-tests-workflow/.github/workflows/tests.yml@main 8 | secrets: 9 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 10 | PACKAGE_PAT: ${{ secrets.PACKAGE_PAT }} 11 | with: 12 | skip_git_clone_socket_api: true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Junk 2 | *.swp 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Ignore all files in the node_modules folder 15 | node_modules/ 16 | 17 | # Default output directory 18 | build/ 19 | 20 | # Provisioning profile 21 | *.mobileprovision 22 | dist/ 23 | package-lock.json 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | test/ 3 | .eslintrc 4 | .gitignore 5 | .vimrc 6 | README.md 7 | -------------------------------------------------------------------------------- /.vimrc: -------------------------------------------------------------------------------- 1 | au BufNewFile,BufRead *.js set syntax=typescript 2 | au BufNewFile,BufRead *.js set filetype=typescript 3 | -------------------------------------------------------------------------------- /backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Backend 3 | * 4 | * Provides methods for the backend process management 5 | */ 6 | 7 | import ipc from './ipc.js' 8 | 9 | /** 10 | * @param {object} opts - an options object 11 | * @param {boolean} [opts.force = false] - whether to force existing process to close 12 | * @return {Promise} 13 | */ 14 | export async function open (opts = {}) { 15 | opts.force ??= false 16 | return await ipc.send('process.open', opts) 17 | } 18 | 19 | /** 20 | * @return {Promise} 21 | */ 22 | export async function close () { 23 | return await ipc.send('process.kill') 24 | } 25 | 26 | /** 27 | * @return {Promise} 28 | */ 29 | export async function sendToProcess (opts) { 30 | return await ipc.send('process.write', opts) 31 | } 32 | 33 | // eslint-disable-next-line 34 | import * as exports from './backend.js' 35 | export default exports 36 | -------------------------------------------------------------------------------- /bin/generate-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as acorn from 'acorn' 3 | import * as walk from 'acorn-walk' 4 | import fs from 'node:fs' 5 | import path from 'node:path' 6 | 7 | try { 8 | fs.unlinkSync('README.md') 9 | } catch {} 10 | 11 | export function transform (filename) { 12 | const srcFile = path.relative(process.cwd(), filename) 13 | const destFile = path.relative(process.cwd(), 'README.md') 14 | 15 | let accumulateComments = [] 16 | const comments = {} 17 | const src = fs.readFileSync(srcFile) 18 | const ast = acorn.parse(String(src), { 19 | tokens: true, 20 | comment: true, 21 | ecmaVersion: 'latest', 22 | sourceType: 'module', 23 | onToken: (token) => { 24 | comments[token.start] = accumulateComments 25 | accumulateComments = [] 26 | }, 27 | onComment: (block, comment) => { 28 | if (!block) return 29 | if (comment[0] !== '*') return // not a JSDoc comment 30 | 31 | comment = comment.replace(/^\s*\*/gm, '').trim() 32 | comment = comment.replace(/^\n/, '') 33 | accumulateComments.push(comment.trim()) 34 | }, 35 | locations: true 36 | }) 37 | 38 | for (const [key, value] of Object.entries(comments)) { 39 | if (!value.length) delete comments[key] // release empty items 40 | } 41 | 42 | const docs = [] 43 | 44 | const onNode = node => { 45 | const item = { 46 | sort: node.loc.start.line, 47 | location: `/${srcFile}#L${node.loc.start.line}`, 48 | type: node.type, 49 | name: node.name, 50 | export: node?.type.includes('Export'), 51 | header: comments[node.start] 52 | } 53 | 54 | if (item.header?.join('').includes('@ignore')) { 55 | return 56 | } 57 | 58 | if (item.header?.join('').includes('@module')) { 59 | item.type = 'Module' 60 | const name = item.header.join('').match(/@module\s*(.*)/) 61 | if (name) item.name = name[1] 62 | } 63 | 64 | if (item.header?.join('').includes('@link')) { 65 | const url = item.header.join('').match(/@link\s*(.*)}/) 66 | if (url) item.url = url[1].trim() 67 | } 68 | 69 | if (node.type.includes('ExportAllDeclaration')) { 70 | return 71 | } 72 | 73 | if (node.type.includes('ExportDefaultDeclaration')) { 74 | return 75 | } 76 | 77 | if (node.type.includes('ExportNamedDeclaration')) { 78 | const firstDeclaration = node.declarations ? node.declarations[0] : node.declaration 79 | if (!firstDeclaration) return 80 | 81 | item.type = firstDeclaration.type || item.type 82 | 83 | if (item.type === 'VariableDeclaration') { 84 | item.name = node.declaration.declarations[0].id.name 85 | } else { 86 | item.name = node.declaration.id.name 87 | } 88 | 89 | if (node.declaration.superClass) { 90 | item.name = `\`${item.name}\` (extends \`${node.declaration.superClass.name}\`)` 91 | } 92 | 93 | if (item.type === 'FunctionDeclaration') { 94 | item.params = [] // node.declaration.params 95 | item.signature = [] 96 | } 97 | } 98 | 99 | if (node.type.includes('MethodDefinition')) { 100 | item.name = node.key?.name 101 | item.signature = [] 102 | 103 | if (node.value.type === 'FunctionExpression') { 104 | item.generator = node.value.generator 105 | item.static = node.static 106 | item.async = node.value.async 107 | item.params = [] 108 | item.returns = [] 109 | } 110 | } 111 | 112 | if (item.export && !item.header) { 113 | item.header = [ 114 | `This is a \`${item.type}\` named \`${item.name}\` ` + 115 | `in \`${srcFile}\`, it's exported but undocumented.\n` 116 | ] 117 | } 118 | 119 | const attrs = item.header?.join('\n').match(/@(.*)[\n$]*/g) 120 | 121 | if (attrs) { 122 | let position = 0 123 | 124 | for (const attr of attrs) { 125 | const isParam = attr.match(/^@(param|arg|argument)/) 126 | const isReturn = attr.match(/^@(returns?)/) 127 | 128 | if (isParam) { 129 | const propType = 'params' 130 | item.signature = item.signature || [] 131 | const parts = attr.split(/-\s+(.*)/) 132 | const { 1: rawType, 2: rawName } = parts[0].match(/{([^}]+)}(.*)/) 133 | const [name, defaultValue] = rawName.replace(/[[\]']+/g, '').trim().split('=') 134 | 135 | // type could be [(string|number)=] 136 | const parenthasisedType = rawType 137 | .replace(/\s*\|\s*/g, ' \\| ') 138 | .replace(/\[|\]/g, '') 139 | // now it is (string|number)= 140 | const optional = parenthasisedType.endsWith('=') 141 | const compundType = parenthasisedType.replace(/=$/, '') 142 | // now it is (string|number) 143 | const type = compundType.match(/^\((.*)\)$/)?.[1] ?? compundType 144 | // now it is string|number 145 | 146 | const param = { 147 | name: name.trim() || `(Position ${position++})`, 148 | type 149 | } 150 | 151 | const params = node.declaration?.params || node.value?.params 152 | if (params) { 153 | const assign = params.find(o => o.left?.name === name) 154 | if (assign) param.default = assign.right.raw 155 | } 156 | 157 | param.default = defaultValue?.trim() ?? '' 158 | param.optional = optional 159 | param.desc = parts[1]?.trim() 160 | 161 | if (!item[propType]) item[propType] = [] 162 | item[propType].push(param) 163 | if (propType === 'params' && !name.includes('.')) item.signature.push(name) 164 | } 165 | 166 | if (isReturn) { 167 | const propType = 'returns' 168 | const match = attr.match(/{([^}]+)}(?:\s*-\s*)?(.*)/) 169 | if (match) { 170 | const { 1: type, 2: rawName } = match 171 | const [name, description] = /\w\s*-\s*(.*)/.test(rawName) ? rawName.split(/-\s+/) : ['Not specified', rawName] 172 | const param = { name: name.trim() || 'Not specified', type, description: description?.trim() } 173 | if (['undefined', 'void'].includes(type)) continue 174 | if (!item[propType]) item[propType] = [] 175 | item[propType].push(param) 176 | } 177 | } 178 | } 179 | } 180 | 181 | if (item.signature && item.type !== 'ClassDeclaration') { 182 | item.name = `\`${item.name}(${item.signature?.join(', ') || ''})\`` 183 | } else if (item.exports) { 184 | item.name = `\`${item.name}\`` 185 | } 186 | 187 | if (item.header) { 188 | item.header = item.header.join('\n').split('\n').filter(line => !line.trim().startsWith('@')) 189 | docs.push(item) 190 | } 191 | } 192 | 193 | walk.full(ast, onNode) 194 | docs.sort((a, b) => a.sort - b.sort) 195 | 196 | const createTableParams = arr => { 197 | if (!arr || !arr.length) return [] 198 | 199 | const tableHeader = [ 200 | '| Argument | Type | Default | Optional | Description |', 201 | '| :--- | :--- | :---: | :---: | :--- |' 202 | ].join('\n') 203 | 204 | let table = `${tableHeader}\n` 205 | 206 | for (const param of arr) { 207 | // let type = param.type || 'Unknown' 208 | // const desc = param.header?.join(' ') 209 | 210 | table += `| ${Object.values(param).join(' | ')} |\n` 211 | } 212 | 213 | return (table + '\n') 214 | } 215 | 216 | const createTableReturn = (arr) => { 217 | if (!arr?.length) return [] 218 | 219 | const tableHeader = [ 220 | '| Return Value | Type | Description |', 221 | '| :--- | :--- | :--- |' 222 | ].join('\n') 223 | 224 | let table = `${tableHeader}\n` 225 | 226 | for (const param of arr) { 227 | // let type = param.type || 'Unknown' 228 | // const desc = param.header?.join(' ') 229 | 230 | table += `| ${Object.values(param).join(' | ')} |\n` 231 | } 232 | 233 | return (table + '\n') 234 | } 235 | 236 | const base = 'https://github.com/socketsupply/socket-api/blob/master' 237 | 238 | for (const doc of docs) { 239 | let h = doc.export ? '##' : '###' 240 | if (doc.type === 'Module') h = '#' 241 | 242 | const title = `\n${h} [${doc.name}](${base}${doc.location})\n` 243 | const header = `${doc.header.join('\n')}\n` 244 | 245 | const md = [ 246 | title ?? [], 247 | doc?.url ? `External docs: ${doc.url}\n` : [], 248 | header ?? [], 249 | createTableParams(doc?.params), 250 | createTableReturn(doc?.returns) 251 | ].flatMap(item => item).join('\n') 252 | 253 | fs.appendFileSync(destFile, md, { flags: 'a' }) 254 | } 255 | } 256 | 257 | [ 258 | 'bluetooth.js', 259 | 'buffer.js', 260 | 'crypto.js', 261 | 'dgram.js', 262 | 'dns/index.js', 263 | 'dns/promises.js', 264 | 'events.js', 265 | 'fs/index.js', 266 | 'fs/promises.js', 267 | 'fs/stream.js', 268 | 'ipc.js', 269 | 'os.js', 270 | 'p2p.js', 271 | 'path/path.js', 272 | 'process.js', 273 | 'runtime.js', 274 | 'stream.js' 275 | ].forEach(transform) 276 | -------------------------------------------------------------------------------- /bluetooth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Bluetooth 3 | * 4 | * A high level, cross-platform API for Bluetooth Pub-Sub 5 | */ 6 | 7 | import * as ipc from './ipc.js' 8 | import { EventEmitter } from './events.js' 9 | 10 | import * as exports from './bluetooth.js' 11 | 12 | /** 13 | * Create an instance of a Bluetooth service. 14 | */ 15 | export class Bluetooth extends EventEmitter { 16 | static isInitalized = false 17 | 18 | /** 19 | * constructor is an example property that is set to `true` 20 | * Creates a new service with key-value pairs 21 | * @param {string} serviceId - Given a default value to determine the type 22 | */ 23 | constructor (serviceId = '') { 24 | super() 25 | 26 | if (!serviceId || serviceId.length !== 36) { 27 | throw new Error('expected serviceId of length 36') 28 | } 29 | 30 | this.serviceId = serviceId 31 | 32 | window.addEventListener('data', e => { 33 | if (!e.detail.params) return 34 | const { err, data } = e.detail.params 35 | 36 | if (err) return this.emit('error', err) 37 | 38 | if (data?.serviceId === this.serviceId) { 39 | this.emit(data.characteristicId, data, e.detail.data) 40 | } 41 | }) 42 | 43 | window.addEventListener('bluetooth', e => { 44 | if (typeof e.detail !== 'object') return 45 | const { err, data } = e.detail 46 | 47 | if (err) { 48 | return this.emit('error', err) 49 | } 50 | 51 | this.emit(data.event, data) 52 | }) 53 | } 54 | 55 | /** 56 | * Start the bluetooth service. 57 | * @return {Promise} 58 | * 59 | */ 60 | start () { 61 | return ipc.send('bluetooth.start', { serviceId: this.serviceId }) 62 | } 63 | 64 | /** 65 | * Start scanning for published values that correspond to a well-known UUID. 66 | * Once subscribed to a UUID, events that correspond to that UUID will be 67 | * emitted. To receive these events you can add an event listener, for example... 68 | * 69 | * ```js 70 | * const ble = new Bluetooth(id) 71 | * ble.subscribe(uuid) 72 | * ble.on(uuid, (data, details) => { 73 | * // ...do something interesting 74 | * }) 75 | * ``` 76 | * 77 | * @param {string} [id = ''] - A well-known UUID 78 | * @return {Promise} 79 | */ 80 | subscribe (id = '') { 81 | return ipc.send('bluetooth.subscribe', { 82 | characteristicId: id, 83 | serviceId: this.serviceId 84 | }) 85 | } 86 | 87 | /** 88 | * Start advertising a new value for a well-known UUID 89 | * @param {string} [id=''] - A well-known UUID 90 | * @param {string} [value=''] 91 | * @return {Promise} 92 | */ 93 | async publish (id = '', value = '') { 94 | if (!id || id.length !== 36) { 95 | throw new Error('expected id of length 36') 96 | } 97 | 98 | const params = { 99 | characteristicId: id, 100 | serviceId: this.serviceId 101 | } 102 | 103 | if (!(value instanceof ArrayBuffer) && typeof value === 'object') { 104 | value = JSON.stringify(value) 105 | } 106 | 107 | if (typeof value === 'string') { 108 | const enc = new TextEncoder().encode(value) 109 | value = enc 110 | params.length = enc.length 111 | } 112 | 113 | const res = await ipc.write('bluetooth.publish', params, value) 114 | 115 | if (res.err) { 116 | throw new Error(res.err.message) 117 | } 118 | } 119 | } 120 | export default exports 121 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | import { readFile } from './fs/promises.js' 2 | import { createWriteStream } from './fs/index.js' 3 | import { PassThrough } from './stream.js' 4 | import { createDigest } from './crypto.js' 5 | import { EventEmitter } from './events.js' 6 | 7 | async function * streamAsyncIterable (stream) { 8 | const reader = stream.getReader() 9 | try { 10 | while (true) { 11 | const { done, value } = await reader.read() 12 | if (done) return 13 | yield value 14 | } 15 | } finally { 16 | reader.releaseLock() 17 | } 18 | } 19 | 20 | /** 21 | * @param {Buffer|String} buf 22 | * @param {string} hashAlgorithm 23 | * @returns {Promise} 24 | */ 25 | async function getHash (buf, hashAlgorithm) { 26 | const digest = await createDigest(hashAlgorithm, buf) 27 | return digest.toString('hex') 28 | } 29 | 30 | /** 31 | * @param {string} dest - file path 32 | * @param {string} hash - hash string 33 | * @param {string} hashAlgorithm - hash algorithm 34 | * @returns {Promise} 35 | */ 36 | export async function checkHash (dest, hash, hashAlgorithm) { 37 | let buf 38 | try { 39 | buf = await readFile(dest) 40 | } catch (err) { 41 | // download if file is corrupted or does not exist 42 | return false 43 | } 44 | return hash === await getHash(buf, hashAlgorithm) 45 | } 46 | 47 | class Bootstrap extends EventEmitter { 48 | constructor (options) { 49 | super() 50 | if (!options.url || !options.dest) { 51 | throw new Error('.url and .dest are required string properties on the object provided to the constructor at the first argument position') 52 | } 53 | this.options = options 54 | } 55 | 56 | async run () { 57 | try { 58 | const fileBuffer = await this.download(this.options.url) 59 | await this.write({ fileBuffer, dest: this.options.dest }) 60 | } catch (err) { 61 | this.emit('error', err) 62 | throw err 63 | } finally { 64 | this.cleanup() 65 | } 66 | } 67 | 68 | /** 69 | * @param {object} options 70 | * @param {Uint8Array} options.fileBuffer 71 | * @param {string} options.dest 72 | * @returns {Promise} 73 | */ 74 | async write ({ fileBuffer, dest }) { 75 | this.emit('write-file', { status: 'started' }) 76 | const passThroughStream = new PassThrough() 77 | const writeStream = createWriteStream(dest, { mode: 0o755 }) 78 | passThroughStream.pipe(writeStream) 79 | let written = 0 80 | passThroughStream.on('data', data => { 81 | written += data.length 82 | const progress = written / fileBuffer.byteLength 83 | this.emit('write-file-progress', progress) 84 | }) 85 | passThroughStream.write(fileBuffer) 86 | passThroughStream.end() 87 | return new Promise((resolve, reject) => { 88 | writeStream.on('finish', () => { 89 | this.emit('write-file', { status: 'finished' }) 90 | resolve() 91 | }) 92 | writeStream.on('error', err => { 93 | this.emit('error', err) 94 | reject(err) 95 | }) 96 | }) 97 | } 98 | 99 | /** 100 | * @param {string} url - url to download 101 | * @returns {Promise} 102 | * @throws {Error} - if status code is not 200 103 | */ 104 | async download (url) { 105 | const response = await fetch(url, { mode: 'cors' }) 106 | if (!response.ok) { 107 | throw new Error(`Bootstrap request failed: ${response.status} ${response.statusText}`) 108 | } 109 | const contentLength = +response.headers.get('Content-Length') 110 | let receivedLength = 0 111 | let prevProgress = 0 112 | const fileData = new Uint8Array(contentLength) 113 | 114 | for await (const chunk of streamAsyncIterable(response.body)) { 115 | fileData.set(chunk, receivedLength) 116 | receivedLength += chunk.length 117 | const progress = (receivedLength / contentLength * 100) | 0 118 | if (progress !== prevProgress) { 119 | this.emit('download-progress', progress) 120 | prevProgress = progress 121 | } 122 | } 123 | return fileData 124 | } 125 | 126 | cleanup () { 127 | this.removeAllListeners() 128 | } 129 | } 130 | 131 | export function bootstrap (options) { 132 | return new Bootstrap(options) 133 | } 134 | 135 | export default { 136 | bootstrap, 137 | checkHash 138 | } 139 | -------------------------------------------------------------------------------- /console.js: -------------------------------------------------------------------------------- 1 | import { format, isObject } from './util.js' 2 | 3 | async function postMessage (...args) { 4 | return await globalThis?.window?.__ipc?.postMessage?.(...args) 5 | } 6 | 7 | function isPatched (console) { 8 | return console?.[Symbol.for('socket.console.patched')] === true 9 | } 10 | 11 | function table (data, columns, formatValues = true) { 12 | const maybeFormat = (value) => formatValues ? format(value) : value 13 | const rows = [] 14 | 15 | columns = Array.isArray(columns) && Array.from(columns) 16 | 17 | if (!columns) { 18 | columns = [] 19 | 20 | if (Array.isArray(data)) { 21 | const first = data[0] 22 | 23 | if (!isObject(first) && first !== undefined) { 24 | columns.push('Values') 25 | } else if (isObject(first)) { 26 | const keys = new Set(data 27 | .map(Object.keys) 28 | .reduce((a, b) => a.concat(b), []) 29 | ) 30 | 31 | columns.push(...keys.values()) 32 | } 33 | } else if (isObject(data)) { 34 | columns.push('Value') 35 | 36 | for (const key in data) { 37 | const value = data[key] 38 | if (isObject(value)) { 39 | columns.push(key) 40 | } 41 | } 42 | } 43 | } 44 | 45 | if (Array.isArray(data)) { 46 | const first = data[0] 47 | 48 | if (!isObject(first) && first !== undefined) { 49 | for (let i = 0; i < data.length; ++i) { 50 | const value = maybeFormat(data[i] ?? null) 51 | rows.push([i, value]) 52 | } 53 | } else { 54 | for (let i = 0; i < data.length; ++i) { 55 | const row = [i] 56 | for (const column of columns) { 57 | if (column === 'Value' && !isObject(data[i])) { 58 | const value = maybeFormat(data[i] ?? null) 59 | row.push(value ?? null) 60 | } else { 61 | const value = maybeFormat(data[i]?.[column] ?? data[i] ?? null) 62 | row.push(value) 63 | } 64 | } 65 | 66 | rows.push(row) 67 | } 68 | } 69 | } else if (isObject(data)) { 70 | for (const key in data) { 71 | rows.push([key, maybeFormat(data[key] ?? null)]) 72 | } 73 | } 74 | 75 | columns.unshift('(index)') 76 | 77 | return { columns, rows } 78 | } 79 | 80 | export const globalConsole = ( 81 | globalThis?.window?.console ?? 82 | globalThis?.console ?? 83 | null 84 | ) 85 | 86 | export class Console { 87 | constructor (options) { 88 | if (typeof options?.postMessage !== 'function') { 89 | throw new TypeError('Expecting `.postMessage` in constructor options') 90 | } 91 | 92 | Object.defineProperties(this, { 93 | postMessage: { 94 | enumerable: false, 95 | configurable: false, 96 | value: options?.postMessage 97 | }, 98 | 99 | console: { 100 | enumerable: false, 101 | configurable: false, 102 | value: options?.console ?? null 103 | }, 104 | 105 | counters: { 106 | enumerable: false, 107 | configurable: false, 108 | value: new Map() 109 | }, 110 | 111 | timers: { 112 | enumerable: false, 113 | configurable: false, 114 | value: new Map() 115 | } 116 | }) 117 | } 118 | 119 | async write (destination, ...args) { 120 | const value = encodeURIComponent(format(...args)) 121 | const uri = `ipc://${destination}?value=${value}` 122 | try { 123 | return await this.postMessage?.(uri) 124 | } catch (err) { 125 | this.console?.warn?.(`Failed to write to ${destination}: ${err.message}`) 126 | } 127 | } 128 | 129 | assert (assertion, ...args) { 130 | this.console?.assert?.(assertion, ...args) 131 | if (Boolean(assertion) !== true) { 132 | this.write('stderr', `Assertion failed: ${format(...args)}`) 133 | } 134 | } 135 | 136 | clear () { 137 | this.console?.clear?.() 138 | } 139 | 140 | count (label = 'default') { 141 | this.console?.count(label) 142 | if (!isPatched(this.console)) { 143 | const count = (this.counters.get(label) || 0) + 1 144 | this.counters.set(label, count) 145 | this.write('stdout', `${label}: ${count}`) 146 | } 147 | } 148 | 149 | countReset (label = 'default') { 150 | this.console?.countReset() 151 | if (!isPatched(this.console)) { 152 | this.counters.set(label, 0) 153 | this.write('stdout', `${label}: 0`) 154 | } 155 | } 156 | 157 | debug (...args) { 158 | this.console?.debug?.(...args) 159 | if (!isPatched(this.console)) { 160 | this.write('stderr', ...args) 161 | } 162 | } 163 | 164 | dir (...args) { 165 | this.console?.dir?.(...args) 166 | } 167 | 168 | dirxml (...args) { 169 | this.console?.dirxml?.(...args) 170 | } 171 | 172 | error (...args) { 173 | this.console?.error?.(...args) 174 | if (!isPatched(this.console)) { 175 | this.write('stderr', ...args) 176 | } 177 | } 178 | 179 | info (...args) { 180 | this.console?.info?.(...args) 181 | if (!isPatched(this.console)) { 182 | this.write('stdout', ...args) 183 | } 184 | } 185 | 186 | log (...args) { 187 | this.console?.log?.(...args) 188 | if (!isPatched(this.console)) { 189 | this.write('stdout', ...args) 190 | } 191 | } 192 | 193 | table (...args) { 194 | if (isPatched(this.console)) { 195 | return this.console.table(...args) 196 | } 197 | 198 | if (!isObject(args[0])) { 199 | return this.log(...args) 200 | } 201 | 202 | const { columns, rows } = table(...args) 203 | const output = [] 204 | const widths = [] 205 | 206 | for (let i = 0; i < columns.length; ++i) { 207 | const column = columns[i] 208 | 209 | let columnWidth = column.length + 2 210 | for (const row of rows) { 211 | const cell = row[i] 212 | const cellContents = String(cell) 213 | const cellWidth = 2 + (cellContents 214 | .split('\n') 215 | .map((r) => r.length) 216 | .sort() 217 | .slice(-1)[0] ?? 0 218 | ) 219 | 220 | columnWidth = Math.max(columnWidth, cellWidth) 221 | } 222 | 223 | columnWidth += 2 224 | widths.push(columnWidth) 225 | output.push(column.padEnd(columnWidth, ' ')) 226 | } 227 | 228 | output.push('\n') 229 | 230 | for (let i = 0; i < rows.length; ++i) { 231 | const row = rows[i] 232 | for (let j = 0; j < row.length; ++j) { 233 | const width = widths[j] 234 | const cell = String(row[j]) 235 | output.push(cell.padEnd(width, ' ')) 236 | } 237 | 238 | output.push('\n') 239 | } 240 | 241 | this.write('stdout', output.join('')) 242 | this.console?.table?.(...args) 243 | } 244 | 245 | time (label = 'default') { 246 | this.console?.time?.(label) 247 | if (isPatched(this.console)) { 248 | if (this.timers.has(label)) { 249 | this.console?.warn?.(`Warning: Label '${label}' already exists for console.time()`) 250 | } else { 251 | this.timers.set(label, Date.now()) 252 | } 253 | } 254 | } 255 | 256 | timeEnd (label = 'default') { 257 | this.console?.timeEnd?.(label) 258 | if (!isPatched(this.console)) { 259 | if (!this.timers.has(label)) { 260 | this.console?.warn?.(`Warning: No such label '${label}' for console.timeEnd()`) 261 | } else { 262 | const time = this.timers.get(label) 263 | this.timers.delete(label) 264 | if (typeof time === 'number' && time > 0) { 265 | const elapsed = Date.now() - time 266 | 267 | if (elapsed * 0.001 > 0) { 268 | this.write('stdout', `${label}: ${elapsed * 0.001}s`) 269 | } else { 270 | this.write('stdout', `${label}: ${elapsed}ms`) 271 | } 272 | } 273 | } 274 | } 275 | } 276 | 277 | timeLog (label = 'default') { 278 | this.console?.timeLog?.(label) 279 | if (!isPatched(this.console)) { 280 | if (!this.timers.has(label)) { 281 | this.console?.warn?.(`Warning: No such label '${label}' for console.timeLog()`) 282 | } else { 283 | const time = this.timers.get(label) 284 | if (typeof time === 'number' && time > 0) { 285 | const elapsed = Date.now() - time 286 | 287 | if (elapsed * 0.001 > 0) { 288 | this.write('stdout', `${label}: ${elapsed * 0.001}s`) 289 | } else { 290 | this.write('stdout', `${label}: ${elapsed}ms`) 291 | } 292 | } 293 | } 294 | } 295 | } 296 | 297 | trace (...objects) { 298 | this.console?.trace?.(...objects) 299 | if (!isPatched(this.console)) { 300 | const stack = new Error().stack.split('\n').slice(1) 301 | stack.unshift(`Trace: ${format(...objects)}`) 302 | this.write('stderr', stack.join('\n')) 303 | } 304 | } 305 | 306 | warn (...args) { 307 | this.console?.warn?.(...args) 308 | if (!isPatched(this.console)) { 309 | this.write('stderr', ...args) 310 | } 311 | } 312 | } 313 | 314 | export function patchGlobalConsole (globalConsole, options) { 315 | if (!globalConsole || typeof globalConsole !== 'object') { 316 | globalConsole = globalThis?.console 317 | } 318 | 319 | if (!globalConsole) { 320 | throw new TypeError('Cannot determine a global console object to patch') 321 | } 322 | 323 | if (!isPatched(globalConsole)) { 324 | const defaultConsole = new Console({ postMessage }) 325 | 326 | globalConsole[Symbol.for('socket.console.patched')] = true 327 | 328 | for (const key in globalConsole) { 329 | if (typeof Console.prototype[key] === 'function') { 330 | const original = globalConsole[key].bind(globalConsole) 331 | globalConsole[key] = (...args) => { 332 | original(...args) 333 | defaultConsole[key](...args) 334 | } 335 | } 336 | } 337 | } 338 | 339 | return globalConsole 340 | } 341 | 342 | export default new Console({ 343 | postMessage, 344 | console: patchGlobalConsole(globalConsole) 345 | }) 346 | -------------------------------------------------------------------------------- /crypto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Crypto 3 | * 4 | * Some high level methods around the `crypto.subtle` api for getting 5 | * random bytes and hashing. 6 | */ 7 | 8 | import { Buffer } from './buffer.js' 9 | import console from './console.js' 10 | 11 | import * as exports from './crypto.js' 12 | 13 | /** 14 | * WebCrypto API 15 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/Crypto} 16 | */ 17 | export const webcrypto = globalThis.crypto?.webcrypto ?? globalThis.crypto 18 | 19 | /** 20 | * Generate cryptographically strong random values into `buffer` 21 | * @param {TypedArray} buffer 22 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues} 23 | * @return {TypedArray} 24 | */ 25 | export function getRandomValues (...args) { 26 | if (typeof webcrypto?.getRandomValues === 'function') { 27 | return webcrypto?.getRandomValues(...args) 28 | } 29 | 30 | console.warn('Missing implementation for window.crypto.getRandomValues()') 31 | return null 32 | } 33 | 34 | /** 35 | * Maximum total size of random bytes per page 36 | */ 37 | export const RANDOM_BYTES_QUOTA = 64 * 1024 38 | 39 | /** 40 | * Maximum total size for random bytes. 41 | */ 42 | export const MAX_RANDOM_BYTES = 0xFFFF_FFFF_FFFF 43 | 44 | /** 45 | * Maximum total amount of allocated per page of bytes (max/quota) 46 | */ 47 | export const MAX_RANDOM_BYTES_PAGES = MAX_RANDOM_BYTES / RANDOM_BYTES_QUOTA 48 | // note: should it do Math.ceil() / Math.round()? 49 | 50 | /** 51 | * Generate `size` random bytes. 52 | * @param {number} size - The number of bytes to generate. The size must not be larger than 2**31 - 1. 53 | * @returns {Buffer} - A promise that resolves with an instance of socket.Buffer with random bytes. 54 | */ 55 | export function randomBytes (size) { 56 | const buffers = [] 57 | 58 | if (size < 0 || size >= MAX_RANDOM_BYTES || !Number.isInteger(size)) { 59 | throw Object.assign(new RangeError( 60 | `The value of "size" is out of range. It must be >= 0 && <= ${MAX_RANDOM_BYTES}. ` + 61 | `Received ${size}` 62 | ), { 63 | code: 'ERR_OUT_OF_RANGE' 64 | }) 65 | } 66 | 67 | do { 68 | const length = size > RANDOM_BYTES_QUOTA ? RANDOM_BYTES_QUOTA : size 69 | const bytes = getRandomValues(new Int8Array(length)) 70 | buffers.push(Buffer.from(bytes)) 71 | size = Math.max(0, size - RANDOM_BYTES_QUOTA) 72 | } while (size > 0) 73 | 74 | return Buffer.concat(buffers) 75 | } 76 | 77 | /** 78 | * @param {string} algorithm - `SHA-1` | `SHA-256` | `SHA-384` | `SHA-512` 79 | * @param {Buffer | TypedArray | DataView} message - An instance of socket.Buffer, TypedArray or Dataview. 80 | * @returns {Promise} - A promise that resolves with an instance of socket.Buffer with the hash. 81 | */ 82 | export async function createDigest (algorithm, buf) { 83 | return Buffer.from(await webcrypto.subtle.digest(algorithm, buf)) 84 | } 85 | export default exports 86 | -------------------------------------------------------------------------------- /dns.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dns 3 | * @notice This is a rexports of `dns/index.js` so consumers will 4 | * need to only `import * as fs from '@socketsupply/socket-api/dns.js'` 5 | */ 6 | import * as exports from './dns.js' 7 | 8 | export * from './dns/index.js' 9 | export default exports 10 | -------------------------------------------------------------------------------- /dns/index.js: -------------------------------------------------------------------------------- 1 | import * as ipc from '../ipc.js' 2 | /** 3 | * @module DNS 4 | * 5 | * This module enables name resolution. For example, use it to look up IP 6 | * addresses of host names. Although named for the Domain Name System (DNS), 7 | * it does not always use the DNS protocol for lookups. dns.lookup() uses the 8 | * operating system facilities to perform name resolution. It may not need to 9 | * perform any network communication. To perform name resolution the way other 10 | * applications on the same system do, use dns.lookup(). 11 | */ 12 | 13 | import { rand64, isFunction } from '../util.js' 14 | import * as promises from './promises.js' 15 | 16 | import * as exports from './index.js' 17 | 18 | /** 19 | * Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or 20 | * AAAA (IPv6) record. All option properties are optional. If options is an 21 | * integer, then it must be 4 or 6 – if options is 0 or not provided, then IPv4 22 | * and IPv6 addresses are both returned if found. 23 | * 24 | * From the node.js website... 25 | * 26 | * > With the all option set to true, the arguments for callback change to (err, 27 | * addresses), with addresses being an array of objects with the properties 28 | * address and family. 29 | * 30 | * > On error, err is an Error object, where err.code is the error code. Keep in 31 | * mind that err.code will be set to 'ENOTFOUND' not only when the host name does 32 | * not exist but also when the lookup fails in other ways such as no available 33 | * file descriptors. dns.lookup() does not necessarily have anything to do with 34 | * the DNS protocol. The implementation uses an operating system facility that 35 | * can associate names with addresses and vice versa. This implementation can 36 | * have subtle but important consequences on the behavior of any Node.js program. 37 | * Please take some time to consult the Implementation considerations section 38 | * before using dns.lookup(). 39 | * 40 | * @see {@link https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback} 41 | * @param {string} hostname - The host name to resolve. 42 | * @param {Object=} opts - An options object. 43 | * @param {number|string} [opts.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. 44 | * @param {function} cb - The function to call after the method is complete. 45 | * @returns {void} 46 | */ 47 | export function lookup (hostname, opts, cb) { 48 | if (typeof hostname !== 'string') { 49 | const err = new TypeError(`The "hostname" argument must be of type string. Received type ${typeof hostname} (${hostname})`) 50 | err.code = 'ERR_INVALID_ARG_TYPE' 51 | throw err 52 | } 53 | 54 | if (typeof opts === 'function') { 55 | cb = opts 56 | } 57 | 58 | if (typeof cb !== 'function') { 59 | const err = new TypeError(`The "callback" argument must be of type function. Received type ${typeof cb} undefined`) 60 | err.code = 'ERR_INVALID_ARG_TYPE' 61 | throw err 62 | } 63 | 64 | if (typeof opts === 'number') { 65 | opts = { family: opts } 66 | } 67 | 68 | if (typeof opts !== 'object') { 69 | opts = {} 70 | } 71 | 72 | const { err, data } = ipc.sendSync('dns.lookup', { ...opts, id: rand64(), hostname }) 73 | 74 | if (err) { 75 | const e = new Error(`getaddrinfo EAI_AGAIN ${hostname}`) 76 | e.code = 'EAI_AGAIN' 77 | e.syscall = 'getaddrinfo' 78 | e.hostname = hostname 79 | // e.errno = -3008, // lib_uv constant? 80 | cb(e, null, null) 81 | return 82 | } 83 | 84 | cb(null, data?.address ?? null, data?.family ?? null) 85 | } 86 | 87 | export { 88 | promises 89 | } 90 | export default exports 91 | 92 | for (const key in exports) { 93 | const value = exports[key] 94 | if (key in promises && isFunction(value) && isFunction(promises[key])) { 95 | value[Symbol.for('nodejs.util.promisify.custom')] = promises[key] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /dns/promises.js: -------------------------------------------------------------------------------- 1 | import * as ipc from '../ipc.js' 2 | /** 3 | * @module DNS.promises 4 | * 5 | * This module enables name resolution. For example, use it to look up IP 6 | * addresses of host names. Although named for the Domain Name System (DNS), 7 | * it does not always use the DNS protocol for lookups. dns.lookup() uses the 8 | * operating system facilities to perform name resolution. It may not need to 9 | * perform any network communication. To perform name resolution the way other 10 | * applications on the same system do, use dns.lookup(). 11 | */ 12 | 13 | import { rand64 } from '../util.js' 14 | 15 | /** 16 | * @async 17 | * @see {@link https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options} 18 | * @param {string} hostname - The host name to resolve. 19 | * @param {Object=} opts - An options object. 20 | * @param {number|string} [opts.family=0] - The record family. Must be 4, 6, or 0. For backward compatibility reasons,'IPv4' and 'IPv6' are interpreted as 4 and 6 respectively. The value 0 indicates that IPv4 and IPv6 addresses are both returned. Default: 0. 21 | * @returns {Promise} 22 | */ 23 | export async function lookup (hostname, opts) { 24 | if (typeof hostname !== 'string') { 25 | const err = new TypeError(`The "hostname" argument must be of type string. Received type ${typeof hostname} (${hostname})`) 26 | err.code = 'ERR_INVALID_ARG_TYPE' 27 | throw err 28 | } 29 | 30 | if (typeof opts === 'number') { 31 | opts = { family: opts } 32 | } 33 | 34 | if (typeof opts !== 'object') { 35 | opts = {} 36 | } 37 | 38 | if (!opts.family) { 39 | opts.family = 4 40 | } 41 | 42 | const { err, data } = await ipc.send('dns.lookup', { ...opts, id: rand64(), hostname }) 43 | 44 | if (err) { 45 | const e = new Error(`getaddrinfo EAI_AGAIN ${hostname}`) 46 | e.code = 'EAI_AGAIN' 47 | e.syscall = 'getaddrinfo' 48 | e.hostname = hostname 49 | // e.errno = -3008, // lib_uv constant? 50 | return e 51 | } 52 | 53 | return data 54 | } 55 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | export const ABORT_ERR = 20 2 | export const ENCODING_ERR = 32 3 | export const INVALID_ACCESS_ERR = 15 4 | export const INDEX_SIZE_ERR = 1 5 | export const NETWORK_ERR = 19 6 | export const NOT_ALLOWED_ERR = 31 7 | export const NOT_FOUND_ERR = 8 8 | export const NOT_SUPPORTED_ERR = 9 9 | export const OPERATION_ERR = 30 10 | export const TIMEOUT_ERR = 23 11 | 12 | /** 13 | * An `AbortError` is an error type thrown in an `onabort()` level 0 14 | * event handler on an `AbortSignal` instance. 15 | */ 16 | export class AbortError extends Error { 17 | /** 18 | * The code given to an `ABORT_ERR` `DOMException` 19 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 20 | */ 21 | static get code () { return ABORT_ERR } 22 | 23 | /** 24 | * `AbortError` class constructor. 25 | * @param {AbortSignal|string} reasonOrSignal 26 | * @param {AbortSignal=} [signal] 27 | */ 28 | constructor (reason, signal, ...args) { 29 | if (reason?.reason) { 30 | signal = reason 31 | reason = signal.reason 32 | } 33 | 34 | super(reason || signal?.reason || 'The operation was aborted', ...args) 35 | 36 | this.signal = signal || null 37 | 38 | if (typeof Error.captureStackTrace === 'function') { 39 | Error.captureStackTrace(this, AbortError) 40 | } 41 | } 42 | 43 | get name () { 44 | return 'AbortError' 45 | } 46 | 47 | get code () { 48 | return 'ABORT_ERR' 49 | } 50 | } 51 | 52 | /** 53 | * An `BadRequestError` is an error type thrown in an `onabort()` level 0 54 | * event handler on an `BadRequestSignal` instance. 55 | */ 56 | export class BadRequestError extends Error { 57 | /** 58 | * The default code given to a `BadRequestError` 59 | */ 60 | static get code () { return 0 } 61 | 62 | /** 63 | * `BadRequestError` class constructor. 64 | * @param {string} message 65 | * @param {number} [code] 66 | */ 67 | constructor (message, ...args) { 68 | super(message, ...args) 69 | 70 | if (typeof Error.captureStackTrace === 'function') { 71 | Error.captureStackTrace(this, BadRequestError) 72 | } 73 | } 74 | 75 | get name () { 76 | return 'BadRequestError' 77 | } 78 | 79 | get code () { 80 | return 'BAD_REQUEST_ERR' 81 | } 82 | } 83 | 84 | /** 85 | * An `EncodingError` is an error type thrown when an internal exception 86 | * has occurred, such as in the native IPC layer. 87 | */ 88 | export class EncodingError extends Error { 89 | /** 90 | * The code given to an `ENCODING_ERR` `DOMException`. 91 | */ 92 | static get code () { return ENCODING_ERR } 93 | 94 | /** 95 | * `EncodingError` class constructor. 96 | * @param {string} message 97 | * @param {number} [code] 98 | */ 99 | constructor (message, ...args) { 100 | super(message, ...args) 101 | 102 | if (typeof Error.captureStackTrace === 'function') { 103 | Error.captureStackTrace(this, EncodingError) 104 | } 105 | } 106 | 107 | get name () { 108 | return 'EncodingError' 109 | } 110 | 111 | get code () { 112 | return 'ENCODING_ERR' 113 | } 114 | } 115 | 116 | /** 117 | * An `FinalizationRegistryCallbackError` is an error type thrown when an internal exception 118 | * has occurred, such as in the native IPC layer. 119 | */ 120 | export class FinalizationRegistryCallbackError extends Error { 121 | /** 122 | * The default code given to an `FinalizationRegistryCallbackError` 123 | */ 124 | static get code () { return 0 } 125 | 126 | /** 127 | * `FinalizationRegistryCallbackError` class constructor. 128 | * @param {string} message 129 | * @param {number} [code] 130 | */ 131 | constructor (message, ...args) { 132 | super(message, ...args) 133 | 134 | if (typeof Error.captureStackTrace === 'function') { 135 | Error.captureStackTrace(this, FinalizationRegistryCallbackError) 136 | } 137 | } 138 | 139 | get name () { 140 | return 'FinalizationRegistryCallbackError' 141 | } 142 | 143 | get code () { 144 | return 'FINALIZATION_REGISTRY_CALLBACK_ERR' 145 | } 146 | } 147 | 148 | /** 149 | * An `IndexSizeError` is an error type thrown when an internal exception 150 | * has occurred, such as in the native IPC layer. 151 | */ 152 | export class IndexSizeError extends Error { 153 | /** 154 | * The code given to an `NOT_FOUND_ERR` `DOMException` 155 | */ 156 | static get code () { return INDEX_SIZE_ERR } 157 | 158 | /** 159 | * `IndexSizeError` class constructor. 160 | * @param {string} message 161 | * @param {number} [code] 162 | */ 163 | constructor (message, ...args) { 164 | super(message, ...args) 165 | 166 | if (typeof Error.captureStackTrace === 'function') { 167 | Error.captureStackTrace(this, IndexSizeError) 168 | } 169 | } 170 | 171 | get name () { 172 | return 'IndexSizeError' 173 | } 174 | 175 | get code () { 176 | return 'INDEX_SIZE_ERR' 177 | } 178 | } 179 | 180 | export const kInternalErrorCode = Symbol.for('InternalError.code') 181 | 182 | /** 183 | * An `InternalError` is an error type thrown when an internal exception 184 | * has occurred, such as in the native IPC layer. 185 | */ 186 | export class InternalError extends Error { 187 | /** 188 | * The default code given to an `InternalError` 189 | */ 190 | static get code () { return 0 } 191 | 192 | /** 193 | * `InternalError` class constructor. 194 | * @param {string} message 195 | * @param {number} [code] 196 | */ 197 | constructor (message, code, ...args) { 198 | super(message, code, ...args) 199 | 200 | if (code) { 201 | this[kInternalErrorCode] = code 202 | } 203 | 204 | if (typeof Error.captureStackTrace === 'function') { 205 | Error.captureStackTrace(this, InternalError) 206 | } 207 | } 208 | 209 | get name () { 210 | return 'InternalError' 211 | } 212 | 213 | get code () { 214 | return this[kInternalErrorCode] || 'INTERNAL_ERR' 215 | } 216 | 217 | set code (code) { 218 | this[kInternalErrorCode] = code 219 | } 220 | } 221 | 222 | /** 223 | * An `InvalidAccessError` is an error type thrown when an internal exception 224 | * has occurred, such as in the native IPC layer. 225 | */ 226 | export class InvalidAccessError extends Error { 227 | /** 228 | * The code given to an `INVALID_ACCESS_ERR` `DOMException` 229 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 230 | */ 231 | static get code () { return INVALID_ACCESS_ERR } 232 | 233 | /** 234 | * `InvalidAccessError` class constructor. 235 | * @param {string} message 236 | * @param {number} [code] 237 | */ 238 | constructor (message, ...args) { 239 | super(message, ...args) 240 | 241 | if (typeof Error.captureStackTrace === 'function') { 242 | Error.captureStackTrace(this, InvalidAccessError) 243 | } 244 | } 245 | 246 | get name () { 247 | return 'InvalidAccessError' 248 | } 249 | 250 | get code () { 251 | return 'INVALID_ACCESS_ERR' 252 | } 253 | } 254 | 255 | /** 256 | * An `NetworkError` is an error type thrown when an internal exception 257 | * has occurred, such as in the native IPC layer. 258 | */ 259 | export class NetworkError extends Error { 260 | /** 261 | * The code given to an `NETWORK_ERR` `DOMException` 262 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 263 | */ 264 | static get code () { return NETWORK_ERR } 265 | 266 | /** 267 | * `NetworkError` class constructor. 268 | * @param {string} message 269 | * @param {number} [code] 270 | */ 271 | constructor (message, ...args) { 272 | super(message, ...args) 273 | 274 | this.name = 'NetworkError' 275 | 276 | if (typeof Error.captureStackTrace === 'function') { 277 | Error.captureStackTrace(this, NetworkError) 278 | } 279 | } 280 | 281 | get name () { 282 | return 'NetworkError' 283 | } 284 | 285 | get code () { 286 | return 'NETWORK_ERR' 287 | } 288 | } 289 | 290 | /** 291 | * An `NotAllowedError` is an error type thrown when an internal exception 292 | * has occurred, such as in the native IPC layer. 293 | */ 294 | export class NotAllowedError extends Error { 295 | /** 296 | * The code given to an `NOT_ALLOWED_ERR` `DOMException` 297 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 298 | */ 299 | static get code () { return NOT_ALLOWED_ERR } 300 | 301 | /** 302 | * `NotAllowedError` class constructor. 303 | * @param {string} message 304 | * @param {number} [code] 305 | */ 306 | constructor (message, ...args) { 307 | super(message, ...args) 308 | 309 | if (typeof Error.captureStackTrace === 'function') { 310 | Error.captureStackTrace(this, NotAllowedError) 311 | } 312 | } 313 | 314 | get name () { 315 | return 'NotAllowedError' 316 | } 317 | 318 | get code () { 319 | return 'NOT_ALLOWED_ERR' 320 | } 321 | } 322 | 323 | /** 324 | * An `NotFoundError` is an error type thrown when an internal exception 325 | * has occurred, such as in the native IPC layer. 326 | */ 327 | export class NotFoundError extends Error { 328 | /** 329 | * The code given to an `NOT_FOUND_ERR` `DOMException` 330 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 331 | */ 332 | static get code () { return NOT_FOUND_ERR } 333 | 334 | /** 335 | * `NotFoundError` class constructor. 336 | * @param {string} message 337 | * @param {number} [code] 338 | */ 339 | constructor (message, ...args) { 340 | super(message, ...args) 341 | 342 | if (typeof Error.captureStackTrace === 'function') { 343 | Error.captureStackTrace(this, NotFoundError) 344 | } 345 | } 346 | 347 | get name () { 348 | return 'NotFoundError' 349 | } 350 | 351 | get code () { 352 | return 'NOT_FOUND_ERR' 353 | } 354 | } 355 | 356 | /** 357 | * An `NotSupportedError` is an error type thrown when an internal exception 358 | * has occurred, such as in the native IPC layer. 359 | */ 360 | export class NotSupportedError extends Error { 361 | /** 362 | * The code given to an `NOT_SUPPORTED_ERR` `DOMException` 363 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 364 | */ 365 | static get code () { return NOT_SUPPORTED_ERR } 366 | 367 | /** 368 | * `NotSupportedError` class constructor. 369 | * @param {string} message 370 | * @param {number} [code] 371 | */ 372 | constructor (message, ...args) { 373 | super(message, ...args) 374 | 375 | if (typeof Error.captureStackTrace === 'function') { 376 | Error.captureStackTrace(this, NotSupportedError) 377 | } 378 | } 379 | 380 | get name () { 381 | return 'NotSupportedError' 382 | } 383 | 384 | get code () { 385 | return 'NOT_SUPPORTED_ERR' 386 | } 387 | } 388 | 389 | /** 390 | * An `OperationError` is an error type thrown when an internal exception 391 | * has occurred, such as in the native IPC layer. 392 | */ 393 | export class OperationError extends Error { 394 | /** 395 | * The code given to an `NOT_FOUND_ERR` `DOMException` 396 | */ 397 | static get code () { return OPERATION_ERR } 398 | 399 | /** 400 | * `OperationError` class constructor. 401 | * @param {string} message 402 | * @param {number} [code] 403 | */ 404 | constructor (message, ...args) { 405 | super(message, ...args) 406 | 407 | if (typeof Error.captureStackTrace === 'function') { 408 | Error.captureStackTrace(this, OperationError) 409 | } 410 | } 411 | 412 | get name () { 413 | return 'OperationError' 414 | } 415 | 416 | get code () { 417 | return 'OPERATION_ERR' 418 | } 419 | } 420 | 421 | /** 422 | * An `TimeoutError` is an error type thrown when an operation timesout. 423 | */ 424 | export class TimeoutError extends Error { 425 | /** 426 | * The code given to an `TIMEOUT_ERR` `DOMException` 427 | * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} 428 | */ 429 | static get code () { return TIMEOUT_ERR } 430 | 431 | /** 432 | * `TimeoutError` class constructor. 433 | * @param {string} message 434 | */ 435 | constructor (message, ...args) { 436 | super(message, ...args) 437 | 438 | if (typeof Error.captureStackTrace === 'function') { 439 | Error.captureStackTrace(this, TimeoutError) 440 | } 441 | } 442 | 443 | get name () { 444 | return 'TimeoutError' 445 | } 446 | 447 | get code () { 448 | return 'TIMEOUT_ERR' 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /fs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @notice This is a rexports of `fs/index.js` so consumers will 3 | * need to only `import * as fs from '@socketsupply/socket-api/fs.js'` 4 | */ 5 | import * as exports from './fs.js' 6 | 7 | export * from './fs/index.js' 8 | export default exports 9 | -------------------------------------------------------------------------------- /fs/binding.js: -------------------------------------------------------------------------------- 1 | import * as ipc from '../ipc.js' 2 | 3 | export default ipc.createBinding('fs', { 4 | default: 'request', 5 | write: 'write' 6 | }) 7 | -------------------------------------------------------------------------------- /fs/constants.js: -------------------------------------------------------------------------------- 1 | import { sendSync } from '../ipc.js' 2 | 3 | const constants = sendSync('fs.constants')?.data || {} 4 | 5 | /** 6 | * This flag can be used with uv_fs_copyfile() to return an error if the 7 | * destination already exists. 8 | */ 9 | export const COPYFILE_EXCL = 0x0001 10 | 11 | /** 12 | * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. 13 | * If copy-on-write is not supported, a fallback copy mechanism is used. 14 | */ 15 | export const COPYFILE_FICLONE = 0x0002 16 | 17 | /** 18 | * This flag can be used with uv_fs_copyfile() to attempt to create a reflink. 19 | * If copy-on-write is not supported, an error is returned. 20 | */ 21 | export const COPYFILE_FICLONE_FORCE = 0x0004 22 | 23 | export const UV_DIRENT_UNKNOWN = constants.UV_DIRENT_UNKNOWN || 0 24 | export const UV_DIRENT_FILE = constants.UV_DIRENT_FILE || 1 25 | export const UV_DIRENT_DIR = constants.UV_DIRENT_DIR || 2 26 | export const UV_DIRENT_LINK = constants.UV_DIRENT_LINK || 3 27 | export const UV_DIRENT_FIFO = constants.UV_DIRENT_FIFO || 4 28 | export const UV_DIRENT_SOCKET = constants.UV_DIRENT_SOCKET || 5 29 | export const UV_DIRENT_CHAR = constants.UV_DIRENT_CHAR || 6 30 | export const UV_DIRENT_BLOCK = constants.UV_DIRENT_BLOCK || 7 31 | 32 | export const O_RDONLY = constants.O_RDONLY || 0 33 | export const O_WRONLY = constants.O_WRONLY || 0 34 | export const O_RDWR = constants.O_RDWR || 0 35 | export const O_APPEND = constants.O_APPEND || 0 36 | export const O_ASYNC = constants.O_ASYNC || 0 37 | export const O_CLOEXEC = constants.O_CLOEXEC || 0 38 | export const O_CREAT = constants.O_CREAT || 0 39 | export const O_DIRECT = constants.O_DIRECT || 0 40 | export const O_DIRECTORY = constants.O_DIRECTORY || 0 41 | export const O_DSYNC = constants.O_DSYNC || 0 42 | export const O_EXCL = constants.O_EXCL || 0 43 | export const O_LARGEFILE = constants.O_LARGEFILE || 0 44 | export const O_NOATIME = constants.O_NOATIME || 0 45 | export const O_NOCTTY = constants.O_NOCTTY || 0 46 | export const O_NOFOLLOW = constants.O_NOFOLLOW || 0 47 | export const O_NONBLOCK = constants.O_NONBLOCK || 0 48 | export const O_NDELAY = constants.O_NDELAY || 0 49 | export const O_PATH = constants.O_PATH || 0 50 | export const O_SYNC = constants.O_SYNC || 0 51 | export const O_TMPFILE = constants.O_TMPFILE || 0 52 | export const O_TRUNC = constants.O_TRUNC || 0 53 | 54 | export const S_IFMT = constants.S_IFMT || 0 55 | export const S_IFREG = constants.S_IFREG || 0 56 | export const S_IFDIR = constants.S_IFDIR || 0 57 | export const S_IFCHR = constants.S_IFCHR || 0 58 | export const S_IFBLK = constants.S_IFBLK || 0 59 | export const S_IFIFO = constants.S_IFIFO || 0 60 | export const S_IFLNK = constants.S_IFLNK || 0 61 | export const S_IFSOCK = constants.S_IFSOCK || 0 62 | export const S_IRWXU = constants.S_IRWXU || 0 63 | export const S_IRUSR = constants.S_IRUSR || 0 64 | export const S_IWUSR = constants.S_IWUSR || 0 65 | export const S_IXUSR = constants.S_IXUSR || 0 66 | export const S_IRWXG = constants.S_IRWXG || 0 67 | export const S_IRGRP = constants.S_IRGRP || 0 68 | export const S_IWGRP = constants.S_IWGRP || 0 69 | export const S_IXGRP = constants.S_IXGRP || 0 70 | export const S_IRWXO = constants.S_IRWXO || 0 71 | export const S_IROTH = constants.S_IROTH || 0 72 | export const S_IWOTH = constants.S_IWOTH || 0 73 | export const S_IXOTH = constants.S_IXOTH || 0 74 | 75 | export const F_OK = constants.F_OK || 0 76 | export const R_OK = constants.R_OK || 0 77 | export const W_OK = constants.W_OK || 0 78 | export const X_OK = constants.X_OK || 0 79 | -------------------------------------------------------------------------------- /fs/dir.js: -------------------------------------------------------------------------------- 1 | import { DirectoryHandle } from './handle.js' 2 | import { Buffer } from '../buffer.js' 3 | import { 4 | UV_DIRENT_UNKNOWN, 5 | UV_DIRENT_FILE, 6 | UV_DIRENT_DIR, 7 | UV_DIRENT_LINK, 8 | UV_DIRENT_FIFO, 9 | UV_DIRENT_SOCKET, 10 | UV_DIRENT_CHAR, 11 | UV_DIRENT_BLOCK 12 | } from './constants.js' 13 | 14 | export const kType = Symbol.for('fs.Dirent.type') 15 | 16 | function checkDirentType (dirent, property) { 17 | return dirent.type === property 18 | } 19 | 20 | /** 21 | * Sorts directory entries 22 | * @param {string|Dirent} a 23 | * @param {string|Dirent} b 24 | * @return {number} 25 | */ 26 | export function sortDirectoryEntries (a, b) { 27 | if (a instanceof Dirent) { 28 | return sortDirectoryEntries(a.name, b.name) 29 | } 30 | 31 | return a < b ? -1 : a > b ? 1 : 0 32 | } 33 | 34 | /** 35 | * A containerr for a directory and its entries. This class supports scanning 36 | * a directory entry by entry with a `read()` method. The `Symbol.asyncIterator` 37 | * interface is exposed along with an AsyncGenerator `entries()` method. 38 | * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdir} 39 | */ 40 | export class Dir { 41 | static from (fdOrHandle, options) { 42 | if (fdOrHandle instanceof DirectoryHandle) { 43 | return new this(fdOrHandle, options) 44 | } 45 | 46 | return new this(DirectoryHandle.from(fdOrHandle, options), options) 47 | } 48 | 49 | /** 50 | * `Dir` class constructor. 51 | * @param {DirectoryHandle} handle 52 | * @param {object=} options 53 | */ 54 | constructor (handle, options) { 55 | this.path = handle?.path ?? null 56 | this.handle = handle 57 | this.encoding = options?.encoding || 'utf8' 58 | this.withFileTypes = options?.withFileTypes !== false 59 | } 60 | 61 | /** 62 | * Closes container and underlying handle. 63 | * @param {object|function} options 64 | * @param {function=} callback 65 | */ 66 | async close (options, callback) { 67 | if (typeof options === 'function') { 68 | callback = options 69 | options = {} 70 | } 71 | 72 | if (typeof callback === 'function') { 73 | try { 74 | await this.handle?.close(options) 75 | } catch (err) { 76 | return callback(err) 77 | } 78 | 79 | return callback(null) 80 | } 81 | 82 | return await this.handle?.close(options) 83 | } 84 | 85 | /** 86 | * Reads and returns directory entry. 87 | * @param {object|function} options 88 | * @param {function=} callback 89 | * @return {Dirent|string} 90 | */ 91 | async read (options, callback) { 92 | if (typeof options === 'function') { 93 | callback = options 94 | options = {} 95 | } 96 | 97 | let results = [] 98 | 99 | try { 100 | results = await this.handle?.read(options) 101 | } catch (err) { 102 | if (typeof callback === 'function') { 103 | callback(err) 104 | return 105 | } 106 | 107 | throw err 108 | } 109 | 110 | results = results.map((result) => { 111 | if (this.withFileTypes) { 112 | result = Dirent.from(result) 113 | 114 | if (this.encoding === 'buffer') { 115 | result.name = Buffer.from(result.name) 116 | } else { 117 | result.name = Buffer.from(result.name).toString(this.encoding) 118 | } 119 | } else { 120 | result = result.name 121 | 122 | if (this.encoding === 'buffer') { 123 | result = Buffer.from(result) 124 | } else { 125 | result = Buffer.from(result).toString(this.encoding) 126 | } 127 | } 128 | 129 | return result 130 | }) 131 | 132 | if (results.length <= 1) { 133 | results = results[0] || null 134 | } 135 | 136 | if (typeof callback === 'function') { 137 | callback(null, results) 138 | return 139 | } 140 | 141 | return results 142 | } 143 | 144 | /** 145 | * AsyncGenerator which yields directory entries. 146 | * @param {object=} options 147 | */ 148 | async * entries (options) { 149 | try { 150 | while (true) { 151 | const results = await this.read(options) 152 | 153 | if (results === null) { 154 | break 155 | } 156 | 157 | if (Array.isArray(results)) { 158 | for (let i = 0; i < results.length; ++i) { 159 | yield results[i] 160 | } 161 | } else { 162 | yield results 163 | } 164 | } 165 | } finally { 166 | await this.close() 167 | } 168 | } 169 | 170 | /** 171 | * `for await (...)` AsyncGenerator support. 172 | */ 173 | get [Symbol.asyncIterator] () { 174 | return this.entries 175 | } 176 | } 177 | 178 | /** 179 | * A container for a directory entry. 180 | * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdirent} 181 | */ 182 | export class Dirent { 183 | static get UNKNOWN () { return UV_DIRENT_UNKNOWN } 184 | static get FILE () { return UV_DIRENT_FILE } 185 | static get DIR () { return UV_DIRENT_DIR } 186 | static get LINK () { return UV_DIRENT_LINK } 187 | static get FIFO () { return UV_DIRENT_FIFO } 188 | static get SOCKET () { return UV_DIRENT_SOCKET } 189 | static get CHAR () { return UV_DIRENT_CHAR } 190 | static get BLOCK () { return UV_DIRENT_BLOCK } 191 | 192 | /** 193 | * Creates `Dirent` instance from input. 194 | * @param {object|string} name 195 | * @param {(string|number)=} type 196 | */ 197 | static from (name, type) { 198 | if (typeof name === 'object') { 199 | return new this(name?.name, name?.type) 200 | } 201 | 202 | return new this(name, type ?? Dirent.UNKNOWN) 203 | } 204 | 205 | /** 206 | * `Dirent` class constructor. 207 | * @param {string} name 208 | * @param {string|number} type 209 | */ 210 | constructor (name, type) { 211 | this.name = name ?? null 212 | this[kType] = parseInt(type ?? Dirent.UNKNOWN) 213 | } 214 | 215 | /** 216 | * Read only type. 217 | */ 218 | get type () { 219 | return this[kType] 220 | } 221 | 222 | /** 223 | * `true` if `Dirent` instance is a directory. 224 | */ 225 | isDirectory () { 226 | return checkDirentType(this, Dirent.DIR) 227 | } 228 | 229 | /** 230 | * `true` if `Dirent` instance is a file. 231 | */ 232 | isFile () { 233 | return checkDirentType(this, Dirent.FILE) 234 | } 235 | 236 | /** 237 | * `true` if `Dirent` instance is a block device. 238 | */ 239 | isBlockDevice () { 240 | return checkDirentType(this, Dirent.BLOCK) 241 | } 242 | 243 | /** 244 | * `true` if `Dirent` instance is a character device. 245 | */ 246 | isCharacterDevice () { 247 | return checkDirentType(this, Dirent.CHAR) 248 | } 249 | 250 | /** 251 | * `true` if `Dirent` instance is a symbolic link. 252 | */ 253 | isSymbolicLink () { 254 | return checkDirentType(this, Dirent.LINK) 255 | } 256 | 257 | /** 258 | * `true` if `Dirent` instance is a FIFO. 259 | */ 260 | isFIFO () { 261 | return checkDirentType(this, Dirent.FIFO) 262 | } 263 | 264 | /** 265 | * `true` if `Dirent` instance is a socket. 266 | */ 267 | isSocket () { 268 | return checkDirentType(this, Dirent.SOCKET) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /fs/fds.js: -------------------------------------------------------------------------------- 1 | import console from '../console.js' 2 | import ipc from '../ipc.js' 3 | 4 | /** 5 | * Static contsiner to map file descriptors to internal 6 | * identifiers with type reflection. 7 | */ 8 | export default new class FileDescriptorsMap { 9 | types = new Map() 10 | fds = new Map() 11 | ids = new Map() 12 | 13 | constructor () { 14 | this.syncOpenDescriptors() 15 | } 16 | 17 | get size () { 18 | return this.ids.size 19 | } 20 | 21 | get (id) { 22 | return this.fds.get(id) 23 | } 24 | 25 | async syncOpenDescriptors () { 26 | // wait for DOM to be loaded and ready 27 | if (typeof globalThis.document === 'object') { 28 | if (globalThis.document.readyState !== 'complete') { 29 | await new Promise((resolve) => { 30 | document.addEventListener('DOMContentLoaded', resolve, { once: true }) 31 | }) 32 | } 33 | } 34 | 35 | const result = ipc.sendSync('fs.getOpenDescriptors') 36 | if (Array.isArray(result.data)) { 37 | for (const { id, fd, type } of result.data) { 38 | this.set(id, fd, type) 39 | } 40 | } 41 | } 42 | 43 | set (id, fd, type) { 44 | if (!type) { 45 | type = id === fd ? 'directory' : 'file' 46 | } 47 | 48 | this.fds.set(id, fd) 49 | this.ids.set(fd, id) 50 | this.types.set(id, type) 51 | this.types.set(fd, type) 52 | } 53 | 54 | has (id) { 55 | return this.fds.has(id) || this.ids.has(id) 56 | } 57 | 58 | setEntry (id, entry) { 59 | if (entry.fd.length > 16) { 60 | this.set(id, entry.fd) 61 | } else { 62 | this.set(id, parseInt(entry.fd)) 63 | } 64 | } 65 | 66 | fd (id) { 67 | return this.get(id) 68 | } 69 | 70 | id (fd) { 71 | return this.ids.get(fd) 72 | } 73 | 74 | async release (id, closeDescriptor = true) { 75 | let result = null 76 | 77 | if (id === undefined) { 78 | this.clear() 79 | 80 | if (closeDescriptor !== false) { 81 | result = await ipc.send('fs.closeOpenDescriptors') 82 | 83 | if (result.err && !/found/i.test(result.err.message)) { 84 | console.warn('fs.fds.release', result.err.message || result.err) 85 | } 86 | } 87 | 88 | return 89 | } 90 | 91 | id = this.ids.get(id) || id 92 | 93 | const fd = this.fds.get(id) 94 | 95 | this.fds.delete(id) 96 | this.fds.delete(fd) 97 | 98 | this.ids.delete(fd) 99 | this.ids.delete(id) 100 | 101 | this.types.delete(id) 102 | this.types.delete(fd) 103 | 104 | if (closeDescriptor !== false) { 105 | result = await ipc.send('fs.closeOpenDescriptor', { id }) 106 | 107 | if (result.err && !/found/i.test(result.err.message)) { 108 | console.warn('fs.fds.release', result.err.message || result.err) 109 | } 110 | } 111 | } 112 | 113 | async retain (id) { 114 | const result = await ipc.send('fs.retainOpenDescriptor', { id }) 115 | 116 | if (result.err) { 117 | throw result.err 118 | } 119 | 120 | return result.data 121 | } 122 | 123 | delete (id) { 124 | this.release(id) 125 | } 126 | 127 | clear () { 128 | this.ids.clear() 129 | this.fds.clear() 130 | this.types.clear() 131 | } 132 | 133 | typeof (id) { 134 | return this.types.get(id) || this.types.get(this.fds.get(id)) 135 | } 136 | 137 | entries () { 138 | return this.ids.entries() 139 | } 140 | }() 141 | -------------------------------------------------------------------------------- /fs/flags.js: -------------------------------------------------------------------------------- 1 | import { 2 | O_APPEND, 3 | O_RDONLY, 4 | O_WRONLY, 5 | O_TRUNC, 6 | O_CREAT, 7 | O_RDWR, 8 | O_EXCL, 9 | O_SYNC 10 | } from './constants.js' 11 | 12 | export function normalizeFlags (flags) { 13 | if (typeof flags === 'number') { 14 | return flags 15 | } 16 | 17 | if (flags !== undefined && typeof flags !== 'string') { 18 | throw new TypeError( 19 | `Expecting flags to be a string or number: Got ${typeof flags}` 20 | ) 21 | } 22 | 23 | switch (flags) { 24 | case 'r': 25 | return O_RDONLY 26 | 27 | case 'rs': case 'sr': 28 | return O_RDONLY | O_SYNC 29 | 30 | case 'r+': 31 | return O_RDWR 32 | 33 | case 'rs+': case 'sr+': 34 | return O_RDWR | O_SYNC 35 | 36 | case 'w': 37 | return O_TRUNC | O_CREAT | O_WRONLY 38 | 39 | case 'wx': case 'xw': 40 | return O_TRUNC | O_CREAT | O_WRONLY | O_EXCL 41 | 42 | case 'w+': 43 | return O_TRUNC | O_CREAT | O_RDWR 44 | 45 | case 'wx+': case 'xw+': 46 | return O_TRUNC | O_CREAT | O_RDWR | O_EXCL 47 | 48 | case 'a': 49 | return O_APPEND | O_CREAT | O_WRONLY 50 | 51 | case 'ax': case 'xa': 52 | return O_APPEND | O_CREAT | O_WRONLY | O_EXCL 53 | 54 | case 'as': case 'sa': 55 | return O_APPEND | O_CREAT | O_WRONLY | O_SYNC 56 | 57 | case 'a+': 58 | return O_APPEND | O_CREAT | O_RDWR 59 | 60 | case 'ax+': case 'xa+': 61 | return O_APPEND | O_CREAT | O_RDWR | O_EXCL 62 | 63 | case 'as+': case 'sa+': 64 | return O_APPEND | O_CREAT | O_RDWR | O_SYNC 65 | } 66 | 67 | return O_RDONLY 68 | } 69 | -------------------------------------------------------------------------------- /fs/promises.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module FS.promises 3 | */ 4 | import { DirectoryHandle, FileHandle } from './handle.js' 5 | import { Dir, sortDirectoryEntries } from './dir.js' 6 | import console from '../console.js' 7 | import ipc from '../ipc.js' 8 | 9 | import * as exports from './promises.js' 10 | 11 | async function visit (path, options, callback) { 12 | if (typeof options === 'function') { 13 | callback = options 14 | options = {} 15 | } 16 | 17 | const { flags, flag, mode } = options || {} 18 | 19 | const handle = await FileHandle.open(path, flags || flag, mode, options) 20 | 21 | // just visit `FileHandle`, without closing if given 22 | if (path instanceof FileHandle) { 23 | return await callback(handle) 24 | } else if (path?.fd) { 25 | return await callback(FileHandle.from(path.fd)) 26 | } 27 | 28 | const value = await callback(handle) 29 | await handle.close(options) 30 | 31 | return value 32 | } 33 | 34 | /** 35 | * Asynchronously check access a file. 36 | * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesaccesspath-mode} 37 | * @param {string | Buffer | URL} path 38 | * @param {string=} [mode] 39 | * @param {object=} [options] 40 | */ 41 | export async function access (path, mode, options) { 42 | return await FileHandle.access(path, mode, options) 43 | } 44 | 45 | /** 46 | * @TODO 47 | * @ignore 48 | */ 49 | export async function appendFile (path, data, options) { 50 | } 51 | 52 | /** 53 | * @see {@link https://nodejs.org/api/fs.html#fspromiseschmodpath-mode} 54 | * @param {string | Buffer | URL} path 55 | * @param {number} mode 56 | * @returns {Promise} 57 | */ 58 | export async function chmod (path, mode) { 59 | if (typeof mode !== 'number') { 60 | throw new TypeError(`The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received ${mode}`) 61 | } 62 | 63 | if (mode < 0 || !Number.isInteger(mode)) { 64 | throw new RangeError(`The value of "mode" is out of range. It must be an integer. Received ${mode}`) 65 | } 66 | 67 | await ipc.send('fs.chmod', { mode, path }) 68 | } 69 | 70 | /** 71 | * @TODO 72 | * @ignore 73 | */ 74 | export async function chown (path, uid, gid) { 75 | } 76 | 77 | /** 78 | * @TODO 79 | * @ignore 80 | */ 81 | export async function copyFile (src, dst, mode) { 82 | } 83 | 84 | /** 85 | * @TODO 86 | * @ignore 87 | */ 88 | export async function lchmod (path, mode) { 89 | } 90 | 91 | /** 92 | * @TODO 93 | * @ignore 94 | */ 95 | export async function lchown (path, uid, gid) { 96 | } 97 | 98 | /** 99 | * @TODO 100 | * @ignore 101 | */ 102 | export async function lutimes (path, atime, mtime) { 103 | } 104 | 105 | /** 106 | * @TODO 107 | * @ignore 108 | */ 109 | export async function link (existingPath, newPath) { 110 | } 111 | 112 | /** 113 | * @TODO 114 | * @ignore 115 | */ 116 | export async function lstat (path, options) { 117 | } 118 | 119 | /** 120 | * Asynchronously creates a directory. 121 | * @todo recursive option is not implemented yet. 122 | * 123 | * @param {String} path - The path to create 124 | * @param {Object} options - The optional options argument can be an integer specifying mode (permission and sticky bits), or an object with a mode property and a recursive property indicating whether parent directories should be created. Calling fs.mkdir() when path is a directory that exists results in an error only when recursive is false. 125 | * @return {Primise} - Upon success, fulfills with undefined if recursive is false, or the first directory path created if recursive is true. 126 | */ 127 | export async function mkdir (path, options = {}) { 128 | const mode = options.mode ?? 0o777 129 | const recursive = options.recurisve === true 130 | 131 | if (typeof mode !== 'number') { 132 | throw new TypeError('mode must be a number.') 133 | } 134 | 135 | if (mode < 0 || !Number.isInteger(mode)) { 136 | throw new RangeError('mode must be a positive finite number.') 137 | } 138 | 139 | return await ipc.request('fs.mkdir', { mode, path, recursive }) 140 | } 141 | 142 | /** 143 | * Asynchronously open a file. 144 | * @see {@link https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode } 145 | * 146 | * @param {string | Buffer | URL} path 147 | * @param {string} flags - default: 'r' 148 | * @param {string} mode - default: 0o666 149 | * @return {Promise} 150 | */ 151 | export async function open (path, flags, mode) { 152 | return await FileHandle.open(path, flags, mode) 153 | } 154 | 155 | /** 156 | * @see {@link https://nodejs.org/api/fs.html#fspromisesopendirpath-options} 157 | * @param {string | Buffer | URL} path 158 | * @param {object=} [options] 159 | * @param {string=} [options.encoding = 'utf8'] 160 | * @param {number=} [options.bufferSize = 32] 161 | * @return {Promise} 162 | */ 163 | export async function opendir (path, options) { 164 | const handle = await DirectoryHandle.open(path, options) 165 | return new Dir(handle, options) 166 | } 167 | 168 | /** 169 | * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreaddirpath-options} 170 | * @param {string | Buffer | URL} path 171 | * @param {object=} options 172 | * @param {string=} [options.encoding = 'utf8'] 173 | * @param {boolean=} [options.withFileTypes = false] 174 | */ 175 | export async function readdir (path, options) { 176 | options = { entries: DirectoryHandle.MAX_ENTRIES, ...options } 177 | 178 | const entries = [] 179 | const handle = await DirectoryHandle.open(path, options) 180 | const dir = new Dir(handle, options) 181 | 182 | for await (const entry of dir.entries(options)) { 183 | entries.push(entry) 184 | } 185 | 186 | if (!dir.closing && !dir.closed) { 187 | try { 188 | await dir.close() 189 | } catch (err) { 190 | if (!/not opened/i.test(err.message)) { 191 | console.warn(err) 192 | } 193 | } 194 | } 195 | 196 | return entries.sort(sortDirectoryEntries) 197 | } 198 | 199 | /** 200 | * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreadfilepath-options} 201 | * @param {string} path 202 | * @param {object=} [options] 203 | * @param {(string|null)=} [options.encoding = null] 204 | * @param {string=} [options.flag = 'r'] 205 | * @param {AbortSignal=} [options.signal] 206 | * @return {Promise} 207 | */ 208 | export async function readFile (path, options) { 209 | if (typeof options === 'string') { 210 | options = { encoding: options } 211 | } 212 | options = { 213 | flags: 'r', 214 | ...options 215 | } 216 | return await visit(path, options, async (handle) => { 217 | return await handle.readFile(options) 218 | }) 219 | } 220 | 221 | /** 222 | * @TODO 223 | * @ignore 224 | */ 225 | export async function readlink (path, options) { 226 | } 227 | 228 | /** 229 | * @TODO 230 | * @ignore 231 | */ 232 | export async function realpath (path, options) { 233 | } 234 | 235 | /** 236 | * @TODO 237 | * @ignore 238 | */ 239 | export async function rename (oldPath, newPath) { 240 | } 241 | 242 | /** 243 | * @TODO 244 | * @ignore 245 | */ 246 | export async function rmdir (path, options) { 247 | } 248 | 249 | /** 250 | * @TODO 251 | * @ignore 252 | */ 253 | export async function rm (path, options) { 254 | } 255 | 256 | /** 257 | * @see {@link https://nodejs.org/api/fs.html#fspromisesstatpath-options} 258 | * @param {string | Buffer | URL} path 259 | * @param {object=} [options] 260 | * @param {boolean=} [options.bigint = false] 261 | * @return {Promise} 262 | */ 263 | export async function stat (path, options) { 264 | return await visit(path, {}, async (handle) => { 265 | return await handle.stat(options) 266 | }) 267 | } 268 | 269 | /** 270 | * @TODO 271 | * @ignore 272 | */ 273 | export async function symlink (target, path, type) { 274 | } 275 | 276 | /** 277 | * @TODO 278 | * @ignore 279 | */ 280 | export async function truncate (path, length) { 281 | } 282 | 283 | /** 284 | * @TODO 285 | * @ignore 286 | */ 287 | export async function unlink (path) { 288 | } 289 | 290 | /** 291 | * @TODO 292 | * @ignore 293 | */ 294 | export async function utimes (path, atime, mtime) { 295 | } 296 | 297 | /** 298 | * @TODO 299 | * @ignore 300 | */ 301 | export async function watch (path, options) { 302 | } 303 | 304 | /** 305 | * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromiseswritefilefile-data-options} 306 | * @param {string | Buffer | URL | FileHandle} path - filename or FileHandle 307 | * @param {string|Buffer|Array|DataView|TypedArray|Stream} data 308 | * @param {object=} [options] 309 | * @param {string|null} [options.encoding = 'utf8'] 310 | * @param {number} [options.mode = 0o666] 311 | * @param {string} [options.flag = 'w'] 312 | * @param {AbortSignal=} [options.signal] 313 | * @return {Promise} 314 | */ 315 | // FIXME: truncate file by default (support flags). Currently it fails if file exists 316 | export async function writeFile (path, data, options) { 317 | if (typeof options === 'string') { 318 | options = { encoding: options } 319 | } 320 | options = { flag: 'w', mode: 0o666, ...options } 321 | return await visit(path, options, async (handle) => { 322 | return await handle.writeFile(data, options) 323 | }) 324 | } 325 | export * as constants from './constants.js' 326 | export default exports 327 | -------------------------------------------------------------------------------- /fs/stats.js: -------------------------------------------------------------------------------- 1 | import * as constants from './constants.js' 2 | import * as os from '../os.js' 3 | 4 | const isWindows = /win/i.test(os.type()) 5 | 6 | function checkMode (mode, property) { 7 | if (isWindows) { 8 | if ( 9 | property === constants.S_IFIFO || 10 | property === constants.S_IFBLK || 11 | property === constants.S_IFSOCK 12 | ) { 13 | return false 14 | } 15 | } 16 | 17 | if (typeof mode === 'bigint') { 18 | return (mode & BigInt(constants.S_IFMT)) === BigInt(property) 19 | } 20 | 21 | return (mode & constants.S_IFMT) === property 22 | } 23 | 24 | /** 25 | * @TODO 26 | */ 27 | export class Stats { 28 | /** 29 | * @TODO 30 | */ 31 | static from (stat, fromBigInt) { 32 | if (fromBigInt) { 33 | return new this({ 34 | dev: BigInt(stat.st_dev), 35 | ino: BigInt(stat.st_ino), 36 | mode: BigInt(stat.st_mode), 37 | nlink: BigInt(stat.st_nlink), 38 | uid: BigInt(stat.st_uid), 39 | gid: BigInt(stat.st_gid), 40 | rdev: BigInt(stat.st_rdev), 41 | size: BigInt(stat.st_size), 42 | blksize: BigInt(stat.st_blksize), 43 | blocks: BigInt(stat.st_blocks), 44 | atimeMs: BigInt(stat.st_atim.tv_sec) * 1000n + BigInt(stat.st_atim.tv_nsec) / 1000_000n, 45 | mtimeMs: BigInt(stat.st_mtim.tv_sec) * 1000n + BigInt(stat.st_mtim.tv_nsec) / 1000_000n, 46 | ctimeMs: BigInt(stat.st_ctim.tv_sec) * 1000n + BigInt(stat.st_ctim.tv_nsec) / 1000_000n, 47 | birthtimeMs: BigInt(stat.st_birthtim.tv_sec) * 1000n + BigInt(stat.st_birthtim.tv_nsec) / 1000_000n, 48 | atimNs: BigInt(stat.st_atim.tv_sec) * 1000_000_000n + BigInt(stat.st_atim.tv_nsec), 49 | mtimNs: BigInt(stat.st_mtim.tv_sec) * 1000_000_000n + BigInt(stat.st_mtim.tv_nsec), 50 | ctimNs: BigInt(stat.st_ctim.tv_sec) * 1000_000_000n + BigInt(stat.st_ctim.tv_nsec), 51 | birthtimNs: BigInt(stat.st_birthtim.tv_sec) * 1000_000_000n + BigInt(stat.st_birthtim.tv_nsec) 52 | }) 53 | } 54 | 55 | return new this({ 56 | dev: Number(stat.st_dev), 57 | ino: Number(stat.st_ino), 58 | mode: Number(stat.st_mode), 59 | nlink: Number(stat.st_nlink), 60 | uid: Number(stat.st_uid), 61 | gid: Number(stat.st_gid), 62 | rdev: Number(stat.st_rdev), 63 | size: Number(stat.st_size), 64 | blksize: Number(stat.st_blksize), 65 | blocks: Number(stat.st_blocks), 66 | atimeMs: Number(stat.st_atim.tv_sec) * 1000 + Number(stat.st_atim.tv_nsec) / 1000_000, 67 | mtimeMs: Number(stat.st_mtim.tv_sec) * 1000 + Number(stat.st_mtim.tv_nsec) / 1000_000, 68 | ctimeMs: Number(stat.st_ctim.tv_sec) * 1000 + Number(stat.st_ctim.tv_nsec) / 1000_000, 69 | birthtimeMs: Number(stat.st_birthtim.tv_sec) * 1000 + Number(stat.st_birthtim.tv_nsec) / 1000_000 70 | }) 71 | } 72 | 73 | /** 74 | * `Stats` class constructor. 75 | * @param {object} stat 76 | */ 77 | constructor (stat) { 78 | this.dev = stat.dev 79 | this.ino = stat.ino 80 | this.mode = stat.mode 81 | this.nlink = stat.nlink 82 | this.uid = stat.uid 83 | this.gid = stat.gid 84 | this.rdev = stat.rdev 85 | this.size = stat.size 86 | this.blksize = stat.blksize 87 | this.blocks = stat.blocks 88 | this.atimeMs = stat.atimeMs 89 | this.mtimeMs = stat.mtimeMs 90 | this.ctimeMs = stat.ctimeMs 91 | this.birthtimeMs = stat.birthtimeMs 92 | 93 | this.atime = new Date(this.atimeMs) 94 | this.mtime = new Date(this.mtimeMs) 95 | this.ctime = new Date(this.ctimeMs) 96 | this.birthtime = new Date(this.birthtimeMs) 97 | 98 | Object.defineProperty(this, 'handle', { 99 | configurable: true, 100 | enumerable: false, 101 | writable: true, 102 | value: null 103 | }) 104 | } 105 | 106 | isDirectory () { 107 | return checkMode(this.mode, constants.S_IFDIR) 108 | } 109 | 110 | isFile () { 111 | return checkMode(this.mode, constants.S_IFREG) 112 | } 113 | 114 | isBlockDevice () { 115 | return checkMode(this.mode, constants.S_IFBLK) 116 | } 117 | 118 | isCharacterDevice () { 119 | return checkMode(this.mode, constants.S_IFCHR) 120 | } 121 | 122 | isSymbolicLink () { 123 | return checkMode(this.mode, constants.S_IFLNK) 124 | } 125 | 126 | isFIFO () { 127 | return checkMode(this.mode, constants.S_IFIFO) 128 | } 129 | 130 | isSocket () { 131 | return checkMode(this.mode, constants.S_IFSOCK) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /fs/stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module FS.Stream 3 | */ 4 | import { Readable, Writable } from '../stream.js' 5 | import { AbortError } from '../errors.js' 6 | import { Buffer } from '../buffer.js' 7 | 8 | export const DEFAULT_STREAM_HIGH_WATER_MARK = 64 * 1024 9 | 10 | /** 11 | * A `Readable` stream for a `FileHandle`. 12 | */ 13 | export class ReadStream extends Readable { 14 | /** 15 | * `ReadStream` class constructor 16 | * @private 17 | */ 18 | constructor (options) { 19 | super(options) 20 | 21 | if (options?.signal?.aborted) { 22 | throw new AbortError(options.signal) 23 | } 24 | 25 | if (typeof options?.highWaterMark !== 'number') { 26 | this._readableState.highWaterMark = this.constructor.highWaterMark 27 | } 28 | 29 | this.end = typeof options?.end === 'number' ? options.end : Infinity 30 | this.start = typeof options?.start === 'number' ? options.start : 0 31 | this.handle = null 32 | this.signal = options?.signal 33 | this.timeout = options?.timeout || undefined 34 | this.bytesRead = 0 35 | this.shouldEmitClose = options?.emitClose !== false 36 | 37 | if (this.start < 0) { 38 | this.start = 0 39 | } 40 | 41 | if (this.end < 0) { 42 | this.end = Infinity 43 | } 44 | 45 | if (options?.handle) { 46 | this.setHandle(options.handle) 47 | } 48 | } 49 | 50 | /** 51 | * Sets file handle for the ReadStream. 52 | * @param {FileHandle} handle 53 | */ 54 | setHandle (handle) { 55 | setHandle(this, handle) 56 | } 57 | 58 | /** 59 | * The max buffer size for the ReadStream. 60 | */ 61 | get highWaterMark () { 62 | return this._readableState.highWaterMark 63 | } 64 | 65 | /** 66 | * Relative or absolute path of the underlying `FileHandle`. 67 | */ 68 | get path () { 69 | return this.handle?.path || null 70 | } 71 | 72 | /** 73 | * `true` if the stream is in a pending state. 74 | */ 75 | get pending () { 76 | return this.handle?.opened !== true 77 | } 78 | 79 | /** 80 | * Handles `shouldEmitClose` setting from `options.emitClose` in constructor. 81 | * @protected 82 | */ 83 | emit (event, ...args) { 84 | if (event === 'close' && this.shouldEmitClose === false) { 85 | return false 86 | } 87 | 88 | return super.emit(event, ...args) 89 | } 90 | 91 | async _open (callback) { 92 | const { signal, handle, timeout } = this 93 | 94 | if (!handle) { 95 | return callback(new Error('Handle not set in ReadStream')) 96 | } 97 | 98 | if (signal?.aborted) { 99 | return callback(new AbortError(signal)) 100 | } 101 | 102 | if (handle?.opened) { 103 | return callback(null) 104 | } 105 | 106 | this.once('ready', () => callback(null)) 107 | 108 | // open if not opening already 109 | if (!handle.opening) { 110 | try { 111 | await handle.open({ signal, timeout }) 112 | } catch (err) { 113 | return callback(err) 114 | } 115 | } 116 | } 117 | 118 | async _read (callback) { 119 | const { signal, handle, timeout } = this 120 | 121 | if (!handle || !handle.opened) { 122 | return callback(new Error('File handle not opened')) 123 | } 124 | 125 | if (this.signal?.aborted) { 126 | return callback(new AbortError(this.signal)) 127 | } 128 | 129 | const position = Math.max(0, this.start) + this.bytesRead 130 | const buffer = Buffer.alloc(this.highWaterMark) 131 | const length = Math.max(0, this.end) < Infinity 132 | ? Math.min(this.end - position, buffer.length) 133 | : buffer.length 134 | 135 | let result = null 136 | 137 | try { 138 | result = await handle.read(buffer, 0, length, position, { 139 | timeout, 140 | signal 141 | }) 142 | } catch (err) { 143 | return callback(err) 144 | } 145 | 146 | if (typeof result.bytesRead === 'number' && result.bytesRead > 0) { 147 | this.bytesRead += result.bytesRead 148 | this.push(buffer.slice(0, result.bytesRead)) 149 | 150 | if (this.bytesRead >= this.end) { 151 | this.push(null) 152 | } 153 | } else { 154 | this.push(null) 155 | } 156 | 157 | callback(null) 158 | } 159 | } 160 | 161 | /** 162 | * A `Writable` stream for a `FileHandle`. 163 | */ 164 | export class WriteStream extends Writable { 165 | /** 166 | * `WriteStream` class constructor 167 | * @private 168 | */ 169 | constructor (options) { 170 | super(options) 171 | 172 | if (typeof options?.highWaterMark !== 'number') { 173 | this._writableState.highWaterMark = this.constructor.highWaterMark 174 | } 175 | 176 | this.start = typeof options?.start === 'number' ? options.start : 0 177 | this.handle = null 178 | this.signal = options?.signal 179 | this.timeout = options?.timeout || undefined 180 | this.bytesWritten = 0 181 | this.shouldEmitClose = options?.emitClose !== false 182 | 183 | if (this.start < 0) { 184 | this.start = 0 185 | } 186 | 187 | if (options?.handle) { 188 | this.setHandle(options.handle) 189 | } 190 | } 191 | 192 | /** 193 | * Sets file handle for the WriteStream. 194 | * @param {FileHandle} handle 195 | */ 196 | setHandle (handle) { 197 | setHandle(this, handle) 198 | } 199 | 200 | /** 201 | * The max buffer size for the Writetream. 202 | */ 203 | get highWaterMark () { 204 | return this._writableState.highWaterMark 205 | } 206 | 207 | /** 208 | * Relative or absolute path of the underlying `FileHandle`. 209 | */ 210 | get path () { 211 | return this.handle?.path || null 212 | } 213 | 214 | /** 215 | * `true` if the stream is in a pending state. 216 | */ 217 | get pending () { 218 | return this.handle?.opened !== true 219 | } 220 | 221 | async _open (callback) { 222 | const { signal, handle, timeout } = this 223 | 224 | if (!handle) { 225 | return callback(new Error('Handle not set in WriteStream')) 226 | } 227 | 228 | if (signal?.aborted) { 229 | return callback(new AbortError(signal)) 230 | } 231 | 232 | if (handle?.opened) { 233 | return callback(null) 234 | } 235 | 236 | this.once('ready', () => callback(null)) 237 | 238 | // open if not opening already 239 | if (!handle.opening) { 240 | try { 241 | await handle.open({ signal, timeout }) 242 | } catch (err) { 243 | return callback(err) 244 | } 245 | } 246 | } 247 | 248 | /** 249 | * Handles `shouldEmitClose` setting from `options.emitClose` in constructor. 250 | * @protected 251 | */ 252 | emit (event, ...args) { 253 | if (event === 'close' && this.shouldEmitClose === false) { 254 | return false 255 | } 256 | 257 | return super.emit(event, ...args) 258 | } 259 | 260 | async _write (buffer, callback) { 261 | const { signal, handle, timeout } = this 262 | 263 | if (!handle || !handle.opened) { 264 | return callback(new Error('File handle not opened')) 265 | } 266 | 267 | const position = this.start + this.bytesWritten 268 | let result = null 269 | 270 | if (!buffer.length) { 271 | return callback(null) 272 | } 273 | 274 | try { 275 | result = await handle.write(buffer, 0, buffer.length, position, { 276 | timeout, 277 | signal 278 | }) 279 | } catch (err) { 280 | return callback(err) 281 | } 282 | 283 | if (typeof result.bytesWritten === 'number' && result.bytesWritten > 0) { 284 | this.bytesWritten += result.bytesWritten 285 | 286 | if (result.bytesWritten !== buffer.length) { 287 | return await this._write(buffer.slice(result.bytesWritten), callback) 288 | } 289 | } 290 | 291 | callback(null) 292 | } 293 | } 294 | 295 | function setHandle (stream, handle) { 296 | if (!handle) return 297 | 298 | if (stream.handle) { 299 | throw new Error('Stream handle already set.') 300 | } 301 | 302 | stream.handle = handle 303 | 304 | if (handle.opened) { 305 | queueMicrotask(() => stream.emit('ready')) 306 | } else { 307 | handle.once('open', (fd) => { 308 | if (stream.handle === handle) { 309 | stream.emit('open', fd) 310 | stream.emit('ready') 311 | } 312 | }) 313 | } 314 | 315 | stream.once('ready', () => { 316 | handle.once('close', () => { 317 | stream.emit('close') 318 | }) 319 | }) 320 | } 321 | 322 | ReadStream.highWaterMark = DEFAULT_STREAM_HIGH_WATER_MARK 323 | WriteStream.highWaterMark = DEFAULT_STREAM_HIGH_WATER_MARK 324 | 325 | export const FileReadStream = ReadStream 326 | export const FileWriteStream = WriteStream 327 | -------------------------------------------------------------------------------- /gc.js: -------------------------------------------------------------------------------- 1 | import { FinalizationRegistryCallbackError } from './errors.js' 2 | import console from './console.js' 3 | import { noop } from './util.js' 4 | 5 | if (typeof FinalizationRegistry === 'undefined') { 6 | console.warn( 7 | 'FinalizationRegistry is not implemented in this environment. ' + 8 | 'gc.ref() will have no effect.' 9 | ) 10 | } 11 | 12 | export const finalizers = new WeakMap() 13 | export const kFinalizer = Symbol.for('gc.finalizer') 14 | export const finalizer = kFinalizer 15 | export const pool = new Set() 16 | 17 | /** 18 | * Static registry for objects to clean up underlying resources when they 19 | * are gc'd by the environment. There is no guarantee that the `finalizer()` 20 | * is called at any time. 21 | */ 22 | export const registry = new FinalizationRegistry(finalizationRegistryCallback) 23 | 24 | /** 25 | * Default exports which also acts a retained value to persist bound 26 | * `Finalizer#handle()` functions from being gc'd before the 27 | * `FinalizationRegistry` callback is called because `heldValue` must be 28 | * strongly held (retained) in order for the callback to be called. 29 | */ 30 | export const gc = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors({ 31 | ref, 32 | pool, 33 | unref, 34 | retain, 35 | release, 36 | registry, 37 | finalize, 38 | finalizer, 39 | finalizers, 40 | 41 | get refs () { return pool.size } 42 | }))) 43 | 44 | // `gc` is also the default export 45 | export default gc 46 | 47 | /** 48 | * Internal `FinalizationRegistry` callback. 49 | * @private 50 | * @param {Finalizer} finalizer 51 | */ 52 | async function finalizationRegistryCallback (finalizer) { 53 | if (typeof finalizer.handle === 'function') { 54 | try { 55 | await finalizer.handle(...finalizer.args) 56 | } catch (e) { 57 | const err = new FinalizationRegistryCallbackError(e.message, { 58 | cause: e 59 | }) 60 | 61 | if (typeof Error.captureStackTrace === 'function') { 62 | Error.captureStackTrace(err, finalizationRegistryCallback) 63 | } 64 | 65 | console.warn(err.name, err.message, err.stack, err.cause) 66 | } 67 | } 68 | 69 | for (const weakRef of pool) { 70 | if (weakRef instanceof WeakRef) { 71 | const ref = weakRef.deref() 72 | if (ref && ref !== finalizer) { 73 | continue 74 | } 75 | } 76 | 77 | pool.delete(weakRef) 78 | } 79 | 80 | finalizer = undefined 81 | } 82 | 83 | /** 84 | * A container for strongly (retain) referenced finalizer function 85 | * with arguments weakly referenced to an object that will be 86 | * garbage collected. 87 | */ 88 | export class Finalizer { 89 | /** 90 | * Creates a `Finalizer` from input. 91 | */ 92 | static from (handler) { 93 | if (typeof handler === 'function') { 94 | return new this([], handler) 95 | } 96 | 97 | let { handle, args } = handler 98 | 99 | if (typeof handle !== 'function') { 100 | handle = noop 101 | } 102 | 103 | if (!Array.isArray(args)) { 104 | args = [] 105 | } 106 | 107 | return new this(args, handle) 108 | } 109 | 110 | /** 111 | * `Finalizer` class constructor. 112 | * @private 113 | * @param {array} args 114 | * @param {function} handle 115 | */ 116 | constructor (args, handle) { 117 | this.args = args 118 | this.handle = handle.bind(gc) 119 | } 120 | } 121 | 122 | /** 123 | * Track `object` ref to call `Symbol.for('gc.finalize')` method when 124 | * environment garbage collects object. 125 | * @param {object} object 126 | * @return {boolean} 127 | */ 128 | export async function ref (object, ...args) { 129 | if (object && typeof object[kFinalizer] === 'function') { 130 | const finalizer = Finalizer.from(await object[kFinalizer](...args)) 131 | const weakRef = new WeakRef(finalizer) 132 | 133 | finalizers.set(object, weakRef) 134 | pool.add(weakRef) 135 | 136 | registry.register(object, finalizer, object) 137 | } 138 | 139 | return finalizers.has(object) 140 | } 141 | 142 | /** 143 | * Stop tracking `object` ref to call `Symbol.for('gc.finalize')` method when 144 | * environment garbage collects object. 145 | * @param {object} object 146 | * @return {boolean} 147 | */ 148 | export function unref (object) { 149 | if (!object || typeof object !== 'object') { 150 | return false 151 | } 152 | 153 | if (typeof object[kFinalizer] === 'function' && finalizers.has(object)) { 154 | const weakRef = finalizers.get(object) 155 | 156 | if (weakRef) { 157 | pool.delete(weakRef) 158 | } 159 | 160 | finalizers.delete(object) 161 | registry.unregister(object) 162 | return true 163 | } 164 | 165 | return false 166 | } 167 | 168 | /** 169 | * An alias for `unref()` 170 | * @param {object} object} 171 | * @return {boolean} 172 | */ 173 | export function retain (object) { 174 | return unref(object) 175 | } 176 | 177 | /** 178 | * Call finalize on `object` for `gc.finalizer` implementation. 179 | * @param {object} object] 180 | * @return {Promise} 181 | */ 182 | export async function finalize (object, ...args) { 183 | const finalizer = finalizers.get(object)?.deref() 184 | 185 | registry.unregister(object) 186 | 187 | try { 188 | if (finalizer instanceof Finalizer && await unref(object)) { 189 | await finalizationRegistryCallback(finalizer) 190 | } else { 191 | const finalizer = Finalizer.from(await object[kFinalizer](...args)) 192 | await finalizationRegistryCallback(finalizer) 193 | } 194 | return true 195 | } catch (err) { 196 | return false 197 | } 198 | } 199 | 200 | /** 201 | * Calls all pending finalization handlers forcefully. This function 202 | * may have unintended consequences as objects be considered finalized 203 | * and still strongly held (retained) somewhere. 204 | */ 205 | export async function release () { 206 | for (const weakRef of pool) { 207 | await finalizationRegistryCallback(weakRef?.deref?.()) 208 | pool.delete(weakRef) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * as backend from './backend.js' 2 | export { Bluetooth } from './bluetooth.js' 3 | export { bootstrap } from './bootstrap.js' 4 | export * as buffer from './buffer.js' 5 | export { Buffer } from './buffer.js' 6 | export { default as console } from './console.js' 7 | export * as crypto from './crypto.js' 8 | export * as dgram from './dgram.js' 9 | export * as dns from './dns.js' 10 | export * as errors from './errors.js' 11 | export * as events from './events.js' 12 | export { EventEmitter } from './events.js' 13 | export * as fs from './fs.js' 14 | export { default as gc } from './gc.js' 15 | export * as ipc from './ipc.js' 16 | export * as os from './os.js' 17 | export { default as path } from './path.js' 18 | export { default as process } from './process.js' 19 | export * as runtime from './runtime.js' 20 | export * as stream from './stream.js' 21 | export * as util from './util.js' 22 | 23 | export const Socket = Object.create(Object.prototype, { 24 | constructor: { 25 | value: class Socket {} 26 | } 27 | }) 28 | 29 | export default Socket 30 | 31 | // eslint-disable-next-line 32 | import * as exports from './index.js' 33 | for (const key in exports) { 34 | if (key !== 'default') { 35 | Socket[key] = exports[key] 36 | } 37 | } 38 | 39 | Object.freeze(Socket) 40 | -------------------------------------------------------------------------------- /net.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Net 3 | * 4 | * THIS MODULE HAS BEEN DISABLED! 5 | * 6 | * This module provides an asynchronous network API for creating 7 | * stream-based TCP or IPC servers (net.createServer()) and clients 8 | * (net.createConnection()). 9 | * 10 | */ 11 | 12 | import { EventEmitter } from './events.js' 13 | import { Duplex } from './stream.js' 14 | import { rand64 } from './util.js' 15 | import console from './console.js' 16 | 17 | const assertType = (name, expected, actual, code) => { 18 | const msg = `'${name}' must be a '${expected}', received '${actual}'` 19 | const err = new TypeError(msg) 20 | err.code = code 21 | throw err 22 | } 23 | 24 | // lifted from nodejs/node/ 25 | const normalizedArgsSymbol = Symbol('normalizedArgsSymbol') 26 | const kLastWriteQueueSize = Symbol('lastWriteQueueSize') 27 | 28 | const normalizeArgs = (args) => { 29 | let arr 30 | 31 | if (args.length === 0) { 32 | arr = [{}, null] 33 | arr[normalizedArgsSymbol] = true 34 | return arr 35 | } 36 | 37 | const arg0 = args[0] 38 | let options = {} 39 | 40 | if (typeof arg0 === 'object' && arg0 !== null) { 41 | // (options[...][, cb]) 42 | options = arg0 43 | 44 | // not supported: pipes 45 | // } else if (isPipeName(arg0)) { 46 | // // (path[...][, cb]) 47 | // options.path = arg0 48 | } else { 49 | // ([port][, host][...][, cb]) 50 | options.port = arg0 51 | 52 | if (args.length > 1 && typeof args[1] === 'string') { 53 | options.host = args[1] 54 | } 55 | } 56 | 57 | const cb = args[args.length - 1] 58 | if (typeof cb !== 'function') { 59 | arr = [options, null] 60 | } else { 61 | arr = [options, cb] 62 | } 63 | 64 | arr[normalizedArgsSymbol] = true 65 | return arr 66 | } 67 | 68 | export class Server extends EventEmitter { 69 | constructor (options, handler) { 70 | super() 71 | 72 | if (typeof options === 'undefined') { 73 | options = handler 74 | } 75 | 76 | this._connections = 0 77 | this.id = rand64() 78 | } 79 | 80 | onconnection (data) { 81 | const socket = new Socket(data) 82 | 83 | if (this.maxConnections && this._connections >= this.maxConnections) { 84 | socket.close(data) 85 | return 86 | } 87 | 88 | this._connections++ 89 | socket._server = this 90 | 91 | this.emit('connection', socket) 92 | } 93 | 94 | listen (port, address, cb) { 95 | ;(async opts => { 96 | const { err, data } = await window._ipc.send('tcpCreateServer', opts) 97 | 98 | if (err && !cb) { 99 | this.emit('error', err) 100 | return 101 | } 102 | 103 | this._address = { port: data.port, address: data.address, family: data.family } 104 | this.connections = {} 105 | 106 | window._ipc.streams[opts.id] = this 107 | 108 | if (cb) return cb(null, data) 109 | this.emit('listening', data) 110 | })({ port, address, id: this.id }) 111 | 112 | return this 113 | } 114 | 115 | address () { 116 | return this._address 117 | } 118 | 119 | close (cb) { 120 | const params = { 121 | id: this.id 122 | } 123 | 124 | ;(async () => { 125 | const { err } = await window._ipc.send('tcpClose', params) 126 | delete window._ipc.streams[this.id] 127 | if (err && !cb) this.emit('error', err) 128 | else if (cb) cb(err) 129 | })() 130 | } 131 | 132 | getConnections (cb) { 133 | assertType('Callback', 'function', typeof cb, 'ERR_INVALID_CALLBACK') 134 | const params = { 135 | id: this.id 136 | } 137 | 138 | ;(async () => { 139 | const { 140 | err, 141 | data 142 | } = await window._ipc.send('tcpServerGetConnections', params) 143 | 144 | if (cb) cb(err, data) 145 | })() 146 | } 147 | 148 | unref () { 149 | return this 150 | } 151 | } 152 | 153 | export class Socket extends Duplex { 154 | constructor (options) { 155 | super() 156 | 157 | this._server = null 158 | 159 | this._address = null 160 | this.allowHalfOpen = options.allowHalfOpen === true 161 | this._flowing = false 162 | /* 163 | this.on('end', () => { 164 | if (!this.allowHalfOpen) 165 | this.writable = false 166 | //this.write = this._writeAfterFIN; 167 | }) 168 | */ 169 | } 170 | 171 | // note: this is not an async method on node, so it's not here 172 | // thus the ipc response is not awaited. since _ipc.send is async 173 | // but the messages are handled in order, you do not need to wait 174 | // for it before sending data, noDelay will be set correctly before the 175 | // next data is sent. 176 | setNoDelay (enable) { 177 | const params = { 178 | id: this.id, enable 179 | } 180 | window._ipc.send('tcpSetNoDelay', params) 181 | } 182 | 183 | // note: see note for setNoDelay 184 | setKeepAlive (enabled) { 185 | const params = { 186 | id: this.id, 187 | enabled 188 | } 189 | 190 | window._ipc.send('tcpSetKeepAlive', params) 191 | } 192 | 193 | _onTimeout () { 194 | const handle = this._handle 195 | const lastWriteQueueSize = this[kLastWriteQueueSize] 196 | 197 | if (lastWriteQueueSize > 0 && handle) { 198 | // `lastWriteQueueSize !== writeQueueSize` means there is 199 | // an active write in progress, so we suppress the timeout. 200 | const { writeQueueSize } = handle 201 | 202 | if (lastWriteQueueSize !== writeQueueSize) { 203 | this[kLastWriteQueueSize] = writeQueueSize 204 | this._unrefTimer() 205 | return 206 | } 207 | } 208 | 209 | this.emit('timeout') 210 | } 211 | 212 | address () { 213 | return { ...this._address } 214 | } 215 | 216 | _final (cb) { 217 | if (this.pending) { 218 | return this.once('connect', () => this._final(cb)) 219 | } 220 | 221 | const params = { 222 | id: this.id 223 | } 224 | ;(async () => { 225 | const { err, data } = await window._ipc.send('tcpShutdown', params) 226 | if (cb) cb(err, data) 227 | })() 228 | } 229 | 230 | _destroy (cb) { 231 | if (this.destroyed) return 232 | ;(async () => { 233 | await window._ipc.send('tcpClose', { id: this.id }) 234 | if (this._server) { 235 | this._server._connections-- 236 | 237 | if (this._server._connections === 0) { 238 | this._server.emit('close') 239 | } 240 | } 241 | cb() 242 | })() 243 | } 244 | 245 | destroySoon () { 246 | if (this.writable) this.end() 247 | 248 | if (this.writableFinished) { 249 | this.destroy() 250 | } else { 251 | this.once('finish', this.destroy) 252 | } 253 | } 254 | 255 | _writev (data, cb) { 256 | ;(async () => { 257 | const allBuffers = data.allBuffers 258 | let chunks 259 | 260 | if (allBuffers) { 261 | chunks = data 262 | for (let i = 0; i < data.length; i++) { 263 | data[i] = data[i].chunk 264 | } 265 | } else { 266 | chunks = new Array(data.length << 1) 267 | 268 | for (let i = 0; i < data.length; i++) { 269 | const entry = data[i] 270 | chunks[i * 2] = entry.chunk 271 | chunks[i * 2 + 1] = entry.encoding 272 | } 273 | } 274 | 275 | const requests = [] 276 | 277 | for (const chunk of chunks) { 278 | const params = { 279 | id: this.id, 280 | data: chunk 281 | } 282 | // sent in order so could just await the last one? 283 | requests.push(window._ipc.send('tcpSend', params)) 284 | } 285 | 286 | try { 287 | await Promise.all(requests) 288 | } catch (err) { 289 | this.destroy(err) 290 | cb(err) 291 | return 292 | } 293 | 294 | cb() 295 | })() 296 | } 297 | 298 | _write (data, cb) { 299 | const params = { 300 | id: this.id, 301 | data 302 | } 303 | ;(async () => { 304 | const { err, data } = await window._ipc.send('tcpSend', params) 305 | console.log('_write', err, data) 306 | cb(err) 307 | })() 308 | } 309 | 310 | // 311 | // This is called internally by incoming _ipc message when there is data to insert to the stream. 312 | // 313 | __write (data) { 314 | if (data.length && !this.destroyed) { 315 | if (!this.push(data)) { 316 | const params = { 317 | id: this.id 318 | } 319 | this._flowing = false 320 | window._ipc.send('tcpReadStop', params) 321 | } 322 | } else { 323 | // if this stream is not full duplex, 324 | // then mark as not writable. 325 | if (!this.allowHalfOpen) { 326 | this.destroySoon() 327 | } 328 | this.push(null) 329 | this.read(0) 330 | } 331 | } 332 | 333 | _read (cb) { 334 | if (this._flowing) return cb() 335 | this._flowing = true 336 | 337 | const params = { 338 | id: this.id 339 | } 340 | 341 | ;(async () => { 342 | const { err } = await window._ipc.send('tcpReadStart', params) 343 | 344 | if (err) { 345 | this._destroy() 346 | } else { 347 | cb() 348 | } 349 | })() 350 | } 351 | 352 | pause () { 353 | Duplex.prototype.pause.call(this) 354 | // send a ReadStop but do not wait for a confirmation. 355 | // ipc is async, but it's ordered, 356 | if (this._flowing) { 357 | this._flowing = false 358 | window._ipc.send('tcpReadStop', { id: this.id }) 359 | } 360 | return this 361 | } 362 | 363 | resume () { 364 | Duplex.prototype.resume.call(this) 365 | // send a ReadStop but do not wait for a confirmation. 366 | // ipc is async, but it's ordered, 367 | if (!this._flowing) { 368 | this._flowing = true 369 | window._ipc.send('tcpReadStart', { id: this.id }) 370 | } 371 | return this 372 | } 373 | 374 | connect (...args) { 375 | const [options, cb] = normalizeArgs(args) 376 | 377 | ;(async () => { 378 | this.id = rand64() 379 | const params = { 380 | port: options.port, 381 | address: options.host, 382 | id: this.id 383 | } 384 | 385 | // TODO: if host is a ip address 386 | // connect, if it is a dns name, lookup 387 | 388 | const { err, data } = await window._ipc.send('tcpConnect', params) 389 | 390 | if (err) { 391 | if (cb) cb(err) 392 | else this.emit('error', err) 393 | return 394 | } 395 | this.remotePort = data.port 396 | this.remoteAddress = data.address 397 | // this.port = port 398 | // this.address = address 399 | 400 | window._ipc.streams[this.id] = this 401 | 402 | if (cb) cb(null, this) 403 | })() 404 | return this 405 | } 406 | 407 | unref () { 408 | return this // for compatibility with the net module 409 | } 410 | } 411 | 412 | export const connect = (...args) => { 413 | const [options, callback] = normalizeArgs(args) 414 | 415 | // supported by node but not here: localAddress, localHost, hints, lookup 416 | 417 | const socket = new Socket(options) 418 | socket.connect(options, callback) 419 | 420 | return socket 421 | } 422 | 423 | export const createServer = (...args) => { 424 | return new Server(...args) 425 | } 426 | 427 | export const getNetworkInterfaces = o => window._ipc.send('os.networkInterfaces', o) 428 | 429 | const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' 430 | const v4Str = `(${v4Seg}[.]){3}${v4Seg}` 431 | const IPv4Reg = new RegExp(`^${v4Str}$`) 432 | 433 | export const isIPv4 = s => { 434 | return IPv4Reg.test(s) 435 | } 436 | -------------------------------------------------------------------------------- /os.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module OS 3 | * 4 | * This module provides normalized system information from all the major 5 | * operating systems. 6 | */ 7 | 8 | import { toProperCase } from './util.js' 9 | import process from './process.js' 10 | import ipc from './ipc.js' 11 | 12 | const UNKNOWN = 'unknown' 13 | 14 | const cache = { 15 | arch: UNKNOWN, 16 | type: UNKNOWN, 17 | platform: UNKNOWN 18 | } 19 | 20 | export function arch () { 21 | let value = UNKNOWN 22 | 23 | if (cache.arch !== UNKNOWN) { 24 | return cache.arch 25 | } 26 | 27 | if (typeof window !== 'object') { 28 | if (typeof process?.arch === 'string') { 29 | return process.arch 30 | } 31 | } 32 | 33 | if (typeof window === 'object') { 34 | value = ( 35 | process.arch || 36 | ipc.sendSync('os.arch')?.data || 37 | UNKNOWN 38 | ) 39 | } 40 | 41 | if (value === 'arm64') { 42 | return value 43 | } 44 | 45 | cache.arch = value 46 | .replace('x86_64', 'x64') 47 | .replace('x86', 'ia32') 48 | .replace(/arm.*/, 'arm') 49 | 50 | return cache.arch 51 | } 52 | 53 | export function networkInterfaces () { 54 | const now = Date.now() 55 | 56 | if (cache.networkInterfaces && cache.networkInterfacesTTL > now) { 57 | return cache.networkInterfaces 58 | } 59 | 60 | const interfaces = {} 61 | 62 | const result = ipc.sendSync('os.networkInterfaces') 63 | if (!result.data) return interfaces 64 | 65 | const { ipv4, ipv6 } = result.data 66 | 67 | for (const type in ipv4) { 68 | const info = typeof ipv4[type] === 'string' 69 | ? { address: ipv4[type] } 70 | : ipv4[type] 71 | 72 | const { address } = info 73 | const family = 'IPv4' 74 | 75 | let internal = info.internal || false 76 | let netmask = info.netmask || '255.255.255.0' 77 | let cidr = `${address}/24` 78 | let mac = info.mac || null 79 | 80 | if (address === '127.0.0.1' || address === '0.0.0.0') { 81 | internal = true 82 | mac = '00:00:00:00:00:00' 83 | 84 | if (address === '127.0.0.1') { 85 | cidr = '127.0.0.1/8' 86 | netmask = '255.0.0.0' 87 | } else { 88 | cidr = '0.0.0.0/0' 89 | netmask = '0.0.0.0' 90 | } 91 | } 92 | 93 | interfaces[type] = interfaces[type] || [] 94 | interfaces[type].push({ 95 | address, 96 | netmask, 97 | internal, 98 | family, 99 | cidr, 100 | mac 101 | }) 102 | } 103 | 104 | for (const type in ipv6) { 105 | const info = typeof ipv6[type] === 'string' 106 | ? { address: ipv6[type] } 107 | : ipv6[type] 108 | 109 | const { address } = info 110 | const family = 'IPv6' 111 | 112 | let internal = info.internal || false 113 | let netmask = internal.netmask || 'ffff:ffff:ffff:ffff::' 114 | let cidr = `${address}/64` 115 | let mac = info.mac || null 116 | 117 | if (address === '::1') { 118 | internal = true 119 | netmask = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' 120 | cidr = '::1/128' 121 | mac = '00:00:00:00:00:00' 122 | } 123 | 124 | interfaces[type] = interfaces[type] || [] 125 | interfaces[type].push({ 126 | address, 127 | netmask, 128 | internal, 129 | family, 130 | cidr, 131 | mac 132 | }) 133 | } 134 | 135 | cache.networkInterfaces = interfaces 136 | cache.networkInterfacesTTL = Date.now() + 512 137 | 138 | return interfaces 139 | } 140 | 141 | export function platform () { 142 | let value = UNKNOWN 143 | 144 | if (cache.platform !== UNKNOWN) { 145 | return cache.platform 146 | } 147 | 148 | if (typeof window !== 'object') { 149 | if (typeof process?.platform === 'string') { 150 | return process.platform.toLowerCase() 151 | } 152 | } 153 | 154 | if (typeof window === 'object') { 155 | value = ( 156 | process.os || 157 | ipc.sendSync('os.platform')?.data || 158 | platform?.platform || 159 | UNKNOWN 160 | ) 161 | } 162 | 163 | cache.platform = value 164 | .replace(/^mac/i, 'darwin') 165 | .toLowerCase() 166 | 167 | return cache.platform 168 | } 169 | 170 | export function type () { 171 | let value = 'unknown' 172 | 173 | if (cache.type !== UNKNOWN) { 174 | return cache.type 175 | } 176 | 177 | if (typeof window !== 'object') { 178 | switch (platform()) { 179 | case 'android': return 'Linux' 180 | case 'cygwin': return 'CYGWIN_NT' 181 | case 'freebsd': return 'FreeBSD' 182 | case 'linux': return 'Linux' 183 | case 'openbsd': return 'OpenBSD' 184 | case 'win32': return 'Windows_NT' 185 | 186 | case 'ios': case 'mac': case 'Mac': case 'darwin': return 'Darwin' 187 | } 188 | } 189 | 190 | if (typeof window === 'object') { 191 | value = ( 192 | platform?.platform || 193 | ipc.sendSync('os.type')?.data || 194 | UNKNOWN 195 | ) 196 | } 197 | 198 | value = value.replace(/android/i, 'Linux') 199 | value = value.replace(/ios/i, 'Darwin') 200 | 201 | if (value !== UNKNOWN) { 202 | value = toProperCase(value) 203 | } 204 | 205 | cache.type = value 206 | 207 | return cache.type 208 | } 209 | 210 | export function isWindows () { 211 | if ('isWindows' in cache) { 212 | return cache.isWindows 213 | } 214 | 215 | cache.isWindows = /^win.*/i.test(type()) 216 | return cache.isWindows 217 | } 218 | 219 | export function tmpdir () { 220 | let path = '' 221 | 222 | if (isWindows()) { 223 | path = ( 224 | process?.env?.TEMPDIR || 225 | process?.env?.TMPDIR || 226 | process?.env?.TEMP || 227 | process?.env?.TMP || 228 | (process?.env?.SystemRoot || process?.env?.windir || '') + '\\temp' 229 | ) 230 | 231 | if (path.length > 1 && path.endsWith('\\') && !path.endsWith(':\\')) { 232 | path = path.slice(0, -1) 233 | } 234 | } else { 235 | path = ( 236 | process?.env?.TEMPDIR || 237 | process?.env?.TMPDIR || 238 | process?.env?.TEMP || 239 | process?.env?.TMP || 240 | '' 241 | ) 242 | 243 | // derive default 244 | if (!path) { 245 | if (platform() === 'ios') { 246 | // @TODO(jwerle): use a path module 247 | path = [process.cwd(), 'tmp'].join('/') 248 | } else if (platform() === 'android') { 249 | path = '/data/local/tmp' 250 | } else { 251 | path = '/tmp' 252 | } 253 | } 254 | 255 | if (path.length > 1 && path.endsWith('/')) { 256 | path = path.slice(0, -1) 257 | } 258 | } 259 | 260 | return path 261 | } 262 | 263 | export const EOL = (() => { 264 | if (isWindows()) { 265 | return '\r\n' 266 | } 267 | 268 | return '\n' 269 | })() 270 | 271 | // eslint-disable-next-line 272 | import * as exports from './os.js' 273 | export default exports 274 | -------------------------------------------------------------------------------- /p2p.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module P2P 3 | * 4 | * A low-level P2P module for networking that allows you to discover peers, 5 | * connect to peers, and send packets reliably. 6 | * 7 | * @see {@link https://github.com/socketsupply/stream-relay} 8 | * 9 | */ 10 | import { createPeer } from '@socketsupply/stream-relay' 11 | import dgram from './dgram.js' 12 | 13 | export * from '@socketsupply/stream-relay' 14 | const Peer = createPeer(dgram) 15 | export { Peer } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@socketsupply/socket-api", 3 | "version": "0.2.29", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "pretest": "standard .", 8 | "readme": "node ./bin/generate-docs.js", 9 | "test": "cd test && npm install --silent --no-audit && npm test", 10 | "test:node": "node ./test/node/index.js", 11 | "test:ios-simulator": "cd test && npm install --silent --no-audit && npm run test:ios-simulator", 12 | "test:android": "cd test && npm install --silent --no-audit && npm run test:android", 13 | "test:android-emulator": "cd test && npm install --silent --no-audit && npm run test:android-emulator", 14 | "test:clean": "cd test && rm -rf dist", 15 | "postpublish": "npm publish --registry https://npm.pkg.github.com" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/socketsupply/socket-api.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/socketsupply/socket-api/issues" 25 | }, 26 | "homepage": "https://github.com/socketsupply/socket-api#readme", 27 | "description": "Socket Runtime JavaScript interface", 28 | "devDependencies": { 29 | "acorn": "8.8.0", 30 | "acorn-walk": "8.2.0", 31 | "esbuild": "^0.16.16", 32 | "standard": "^17.0.0" 33 | }, 34 | "optionalDependencies": { 35 | "@socketsupply/stream-relay": "^1.0.22" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /path.js: -------------------------------------------------------------------------------- 1 | import { posix, win32 } from './path/index.js' 2 | import os from './os.js' 3 | 4 | export * from './path/index.js' 5 | 6 | export default class Path extends (os.platform() === 'win32' ? win32 : posix) { 7 | static get win32 () { return win32 } 8 | static get posix () { return posix } 9 | } 10 | -------------------------------------------------------------------------------- /path/index.js: -------------------------------------------------------------------------------- 1 | import posix from './posix.js' 2 | import win32 from './win32.js' 3 | 4 | export { 5 | posix, 6 | win32 7 | } 8 | -------------------------------------------------------------------------------- /path/path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Path 3 | */ 4 | import process from '../process.js' 5 | import os from '../os.js' 6 | 7 | const isWin32 = os.platform() === 'win32' 8 | 9 | export class Path { 10 | /** 11 | * Computes current working directory for a path 12 | * @param {object=} [opts] 13 | * @param {boolean=} [opts.posix] Set to `true` to force POSIX style path 14 | */ 15 | static cwd (opts) { 16 | if (isWin32 && opts?.posix === true) { 17 | const cwd = process.cwd().replace(/\\/g, '/') 18 | return cwd.slice(cwd.indexOf('/')) 19 | } 20 | 21 | return process.cwd() 22 | } 23 | 24 | static from (input) { 25 | if (typeof input === 'string') { 26 | return new this(this.parse(input)) 27 | } else if (input && typeof input === 'object') { 28 | return new this(input) 29 | } 30 | 31 | throw new TypeError('Invalid input given to `Path.from()`') 32 | } 33 | 34 | /** 35 | * `Path` class constructor. 36 | * @protected 37 | * @param {object=} [opts] 38 | * @param {string=} [opts.root] 39 | * @param {string=} [opts.base] 40 | * @param {string=} [opts.name] 41 | * @param {string=} [opts.dir] 42 | * @param {string=} [opts.ext] 43 | */ 44 | constructor (opts) { 45 | this.root = opts?.root ?? '' 46 | this.base = opts?.base ?? '' 47 | this.name = opts?.name ?? '' 48 | this.dir = opts?.dir ?? '' 49 | this.ext = opts?.ext ?? '' 50 | } 51 | 52 | toString () { 53 | const { format } = this.constructor 54 | return format(this) 55 | } 56 | 57 | /** 58 | * @TODO 59 | */ 60 | static resolve (...args) { 61 | } 62 | 63 | /** 64 | * @TODO 65 | */ 66 | static normalize (path) { 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /path/posix.js: -------------------------------------------------------------------------------- 1 | import { Path } from './path.js' 2 | 3 | export default class POSIXPath extends Path { 4 | static get sep () { return '/' } 5 | static get delimiter () { return ':' } 6 | } 7 | -------------------------------------------------------------------------------- /path/win32.js: -------------------------------------------------------------------------------- 1 | import { Path } from './path.js' 2 | 3 | export default class Win32Path extends Path { 4 | static get sep () { return '\\' } 5 | static get delimiter () { return ';' } 6 | } 7 | -------------------------------------------------------------------------------- /polyfills.js: -------------------------------------------------------------------------------- 1 | /* global MutationObserver */ 2 | import console from './console.js' 3 | import ipc from './ipc.js' 4 | 5 | export function applyPolyfills (window) { 6 | Object.defineProperties(window, Object.getOwnPropertyDescriptors({ 7 | resizeTo (width, height) { 8 | const index = window.__args.index 9 | const o = new URLSearchParams({ index, width, height }).toString() 10 | return ipc.postMessage(`ipc://window.setSize?${o}`) 11 | }, 12 | 13 | // TODO(@heapwolf) the properties do not yet conform to the MDN spec 14 | async showOpenFilePicker (o) { 15 | console.warn('window.showOpenFilePicker may not conform to the standard') 16 | const { data } = await ipc.send('dialog', { type: 'open', ...o }) 17 | return typeof data === 'string' ? data.split('\n') : [] 18 | }, 19 | 20 | // TODO(@heapwolf) the properties do not yet conform to the MDN spec 21 | async showSaveFilePicker (o) { 22 | console.warn('window.showSaveFilePicker may not conform to the standard') 23 | const { data } = await ipc.send('dialog', { type: 'save', ...o }) 24 | return typeof data === 'string' ? data.split('\n') : [] 25 | }, 26 | 27 | // TODO(@heapwolf) the properties do not yet conform to the MDN spec 28 | async showDirectoryFilePicker (o) { 29 | console.warn('window.showDirectoryFilePicker may not conform to the standard') 30 | const { data } = await ipc.send('dialog', { allowDirs: true, ...o }) 31 | return typeof data === 'string' ? data.split('\n') : [] 32 | } 33 | })) 34 | 35 | // create tag in document if it doesn't exist 36 | window.document.title ||= '' 37 | // initial value 38 | window.addEventListener('DOMContentLoaded', async () => { 39 | const title = window.document.title 40 | if (title.length !== 0) { 41 | const index = window.__args.index 42 | const o = new URLSearchParams({ value: title, index }).toString() 43 | ipc.postMessage(`ipc://window.setTitle?${o}`) 44 | } 45 | }) 46 | 47 | // 48 | // window.document is uncofigurable property so we need to use MutationObserver here 49 | // 50 | const observer = new MutationObserver((mutationList) => { 51 | for (const mutation of mutationList) { 52 | if (mutation.type === 'childList') { 53 | const index = window.__args.index 54 | const title = mutation.addedNodes[0].textContent 55 | const o = new URLSearchParams({ value: title, index }).toString() 56 | ipc.postMessage(`ipc://window.setTitle?${o}`) 57 | } 58 | } 59 | }) 60 | 61 | const titleElement = document.querySelector('head > title') 62 | if (titleElement) { 63 | observer.observe(titleElement, { childList: true }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /process.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Process 3 | */ 4 | import { EventEmitter } from './events.js' 5 | import { send } from './ipc.js' 6 | 7 | let didEmitExitEvent = false 8 | 9 | const isNode = Boolean(globalThis.process?.versions?.node) 10 | const process = isNode 11 | ? globalThis.process 12 | : Object.create(globalThis.__args, Object.getOwnPropertyDescriptors({ 13 | ...EventEmitter.prototype, 14 | homedir, 15 | argv0: globalThis.__args?.argv?.[0] ?? null, 16 | exit, 17 | env: {}, 18 | platform: globalThis?.__args?.os ?? '', 19 | ...globalThis.__args 20 | })) 21 | 22 | if (!isNode) { 23 | EventEmitter.call(process) 24 | } 25 | 26 | export default process 27 | 28 | /** 29 | * @returns {string} The home directory of the current user. 30 | */ 31 | export function homedir () { 32 | return process.env.HOME ?? '' 33 | } 34 | 35 | /** 36 | * @param {number=} [code=0] - The exit code. Default: 0. 37 | */ 38 | export function exit (code) { 39 | if (!didEmitExitEvent) { 40 | didEmitExitEvent = true 41 | queueMicrotask(() => process.emit('exit', code)) 42 | send('exit', { value: code || 0 }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Runtime 3 | * 4 | * Provides runtime-specific methods 5 | */ 6 | 7 | import { applyPolyfills } from './polyfills.js' 8 | import ipc from './ipc.js' 9 | 10 | export const currentWindow = globalThis?.window?.__args?.index ?? 0 11 | // eslint-disable-next-line 12 | export const debug = globalThis?.window?.__args?.debug ?? false 13 | 14 | export const config = Object.freeze(globalThis?.window?.__args?.config ?? {}) 15 | 16 | function formatFileUrl (url) { 17 | return `file://${globalThis?.window?.__args?.cwd()}/${url}` 18 | } 19 | 20 | if (globalThis.window) { 21 | applyPolyfills(globalThis.window) 22 | } 23 | 24 | export async function send (o) { 25 | o.index = currentWindow 26 | o.window ??= -1 27 | 28 | if (typeof o.value !== 'string') { 29 | o.value = JSON.stringify(o.value) 30 | } 31 | 32 | return await ipc.send('send', { 33 | index: o.index, 34 | window: o.window, 35 | event: encodeURIComponent(o.event), 36 | value: encodeURIComponent(o.value) 37 | }) 38 | } 39 | 40 | export async function getWindows (options = {}) { 41 | return await ipc.send('getWindows', options) 42 | } 43 | 44 | export async function openExternal (options) { 45 | return await ipc.postMessage(`ipc://external?value=${encodeURIComponent(options)}`) 46 | } 47 | 48 | /** 49 | * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. 50 | * @param {object} options - an options object 51 | * @return {Promise<Any>} 52 | */ 53 | export async function exit (o) { 54 | return await ipc.send('exit', o) 55 | } 56 | 57 | /** 58 | * Sets the title of the window (if applicable). 59 | * @param {obnject} options - an options object 60 | * @return {Promise<ipc.Result>} 61 | */ 62 | export async function setTitle (o) { 63 | return await ipc.send('window.setTitle', o) 64 | } 65 | 66 | export async function inspect (o) { 67 | return await ipc.postMessage('ipc://inspect') 68 | } 69 | 70 | inspect[Symbol.for('socket.util.inspect.ignore')] = true 71 | 72 | /** 73 | * @param {object} opts - an options object 74 | * @return {Promise<ipc.Result>} 75 | */ 76 | export async function show (opts = {}) { 77 | opts.index = currentWindow 78 | opts.window ??= currentWindow 79 | if (opts.url) { 80 | opts.url = formatFileUrl(opts.url) 81 | } 82 | return await ipc.send('window.show', opts) 83 | } 84 | 85 | /** 86 | * @param {object} opts - an options object 87 | * @return {Promise<ipc.Result>} 88 | */ 89 | export async function hide (opts = {}) { 90 | opts.index = currentWindow 91 | return await ipc.send('window.hide', opts) 92 | } 93 | 94 | /** 95 | * @param {object} opts - an options object 96 | * @param {number} [opts.window = currentWindow] - the index of the window 97 | * @param {number} opts.url - the path to the HTML file to load into the window 98 | * @return {Promise<ipc.Result>} 99 | */ 100 | export async function navigate (opts = {}) { 101 | opts.index = currentWindow 102 | opts.window ??= currentWindow 103 | if (opts.url) { 104 | opts.url = formatFileUrl(opts.url) 105 | } 106 | return await ipc.send('window.navigate', opts) 107 | } 108 | 109 | export async function setWindowBackgroundColor (opts) { 110 | opts.index = currentWindow 111 | const o = new URLSearchParams(opts).toString() 112 | await ipc.postMessage(`ipc://window.setBackgroundColor?${o}`) 113 | } 114 | 115 | /** 116 | * Opens a native context menu. 117 | * @param {object} options - an options object 118 | * @return {Promise<Any>} 119 | */ 120 | export async function setContextMenu (o) { 121 | o = Object 122 | .entries(o) 123 | .flatMap(a => a.join(':')) 124 | .join('_') 125 | return await ipc.send('context', o) 126 | } 127 | 128 | export async function setSystemMenuItemEnabled (value) { 129 | return await ipc.send('menuItemEnabled', value) 130 | } 131 | 132 | /** 133 | * Set the native menu for the app. 134 | * 135 | * @param {object} options - an options object 136 | * @param {string} options.value - the menu layout 137 | * @param {number} options.index - the window to target (if applicable) 138 | * @return {Promise<Any>} 139 | * 140 | * Socket Runtime provides a minimalist DSL that makes it easy to create 141 | * cross platform native system and context menus. 142 | * 143 | * Menus are created at run time. They can be created from either the Main or 144 | * Render process. The can be recreated instantly by calling the `setSystemMenu` method. 145 | * 146 | * The method takes a string. Here's an example of a menu. The semi colon is 147 | * significant indicates the end of the menu. Use an underscore when there is no 148 | * accelerator key. Modifiers are optional. And well known OS menu options like 149 | * the edit menu will automatically get accelerators you dont need to specify them. 150 | * 151 | * 152 | * ```js 153 | * socket.runtime.setSystemMenu({ index: 0, value: ` 154 | * App: 155 | * Foo: f; 156 | * 157 | * Edit: 158 | * Cut: x 159 | * Copy: c 160 | * Paste: v 161 | * Delete: _ 162 | * Select All: a; 163 | * 164 | * Other: 165 | * Apple: _ 166 | * Another Test: T 167 | * !Im Disabled: I 168 | * Some Thing: S + Meta 169 | * --- 170 | * Bazz: s + Meta, Control, Alt; 171 | * `) 172 | * ``` 173 | * 174 | * #### Separators 175 | * 176 | * To create a separator, use three dashes `---`. 177 | * 178 | * #### Accelerator Modifiers 179 | * 180 | * Accelerator modifiers are used as visual indicators but don't have a 181 | * material impact as the actual key binding is done in the event listener. 182 | * 183 | * A capital letter implies that the accelerator is modified by the `Shift` key. 184 | * 185 | * Additional accelerators are `Meta`, `Control`, `Option`, each separated 186 | * by commas. If one is not applicable for a platform, it will just be ignored. 187 | * 188 | * On MacOS `Meta` is the same as `Command`. 189 | * 190 | * #### Disabled Items 191 | * 192 | * If you want to disable a menu item just prefix the item with the `!` character. 193 | * This will cause the item to appear disabled when the system menu renders. 194 | * 195 | * #### Submenus 196 | * 197 | * We feel like nested menus are an anti-pattern. We don't use them. If you have a 198 | * strong argument for them and a very simple pull request that makes them work we 199 | * may consider them. 200 | * 201 | * #### Event Handling 202 | * 203 | * When a menu item is activated, it raises the `menuItemSelected` event in 204 | * the front end code, you can then communicate with your backend code if you 205 | * want from there. 206 | * 207 | * For example, if the `Apple` item is selected from the `Other` menu... 208 | * 209 | * ```js 210 | * window.addEventListener('menuItemSelected', event => { 211 | * assert(event.detail.parent === 'Other') 212 | * assert(event.detail.title === 'Apple') 213 | * }) 214 | * ``` 215 | * 216 | */ 217 | export async function setSystemMenu (o) { 218 | const menu = o.value 219 | 220 | // validate the menu 221 | if (typeof menu !== 'string' || menu.trim().length === 0) { 222 | throw new Error('Menu must be a non-empty string') 223 | } 224 | 225 | const menus = menu.match(/\w+:\n/g) 226 | if (!menus) { 227 | throw new Error('Menu must have a valid format') 228 | } 229 | const menuTerminals = menu.match(/;/g) 230 | const delta = menus.length - (menuTerminals?.length ?? 0) 231 | 232 | if ((delta !== 0) && (delta !== -1)) { 233 | throw new Error(`Expected ${menuTerminals.length} ';', found ${menus}.`) 234 | } 235 | 236 | const lines = menu.split('\n') 237 | const e = new Error() 238 | const frame = e.stack.split('\n')[2] 239 | const callerLineNo = frame.split(':').reverse()[1] 240 | 241 | for (let i = 0; i < lines.length; i++) { 242 | const line = lines[i] 243 | const l = Number(callerLineNo) + i 244 | 245 | let errMsg 246 | 247 | if (line.trim().length === 0) continue 248 | if (/.*:\n/.test(line)) continue // ignore submenu labels 249 | if (/---/.test(line)) continue // ignore separators 250 | if (/\w+/.test(line) && !line.includes(':')) { 251 | errMsg = 'Missing label' 252 | } else if (/:\s*\+/.test(line)) { 253 | errMsg = 'Missing accelerator' 254 | } else if (/\+(\n|$)/.test(line)) { 255 | errMsg = 'Missing modifier' 256 | } 257 | 258 | if (errMsg) { 259 | throw new Error(`${errMsg} on line ${l}: "${line}"`) 260 | } 261 | } 262 | 263 | return await ipc.send('menu', o) 264 | } 265 | 266 | export function reload () { 267 | ipc.postMessage('ipc://reload') 268 | } 269 | 270 | // eslint-disable-next-line 271 | import * as exports from './runtime.js' 272 | export default exports 273 | -------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import path from 'node:path' 3 | import fs from 'node:fs/promises' 4 | 5 | import esbuild from 'esbuild' 6 | 7 | const cp = async (a, b) => fs.cp( 8 | path.resolve(a), 9 | path.join(b, path.basename(a)), 10 | { recursive: true, force: true } 11 | ) 12 | 13 | async function copy (target) { 14 | await Promise.all([ 15 | cp('src/frontend/index.html', target), 16 | cp('src/frontend/index_second_window.html', target), 17 | cp('src/frontend/index_second_window2.html', target), 18 | cp('fixtures', target), 19 | // for testing purposes 20 | cp('socket.ini', target), 21 | // backend 22 | cp('src/backend/backend.js', target) 23 | ]) 24 | } 25 | 26 | async function main () { 27 | const params = { 28 | entryPoints: ['src/index.js'], 29 | format: 'esm', 30 | bundle: true, 31 | keepNames: true, 32 | platform: 'browser', 33 | sourcemap: 'inline', 34 | outdir: path.resolve(process.argv[2]) 35 | } 36 | 37 | await Promise.all([ 38 | esbuild.build(params), 39 | esbuild.build({ ...params, entryPoints: ['src/frontend/index_second_window.js'] }), 40 | copy(params.outdir) 41 | ]) 42 | } 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /test/fixtures/bin/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/bin/file -------------------------------------------------------------------------------- /test/fixtures/directory/0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/0.txt -------------------------------------------------------------------------------- /test/fixtures/directory/1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/1.txt -------------------------------------------------------------------------------- /test/fixtures/directory/2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/2.txt -------------------------------------------------------------------------------- /test/fixtures/directory/a.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/a.txt -------------------------------------------------------------------------------- /test/fixtures/directory/b.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/b.txt -------------------------------------------------------------------------------- /test/fixtures/directory/c.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/fixtures/directory/c.txt -------------------------------------------------------------------------------- /test/fixtures/file.js: -------------------------------------------------------------------------------- 1 | console.log('test 123') 2 | -------------------------------------------------------------------------------- /test/fixtures/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": "test 123" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | test 123 2 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "scripts": { 5 | "test": "npm run test:desktop", 6 | "test:desktop": "./scripts/test-desktop.sh", 7 | "test:android": "./scripts/test-android.sh", 8 | "test:ios-simulator": "./scripts/test-ios-simulator.sh", 9 | "test:android-emulator": "./scripts/test-android-emulator.sh", 10 | "start": "npm test" 11 | }, 12 | "dependencies": { 13 | "@socketsupply/ssc-node": "github:socketsupply/ssc-node" 14 | }, 15 | "devDependencies": { 16 | "esbuild": "^0.16.4", 17 | "path-browserify": "^1.0.1", 18 | "@socketsupply/tapzero": "^0.7.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/scripts/bootstrap-android-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare ANDROID_HOME="${ANDROID_HOME:-$HOME/.android/sdk}" 4 | 5 | declare emulator_flags=() 6 | declare emulator="$(which emulator 2>/dev/null)" 7 | 8 | if [ -z "$emulator" ]; then 9 | emulator="$ANDROID_HOME/emulator/emulator" 10 | fi 11 | 12 | emulator_flags+=( 13 | -gpu swiftshader_indirect 14 | -camera-back none 15 | -no-boot-anim 16 | -no-window 17 | -noaudio 18 | ) 19 | 20 | emulator @SSCAVD "${emulator_flags[@]}" >/dev/null 21 | -------------------------------------------------------------------------------- /test/scripts/poll-adb-logcat.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare id="" 4 | declare pid="" 5 | 6 | id="co.socketsupply.socket.tests" 7 | 8 | ## Start application 9 | adb shell am start -n "$id/.MainActivity" || exit $? 10 | 11 | echo "polling for '$id' PID in adb" 12 | while [ -z "$pid" ]; do 13 | ## Probe for application process ID 14 | pid="$(adb shell ps | grep "$id" | awk '{print $2}' 2>/dev/null)" 15 | sleep 1s 16 | done 17 | 18 | ## Process logs from 'adb logcat' 19 | while read -r line; do 20 | if grep 'Console' < <(echo "$line") >/dev/null; then 21 | line="$(echo "$line" | sed 's/.*Console:\s*//g')" 22 | 23 | if [[ "$line" =~ __EXIT_SIGNAL__ ]]; then 24 | status="${line//__EXIT_SIGNAL__=/}" 25 | exit "$status" 26 | fi 27 | 28 | echo "$line" 29 | 30 | if [ "$line" == "# ok" ]; then 31 | exit 32 | fi 33 | 34 | if [ "$line" == "# fail" ]; then 35 | exit 1 36 | fi 37 | fi 38 | done < <(adb logcat --pid="$pid") 39 | -------------------------------------------------------------------------------- /test/scripts/test-android-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare id="" 4 | declare root="" 5 | 6 | id="co.socketsupply.socket.tests" 7 | root="$(dirname "$(dirname "${BASH_SOURCE[0]}")")" 8 | 9 | "$root/scripts/bootstrap-android-emulator.sh" & 10 | 11 | echo "info: Waiting for Android Emulator to boot" 12 | while ! adb shell getprop sys.boot_completed >/dev/null 2>&1 ; do 13 | sleep 0.5s 14 | done 15 | echo "info: Android Emulator booted" 16 | 17 | adb uninstall "$id" 18 | ssc build --headless --platform=android -r -o . >/dev/null || { 19 | rc=$? 20 | echo "info: Shutting Android Emulator" 21 | adb devices | grep emulator | cut -f1 | while read -r line; do 22 | adb -s "$line" emu kill 23 | done 24 | exit "$rc" 25 | } 26 | 27 | adb shell rm -rf "/data/local/tmp/ssc-socket-test-fixtures" 28 | adb push "$root/fixtures/" "/data/local/tmp/ssc-socket-test-fixtures" 29 | 30 | "$root/scripts/poll-adb-logcat.sh" 31 | 32 | echo "info: Shutting Android Emulator" 33 | adb devices | grep emulator | cut -f1 | while read -r line; do 34 | adb -s "$line" emu kill 35 | done 36 | 37 | adb kill-server 38 | -------------------------------------------------------------------------------- /test/scripts/test-android.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare id="" 4 | declare root="" 5 | 6 | id="co.socketsupply.socket.tests" 7 | root="$(dirname "$(dirname "${BASH_SOURCE[0]}")")" 8 | 9 | adb uninstall "$id" 10 | 11 | ssc build --headless --platform=android -r -o . 12 | 13 | adb shell rm -rf "/data/local/tmp/ssc-socket-test-fixtures" 14 | adb push "$root/fixtures/" "/data/local/tmp/ssc-socket-test-fixtures" 15 | 16 | "$root/scripts/poll-adb-logcat.sh" 17 | -------------------------------------------------------------------------------- /test/scripts/test-desktop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare root="" 4 | declare TMPDIR="${TMPDIR:-${TMP:-/tmp}}" 5 | 6 | root="$(dirname "$(dirname "${BASH_SOURCE[0]}")")" 7 | rm -rf "$TMPDIR/ssc-socket-test-fixtures" 8 | cp -rf "$root/fixtures/" "$TMPDIR/ssc-socket-test-fixtures" 9 | 10 | if [ -z "$DEBUG" ]; then 11 | ssc build --headless --prod -r -o . 12 | else 13 | ssc build -r -o . 14 | fi 15 | 16 | # rm -rf "$TMPDIR/ssc-socket-test-fixtures" 17 | -------------------------------------------------------------------------------- /test/scripts/test-ios-simulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ssc build --headless --platform=ios-simulator --prod -r -o . 4 | -------------------------------------------------------------------------------- /test/socket.ini: -------------------------------------------------------------------------------- 1 | ; Build Settings 2 | input = src 3 | build = node build.js 4 | output = build 5 | executable = socket-tests 6 | 7 | ; Compiler Settings 8 | flags = "-O3 -g" 9 | 10 | ; Package Metadata 11 | version = "1.0.0" 12 | revision = 1 13 | name = "socketsupply-socket-tests" 14 | description = "@socketsupply/socket-api Tests" 15 | lang = en-US 16 | copyright = "Socket Supply Co. © 2021-2022" 17 | maintainer = "Socket Supply Co." 18 | bundle_identifier = co.socketsupply.socket.tests 19 | 20 | headless = true 21 | 22 | env = "TMPDIR, TMP, TEMP, HOME, PWD" 23 | 24 | [window] 25 | width = 80% 26 | height = 80% 27 | 28 | [debug] 29 | flags = -g 30 | [mac] 31 | cmd = "node backend.js" 32 | [linux] 33 | cmd = "node backend.js" 34 | [win] 35 | cmd = "node backend.js" 36 | [ios] 37 | simulator_device = "iPhone 14" 38 | [android] 39 | manifest_permissions = MANAGE_EXTERNAL_STORAGE 40 | -------------------------------------------------------------------------------- /test/src/backend.js: -------------------------------------------------------------------------------- 1 | import backend from '../../backend.js' 2 | import { test } from '@socketsupply/tapzero' 3 | 4 | if (window.__args.os !== 'android' && window.__args.os !== 'ios') { 5 | test('backend.open()', async (t) => { 6 | const openResult = await backend.open() 7 | t.deepEqual(Object.keys(openResult.data).sort(), ['argv', 'cmd', 'path'], 'returns a result with the correct keys') 8 | }) 9 | 10 | test('backend', async (t) => { 11 | const successOpenPromise = Promise.race([ 12 | new Promise(resolve => window.addEventListener('process-error', ({ detail }) => { 13 | if (detail) t.fail(`process-error event emitted: ${detail}`) 14 | resolve(false) 15 | }, { once: true })), 16 | new Promise(resolve => setTimeout(() => resolve(true), 0)) 17 | ]) 18 | const backendReadyPromise = new Promise(resolve => window.addEventListener('backend:ready', () => resolve(true), { once: true })) 19 | const backenSendDataPromise = new Promise(resolve => window.addEventListener('character', ({ detail }) => resolve(detail), { once: true })) 20 | const [successOpen, backendReady, backendSendData] = await Promise.all([successOpenPromise, backendReadyPromise, backenSendDataPromise]) 21 | t.ok(successOpen, 'does not emit a process-error event') 22 | t.ok(backendReady, 'can send events to window 0') 23 | t.deepEqual(backendSendData, { character: { firstname: 'Morty', secondname: 'Smith' } }, 'can send events with data') 24 | }) 25 | 26 | test('backend.open() again', async (t) => { 27 | const openResult = await backend.open() 28 | t.deepEqual(Object.keys(openResult.data).sort(), ['argv', 'cmd', 'path'], 'returns a result with the correct keys') 29 | const doesNotRestart = await Promise.race([ 30 | new Promise(resolve => window.addEventListener('backend:ready', () => resolve(false), { once: true })), 31 | new Promise(resolve => setTimeout(() => resolve(true), 256)) 32 | ]) 33 | t.ok(doesNotRestart, 'does not emit a backend:ready event') 34 | }) 35 | 36 | test('backend.open({ force: true })', async (t) => { 37 | const openResult = await backend.open({ force: true }) 38 | t.deepEqual(Object.keys(openResult.data).sort(), ['argv', 'cmd', 'path'], 'returns a result with the correct keys') 39 | const doesRestart = await Promise.race([ 40 | new Promise(resolve => window.addEventListener('backend:ready', () => resolve(true), { once: true })), 41 | new Promise(resolve => setTimeout(() => resolve(false), 256)) 42 | ]) 43 | t.ok(doesRestart, 'emits a backend:ready event') 44 | }) 45 | 46 | test('backend.sendToProcess()', async (t) => { 47 | const sendResult = await backend.sendToProcess({ firstname: 'Morty', secondname: 'Smith' }) 48 | // TODO: what is the correct result? 49 | t.ok(sendResult.err == null, 'returns correct result') 50 | const character = await new Promise(resolve => window.addEventListener('character.backend', ({ detail }) => resolve(detail), { once: true })) 51 | t.deepEqual(character, { character: { firstname: 'Summer', secondname: 'Smith' } }, 'send data to process') 52 | }) 53 | 54 | test('backend.close()', async (t) => { 55 | const closeResult = await backend.close() 56 | t.ok(closeResult.err == null, 'returns correct result') 57 | const doesNotRestart = await Promise.race([ 58 | new Promise(resolve => window.addEventListener('backend:ready', () => resolve(false), { once: true })), 59 | new Promise(resolve => setTimeout(() => resolve(true), 256)) 60 | ]) 61 | t.ok(doesNotRestart, 'does not emit a backend:ready event') 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /test/src/backend/backend.js: -------------------------------------------------------------------------------- 1 | import system from '@socketsupply/ssc-node' 2 | 3 | system.receive = async (command, value) => { 4 | if (command === 'process.write') { 5 | await system.send({ 6 | window: 0, 7 | event: 'character.backend', 8 | value: { character: { firstname: 'Summer', secondname: 'Smith' } } 9 | }) 10 | } 11 | }; 12 | 13 | (async () => { 14 | await system.send({ 15 | window: 0, 16 | event: 'backend:ready' 17 | }) 18 | await system.send({ 19 | window: 0, 20 | event: 'character', 21 | value: { character: { firstname: 'Morty', secondname: 'Smith' } } 22 | }) 23 | })() 24 | -------------------------------------------------------------------------------- /test/src/crypto.js: -------------------------------------------------------------------------------- 1 | import crypto from '../../crypto.js' 2 | import Buffer from '../../buffer.js' 3 | import { test } from '@socketsupply/tapzero' 4 | 5 | test('crypto', async (t) => { 6 | t.equal(crypto.webcrypto, window.crypto, 'crypto.webcrypto is window.crypto') 7 | const randomValues = crypto.getRandomValues(new Uint32Array(10)) 8 | t.equal(randomValues.length, 10, 'crypto.getRandomValues returns an array of the correct length') 9 | t.ok(randomValues.some(value => value !== randomValues[9], 'crypto.getRandomValues returns an array of random values')) 10 | t.ok(randomValues.every(value => Number.isInteger(value)), 'crypto.getRandomValues returns an array of integers') 11 | 12 | t.equal(crypto.RANDOM_BYTES_QUOTA, 64 * 1024, 'crypto.RANDOM_BYTES_QUOTA is 65536') 13 | t.equal(crypto.MAX_RANDOM_BYTES, 0xFFFF_FFFF_FFFF, 'crypto.MAX_RANDOM_BYTES is 0xFFFF_FFFF_FFFF') 14 | t.equal(crypto.MAX_RANDOM_BYTES_PAGES, crypto.MAX_RANDOM_BYTES / crypto.RANDOM_BYTES_QUOTA, `crypto.MAX_RANDOM_BYTES_PAGES is ${crypto.MAX_RANDOM_BYTES / crypto.RANDOM_BYTES_QUOTA}`) 15 | 16 | const buffer = crypto.randomBytes(10) 17 | t.equal(buffer.length, 10, 'crypto.randomBytes returns a buffer of the correct length') 18 | t.ok(buffer.some(value => value !== buffer[9], 'crypto.randomBytes returns a buffer of random values')) 19 | t.ok(buffer.every(value => Number.isInteger(value)), 'crypto.randomBytes returns a buffer of integers') 20 | 21 | const digest = await crypto.createDigest('SHA-256', new Uint8Array(32)) 22 | t.ok(digest instanceof Buffer, 'crypto.createDigest returns a buffer') 23 | t.equal(digest.length, 32, 'crypto.createDigest returns a buffer of the correct length') 24 | }) 25 | -------------------------------------------------------------------------------- /test/src/dgram.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '../../events.js' 2 | import console from '../../console.js' 3 | import crypto from '../../crypto.js' 4 | import Buffer from '../../buffer.js' 5 | import dgram from '../../dgram.js' 6 | import util from '../../util.js' 7 | 8 | import { test } from '@socketsupply/tapzero' 9 | 10 | // node compat 11 | /* 12 | import { EventEmitter } from 'node:events' 13 | import crypto from 'node:crypto' 14 | import { Buffer } from 'node:buffer' 15 | import dgram from 'node:dgram' 16 | import util from 'node:util' 17 | */ 18 | 19 | const MTU = 1518 20 | 21 | function makePayload () { 22 | const r = Math.random() * MTU 23 | return Array(Math.floor(r)).fill(0).join('') 24 | } 25 | 26 | test('dgram exports', t => { 27 | t.ok(dgram, 'dgram is available') 28 | t.ok(dgram.Socket.prototype instanceof EventEmitter, 'dgram.Socket is an EventEmitter') 29 | t.ok(dgram.Socket.length === 2, 'dgram.Socket accepts two arguments') 30 | t.ok(dgram.createSocket, 'dgram.createSocket is available') 31 | t.ok(dgram.createSocket.length === 2, 'dgram.createSocket accepts two arguments') 32 | }) 33 | 34 | test('Socket creation', t => { 35 | t.throws( 36 | () => dgram.createSocket(), 37 | // eslint-disable-next-line prefer-regex-literals 38 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 39 | 'throws on missing type for dgram.createSocket' 40 | ) 41 | t.throws( 42 | () => new dgram.Socket(), 43 | // eslint-disable-next-line prefer-regex-literals 44 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 45 | 'throws on missing type for new dgram.Socket' 46 | ) 47 | t.throws( 48 | () => dgram.createSocket('udp5'), 49 | // eslint-disable-next-line prefer-regex-literals 50 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 51 | 'throws on invalid string type for dgram.createSocket' 52 | ) 53 | t.throws( 54 | () => dgram.createSocket({ type: 'udp5' }), 55 | // eslint-disable-next-line prefer-regex-literals 56 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 57 | 'throws on invalid object entry type for dgram.createSocket' 58 | ) 59 | t.throws( 60 | () => new dgram.Socket('udp5'), 61 | // eslint-disable-next-line prefer-regex-literals 62 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 63 | 'throws on invalid string type for new dgram.Socket' 64 | ) 65 | t.throws( 66 | () => new dgram.Socket({ type: 'udp5' }), 67 | // eslint-disable-next-line prefer-regex-literals 68 | RegExp('Bad socket type specified. Valid types are: udp4, udp6'), 69 | 'throws on invalid object entry type for new dgram.Socket' 70 | ) 71 | t.ok(dgram.createSocket('udp4'), 'works for dgram.createSocket with string type udp4') 72 | t.ok(dgram.createSocket('udp6'), 'works for dgram.createSocket with string type udp6') 73 | t.ok(dgram.createSocket({ type: 'udp4' }), 'works for dgram.createSocket with string type udp4') 74 | t.ok(dgram.createSocket({ type: 'udp6' }), 'works for dgram.createSocket with object entry type udp6') 75 | t.ok(new dgram.Socket('udp4'), 'works for new dgram.Socket with string type udp4') 76 | t.ok(new dgram.Socket('udp6'), 'works for new dgram.Socket with string type udp6') 77 | t.ok(new dgram.Socket({ type: 'udp4' }), 'works for new dgram.Socket with string type udp4') 78 | t.ok(new dgram.Socket({ type: 'udp6' }), 'works for new dgram.Socket with object entry type udp6') 79 | }) 80 | 81 | test('dgram createSocket, address, bind, close', async (t) => { 82 | const server = dgram.createSocket({ type: 'udp4' }) 83 | t.ok(server instanceof dgram.Socket, 'dgram.createSocket returns a dgram.Socket') 84 | t.throws( 85 | () => server.address(), 86 | // eslint-disable-next-line prefer-regex-literals 87 | RegExp('(Not running)|(getsockname EBADF)'), 88 | 'server.address() throws an error if the socket is not bound' 89 | ) 90 | t.ok(server.bind(41233) === server, 'dgram.bind returns the socket') 91 | await new Promise((resolve) => { 92 | server.once('listening', () => { 93 | // FIXME: 94 | // t.throws( 95 | // () => server.bind(41233), 96 | // RegExp('bind EADDRINUSE 0.0.0.0:41233'), 97 | // 'server.bind throws an error if the socket is already bound' 98 | // ) 99 | t.deepEqual( 100 | server.address(), 101 | { address: '0.0.0.0', port: 41233, family: 'IPv4' }, 102 | 'server.address() returns the bound address' 103 | ) 104 | t.equal(server.close(), server, 'server.close() returns instance') 105 | t.throws( 106 | () => server.close(), 107 | // eslint-disable-next-line prefer-regex-literals 108 | RegExp('Not running'), 109 | 'server.close() throws an error is the socket is already closed' 110 | ) 111 | 112 | resolve() 113 | }) 114 | }) 115 | }) 116 | 117 | test('udp bind, send, remoteAddress', async (t) => { 118 | const server = dgram.createSocket({ 119 | type: 'udp4', 120 | reuseAddr: false 121 | }) 122 | 123 | const client = dgram.createSocket('udp4') 124 | 125 | const msg = new Promise((resolve, reject) => { 126 | server.on('message', (data, addr) => { 127 | t.equal('number', typeof addr.port, 'port is a number') 128 | t.equal(addr.address, '127.0.0.1') 129 | resolve(data) 130 | }) 131 | server.on('error', reject) 132 | }) 133 | 134 | const payload = makePayload() 135 | t.ok(payload.length > 0, `${payload.length} bytes prepared`) 136 | 137 | server.on('listening', () => { 138 | t.ok(true, 'listening') 139 | client.send(Buffer.from(payload), 41234, '0.0.0.0') 140 | }) 141 | 142 | server.bind(41234) 143 | 144 | try { 145 | const r = Buffer.from(await msg).toString() 146 | t.ok(r === payload, `${payload.length} bytes match`) 147 | } catch (err) { 148 | t.fail(err, err.message) 149 | } 150 | 151 | server.close() 152 | client.close() 153 | }) 154 | 155 | test('udp socket message and bind callbacks', async (t) => { 156 | let server 157 | const msgCbResult = new Promise(resolve => { 158 | server = dgram.createSocket({ 159 | type: 'udp4', 160 | reuseAddr: false 161 | }, (msg, rinfo) => { 162 | resolve({ msg, rinfo }) 163 | }) 164 | }) 165 | 166 | const client = dgram.createSocket('udp4') 167 | 168 | server.on('listening', () => { 169 | client.send('payload', 41235, '0.0.0.0') 170 | }) 171 | 172 | const listeningCbResult = new Promise(resolve => { 173 | server.bind(41235, '0.0.0.0', resolve) 174 | }) 175 | 176 | const [{ msg, rinfo }] = await Promise.all([msgCbResult, listeningCbResult]) 177 | t.ok(true, 'listening callback called') 178 | t.equal(Buffer.from(msg).toString(), 'payload', 'message matches') 179 | t.equal(rinfo.address, '127.0.0.1', 'rinfo.address is correct') 180 | t.ok(Number.isInteger(rinfo.port), 'rinfo.port is correct') 181 | t.equal(rinfo.family, 'IPv4', 'rinfo.family is correct') 182 | 183 | server.close() 184 | client.close() 185 | }) 186 | 187 | test('udp bind, connect, send', async (t) => { 188 | const payload = makePayload() 189 | const server = dgram.createSocket('udp4') 190 | const client = dgram.createSocket('udp4') 191 | 192 | const msg = new Promise((resolve, reject) => { 193 | server.on('message', resolve) 194 | server.on('error', reject) 195 | }) 196 | 197 | t.throws( 198 | () => client.remoteAddress(), 199 | // eslint-disable-next-line prefer-regex-literals 200 | RegExp('Not connected'), 201 | 'client.remoteAddress() throws an error if the socket is not connected' 202 | ) 203 | 204 | server.on('listening', () => { 205 | client.connect(41236, '0.0.0.0', (err) => { 206 | if (err) return t.fail(err.message) 207 | t.deepEqual( 208 | client.remoteAddress(), 209 | { address: '127.0.0.1', port: 41236, family: 'IPv4' }, 210 | 'client.remoteAddress() returns the remote address' 211 | ) 212 | client.send(Buffer.from(payload)) 213 | }) 214 | }) 215 | 216 | server.bind(41236) 217 | 218 | try { 219 | const r = Buffer.from(await msg).toString() 220 | t.ok(r === payload, `${payload.length} bytes match`) 221 | } catch (err) { 222 | t.fail(err, err.message) 223 | } 224 | 225 | server.close() 226 | client.close() 227 | }) 228 | 229 | test('udp send callback', async (t) => { 230 | const message = Buffer.from('Some bytes') 231 | const client = dgram.createSocket('udp4') 232 | const result = await new Promise(resolve => { 233 | client.send(message, 41237, '0.0.0.0', (err) => { 234 | client.close() 235 | if (err) return t.fail(err.message) 236 | resolve(true) 237 | }) 238 | }) 239 | t.ok(result, 'send callback called') 240 | }) 241 | 242 | test('udp createSocket AbortSignal', async (t) => { 243 | const controller = new AbortController() 244 | const { signal } = controller 245 | const server = dgram.createSocket({ type: 'udp4', signal }) 246 | let isSocketClosed = false 247 | await new Promise(resolve => { 248 | server.bind(44444) 249 | server.once('listening', () => { 250 | controller.abort() 251 | isSocketClosed = true 252 | 253 | t.throws( 254 | () => server.close(), 255 | // eslint-disable-next-line prefer-regex-literals 256 | RegExp('Not running'), 257 | 'server.close() throws an error is the socket is already closed' 258 | ) 259 | 260 | resolve() 261 | }) 262 | }) 263 | t.ok(isSocketClosed, 'socket is closed after abort, close event is emitted') 264 | }) 265 | 266 | test('client ~> server (~512 messages)', async (t) => { 267 | const TIMEOUT = 1024 268 | const buffers = Array.from(Array(512), () => crypto.randomBytes(1024)) 269 | const server = dgram.createSocket('udp4') 270 | const client = dgram.createSocket('udp4') 271 | const addr = '0.0.0.0' 272 | const port = 3000 273 | 274 | await new Promise((resolve) => { 275 | let timeout = setTimeout(ontimeout, TIMEOUT) 276 | let i = 0 277 | 278 | function ontimeout () { 279 | t.fail(`Not all messagess received (${buffers.length - i} missing)`) 280 | resolve() 281 | } 282 | 283 | server.bind(port, addr, () => { 284 | server.on('message', (message) => { 285 | clearTimeout(timeout) 286 | timeout = setTimeout(ontimeout, TIMEOUT) 287 | 288 | if (++i === buffers.length) { 289 | clearTimeout(timeout) 290 | t.ok(true, `all ${buffers.length} messages received`) 291 | resolve() 292 | } 293 | }) 294 | 295 | client.connect(port, addr, async () => { 296 | for (const buffer of buffers) { 297 | await new Promise((resolve) => { 298 | setTimeout(() => client.send(buffer, resolve)) 299 | }) 300 | } 301 | }) 302 | }) 303 | }) 304 | 305 | await Promise.all([ 306 | util.promisify(server.close.bind(server))(), 307 | util.promisify(client.close.bind(client))() 308 | ]) 309 | }) 310 | 311 | test('connect + disconnect', async (t) => { 312 | await new Promise((resolve) => { 313 | const server = dgram.createSocket('udp4').bind(3000, (err) => { 314 | if (err) { 315 | console.error(err) 316 | return t.fail('failed to bind') 317 | } 318 | 319 | const client = dgram.createSocket('udp4') 320 | client.connect(3000, (err) => { 321 | if (err) { 322 | console.error(err) 323 | return t.fail('failed to connect') 324 | } 325 | 326 | client.send('hello', (err) => { 327 | if (err) { 328 | console.error(err) 329 | return t.fail('failed to send "hello"') 330 | } 331 | }) 332 | 333 | server.once('message', (message) => { 334 | t.ok( 335 | Buffer.compare(Buffer.from(message), Buffer.from('hello')) === 0, 336 | 'client sent message matches' 337 | ) 338 | client.disconnect() 339 | server.once('message', (message) => { 340 | t.fail('client did not disconnect') 341 | }) 342 | 343 | try { 344 | client.send('unreachable') 345 | t.fail('send did not throw') 346 | } catch (err) { 347 | t.ok(err?.code === 'ERR_SOCKET_BAD_PORT', 'Bad port for disconnected socket') 348 | } 349 | 350 | client.close(() => { 351 | server.close(resolve) 352 | }) 353 | }) 354 | }) 355 | }) 356 | }) 357 | }) 358 | 359 | /* 360 | test('can send and receive packets to a remote server', async (t) => { 361 | const remoteAddress = '3.25.141.150' 362 | const remotePort = 3456 363 | const server = dgram.createSocket('udp4').bind(remotePort) 364 | const msg = new Promise((resolve, reject) => { 365 | let timer = null 366 | 367 | server.on('message', (data) => { 368 | clearTimeout(timer) 369 | resolve(data) 370 | }) 371 | 372 | server.on('error', reject) 373 | 374 | server.on('listening', async () => { 375 | const payload = JSON.stringify({ 376 | type: 'ping', 377 | id: crypto.randomBytes(32).toString('hex') 378 | }) 379 | 380 | server.send(payload, remotePort, remoteAddress, (err) => { 381 | if (err) { 382 | t.fail(err) 383 | } 384 | }) 385 | 386 | timer = setTimeout(() => { 387 | reject(new Error('no ping back after 3 seconds')) 388 | }, 3000) 389 | }) 390 | }) 391 | 392 | try { 393 | const data = JSON.parse(Buffer.from(await msg)) 394 | t.ok(data && typeof data === 'object', 'response is an object') 395 | t.ok(data?.type === 'pong', 'response contains type.pong') 396 | t.ok(typeof data?.address === 'string', 'response contains address') 397 | } catch (err) { 398 | t.fail('package not received') 399 | } 400 | 401 | server.close() 402 | }) 403 | */ 404 | -------------------------------------------------------------------------------- /test/src/dns.js: -------------------------------------------------------------------------------- 1 | import dns from '../../dns.js' 2 | 3 | import { test } from '@socketsupply/tapzero' 4 | 5 | // node compat 6 | // import dns from 'node:dns' 7 | 8 | const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ 9 | const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/ 10 | 11 | const isOnline = Boolean(globalThis?.navigator?.onLine || process?.versions?.node) 12 | 13 | test('dns exports', t => { 14 | t.ok(typeof dns.lookup === 'function', 'lookup is available') 15 | t.equal(dns.lookup.length, 3, 'lookup expect 3 arguments') 16 | t.ok(typeof dns.promises.lookup === 'function', 'promises.lookup is available') 17 | t.equal(dns.promises.lookup.length, 2, 'promises.lookup expect 2 arguments') 18 | }) 19 | 20 | test('dns.lookup', async t => { 21 | if (!isOnline) { 22 | return t.comment('skipping offline') 23 | } 24 | 25 | await Promise.all([ 26 | new Promise(resolve => { 27 | dns.lookup('google.com', (err, address, family) => { 28 | if (err) return t.fail(err) 29 | 30 | const isValidFamily = (family === 4) || (family === 6) 31 | 32 | t.ok(isValidFamily, 'is either IPv4 or IPv6 family') 33 | 34 | const v4 = IPV4_REGEX.test(address) 35 | const v6 = IPV6_REGEX.test(address) 36 | 37 | t.ok(v4 || v6, 'has valid address') 38 | resolve() 39 | }) 40 | }), 41 | new Promise(resolve => { 42 | dns.lookup('google.com', 4, (err, address, family) => { 43 | if (err) return t.fail(err) 44 | t.equal(family, 4, 'is IPv4 family') 45 | t.ok(IPV4_REGEX.test(address), 'has valid IPv4 address') 46 | resolve() 47 | }) 48 | }), 49 | new Promise(resolve => { 50 | dns.lookup('cloudflare.com', 6, (err, address, family) => { 51 | if (err) return t.fail(err) 52 | t.equal(family, 6, 'is IPv6 family') 53 | t.ok(IPV6_REGEX.test(address), 'has valid IPv6 address') 54 | resolve() 55 | }) 56 | }), 57 | new Promise(resolve => { 58 | dns.lookup('google.com', { family: 4 }, (err, address, family) => { 59 | if (err) return t.fail(err) 60 | t.equal(family, 4, 'is IPv4 family') 61 | t.ok(IPV4_REGEX.test(address), 'has valid IPv4 address') 62 | resolve() 63 | }) 64 | }), 65 | new Promise(resolve => { 66 | dns.lookup('cloudflare.com', { family: 6 }, (err, address, family) => { 67 | if (err) return t.fail(err) 68 | t.equal(family, 6, 'is IPv6 family') 69 | t.ok(IPV6_REGEX.test(address), 'has valid IPv6 address') 70 | resolve() 71 | }) 72 | }) 73 | // TODO: call with other options 74 | ]) 75 | }) 76 | 77 | const BAD_HOSTNAME = 'thisisnotahostname' 78 | 79 | test('dns.lookup bad hostname', async t => { 80 | if (!isOnline) { 81 | return t.comment('skipping offline') 82 | } 83 | 84 | await new Promise(resolve => { 85 | dns.lookup(BAD_HOSTNAME, (err, info) => { 86 | t.equal(err.message, `getaddrinfo EAI_AGAIN ${BAD_HOSTNAME}`, 'returns an error on unexisting hostname') 87 | resolve() 88 | }) 89 | }) 90 | }) 91 | 92 | test('dns.promises.lookup', async t => { 93 | if (!isOnline) { 94 | return t.comment('skipping offline') 95 | } 96 | 97 | try { 98 | const info = await dns.promises.lookup('google.com', 4) 99 | t.ok(info && typeof info === 'object', 'returns a non-error object after resolving a hostname') 100 | t.equal(info.family, 4, 'is IPv4 family') 101 | t.ok(IPV4_REGEX.test(info.address), 'has valid IPv4 address') 102 | } catch (err) { 103 | t.fail(err) 104 | } 105 | try { 106 | const info = await dns.promises.lookup('google.com', 6) 107 | t.ok(info && typeof info === 'object', 'returns a non-error object after resolving a hostname') 108 | t.equal(info.family, 6, 'is IPv6 family') 109 | t.ok(IPV6_REGEX.test(info.address), 'has valid IPv4 address') 110 | } catch (err) { 111 | t.fail(err) 112 | } 113 | try { 114 | const info = await dns.promises.lookup('google.com', { family: 4 }) 115 | t.ok(info && typeof info === 'object', 'returns a non-error object after resolving a hostname') 116 | t.equal(info.family, 4, 'is IPv4 family') 117 | t.ok(IPV4_REGEX.test(info.address), 'has valid IPv4 address') 118 | } catch (err) { 119 | t.fail(err) 120 | } 121 | try { 122 | const info = await dns.promises.lookup('cloudflare.com', 6) 123 | t.ok(info && typeof info === 'object', 'returns a non-error object after resolving a hostname') 124 | t.equal(info.family, 6, 'is IPv6 family') 125 | t.ok(IPV6_REGEX.test(info.address), 'has valid IPv6 address') 126 | } catch (err) { 127 | t.fail(err) 128 | } 129 | try { 130 | const info = await dns.promises.lookup('cloudflare.com', { family: 6 }) 131 | t.ok(info && typeof info === 'object', 'returns a non-error object after resolving a hostname') 132 | t.equal(info.family, 6, 'is IPv6 family') 133 | t.ok(IPV6_REGEX.test(info.address), 'has valid IPv6 address') 134 | } catch (err) { 135 | t.fail(err) 136 | } 137 | }) 138 | 139 | test('dns.promises.lookup bad hostname', async t => { 140 | if (!isOnline) { 141 | return t.comment('skipping offline') 142 | } 143 | 144 | try { 145 | await dns.promises.lookup(BAD_HOSTNAME) 146 | } catch (err) { 147 | t.equal(err.message, `getaddrinfo EAI_AGAIN ${BAD_HOSTNAME}`, 'returns an error on unexisting hostname') 148 | } 149 | }) 150 | -------------------------------------------------------------------------------- /test/src/frontend/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <meta http-equiv="content-type" content="text/html; charset=utf-8" /> 5 | <title>@socketsupply/socket-api E2E Tests 6 | 7 | 8 | 9 |

window 0

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/src/frontend/index_second_window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Secondary window 6 | 7 | 8 | 9 |

window

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/src/frontend/index_second_window.js: -------------------------------------------------------------------------------- 1 | import runtime from '../../../runtime.js' 2 | 3 | runtime.send({ event: `secondary window ${runtime.currentWindow} loaded`, window: 0 }) 4 | 5 | document.querySelector('body > h1').textContent += ` ${runtime.currentWindow}` 6 | 7 | window.addEventListener('character', e => { 8 | runtime.send({ event: `message from secondary window ${runtime.currentWindow}`, value: e.detail, window: 0 }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/src/frontend/index_second_window2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Second window 2 6 | 7 | 8 |

window 1, new html

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/src/fs.js: -------------------------------------------------------------------------------- 1 | import './fs/index.js' 2 | import './fs/promises.js' 3 | import './fs/flags.js' 4 | -------------------------------------------------------------------------------- /test/src/fs/binding.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/binding.js -------------------------------------------------------------------------------- /test/src/fs/constants.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/constants.js -------------------------------------------------------------------------------- /test/src/fs/dir.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/dir.js -------------------------------------------------------------------------------- /test/src/fs/fds.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/fds.js -------------------------------------------------------------------------------- /test/src/fs/flags.js: -------------------------------------------------------------------------------- 1 | import { normalizeFlags } from '../../../fs/flags.js' 2 | import { test } from '@socketsupply/tapzero' 3 | import * as fs from '../../../fs.js' 4 | 5 | test('flags', (t) => { 6 | t.ok( 7 | normalizeFlags() === fs.constants.O_RDONLY, 8 | 'undefined === fs.constants.O_RDONLY' 9 | ) 10 | 11 | t.ok( 12 | normalizeFlags(fs.constants.O_WRONLY) === fs.constants.O_WRONLY, 13 | 'fs.constants.O_WRONLY=== fs.constants.O_WRONLY' 14 | ) 15 | 16 | t.throws( 17 | () => normalizeFlags(null), 18 | // eslint-disable-next-line prefer-regex-literals 19 | RegExp('Expecting flags to be a string or number: Got object'), 20 | 'normalizeFlags() throws on null' 21 | ) 22 | 23 | t.throws(() => 24 | normalizeFlags({}), 25 | // eslint-disable-next-line prefer-regex-literals 26 | RegExp('Expecting flags to be a string or number: Got object'), 27 | 'normalizeFlags() throws on object' 28 | ) 29 | 30 | t.throws( 31 | () => normalizeFlags(true), 32 | // eslint-disable-next-line prefer-regex-literals 33 | RegExp('Expecting flags to be a string or number: Got boolean'), 34 | 'normalizeFlags() throws on boolean' 35 | ) 36 | 37 | t.ok( 38 | normalizeFlags('r') === fs.constants.O_RDONLY, 39 | 'r === fs.constants.O_RDONLY' 40 | ) 41 | 42 | t.ok( 43 | normalizeFlags('rs') === fs.constants.O_RDONLY | fs.constants.O_SYNC, 44 | 'rs === fs.constants.O_RDONLY | fs.constants.O_SYNC' 45 | ) 46 | 47 | t.ok( 48 | normalizeFlags('sr') === fs.constants.O_RDONLY | fs.constants.O_SYNC, 'sr === fs.constants.O_RDONLY | fs.constants.O_SYNC') 49 | 50 | t.ok( 51 | normalizeFlags('r+') === fs.constants.O_RDWR, 52 | 'r+ === fs.constants.O_RDWR' 53 | ) 54 | 55 | t.ok( 56 | normalizeFlags('rs+') === fs.constants.O_RDWR | fs.constants.O_SYNC, 57 | 'rs+ === fs.constants.O_RDWR | fs.constants.O_SYNC' 58 | ) 59 | 60 | t.ok( 61 | normalizeFlags('sr+') === fs.constants.O_RDWR | fs.constants.O_SYNC, 62 | 'sr+ === fs.constants.O_RDWR | fs.constants.O_SYNC' 63 | ) 64 | 65 | t.ok( 66 | normalizeFlags('w') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY, 67 | 'w === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY' 68 | ) 69 | 70 | t.ok( 71 | normalizeFlags('wx') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, 72 | 'wx === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' 73 | ) 74 | 75 | t.ok( 76 | normalizeFlags('xw') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, 77 | 'xw === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' 78 | ) 79 | 80 | t.ok( 81 | normalizeFlags('w+') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR, 82 | 'w+ === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR' 83 | ) 84 | 85 | t.ok( 86 | normalizeFlags('wx+') === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, 87 | 'wx+ === fs.constants.O_TRUNC | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' 88 | ) 89 | t.ok( 90 | 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') 91 | 92 | t.ok( 93 | normalizeFlags('a') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, 94 | 'a === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY' 95 | ) 96 | 97 | t.ok( 98 | normalizeFlags('ax') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, 99 | 'ax === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' 100 | ) 101 | 102 | t.ok( 103 | normalizeFlags('xa') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL, 104 | 'xa === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL' 105 | ) 106 | 107 | t.ok( 108 | normalizeFlags('as') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC, 109 | 'as === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC' 110 | ) 111 | 112 | t.ok( 113 | normalizeFlags('sa') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC, 114 | 'sa === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_SYNC' 115 | ) 116 | 117 | t.ok( 118 | normalizeFlags('a+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR, 119 | 'a+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR' 120 | ) 121 | 122 | t.ok( 123 | normalizeFlags('ax+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, 124 | 'ax+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' 125 | ) 126 | 127 | t.ok( 128 | normalizeFlags('xa+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL, 129 | 'xa+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_EXCL' 130 | ) 131 | 132 | t.ok( 133 | normalizeFlags('as+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC, 134 | 'as+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC' 135 | ) 136 | 137 | t.ok( 138 | normalizeFlags('sa+') === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC, 139 | 'sa+ === fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_SYNC' 140 | ) 141 | }) 142 | -------------------------------------------------------------------------------- /test/src/fs/handle.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/handle.js -------------------------------------------------------------------------------- /test/src/fs/index.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from '../../../buffer.js' 2 | // import console from '../../../console.js' 3 | import crypto from '../../../crypto.js' 4 | import path from '../../../path.js' 5 | import fs from '../../../fs.js' 6 | import os from '../../../os.js' 7 | 8 | import deepEqual from '@socketsupply/tapzero/fast-deep-equal.js' 9 | import { test } from '@socketsupply/tapzero' 10 | 11 | const TMPDIR = `${os.tmpdir()}${path.sep}` 12 | const FIXTURES = /android/i.test(os.platform()) 13 | ? '/data/local/tmp/ssc-socket-test-fixtures/' 14 | : `${TMPDIR}ssc-socket-test-fixtures${path.sep}` 15 | 16 | // node compat 17 | /* 18 | import crypto from 'node:crypto' 19 | import fs from 'node:fs' 20 | import os from 'node:os' 21 | */ 22 | test('fs.access', async (t) => { 23 | await new Promise((resolve, reject) => { 24 | fs.access(FIXTURES, fs.constants.F_OK, (err) => { 25 | if (err) t.fail(err, '(F_OK) fixtures/ is not accessible') 26 | else t.ok(true, '(F_OK) fixtures/ directory is accessible') 27 | resolve() 28 | }) 29 | }) 30 | 31 | await new Promise((resolve, reject) => { 32 | fs.access(FIXTURES, fs.constants.F_OK | fs.constants.R_OK, (err) => { 33 | if (err) t.fail(err, '(F_OK | R_OK) fixtures/ directory is not readable') 34 | else t.ok(true, '(F_OK | R_OK) fixtures/ directory is readable') 35 | resolve() 36 | }) 37 | }) 38 | 39 | await new Promise((resolve, reject) => { 40 | fs.access('.', fs.constants.W_OK, (err) => { 41 | if (err) t.fail(err, '(W_OK) ./ directory is not writable') 42 | else t.ok(true, '(W_OK) ./ directory is writable') 43 | resolve() 44 | }) 45 | }) 46 | 47 | await new Promise((resolve, reject) => { 48 | fs.access(FIXTURES, fs.constants.X_OK, (err) => { 49 | if (err) t.fail(err, '(X_OK) fixtures/ directory is not "executable" - cannot list items') 50 | else t.ok(true, '(X_OK) fixtures/ directory is "executable" - can list items') 51 | resolve() 52 | }) 53 | }) 54 | }) 55 | 56 | test('fs.appendFile', async (t) => {}) 57 | test('fs.chmod', async (t) => { 58 | await new Promise((resolve, reject) => { 59 | fs.chmod(FIXTURES + 'file.txt', 0o777, (err) => { 60 | if (err) t.fail(err) 61 | fs.stat(FIXTURES + 'file.txt', (err, stats) => { 62 | if (err) t.fail(err) 63 | t.equal(stats.mode & 0o777, 0o777, 'file.txt mode is 777') 64 | resolve() 65 | }) 66 | }) 67 | }) 68 | }) 69 | test('fs.chown', async (t) => {}) 70 | test('fs.close', async (t) => { 71 | if (os.platform() === 'android') { 72 | t.comment('FIXME for Android') 73 | return 74 | } 75 | 76 | await new Promise((resolve, reject) => { 77 | fs.open(FIXTURES + 'file.txt', (err, fd) => { 78 | if (err) { 79 | t.fail(err) 80 | return resolve() 81 | } 82 | 83 | t.ok(Number.isFinite(fd), 'isFinite(fd)') 84 | fs.close(fd, (err) => { 85 | if (err) t.fail(err) 86 | 87 | t.ok(!err, 'fd closed') 88 | resolve() 89 | }) 90 | }) 91 | }) 92 | }) 93 | 94 | test('fs.copyFile', async (t) => {}) 95 | test('fs.createReadStream', async (t) => { 96 | if (os.platform() === 'android') { 97 | t.comment('FIXME for Android') 98 | return 99 | } 100 | 101 | const buffers = [] 102 | await new Promise((resolve, reject) => { 103 | const stream = fs.createReadStream(FIXTURES + 'file.txt') 104 | const expected = Buffer.from('test 123') 105 | 106 | stream.on('close', resolve) 107 | stream.on('data', (buffer) => { 108 | buffers.push(buffer) 109 | }) 110 | 111 | stream.on('error', (err) => { 112 | if (err) t.fail(err) 113 | resolve() 114 | }) 115 | 116 | stream.on('end', () => { 117 | let actual = Buffer.concat(buffers) 118 | if (actual[actual.length - 1] === 0x0A) { 119 | actual = actual.slice(0, -1) 120 | } 121 | 122 | t.ok( 123 | Buffer.compare(expected, actual) === 0, 124 | `fixtures/file.txt contents match "${expected}"` 125 | ) 126 | }) 127 | }) 128 | }) 129 | 130 | test('fs.createWriteStream', async (t) => { 131 | if (os.platform() === 'android') return t.comment('TODO') 132 | const writer = fs.createWriteStream(TMPDIR + 'new-file.txt') 133 | const bytes = crypto.randomBytes(32 * 1024 * 1024) 134 | writer.write(bytes.slice(0, 512 * 1024)) 135 | writer.write(bytes.slice(512 * 1024)) 136 | writer.end() 137 | await new Promise((resolve) => { 138 | writer.once('error', (err) => { 139 | t.fail(err.message) 140 | writer.removeAllListeners() 141 | resolve() 142 | }) 143 | writer.once('close', () => { 144 | const reader = fs.createReadStream(TMPDIR + 'new-file.txt') 145 | const buffers = [] 146 | reader.on('data', (buffer) => buffers.push(buffer)) 147 | reader.on('end', () => { 148 | t.ok(Buffer.compare(bytes, Buffer.concat(buffers)) === 0, 'bytes match') 149 | resolve() 150 | }) 151 | }) 152 | }) 153 | }) 154 | 155 | test('fs.fstat', async (t) => {}) 156 | test('fs.lchmod', async (t) => {}) 157 | test('fs.lchown', async (t) => {}) 158 | test('fs.lutimes', async (t) => {}) 159 | test('fs.link', async (t) => {}) 160 | test('fs.lstat', async (t) => {}) 161 | test('fs.mkdir', async (t) => { 162 | const dirname = FIXTURES + Math.random().toString(16).slice(2) 163 | await new Promise((resolve, reject) => { 164 | fs.mkdir(dirname, {}, (err) => { 165 | if (err) reject(err) 166 | 167 | fs.stat(dirname, (err) => { 168 | if (err) reject(err) 169 | resolve() 170 | }) 171 | }) 172 | }) 173 | }) 174 | test('fs.open', async (t) => {}) 175 | test('fs.opendir', async (t) => {}) 176 | test('fs.read', async (t) => {}) 177 | test('fs.readdir', async (t) => {}) 178 | test('fs.readFile', async (t) => { 179 | let failed = false 180 | const iterations = 16 // generate ~1k _concurrent_ requests 181 | const expected = { data: 'test 123' } 182 | const promises = Array.from(Array(iterations), (_, i) => new Promise((resolve) => { 183 | if (failed) return resolve(false) 184 | fs.readFile(FIXTURES + 'file.json', (err, buf) => { 185 | if (failed) return resolve(false) 186 | 187 | const message = `fs.readFile('fixtures/file.json') [iteration=${i + 1}]` 188 | 189 | try { 190 | if (err) { 191 | t.fail(err, message) 192 | failed = true 193 | } else if (!deepEqual(expected, JSON.parse(buf))) { 194 | failed = true 195 | } 196 | } catch (err) { 197 | t.fail(err, message) 198 | failed = true 199 | } 200 | 201 | resolve(!failed) 202 | }) 203 | })) 204 | 205 | const results = await Promise.all(promises) 206 | t.ok(results.every(Boolean), 'fs.readFile(\'fixtures/file.json\')') 207 | }) 208 | 209 | test('fs.readlink', async (t) => {}) 210 | test('fs.realpath', async (t) => {}) 211 | test('fs.rename', async (t) => {}) 212 | test('fs.rmdir', async (t) => {}) 213 | test('fs.rm', async (t) => {}) 214 | test('fs.stat', async (t) => {}) 215 | test('fs.symlink', async (t) => {}) 216 | test('fs.truncate', async (t) => {}) 217 | test('fs.unlink', async (t) => {}) 218 | test('fs.utimes', async (t) => {}) 219 | test('fs.watch', async (t) => {}) 220 | test('fs.write', async (t) => {}) 221 | test('fs.writeFile', async (t) => { 222 | if (os.platform() === 'android') return t.comment('TODO') 223 | const alloc = (size) => crypto.randomBytes(size) 224 | const small = Array.from({ length: 32 }, (_, i) => i * 2 * 1024).map(alloc) 225 | const large = Array.from({ length: 16 }, (_, i) => i * 2 * 1024 * 1024).map(alloc) 226 | const buffers = [...small, ...large] 227 | 228 | // const pending = buffers.length 229 | let failed = false 230 | const writes = [] 231 | 232 | // const now = Date.now() 233 | while (!failed && buffers.length) { 234 | writes.push(testWrite(buffers.length - 1, buffers.pop())) 235 | } 236 | 237 | await Promise.all(writes) 238 | /* 239 | console.log( 240 | '%d writes to %sms to write %s bytes', 241 | small.length + large.length, 242 | Date.now() - now, 243 | [...small, ...large].reduce((n, a) => n + a.length, 0) 244 | ) */ 245 | 246 | t.ok(!failed, 'all bytes match') 247 | 248 | async function testWrite (i, buffer) { 249 | await new Promise((resolve) => { 250 | const filename = TMPDIR + `new-file-${i}.txt` 251 | fs.writeFile(filename, buffer, async (err) => { 252 | if (err) { 253 | failed = true 254 | t.fail(err.message) 255 | return resolve() 256 | } 257 | 258 | fs.readFile(filename, (err, result) => { 259 | if (err) { 260 | failed = true 261 | t.fail(err.message) 262 | } else if (Buffer.compare(result, buffer) !== 0) { 263 | failed = true 264 | t.fail('bytes do not match') 265 | } 266 | 267 | resolve() 268 | }) 269 | }) 270 | }) 271 | } 272 | }) 273 | 274 | test('fs.writev', async (t) => {}) 275 | -------------------------------------------------------------------------------- /test/src/fs/promises.js: -------------------------------------------------------------------------------- 1 | import fs from '../../../fs/promises.js' 2 | import os from '../../../os.js' 3 | import path from '../../../path.js' 4 | import Buffer from '../../../buffer.js' 5 | 6 | import { test } from '@socketsupply/tapzero' 7 | import { FileHandle } from '../../../fs/handle.js' 8 | import { Dir } from '../../../fs/dir.js' 9 | 10 | const TMPDIR = `${os.tmpdir()}${path.sep}` 11 | const FIXTURES = /android/i.test(os.platform()) 12 | ? '/data/local/tmp/ssc-socket-test-fixtures/' 13 | : `${TMPDIR}ssc-socket-test-fixtures${path.sep}` 14 | 15 | test('fs.promises.access', async (t) => { 16 | let access = await fs.access(FIXTURES, fs.constants.F_OK) 17 | t.equal(access, true, '(F_OK) fixtures/ directory is accessible') 18 | 19 | access = await fs.access(FIXTURES, fs.constants.F_OK | fs.constants.R_OK) 20 | t.equal(access, true, '(F_OK | R_OK) fixtures/ directory is readable') 21 | 22 | access = await fs.access('.', fs.constants.W_OK) 23 | t.equal(access, true, '(W_OK) ./ directory is writable') 24 | 25 | access = await fs.access(FIXTURES, fs.constants.X_OK) 26 | t.equal(access, true, '(X_OK) fixtures/ directory is "executable" - can list items') 27 | }) 28 | 29 | test('fs.promises.chmod', async (t) => { 30 | const chmod = await fs.chmod(FIXTURES + 'file.txt', 0o777) 31 | t.equal(chmod, undefined, 'file.txt is chmod 777') 32 | }) 33 | 34 | test('fs.promises.mkdir', async (t) => { 35 | const dirname = FIXTURES + Math.random().toString(16).slice(2) 36 | const { err } = fs.mkdir(dirname, {}) 37 | t.equal(err, undefined, 'mkdir does not throw') 38 | }) 39 | 40 | test('fs.promises.open', async (t) => { 41 | const fd = await fs.open(FIXTURES + 'file.txt', 'r') 42 | t.ok(fd instanceof FileHandle, 'FileHandle is returned') 43 | await fd.close() 44 | }) 45 | 46 | test('fs.promises.opendir', async (t) => { 47 | const dir = await fs.opendir(FIXTURES + 'directory') 48 | t.ok(dir instanceof Dir, 'fs.Dir is returned') 49 | await dir.close() 50 | }) 51 | 52 | test('fs.promises.readdir', async (t) => { 53 | const files = await fs.readdir(FIXTURES + 'directory') 54 | t.ok(Array.isArray(files), 'array is returned') 55 | t.equal(files.length, 6, 'array contains 2 items') 56 | t.deepEqual(files.map(file => file.name), ['0', '1', '2', 'a', 'b', 'c'].map(name => `${name}.txt`), 'array contains files') 57 | }) 58 | 59 | test('fs.promises.readFile', async (t) => { 60 | const data = await fs.readFile(FIXTURES + 'file.txt') 61 | t.ok(Buffer.isBuffer(data), 'buffer is returned') 62 | t.equal(data.toString(), 'test 123\n', 'buffer contains file contents') 63 | }) 64 | 65 | test('fs.promises.stat', async (t) => { 66 | let stats = await fs.stat(FIXTURES + 'file.txt') 67 | t.ok(stats, 'stats are returned') 68 | t.equal(stats.isFile(), true, 'stats are for a file') 69 | t.equal(stats.isDirectory(), false, 'stats are not for a directory') 70 | t.equal(stats.isSymbolicLink(), false, 'stats are not for a symbolic link') 71 | t.equal(stats.isSocket(), false, 'stats are not for a socket') 72 | t.equal(stats.isFIFO(), false, 'stats are not for a FIFO') 73 | t.equal(stats.isBlockDevice(), false, 'stats are not for a block device') 74 | t.equal(stats.isCharacterDevice(), false, 'stats are not for a character device') 75 | 76 | stats = await fs.stat(FIXTURES + 'directory') 77 | t.ok(stats, 'stats are returned') 78 | t.equal(stats.isFile(), false, 'stats are not for a file') 79 | t.equal(stats.isDirectory(), true, 'stats are for a directory') 80 | t.equal(stats.isSymbolicLink(), false, 'stats are not for a symbolic link') 81 | t.equal(stats.isSocket(), false, 'stats are not for a socket') 82 | t.equal(stats.isFIFO(), false, 'stats are not for a FIFO') 83 | t.equal(stats.isBlockDevice(), false, 'stats are not for a block device') 84 | t.equal(stats.isCharacterDevice(), false, 'stats are not for a character device') 85 | }) 86 | 87 | test('fs.promises.writeFile', async (t) => { 88 | const file = FIXTURES + 'write-file.txt' 89 | const data = 'test 123\n' 90 | await fs.writeFile(file, data) 91 | const contents = await fs.readFile(file) 92 | t.equal(contents.toString(), data, 'file contents are correct') 93 | }) 94 | -------------------------------------------------------------------------------- /test/src/fs/stats.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/stats.js -------------------------------------------------------------------------------- /test/src/fs/stream.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketsupply/socket-api/0e4cbbdf5dcce5a487a14e7dd08bbce7c90aa735/test/src/fs/stream.js -------------------------------------------------------------------------------- /test/src/index.js: -------------------------------------------------------------------------------- 1 | import './test-context.js' // this should be first 2 | 3 | // @TODO(jwerle): tapzero needs a `t.plan()` so we know exactly how to 4 | // expect the total pass count 5 | 6 | import './ipc.js' 7 | import './os.js' 8 | import './process.js' 9 | import './path.js' 10 | import './fs.js' 11 | import './dgram.js' 12 | import './dns.js' 13 | import './runtime.js' 14 | import './backend.js' 15 | import './crypto.js' 16 | import './util.js' 17 | -------------------------------------------------------------------------------- /test/src/ipc.js: -------------------------------------------------------------------------------- 1 | import * as ipc from '../../ipc.js' 2 | import { test } from '@socketsupply/tapzero' 3 | import { Buffer } from '../../buffer.js' 4 | 5 | // node compat 6 | // import { Buffer } from 'node:buffer' 7 | // import './test-context.js' 8 | 9 | test('ipc exports', async (t) => { 10 | t.deepEqual(Object.keys(ipc).sort(), [ 11 | 'OK', 12 | 'Result', 13 | 'TIMEOUT', 14 | 'createBinding', 15 | 'debug', 16 | 'default', 17 | 'emit', 18 | 'ERROR', 19 | 'kDebugEnabled', 20 | 'Message', 21 | 'parseSeq', 22 | 'postMessage', 23 | 'ready', 24 | 'request', 25 | 'resolve', 26 | 'send', 27 | 'sendSync', 28 | 'write' 29 | ].sort()) 30 | 31 | try { 32 | await ipc.ready() 33 | } catch (err) { 34 | t.fail(err) 35 | } 36 | }) 37 | 38 | test('ipc constants', (t) => { 39 | t.equal(ipc.OK, 0) 40 | t.equal(ipc.ERROR, 1) 41 | t.equal(ipc.TIMEOUT, 32000) 42 | t.equal(ipc.kDebugEnabled, Symbol.for('ipc.debug.enabled')) 43 | }) 44 | 45 | test('ipc.debug', (t) => { 46 | ipc.debug(true) 47 | t.equal(ipc.debug.enabled, true) 48 | ipc.debug(false) 49 | t.equal(ipc.debug.enabled, false) 50 | ipc.debug(true) 51 | }) 52 | 53 | test('ipc.Message', (t) => { 54 | t.ok(ipc.Message.prototype instanceof URL, 'is a URL') 55 | // pass a Buffer 56 | let msg = ipc.Message.from(Buffer.from('test'), { foo: 'bar' }) 57 | t.equal(msg.protocol, ipc.Message.PROTOCOL) 58 | t.equal(msg.command, 'test') 59 | t.deepEqual(msg.params, { foo: 'bar' }) 60 | // pass an ipc.Message 61 | msg = ipc.Message.from(msg) 62 | t.equal(msg.protocol, ipc.Message.PROTOCOL) 63 | t.equal(msg.command, 'test') 64 | t.deepEqual(msg.params, { foo: 'bar' }) 65 | // pass an object 66 | msg = ipc.Message.from({ protocol: ipc.Message.PROTOCOL, command: 'test' }, { foo: 'bar' }) 67 | t.equal(msg.protocol, ipc.Message.PROTOCOL) 68 | t.equal(msg.command, 'test') 69 | t.deepEqual(msg.params, { foo: 'bar' }) 70 | // pass a string 71 | msg = ipc.Message.from('test', { foo: 'bar' }) 72 | t.equal(msg.protocol, ipc.Message.PROTOCOL) 73 | t.equal(msg.command, 'test') 74 | t.deepEqual(msg.params, { foo: 'bar' }) 75 | t.ok(ipc.Message.isValidInput(`${ipc.Message.PROTOCOL}//test`), 'is valid input') 76 | t.ok(!ipc.Message.isValidInput('test'), 'is valid input') 77 | t.ok(!ipc.Message.isValidInput('foo://test'), 'is valid input') 78 | }) 79 | 80 | if (window.__args.os !== 'ios' && window.__args.os !== 'android') { 81 | test('ipc.sendSync not found', (t) => { 82 | const response = ipc.sendSync('test', { foo: 'bar' }) 83 | t.ok(response instanceof ipc.Result) 84 | const { err } = response 85 | t.equal(err?.toString(), 'NotFoundError: Not found') 86 | t.equal(err?.name, 'NotFoundError') 87 | t.equal(err?.message, 'Not found') 88 | t.ok(err?.url.startsWith(`${ipc.Message.PROTOCOL}//test?foo=bar&index=0&seq=R`)) 89 | t.equal(err?.code, 'NOT_FOUND_ERR') 90 | }) 91 | 92 | test('ipc.sendSync success', (t) => { 93 | const response = ipc.sendSync('os.arch') 94 | t.ok(response instanceof ipc.Result) 95 | const { data } = response 96 | t.ok(['x86_64', 'arm64'].includes(data)) 97 | }) 98 | 99 | // 100 | // TODO: ipc.send error should match ipc.sendSync error 101 | // 102 | test('ipc.send not found', async (t) => { 103 | const response = await ipc.send('test', { foo: 'bar' }) 104 | t.ok(response instanceof ipc.Result, 'response is an ipc.Result') 105 | t.ok(response.err instanceof Error, 'response.err is an Error') 106 | t.equal(response.err.toString(), 'Error: unsupported IPC message: test') 107 | t.equal(response.err.name, 'Error') 108 | t.equal(response.err.message, 'unsupported IPC message: test') 109 | }) 110 | 111 | test('ipc.send success', async (t) => { 112 | const response = await ipc.send('os.arch') 113 | t.ok(response instanceof ipc.Result) 114 | const { data } = response 115 | t.ok(['x86_64', 'arm64'].includes(data)) 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /test/src/os.js: -------------------------------------------------------------------------------- 1 | import * as os from '../../os.js' 2 | import { test } from '@socketsupply/tapzero' 3 | 4 | const archs = ['arm64', 'ia32', 'x64', 'unknown'] 5 | const platforms = ['android', 'cygwin', 'freebsd', 'linux', 'darwin', 'ios', 'openbsd', 'win32', 'unknown'] 6 | const types = ['CYGWIN_NT', 'Mac', 'Darwin', 'FreeBSD', 'Linux', 'OpenBSD', 'Windows_NT', 'Unknown'] 7 | 8 | test('os.arch()', (t) => { 9 | t.ok(archs.includes(os.arch()), 'os.arch() value is valid') 10 | }) 11 | 12 | test('os.platform()', (t) => { 13 | t.ok(platforms.includes(os.platform()), 'os.platform()') 14 | }) 15 | 16 | test('os.type()', (t) => { 17 | t.ok(types.includes(os.type()), 'os.type()') 18 | }) 19 | 20 | test('os.networkInterfaces()', (t) => { 21 | function isValidAddress (address) { 22 | return Object.keys(address).toString() === ['address', 'netmask', 'internal', 'family', 'cidr', 'mac'].toString() 23 | } 24 | 25 | const networkInterfaces = os.networkInterfaces() 26 | t.ok(Array.isArray(networkInterfaces.lo) || Array.isArray(networkInterfaces.lo0), 'iterface is "lo"') 27 | const interfaces = Object.values(networkInterfaces) 28 | t.ok(interfaces.length >= 2, 'network interfaces has at least two keys, loopback + wifi, was:' + interfaces.length) 29 | t.ok(interfaces.every(addresses => Array.isArray(addresses)), 'network interface is an array') 30 | t.ok( 31 | interfaces.every(addresses => addresses.every(isValidAddress)), 32 | 'all network interfaces addresses has correct keys' 33 | ) 34 | }) 35 | 36 | test('os.EOL', (t) => { 37 | if (/windows/i.test(os.type())) { 38 | t.equal(os.EOL, '\r\n') 39 | } else { 40 | t.equal(os.EOL, '\n') 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /test/src/path.js: -------------------------------------------------------------------------------- 1 | import path from '../../path.js' 2 | import process from '../../process.js' 3 | import os from '../../os.js' 4 | 5 | import { test } from '@socketsupply/tapzero' 6 | 7 | test('path', (t) => { 8 | const isUnix = os.platform() !== 'win32' 9 | t.ok(path.posix, 'path.posix exports') 10 | t.ok(path.win32, 'path.win32 exports') 11 | const expectedSep = isUnix ? '/' : '\\' 12 | t.equal(path.sep, expectedSep, 'path.sep is correct') 13 | const expectedDelimiter = isUnix ? ':' : ';' 14 | t.equal(path.delimiter, expectedDelimiter, 'path.delimiter is correct') 15 | const cwd = isUnix ? process.cwd() : process.cwd().replace(/\\/g, '/') 16 | t.equal(path.cwd(), cwd, 'path.cwd() returns the current working directory') 17 | }) 18 | -------------------------------------------------------------------------------- /test/src/process.js: -------------------------------------------------------------------------------- 1 | import { test } from '@socketsupply/tapzero' 2 | import process from '../../process.js' 3 | import path from 'path-browserify' 4 | 5 | test('process', (t) => { 6 | t.ok(typeof process.addListener === 'function', 'process is an EventEmitter') 7 | }) 8 | 9 | test('process.homedir()', (t) => { 10 | t.ok(typeof process.homedir() === 'string', 'process.homedir() returns a string') 11 | }) 12 | 13 | test('process.exit()', (t) => { 14 | t.ok(typeof process.exit === 'function', 'process.exit() is a function') 15 | }) 16 | 17 | test('process.cwd', async (t) => { 18 | t.ok(typeof process.cwd() === 'string', 'process.cwd() returns a string') 19 | if (process.platform === 'mac') { 20 | t.equal(process.cwd(), path.resolve(process.argv0, '../../Resources'), 'process.cwd() returns a correct value') 21 | } else if (process.platform === 'linux') { 22 | t.equal(process.cwd(), path.resolve(process.argv0, '../../socketsupply-socket-tests'), 'process.cwd() returns a correct value') 23 | } else if (process.platform === 'android') { 24 | t.ok(process.cwd(), 'process.cwd() returns a correct value') 25 | } else { 26 | // TODO: iOS, Windows 27 | t.fail(`FIXME: not implemented for platform ${process.platform}`) 28 | } 29 | }) 30 | 31 | test('process.arch', (t) => { 32 | t.ok(['x86_64', 'arm64'].includes(process.arch), 'process.arch is correct') 33 | t.equal(process.arch, window.__args.arch, 'process.arch equals window.__args.arch') 34 | }) 35 | 36 | test('process.platform', (t) => { 37 | t.ok(typeof process.platform === 'string', 'process.platform returns an string') 38 | t.ok(['mac', 'linux', 'android', 'ios', 'win'].includes(process.platform), 'process.platform is correct') 39 | t.equal(process.platform, window.__args.os, 'process.platform equals window.__args.platform') 40 | t.equal(process.platform, process.os, 'process.platform returns the same value as process.os') 41 | }) 42 | 43 | test('process.env', (t) => { 44 | t.deepEqual(process.env, window.__args.env, 'process.env is equal to window.__args.env') 45 | }) 46 | 47 | test('process.argv', (t) => { 48 | t.deepEqual(process.argv, window.__args.argv, 'process.argv is equal to window.__args.argv') 49 | }) 50 | -------------------------------------------------------------------------------- /test/src/runtime.js: -------------------------------------------------------------------------------- 1 | import runtime from '../../runtime.js' 2 | import { readFile } from '../../fs/promises.js' 3 | import ipc from '../../ipc.js' 4 | import { test } from '@socketsupply/tapzero' 5 | 6 | // Desktop-only runtime functions 7 | if (window.__args.os !== 'android' && window.__args.os !== 'ios') { 8 | // Polyfills 9 | test('window.resizeTo', async (t) => { 10 | t.equal(typeof window.resizeTo, 'function', 'window.resizeTo is a function') 11 | t.ok(window.resizeTo(420, 200), 'succesfully completes') 12 | const { data: { width, height } } = await ipc.send('window.getSize') 13 | t.equal(width, 420, 'width is 420') 14 | t.equal(height, 200, 'height is 200') 15 | }) 16 | 17 | test('window.document.title', async (t) => { 18 | window.document.title = 'idkfa' 19 | t.equal(window.document.title, 'idkfa', 'window.document.title is has been changed') 20 | t.notEqual(window.__args.title, window.document.title, 'window.__args.title is not changed') 21 | const { data: { title } } = await ipc.send('window.getTitle') 22 | t.equal(title, 'idkfa', 'window title is correct') 23 | }) 24 | 25 | // Other runtime tests 26 | 27 | test('currentWindow', (t) => { 28 | t.equal(runtime.currentWindow, window.__args.index, 'runtime.currentWindow equals window.__args.index') 29 | t.equal(runtime.currentWindow, 0, 'runtime.currentWindow equals 0') 30 | t.throws(() => { runtime.currentWindow = 1 }, 'runtime.currentWindow is immutable') 31 | }) 32 | 33 | test('debug', (t) => { 34 | t.equal(runtime.debug, window.__args.debug, 'debug is correct') 35 | t.throws(() => { runtime.debug = 1 }, 'debug is immutable') 36 | }) 37 | 38 | test('config', async (t) => { 39 | const rawConfig = await readFile('socket.ini', 'utf8') 40 | let prefix = '' 41 | const lines = rawConfig.split('\n') 42 | const config = [] 43 | for (let line of lines) { 44 | line = line.trim() 45 | if (line.length === 0 || line.startsWith(';')) continue 46 | if (line.startsWith('[') && line.endsWith(']')) { 47 | prefix = line.slice(1, -1) 48 | continue 49 | } 50 | let [key, value] = line.split('=') 51 | key = key.trim() 52 | value = value.trim().replace(/"/g, '') 53 | config.push([prefix.length === 0 ? key : prefix + '_' + key, value]) 54 | } 55 | config.filter(([key]) => key !== 'headless' && key !== 'name').forEach(([key, value]) => { 56 | t.equal(runtime.config[key], value, `runtime.config.${key} is correct`) 57 | t.throws( 58 | () => { runtime.config[key] = 0 }, 59 | // eslint-disable-next-line prefer-regex-literals 60 | RegExp('Attempted to assign to readonly property.'), 61 | `runtime.config.${key} is read-only` 62 | ) 63 | }) 64 | t.equal(runtime.config.headless, true, 'runtime.config.headless is correct') 65 | t.throws( 66 | () => { runtime.config.hedless = 0 }, 67 | // eslint-disable-next-line prefer-regex-literals 68 | RegExp('Attempting to define property on object that is not extensible.'), 69 | 'runtime.config.headless is read-only' 70 | ) 71 | t.ok(runtime.config.name.startsWith(config.find(([key]) => key === 'name')[1]), 'runtime.config.name is correct') 72 | t.throws( 73 | () => { runtime.config.name = 0 }, 74 | // eslint-disable-next-line prefer-regex-literals 75 | RegExp('Attempted to assign to readonly property.'), 76 | 'runtime.config.name is read-only' 77 | ) 78 | }) 79 | 80 | test('setTitle', async (t) => { 81 | t.equal(typeof runtime.setTitle, 'function', 'setTitle is a function') 82 | const result = await runtime.setTitle('test') 83 | t.equal(result.err, null, 'succesfully completes') 84 | }) 85 | 86 | // TODO: it crashes the app 87 | test('inspect', async (t) => { 88 | t.equal(typeof runtime.inspect, 'function', 'inspect is a function') 89 | }) 90 | 91 | test('show', async (t) => { 92 | t.equal(typeof runtime.show, 'function', 'show is a function') 93 | await runtime.show({ 94 | window: 1, 95 | url: 'index_second_window.html', 96 | title: 'Hello World', 97 | width: 400, 98 | height: 400 99 | }) 100 | const { data: { status, index: i1 } } = await ipc.send('window.getStatus', { window: 1 }) 101 | const { data: { title, index: i2 } } = await ipc.send('window.getTitle', { window: 1 }) 102 | const { data: { height, width, index: i3 } } = await ipc.send('window.getSize', { window: 1 }) 103 | t.equal(status, 31, 'window is shown') 104 | t.equal(title, 'Hello World', 'window title is correct') 105 | t.equal(height, 400, 'window height is correct') 106 | t.equal(width, 400, 'window width is correct') 107 | t.equal(i1, 1, 'window index is correct') 108 | t.equal(i2, 1, 'window index is correct') 109 | t.equal(i3, 1, 'window index is correct') 110 | }) 111 | 112 | test('send', async (t) => { 113 | await runtime.show({ 114 | window: 2, 115 | url: 'index_second_window.html', 116 | title: 'Hello World', 117 | width: 400, 118 | height: 400 119 | }) 120 | // wait for window to load 121 | await Promise.all([ 122 | new Promise(resolve => window.addEventListener('secondary window 1 loaded', resolve, { once: true })), 123 | new Promise(resolve => window.addEventListener('secondary window 2 loaded', resolve, { once: true })) 124 | ]) 125 | t.equal(typeof runtime.send, 'function', 'send is a function') 126 | const value = { firstname: 'Rick', secondname: 'Sanchez' } 127 | runtime.send({ event: 'character', value }) 128 | const [result, pong] = await Promise.all([ 129 | new Promise(resolve => window.addEventListener('message from secondary window 1', e => resolve(e.detail))), 130 | new Promise(resolve => window.addEventListener('message from secondary window 2', e => resolve(e.detail))) 131 | ]) 132 | t.deepEqual(result, value, 'send succeeds') 133 | t.deepEqual(pong, value, 'send back from window 1 succeeds') 134 | }) 135 | 136 | test('getWindows', async (t) => { 137 | const { data: windows } = await runtime.getWindows() 138 | t.ok(Array.isArray(windows), 'windows is an array') 139 | t.ok(windows.length > 0, 'windows is not empty') 140 | t.ok(windows.every(w => Number.isInteger(w)), 'windows are integers') 141 | t.deepEqual(windows, [0, 1, 2], 'windows are correct') 142 | }) 143 | 144 | test('getWindows with props', async (t) => { 145 | await runtime.show() 146 | const { data: windows1 } = await runtime.getWindows({ title: true, size: true, status: true }) 147 | t.ok(Array.isArray(windows1), 'windows is an array') 148 | t.ok(windows1.length > 0, 'windows is not empty') 149 | t.deepEqual(windows1, [ 150 | { 151 | title: 'test', 152 | width: 420, 153 | height: 200, 154 | status: 31, 155 | index: 0 156 | }, 157 | { 158 | title: 'Secondary window', 159 | width: 400, 160 | height: 400, 161 | status: 31, 162 | index: 1 163 | }, 164 | { 165 | title: 'Secondary window', 166 | width: 400, 167 | height: 400, 168 | status: 31, 169 | index: 2 170 | } 171 | ], 'windows are correct') 172 | }) 173 | 174 | test('navigate', async (t) => { 175 | t.equal(typeof runtime.navigate, 'function', 'navigate is a function') 176 | const result = await runtime.navigate({ window: 1, url: 'index_second_window2.html' }) 177 | t.equal(result.err, null, 'navigate succeeds') 178 | }) 179 | 180 | test('hide', async (t) => { 181 | t.equal(typeof runtime.hide, 'function', 'hide is a function') 182 | await runtime.hide({ window: 1 }) 183 | await runtime.hide({ window: 2 }) 184 | const { data: statuses } = await runtime.getWindows({ status: true }) 185 | t.deepEqual(statuses, [ 186 | { 187 | status: 31, 188 | index: 0 189 | }, 190 | { 191 | status: 21, 192 | index: 1 193 | }, 194 | { 195 | status: 21, 196 | index: 2 197 | } 198 | ], 'statuses are correct') 199 | }) 200 | 201 | test('setWindowBackgroundColor', async (t) => { 202 | t.equal(typeof runtime.setWindowBackgroundColor, 'function', 'setWindowBackgroundColor is a function') 203 | const result = await runtime.setWindowBackgroundColor({ red: 0, green: 0, blue: 0, alpha: 0 }) 204 | // TODO: should be result.err === null? 205 | t.equal(result, null, 'setWindowBackgroundColor succeeds') 206 | }) 207 | 208 | // FIXME: it hangs 209 | test('setContextMenu', async (t) => { 210 | t.equal(typeof runtime.setContextMenu, 'function', 'setContextMenu is a function') 211 | // const result = await runtime.setContextMenu({ 'Foo': '', 'Bar': '' }) 212 | // t.equal(result, null, 'setContextMenu succeeds') 213 | }) 214 | 215 | test('setSystemMenuItemEnabled', async (t) => { 216 | t.equal(typeof runtime.setSystemMenuItemEnabled, 'function', 'setSystemMenuItemEnabled is a function') 217 | const result = await runtime.setSystemMenuItemEnabled({ indexMain: 0, indexSub: 0, enabled: true }) 218 | t.equal(result.err, null, 'setSystemMenuItemEnabled succeeds') 219 | }) 220 | 221 | test('setSystemMenu', async (t) => { 222 | t.equal(typeof runtime.setSystemMenu, 'function', 'setSystemMenuItemVisible is a function') 223 | const result = await runtime.setSystemMenu({ 224 | index: 0, value: ` 225 | App: 226 | Foo: f; 227 | Edit: 228 | Cut: x 229 | Copy: c 230 | Paste: v 231 | Delete: _ 232 | Select All: a; 233 | Other: 234 | Apple: _ 235 | Another Test: T 236 | !Im Disabled: I 237 | Some Thing: S + Meta 238 | --- 239 | Bazz: s + Meta, Control, Alt; 240 | ` 241 | }) 242 | t.equal(result.err, null, 'setSystemMenuItemVisible succeeds') 243 | }) 244 | } 245 | 246 | // Common runtime functions 247 | test('openExternal', async (t) => { 248 | t.equal(typeof runtime.openExternal, 'function', 'openExternal is a function') 249 | // can't test results without browser 250 | // t.equal(await runtime.openExternal('https://sockets.sh'), null, 'succesfully completes') 251 | }) 252 | 253 | test('window.showOpenFilePicker', async (t) => { 254 | t.equal(typeof window.showOpenFilePicker, 'function', 'window.showOpenFilePicker is a function') 255 | t.ok(window.showOpenFilePicker()) 256 | }) 257 | 258 | test('window.showSaveFilePicker', (t) => { 259 | t.equal(typeof window.showSaveFilePicker, 'function', 'window.showSaveFilePicker is a function') 260 | t.ok(window.showSaveFilePicker()) 261 | }) 262 | 263 | test('window.showDirectoryFilePicker', (t) => { 264 | t.equal(typeof window.showDirectoryFilePicker, 'function', 'window.showDirectoryFilePicker is a function') 265 | t.ok(window.showDirectoryFilePicker()) 266 | }) 267 | 268 | // TODO: can we improve this test? 269 | test('reload', (t) => { 270 | t.equal(typeof runtime.reload, 'function', 'reload is a function') 271 | }) 272 | 273 | // We don't need to test runtime.exit. It works if the app exits after the tests. 274 | -------------------------------------------------------------------------------- /test/src/test-context.js: -------------------------------------------------------------------------------- 1 | import { GLOBAL_TEST_RUNNER } from '@socketsupply/tapzero' 2 | import console from '../../console.js' 3 | import process from '../../process.js' 4 | import '../../runtime.js' 5 | 6 | // uncomment below to get IPC debug output in stdout 7 | // import ipc from '../../ipc.js' 8 | // ipc.debug.enabled = true 9 | // ipc.debug.log = (...args) => console.log(...args) 10 | 11 | if (typeof globalThis?.addEventListener === 'function') { 12 | globalThis.addEventListener('error', onerror) 13 | globalThis.addEventListener('unhandledrejection', onerror) 14 | } 15 | 16 | GLOBAL_TEST_RUNNER.onFinish(() => { 17 | setTimeout(() => { 18 | process.exit(0) 19 | }, 10) 20 | }) 21 | 22 | function onerror (err) { 23 | console.error(err.stack || err.reason || err.message || err) 24 | process.exit(1) 25 | } 26 | -------------------------------------------------------------------------------- /test/src/util.js: -------------------------------------------------------------------------------- 1 | import util, { hasOwnProperty } from '../../util.js' 2 | import Buffer from '../../buffer.js' 3 | import { test } from '@socketsupply/tapzero' 4 | 5 | test('util.hasOwnProperty', (t) => { 6 | const obj = { foo: 'bar' } 7 | t.ok(hasOwnProperty(obj, 'foo'), 'util.hasOwnProperty returns true for own properties') 8 | t.ok(!hasOwnProperty(obj, 'bar'), 'util.hasOwnProperty returns false for non-own properties') 9 | }) 10 | 11 | test('util.isTypedArray', (t) => { 12 | const arr = new Uint8Array(8) 13 | t.ok(util.isTypedArray(arr), 'util.isTypedArray returns true for typed arrays') 14 | t.ok(!util.isTypedArray([]), 'util.isTypedArray returns false for non-typed arrays') 15 | }) 16 | 17 | test('util.isArrayLike', (t) => { 18 | const arr = new Uint8Array(8) 19 | t.ok(util.isArrayLike(arr), 'util.isArrayLike returns true for typed arrays') 20 | t.ok(util.isArrayLike([]), 'util.isArrayLike returns true for arrays') 21 | t.ok(!util.isArrayLike({}), 'util.isArrayLike returns false for objects') 22 | }) 23 | 24 | test('util.isArrayBufferView', (t) => { 25 | const arr = new Uint8Array(8) 26 | t.ok(util.isArrayBufferView(arr), 'util.isArrayBufferView returns true for typed arrays') 27 | t.ok(!util.isArrayBufferView([]), 'util.isArrayBufferView returns false for arrays') 28 | t.ok(!util.isArrayBufferView({}), 'util.isArrayBufferView returns false for objects') 29 | }) 30 | 31 | test('util.isAsyncFunction', (t) => { 32 | const fn = async () => {} 33 | t.ok(util.isAsyncFunction(fn), 'util.isAsyncFunction returns true for async functions') 34 | t.ok(!util.isAsyncFunction(() => {}), 'util.isAsyncFunction returns false for non-async functions') 35 | }) 36 | 37 | test('util.isArgumentsObject', (t) => { 38 | const args = (function () { return arguments })() 39 | t.ok(util.isArgumentsObject(args), 'util.isArgumentsObject returns true for arguments objects') 40 | // should it? 41 | // t.ok(!util.isArgumentsObject({}), 'util.isArgumentsObject returns false for non-arguments objects') 42 | }) 43 | 44 | test('util.isEmptyObject', (t) => { 45 | const obj = {} 46 | t.ok(util.isEmptyObject(obj), 'util.isEmptyObject returns true for empty objects') 47 | t.ok(!util.isEmptyObject({ foo: 'bar' }), 'util.isEmptyObject returns false for non-empty objects') 48 | }) 49 | 50 | test('util.isObject', (t) => { 51 | const obj = {} 52 | t.ok(util.isObject(obj), 'util.isObject returns true for objects') 53 | // Should it? 54 | // t.ok(!util.isObject([]), 'util.isObject returns false for arrays') 55 | t.ok(!util.isObject('foo'), 'util.isObject returns false for strings') 56 | t.ok(!util.isObject(1), 'util.isObject returns false for numbers') 57 | t.ok(!util.isObject(true), 'util.isObject returns false for booleans') 58 | t.ok(!util.isObject(null), 'util.isObject returns false for null') 59 | t.ok(!util.isObject(undefined), 'util.isObject returns false for undefined') 60 | }) 61 | 62 | test('util.isPlainObject', (t) => { 63 | const obj = {} 64 | t.ok(util.isPlainObject(obj), 'util.isPlainObject returns true for plain objects') 65 | t.ok(!util.isPlainObject([]), 'util.isPlainObject returns false for arrays') 66 | t.ok(!util.isPlainObject('foo'), 'util.isPlainObject returns false for strings') 67 | t.ok(!util.isPlainObject(1), 'util.isPlainObject returns false for numbers') 68 | t.ok(!util.isPlainObject(true), 'util.isPlainObject returns false for booleans') 69 | t.ok(!util.isPlainObject(null), 'util.isPlainObject returns false for null') 70 | t.ok(!util.isPlainObject(undefined), 'util.isPlainObject returns false for undefined') 71 | t.ok(!util.isPlainObject(new Date()), 'util.isPlainObject returns false for Date objects') 72 | }) 73 | 74 | test('util.isBufferLike', (t) => { 75 | const buf = Buffer.from('foo') 76 | t.ok(util.isBufferLike(buf), 'util.isBufferLike returns true for Buffer objects') 77 | t.ok(util.isBufferLike(new Uint8Array(8)), 'util.isBufferLike returns true for Uint8Array objects') 78 | t.ok(!util.isBufferLike('foo'), 'util.isBufferLike returns false for strings') 79 | t.ok(!util.isBufferLike(1), 'util.isBufferLike returns false for numbers') 80 | t.ok(!util.isBufferLike(true), 'util.isBufferLike returns false for booleans') 81 | t.ok(!util.isBufferLike(null), 'util.isBufferLike returns false for null') 82 | t.ok(!util.isBufferLike(undefined), 'util.isBufferLike returns false for undefined') 83 | }) 84 | 85 | test('util.isFunction', (t) => { 86 | const fn = () => {} 87 | t.ok(util.isFunction(fn), 'util.isFunction returns true for functions') 88 | t.ok(!util.isFunction('foo'), 'util.isFunction returns false for strings') 89 | t.ok(!util.isFunction(1), 'util.isFunction returns false for numbers') 90 | t.ok(!util.isFunction(true), 'util.isFunction returns false for booleans') 91 | t.ok(!util.isFunction(null), 'util.isFunction returns false for null') 92 | t.ok(!util.isFunction(undefined), 'util.isFunction returns false for undefined') 93 | t.ok(!util.isFunction({}), 'util.isFunction returns false for objects') 94 | t.ok(!util.isFunction([]), 'util.isFunction returns false for arrays') 95 | 96 | const asyncFn = async () => {} 97 | t.ok(util.isFunction(asyncFn), 'util.isFunction returns true for async functions') 98 | }) 99 | 100 | test('util.isErrorLike', (t) => { 101 | const err = new Error('foo') 102 | t.ok(util.isErrorLike(err), 'util.isErrorLike returns true for Error objects') 103 | t.ok(!util.isErrorLike('foo'), 'util.isErrorLike returns false for strings') 104 | t.ok(!util.isErrorLike(1), 'util.isErrorLike returns false for numbers') 105 | t.ok(!util.isErrorLike(true), 'util.isErrorLike returns false for booleans') 106 | t.ok(!util.isErrorLike(null), 'util.isErrorLike returns false for null') 107 | t.ok(!util.isErrorLike(undefined), 'util.isErrorLike returns false for undefined') 108 | t.ok(!util.isErrorLike({}), 'util.isErrorLike returns false for objects') 109 | t.ok(!util.isErrorLike([]), 'util.isErrorLike returns false for arrays') 110 | const errLike = { message: 'foo', name: 'Error' } 111 | t.ok(util.isErrorLike(errLike), 'util.isErrorLike returns true for objects with message and name properties') 112 | }) 113 | 114 | test('util.isPromiseLike', (t) => { 115 | const promise = Promise.resolve() 116 | t.ok(util.isPromiseLike(promise), 'util.isPromiseLike returns true for Promise objects') 117 | t.ok(!util.isPromiseLike('foo'), 'util.isPromiseLike returns false for strings') 118 | t.ok(!util.isPromiseLike(1), 'util.isPromiseLike returns false for numbers') 119 | t.ok(!util.isPromiseLike(true), 'util.isPromiseLike returns false for booleans') 120 | t.ok(!util.isPromiseLike(null), 'util.isPromiseLike returns false for null') 121 | t.ok(!util.isPromiseLike(undefined), 'util.isPromiseLike returns false for undefined') 122 | t.ok(!util.isPromiseLike({}), 'util.isPromiseLike returns false for objects') 123 | t.ok(!util.isPromiseLike([]), 'util.isPromiseLike returns false for arrays') 124 | const promiseLike = { then: () => {} } 125 | t.ok(util.isPromiseLike(promiseLike), 'util.isPromiseLike returns true for objects with a then method') 126 | const notPromiseLike = { then: 'foo' } 127 | t.ok(!util.isPromiseLike(notPromiseLike), 'util.isPromiseLike returns false for objects with a then property that is not a function') 128 | }) 129 | 130 | test('util.toProperCase', (t) => { 131 | t.equal(util.toProperCase('foo'), 'Foo', 'util.toProperCase returns a string with the first letter capitalized') 132 | t.equal(util.toProperCase('foo bar'), 'Foo bar', 'util.toProperCase returns a string with the first letter capitalized') 133 | t.equal(util.toProperCase('foo bar-baz'), 'Foo bar-baz', 'util.toProperCase returns a string with the first letter capitalized') 134 | }) 135 | 136 | test('util.rand64', (t) => { 137 | const randoms = Array.from({ length: 10 }, _ => util.rand64()) 138 | t.ok(randoms.every(b => typeof b === 'bigint'), 'util.rand64 returns a bigint') 139 | t.ok(randoms.some(b => b !== randoms[9]), 'util.rand64 returns a different bigint each time') 140 | }) 141 | 142 | test('util.splitBuffer', (t) => { 143 | const buf = Buffer.from('foobar') 144 | const [a, b, c] = util.splitBuffer(buf, 2) 145 | t.equal(a.toString(), 'fo', 'util.splitBuffer returns an array of buffers') 146 | t.equal(b.toString(), 'ob', 'util.splitBuffer returns an array of buffers') 147 | t.equal(c.toString(), 'ar', 'util.splitBuffer returns an array of buffers') 148 | }) 149 | 150 | test('util.InvertedPromise', (t) => {}) 151 | 152 | test('util.clamp', (t) => { 153 | t.equal(util.clamp(0, 0, 1), 0, 'util.clamp returns the lower bound if the value is less than the lower bound') 154 | t.equal(util.clamp(1, 0, 1), 1, 'util.clamp returns the upper bound if the value is greater than the upper bound') 155 | t.equal(util.clamp(0.5, 0, 1), 0.5, 'util.clamp returns the value if it is between the lower and upper bounds') 156 | }) 157 | 158 | test('util.promisify', async (t) => { 159 | const fn = (a, b, cb) => cb(null, a + b) 160 | const promisified = util.promisify(fn) 161 | t.ok(util.isFunction(promisified), 'util.promisify returns a function') 162 | const res = await promisified(1, 2) 163 | t.equal(res, 3, 'util.promisify returns a function that returns a promise') 164 | }) 165 | --------------------------------------------------------------------------------