OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / schema / build.js

242 lines · javascript ref:
// Generates swarmd's wire in C. BUILD TIME ONLY.
//
// Run `npm run build:schema` after editing this file; the emitted .c/.h are
// COMMITTED beside it and the daemon builds from them with no npm and no Node.
// This mirrors libhc, which does exactly the same thing with the same tools
// (libhc/schema/build.js -> schema/hc_schema.{c,h}, both checked in).
//
// ⛔ These generators are for protocols WE define. They cannot generate
// dht_messages.h: hyperdht has no spec/, no hyperschema dependency, and
// lib/messages.js opens `const c = require('compact-encoding')` — it hand-writes
// its wire exactly as we do. See docs/SWARMD-DESIGN.md §3.

const CHyperschema = require('hyperschema-c')
const CHRPC = require('hrpc-c')

const schema = CHyperschema.from(__dirname)
const s = schema.namespace('swarmd')

// ---- session ----

s.register({
  name: 'hello-request',
  fields: [
    { name: 'name', type: 'string', required: true },
    // The APPLICATION identity, durable and supplied by the client. Distinct
    // from the daemon's transport keypair: transport identity is not
    // application identity (see memory identity-vs-writer-keypair).
    { name: 'app-identity', type: 'fixed32', required: true },
    { name: 'version', type: 'uint', required: true }
  ]
})

s.register({
  name: 'hello-response',
  fields: [
    { name: 'client-id', type: 'uint', required: true },
    { name: 'daemon-pubkey', type: 'fixed32', required: true }
  ]
})

// ⛔ AN UNOBSERVABLE DAEMON IS HOW A DEAD DHT LOOKED HEALTHY. Two peers sat on
// one topic for ten minutes both reporting "joined" while neither had ever
// bootstrapped (hyper-games #68). "joined" is a LOCAL fact; these are the ones
// that mean reachable.
s.register({
  name: 'status',
  fields: [
    { name: 'dht-state', type: 'uint', required: true },   // 0 starting 1 connecting 2 up
    { name: 'bootstrapped', type: 'bool', required: true },
    { name: 'peers-known', type: 'uint', required: true },
    { name: 'peers-connected', type: 'uint', required: true },
    { name: 'connecting', type: 'uint', required: true },
    { name: 'queued', type: 'uint', required: true },
    { name: 'local-clients', type: 'uint', required: true }
  ]
})

s.register({ name: 'ok', fields: [{ name: 'ok', type: 'bool', required: true }] })
s.register({ name: 'none', fields: [{ name: 'reserved', type: 'uint', required: true }] })

// ---- swarm ----

s.register({
  name: 'swarm-join',
  fields: [
    { name: 'topic', type: 'fixed32', required: true },
    { name: 'server', type: 'bool', required: true },
    { name: 'client', type: 'bool', required: true }
  ]
})

s.register({ name: 'topic-ref', fields: [{ name: 'topic', type: 'fixed32', required: true }] })
s.register({ name: 'peer-key', fields: [{ name: 'pubkey', type: 'fixed32', required: true }] })

// ---- channels ----

s.register({
  name: 'channel-open',
  fields: [
    { name: 'protocol', type: 'string', required: true },
    { name: 'id', type: 'buffer', required: true },
    { name: 'slots', type: 'uint', required: true }
  ]
})

s.register({ name: 'channel-ref', fields: [{ name: 'channel', type: 'uint', required: true }] })

s.register({
  name: 'send-request',
  fields: [
    { name: 'channel', type: 'uint', required: true },
    { name: 'slot', type: 'uint', required: true },
    { name: 'target', type: 'uint', required: true },   // 0 = broadcast
    { name: 'bytes', type: 'buffer', required: true }
  ]
})

// ⛔ WRITABILITY MUST REACH THE CLIENT. Protomux returns it and its own header
// says discarding that answer "left every protocol above with no way to avoid
// burying the connection".
s.register({
  name: 'send-result',
  fields: [
    { name: 'accepted', type: 'bool', required: true },
    { name: 'writable', type: 'bool', required: true }
  ]
})

// ---- content (hyper-overlay) ----
//
// ⛔ BULK BYTES NEVER CROSS THE SOCKET. A .res is 108 MB. The overlay writes to
// a path and the client opens the path; only a 32-byte hash travels here.

s.register({ name: 'path', fields: [{ name: 'path', type: 'string', required: true }] })
s.register({ name: 'hash', fields: [{ name: 'hash', type: 'fixed32', required: true }] })

s.register({
  name: 'content-serve',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'path', type: 'string', required: true }
  ]
})

s.register({
  name: 'content-fetch',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'dest-path', type: 'string', required: true }
  ]
})

s.register({
  name: 'content-cancel',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'discard', type: 'bool', required: true }
  ]
})

s.register({
  name: 'content-progress',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'received', type: 'uint', required: true },
    { name: 'total', type: 'uint', required: true }
  ]
})

s.register({
  name: 'content-done',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'path', type: 'string', required: true }
  ]
})

s.register({
  name: 'content-failed',
  fields: [
    { name: 'hash', type: 'fixed32', required: true },
    { name: 'reason', type: 'string', required: true }
  ]
})

// ---- events ----

s.register({
  name: 'peer-up',
  fields: [
    { name: 'peer', type: 'uint', required: true },
    { name: 'pubkey', type: 'fixed32', required: true }
  ]
})

s.register({ name: 'peer-down', fields: [{ name: 'peer', type: 'uint', required: true }] })

// origin-kind separates a REMOTE peer from ANOTHER LOCAL CLIENT. That is what
// lets a client suppress its own echo, which session-daemon cannot do.
s.register({
  name: 'message',
  fields: [
    { name: 'channel', type: 'uint', required: true },
    { name: 'slot', type: 'uint', required: true },
    { name: 'origin-kind', type: 'uint', required: true },  // 0 peer, 1 local client
    { name: 'origin-id', type: 'uint', required: true },
    { name: 'bytes', type: 'buffer', required: true }
  ]
})

// ---- RPC ----

// ⛔ NOT `new CHRPC(schema, __dirname)`, which is what hrpc-c's README shows.
// HRPC's constructor is (schema, hrpcJson, { hrpcDir, schemaDir }) — the second
// argument is a PARSED JSON OBJECT, not a directory. Passing a path there makes
// `hrpcJson.schema` undefined and the constructor dies on `.length`. The static
// HRPC.from(schemaDir, hrpcDir) does take paths, but it re-reads schema.json
// from disk, which does not exist until toDisk has run. Passing the in-memory
// schema with explicit dirs avoids the round-trip entirely.
const hrpc = new CHRPC(schema, null, { hrpcDir: __dirname, schemaDir: __dirname })
const r = hrpc.namespace('swarmd')

const unary = (name, req, res) => r.register({
  name,
  request: { name: `@swarmd/${req}`, stream: false },
  response: { name: `@swarmd/${res}`, stream: false }
})
const event = (name, req) => r.register({
  name,
  request: { name: `@swarmd/${req}`, send: true }
})

unary('hello', 'hello-request', 'hello-response')
unary('status-get', 'none', 'status')

unary('swarm-join', 'swarm-join', 'ok')
unary('swarm-leave', 'topic-ref', 'ok')
unary('peer-add', 'peer-key', 'ok')
unary('peer-remove', 'peer-key', 'ok')

unary('channel-open', 'channel-open', 'channel-ref')
unary('channel-close', 'channel-ref', 'ok')
unary('send', 'send-request', 'send-result')

unary('content-hash', 'path', 'hash')
unary('content-serve', 'content-serve', 'ok')
unary('content-fetch', 'content-fetch', 'ok')
unary('content-cancel', 'content-cancel', 'ok')

// Daemon -> client. Fire-and-forget: nobody replies to a peer arriving.
event('on-peer-up', 'peer-up')
event('on-peer-down', 'peer-down')
event('on-message', 'message')
event('on-drain', 'channel-ref')
event('on-status', 'status')
event('on-content-progress', 'content-progress')
event('on-content-done', 'content-done')
event('on-content-failed', 'content-failed')

CHyperschema.toDisk(schema)
CHRPC.toDisk(hrpc)