onelobby 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/blueprint.schema.json +244 -0
  3. package/dist/blueprint.d.ts +170 -0
  4. package/dist/blueprint.js +286 -0
  5. package/dist/blueprint.js.map +1 -0
  6. package/dist/codec.d.ts +23 -0
  7. package/dist/codec.js +28 -0
  8. package/dist/codec.js.map +1 -1
  9. package/dist/direct/browser-host.d.ts +46 -0
  10. package/dist/direct/browser-host.js +93 -0
  11. package/dist/direct/browser-host.js.map +1 -0
  12. package/dist/direct/hub-host.d.ts +34 -23
  13. package/dist/direct/hub-host.js +75 -59
  14. package/dist/direct/hub-host.js.map +1 -1
  15. package/dist/direct/hub-local.d.ts +22 -0
  16. package/dist/direct/hub-local.js +46 -0
  17. package/dist/direct/hub-local.js.map +1 -0
  18. package/dist/direct/hub.d.ts +37 -10
  19. package/dist/direct/hub.js +114 -23
  20. package/dist/direct/hub.js.map +1 -1
  21. package/dist/direct/index.d.ts +37 -4
  22. package/dist/direct/index.js +162 -21
  23. package/dist/direct/index.js.map +1 -1
  24. package/dist/direct/metrics.d.ts +12 -0
  25. package/dist/direct/metrics.js +45 -0
  26. package/dist/direct/metrics.js.map +1 -0
  27. package/dist/direct/portmap.d.ts +9 -2
  28. package/dist/direct/portmap.js +7 -2
  29. package/dist/direct/portmap.js.map +1 -1
  30. package/dist/direct/sha1.d.ts +16 -0
  31. package/dist/direct/sha1.js +111 -0
  32. package/dist/direct/sha1.js.map +1 -0
  33. package/dist/direct-address.d.ts +2 -2
  34. package/dist/direct-address.js.map +1 -1
  35. package/dist/friendly.d.ts +28 -0
  36. package/dist/friendly.js +40 -0
  37. package/dist/friendly.js.map +1 -1
  38. package/dist/index.d.ts +11 -3
  39. package/dist/index.js +8 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/internals.d.ts +9 -0
  42. package/dist/internals.js +9 -0
  43. package/dist/internals.js.map +1 -1
  44. package/dist/join-code-kind.d.ts +10 -0
  45. package/dist/join-code-kind.js +10 -0
  46. package/dist/join-code-kind.js.map +1 -0
  47. package/dist/joincode.d.ts +10 -1
  48. package/dist/joincode.js +23 -3
  49. package/dist/joincode.js.map +1 -1
  50. package/dist/lobby.d.ts +65 -4
  51. package/dist/lobby.js +151 -12
  52. package/dist/lobby.js.map +1 -1
  53. package/dist/lockstep.d.ts +8 -0
  54. package/dist/lockstep.js +23 -2
  55. package/dist/lockstep.js.map +1 -1
  56. package/dist/nostr/crypto.d.ts +20 -0
  57. package/dist/nostr/crypto.js +78 -0
  58. package/dist/nostr/crypto.js.map +1 -0
  59. package/dist/nostr/host.d.ts +66 -0
  60. package/dist/nostr/host.js +196 -0
  61. package/dist/nostr/host.js.map +1 -0
  62. package/dist/nostr/limits.d.ts +31 -0
  63. package/dist/nostr/limits.js +36 -0
  64. package/dist/nostr/limits.js.map +1 -0
  65. package/dist/nostr/link.d.ts +70 -0
  66. package/dist/nostr/link.js +180 -0
  67. package/dist/nostr/link.js.map +1 -0
  68. package/dist/nostr/relays.d.ts +79 -0
  69. package/dist/nostr/relays.js +315 -0
  70. package/dist/nostr/relays.js.map +1 -0
  71. package/dist/nostr/room.d.ts +43 -0
  72. package/dist/nostr/room.js +104 -0
  73. package/dist/nostr/room.js.map +1 -0
  74. package/dist/nostr/secp256k1.d.ts +10 -0
  75. package/dist/nostr/secp256k1.js +171 -0
  76. package/dist/nostr/secp256k1.js.map +1 -0
  77. package/dist/nostr/socket.d.ts +48 -0
  78. package/dist/nostr/socket.js +141 -0
  79. package/dist/nostr/socket.js.map +1 -0
  80. package/dist/onelobby.bundle.js +146 -0
  81. package/dist/onelobby.bundle.js.map +7 -0
  82. package/dist/onelobby.bundle.mjs +11 -5
  83. package/dist/onelobby.bundle.mjs.map +4 -4
  84. package/dist/onelobby.d.ts +54 -4
  85. package/dist/onelobby.global.js +2932 -49
  86. package/dist/onelobby.global.js.map +4 -4
  87. package/dist/onelobby.global.min.js +11 -5
  88. package/dist/onelobby.global.min.js.map +4 -4
  89. package/dist/onelobby.js +158 -15
  90. package/dist/onelobby.js.map +1 -1
  91. package/dist/pause.d.ts +85 -0
  92. package/dist/pause.js +160 -0
  93. package/dist/pause.js.map +1 -0
  94. package/dist/transport.d.ts +3 -1
  95. package/dist/transport.js.map +1 -1
  96. package/dist/webrtc.d.ts +1 -0
  97. package/dist/webrtc.js +6 -1
  98. package/dist/webrtc.js.map +1 -1
  99. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"lockstep.js","sourceRoot":"","sources":["../src/lockstep.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHUNK_PAYLOAD, splitChunks } from \"./chunking.js\";\nimport { PROTO, type FrameCommand, type Message } from \"./codec.js\";\nimport { deflate, deflateAvailable, inflate } from \"./deflate.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\n\nexport interface Frame {\n tick: number;\n commands: FrameCommand[];\n}\n\nexport interface DesyncInfo {\n tick: number;\n hostHash: bigint;\n yourHash: bigint;\n}\n\nexport type LockstepEvents = {\n desync: [DesyncInfo];\n /** A snapshot was applied (late join, desync, falling too far behind). */\n restored: [tick: number];\n /** First frame (or snapshot) of the game reached this peer: the session is in game. */\n started: [];\n error: [Error];\n};\n\nexport const MAX_DESYNCS = 3;\nexport const MAX_RESTORE_RETRIES = 3;\nexport const HOST_SILENCE_MS = 3000;\nexport const PEER_HASH_SILENCE_MS = 10_000;\n/** Commands one remote peer may have waiting for the host's next frame; the excess is dropped. */\nexport const MAX_PENDING_PER_PEER = 256;\nconst SNAPSHOT_COMPRESS_MIN = 1024;\n\nexport interface LockstepOptions {\n hashEveryTicks: number;\n /** Frames kept for replays (late join, migration). Default 30 s of ticks. */\n historyFrames?: number;\n}\n\n/**\n * Host-ordered lockstep (spec/binary.md, DESIGN s.8). One class for both roles: the host emits `frame(T)` on a fixed\n * cadence and never waits; joiners queue frames and hand them out through `nextFrame()`.\n *\n * Ticks: the initial state is tick 0 and has no frame; `frame(T)` produces tick T. `reportHash(tick, h)` is called\n * after applying `frame.tick` (the library only sends when `tick % hashEveryTicks == 0`, tick > 0). `snapshot()` must\n * return the state after the last frame returned by `nextFrame()`; `restore(tick, bytes)` receives that tick, and the\n * next frame applied is `tick + 1`.\n */\nexport class Lockstep extends Emitter<LockstepEvents> {\n snapshot: (() => Uint8Array) | null = null;\n restore: ((tick: number, bytes: Uint8Array) => void) | null = null;\n\n readonly hashEveryTicks: number;\n /** Chunk body size used when serving snapshots. */\n chunkSize = CHUNK_PAYLOAD;\n private historyMax: number;\n\n private queue: Frame[] = [];\n private history: Frame[] = [];\n /** Highest frame tick received/emitted; -1 = no state (a fresh joiner). */\n private lastFrameTick = -1;\n /** Tick of the state the game holds (last frame returned by nextFrame, or a restored snapshot). */\n private consumedTick = 0;\n private cmdSeq = 1;\n private awaitingSnapshot = false;\n private lastFrameRx = 0;\n private lastSnapshotReq = -Infinity;\n private restoreFailures = 0;\n\n // host side\n private hosting = false;\n private _started = false;\n private pending: FrameCommand[] = [];\n private _droppedCommands = 0;\n private ready = new Set<number>();\n private snapshotting = new Set<number>();\n private cancelTicker: (() => void) | null = null;\n private baseTime = 0;\n private emitted = 0;\n private ownHashes = new Map<number, bigint>();\n private peerHashes = new Map<number, Map<number, bigint>>();\n private desyncs = new Map<number, number>();\n private lastHashRx = new Map<number, number>();\n /** Hashes a peer sent for ticks up to this were computed before it restored a snapshot: stale, not evidence. */\n private staleUpTo = new Map<number, { from: number; to: number }>();\n\n // joiner side\n private pendingSnapshot: { tick: number; total: number; flags: number } | null = null;\n\n constructor(private hub: Hub, opts: LockstepOptions) {\n super();\n this.hashEveryTicks = Math.max(1, opts.hashEveryTicks);\n this.historyMax = opts.historyFrames ?? Math.max(60, hub.tickRate * 30);\n this.lastFrameRx = hub.clock.now();\n }\n\n // ---- public game-facing surface -------------------------------------------------------------------------------\n\n /** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */\n get droppedCommands(): number {\n return this._droppedCommands;\n }\n get started(): boolean {\n return this._started;\n }\n get framesBuffered(): number {\n return this.queue.length;\n }\n /** True when the game should run ticks without rendering (> 2 frames behind). */\n get catchingUp(): boolean {\n return this.queue.length > 2;\n }\n /** Tick of the game's current state. */\n get currentTick(): number {\n return this.consumedTick;\n }\n /** Highest frame tick known to this peer (0 when none yet; the host's `welcome_dc.tick`). */\n get lastTick(): number {\n return Math.max(0, this.lastFrameTick);\n }\n /** What `hi.lastTick` carries: -1 until the first frame/snapshot. */\n get hiTick(): number {\n return this.lastFrameTick;\n }\n\n /** Queue a command. Host: into its own next frame. Joiner: to the host (reliable). */\n submit(bytes: Uint8Array): void {\n if (this.hosting) {\n this.pending.push({ peerId: this.hub.localPeerId, bytes: bytes.slice() });\n } else {\n this.hub.sendToHost({ type: \"cmd\", seq: this.cmdSeq++ >>> 0, bytes });\n }\n }\n\n nextFrame(): Frame | null {\n const f = this.queue.shift() ?? null;\n if (f) this.consumedTick = f.tick;\n return f;\n }\n\n reportHash(tick: number, hash: bigint): void {\n if (this.awaitingSnapshot || tick <= 0 || tick % this.hashEveryTicks !== 0) return;\n if (this.hosting) {\n this.ownHashes.set(tick, hash);\n this.prune(this.ownHashes);\n const theirs = this.peerHashes.get(tick);\n if (theirs) {\n this.peerHashes.delete(tick);\n for (const [peer, h] of [...theirs].sort((a, b) => a[0] - b[0])) this.compare(peer, tick, hash, h);\n }\n } else {\n this.hub.sendToHost({ type: \"hash\", tick, hash });\n }\n }\n\n // ---- role control (called by Lobby) ---------------------------------------------------------------------------\n\n /** Host role without emitting yet (lobby phase): peers may already say hi. */\n hostIdle(): void {\n this.hosting = true;\n if (this.lastFrameTick < 0) this.lastFrameTick = 0;\n }\n\n /** Host begins emitting frames (game start, or after winning a migration), continuing after the last known tick. */\n hostStart(): void {\n this.hostIdle();\n this._started = true;\n this.emitted = 0;\n this.baseTime = this.hub.clock.now();\n this.cancelTicker?.();\n const interval = 1000 / this.hub.tickRate;\n this.cancelTicker = this.hub.clock.setInterval(() => this.pump(interval), Math.max(1, Math.floor(interval / 2)));\n }\n\n /** Winning a migration: continue from our last received frame. Pending commands are lost by design. */\n becomeHost(): void {\n this.pending = [];\n this.ready.clear();\n this.snapshotting.clear();\n this.ownHashes.clear();\n this.peerHashes.clear();\n this.desyncs.clear();\n this.awaitingSnapshot = false;\n this.hostIdle();\n if (this._started) this.hostStart();\n }\n\n /** Losing a claim, or a new host appeared: stop emitting. Keep history; the new host decides replay vs snapshot. */\n becomeJoiner(): void {\n this.hosting = false;\n this.cancelTicker?.();\n this.cancelTicker = null;\n this.pending = [];\n this.ready.clear();\n this.lastFrameRx = this.hub.clock.now();\n }\n\n /** A joiner just learned the session is in game (first frame, snapshot, or phase from the server). */\n markStarted(): void {\n if (this._started) return;\n this._started = true;\n this.emit(\"started\");\n }\n\n stop(): void {\n this.cancelTicker?.();\n this.cancelTicker = null;\n }\n\n onPeerLeft(peerId: number): void {\n this.ready.delete(peerId);\n this.snapshotting.delete(peerId);\n this.desyncs.delete(peerId);\n this.lastHashRx.delete(peerId);\n this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.pending = this.pending.filter((c) => c.peerId !== peerId);\n }\n\n // ---- host: frame emission ---------------------------------------------------------------------------------------\n\n private pump(interval: number): void {\n if (!this.hosting) return;\n const now = this.hub.clock.now();\n let due = Math.floor((now - this.baseTime + 1) / interval) - this.emitted;\n if (due > 10) {\n // Starved (e.g. a throttled tab): do not flood. Rebase rather than emit a burst of empty frames.\n this.baseTime = now - this.emitted * interval;\n due = 1;\n }\n for (let i = 0; i < due; i++) this.emitFrame();\n }\n\n private emitFrame(): void {\n // sorted by peerId, stable: one peer's commands keep their arrival order\n const commands = this.pending.splice(0).sort((a, b) => a.peerId - b.peerId);\n const frame: Frame = { tick: this.lastFrameTick + 1, commands };\n this.emitted++;\n this.lastFrameTick = frame.tick;\n this.record(frame);\n this.queue.push(frame);\n const msg: Message = { type: \"frame\", tick: frame.tick, commands };\n for (const p of this.ready) this.hub.sendTo(p, msg, 0);\n this.checkPeerSilence();\n }\n\n private record(f: Frame): void {\n this.history.push(f);\n if (this.history.length > this.historyMax) this.history.splice(0, this.history.length - this.historyMax);\n }\n\n /** The host drops a peer it has had no `hash` from for 10 s (scaled up when hashes are rare). */\n private checkPeerSilence(): void {\n if (this.ownHashes.size === 0) return;\n const now = this.hub.clock.now();\n const limit = Math.max(PEER_HASH_SILENCE_MS, (this.hashEveryTicks / this.hub.tickRate) * 1000 * 3);\n for (const p of [...this.ready]) {\n const last = this.lastHashRx.get(p) ?? now;\n if (now - last > limit) this.hub.dropPeer(p, \"timeout\");\n }\n }\n\n // ---- message handling -------------------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"cmd\":\n if (this.hosting) {\n let waiting = 0;\n for (const c of this.pending) if (c.peerId === from) waiting++;\n if (waiting >= MAX_PENDING_PER_PEER) this._droppedCommands++;\n else this.pending.push({ peerId: from, bytes: msg.bytes });\n }\n break;\n case \"frame\":\n if (!this.hosting) this.onFrame(msg.tick, msg.commands);\n break;\n case \"hash\":\n if (this.hosting) this.onPeerHash(from, msg.tick, msg.hash);\n break;\n case \"desync\":\n if (!this.hosting) {\n this.awaitingSnapshot = true;\n this.queue = [];\n this.emit(\"desync\", { tick: msg.tick, hostHash: msg.hostHash, yourHash: msg.yourHash });\n }\n break;\n case \"snapshot_req\":\n if (this.hosting) void this.sendSnapshot(from);\n break;\n case \"snapshot\":\n if (!this.hosting) {\n this.markStarted();\n this.pendingSnapshot = { tick: msg.tick, total: msg.total, flags: msg.flags };\n }\n break;\n }\n }\n\n /** True when a `snapshot` header was seen and its chunk set has not completed yet. */\n get snapshotPending(): boolean {\n return this.pendingSnapshot !== null;\n }\n\n /** The reassembled chunk set that follows a `snapshot` header. */\n onSnapshotPayload(payload: Uint8Array): void {\n const ps = this.pendingSnapshot;\n this.pendingSnapshot = null;\n if (!ps) return;\n if (payload.length !== ps.total) {\n // spec: a receiver that gets a different sum drops the snapshot and re-requests\n this.awaitingSnapshot = false;\n this.requestSnapshot();\n return;\n }\n void this.applySnapshot(ps, payload);\n }\n\n private onFrame(tick: number, commands: FrameCommand[]): void {\n this.lastFrameRx = this.hub.clock.now();\n this.markStarted();\n if (this.awaitingSnapshot) return;\n const expected = Math.max(0, this.lastFrameTick) + 1;\n if (tick < expected) return; // duplicate (replay overlap)\n if (tick > expected) {\n this.requestSnapshot(); // gap: the host changed under us\n return;\n }\n const f: Frame = { tick, commands };\n this.lastFrameTick = tick;\n this.record(f);\n this.queue.push(f);\n if (this.queue.length > this.hub.tickRate * 3) this.requestSnapshot();\n }\n\n /** Joiner asks the host for a snapshot (far behind, gap, silent host). Throttled while one is outstanding. */\n requestSnapshot(): void {\n const now = this.hub.clock.now();\n if (this.awaitingSnapshot && now - this.lastSnapshotReq < 2000) return;\n this.lastSnapshotReq = now;\n this.awaitingSnapshot = true;\n this.queue = [];\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n /** 3 s without frames: ask the host for a snapshot without discarding anything (a live host answers). */\n pokeHost(): void {\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n private async applySnapshot(ps: { tick: number; total: number; flags: number }, payload: Uint8Array): Promise<void> {\n try {\n let bytes = payload;\n if (ps.flags & 1) bytes = await inflate(payload);\n if (!this.restore) throw new Error(\"lockstep.restore is not set; cannot apply a snapshot\");\n // Restore first: if the game's restore() throws, our bookkeeping must not have moved.\n this.restore(ps.tick, bytes);\n this.queue = [];\n this.history = [];\n this.lastFrameTick = ps.tick;\n this.consumedTick = ps.tick;\n this.awaitingSnapshot = false;\n this.restoreFailures = 0;\n this.markStarted();\n this.lastFrameRx = this.hub.clock.now();\n this.emit(\"restored\", ps.tick);\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n // The game's state is unknown after a failed restore: keep our tick where it was, drop frames, ask again.\n if (++this.restoreFailures <= MAX_RESTORE_RETRIES) {\n this.awaitingSnapshot = false;\n this.lastSnapshotReq = -Infinity;\n this.requestSnapshot();\n } else {\n this.hub.reportError(new Error(`snapshot restore failed ${this.restoreFailures} times; giving up`));\n }\n }\n }\n\n // ---- host: hello, hashes, snapshots -----------------------------------------------------------------------------\n\n /** Host: a peer said hi (welcome_dc and chat_history already sent). Decide: nothing, replay, or snapshot. */\n onPeerHi(peerId: number, lastTick: number): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n const hostTick = this.lastFrameTick;\n const first = this.history.length ? this.history[0].tick : hostTick + 1;\n if (lastTick < 0) {\n // fresh joiner: initial state if no game is running, otherwise a snapshot\n if (this._started && hostTick > 0) void this.sendSnapshot(peerId);\n else this.ready.add(peerId);\n return;\n }\n if (lastTick <= hostTick && lastTick + 1 >= first) {\n // The peer's timeline is a prefix of ours: replay what it lacks, then go live.\n for (const f of this.history) {\n if (f.tick > lastTick) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n this.ready.add(peerId);\n } else {\n void this.sendSnapshot(peerId);\n }\n }\n\n private onPeerHash(peerId: number, tick: number, hash: bigint): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n if (this.isStale(peerId, tick)) return;\n const own = this.ownHashes.get(tick);\n if (own === undefined) {\n let m = this.peerHashes.get(tick);\n if (!m) this.peerHashes.set(tick, (m = new Map()));\n m.set(peerId, hash);\n this.prune(this.peerHashes);\n return;\n }\n this.compare(peerId, tick, own, hash);\n }\n\n private compare(peerId: number, tick: number, hostHash: bigint, yourHash: bigint): void {\n if (hostHash === yourHash) return;\n if (this.isStale(peerId, tick)) return;\n if (!this.ready.has(peerId) || this.snapshotting.has(peerId)) return; // already being resynced\n const n = (this.desyncs.get(peerId) ?? 0) + 1;\n this.desyncs.set(peerId, n);\n if (n >= MAX_DESYNCS) {\n this.hub.dropPeer(peerId, \"desync\");\n return;\n }\n this.hub.sendTo(peerId, { type: \"desync\", tick, hostHash, yourHash }, 0);\n void this.sendSnapshot(peerId, tick);\n }\n\n /** Hashes computed before a peer restored its snapshot (ticks after the strike, up to what we had emitted) are not evidence. */\n private isStale(peerId: number, tick: number): boolean {\n const w = this.staleUpTo.get(peerId);\n return !!w && tick > w.from && tick <= w.to;\n }\n\n private async sendSnapshot(peerId: number, strikeTick = Infinity): Promise<void> {\n if (this.snapshotting.has(peerId)) return;\n if (!this.snapshot) {\n this.hub.reportError(new Error(\"lockstep.snapshot is not set; cannot serve a snapshot\"));\n return;\n }\n this.snapshotting.add(peerId);\n this.ready.delete(peerId); // live frames now flow only through the replay below\n try {\n const S = this.consumedTick;\n const raw = this.snapshot();\n let payload = raw;\n let flags = 0;\n if (raw.length >= SNAPSHOT_COMPRESS_MIN && deflateAvailable()) {\n payload = await deflate(raw);\n flags = 1;\n }\n if (!this.snapshotting.has(peerId)) return; // peer left while compressing\n // header + one contiguous chunk set + frames S+1... with nothing else in between\n this.hub.sendTo(peerId, { type: \"snapshot\", tick: S, total: payload.length, flags }, 0);\n for (const c of splitChunks(this.hub.nextChunkId(), payload, this.chunkSize)) this.hub.sendTo(peerId, c, 0);\n for (const f of this.history) {\n if (f.tick > S) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n if (strikeTick !== Infinity) this.staleUpTo.set(peerId, { from: strikeTick, to: this.lastFrameTick });\n else this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.ready.add(peerId);\n this.lastHashRx.set(peerId, this.hub.clock.now());\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n this.snapshotting.delete(peerId);\n }\n }\n\n private prune<T>(m: Map<number, T>): void {\n if (m.size <= 64) return;\n const keys = [...m.keys()].sort((a, b) => a - b);\n for (const k of keys.slice(0, keys.length - 64)) m.delete(k);\n }\n\n // ---- joiner watchdog --------------------------------------------------------------------------------------------\n\n /** Joiner: ms since the last frame from the host (only meaningful once in game). */\n get msSinceFrame(): number {\n return this.hub.clock.now() - this.lastFrameRx;\n }\n\n /** True when a started joiner has heard no frames for 3 s: the Lobby then runs the host-loss rule (s.9). */\n hostSilent(): boolean {\n return !this.hosting && this._started && this.msSinceFrame > HOST_SILENCE_MS;\n }\n\n /** Reset the silence timer, e.g. after the link to a new host opened. */\n touch(): void {\n this.lastFrameRx = this.hub.clock.now();\n }\n\n hiMessage(peerId: number): Message {\n return { type: \"hi\", proto: PROTO, peerId, lastTick: this.lastFrameTick };\n }\n\n get resyncCount(): Map<number, number> {\n return this.desyncs;\n }\n}\n"]}
1
+ {"version":3,"file":"lockstep.js","sourceRoot":"","sources":["../src/lockstep.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHUNK_PAYLOAD, splitChunks } from \"./chunking.js\";\nimport { PROTO, type FrameCommand, type Message } from \"./codec.js\";\nimport { deflate, deflateAvailable, inflate } from \"./deflate.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\n\nexport interface Frame {\n tick: number;\n commands: FrameCommand[];\n}\n\nexport interface DesyncInfo {\n tick: number;\n hostHash: bigint;\n yourHash: bigint;\n}\n\nexport type LockstepEvents = {\n desync: [DesyncInfo];\n /** A snapshot was applied (late join, desync, falling too far behind). */\n restored: [tick: number];\n /** First frame (or snapshot) of the game reached this peer: the session is in game. */\n started: [];\n error: [Error];\n};\n\nexport const MAX_DESYNCS = 3;\nexport const MAX_RESTORE_RETRIES = 3;\nexport const HOST_SILENCE_MS = 3000;\nexport const PEER_HASH_SILENCE_MS = 10_000;\n/** Commands one remote peer may have waiting for the host's next frame; the excess is dropped. */\nexport const MAX_PENDING_PER_PEER = 256;\nconst SNAPSHOT_COMPRESS_MIN = 1024;\n\nexport interface LockstepOptions {\n hashEveryTicks: number;\n /** Frames kept for replays (late join, migration). Default 30 s of ticks. */\n historyFrames?: number;\n}\n\n/**\n * Host-ordered lockstep (spec/binary.md, DESIGN s.8). One class for both roles: the host emits `frame(T)` on a fixed\n * cadence and never waits; joiners queue frames and hand them out through `nextFrame()`.\n *\n * Ticks: the initial state is tick 0 and has no frame; `frame(T)` produces tick T. `reportHash(tick, h)` is called\n * after applying `frame.tick` (the library only sends when `tick % hashEveryTicks == 0`, tick > 0). `snapshot()` must\n * return the state after the last frame returned by `nextFrame()`; `restore(tick, bytes)` receives that tick, and the\n * next frame applied is `tick + 1`.\n */\nexport class Lockstep extends Emitter<LockstepEvents> {\n snapshot: (() => Uint8Array) | null = null;\n restore: ((tick: number, bytes: Uint8Array) => void) | null = null;\n\n readonly hashEveryTicks: number;\n /** Chunk body size used when serving snapshots. */\n chunkSize = CHUNK_PAYLOAD;\n private historyMax: number;\n\n private queue: Frame[] = [];\n private history: Frame[] = [];\n /** Highest frame tick received/emitted; -1 = no state (a fresh joiner). */\n private lastFrameTick = -1;\n /** Tick of the state the game holds (last frame returned by nextFrame, or a restored snapshot). */\n private consumedTick = 0;\n private cmdSeq = 1;\n private awaitingSnapshot = false;\n private lastFrameRx = 0;\n private lastSnapshotReq = -Infinity;\n private restoreFailures = 0;\n\n // host side\n private hosting = false;\n private _started = false;\n private pending: FrameCommand[] = [];\n private _droppedCommands = 0;\n private ready = new Set<number>();\n private snapshotting = new Set<number>();\n private cancelTicker: (() => void) | null = null;\n private baseTime = 0;\n private emitted = 0;\n private held = false;\n private ownHashes = new Map<number, bigint>();\n private peerHashes = new Map<number, Map<number, bigint>>();\n private desyncs = new Map<number, number>();\n private lastHashRx = new Map<number, number>();\n /** Hashes a peer sent for ticks up to this were computed before it restored a snapshot: stale, not evidence. */\n private staleUpTo = new Map<number, { from: number; to: number }>();\n\n // joiner side\n private pendingSnapshot: { tick: number; total: number; flags: number } | null = null;\n\n constructor(private hub: Hub, opts: LockstepOptions) {\n super();\n this.hashEveryTicks = Math.max(1, opts.hashEveryTicks);\n this.historyMax = opts.historyFrames ?? Math.max(60, hub.tickRate * 30);\n this.lastFrameRx = hub.clock.now();\n }\n\n // ---- public game-facing surface -------------------------------------------------------------------------------\n\n /** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */\n get droppedCommands(): number {\n return this._droppedCommands;\n }\n get started(): boolean {\n return this._started;\n }\n get framesBuffered(): number {\n return this.queue.length;\n }\n /** True when the game should run ticks without rendering (> 2 frames behind). */\n get catchingUp(): boolean {\n return this.queue.length > 2;\n }\n /** Tick of the game's current state. */\n get currentTick(): number {\n return this.consumedTick;\n }\n /** Highest frame tick known to this peer (0 when none yet; the host's `welcome_dc.tick`). */\n get lastTick(): number {\n return Math.max(0, this.lastFrameTick);\n }\n /** What `hi.lastTick` carries: -1 until the first frame/snapshot. */\n get hiTick(): number {\n return this.lastFrameTick;\n }\n\n /** Queue a command. Host: into its own next frame. Joiner: to the host (reliable). */\n submit(bytes: Uint8Array): void {\n if (this.hosting) {\n this.pending.push({ peerId: this.hub.localPeerId, bytes: bytes.slice() });\n } else {\n this.hub.sendToHost({ type: \"cmd\", seq: this.cmdSeq++ >>> 0, bytes });\n }\n }\n\n nextFrame(): Frame | null {\n const f = this.queue.shift() ?? null;\n if (f) this.consumedTick = f.tick;\n return f;\n }\n\n reportHash(tick: number, hash: bigint): void {\n if (this.awaitingSnapshot || tick <= 0 || tick % this.hashEveryTicks !== 0) return;\n if (this.hosting) {\n this.ownHashes.set(tick, hash);\n this.prune(this.ownHashes);\n const theirs = this.peerHashes.get(tick);\n if (theirs) {\n this.peerHashes.delete(tick);\n for (const [peer, h] of [...theirs].sort((a, b) => a[0] - b[0])) this.compare(peer, tick, hash, h);\n }\n } else {\n this.hub.sendToHost({ type: \"hash\", tick, hash });\n }\n }\n\n // ---- role control (called by Lobby) ---------------------------------------------------------------------------\n\n /** Host role without emitting yet (lobby phase): peers may already say hi. */\n hostIdle(): void {\n this.hosting = true;\n if (this.lastFrameTick < 0) this.lastFrameTick = 0;\n }\n\n /** Host begins emitting frames (game start, or after winning a migration), continuing after the last known tick. */\n hostStart(): void {\n this.hostIdle();\n this._started = true;\n this.emitted = 0;\n this.baseTime = this.hub.clock.now();\n this.cancelTicker?.();\n const interval = 1000 / this.hub.tickRate;\n this.cancelTicker = this.hub.clock.setInterval(() => this.pump(interval), Math.max(1, Math.floor(interval / 2)));\n }\n\n /** Winning a migration: continue from our last received frame. Pending commands are lost by design. */\n becomeHost(): void {\n this.pending = [];\n this.ready.clear();\n this.snapshotting.clear();\n this.ownHashes.clear();\n this.peerHashes.clear();\n this.desyncs.clear();\n this.awaitingSnapshot = false;\n this.hostIdle();\n if (this._started) this.hostStart();\n }\n\n /** Losing a claim, or a new host appeared: stop emitting. Keep history; the new host decides replay vs snapshot. */\n becomeJoiner(): void {\n this.hosting = false;\n this.cancelTicker?.();\n this.cancelTicker = null;\n this.pending = [];\n this.ready.clear();\n this.lastFrameRx = this.hub.clock.now();\n }\n\n /** A joiner just learned the session is in game (first frame, snapshot, or phase from the server). */\n markStarted(): void {\n if (this._started) return;\n this._started = true;\n this.emit(\"started\");\n }\n\n /** The shared pause is on: the host emits no frames and a joiner expects none (no silent-host check). */\n get isHeld(): boolean {\n return this.held;\n }\n\n /**\n * Pause (true) or resume (false) frame emission (the shared pause calls this; not for games). Resuming rebases the\n * host's tick clock to now, so the next frame is simply tick+1 (no gap, no burst) and nobody counts as silent.\n */\n hold(held: boolean): void {\n if (this.held === held) return;\n this.held = held;\n if (held) return;\n const now = this.hub.clock.now();\n this.baseTime = now - this.emitted * (1000 / this.hub.tickRate);\n this.lastFrameRx = now;\n for (const p of this.ready) this.lastHashRx.set(p, now);\n }\n\n stop(): void {\n this.cancelTicker?.();\n this.cancelTicker = null;\n }\n\n onPeerLeft(peerId: number): void {\n this.ready.delete(peerId);\n this.snapshotting.delete(peerId);\n this.desyncs.delete(peerId);\n this.lastHashRx.delete(peerId);\n this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.pending = this.pending.filter((c) => c.peerId !== peerId);\n }\n\n // ---- host: frame emission ---------------------------------------------------------------------------------------\n\n private pump(interval: number): void {\n if (!this.hosting || this.held) return;\n const now = this.hub.clock.now();\n let due = Math.floor((now - this.baseTime + 1) / interval) - this.emitted;\n if (due > 10) {\n // Starved (e.g. a throttled tab): do not flood. Rebase rather than emit a burst of empty frames.\n this.baseTime = now - this.emitted * interval;\n due = 1;\n }\n for (let i = 0; i < due; i++) this.emitFrame();\n }\n\n private emitFrame(): void {\n // sorted by peerId, stable: one peer's commands keep their arrival order\n const commands = this.pending.splice(0).sort((a, b) => a.peerId - b.peerId);\n const frame: Frame = { tick: this.lastFrameTick + 1, commands };\n this.emitted++;\n this.lastFrameTick = frame.tick;\n this.record(frame);\n this.queue.push(frame);\n const msg: Message = { type: \"frame\", tick: frame.tick, commands };\n for (const p of this.ready) this.hub.sendTo(p, msg, 0);\n this.checkPeerSilence();\n }\n\n private record(f: Frame): void {\n this.history.push(f);\n if (this.history.length > this.historyMax) this.history.splice(0, this.history.length - this.historyMax);\n }\n\n /** The host drops a peer it has had no `hash` from for 10 s (scaled up when hashes are rare). */\n private checkPeerSilence(): void {\n if (this.ownHashes.size === 0) return;\n const now = this.hub.clock.now();\n const limit = Math.max(PEER_HASH_SILENCE_MS, (this.hashEveryTicks / this.hub.tickRate) * 1000 * 3);\n for (const p of [...this.ready]) {\n const last = this.lastHashRx.get(p) ?? now;\n if (now - last > limit) this.hub.dropPeer(p, \"timeout\");\n }\n }\n\n // ---- message handling -------------------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"cmd\":\n if (this.hosting) {\n let waiting = 0;\n for (const c of this.pending) if (c.peerId === from) waiting++;\n if (waiting >= MAX_PENDING_PER_PEER) this._droppedCommands++;\n else this.pending.push({ peerId: from, bytes: msg.bytes });\n }\n break;\n case \"frame\":\n if (!this.hosting) this.onFrame(msg.tick, msg.commands);\n break;\n case \"hash\":\n if (this.hosting) this.onPeerHash(from, msg.tick, msg.hash);\n break;\n case \"desync\":\n if (!this.hosting) {\n this.awaitingSnapshot = true;\n this.queue = [];\n this.emit(\"desync\", { tick: msg.tick, hostHash: msg.hostHash, yourHash: msg.yourHash });\n }\n break;\n case \"snapshot_req\":\n if (this.hosting) void this.sendSnapshot(from);\n break;\n case \"snapshot\":\n if (!this.hosting) {\n this.markStarted();\n this.pendingSnapshot = { tick: msg.tick, total: msg.total, flags: msg.flags };\n }\n break;\n }\n }\n\n /** True when a `snapshot` header was seen and its chunk set has not completed yet. */\n get snapshotPending(): boolean {\n return this.pendingSnapshot !== null;\n }\n\n /** The reassembled chunk set that follows a `snapshot` header. */\n onSnapshotPayload(payload: Uint8Array): void {\n const ps = this.pendingSnapshot;\n this.pendingSnapshot = null;\n if (!ps) return;\n if (payload.length !== ps.total) {\n // spec: a receiver that gets a different sum drops the snapshot and re-requests\n this.awaitingSnapshot = false;\n this.requestSnapshot();\n return;\n }\n void this.applySnapshot(ps, payload);\n }\n\n private onFrame(tick: number, commands: FrameCommand[]): void {\n this.lastFrameRx = this.hub.clock.now();\n this.markStarted();\n if (this.awaitingSnapshot) return;\n const expected = Math.max(0, this.lastFrameTick) + 1;\n if (tick < expected) return; // duplicate (replay overlap)\n if (tick > expected) {\n this.requestSnapshot(); // gap: the host changed under us\n return;\n }\n const f: Frame = { tick, commands };\n this.lastFrameTick = tick;\n this.record(f);\n this.queue.push(f);\n if (this.queue.length > this.hub.tickRate * 3) this.requestSnapshot();\n }\n\n /** Joiner asks the host for a snapshot (far behind, gap, silent host). Throttled while one is outstanding. */\n requestSnapshot(): void {\n const now = this.hub.clock.now();\n if (this.awaitingSnapshot && now - this.lastSnapshotReq < 2000) return;\n this.lastSnapshotReq = now;\n this.awaitingSnapshot = true;\n this.queue = [];\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n /** 3 s without frames: ask the host for a snapshot without discarding anything (a live host answers). */\n pokeHost(): void {\n this.hub.sendToHost({ type: \"snapshot_req\", sinceTick: this.lastFrameTick });\n }\n\n private async applySnapshot(ps: { tick: number; total: number; flags: number }, payload: Uint8Array): Promise<void> {\n try {\n let bytes = payload;\n if (ps.flags & 1) bytes = await inflate(payload);\n if (!this.restore) throw new Error(\"lockstep.restore is not set; cannot apply a snapshot\");\n // Restore first: if the game's restore() throws, our bookkeeping must not have moved.\n this.restore(ps.tick, bytes);\n this.queue = [];\n this.history = [];\n this.lastFrameTick = ps.tick;\n this.consumedTick = ps.tick;\n this.awaitingSnapshot = false;\n this.restoreFailures = 0;\n this.markStarted();\n this.lastFrameRx = this.hub.clock.now();\n this.emit(\"restored\", ps.tick);\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n // The game's state is unknown after a failed restore: keep our tick where it was, drop frames, ask again.\n if (++this.restoreFailures <= MAX_RESTORE_RETRIES) {\n this.awaitingSnapshot = false;\n this.lastSnapshotReq = -Infinity;\n this.requestSnapshot();\n } else {\n this.hub.reportError(new Error(`snapshot restore failed ${this.restoreFailures} times; giving up`));\n }\n }\n }\n\n // ---- host: hello, hashes, snapshots -----------------------------------------------------------------------------\n\n /** Host: a peer said hi (welcome_dc and chat_history already sent). Decide: nothing, replay, or snapshot. */\n onPeerHi(peerId: number, lastTick: number): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n const hostTick = this.lastFrameTick;\n const first = this.history.length ? this.history[0].tick : hostTick + 1;\n if (lastTick < 0) {\n // fresh joiner: initial state if no game is running, otherwise a snapshot\n if (this._started && hostTick > 0) void this.sendSnapshot(peerId);\n else this.ready.add(peerId);\n return;\n }\n if (lastTick <= hostTick && lastTick + 1 >= first) {\n // The peer's timeline is a prefix of ours: replay what it lacks, then go live.\n for (const f of this.history) {\n if (f.tick > lastTick) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n this.ready.add(peerId);\n } else {\n void this.sendSnapshot(peerId);\n }\n }\n\n private onPeerHash(peerId: number, tick: number, hash: bigint): void {\n this.lastHashRx.set(peerId, this.hub.clock.now());\n if (this.isStale(peerId, tick)) return;\n const own = this.ownHashes.get(tick);\n if (own === undefined) {\n let m = this.peerHashes.get(tick);\n if (!m) this.peerHashes.set(tick, (m = new Map()));\n m.set(peerId, hash);\n this.prune(this.peerHashes);\n return;\n }\n this.compare(peerId, tick, own, hash);\n }\n\n private compare(peerId: number, tick: number, hostHash: bigint, yourHash: bigint): void {\n if (hostHash === yourHash) return;\n if (this.isStale(peerId, tick)) return;\n if (!this.ready.has(peerId) || this.snapshotting.has(peerId)) return; // already being resynced\n const n = (this.desyncs.get(peerId) ?? 0) + 1;\n this.desyncs.set(peerId, n);\n if (n >= MAX_DESYNCS) {\n this.hub.dropPeer(peerId, \"desync\");\n return;\n }\n this.hub.sendTo(peerId, { type: \"desync\", tick, hostHash, yourHash }, 0);\n void this.sendSnapshot(peerId, tick);\n }\n\n /** Hashes computed before a peer restored its snapshot (ticks after the strike, up to what we had emitted) are not evidence. */\n private isStale(peerId: number, tick: number): boolean {\n const w = this.staleUpTo.get(peerId);\n return !!w && tick > w.from && tick <= w.to;\n }\n\n private async sendSnapshot(peerId: number, strikeTick = Infinity): Promise<void> {\n if (this.snapshotting.has(peerId)) return;\n if (!this.snapshot) {\n this.hub.reportError(new Error(\"lockstep.snapshot is not set; cannot serve a snapshot\"));\n return;\n }\n this.snapshotting.add(peerId);\n this.ready.delete(peerId); // live frames now flow only through the replay below\n try {\n const S = this.consumedTick;\n const raw = this.snapshot();\n let payload = raw;\n let flags = 0;\n if (raw.length >= SNAPSHOT_COMPRESS_MIN && deflateAvailable()) {\n payload = await deflate(raw);\n flags = 1;\n }\n if (!this.snapshotting.has(peerId)) return; // peer left while compressing\n // header + one contiguous chunk set + frames S+1... with nothing else in between\n this.hub.sendTo(peerId, { type: \"snapshot\", tick: S, total: payload.length, flags }, 0);\n for (const c of splitChunks(this.hub.nextChunkId(), payload, this.chunkSize)) this.hub.sendTo(peerId, c, 0);\n for (const f of this.history) {\n if (f.tick > S) this.hub.sendTo(peerId, { type: \"frame\", tick: f.tick, commands: f.commands }, 0);\n }\n if (strikeTick !== Infinity) this.staleUpTo.set(peerId, { from: strikeTick, to: this.lastFrameTick });\n else this.staleUpTo.delete(peerId);\n for (const m of this.peerHashes.values()) m.delete(peerId);\n this.ready.add(peerId);\n this.lastHashRx.set(peerId, this.hub.clock.now());\n } catch (e) {\n this.hub.reportError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n this.snapshotting.delete(peerId);\n }\n }\n\n private prune<T>(m: Map<number, T>): void {\n if (m.size <= 64) return;\n const keys = [...m.keys()].sort((a, b) => a - b);\n for (const k of keys.slice(0, keys.length - 64)) m.delete(k);\n }\n\n // ---- joiner watchdog --------------------------------------------------------------------------------------------\n\n /** Joiner: ms since the last frame from the host (only meaningful once in game). */\n get msSinceFrame(): number {\n return this.hub.clock.now() - this.lastFrameRx;\n }\n\n /** True when a started joiner has heard no frames for 3 s: the Lobby then runs the host-loss rule (s.9). */\n hostSilent(): boolean {\n return !this.hosting && !this.held && this._started && this.msSinceFrame > HOST_SILENCE_MS;\n }\n\n /** Reset the silence timer, e.g. after the link to a new host opened. */\n touch(): void {\n this.lastFrameRx = this.hub.clock.now();\n }\n\n hiMessage(peerId: number): Message {\n return { type: \"hi\", proto: PROTO, peerId, lastTick: this.lastFrameTick };\n }\n\n get resyncCount(): Map<number, number> {\n return this.desyncs;\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /** WebCrypto helpers for Nostr signaling (spec/nostr-signaling.md s.2-3); Node 18+ and every browser have it. */
2
+ export declare function concatBytes(...parts: Uint8Array[]): Uint8Array;
3
+ export declare const toHex: (b: Uint8Array) => string;
4
+ export declare function fromHex(s: string): Uint8Array;
5
+ export declare function toBase64(b: Uint8Array): string;
6
+ export declare function fromBase64(s: string): Uint8Array;
7
+ export declare function sha256(data: Uint8Array): Promise<Uint8Array>;
8
+ /** HKDF-SHA256 (RFC 5869) with an ASCII salt and info, `length` bytes. */
9
+ export declare function hkdf(ikm: Uint8Array, salt: string, info: string, length?: number): Promise<Uint8Array>;
10
+ /** Keys of one room (s.2): `key` encrypts frames, `topic` is the relay-visible tag. */
11
+ export interface RoomKeys {
12
+ key: Uint8Array;
13
+ topic: string;
14
+ }
15
+ /** `normalizedCode` is the 11 normalised code characters (no hyphens, upper case). */
16
+ export declare function deriveRoomKeys(normalizedCode: string): Promise<RoomKeys>;
17
+ /** base64( iv(12) || ciphertext || tag(16) ), no associated data. `iv` is fresh random unless a test pins it. */
18
+ export declare function encryptContent(key: Uint8Array, plaintext: string, iv?: Uint8Array): Promise<string>;
19
+ /** Plain text, or null when the content is not valid base64, too short or fails the GCM tag. */
20
+ export declare function decryptContent(key: Uint8Array, content: string): Promise<string | null>;
@@ -0,0 +1,78 @@
1
+ /** WebCrypto helpers for Nostr signaling (spec/nostr-signaling.md s.2-3); Node 18+ and every browser have it. */
2
+ const subtle = () => {
3
+ const s = globalThis.crypto?.subtle;
4
+ if (!s)
5
+ throw new Error("WebCrypto (crypto.subtle) is not available; Nostr signaling needs a secure context or Node 18+");
6
+ return s;
7
+ };
8
+ const buf = (b) => b;
9
+ const utf8 = new TextEncoder();
10
+ export function concatBytes(...parts) {
11
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
12
+ let o = 0;
13
+ for (const p of parts) {
14
+ out.set(p, o);
15
+ o += p.length;
16
+ }
17
+ return out;
18
+ }
19
+ export const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
20
+ export function fromHex(s) {
21
+ if (s.length % 2 || /[^0-9a-fA-F]/.test(s))
22
+ throw new Error("bad hex");
23
+ const out = new Uint8Array(s.length / 2);
24
+ for (let i = 0; i < out.length; i++)
25
+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
26
+ return out;
27
+ }
28
+ export function toBase64(b) {
29
+ let s = "";
30
+ for (let i = 0; i < b.length; i += 0x8000)
31
+ s += String.fromCharCode(...b.subarray(i, i + 0x8000));
32
+ return btoa(s);
33
+ }
34
+ export function fromBase64(s) {
35
+ const bin = atob(s);
36
+ const out = new Uint8Array(bin.length);
37
+ for (let i = 0; i < bin.length; i++)
38
+ out[i] = bin.charCodeAt(i);
39
+ return out;
40
+ }
41
+ export async function sha256(data) {
42
+ return new Uint8Array(await subtle().digest("SHA-256", buf(data)));
43
+ }
44
+ /** HKDF-SHA256 (RFC 5869) with an ASCII salt and info, `length` bytes. */
45
+ export async function hkdf(ikm, salt, info, length = 32) {
46
+ const key = await subtle().importKey("raw", buf(ikm), "HKDF", false, ["deriveBits"]);
47
+ const bits = await subtle().deriveBits({ name: "HKDF", hash: "SHA-256", salt: buf(utf8.encode(salt)), info: buf(utf8.encode(info)) }, key, length * 8);
48
+ return new Uint8Array(bits);
49
+ }
50
+ /** `normalizedCode` is the 11 normalised code characters (no hyphens, upper case). */
51
+ export async function deriveRoomKeys(normalizedCode) {
52
+ const ikm = utf8.encode(normalizedCode);
53
+ const salt = "onelobby-nostr-v1";
54
+ const [key, topic] = await Promise.all([hkdf(ikm, salt, "key"), hkdf(ikm, salt, "topic")]);
55
+ return { key, topic: toHex(topic) };
56
+ }
57
+ /** base64( iv(12) || ciphertext || tag(16) ), no associated data. `iv` is fresh random unless a test pins it. */
58
+ export async function encryptContent(key, plaintext, iv) {
59
+ const k = await subtle().importKey("raw", buf(key), "AES-GCM", false, ["encrypt"]);
60
+ const v = iv ?? globalThis.crypto.getRandomValues(new Uint8Array(12));
61
+ const ct = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv: buf(v) }, k, buf(utf8.encode(plaintext))));
62
+ return toBase64(concatBytes(v, ct));
63
+ }
64
+ /** Plain text, or null when the content is not valid base64, too short or fails the GCM tag. */
65
+ export async function decryptContent(key, content) {
66
+ try {
67
+ const raw = fromBase64(content);
68
+ if (raw.length < 12 + 16)
69
+ return null;
70
+ const k = await subtle().importKey("raw", buf(key), "AES-GCM", false, ["decrypt"]);
71
+ const pt = await subtle().decrypt({ name: "AES-GCM", iv: buf(raw.subarray(0, 12)) }, k, buf(raw.subarray(12)));
72
+ return new TextDecoder().decode(pt);
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/nostr/crypto.ts"],"names":[],"mappings":"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","sourcesContent":["/** WebCrypto helpers for Nostr signaling (spec/nostr-signaling.md s.2-3); Node 18+ and every browser have it. */\n\nconst subtle = (): SubtleCrypto => {\n const s = globalThis.crypto?.subtle;\n if (!s) throw new Error(\"WebCrypto (crypto.subtle) is not available; Nostr signaling needs a secure context or Node 18+\");\n return s;\n};\n\nconst buf = (b: Uint8Array): BufferSource => b as unknown as BufferSource;\nconst utf8 = new TextEncoder();\n\nexport function concatBytes(...parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n let o = 0;\n for (const p of parts) {\n out.set(p, o);\n o += p.length;\n }\n return out;\n}\n\nexport const toHex = (b: Uint8Array): string => Array.from(b, (x) => x.toString(16).padStart(2, \"0\")).join(\"\");\n\nexport function fromHex(s: string): Uint8Array {\n if (s.length % 2 || /[^0-9a-fA-F]/.test(s)) throw new Error(\"bad hex\");\n const out = new Uint8Array(s.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nexport function toBase64(b: Uint8Array): string {\n let s = \"\";\n for (let i = 0; i < b.length; i += 0x8000) s += String.fromCharCode(...b.subarray(i, i + 0x8000));\n return btoa(s);\n}\n\nexport function fromBase64(s: string): Uint8Array {\n const bin = atob(s);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nexport async function sha256(data: Uint8Array): Promise<Uint8Array> {\n return new Uint8Array(await subtle().digest(\"SHA-256\", buf(data)));\n}\n\n/** HKDF-SHA256 (RFC 5869) with an ASCII salt and info, `length` bytes. */\nexport async function hkdf(ikm: Uint8Array, salt: string, info: string, length = 32): Promise<Uint8Array> {\n const key = await subtle().importKey(\"raw\", buf(ikm), \"HKDF\", false, [\"deriveBits\"]);\n const bits = await subtle().deriveBits({ name: \"HKDF\", hash: \"SHA-256\", salt: buf(utf8.encode(salt)), info: buf(utf8.encode(info)) }, key, length * 8);\n return new Uint8Array(bits);\n}\n\n/** Keys of one room (s.2): `key` encrypts frames, `topic` is the relay-visible tag. */\nexport interface RoomKeys {\n key: Uint8Array;\n topic: string;\n}\n\n/** `normalizedCode` is the 11 normalised code characters (no hyphens, upper case). */\nexport async function deriveRoomKeys(normalizedCode: string): Promise<RoomKeys> {\n const ikm = utf8.encode(normalizedCode);\n const salt = \"onelobby-nostr-v1\";\n const [key, topic] = await Promise.all([hkdf(ikm, salt, \"key\"), hkdf(ikm, salt, \"topic\")]);\n return { key, topic: toHex(topic) };\n}\n\n/** base64( iv(12) || ciphertext || tag(16) ), no associated data. `iv` is fresh random unless a test pins it. */\nexport async function encryptContent(key: Uint8Array, plaintext: string, iv?: Uint8Array): Promise<string> {\n const k = await subtle().importKey(\"raw\", buf(key), \"AES-GCM\", false, [\"encrypt\"]);\n const v = iv ?? globalThis.crypto.getRandomValues(new Uint8Array(12));\n const ct = new Uint8Array(await subtle().encrypt({ name: \"AES-GCM\", iv: buf(v) }, k, buf(utf8.encode(plaintext))));\n return toBase64(concatBytes(v, ct));\n}\n\n/** Plain text, or null when the content is not valid base64, too short or fails the GCM tag. */\nexport async function decryptContent(key: Uint8Array, content: string): Promise<string | null> {\n try {\n const raw = fromBase64(content);\n if (raw.length < 12 + 16) return null;\n const k = await subtle().importKey(\"raw\", buf(key), \"AES-GCM\", false, [\"decrypt\"]);\n const pt = await subtle().decrypt({ name: \"AES-GCM\", iv: buf(raw.subarray(0, 12)) }, k, buf(raw.subarray(12)));\n return new TextDecoder().decode(pt);\n } catch {\n return null;\n }\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The host side of Nostr signaling (spec/nostr-signaling.md s.4): each joiner pubkey becomes one hub session, as if it
3
+ * had opened a TCP WebSocket. The hub speaks the usual protocol; this file only moves its texts over frames.
4
+ */
5
+ import type { Clock } from "../clock.js";
6
+ import type { SignalingHub } from "../direct/hub.js";
7
+ import type { RelayConnect } from "./relays.js";
8
+ import { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS } from "./limits.js";
9
+ export interface NostrHostOptions {
10
+ /** The version 1 join code this host publishes. */
11
+ code: string;
12
+ relays?: readonly string[];
13
+ connect?: RelayConnect;
14
+ clock: Clock;
15
+ wall?: () => number;
16
+ /** The number of connected relays changed. */
17
+ onChange?: () => void;
18
+ /** The lobby's player cap: the new-session burst grows to it (see newSessionBurst). */
19
+ maxPlayers?: number;
20
+ }
21
+ /** A lobby sized for more than NEW_SESSION_BURST players gets a burst that fits it (up to MAX_SESSION_BURST). */
22
+ export declare function newSessionBurst(maxPlayers?: number): number;
23
+ /**
24
+ * NEW joiner pubkeys per lobby are a token bucket: a burst of newSessionBurst(maxPlayers) at once (a party joining
25
+ * together), then one more per NEW_SESSION_REFILL_MS. Refused pubkeys wait in a list (up to WAITING_MAX, oldest
26
+ * first) and are answered the moment a token refills; a waiting pubkey whose `open` (repeated every 2 s) has not
27
+ * been heard for WAITING_STALE_MS has given up and is dropped.
28
+ */
29
+ export { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS };
30
+ export declare class NostrHostBridge {
31
+ private readonly hub;
32
+ private readonly room;
33
+ private readonly clock;
34
+ private readonly peers;
35
+ private readonly gone;
36
+ private stopped;
37
+ private readonly burst;
38
+ private newTokens;
39
+ private newTokensAt;
40
+ /** Refused new pubkeys, oldest first, with when their last `open` was heard. */
41
+ private readonly waiting;
42
+ private cancelWaiting;
43
+ private constructor();
44
+ static start(hub: SignalingHub, o: NostrHostOptions): Promise<NostrHostBridge>;
45
+ get relaysConnected(): number;
46
+ get relaysTotal(): number;
47
+ /** Resolves true once one relay is connected, false after `timeoutMs`. */
48
+ whenUp(timeoutMs: number): Promise<boolean>;
49
+ get relaysSettled(): boolean;
50
+ /** Waits for the relays: true once one is connected (then up to RELAY_REST_WAIT_MS more, less when every relay has answered), false after `firstRelayMs`. */
51
+ settle(firstRelayMs: number): Promise<boolean>;
52
+ /** Joiners currently attached. */
53
+ get peerCount(): number;
54
+ close(): void;
55
+ private onFrame;
56
+ /** Arms one timer for the next token while anyone waits. */
57
+ private scheduleWaiting;
58
+ /** Answers waiting pubkeys, oldest first, while tokens last; drops the ones that gave up. */
59
+ private admitWaiting;
60
+ /** Spends one token of the new-session bucket, or false when it is empty. */
61
+ private admitNew;
62
+ private answer;
63
+ private accept;
64
+ private detach;
65
+ private forget;
66
+ }
@@ -0,0 +1,196 @@
1
+ import { ReliableLink } from "./link.js";
2
+ import { NostrRoom } from "./room.js";
3
+ import { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS, WAITING_MAX, WAITING_STALE_MS, RELAY_REST_WAIT_MS, RELAY_REST_POLL_MS } from "./limits.js";
4
+ /** A lobby sized for more than NEW_SESSION_BURST players gets a burst that fits it (up to MAX_SESSION_BURST). */
5
+ export function newSessionBurst(maxPlayers) {
6
+ return Math.max(NEW_SESSION_BURST, Math.min(MAX_SESSION_BURST, maxPlayers ?? 0));
7
+ }
8
+ const PING = '{"t":"ping","n":0}';
9
+ const CLOSED_MEMORY = 512;
10
+ /**
11
+ * NEW joiner pubkeys per lobby are a token bucket: a burst of newSessionBurst(maxPlayers) at once (a party joining
12
+ * together), then one more per NEW_SESSION_REFILL_MS. Refused pubkeys wait in a list (up to WAITING_MAX, oldest
13
+ * first) and are answered the moment a token refills; a waiting pubkey whose `open` (repeated every 2 s) has not
14
+ * been heard for WAITING_STALE_MS has given up and is dropped.
15
+ */
16
+ export { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS };
17
+ export class NostrHostBridge {
18
+ hub;
19
+ room;
20
+ clock;
21
+ peers = new Map();
22
+ gone = new Set();
23
+ stopped = false;
24
+ burst;
25
+ newTokens;
26
+ newTokensAt = null;
27
+ /** Refused new pubkeys, oldest first, with when their last `open` was heard. */
28
+ waiting = new Map();
29
+ cancelWaiting = null;
30
+ constructor(hub, room, clock, maxPlayers) {
31
+ this.hub = hub;
32
+ this.room = room;
33
+ this.clock = clock;
34
+ this.burst = this.newTokens = newSessionBurst(maxPlayers);
35
+ room.onFrame = (f) => this.onFrame(f);
36
+ }
37
+ static async start(hub, o) {
38
+ return new NostrHostBridge(hub, await NostrRoom.open(o), o.clock, o.maxPlayers);
39
+ }
40
+ get relaysConnected() {
41
+ return this.room.relaysConnected;
42
+ }
43
+ get relaysTotal() {
44
+ return this.room.relaysTotal;
45
+ }
46
+ /** Resolves true once one relay is connected, false after `timeoutMs`. */
47
+ whenUp(timeoutMs) {
48
+ return this.room.whenUp(timeoutMs);
49
+ }
50
+ get relaysSettled() {
51
+ return this.room.relaysSettled;
52
+ }
53
+ /** Waits for the relays: true once one is connected (then up to RELAY_REST_WAIT_MS more, less when every relay has answered), false after `firstRelayMs`. */
54
+ async settle(firstRelayMs) {
55
+ if (!(await this.whenUp(firstRelayMs)))
56
+ return false;
57
+ for (let i = 0; i < RELAY_REST_WAIT_MS / RELAY_REST_POLL_MS && !this.relaysSettled; i++)
58
+ await new Promise((r) => setTimeout(r, RELAY_REST_POLL_MS));
59
+ return true;
60
+ }
61
+ /** Joiners currently attached. */
62
+ get peerCount() {
63
+ return this.peers.size;
64
+ }
65
+ close() {
66
+ if (this.stopped)
67
+ return;
68
+ this.stopped = true;
69
+ this.cancelWaiting?.();
70
+ this.cancelWaiting = null;
71
+ this.waiting.clear();
72
+ for (const [pub, p] of this.peers) {
73
+ p.link.close("host left");
74
+ if (p.session)
75
+ this.hub.disconnect(p.session);
76
+ this.peers.delete(pub);
77
+ }
78
+ this.room.close(true);
79
+ }
80
+ onFrame(f) {
81
+ if (this.stopped)
82
+ return;
83
+ const known = this.peers.get(f.from);
84
+ if (f.type === "open") {
85
+ if (this.gone.has(f.from))
86
+ return;
87
+ if (known) {
88
+ known.link.heard();
89
+ this.answer(f.from);
90
+ }
91
+ else if (this.waiting.has(f.from)) {
92
+ this.waiting.set(f.from, this.clock.now()); // still trying: keep its place in the queue
93
+ this.admitWaiting();
94
+ }
95
+ else if (this.waiting.size === 0 && this.admitNew()) {
96
+ this.accept(f.from);
97
+ }
98
+ else if (this.waiting.size < WAITING_MAX) {
99
+ this.waiting.set(f.from, this.clock.now()); // over the cap: answered when a token refills
100
+ this.scheduleWaiting();
101
+ } // waiting list full too: not answered, the joiner's next open retries
102
+ return;
103
+ }
104
+ if (!known)
105
+ return;
106
+ known.link.receive(f);
107
+ // a joiner's beat is its keep-alive: the hub counts it as activity (its pong is dropped below)
108
+ if (f.type === "beat" && known.session)
109
+ this.hub.receive(known.session, PING);
110
+ }
111
+ /** Arms one timer for the next token while anyone waits. */
112
+ scheduleWaiting() {
113
+ if (this.cancelWaiting || this.waiting.size === 0 || this.stopped)
114
+ return;
115
+ const missing = Math.max(0, 1 - this.newTokens);
116
+ this.cancelWaiting = this.clock.setTimeout(() => {
117
+ this.cancelWaiting = null;
118
+ this.admitWaiting();
119
+ }, Math.max(1, Math.ceil(missing * NEW_SESSION_REFILL_MS)));
120
+ }
121
+ /** Answers waiting pubkeys, oldest first, while tokens last; drops the ones that gave up. */
122
+ admitWaiting() {
123
+ if (this.stopped)
124
+ return;
125
+ const now = this.clock.now();
126
+ for (const [pub, heard] of this.waiting) {
127
+ if (now - heard > WAITING_STALE_MS || this.peers.has(pub) || this.gone.has(pub)) {
128
+ this.waiting.delete(pub);
129
+ continue;
130
+ }
131
+ if (!this.admitNew())
132
+ break;
133
+ this.waiting.delete(pub);
134
+ this.accept(pub);
135
+ }
136
+ this.scheduleWaiting();
137
+ }
138
+ /** Spends one token of the new-session bucket, or false when it is empty. */
139
+ admitNew() {
140
+ const now = this.clock.now();
141
+ if (this.newTokensAt !== null) {
142
+ this.newTokens = Math.min(this.burst, this.newTokens + (now - this.newTokensAt) / NEW_SESSION_REFILL_MS);
143
+ }
144
+ this.newTokensAt = now;
145
+ if (this.newTokens < 1 - 1e-6)
146
+ return false; // fractional clock times can leave a full token at 0.9999999999999999
147
+ this.newTokens = Math.max(0, this.newTokens - 1);
148
+ return true;
149
+ }
150
+ answer(pub) {
151
+ void this.room.send(pub, { type: "open", seq: 0, ack: 0 });
152
+ }
153
+ accept(pub) {
154
+ const peer = { session: null, link: null };
155
+ peer.link = new ReliableLink({
156
+ clock: this.clock,
157
+ send: (out) => void this.room.send(pub, out),
158
+ deliver: (text) => peer.session && this.hub.receive(peer.session, text),
159
+ onPeerClose: () => this.detach(pub, peer),
160
+ onDead: () => this.detach(pub, peer),
161
+ });
162
+ this.peers.set(pub, peer);
163
+ this.answer(pub);
164
+ const session = this.hub.connect(`nostr:${pub.slice(0, 16)}`, (text) => {
165
+ if (!text.startsWith('{"t":"pong"'))
166
+ peer.link.send(text); // keep-alive pongs never cross Nostr
167
+ }, () => {
168
+ // the hub closed this session: say so, keep the pubkey out for the rest of the session
169
+ peer.link.close();
170
+ this.forget(pub, peer);
171
+ });
172
+ if (peer.link.closed)
173
+ return; // full: the hub already had us close it
174
+ if (!session) {
175
+ peer.link.close("closed");
176
+ this.forget(pub, peer);
177
+ return;
178
+ }
179
+ peer.session = session;
180
+ }
181
+ detach(pub, peer) {
182
+ peer.link.stop();
183
+ if (peer.session)
184
+ this.hub.disconnect(peer.session);
185
+ peer.session = null;
186
+ this.forget(pub, peer);
187
+ }
188
+ forget(pub, peer) {
189
+ if (this.peers.get(pub) === peer)
190
+ this.peers.delete(pub);
191
+ this.gone.add(pub);
192
+ if (this.gone.size > CLOSED_MEMORY)
193
+ this.gone.delete(this.gone.values().next().value);
194
+ }
195
+ }
196
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/nostr/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The host side of Nostr signaling (spec/nostr-signaling.md s.4): each joiner pubkey becomes one hub session, as if it\n * had opened a TCP WebSocket. The hub speaks the usual protocol; this file only moves its texts over frames.\n */\nimport type { Clock } from \"../clock.js\";\nimport type { SignalingHub, Session } from \"../direct/hub.js\";\nimport { ReliableLink, type Frame } from \"./link.js\";\nimport { NostrRoom } from \"./room.js\";\nimport type { RelayConnect } from \"./relays.js\";\nimport { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS, WAITING_MAX, WAITING_STALE_MS, RELAY_REST_WAIT_MS, RELAY_REST_POLL_MS } from \"./limits.js\";\n\nexport interface NostrHostOptions {\n /** The version 1 join code this host publishes. */\n code: string;\n relays?: readonly string[];\n connect?: RelayConnect;\n clock: Clock;\n wall?: () => number;\n /** The number of connected relays changed. */\n onChange?: () => void;\n /** The lobby's player cap: the new-session burst grows to it (see newSessionBurst). */\n maxPlayers?: number;\n}\n\n/** A lobby sized for more than NEW_SESSION_BURST players gets a burst that fits it (up to MAX_SESSION_BURST). */\nexport function newSessionBurst(maxPlayers?: number): number {\n return Math.max(NEW_SESSION_BURST, Math.min(MAX_SESSION_BURST, maxPlayers ?? 0));\n}\n\ninterface Peer {\n link: ReliableLink;\n session: Session | null;\n}\n\nconst PING = '{\"t\":\"ping\",\"n\":0}';\nconst CLOSED_MEMORY = 512;\n/**\n * NEW joiner pubkeys per lobby are a token bucket: a burst of newSessionBurst(maxPlayers) at once (a party joining\n * together), then one more per NEW_SESSION_REFILL_MS. Refused pubkeys wait in a list (up to WAITING_MAX, oldest\n * first) and are answered the moment a token refills; a waiting pubkey whose `open` (repeated every 2 s) has not\n * been heard for WAITING_STALE_MS has given up and is dropped.\n */\nexport { NEW_SESSION_BURST, MAX_SESSION_BURST, NEW_SESSION_REFILL_MS };\n\nexport class NostrHostBridge {\n private readonly peers = new Map<string, Peer>();\n private readonly gone = new Set<string>();\n private stopped = false;\n private readonly burst: number;\n private newTokens: number;\n private newTokensAt: number | null = null;\n /** Refused new pubkeys, oldest first, with when their last `open` was heard. */\n private readonly waiting = new Map<string, number>();\n private cancelWaiting: (() => void) | null = null;\n\n private constructor(private readonly hub: SignalingHub, private readonly room: NostrRoom, private readonly clock: Clock, maxPlayers?: number) {\n this.burst = this.newTokens = newSessionBurst(maxPlayers);\n room.onFrame = (f) => this.onFrame(f);\n }\n\n static async start(hub: SignalingHub, o: NostrHostOptions): Promise<NostrHostBridge> {\n return new NostrHostBridge(hub, await NostrRoom.open(o), o.clock, o.maxPlayers);\n }\n\n get relaysConnected(): number {\n return this.room.relaysConnected;\n }\n\n get relaysTotal(): number {\n return this.room.relaysTotal;\n }\n\n /** Resolves true once one relay is connected, false after `timeoutMs`. */\n whenUp(timeoutMs: number): Promise<boolean> {\n return this.room.whenUp(timeoutMs);\n }\n\n get relaysSettled(): boolean {\n return this.room.relaysSettled;\n }\n\n /** Waits for the relays: true once one is connected (then up to RELAY_REST_WAIT_MS more, less when every relay has answered), false after `firstRelayMs`. */\n async settle(firstRelayMs: number): Promise<boolean> {\n if (!(await this.whenUp(firstRelayMs))) return false;\n for (let i = 0; i < RELAY_REST_WAIT_MS / RELAY_REST_POLL_MS && !this.relaysSettled; i++) await new Promise((r) => setTimeout(r, RELAY_REST_POLL_MS));\n return true;\n }\n\n /** Joiners currently attached. */\n get peerCount(): number {\n return this.peers.size;\n }\n\n close(): void {\n if (this.stopped) return;\n this.stopped = true;\n this.cancelWaiting?.();\n this.cancelWaiting = null;\n this.waiting.clear();\n for (const [pub, p] of this.peers) {\n p.link.close(\"host left\");\n if (p.session) this.hub.disconnect(p.session);\n this.peers.delete(pub);\n }\n this.room.close(true);\n }\n\n private onFrame(f: Frame): void {\n if (this.stopped) return;\n const known = this.peers.get(f.from);\n if (f.type === \"open\") {\n if (this.gone.has(f.from)) return;\n if (known) {\n known.link.heard();\n this.answer(f.from);\n } else if (this.waiting.has(f.from)) {\n this.waiting.set(f.from, this.clock.now()); // still trying: keep its place in the queue\n this.admitWaiting();\n } else if (this.waiting.size === 0 && this.admitNew()) {\n this.accept(f.from);\n } else if (this.waiting.size < WAITING_MAX) {\n this.waiting.set(f.from, this.clock.now()); // over the cap: answered when a token refills\n this.scheduleWaiting();\n } // waiting list full too: not answered, the joiner's next open retries\n return;\n }\n if (!known) return;\n known.link.receive(f);\n // a joiner's beat is its keep-alive: the hub counts it as activity (its pong is dropped below)\n if (f.type === \"beat\" && known.session) this.hub.receive(known.session, PING);\n }\n\n /** Arms one timer for the next token while anyone waits. */\n private scheduleWaiting(): void {\n if (this.cancelWaiting || this.waiting.size === 0 || this.stopped) return;\n const missing = Math.max(0, 1 - this.newTokens);\n this.cancelWaiting = this.clock.setTimeout(() => {\n this.cancelWaiting = null;\n this.admitWaiting();\n }, Math.max(1, Math.ceil(missing * NEW_SESSION_REFILL_MS)));\n }\n\n /** Answers waiting pubkeys, oldest first, while tokens last; drops the ones that gave up. */\n private admitWaiting(): void {\n if (this.stopped) return;\n const now = this.clock.now();\n for (const [pub, heard] of this.waiting) {\n if (now - heard > WAITING_STALE_MS || this.peers.has(pub) || this.gone.has(pub)) {\n this.waiting.delete(pub);\n continue;\n }\n if (!this.admitNew()) break;\n this.waiting.delete(pub);\n this.accept(pub);\n }\n this.scheduleWaiting();\n }\n\n /** Spends one token of the new-session bucket, or false when it is empty. */\n private admitNew(): boolean {\n const now = this.clock.now();\n if (this.newTokensAt !== null) {\n this.newTokens = Math.min(this.burst, this.newTokens + (now - this.newTokensAt) / NEW_SESSION_REFILL_MS);\n }\n this.newTokensAt = now;\n if (this.newTokens < 1 - 1e-6) return false; // fractional clock times can leave a full token at 0.9999999999999999\n this.newTokens = Math.max(0, this.newTokens - 1);\n return true;\n }\n\n private answer(pub: string): void {\n void this.room.send(pub, { type: \"open\", seq: 0, ack: 0 });\n }\n\n private accept(pub: string): void {\n const peer: Peer = { session: null, link: null as unknown as ReliableLink };\n peer.link = new ReliableLink({\n clock: this.clock,\n send: (out) => void this.room.send(pub, out),\n deliver: (text) => peer.session && this.hub.receive(peer.session, text),\n onPeerClose: () => this.detach(pub, peer),\n onDead: () => this.detach(pub, peer),\n });\n this.peers.set(pub, peer);\n this.answer(pub);\n const session = this.hub.connect(\n `nostr:${pub.slice(0, 16)}`,\n (text) => {\n if (!text.startsWith('{\"t\":\"pong\"')) peer.link.send(text); // keep-alive pongs never cross Nostr\n },\n () => {\n // the hub closed this session: say so, keep the pubkey out for the rest of the session\n peer.link.close();\n this.forget(pub, peer);\n },\n );\n if (peer.link.closed) return; // full: the hub already had us close it\n if (!session) {\n peer.link.close(\"closed\");\n this.forget(pub, peer);\n return;\n }\n peer.session = session;\n }\n\n private detach(pub: string, peer: Peer): void {\n peer.link.stop();\n if (peer.session) this.hub.disconnect(peer.session);\n peer.session = null;\n this.forget(pub, peer);\n }\n\n private forget(pub: string, peer: Peer): void {\n if (this.peers.get(pub) === peer) this.peers.delete(pub);\n this.gone.add(pub);\n if (this.gone.size > CLOSED_MEMORY) this.gone.delete(this.gone.values().next().value as string);\n }\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Every Nostr limit and timer shared with the C# client. Pinned by `spec/vectors/nostr-limits.json` (test/nostr.test.ts);
3
+ * C# mirror: OneLobby.Direct/Nostr/NostrLimits.cs. Change a number in all three.
4
+ */
5
+ export declare const BATCH_MS = 30;
6
+ export declare const RESEND_MS = 3000;
7
+ export declare const MAX_RESENDS = 5;
8
+ export declare const BEAT_MS = 10000;
9
+ export declare const SILENCE_MS = 30000;
10
+ export declare const MAX_HELD = 64;
11
+ /** Outbound: queued texts plus unacknowledged frames, at most this many frames and bytes; excess texts are dropped. */
12
+ export declare const MAX_OUT_FRAMES = 128;
13
+ export declare const MAX_OUT_BYTES: number;
14
+ export declare const MAX_FRAME_BYTES = 30000;
15
+ export declare const TICK_MS = 250;
16
+ export declare const NEW_SESSION_BURST = 16;
17
+ export declare const MAX_SESSION_BURST = 64;
18
+ export declare const NEW_SESSION_REFILL_MS = 1000;
19
+ export declare const WAITING_MAX = 64;
20
+ export declare const WAITING_STALE_MS = 5000;
21
+ export declare const OPEN_EVERY_MS = 2000;
22
+ export declare const GIVE_UP_MS = 15000;
23
+ /** `hostDirect`: how long to wait for a first relay before hosting by IP alone (the IP code shows first, the Nostr code takes over later). */
24
+ export declare const HOST_FIRST_RELAY_MS = 3000;
25
+ /** `hostNostr` (no IP fallback): how long to wait for a first relay before `direct_unreachable`. */
26
+ export declare const HOST_NOSTR_FIRST_RELAY_MS = 10000;
27
+ /** A joiner: how long to wait for a first relay before `connect_failed`. */
28
+ export declare const JOINER_FIRST_RELAY_MS = 10000;
29
+ /** After the first relay: at most this long for the rest of the list; ends at once when every relay has answered. */
30
+ export declare const RELAY_REST_WAIT_MS = 1500;
31
+ export declare const RELAY_REST_POLL_MS = 50;