bare-switch-core
Advanced tools
| * text=auto eol=lf | ||
| spec/** linguist-generated=true |
| name: Integrate | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
| branches: | ||
| - main | ||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
| name: Lint | ||
| steps: | ||
| - uses: holepunchto/actions/node-base@v1 | ||
| - run: npm run lint | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| name: Test | ||
| steps: | ||
| - uses: holepunchto/actions/bare-base@v1 | ||
| - run: npm test |
| name: Publish | ||
| on: | ||
| push: | ||
| tags: | ||
| - v* | ||
| permissions: | ||
| id-token: write | ||
| contents: write | ||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: npm | ||
| name: Publish | ||
| steps: | ||
| - uses: holepunchto/actions/node-base@v1 | ||
| - uses: holepunchto/actions/publish@v1 |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| "prettier-config-holepunch" |
+80
| // The Bare worklet: the "backend" of the app, running on its own thread inside | ||
| // the host app's process (macOS, iOS, ...). It owns a Hyperswarm node - a real | ||
| // peer-to-peer connection to every other copy of this app on the same topic - | ||
| // and exposes a typed hrpc interface to the native Swift UI over the IPC channel. | ||
| // | ||
| // There is no server anywhere. Two instances of the app find each other through | ||
| // the distributed hash table and talk directly, end-to-end encrypted. | ||
| // | ||
| // The interesting state logic (local vs. remote changes, no-echo/no-loop) lives | ||
| // in ./lib/switch.js and is unit-tested; this file is just the wiring. | ||
| const Hyperswarm = require('hyperswarm') | ||
| const HRPC = require('./spec/hrpc') | ||
| const Switch = require('./lib/switch') | ||
| // `Bare.IPC` is the duplex byte stream to the Swift side injected by the host. | ||
| // hrpc rides on top of it and handles all framing/encoding. | ||
| const { IPC } = Bare | ||
| const rpc = new HRPC(IPC) | ||
| // Every copy of the app joins the same 32-byte topic, so they all meet on the | ||
| // DHT. (A real app would let the user pick a room; we keep one fixed room so | ||
| // "launch it twice and watch them sync" just works.) | ||
| const ROOM = 'bare-macos-switch' | ||
| const topic = Buffer.alloc(32).fill(ROOM) | ||
| const swarm = new Hyperswarm() | ||
| const peers = new Set() | ||
| // INTENTIONALLY NAIVE: this is a last-writer-wins value with no conflict | ||
| // resolution, so peers can diverge (e.g. flip before another peer joins). That | ||
| // divergence is a teaching point demonstrated in the README, not a bug to fix | ||
| // here - convergent multi-writer state belongs in Autobase | ||
| // (https://github.com/holepunchto/autobase). Please don't "fix" it with a clock | ||
| // or merge strategy; it would defeat the example. | ||
| const state = new Switch({ | ||
| broadcast: (on) => { | ||
| for (const connection of peers) connection.write(Switch.encode(on)) | ||
| }, | ||
| notify: (on) => rpc.newState({ on }) | ||
| }) | ||
| // --- UI -> worklet --- | ||
| // The user flipped the switch; apply it locally, push to peers, reply with the | ||
| // authoritative state. | ||
| rpc.onSetState(({ on }) => ({ on: state.setLocal(on) })) | ||
| // --- peer wiring --- | ||
| // The peer protocol is deliberately a single byte: each write is one | ||
| // `Switch.encode(...)` (one byte) and `Switch.decode` takes the last byte of a | ||
| // chunk (latest wins if writes coalesce). Adding a second message type here | ||
| // would break that invariant and need real framing (e.g. bare-rpc). | ||
| swarm.on('connection', (connection) => { | ||
| peers.add(connection) | ||
| console.log('[worklet] peer connected -', peers.size, 'total') | ||
| announcePeers() | ||
| // Bring the newcomer in sync with our current state immediately. | ||
| connection.write(Switch.encode(state.on)) | ||
| connection.on('data', (data) => state.applyRemote(Switch.decode(data))) | ||
| connection.on('error', () => {}) // ignore peer resets | ||
| connection.on('close', () => { | ||
| peers.delete(connection) | ||
| announcePeers() | ||
| }) | ||
| }) | ||
| swarm.join(topic, { server: true, client: true }) | ||
| // Tell the UI who we are. Sent once; the IPC stream buffers it until the UI's | ||
| // read loop attaches. | ||
| const publicKey = Buffer.from(swarm.keyPair.publicKey).toString('hex').slice(0, 8) | ||
| console.log('[worklet] up - key', publicKey, 'topic', ROOM) | ||
| rpc.info({ publicKey, topic: ROOM }) | ||
| function announcePeers() { | ||
| rpc.peersChanged({ count: peers.size }) | ||
| } |
| // The shared switch, as a pure unit: it holds the boolean and decides what | ||
| // happens on a local vs. a remote change. The collaborators that actually touch | ||
| // the world - `broadcast` (write to peers) and `notify` (tell the UI) - are | ||
| // injected, so this logic is testable without a network or the BareKit host. | ||
| module.exports = class Switch { | ||
| constructor({ broadcast = () => {}, notify = () => {} } = {}) { | ||
| this._broadcast = broadcast | ||
| this._notify = notify | ||
| this.on = false | ||
| } | ||
| // The user flipped it: update, push to peers, and return the new state. | ||
| // We do NOT notify our own UI - it made the change and already knows. | ||
| setLocal(on) { | ||
| this.on = on | ||
| this._broadcast(on) | ||
| return this.on | ||
| } | ||
| // A peer flipped it: update and tell our UI. We do NOT re-broadcast, or two | ||
| // peers would echo a change back and forth forever. | ||
| applyRemote(on) { | ||
| this.on = on | ||
| this._notify(on) | ||
| } | ||
| static encode(on) { | ||
| return Buffer.from([on ? 1 : 0]) | ||
| } | ||
| static decode(data) { | ||
| // A chunk may coalesce several updates; the latest byte wins. | ||
| return data[data.length - 1] === 1 | ||
| } | ||
| } |
+16
| # bare-switch-core | ||
| The shared peer-to-peer **switch core** behind the [bare-macos](https://github.com/holepunchto/bare-macos) / [bare-ios](https://github.com/holepunchto/bare-ios) example shells. It is the "core" in the Tether-stack sense: the worklet (`backend.js` + `lib/switch.js`), the protocol (`schema.js`), and the committed generated bindings under `spec/` (JS for the worklet, Swift for the shells). | ||
| Shells consume this package; they run no codegen. Edit `schema.js`, run `npm run schema`, commit the regenerated `spec/`. | ||
| > The switch is deliberately last-writer-wins with no conflict resolution. The divergence that causes is a teaching point (it motivates Autobase), not a bug. | ||
| ## Develop | ||
| ```sh | ||
| npm install | ||
| npm run schema # regenerate spec/ (JS + Swift) | ||
| npm test # switch logic on Bare | ||
| npm run lint | ||
| ``` |
+110
| // One schema, two runtimes. | ||
| // | ||
| // This single file defines the wire protocol once and emits it for both sides | ||
| // of the app, all under spec/ (the conventional home for generated code): | ||
| // | ||
| // - the Bare worklet (JavaScript): spec/schema, spec/hrpc | ||
| // - the native macOS UI (Swift): the same dirs (Package.swift + Sources) | ||
| // | ||
| // The Swift UI and the Bare backend speak a typed, generated protocol with no | ||
| // hand-written byte parsing on either side. Edit the definitions below and | ||
| // re-run `npm run schema` (the Xcode build re-runs it too). | ||
| const path = require('path') | ||
| const Hyperschema = require('hyperschema') | ||
| const HRPCBuilder = require('hrpc') | ||
| const SwiftHyperschema = require('hyperschema-swift') | ||
| const SwiftHRPC = require('hrpc-swift') | ||
| const ROOT = __dirname | ||
| const SCHEMA_DIR = path.join(ROOT, 'spec', 'schema') | ||
| const HRPC_DIR = path.join(ROOT, 'spec', 'hrpc') | ||
| const NS = 'sync' | ||
| // --- The protocol, defined once --- | ||
| // Message types (become compact-encoding codecs in JS and structs in Swift). | ||
| function defineTypes(ns) { | ||
| // Do not name this 'state': the Swift codegen would emit a `State` struct that | ||
| // collides with `CompactEncoding.State` inside the generated codecs. | ||
| // 'switch-state' -> `SwitchState`, which is collision-free. | ||
| ns.register({ | ||
| name: 'switch-state', | ||
| fields: [{ name: 'on', type: 'bool', required: true }] | ||
| }) | ||
| // Named 'identity' rather than 'info' so its codec property does not collide | ||
| // with the 'info' command method in the generated Swift class. | ||
| ns.register({ | ||
| name: 'identity', | ||
| fields: [ | ||
| { name: 'publicKey', type: 'string', required: true }, | ||
| { name: 'topic', type: 'string', required: true } | ||
| ] | ||
| }) | ||
| ns.register({ | ||
| name: 'peers', | ||
| fields: [{ name: 'count', type: 'uint', required: true }] | ||
| }) | ||
| } | ||
| // RPC commands (become typed methods + handlers on both sides). | ||
| function defineCommands(ns) { | ||
| // UI -> worklet: request the switch be set; worklet replies with the | ||
| // authoritative state after broadcasting it to peers. | ||
| ns.register({ | ||
| name: 'set-state', | ||
| request: { name: '@sync/switch-state', stream: false }, | ||
| response: { name: '@sync/switch-state', stream: false } | ||
| }) | ||
| // worklet -> UI: the switch changed because a peer flipped it. | ||
| ns.register({ | ||
| name: 'new-state', | ||
| request: { name: '@sync/switch-state', stream: false, send: true }, | ||
| response: null | ||
| }) | ||
| // worklet -> UI: the number of connected peers changed. | ||
| ns.register({ | ||
| name: 'peers-changed', | ||
| request: { name: '@sync/peers', stream: false, send: true }, | ||
| response: null | ||
| }) | ||
| // worklet -> UI: our identity and topic, emitted once on startup. | ||
| ns.register({ | ||
| name: 'info', | ||
| request: { name: '@sync/identity', stream: false, send: true }, | ||
| response: null | ||
| }) | ||
| } | ||
| // --- Emit the JavaScript side (for the Bare worklet) --- | ||
| const jsSchema = Hyperschema.from(SCHEMA_DIR) | ||
| defineTypes(jsSchema.namespace(NS)) | ||
| Hyperschema.toDisk(jsSchema) | ||
| const jsHrpc = HRPCBuilder.from(SCHEMA_DIR, HRPC_DIR) | ||
| defineCommands(jsHrpc.namespace(NS)) | ||
| HRPCBuilder.toDisk(jsHrpc) | ||
| // --- Emit the Swift side (for the native UI), into the same spec/ dirs --- | ||
| const swiftSchema = SwiftHyperschema.from(null) | ||
| defineTypes(swiftSchema.namespace(NS)) | ||
| const swiftHrpc = SwiftHRPC.from(swiftSchema) | ||
| defineCommands(swiftHrpc.namespace(NS)) | ||
| SwiftHyperschema.toDisk(swiftSchema, SCHEMA_DIR) | ||
| SwiftHRPC.toDisk(swiftHrpc, HRPC_DIR, { | ||
| schemaPackagePath: '../schema', | ||
| schemaPackageName: 'Schema', | ||
| schemaPackageId: 'schema' | ||
| }) | ||
| console.log('Wrote generated code to', path.relative(ROOT, path.dirname(SCHEMA_DIR))) |
| { | ||
| "version": 1, | ||
| "schema": [ | ||
| { | ||
| "id": 0, | ||
| "name": "@sync/set-state", | ||
| "request": { | ||
| "name": "@sync/switch-state", | ||
| "stream": false | ||
| }, | ||
| "response": { | ||
| "name": "@sync/switch-state", | ||
| "stream": false | ||
| }, | ||
| "version": 1 | ||
| }, | ||
| { | ||
| "id": 1, | ||
| "name": "@sync/new-state", | ||
| "request": { | ||
| "name": "@sync/switch-state", | ||
| "stream": false, | ||
| "send": true | ||
| }, | ||
| "response": null, | ||
| "version": 1 | ||
| }, | ||
| { | ||
| "id": 2, | ||
| "name": "@sync/peers-changed", | ||
| "request": { | ||
| "name": "@sync/peers", | ||
| "stream": false, | ||
| "send": true | ||
| }, | ||
| "response": null, | ||
| "version": 1 | ||
| }, | ||
| { | ||
| "id": 3, | ||
| "name": "@sync/info", | ||
| "request": { | ||
| "name": "@sync/identity", | ||
| "stream": false, | ||
| "send": true | ||
| }, | ||
| "response": null, | ||
| "version": 1 | ||
| } | ||
| ] | ||
| } |
| // This file is autogenerated by the hrpc compiler | ||
| /* eslint-disable camelcase */ | ||
| /* eslint-disable space-before-function-paren */ | ||
| const { c, RPC, RPCStream, RPCRequestStream } = require('hrpc/runtime') | ||
| const { getEncoding } = require('./messages.js') | ||
| const methods = new Map([ | ||
| ['@sync/set-state', 0], | ||
| [0, '@sync/set-state'], | ||
| ['@sync/new-state', 1], | ||
| [1, '@sync/new-state'], | ||
| ['@sync/peers-changed', 2], | ||
| [2, '@sync/peers-changed'], | ||
| ['@sync/info', 3], | ||
| [3, '@sync/info'] | ||
| ]) | ||
| class HRPC { | ||
| constructor(stream) { | ||
| this._stream = stream | ||
| this._handlers = [] | ||
| this._requestEncodings = new Map([ | ||
| ['@sync/set-state', getEncoding('@sync/switch-state')], | ||
| ['@sync/new-state', getEncoding('@sync/switch-state')], | ||
| ['@sync/peers-changed', getEncoding('@sync/peers')], | ||
| ['@sync/info', getEncoding('@sync/identity')] | ||
| ]) | ||
| this._responseEncodings = new Map([ | ||
| ['@sync/set-state', getEncoding('@sync/switch-state')] | ||
| ]) | ||
| this._rpc = new RPC(stream, async (req) => { | ||
| const command = methods.get(req.command) | ||
| const responseEncoding = this._responseEncodings.get(command) | ||
| const requestEncoding = this._requestEncodings.get(command) | ||
| if (this._requestIsSend(command)) { | ||
| const request = req.data ? c.decode(requestEncoding, req.data) : null | ||
| await this._handlers[command](request) | ||
| return | ||
| } | ||
| if (!this._requestIsStream(command) && !this._responseIsStream(command)) { | ||
| const request = req.data ? c.decode(requestEncoding, req.data) : null | ||
| const response = await this._handlers[command](request) | ||
| req.reply(c.encode(responseEncoding, response)) | ||
| } | ||
| if (!this._requestIsStream(command) && this._responseIsStream(command)) { | ||
| const request = req.data ? c.decode(requestEncoding, req.data) : null | ||
| const responseStream = new RPCStream( | ||
| null, | ||
| null, | ||
| req.createResponseStream(), | ||
| responseEncoding | ||
| ) | ||
| responseStream.data = request | ||
| await this._handlers[command](responseStream) | ||
| } | ||
| if (this._requestIsStream(command) && !this._responseIsStream(command)) { | ||
| const requestStream = new RPCRequestStream( | ||
| req, | ||
| responseEncoding, | ||
| req.createRequestStream(), | ||
| requestEncoding | ||
| ) | ||
| const response = await this._handlers[command](requestStream) | ||
| req.reply(c.encode(responseEncoding, response)) | ||
| } | ||
| if (this._requestIsStream(command) && this._responseIsStream(command)) { | ||
| const requestStream = new RPCRequestStream( | ||
| req, | ||
| responseEncoding, | ||
| req.createRequestStream(), | ||
| requestEncoding, | ||
| req.createResponseStream(), | ||
| responseEncoding | ||
| ) | ||
| await this._handlers[command](requestStream) | ||
| } | ||
| }) | ||
| } | ||
| async _call(name, args) { | ||
| const requestEncoding = this._requestEncodings.get(name) | ||
| const responseEncoding = this._responseEncodings.get(name) | ||
| const request = this._rpc.request(methods.get(name)) | ||
| const encoded = c.encode(requestEncoding, args) | ||
| request.send(encoded) | ||
| return c.decode(responseEncoding, await request.reply()) | ||
| } | ||
| _callSync(name, args) { | ||
| const requestEncoding = this._requestEncodings.get(name) | ||
| const responseEncoding = this._responseEncodings.get(name) | ||
| if (this._requestIsSend(name)) { | ||
| const encoded = c.encode(requestEncoding, args) | ||
| const request = this._rpc.event(methods.get(name)) | ||
| request.send(encoded) | ||
| } | ||
| const request = this._rpc.request(methods.get(name)) | ||
| if (!this._requestIsStream(name) && this._responseIsStream(name)) { | ||
| const encoded = c.encode(requestEncoding, args) | ||
| request.send(encoded) | ||
| return new RPCStream(request.createResponseStream(), responseEncoding) | ||
| } | ||
| if (this._requestIsStream(name) && !this._responseIsStream(name)) { | ||
| return new RPCRequestStream( | ||
| request, | ||
| responseEncoding, | ||
| null, | ||
| null, | ||
| request.createRequestStream(), | ||
| requestEncoding | ||
| ) | ||
| } | ||
| if (this._requestIsStream(name) && this._responseIsStream(name)) { | ||
| return new RPCRequestStream( | ||
| request, | ||
| responseEncoding, | ||
| request.createResponseStream(), | ||
| responseEncoding, | ||
| request.createRequestStream(), | ||
| requestEncoding | ||
| ) | ||
| } | ||
| } | ||
| async setState(args) { | ||
| return this._call('@sync/set-state', args) | ||
| } | ||
| newState(args) { | ||
| return this._callSync('@sync/new-state', args) | ||
| } | ||
| peersChanged(args) { | ||
| return this._callSync('@sync/peers-changed', args) | ||
| } | ||
| info(args) { | ||
| return this._callSync('@sync/info', args) | ||
| } | ||
| onSetState(responseFn) { | ||
| this._handlers['@sync/set-state'] = responseFn | ||
| } | ||
| onNewState(responseFn) { | ||
| this._handlers['@sync/new-state'] = responseFn | ||
| } | ||
| onPeersChanged(responseFn) { | ||
| this._handlers['@sync/peers-changed'] = responseFn | ||
| } | ||
| onInfo(responseFn) { | ||
| this._handlers['@sync/info'] = responseFn | ||
| } | ||
| _requestIsStream(command) { | ||
| return [ | ||
| ].includes(command) | ||
| } | ||
| _responseIsStream(command) { | ||
| return [ | ||
| ].includes(command) | ||
| } | ||
| // prettier-ignore-start | ||
| _requestIsSend(command) { | ||
| return [ | ||
| // prettier-ignore | ||
| '@sync/new-state', | ||
| '@sync/peers-changed', | ||
| '@sync/info' | ||
| ].includes(command) | ||
| } | ||
| } | ||
| module.exports = HRPC |
| // This file is autogenerated by the hyperschema compiler | ||
| // Schema Version: 1 | ||
| /* eslint-disable camelcase */ | ||
| /* eslint-disable quotes */ | ||
| /* eslint-disable space-before-function-paren */ | ||
| const { c } = require('hyperschema/runtime') | ||
| const VERSION = 1 | ||
| // eslint-disable-next-line no-unused-vars | ||
| let version = VERSION | ||
| // @sync/switch-state | ||
| const encoding0 = { | ||
| preencode(state, m) { | ||
| state.end++ // max flag is 1 so always one byte | ||
| }, | ||
| encode(state, m) { | ||
| const flags = m.on ? 1 : 0 | ||
| c.uint.encode(state, flags) | ||
| }, | ||
| decode(state) { | ||
| const flags = c.uint.decode(state) | ||
| return { | ||
| on: (flags & 1) !== 0 | ||
| } | ||
| } | ||
| } | ||
| // @sync/identity | ||
| const encoding1 = { | ||
| preencode(state, m) { | ||
| c.string.preencode(state, m.publicKey) | ||
| c.string.preencode(state, m.topic) | ||
| }, | ||
| encode(state, m) { | ||
| c.string.encode(state, m.publicKey) | ||
| c.string.encode(state, m.topic) | ||
| }, | ||
| decode(state) { | ||
| const r0 = c.string.decode(state) | ||
| const r1 = c.string.decode(state) | ||
| return { | ||
| publicKey: r0, | ||
| topic: r1 | ||
| } | ||
| } | ||
| } | ||
| // @sync/peers | ||
| const encoding2 = { | ||
| preencode(state, m) { | ||
| c.uint.preencode(state, m.count) | ||
| }, | ||
| encode(state, m) { | ||
| c.uint.encode(state, m.count) | ||
| }, | ||
| decode(state) { | ||
| const r0 = c.uint.decode(state) | ||
| return { | ||
| count: r0 | ||
| } | ||
| } | ||
| } | ||
| function setVersion(v) { | ||
| version = v | ||
| } | ||
| function encode(name, value, v = VERSION) { | ||
| version = v | ||
| return c.encode(getEncoding(name), value) | ||
| } | ||
| function decode(name, buffer, v = VERSION) { | ||
| version = v | ||
| return c.decode(getEncoding(name), buffer) | ||
| } | ||
| function getEnum(name) { | ||
| switch (name) { | ||
| default: | ||
| throw new Error('Enum not found ' + name) | ||
| } | ||
| } | ||
| function getEncoding(name) { | ||
| switch (name) { | ||
| case '@sync/switch-state': | ||
| return encoding0 | ||
| case '@sync/identity': | ||
| return encoding1 | ||
| case '@sync/peers': | ||
| return encoding2 | ||
| default: | ||
| throw new Error('Encoder not found ' + name) | ||
| } | ||
| } | ||
| function getStruct(name, v = VERSION) { | ||
| const enc = getEncoding(name) | ||
| return { | ||
| preencode(state, m) { | ||
| version = v | ||
| enc.preencode(state, m) | ||
| }, | ||
| encode(state, m) { | ||
| version = v | ||
| enc.encode(state, m) | ||
| }, | ||
| decode(state) { | ||
| version = v | ||
| return enc.decode(state) | ||
| } | ||
| } | ||
| } | ||
| const resolveStruct = getStruct // compat | ||
| module.exports = { | ||
| resolveStruct, | ||
| getStruct, | ||
| getEnum, | ||
| getEncoding, | ||
| encode, | ||
| decode, | ||
| setVersion, | ||
| version | ||
| } |
| // swift-tools-version: 5.10 | ||
| import PackageDescription | ||
| let package = Package( | ||
| name: "HRPC", | ||
| platforms: [.macOS(.v11), .iOS(.v14)], | ||
| products: [ | ||
| .library(name: "HRPC", targets: ["HRPC"]) | ||
| ], | ||
| dependencies: [ | ||
| .package(url: "https://github.com/holepunchto/bare-rpc-swift", branch: "main"), | ||
| .package(path: "../schema") | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "HRPC", | ||
| dependencies: [ | ||
| .product(name: "BareRPC", package: "bare-rpc-swift"), | ||
| .product(name: "Schema", package: "schema") | ||
| ], | ||
| path: "Sources" | ||
| ) | ||
| ] | ||
| ) |
| // This file is autogenerated by the hrpc compiler | ||
| import BareRPC | ||
| import CompactEncoding | ||
| import Foundation | ||
| import Schema | ||
| public class HRPC { | ||
| private let _rpc: RPC | ||
| private let _forwarder: _HRPCDelegateForwarder | ||
| private var _handlers: [String: Any] = [:] | ||
| public init<D: RPCDelegate>(delegate: D) { | ||
| let forwarder = _HRPCDelegateForwarder(transport: delegate) | ||
| self._rpc = RPC(delegate: forwarder) | ||
| self._forwarder = forwarder | ||
| forwarder.hrpc = self | ||
| } | ||
| public func receive(_ data: Data) async { | ||
| await self._rpc.receive(data) | ||
| } | ||
| // Request/response — client | ||
| public func setState(_ args: SwitchState? = nil) async throws -> SwitchState { | ||
| let encoded = try args.map { try _encode(switchState, $0) } | ||
| guard let raw = try await _rpc.request(0, data: encoded) else { | ||
| throw RPCRemoteError(message: "Missing response", code: "MISSING_RESPONSE") | ||
| } | ||
| return try _decode(switchState, raw) | ||
| } | ||
| public func onSetState(_ handler: @escaping (SwitchState?) async throws -> SwitchState) { | ||
| _handlers["@sync/set-state"] = handler | ||
| } | ||
| // Send-only — client (fire and forget) | ||
| public func newState(_ args: SwitchState? = nil) async throws { | ||
| let encoded = try args.map { try _encode(switchState, $0) } | ||
| await _rpc.event(1, data: encoded) | ||
| } | ||
| public func onNewState(_ handler: @escaping (SwitchState?) async -> Void) { | ||
| _handlers["@sync/new-state"] = handler | ||
| } | ||
| // Send-only — client (fire and forget) | ||
| public func peersChanged(_ args: Peers? = nil) async throws { | ||
| let encoded = try args.map { try _encode(peers, $0) } | ||
| await _rpc.event(2, data: encoded) | ||
| } | ||
| public func onPeersChanged(_ handler: @escaping (Peers?) async -> Void) { | ||
| _handlers["@sync/peers-changed"] = handler | ||
| } | ||
| // Send-only — client (fire and forget) | ||
| public func info(_ args: Identity? = nil) async throws { | ||
| let encoded = try args.map { try _encode(identity, $0) } | ||
| await _rpc.event(3, data: encoded) | ||
| } | ||
| public func onInfo(_ handler: @escaping (Identity?) async -> Void) { | ||
| _handlers["@sync/info"] = handler | ||
| } | ||
| fileprivate func _dispatchRequest(_ req: IncomingRequest) async { | ||
| await _dispatchNormal(req) | ||
| } | ||
| private func _dispatchNormal(_ req: IncomingRequest) async { | ||
| switch req.command { | ||
| case 0: // @sync/set-state | ||
| guard let handler = _handlers["@sync/set-state"] as? (SwitchState?) async throws -> SwitchState else { await req.reject("No handler registered", code: "NO_HANDLER"); return } | ||
| do { | ||
| let args: SwitchState? = try req.data.map { try _decode(switchState, $0) } | ||
| let response = try await handler(args) | ||
| await req.reply(try _encode(switchState, response)) | ||
| } catch { | ||
| await req.reject(error.localizedDescription, code: "HANDLER_ERROR") | ||
| } | ||
| default: | ||
| await req.reject("Unknown command", code: "UNKNOWN_COMMAND") | ||
| } | ||
| } | ||
| fileprivate func _dispatchEvent(_ event: IncomingEvent) async { | ||
| switch event.command { | ||
| case 1: // @sync/new-state | ||
| guard let handler = _handlers["@sync/new-state"] as? (SwitchState?) async -> Void else { return } | ||
| do { | ||
| let args: SwitchState? = try event.data.map { try _decode(switchState, $0) } | ||
| await handler(args) | ||
| } catch { | ||
| _forwarder.transport.rpc(_rpc, didFailWith: error) | ||
| } | ||
| case 2: // @sync/peers-changed | ||
| guard let handler = _handlers["@sync/peers-changed"] as? (Peers?) async -> Void else { return } | ||
| do { | ||
| let args: Peers? = try event.data.map { try _decode(peers, $0) } | ||
| await handler(args) | ||
| } catch { | ||
| _forwarder.transport.rpc(_rpc, didFailWith: error) | ||
| } | ||
| case 3: // @sync/info | ||
| guard let handler = _handlers["@sync/info"] as? (Identity?) async -> Void else { return } | ||
| do { | ||
| let args: Identity? = try event.data.map { try _decode(identity, $0) } | ||
| await handler(args) | ||
| } catch { | ||
| _forwarder.transport.rpc(_rpc, didFailWith: error) | ||
| } | ||
| default: | ||
| break | ||
| } | ||
| } | ||
| private func _encode<C: Codec>(_ codec: C, _ value: C.Value) throws -> Data { | ||
| var state = State() | ||
| codec.preencode(&state, value) | ||
| state.allocate() | ||
| try codec.encode(&state, value) | ||
| return state.buffer | ||
| } | ||
| private func _decode<C: Codec>(_ codec: C, _ data: Data) throws -> C.Value { | ||
| var state = State(data) | ||
| return try codec.decode(&state) | ||
| } | ||
| } | ||
| private final class _HRPCDelegateForwarder: RPCDelegate { | ||
| let transport: any RPCDelegate | ||
| weak var hrpc: HRPC? | ||
| init<D: RPCDelegate>(transport: D) { | ||
| self.transport = transport | ||
| } | ||
| func rpc(_ rpc: RPC, send data: Data) { | ||
| transport.rpc(rpc, send: data) | ||
| } | ||
| func rpc(_ rpc: RPC, didFailWith error: Error) { | ||
| transport.rpc(rpc, didFailWith: error) | ||
| } | ||
| func rpc(_ rpc: RPC, didReceiveRequest req: IncomingRequest) async throws { | ||
| await hrpc?._dispatchRequest(req) | ||
| } | ||
| func rpc(_ rpc: RPC, didReceiveEvent event: IncomingEvent) async { | ||
| await hrpc?._dispatchEvent(event) | ||
| } | ||
| } |
| // This file is autogenerated by the hyperschema compiler | ||
| // Schema Version: 1 | ||
| /* eslint-disable camelcase */ | ||
| /* eslint-disable quotes */ | ||
| /* eslint-disable space-before-function-paren */ | ||
| const { c } = require('hyperschema/runtime') | ||
| const VERSION = 1 | ||
| // eslint-disable-next-line no-unused-vars | ||
| let version = VERSION | ||
| // @sync/switch-state | ||
| const encoding0 = { | ||
| preencode(state, m) { | ||
| state.end++ // max flag is 1 so always one byte | ||
| }, | ||
| encode(state, m) { | ||
| const flags = m.on ? 1 : 0 | ||
| c.uint.encode(state, flags) | ||
| }, | ||
| decode(state) { | ||
| const flags = c.uint.decode(state) | ||
| return { | ||
| on: (flags & 1) !== 0 | ||
| } | ||
| } | ||
| } | ||
| // @sync/identity | ||
| const encoding1 = { | ||
| preencode(state, m) { | ||
| c.string.preencode(state, m.publicKey) | ||
| c.string.preencode(state, m.topic) | ||
| }, | ||
| encode(state, m) { | ||
| c.string.encode(state, m.publicKey) | ||
| c.string.encode(state, m.topic) | ||
| }, | ||
| decode(state) { | ||
| const r0 = c.string.decode(state) | ||
| const r1 = c.string.decode(state) | ||
| return { | ||
| publicKey: r0, | ||
| topic: r1 | ||
| } | ||
| } | ||
| } | ||
| // @sync/peers | ||
| const encoding2 = { | ||
| preencode(state, m) { | ||
| c.uint.preencode(state, m.count) | ||
| }, | ||
| encode(state, m) { | ||
| c.uint.encode(state, m.count) | ||
| }, | ||
| decode(state) { | ||
| const r0 = c.uint.decode(state) | ||
| return { | ||
| count: r0 | ||
| } | ||
| } | ||
| } | ||
| function setVersion(v) { | ||
| version = v | ||
| } | ||
| function encode(name, value, v = VERSION) { | ||
| version = v | ||
| return c.encode(getEncoding(name), value) | ||
| } | ||
| function decode(name, buffer, v = VERSION) { | ||
| version = v | ||
| return c.decode(getEncoding(name), buffer) | ||
| } | ||
| function getEnum(name) { | ||
| switch (name) { | ||
| default: | ||
| throw new Error('Enum not found ' + name) | ||
| } | ||
| } | ||
| function getEncoding(name) { | ||
| switch (name) { | ||
| case '@sync/switch-state': | ||
| return encoding0 | ||
| case '@sync/identity': | ||
| return encoding1 | ||
| case '@sync/peers': | ||
| return encoding2 | ||
| default: | ||
| throw new Error('Encoder not found ' + name) | ||
| } | ||
| } | ||
| function getStruct(name, v = VERSION) { | ||
| const enc = getEncoding(name) | ||
| return { | ||
| preencode(state, m) { | ||
| version = v | ||
| enc.preencode(state, m) | ||
| }, | ||
| encode(state, m) { | ||
| version = v | ||
| enc.encode(state, m) | ||
| }, | ||
| decode(state) { | ||
| version = v | ||
| return enc.decode(state) | ||
| } | ||
| } | ||
| } | ||
| const resolveStruct = getStruct // compat | ||
| module.exports = { | ||
| resolveStruct, | ||
| getStruct, | ||
| getEnum, | ||
| getEncoding, | ||
| encode, | ||
| decode, | ||
| setVersion, | ||
| version | ||
| } |
| // swift-tools-version: 5.10 | ||
| import PackageDescription | ||
| let package = Package( | ||
| name: "Schema", | ||
| platforms: [.macOS(.v11), .iOS(.v14)], | ||
| products: [ | ||
| .library(name: "Schema", targets: ["Schema"]) | ||
| ], | ||
| dependencies: [ | ||
| .package(url: "https://github.com/holepunchto/compact-encoding-swift", branch: "main") | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "Schema", | ||
| dependencies: [.product(name: "CompactEncoding", package: "compact-encoding-swift")], | ||
| path: "Sources" | ||
| ) | ||
| ] | ||
| ) |
| { | ||
| "version": 1, | ||
| "schema": [ | ||
| { | ||
| "name": "switch-state", | ||
| "namespace": "sync", | ||
| "compact": false, | ||
| "flagsPosition": 0, | ||
| "fields": [ | ||
| { | ||
| "name": "on", | ||
| "required": false, | ||
| "type": "bool", | ||
| "version": 1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "identity", | ||
| "namespace": "sync", | ||
| "compact": false, | ||
| "flagsPosition": -1, | ||
| "fields": [ | ||
| { | ||
| "name": "publicKey", | ||
| "required": true, | ||
| "type": "string", | ||
| "version": 1 | ||
| }, | ||
| { | ||
| "name": "topic", | ||
| "required": true, | ||
| "type": "string", | ||
| "version": 1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "peers", | ||
| "namespace": "sync", | ||
| "compact": false, | ||
| "flagsPosition": -1, | ||
| "fields": [ | ||
| { | ||
| "name": "count", | ||
| "required": true, | ||
| "type": "uint", | ||
| "version": 1 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| // This file is autogenerated by the hyperschema compiler | ||
| // !!DO NOT EDIT THIS FILE!! | ||
| // Schema Version: 1 | ||
| import CompactEncoding | ||
| import Foundation | ||
| public func encode<C: Codec>(_ codec: C, _ value: C.Value) throws -> Data { | ||
| var state = State() | ||
| codec.preencode(&state, value) | ||
| state.allocate() | ||
| try codec.encode(&state, value) | ||
| return state.buffer | ||
| } | ||
| public func decode<C: Codec>(_ codec: C, _ data: Data) throws -> C.Value { | ||
| var state = State(data) | ||
| return try codec.decode(&state) | ||
| } | ||
| // @sync/switch-state | ||
| public struct SwitchState { | ||
| public var on: Bool | ||
| public init(on: Bool = false) { | ||
| self.on = on | ||
| } | ||
| } | ||
| public struct SwitchStateCodec: Codec { | ||
| public typealias Value = SwitchState | ||
| let _uint = Primitive.UInt() | ||
| public init() {} | ||
| public func preencode(_ state: inout State, _ value: SwitchState) { | ||
| var _flags: UInt = 0 | ||
| if value.on { _flags |= 1 } | ||
| _uint.preencode(&state, _flags) | ||
| } | ||
| public func encode(_ state: inout State, _ value: SwitchState) throws { | ||
| var _flags: UInt = 0 | ||
| if value.on { _flags |= 1 } | ||
| try _uint.encode(&state, _flags) | ||
| } | ||
| public func decode(_ state: inout State) throws -> SwitchState { | ||
| let _flags = try _uint.decode(&state) | ||
| let on = _flags & 1 != 0 | ||
| return SwitchState( | ||
| on: on | ||
| ) | ||
| } | ||
| } | ||
| public let switchState = SwitchStateCodec() | ||
| // @sync/identity | ||
| public struct Identity { | ||
| public var publicKey: String | ||
| public var topic: String | ||
| public init(publicKey: String, topic: String) { | ||
| self.publicKey = publicKey | ||
| self.topic = topic | ||
| } | ||
| } | ||
| public struct IdentityCodec: Codec { | ||
| public typealias Value = Identity | ||
| let _string = Primitive.UTF8() | ||
| public init() {} | ||
| public func preencode(_ state: inout State, _ value: Identity) { | ||
| _string.preencode(&state, value.publicKey) | ||
| _string.preencode(&state, value.topic) | ||
| } | ||
| public func encode(_ state: inout State, _ value: Identity) throws { | ||
| try _string.encode(&state, value.publicKey) | ||
| try _string.encode(&state, value.topic) | ||
| } | ||
| public func decode(_ state: inout State) throws -> Identity { | ||
| let publicKey = try _string.decode(&state) | ||
| let topic = try _string.decode(&state) | ||
| return Identity( | ||
| publicKey: publicKey, | ||
| topic: topic | ||
| ) | ||
| } | ||
| } | ||
| public let identity = IdentityCodec() | ||
| // @sync/peers | ||
| public struct Peers { | ||
| public var count: UInt | ||
| public init(count: UInt) { | ||
| self.count = count | ||
| } | ||
| } | ||
| public struct PeersCodec: Codec { | ||
| public typealias Value = Peers | ||
| let _uint = Primitive.UInt() | ||
| public init() {} | ||
| public func preencode(_ state: inout State, _ value: Peers) { | ||
| _uint.preencode(&state, value.count) | ||
| } | ||
| public func encode(_ state: inout State, _ value: Peers) throws { | ||
| try _uint.encode(&state, value.count) | ||
| } | ||
| public func decode(_ state: inout State) throws -> Peers { | ||
| let count = try _uint.decode(&state) | ||
| return Peers( | ||
| count: count | ||
| ) | ||
| } | ||
| } | ||
| public let peers = PeersCodec() |
+1
| require('./test/switch') |
| const test = require('brittle') | ||
| const Switch = require('../lib/switch') | ||
| test('setLocal updates state, broadcasts to peers, does not echo to the UI', (t) => { | ||
| const broadcast = [] | ||
| const notify = [] | ||
| const sw = new Switch({ | ||
| broadcast: (on) => broadcast.push(on), | ||
| notify: (on) => notify.push(on) | ||
| }) | ||
| const result = sw.setLocal(true) | ||
| t.is(result, true, 'returns the new authoritative state') | ||
| t.is(sw.on, true, 'state is updated') | ||
| t.alike(broadcast, [true], 'broadcast to peers') | ||
| t.alike(notify, [], 'no echo back to the UI that made the change') | ||
| }) | ||
| test('applyRemote updates state, notifies the UI, does not re-broadcast', (t) => { | ||
| const broadcast = [] | ||
| const notify = [] | ||
| const sw = new Switch({ | ||
| broadcast: (on) => broadcast.push(on), | ||
| notify: (on) => notify.push(on) | ||
| }) | ||
| sw.applyRemote(true) | ||
| t.is(sw.on, true, 'state is updated') | ||
| t.alike(notify, [true], 'the UI is told a peer changed it') | ||
| t.alike(broadcast, [], 'not re-broadcast, so peers cannot loop') | ||
| }) | ||
| test('decode reads the last byte when chunks coalesce', (t) => { | ||
| t.is(Switch.decode(Buffer.from([0, 1, 0, 1])), true) | ||
| t.is(Switch.decode(Buffer.from([1, 0])), false) | ||
| }) | ||
| test('encode produces a single 0/1 byte', (t) => { | ||
| t.alike(Switch.encode(true), Buffer.from([1])) | ||
| t.alike(Switch.encode(false), Buffer.from([0])) | ||
| }) |
+33
-1
@@ -1,1 +0,33 @@ | ||
| {"name":"bare-switch-core","version":"0.0.0"} | ||
| { | ||
| "name": "bare-switch-core", | ||
| "version": "1.0.1", | ||
| "description": "Shared peer-to-peer switch core behind the bare example apps (bare-macos, bare-ios)", | ||
| "license": "Apache-2.0", | ||
| "author": "Holepunch", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/holepunchto/bare-switch-core.git" | ||
| }, | ||
| "exports": { | ||
| ".": "./backend.js" | ||
| }, | ||
| "scripts": { | ||
| "schema": "node schema.js", | ||
| "test": "brittle-bare --coverage test.js", | ||
| "lint": "prettier --check . && lunte", | ||
| "format": "prettier --write . && lunte --fix" | ||
| }, | ||
| "dependencies": { | ||
| "hrpc": "^4.3.0", | ||
| "hyperschema": "^1.21.0", | ||
| "hyperswarm": "^4.17.0" | ||
| }, | ||
| "devDependencies": { | ||
| "brittle": "^4.0.2", | ||
| "hrpc-swift": "^0.2.0", | ||
| "hyperschema-swift": "^0.3.0", | ||
| "lunte": "^1.8.0", | ||
| "prettier": "^3.4.2", | ||
| "prettier-config-holepunch": "^2.0.0" | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
No License Found
LicenseLicense information could not be found.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
32973
73173.33%22
2100%0
-100%723
Infinity%1
-50%1
-66.67%0
-100%17
Infinity%0
-100%3
Infinity%6
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added