UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

399 lines (314 loc) 12.5 kB
import { Peer, Encryption, sha256 } from './index.js' import { PeerWorkerProxy } from './proxy.js' import { sodium } from '../crypto.js' import { Buffer } from '../buffer.js' import { isBufferLike } from '../util.js' import { Packet, CACHE_TTL } from './packets.js' /** * Initializes and returns the network bus. * * @async * @function * @param {object} options - Configuration options for the network bus. * @param {object} events - A nodejs compatibe implementation of the events module. * @param {object} dgram - A nodejs compatible implementation of the dgram module. * @returns {Promise<events.EventEmitter>} - A promise that resolves to the initialized network bus. */ async function api (options = {}, events, dgram) { await sodium.ready const bus = new events.EventEmitter() bus._on = bus.on bus._once = bus.once bus._emit = bus.emit if (!options.indexed) { if (!options.clusterId && !options.config?.clusterId) { throw new Error('expected options.clusterId') } if (typeof options.signingKeys !== 'object') throw new Error('expected options.signingKeys to be of type Object') if (options.signingKeys.publicKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.publicKey to be of type Uint8Array') if (options.signingKeys.privateKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.privateKey to be of type Uint8Array') } let clusterId = bus.clusterId = options.clusterId || options.config?.clusterId if (clusterId) clusterId = Buffer.from(clusterId) // some peers don't have clusters let useWorker = globalThis.isSocketRuntime if (options.worker === false) useWorker = false const Ctor = useWorker ? PeerWorkerProxy : Peer const _peer = new Ctor(options, dgram) _peer.onJoin = (packet, ...args) => { packet = Packet.from(packet) if (packet.clusterId.compare(clusterId) !== 0) return bus._emit('#join', packet, ...args) } _peer.onPacket = (packet, ...args) => { packet = Packet.from(packet) if (packet.clusterId.compare(clusterId) !== 0) return bus._emit('#packet', packet, ...args) } _peer.onStream = (packet, ...args) => { packet = Packet.from(packet) if (packet.clusterId.compare(clusterId) !== 0) return bus._emit('#stream', packet, ...args) } _peer.onData = (...args) => bus._emit('#data', ...args) _peer.onDebug = (...args) => bus._emit('#debug', ...args) _peer.onSend = (...args) => bus._emit('#send', ...args) _peer.onFirewall = (...args) => bus._emit('#firewall', ...args) _peer.onMulticast = (...args) => bus._emit('#multicast', ...args) _peer.onSync = (...args) => bus._emit('#sync', ...args) _peer.onSyncStart = (...args) => bus._emit('#sync-start', ...args) _peer.onSyncEnd = (...args) => bus._emit('#sync-end', ...args) _peer.onDisconnection = (...args) => bus._emit('#disconnection', ...args) _peer.onQuery = (...args) => bus._emit('#query', ...args) _peer.onNat = (...args) => bus._emit('#network-change', ...args) _peer.onWarn = (...args) => bus._emit('#warning', ...args) _peer.onState = (...args) => bus._emit('#state', ...args) _peer.onConnecting = (...args) => bus._emit('#connecting', ...args) _peer.onConnection = (...args) => bus._emit('#connection', ...args) // TODO check if its not a network error _peer.onError = (...args) => bus._emit('#error', ...args) _peer.onReady = info => { Object.assign(_peer, { isReady: true, ...info }) bus._emit('#ready', info) } bus.subclusters = new Map() /** * Gets general, read only information of the network peer. * * @function * @returns {object} - The general information. */ bus.getInfo = () => _peer.getInfo() bus.getMetrics = () => _peer.getMetrics() /** * Gets the read only state of the network peer. * * @function * @returns {object} - The address information. */ bus.getState = () => _peer.getState() /** * Indexes a new peer in the network. * * @function * @param {object} params - Peer information. * @param {string} params.peerId - The peer ID. * @param {string} params.address - The peer address. * @param {number} params.port - The peer port. * @throws {Error} - Throws an error if required parameters are missing. */ bus.addIndexedPeer = ({ peerId, address, port }) => { return _peer.addIndexedPeer({ peerId, address, port }) } bus.close = () => _peer.close() bus.sync = (peerId) => _peer.sync(peerId) bus.reconnect = () => _peer.reconnect() bus.disconnect = () => _peer.disconnect() bus.sealUnsigned = (m, v = options.signingKeys) => _peer.sealUnsigned(m, v) bus.openUnsigned = (m, v = options.signingKeys) => _peer.openUnsigned(m, v) bus.seal = (m, v = options.signingKeys) => _peer.seal(m, v) bus.open = (m, v = options.signingKeys) => _peer.open(m, v) bus.send = (...args) => _peer.send(...args) bus.query = (...args) => _peer.query(...args) bus.MAX_CACHE_TTL = CACHE_TTL const pack = async (eventName, value, opts = {}) => { if (typeof eventName !== 'string') throw new Error('event name must be a string') if (eventName.length === 0) throw new Error('event name too short') if (opts.ttl) opts.ttl = Math.min(opts.ttl, CACHE_TTL) const args = { clusterId, ...opts, usr1: await sha256(eventName, { bytes: true }) } if (!isBufferLike(value) && typeof value === 'object') { try { args.message = Buffer.from(JSON.stringify(value)) } catch (err) { return bus._emit('error', err) } } else { args.message = Buffer.from(value) } args.usr2 = Buffer.from(options.signingKeys.publicKey) args.sig = Encryption.sign(args.message, options.signingKeys.privateKey) return args } const unpack = async packet => { let verified const scid = Buffer.from(packet.subclusterId).toString('base64') const sub = bus.subclusters.get(scid) if (!sub) return {} const opened = await _peer.open(packet.message, scid) if (!opened) { sub._emit('unopened', { packet }) return {} } if (packet.sig) { try { if (Encryption.verify(opened, packet.sig, packet.usr2)) { verified = true } } catch (err) { sub._emit('unverified', packet) return {} } } return { opened: Buffer.from(opened), verified } } /** * Publishes an event to the network bus. * * @async * @function * @param {string} eventName - The name of the event. * @param {any} value - The value associated with the event. * @param {object} opts - Additional options for publishing. * @returns {Promise<any>} - A promise that resolves to the published event details. */ bus.emit = async (eventName, value, opts = {}) => { const args = await pack(eventName, value, opts) if (!options.sharedKey) { throw new Error('Can\'t emit to the top level cluster, a shared key was not provided in the constructor or the arguments options') } return await _peer.publish(options.sharedKey || opts.sharedKey, args) } bus.on = async (eventName, cb) => { if (eventName[0] !== '#') eventName = await sha256(eventName) bus._on(eventName, cb) } bus.subcluster = async (options = {}) => { if (!options.sharedKey?.constructor.name) { throw new Error('expected options.sharedKey to be of type Uint8Array') } const derivedKeys = await Encryption.createKeyPair(options.sharedKey) const subclusterId = Buffer.from(derivedKeys.publicKey) const scid = subclusterId.toString('base64') if (bus.subclusters.has(scid)) return bus.subclusters.get(scid) const sub = new events.EventEmitter() sub._emit = sub.emit sub._on = sub.on sub.peers = new Map() bus.subclusters.set(scid, sub) sub.peerId = _peer.peerId sub.subclusterId = subclusterId sub.sharedKey = options.sharedKey sub.derivedKeys = derivedKeys sub.stream = async (eventName, value, opts = {}) => { opts.clusterId = opts.clusterId || clusterId opts.subclusterId = opts.subclusterId || sub.subclusterId const args = await pack(eventName, value, opts) let packets for (const p of sub.peers.values()) { const result = await _peer.stream(p.peerId, sub.sharedKey, args) if (!packets) packets = result } return packets } sub.emit = async (eventName, value, opts = {}) => { opts.clusterId = opts.clusterId || clusterId opts.subclusterId = opts.subclusterId || sub.subclusterId const args = await pack(eventName, value, opts) if (sub.peers.values().length) { let packets = [] for (const p of sub.peers.values()) { const r = await _peer.stream(p.peerId, sub.sharedKey, args) if (packets.length === 0) packets = r } for (const packet of packets) { const p = Packet.from(packet) const pid = packet.packetId.toString('hex') _peer.cache.insert(pid, p) _peer.unpublished[pid] = Date.now() if (!Peer.onLine()) continue _peer.mcast(packet) } const head = packets.find(p => p.index === 0) if (_peer.onPacket && head) { // try to emit a single packet const p = await _peer.cache.compose(head) _peer.onPacket(p, _peer.port, _peer.address, true) return [p] } return packets } else { const packets = await _peer.publish(sub.sharedKey, args) return packets } } sub.on = async (eventName, cb) => { if (eventName[0] !== '#') eventName = await sha256(eventName) sub._on(eventName, cb) } sub.off = async (eventName, fn) => { if (eventName[0] !== '#') eventName = await sha256(eventName) sub.removeListener(eventName, fn) } sub.join = () => _peer.join(sub.sharedKey, options) bus._on('#ready', () => { const scid = sub.subclusterId.toString('base64') const subcluster = bus.subclusters.get(scid) if (subcluster) _peer.join(subcluster.sharedKey, options) }) _peer.join(sub.sharedKey, options) return sub } bus._on('#join', async (packet, peer) => { const scid = packet.subclusterId.toString('base64') const sub = bus.subclusters.get(scid) if (!sub) return if (!peer || !peer.peerId) return let ee = sub.peers.get(peer.peerId) if (!ee) { ee = new events.EventEmitter() ee._on = ee.on ee._emit = ee.emit ee.peerId = peer.peerId ee.address = peer.address ee.port = peer.port ee.emit = async (eventName, value, opts = {}) => { opts.clusterId = opts.clusterId || clusterId opts.subclusterId = opts.subclusterId || sub.subclusterId const args = await pack(eventName, value, opts) return _peer.stream(peer.peerId, sub.sharedKey, args) } ee.on = async (eventName, cb) => { if (eventName[0] !== '#') eventName = await sha256(eventName) ee._on(eventName, cb) } } const oldPeer = sub.peers.has(peer.peerId) const portChange = oldPeer.port !== peer.port const addressChange = oldPeer.address !== peer.address const natChange = oldPeer.natType !== peer.natType const change = portChange || addressChange || natChange ee._peer = peer sub.peers.set(peer.peerId, ee) const isStateChange = !oldPeer || change _peer.onDebug(_peer.peerId, `<-- API CONNECTION JOIN (scid=${scid}, peerId=${peer.peerId.slice(0, 6)})`) sub._emit('#join', ee, packet, isStateChange) }) const handlePacket = async (packet, peer, port, address) => { const scid = packet.subclusterId.toString('base64') const sub = bus.subclusters.get(scid) if (!sub) return const eventName = packet.usr1.toString('hex') const { verified, opened } = await unpack(packet) if (verified) packet.verified = true sub._emit(eventName, opened, packet) const ee = sub.peers.get(packet.streamFrom || peer?.peerId) if (ee) ee._emit(eventName, opened, packet) } bus._on('#stream', handlePacket) bus._on('#packet', handlePacket) bus._on('#disconnection', peer => { for (const sub of [...bus.subclusters.values()]) { sub._emit('#leave', peer) sub.peers.delete(peer.peerId) } }) await _peer.init() return bus } export { api } export default api