🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

bare-switch-core

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bare-switch-core - npm Package Compare versions

Comparing version
0.0.0
to
1.0.1
+2
.gitattributes
* 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"
// 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
}
}
# 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
```
// 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()
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"
}
}