@kikorin/netcode 1.0.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/change-tracker.ts","../src/connection-pool.ts","../src/message-codec.ts","../src/lead-election.ts","../src/interest-group.ts","../src/peer-net.ts","../src/priority-queue.ts"],"sourcesContent":["export * from './types'\nexport * from './peer-net'\nexport * from './interest-group'\nexport * from './change-tracker'\nexport * from './lead-election'\nexport * from './message-codec'\nexport * from './priority-queue'\nexport type { ConnectionPool, PeerJSPeer, PeerJSDataConnection } from './connection-pool'\n","export type PeerId = string\nexport type GroupId = string\nexport type EntityId = number\nexport type ComponentId = number\nexport type FieldId = number\nexport type SequenceNumber = number\n\nexport type NetTypedArray =\n | Float32Array\n | Float64Array\n | Int32Array\n | Uint32Array\n | Uint16Array\n | Uint8Array\n | Int8Array\n | Int16Array\n\n// 8-byte message header:\n// [type:1][flags:1][seq:2][ack:2][payloadLen:2] (all LE)\nexport const HEADER_SIZE = 8\n\nexport const enum MessageType {\n Handshake = 0x01,\n Subscribe = 0x02,\n Unsubscribe = 0x03,\n DeltaUpdate = 0x04,\n FullSync = 0x05,\n Ack = 0x06,\n Ping = 0x07,\n Pong = 0x08,\n LeadClaim = 0x09,\n LeadYield = 0x0a,\n PeerList = 0x0b,\n GameEvent = 0x0c,\n}\n\nexport const enum MessageFlag {\n None = 0x00,\n Reliable = 0x01,\n Compressed = 0x02,\n Fragmented = 0x04,\n}\n\nexport interface NetMessage {\n type: MessageType\n flags: MessageFlag\n seq: SequenceNumber\n ack: SequenceNumber\n payload: ArrayBuffer\n}\n\nexport interface FieldSchema {\n id: FieldId\n name: string\n /** Direct reference to the bitecs component TypedArray (e.g. Position.x) */\n array: NetTypedArray\n}\n\nexport interface ComponentSchema {\n id: ComponentId\n name: string\n fields: FieldSchema[]\n}\n\nexport interface InterestGroupConfig {\n id: GroupId\n /** Max entities tracked by this group. Default 4096. */\n maxEntities?: number\n /** Flush interval in ms. Default 50 (20hz). */\n tickRateMs?: number\n /** Lead election strategy. Default 'min-id'. */\n electionStrategy?: ElectionStrategy\n}\n\nexport interface PeerNetConfig {\n peerId: PeerId\n /** Exponential moving average alpha for RTT. Default 0.125. */\n rttAlpha?: number\n}\n\nexport interface DeltaEntry {\n entityId: EntityId\n componentId: ComponentId\n fieldId: FieldId\n value: number\n}\n\nexport type DeltaSet = DeltaEntry[]\n\nexport type DeltaHandler = (deltas: DeltaSet, groupId: GroupId, fromPeer: PeerId) => void\n\nexport type ElectionStrategy = 'min-id' | 'hash-ring' | 'load-balanced'\n\nexport interface LoadInfo {\n peerId: PeerId\n connectionCount: number\n leadGroupCount: number\n}\n","import type { ComponentId, ComponentSchema, DeltaSet, EntityId } from './types'\n\n// Per-entity, per-component field snapshot.\n// Indexed as Float64Array with stride = fieldCount, so slot[fieldId] = last sent value.\ntype ComponentSnapshot = Float64Array\n\n// Snapshot store: componentId → Float64Array[eid * fieldCount + fieldId]\ntype EntitySnapshot = Map<ComponentId, ComponentSnapshot>\n\n/**\n * Tracks the last-flushed values of registered ECS components and produces\n * minimal delta sets by comparing current TypedArray state to the snapshot.\n *\n * Thread of ownership: single-threaded (JS). Call markDirty() whenever an\n * ECS system modifies a component, then flush() once per network tick to\n * collect only what changed.\n */\nexport class ChangeTracker {\n private _schemas = new Map<ComponentId, ComponentSchema>()\n // Flat snapshot per entity (reused across components)\n private _snapshots = new Map<EntityId, EntitySnapshot>()\n private _dirtySet = new Set<EntityId>()\n\n registerComponent(schema: ComponentSchema): void {\n if (this._schemas.has(schema.id)) {\n throw new Error(`Component id ${schema.id} already registered`)\n }\n this._schemas.set(schema.id, schema)\n }\n\n unregisterComponent(componentId: ComponentId): void {\n this._schemas.delete(componentId)\n // Invalidate snapshots that referenced this component\n for (const snap of this._snapshots.values()) {\n snap.delete(componentId)\n }\n }\n\n markDirty(entityId: EntityId): void {\n this._dirtySet.add(entityId)\n }\n\n markDirtyBatch(entities: EntityId[]): void {\n for (const eid of entities) this._dirtySet.add(eid)\n }\n\n get dirtyCount(): number { return this._dirtySet.size }\n\n /**\n * Compute deltas for a subset of entities and update the snapshot.\n * Only entities that were marked dirty are compared; clean entities are skipped.\n * Clears dirty flags for all entities in the provided list.\n */\n flush(entities: EntityId[]): DeltaSet {\n const deltas: DeltaSet = []\n\n for (const eid of entities) {\n if (!this._dirtySet.has(eid)) continue\n\n let entitySnap = this._snapshots.get(eid)\n if (!entitySnap) { entitySnap = new Map(); this._snapshots.set(eid, entitySnap) }\n\n for (const [cid, schema] of this._schemas) {\n const fieldCount = schema.fields.length\n let compSnap = entitySnap.get(cid)\n if (!compSnap) {\n compSnap = new Float64Array(fieldCount).fill(NaN)\n entitySnap.set(cid, compSnap)\n }\n\n for (let fi = 0; fi < fieldCount; fi++) {\n const field = schema.fields[fi]\n const cur = field.array[eid]\n if (!Object.is(compSnap[fi], cur)) {\n compSnap[fi] = cur\n deltas.push({ entityId: eid, componentId: cid, fieldId: fi, value: cur })\n }\n }\n }\n\n this._dirtySet.delete(eid)\n }\n\n return deltas\n }\n\n /**\n * Force-flush all registered fields for the given entities, ignoring dirty state.\n * Use when a new peer joins and needs a full state sync.\n */\n fullSnapshot(entities: EntityId[]): DeltaSet {\n const deltas: DeltaSet = []\n\n for (const eid of entities) {\n for (const [cid, schema] of this._schemas) {\n for (let fi = 0; fi < schema.fields.length; fi++) {\n deltas.push({\n entityId: eid,\n componentId: cid,\n fieldId: fi,\n value: schema.fields[fi].array[eid],\n })\n }\n }\n }\n\n return deltas\n }\n\n /**\n * Invalidate the snapshot for an entity (e.g. after it is destroyed and respawned).\n * Next flush will treat all fields as changed.\n */\n invalidateEntity(entityId: EntityId): void {\n this._snapshots.delete(entityId)\n this._dirtySet.add(entityId)\n }\n\n clearDirtyAll(): void {\n this._dirtySet.clear()\n }\n}\n","import type { PeerId } from './types'\n\n// ---------------------------------------------------------------------------\n// PeerJS duck-type interfaces — no direct peerjs npm dependency needed.\n// Inject a real Peer instance via setPeer().\n// ---------------------------------------------------------------------------\n\nexport interface PeerJSDataConnection {\n readonly peer: string\n readonly open: boolean\n send(data: ArrayBuffer | Uint8Array): void\n close(): void\n on(event: 'data', cb: (data: unknown) => void): void\n on(event: 'open', cb: () => void): void\n on(event: 'close', cb: () => void): void\n on(event: 'error', cb: (err: Error) => void): void\n}\n\nexport interface PeerJSPeer {\n readonly id: string\n connect(peerId: string, options?: { reliable?: boolean; serialization?: string }): PeerJSDataConnection\n on(event: 'connection', cb: (conn: PeerJSDataConnection) => void): void\n on(event: 'open', cb: (id: string) => void): void\n on(event: 'error', cb: (err: Error) => void): void\n on(event: 'close', cb: () => void): void\n destroy(): void\n}\n\n// ---------------------------------------------------------------------------\n\ntype ConnState = 'pending' | 'open' | 'closed' | 'error'\n\ninterface ManagedConn {\n conn: PeerJSDataConnection\n state: ConnState\n queue: ArrayBuffer[]\n resolve: () => void\n reject: (e: Error) => void\n openPromise: Promise<void>\n}\n\ntype DataHandler = (data: ArrayBuffer, from: PeerId) => void\ntype DisconnectHandler = (peerId: PeerId) => void\n\n/**\n * Manages the lifecycle of PeerJS data connections.\n * Provides:\n * - Lazy connection (auto-connects on first send)\n * - Per-connection message queue drained on open\n * - Exponential-backoff reconnect on close/error\n * - Deduplication of inbound vs outbound connections to the same peer\n */\nexport class ConnectionPool {\n private _peer: PeerJSPeer | null = null\n private _conns = new Map<PeerId, ManagedConn>()\n private _handlers = new Set<DataHandler>()\n private _disconnectHandlers = new Set<DisconnectHandler>()\n // Reconnect state: peerId → current delay ms\n private _reconnectDelay = new Map<PeerId, number>()\n private _reconnectHandles = new Map<PeerId, ReturnType<typeof setTimeout>>()\n private static readonly BASE_DELAY = 500\n private static readonly MAX_DELAY = 30_000\n\n setPeer(peer: PeerJSPeer): void {\n this._peer = peer\n peer.on('connection', conn => this._registerConn(conn.peer, conn))\n }\n\n async connect(peerId: PeerId): Promise<void> {\n const existing = this._conns.get(peerId)\n if (existing?.state === 'open') return\n if (existing?.state === 'pending') return existing.openPromise\n if (!this._peer) throw new Error('Peer not initialized — call setPeer() first')\n const conn = this._peer.connect(peerId, { reliable: false, serialization: 'binary' })\n this._registerConn(peerId, conn)\n return this._conns.get(peerId)!.openPromise\n }\n\n send(peerId: PeerId, data: ArrayBuffer): void {\n const managed = this._conns.get(peerId)\n if (!managed) {\n // Lazy connect — queue and fire\n void this.connect(peerId).then(() => this.send(peerId, data)).catch(() => {})\n return\n }\n if (managed.state === 'open') {\n managed.conn.send(data)\n } else {\n managed.queue.push(data)\n }\n }\n\n isOpen(peerId: PeerId): boolean {\n return this._conns.get(peerId)?.state === 'open'\n }\n\n disconnect(peerId: PeerId): void {\n clearTimeout(this._reconnectHandles.get(peerId))\n this._reconnectHandles.delete(peerId)\n this._reconnectDelay.delete(peerId)\n const managed = this._conns.get(peerId)\n if (managed) { managed.conn.close(); this._conns.delete(peerId) }\n }\n\n onData(handler: DataHandler): () => void {\n this._handlers.add(handler)\n return () => this._handlers.delete(handler)\n }\n\n /** Fires when a connection closes unexpectedly (not from a local disconnect() call). */\n onDisconnect(handler: DisconnectHandler): () => void {\n this._disconnectHandlers.add(handler)\n return () => this._disconnectHandlers.delete(handler)\n }\n\n dispose(): void {\n for (const h of this._reconnectHandles.values()) clearTimeout(h)\n this._reconnectHandles.clear()\n for (const m of this._conns.values()) m.conn.close()\n this._conns.clear()\n this._peer?.destroy()\n this._peer = null\n }\n\n // ---------------------------------------------------------------------------\n\n private _registerConn(peerId: PeerId, conn: PeerJSDataConnection): void {\n // Prefer keeping an existing open outbound connection over an inbound one\n const existing = this._conns.get(peerId)\n if (existing?.state === 'open') return\n\n let resolve!: () => void\n let reject!: (e: Error) => void\n const openPromise = new Promise<void>((res, rej) => { resolve = res; reject = rej })\n\n const managed: ManagedConn = { conn, state: 'pending', queue: [], resolve, reject, openPromise }\n this._conns.set(peerId, managed)\n\n conn.on('open', () => {\n managed.state = 'open'\n this._reconnectDelay.delete(peerId)\n clearTimeout(this._reconnectHandles.get(peerId))\n this._reconnectHandles.delete(peerId)\n for (const buf of managed.queue) conn.send(buf)\n managed.queue = []\n resolve()\n })\n\n conn.on('data', raw => {\n const buf: ArrayBuffer | null = raw instanceof ArrayBuffer ? raw\n : raw instanceof Uint8Array ? raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength) as ArrayBuffer\n : null\n if (!buf) return\n for (const h of this._handlers) h(buf, peerId)\n })\n\n conn.on('close', () => {\n managed.state = 'closed'\n for (const h of this._disconnectHandlers) h(peerId)\n this._scheduleReconnect(peerId)\n })\n\n conn.on('error', err => {\n managed.state = 'error'\n reject(err)\n this._scheduleReconnect(peerId)\n })\n }\n\n private _scheduleReconnect(peerId: PeerId): void {\n if (!this._peer) return\n const prev = this._reconnectDelay.get(peerId) ?? ConnectionPool.BASE_DELAY / 2\n const delay = Math.min(prev * 2, ConnectionPool.MAX_DELAY)\n this._reconnectDelay.set(peerId, delay)\n clearTimeout(this._reconnectHandles.get(peerId))\n const handle = setTimeout(() => {\n if (this._conns.get(peerId)?.state !== 'open') {\n void this.connect(peerId).catch(() => {})\n }\n }, delay)\n this._reconnectHandles.set(peerId, handle)\n }\n}\n","import { HEADER_SIZE, MessageFlag, MessageType, type DeltaEntry, type DeltaSet, type NetMessage } from './types'\n\n// ---------------------------------------------------------------------------\n// Buffer pool — reuse fixed-size buffers to reduce GC pressure\n// ---------------------------------------------------------------------------\n\nconst POOL_BUFFER_SIZE = 65_536\nconst POOL_MAX = 16\nconst _pool: ArrayBuffer[] = []\n\nfunction acquireBuffer(): ArrayBuffer {\n return _pool.pop() ?? new ArrayBuffer(POOL_BUFFER_SIZE)\n}\n\nfunction releaseBuffer(buf: ArrayBuffer): void {\n if (buf.byteLength === POOL_BUFFER_SIZE && _pool.length < POOL_MAX) {\n _pool.push(buf)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Header encode / decode\n// ---------------------------------------------------------------------------\n\nexport function encodeHeader(\n view: DataView,\n type: MessageType,\n flags: MessageFlag,\n seq: number,\n ack: number,\n payloadLen: number,\n): void {\n view.setUint8(0, type)\n view.setUint8(1, flags)\n view.setUint16(2, seq, true)\n view.setUint16(4, ack, true)\n view.setUint16(6, payloadLen, true)\n}\n\nexport function decodeHeader(view: DataView): {\n type: MessageType\n flags: MessageFlag\n seq: number\n ack: number\n payloadLen: number\n} {\n return {\n type: view.getUint8(0) as MessageType,\n flags: view.getUint8(1) as MessageFlag,\n seq: view.getUint16(2, true),\n ack: view.getUint16(4, true),\n payloadLen: view.getUint16(6, true),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Full message encode / decode\n// ---------------------------------------------------------------------------\n\nexport function encodeMessage(msg: NetMessage): ArrayBuffer {\n const payloadLen = msg.payload.byteLength\n const total = HEADER_SIZE + payloadLen\n const buf = total <= POOL_BUFFER_SIZE ? acquireBuffer() : new ArrayBuffer(total)\n encodeHeader(new DataView(buf), msg.type, msg.flags, msg.seq, msg.ack, payloadLen)\n new Uint8Array(buf, HEADER_SIZE, payloadLen).set(new Uint8Array(msg.payload))\n const out = buf.slice(0, total)\n releaseBuffer(buf)\n return out\n}\n\nexport function decodeMessage(buf: ArrayBuffer): NetMessage {\n const view = new DataView(buf)\n const { type, flags, seq, ack, payloadLen } = decodeHeader(view)\n return {\n type,\n flags,\n seq,\n ack,\n payload: buf.slice(HEADER_SIZE, HEADER_SIZE + payloadLen),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Delta payload encode / decode\n//\n// Wire format:\n// [groupId_len:1][groupId:N]\n// [entityCount:2]\n// per entity:\n// [eid:4][componentCount:1]\n// per component:\n// [cid:1][fieldCount:1]\n// per field:\n// [fid:1][value:4 f32 LE]\n// ---------------------------------------------------------------------------\n\nexport function encodeDeltaPayload(groupId: string, deltas: DeltaSet): ArrayBuffer {\n const enc = new TextEncoder()\n const groupIdBytes = enc.encode(groupId)\n\n // Group deltas: entity → component → [fieldId, value][]\n const byEntity = new Map<number, Map<number, Array<[number, number]>>>()\n for (const { entityId, componentId, fieldId, value } of deltas) {\n let byComp = byEntity.get(entityId)\n if (!byComp) { byComp = new Map(); byEntity.set(entityId, byComp) }\n let fields = byComp.get(componentId)\n if (!fields) { fields = []; byComp.set(componentId, fields) }\n fields.push([fieldId, value])\n }\n\n // Size: 1 + groupIdBytes.length + 2 + entities*(4+1 + comps*(1+1 + fields*(1+4)))\n const maxSize = 1 + groupIdBytes.length + 2 + deltas.length * 12\n const buf = maxSize <= POOL_BUFFER_SIZE ? acquireBuffer() : new ArrayBuffer(maxSize)\n const view = new DataView(buf)\n let off = 0\n\n view.setUint8(off, groupIdBytes.length); off += 1\n new Uint8Array(buf, off, groupIdBytes.length).set(groupIdBytes); off += groupIdBytes.length\n view.setUint16(off, byEntity.size, true); off += 2\n\n for (const [eid, byComp] of byEntity) {\n view.setUint32(off, eid, true); off += 4\n view.setUint8(off, byComp.size); off += 1\n for (const [cid, fields] of byComp) {\n view.setUint8(off, cid); off += 1\n view.setUint8(off, fields.length); off += 1\n for (const [fid, val] of fields) {\n view.setUint8(off, fid); off += 1\n view.setFloat32(off, val, true); off += 4\n }\n }\n }\n\n const out = buf.slice(0, off)\n releaseBuffer(buf)\n return out\n}\n\nexport function decodeDeltaPayload(buf: ArrayBuffer): { groupId: string; deltas: DeltaSet } {\n const view = new DataView(buf)\n const dec = new TextDecoder()\n let off = 0\n\n const groupIdLen = view.getUint8(off); off += 1\n const groupId = dec.decode(buf.slice(off, off + groupIdLen)); off += groupIdLen\n const entityCount = view.getUint16(off, true); off += 2\n\n const deltas: DeltaEntry[] = []\n for (let e = 0; e < entityCount; e++) {\n const entityId = view.getUint32(off, true); off += 4\n const componentCount = view.getUint8(off); off += 1\n for (let c = 0; c < componentCount; c++) {\n const componentId = view.getUint8(off); off += 1\n const fieldCount = view.getUint8(off); off += 1\n for (let f = 0; f < fieldCount; f++) {\n const fieldId = view.getUint8(off); off += 1\n const value = view.getFloat32(off, true); off += 4\n deltas.push({ entityId, componentId, fieldId, value })\n }\n }\n }\n\n return { groupId, deltas }\n}\n\n// ---------------------------------------------------------------------------\n// Control payload helpers (JSON-over-text for low-freq control messages)\n// ---------------------------------------------------------------------------\n\nconst _enc = new TextEncoder()\nconst _dec = new TextDecoder()\n\nexport function encodeJson(obj: Record<string, unknown>): ArrayBuffer {\n return _enc.encode(JSON.stringify(obj)).buffer as ArrayBuffer\n}\n\nexport function decodeJson(buf: ArrayBuffer): Record<string, unknown> {\n return JSON.parse(_dec.decode(buf)) as Record<string, unknown>\n}\n","import type { ElectionStrategy, GroupId, LoadInfo, PeerId } from './types'\n\n// FNV-1a 32-bit hash — fast, good distribution, deterministic across peers\nfunction fnv1a(s: string): number {\n let h = 2_166_136_261\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 16_777_619) >>> 0\n }\n return h\n}\n\n/**\n * Deterministic lead election for an interest group.\n *\n * All peers in a group run the same algorithm over the same candidate list,\n * so they converge on the same lead without coordination messages. When the\n * candidate set changes (peer joins/leaves), every peer re-elects locally.\n *\n * Strategies:\n * min-id — lexicographically smallest peer ID wins. Zero coordination\n * overhead, deterministic, but always assigns load to the\n * same peer. Best for low-churn groups.\n *\n * hash-ring — consistent hashing with 20 virtual nodes per peer.\n * Spreads lead responsibility across peers as groups multiply.\n * A single peer join/leave only re-assigns ~1/N groups.\n *\n * load-balanced — weighted score from live load metrics sent by peers.\n * Falls back to min-id for peers without reported metrics.\n * Best when peers have heterogeneous capacity.\n */\nexport class LeadElector {\n private _strategy: ElectionStrategy\n private _loadInfo = new Map<PeerId, LoadInfo>()\n\n constructor(strategy: ElectionStrategy = 'min-id') {\n this._strategy = strategy\n }\n\n updateLoadInfo(info: LoadInfo): void {\n this._loadInfo.set(info.peerId, info)\n }\n\n removeLoadInfo(peerId: PeerId): void {\n this._loadInfo.delete(peerId)\n }\n\n electLead(groupId: GroupId, candidates: PeerId[]): PeerId {\n if (candidates.length === 0) throw new Error('No candidates for election')\n if (candidates.length === 1) return candidates[0]\n\n switch (this._strategy) {\n case 'min-id': return this._minId(candidates)\n case 'hash-ring': return this._hashRing(groupId, candidates)\n case 'load-balanced': return this._loadBalanced(candidates)\n }\n }\n\n wouldReelect(groupId: GroupId, currentLead: PeerId, candidates: PeerId[]): boolean {\n try {\n return this.electLead(groupId, candidates) !== currentLead\n } catch {\n return true\n }\n }\n\n private _minId(candidates: PeerId[]): PeerId {\n return candidates.reduce((best, id) => (id < best ? id : best))\n }\n\n private _hashRing(groupId: GroupId, candidates: PeerId[]): PeerId {\n const target = fnv1a(groupId)\n // 20 virtual nodes per peer — enough for good distribution with small group sets\n const ring: Array<{ pos: number; peerId: PeerId }> = []\n for (const peerId of candidates) {\n for (let v = 0; v < 20; v++) {\n ring.push({ pos: fnv1a(`${peerId}:vn${v}`), peerId })\n }\n }\n ring.sort((a, b) => (a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : 0))\n // First node clockwise from the group's hash position\n return (ring.find(n => n.pos >= target) ?? ring[0]).peerId\n }\n\n private _loadBalanced(candidates: PeerId[]): PeerId {\n let best = candidates[0]\n let bestScore = Infinity\n for (const peerId of candidates) {\n const info = this._loadInfo.get(peerId)\n // connectionCount * 2 + leadGroupCount * 5 — leads cost more than plain connections\n const score = info ? info.connectionCount * 2 + info.leadGroupCount * 5 : 0\n if (score < bestScore) { bestScore = score; best = peerId }\n }\n return best\n }\n}\n","import { decodeDeltaPayload, decodeJson, encodeDeltaPayload, encodeJson } from './message-codec'\nimport { LeadElector } from './lead-election'\nimport {\n MessageFlag,\n MessageType,\n type DeltaHandler,\n type DeltaSet,\n type ElectionStrategy,\n type GroupId,\n type InterestGroupConfig,\n type NetMessage,\n type PeerId,\n} from './types'\n\nexport interface PeerState {\n readonly peerId: PeerId\n isLead: boolean\n joinedAt: number\n lastSeenAt: number\n rttMs: number\n}\n\nexport type SendFn = (to: PeerId, msg: NetMessage) => void\n\n/**\n * Manages one interest group: membership, lead election, and message routing.\n *\n * Routing rules:\n * - Non-lead peers send outbound deltas → lead only\n * - Lead receives deltas → re-broadcasts to all other group peers + notifies local handlers\n * - Local delta handlers always fire regardless of lead status\n *\n * Lead election is deterministic (all peers run the same algorithm) and\n * re-runs on every membership change. No election messages are needed for\n * convergence; LeadClaim messages just inform remote peers of the local result.\n */\nexport class InterestGroup {\n readonly id: GroupId\n\n private _config: Required<InterestGroupConfig>\n private _localPeerId: PeerId\n private _peers = new Map<PeerId, PeerState>()\n private _leadId: PeerId | null = null\n private _elector: LeadElector\n private _sendFn: SendFn | null = null\n private _deltaHandlers = new Set<DeltaHandler>()\n private _pending: DeltaSet = []\n private _tickHandle: ReturnType<typeof setInterval> | null = null\n private _seq = 0\n private _ack = 0\n\n constructor(config: InterestGroupConfig, localPeerId: PeerId) {\n this.id = config.id\n this._config = {\n id: config.id,\n maxEntities: config.maxEntities ?? 4096,\n tickRateMs: config.tickRateMs ?? 50,\n electionStrategy: config.electionStrategy ?? 'min-id',\n }\n this._localPeerId = localPeerId\n this._elector = new LeadElector(this._config.electionStrategy as ElectionStrategy)\n }\n\n // ---------------------------------------------------------------------------\n // Accessors\n // ---------------------------------------------------------------------------\n\n get leadId(): PeerId | null { return this._leadId }\n get isLead(): boolean { return this._leadId === this._localPeerId }\n get peerCount(): number { return this._peers.size }\n\n getPeer(peerId: PeerId): Readonly<PeerState> | undefined {\n return this._peers.get(peerId)\n }\n\n get peerIds(): ReadonlySet<PeerId> {\n return new Set(this._peers.keys())\n }\n\n // ---------------------------------------------------------------------------\n // Wiring\n // ---------------------------------------------------------------------------\n\n setSendFn(fn: SendFn): void { this._sendFn = fn }\n\n // ---------------------------------------------------------------------------\n // Membership\n // ---------------------------------------------------------------------------\n\n addPeer(peerId: PeerId): void {\n if (this._peers.has(peerId)) return\n this._peers.set(peerId, {\n peerId,\n isLead: false,\n joinedAt: Date.now(),\n lastSeenAt: Date.now(),\n rttMs: 0,\n })\n this._reelect()\n }\n\n removePeer(peerId: PeerId): void {\n if (!this._peers.has(peerId)) return\n this._peers.delete(peerId)\n this._elector.removeLoadInfo(peerId)\n if (this._leadId === peerId) {\n this._leadId = null\n this._reelect()\n }\n }\n\n // ---------------------------------------------------------------------------\n // Delta publishing\n // ---------------------------------------------------------------------------\n\n /** Queue deltas to be sent on the next tick flush */\n publishDeltas(deltas: DeltaSet): void {\n if (deltas.length > 0) this._pending.push(...deltas)\n }\n\n /** Send all pending deltas immediately (called by the tick interval) */\n flush(): void {\n if (this._pending.length === 0 || !this._sendFn) return\n\n const payload = encodeDeltaPayload(this.id, this._pending)\n this._pending = []\n\n const msg: NetMessage = {\n type: MessageType.DeltaUpdate,\n flags: MessageFlag.None,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n }\n\n if (this.isLead) {\n // Lead fans out to all group peers\n for (const peerId of this._peers.keys()) {\n this._sendFn(peerId, msg)\n }\n } else if (this._leadId !== null) {\n // Non-lead sends only to the lead\n this._sendFn(this._leadId, msg)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Inbound message dispatch\n // ---------------------------------------------------------------------------\n\n handleMessage(msg: NetMessage, fromPeer: PeerId): void {\n const state = this._peers.get(fromPeer)\n if (state) state.lastSeenAt = Date.now()\n this._ack = msg.seq\n\n switch (msg.type) {\n case MessageType.DeltaUpdate:\n this._onDeltaUpdate(msg, fromPeer)\n break\n case MessageType.FullSync:\n this._onFullSync(msg, fromPeer)\n break\n case MessageType.LeadClaim:\n this._onLeadClaim(msg)\n break\n }\n }\n\n // ---------------------------------------------------------------------------\n // Handler registration\n // ---------------------------------------------------------------------------\n\n onDelta(handler: DeltaHandler): () => void {\n this._deltaHandlers.add(handler)\n return () => this._deltaHandlers.delete(handler)\n }\n\n // ---------------------------------------------------------------------------\n // Tick control\n // ---------------------------------------------------------------------------\n\n startTick(): void {\n if (this._tickHandle !== null) return\n this._tickHandle = setInterval(() => this.flush(), this._config.tickRateMs)\n }\n\n stopTick(): void {\n if (this._tickHandle !== null) {\n clearInterval(this._tickHandle)\n this._tickHandle = null\n }\n }\n\n dispose(): void {\n this.stopTick()\n this._deltaHandlers.clear()\n this._peers.clear()\n }\n\n // ---------------------------------------------------------------------------\n // Private\n // ---------------------------------------------------------------------------\n\n private _reelect(): void {\n if (this._peers.size === 0) {\n // Only local peer remains — we are the lead by default\n this._leadId = this._localPeerId\n return\n }\n const candidates = [this._localPeerId, ...this._peers.keys()]\n const newLead = this._elector.electLead(this.id, candidates)\n if (newLead !== this._leadId) {\n this._leadId = newLead\n if (newLead === this._localPeerId) {\n this._broadcastLeadClaim()\n }\n }\n }\n\n private _broadcastLeadClaim(): void {\n if (!this._sendFn) return\n const payload = encodeJson({ groupId: this.id, leadId: this._localPeerId })\n const msg: NetMessage = {\n type: MessageType.LeadClaim,\n flags: MessageFlag.Reliable,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n }\n for (const peerId of this._peers.keys()) {\n this._sendFn(peerId, msg)\n }\n }\n\n private _onDeltaUpdate(msg: NetMessage, fromPeer: PeerId): void {\n let groupId: string\n let deltas: DeltaSet\n try {\n ;({ groupId, deltas } = decodeDeltaPayload(msg.payload))\n } catch { return }\n if (groupId !== this.id) return\n\n if (this.isLead && this._sendFn) {\n // Fan out to all peers except the sender\n for (const peerId of this._peers.keys()) {\n if (peerId !== fromPeer) this._sendFn(peerId, msg)\n }\n }\n\n for (const h of this._deltaHandlers) h(deltas, this.id, fromPeer)\n }\n\n private _onFullSync(msg: NetMessage, fromPeer: PeerId): void {\n let groupId: string\n let deltas: DeltaSet\n try {\n ;({ groupId, deltas } = decodeDeltaPayload(msg.payload))\n } catch { return }\n if (groupId !== this.id) return\n // Full sync is never re-broadcast — it is a directed one-shot to this peer\n for (const h of this._deltaHandlers) h(deltas, this.id, fromPeer)\n }\n\n private _onLeadClaim(msg: NetMessage): void {\n try {\n const { groupId, leadId } = decodeJson(msg.payload)\n if (groupId !== this.id || typeof leadId !== 'string') return\n // Accept claim only if the claimed lead is a known peer\n if (this._peers.has(leadId) || leadId === this._localPeerId) {\n this._leadId = leadId\n for (const [pid, state] of this._peers) state.isLead = pid === leadId\n }\n } catch { /* malformed */ }\n }\n\n private _nextSeq(): number {\n return (this._seq = (this._seq + 1) & 0xffff)\n }\n}\n","import { ChangeTracker } from './change-tracker'\nimport { ConnectionPool, type PeerJSPeer } from './connection-pool'\nimport { InterestGroup } from './interest-group'\nimport { decodeJson, decodeMessage, encodeDeltaPayload, encodeJson, encodeMessage } from './message-codec'\nimport {\n MessageFlag,\n MessageType,\n type ComponentSchema,\n type DeltaHandler,\n type DeltaSet,\n type EntityId,\n type GroupId,\n type InterestGroupConfig,\n type NetMessage,\n type PeerId,\n type PeerNetConfig,\n} from './types'\n\n/**\n * PeerNet — top-level peer-to-peer netcode facade.\n *\n * Responsibilities:\n * - Owns the ConnectionPool (PeerJS connections)\n * - Owns all InterestGroups (pub/sub routing)\n * - Owns the ChangeTracker (minimal delta generation)\n * - Routes raw inbound buffers to the correct group\n * - RTT estimation via Ping/Pong\n *\n * Typical usage per game tick:\n * 1. ECS systems run; call net.markEntityDirty(eid) for any changed entity\n * 2. Call net.flushGroupDeltas(groupId, entityList) to compute+queue deltas\n * 3. InterestGroup tick fires automatically (setInterval) and sends queued data\n *\n * Incoming deltas from remote peers surface via net.onGroupDelta(groupId, handler).\n * Apply them to your ECS TypedArrays inside that handler.\n */\nexport class PeerNet {\n readonly localPeerId: PeerId\n\n private _config: Required<PeerNetConfig>\n private _pool: ConnectionPool\n private _groups = new Map<GroupId, InterestGroup>()\n private _tracker = new ChangeTracker()\n private _seq = 0\n private _ack = 0\n private _rttMs = 50\n private _gameEventHandlers: ((payload: ArrayBuffer, from: PeerId) => void)[] = []\n\n constructor(config: PeerNetConfig) {\n this.localPeerId = config.peerId\n this._config = {\n peerId: config.peerId,\n rttAlpha: config.rttAlpha ?? 0.125,\n }\n this._pool = new ConnectionPool()\n this._pool.onData((buf, from) => this._onRawData(buf, from))\n this._pool.onDisconnect(peerId => {\n for (const group of this._groups.values()) group.removePeer(peerId)\n })\n }\n\n // ---------------------------------------------------------------------------\n // PeerJS wiring\n // ---------------------------------------------------------------------------\n\n /** Attach the live PeerJS Peer instance after it opens. */\n attachPeer(peer: PeerJSPeer): void {\n this._pool.setPeer(peer)\n }\n\n // ---------------------------------------------------------------------------\n // Component schema\n // ---------------------------------------------------------------------------\n\n registerComponent(schema: ComponentSchema): void {\n this._tracker.registerComponent(schema)\n }\n\n unregisterComponent(componentId: number): void {\n this._tracker.unregisterComponent(componentId)\n }\n\n // ---------------------------------------------------------------------------\n // Group management\n // ---------------------------------------------------------------------------\n\n createGroup(config: InterestGroupConfig): InterestGroup {\n const existing = this._groups.get(config.id)\n if (existing) return existing\n\n const group = new InterestGroup(config, this.localPeerId)\n group.setSendFn((to, msg) => this._send(to, msg))\n group.startTick()\n this._groups.set(config.id, group)\n return group\n }\n\n destroyGroup(groupId: GroupId): void {\n const group = this._groups.get(groupId)\n if (!group) return\n group.dispose()\n this._groups.delete(groupId)\n }\n\n getGroup(groupId: GroupId): InterestGroup | undefined {\n return this._groups.get(groupId)\n }\n\n // ---------------------------------------------------------------------------\n // Peer connections\n // ---------------------------------------------------------------------------\n\n async connectPeer(peerId: PeerId): Promise<void> {\n await this._pool.connect(peerId)\n }\n\n disconnectPeer(peerId: PeerId): void {\n this._pool.disconnect(peerId)\n for (const group of this._groups.values()) {\n group.removePeer(peerId)\n }\n }\n\n onPeerDisconnect(handler: (peerId: PeerId) => void): () => void {\n return this._pool.onDisconnect(handler)\n }\n\n sendGameEvent(peerId: PeerId, payload: ArrayBuffer): void {\n this._send(peerId, {\n type: MessageType.GameEvent,\n flags: MessageFlag.None,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n })\n }\n\n onGameEvent(handler: (payload: ArrayBuffer, from: PeerId) => void): () => void {\n this._gameEventHandlers.push(handler)\n return () => {\n const idx = this._gameEventHandlers.indexOf(handler)\n if (idx !== -1) this._gameEventHandlers.splice(idx, 1)\n }\n }\n\n // ---------------------------------------------------------------------------\n // Group subscriptions\n // ---------------------------------------------------------------------------\n\n /**\n * Subscribe the local peer to a group and introduce remote peers.\n * Sends a Subscribe control message to each remote peer.\n */\n joinGroup(groupId: GroupId, remotePeers: PeerId[]): void {\n const group = this._requireGroup(groupId)\n for (const p of remotePeers) group.addPeer(p)\n this._sendSubscribe(groupId, remotePeers)\n }\n\n /**\n * Leave a group: notify remote peers and remove local state.\n */\n leaveGroup(groupId: GroupId): void {\n const group = this._groups.get(groupId)\n if (!group) return\n const peers = [...group.peerIds]\n this._sendUnsubscribe(groupId, peers)\n for (const p of peers) group.removePeer(p)\n }\n\n // ---------------------------------------------------------------------------\n // Delta pipeline\n // ---------------------------------------------------------------------------\n\n markEntityDirty(entityId: EntityId): void {\n this._tracker.markDirty(entityId)\n }\n\n markEntitiesDirty(entities: EntityId[]): void {\n this._tracker.markDirtyBatch(entities)\n }\n\n invalidateEntity(entityId: EntityId): void {\n this._tracker.invalidateEntity(entityId)\n }\n\n /**\n * Compute deltas for dirty entities and push them into the group's send queue.\n * The group's tick interval handles the actual sending.\n *\n * Call once per game tick, after ECS systems run.\n */\n flushGroupDeltas(groupId: GroupId, entities: EntityId[]): void {\n const group = this._groups.get(groupId)\n if (!group) return\n const deltas: DeltaSet = this._tracker.flush(entities)\n if (deltas.length > 0) group.publishDeltas(deltas)\n }\n\n /**\n * Send a full-state sync to a newly joined peer for a given group.\n * Generates a snapshot of all registered component fields for all provided entities.\n */\n sendFullSync(groupId: GroupId, toPeer: PeerId, entities: EntityId[]): void {\n const group = this._groups.get(groupId)\n if (!group) return\n const deltas = this._tracker.fullSnapshot(entities)\n if (deltas.length === 0) return\n const payload = encodeDeltaPayload(groupId, deltas)\n this._send(toPeer, {\n type: MessageType.FullSync,\n flags: MessageFlag.Reliable,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n })\n }\n\n onGroupDelta(groupId: GroupId, handler: DeltaHandler): () => void {\n return this._requireGroup(groupId).onDelta(handler)\n }\n\n // ---------------------------------------------------------------------------\n // Diagnostics\n // ---------------------------------------------------------------------------\n\n get rttMs(): number { return this._rttMs }\n get dirtyEntityCount(): number { return this._tracker.dirtyCount }\n\n ping(peerId: PeerId): void {\n const buf = new ArrayBuffer(8)\n new DataView(buf).setFloat64(0, performance.now(), true)\n this._send(peerId, {\n type: MessageType.Ping,\n flags: MessageFlag.None,\n seq: this._nextSeq(),\n ack: this._ack,\n payload: buf,\n })\n }\n\n // ---------------------------------------------------------------------------\n // Teardown\n // ---------------------------------------------------------------------------\n\n dispose(): void {\n for (const group of this._groups.values()) group.dispose()\n this._groups.clear()\n this._pool.dispose()\n }\n\n // ---------------------------------------------------------------------------\n // Private routing\n // ---------------------------------------------------------------------------\n\n private _onRawData(buf: ArrayBuffer, from: PeerId): void {\n let msg: NetMessage\n try { msg = decodeMessage(buf) } catch { return }\n this._ack = msg.seq\n\n switch (msg.type) {\n case MessageType.Subscribe: {\n this._onSubscribe(msg, from)\n break\n }\n case MessageType.Unsubscribe: {\n this._onUnsubscribe(msg, from)\n break\n }\n case MessageType.DeltaUpdate:\n case MessageType.FullSync:\n case MessageType.LeadClaim: {\n // Route to the group that owns this peer\n for (const group of this._groups.values()) {\n if (group.peerIds.has(from)) group.handleMessage(msg, from)\n }\n break\n }\n case MessageType.GameEvent: {\n for (const h of this._gameEventHandlers) h(msg.payload, from)\n break\n }\n case MessageType.Ping: {\n this._send(from, {\n type: MessageType.Pong,\n flags: MessageFlag.None,\n seq: this._nextSeq(),\n ack: this._ack,\n payload: msg.payload,\n })\n break\n }\n case MessageType.Pong: {\n try {\n const sent = new DataView(msg.payload).getFloat64(0, true)\n const rtt = performance.now() - sent\n this._rttMs += this._config.rttAlpha * (rtt - this._rttMs)\n } catch { /* malformed */ }\n break\n }\n }\n }\n\n private _onSubscribe(msg: NetMessage, from: PeerId): void {\n try {\n const { groupId } = decodeJson(msg.payload)\n if (typeof groupId === 'string') this._groups.get(groupId)?.addPeer(from)\n } catch { /* malformed */ }\n }\n\n private _onUnsubscribe(msg: NetMessage, from: PeerId): void {\n try {\n const { groupId } = decodeJson(msg.payload)\n if (typeof groupId === 'string') this._groups.get(groupId)?.removePeer(from)\n } catch { /* malformed */ }\n }\n\n private _sendSubscribe(groupId: GroupId, peers: PeerId[]): void {\n const payload = encodeJson({ groupId })\n const msg: NetMessage = {\n type: MessageType.Subscribe,\n flags: MessageFlag.Reliable,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n }\n for (const p of peers) this._send(p, msg)\n }\n\n private _sendUnsubscribe(groupId: GroupId, peers: PeerId[]): void {\n const payload = encodeJson({ groupId })\n const msg: NetMessage = {\n type: MessageType.Unsubscribe,\n flags: MessageFlag.Reliable,\n seq: this._nextSeq(),\n ack: this._ack,\n payload,\n }\n for (const p of peers) this._send(p, msg)\n }\n\n private _send(to: PeerId, msg: NetMessage): void {\n this._pool.send(to, encodeMessage(msg))\n }\n\n private _nextSeq(): number {\n return (this._seq = (this._seq + 1) & 0xffff)\n }\n\n private _requireGroup(groupId: GroupId): InterestGroup {\n const group = this._groups.get(groupId)\n if (!group) throw new Error(`Interest group \"${groupId}\" not found — call createGroup() first`)\n return group\n }\n}\n","// Min-heap priority queue. Lower priority value = higher urgency.\n// Stable: equal-priority items are dequeued in insertion order.\n\nexport interface PQEntry<T> {\n readonly priority: number\n readonly seq: number\n readonly value: T\n}\n\nexport class PriorityQueue<T> {\n private _heap: PQEntry<T>[] = []\n private _seq = 0\n\n get size(): number { return this._heap.length }\n get isEmpty(): boolean { return this._heap.length === 0 }\n\n push(value: T, priority: number): void {\n const entry: PQEntry<T> = { priority, seq: this._seq++, value }\n this._heap.push(entry)\n this._bubbleUp(this._heap.length - 1)\n }\n\n pop(): T | undefined {\n if (this._heap.length === 0) return undefined\n const top = this._heap[0]\n const last = this._heap.pop()!\n if (this._heap.length > 0) {\n this._heap[0] = last\n this._siftDown(0)\n }\n return top.value\n }\n\n peek(): T | undefined {\n return this._heap[0]?.value\n }\n\n clear(): void {\n this._heap = []\n }\n\n private _lt(a: PQEntry<T>, b: PQEntry<T>): boolean {\n return a.priority !== b.priority ? a.priority < b.priority : a.seq < b.seq\n }\n\n private _bubbleUp(i: number): void {\n while (i > 0) {\n const parent = (i - 1) >> 1\n if (this._lt(this._heap[i], this._heap[parent])) {\n ;[this._heap[i], this._heap[parent]] = [this._heap[parent], this._heap[i]]\n i = parent\n } else break\n }\n }\n\n private _siftDown(i: number): void {\n const n = this._heap.length\n for (;;) {\n let min = i\n const l = (i << 1) + 1\n const r = l + 1\n if (l < n && this._lt(this._heap[l], this._heap[min])) min = l\n if (r < n && this._lt(this._heap[r], this._heap[min])) min = r\n if (min === i) break\n ;[this._heap[i], this._heap[min]] = [this._heap[min], this._heap[i]]\n i = min\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmBO,IAAM,cAAc;AAEpB,IAAW,cAAX,kBAAWA,iBAAX;AACL,EAAAA,0BAAA,eAAc,KAAd;AACA,EAAAA,0BAAA,eAAc,KAAd;AACA,EAAAA,0BAAA,iBAAc,KAAd;AACA,EAAAA,0BAAA,iBAAc,KAAd;AACA,EAAAA,0BAAA,cAAc,KAAd;AACA,EAAAA,0BAAA,SAAc,KAAd;AACA,EAAAA,0BAAA,UAAc,KAAd;AACA,EAAAA,0BAAA,UAAc,KAAd;AACA,EAAAA,0BAAA,eAAc,KAAd;AACA,EAAAA,0BAAA,eAAc,MAAd;AACA,EAAAA,0BAAA,cAAc,MAAd;AACA,EAAAA,0BAAA,eAAc,MAAd;AAZgB,SAAAA;AAAA,GAAA;AAeX,IAAW,cAAX,kBAAWC,iBAAX;AACL,EAAAA,0BAAA,UAAa,KAAb;AACA,EAAAA,0BAAA,cAAa,KAAb;AACA,EAAAA,0BAAA,gBAAa,KAAb;AACA,EAAAA,0BAAA,gBAAa,KAAb;AAJgB,SAAAA;AAAA,GAAA;;;ACnBX,IAAM,gBAAN,MAAoB;AAAA,EAApB;AACL,SAAQ,WAAW,oBAAI,IAAkC;AAEzD;AAAA,SAAQ,aAAa,oBAAI,IAA8B;AACvD,SAAQ,YAAY,oBAAI,IAAc;AAAA;AAAA,EAEtC,kBAAkB,QAA+B;AAC/C,QAAI,KAAK,SAAS,IAAI,OAAO,EAAE,GAAG;AAChC,YAAM,IAAI,MAAM,gBAAgB,OAAO,EAAE,qBAAqB;AAAA,IAChE;AACA,SAAK,SAAS,IAAI,OAAO,IAAI,MAAM;AAAA,EACrC;AAAA,EAEA,oBAAoB,aAAgC;AAClD,SAAK,SAAS,OAAO,WAAW;AAEhC,eAAW,QAAQ,KAAK,WAAW,OAAO,GAAG;AAC3C,WAAK,OAAO,WAAW;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,UAAU,UAA0B;AAClC,SAAK,UAAU,IAAI,QAAQ;AAAA,EAC7B;AAAA,EAEA,eAAe,UAA4B;AACzC,eAAW,OAAO,SAAU,MAAK,UAAU,IAAI,GAAG;AAAA,EACpD;AAAA,EAEA,IAAI,aAAqB;AAAE,WAAO,KAAK,UAAU;AAAA,EAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,MAAM,UAAgC;AACpC,UAAM,SAAmB,CAAC;AAE1B,eAAW,OAAO,UAAU;AAC1B,UAAI,CAAC,KAAK,UAAU,IAAI,GAAG,EAAG;AAE9B,UAAI,aAAa,KAAK,WAAW,IAAI,GAAG;AACxC,UAAI,CAAC,YAAY;AAAE,qBAAa,oBAAI,IAAI;AAAG,aAAK,WAAW,IAAI,KAAK,UAAU;AAAA,MAAE;AAEhF,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK,UAAU;AACzC,cAAM,aAAa,OAAO,OAAO;AACjC,YAAI,WAAW,WAAW,IAAI,GAAG;AACjC,YAAI,CAAC,UAAU;AACb,qBAAW,IAAI,aAAa,UAAU,EAAE,KAAK,GAAG;AAChD,qBAAW,IAAI,KAAK,QAAQ;AAAA,QAC9B;AAEA,iBAAS,KAAK,GAAG,KAAK,YAAY,MAAM;AACtC,gBAAM,QAAQ,OAAO,OAAO,EAAE;AAC9B,gBAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,cAAI,CAAC,OAAO,GAAG,SAAS,EAAE,GAAG,GAAG,GAAG;AACjC,qBAAS,EAAE,IAAI;AACf,mBAAO,KAAK,EAAE,UAAU,KAAK,aAAa,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC;AAAA,UAC1E;AAAA,QACF;AAAA,MACF;AAEA,WAAK,UAAU,OAAO,GAAG;AAAA,IAC3B;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,UAAgC;AAC3C,UAAM,SAAmB,CAAC;AAE1B,eAAW,OAAO,UAAU;AAC1B,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK,UAAU;AACzC,iBAAS,KAAK,GAAG,KAAK,OAAO,OAAO,QAAQ,MAAM;AAChD,iBAAO,KAAK;AAAA,YACV,UAAU;AAAA,YACV,aAAa;AAAA,YACb,SAAS;AAAA,YACT,OAAO,OAAO,OAAO,EAAE,EAAE,MAAM,GAAG;AAAA,UACpC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,UAA0B;AACzC,SAAK,WAAW,OAAO,QAAQ;AAC/B,SAAK,UAAU,IAAI,QAAQ;AAAA,EAC7B;AAAA,EAEA,gBAAsB;AACpB,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;;;ACrEO,IAAM,kBAAN,MAAM,gBAAe;AAAA,EAArB;AACL,SAAQ,QAA2B;AACnC,SAAQ,SAAS,oBAAI,IAAyB;AAC9C,SAAQ,YAAY,oBAAI,IAAiB;AACzC,SAAQ,sBAAsB,oBAAI,IAAuB;AAEzD;AAAA,SAAQ,kBAAkB,oBAAI,IAAoB;AAClD,SAAQ,oBAAoB,oBAAI,IAA2C;AAAA;AAAA,EAI3E,QAAQ,MAAwB;AAC9B,SAAK,QAAQ;AACb,SAAK,GAAG,cAAc,UAAQ,KAAK,cAAc,KAAK,MAAM,IAAI,CAAC;AAAA,EACnE;AAAA,EAEA,MAAM,QAAQ,QAA+B;AAC3C,UAAM,WAAW,KAAK,OAAO,IAAI,MAAM;AACvC,QAAI,UAAU,UAAU,OAAQ;AAChC,QAAI,UAAU,UAAU,UAAW,QAAO,SAAS;AACnD,QAAI,CAAC,KAAK,MAAO,OAAM,IAAI,MAAM,kDAA6C;AAC9E,UAAM,OAAO,KAAK,MAAM,QAAQ,QAAQ,EAAE,UAAU,OAAO,eAAe,SAAS,CAAC;AACpF,SAAK,cAAc,QAAQ,IAAI;AAC/B,WAAO,KAAK,OAAO,IAAI,MAAM,EAAG;AAAA,EAClC;AAAA,EAEA,KAAK,QAAgB,MAAyB;AAC5C,UAAM,UAAU,KAAK,OAAO,IAAI,MAAM;AACtC,QAAI,CAAC,SAAS;AAEZ,WAAK,KAAK,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAK,KAAK,QAAQ,IAAI,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC5E;AAAA,IACF;AACA,QAAI,QAAQ,UAAU,QAAQ;AAC5B,cAAQ,KAAK,KAAK,IAAI;AAAA,IACxB,OAAO;AACL,cAAQ,MAAM,KAAK,IAAI;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,OAAO,QAAyB;AAC9B,WAAO,KAAK,OAAO,IAAI,MAAM,GAAG,UAAU;AAAA,EAC5C;AAAA,EAEA,WAAW,QAAsB;AAC/B,iBAAa,KAAK,kBAAkB,IAAI,MAAM,CAAC;AAC/C,SAAK,kBAAkB,OAAO,MAAM;AACpC,SAAK,gBAAgB,OAAO,MAAM;AAClC,UAAM,UAAU,KAAK,OAAO,IAAI,MAAM;AACtC,QAAI,SAAS;AAAE,cAAQ,KAAK,MAAM;AAAG,WAAK,OAAO,OAAO,MAAM;AAAA,IAAE;AAAA,EAClE;AAAA,EAEA,OAAO,SAAkC;AACvC,SAAK,UAAU,IAAI,OAAO;AAC1B,WAAO,MAAM,KAAK,UAAU,OAAO,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,aAAa,SAAwC;AACnD,SAAK,oBAAoB,IAAI,OAAO;AACpC,WAAO,MAAM,KAAK,oBAAoB,OAAO,OAAO;AAAA,EACtD;AAAA,EAEA,UAAgB;AACd,eAAW,KAAK,KAAK,kBAAkB,OAAO,EAAG,cAAa,CAAC;AAC/D,SAAK,kBAAkB,MAAM;AAC7B,eAAW,KAAK,KAAK,OAAO,OAAO,EAAG,GAAE,KAAK,MAAM;AACnD,SAAK,OAAO,MAAM;AAClB,SAAK,OAAO,QAAQ;AACpB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAIQ,cAAc,QAAgB,MAAkC;AAEtE,UAAM,WAAW,KAAK,OAAO,IAAI,MAAM;AACvC,QAAI,UAAU,UAAU,OAAQ;AAEhC,QAAI;AACJ,QAAI;AACJ,UAAM,cAAc,IAAI,QAAc,CAAC,KAAK,QAAQ;AAAE,gBAAU;AAAK,eAAS;AAAA,IAAI,CAAC;AAEnF,UAAM,UAAuB,EAAE,MAAM,OAAO,WAAW,OAAO,CAAC,GAAG,SAAS,QAAQ,YAAY;AAC/F,SAAK,OAAO,IAAI,QAAQ,OAAO;AAE/B,SAAK,GAAG,QAAQ,MAAM;AACpB,cAAQ,QAAQ;AAChB,WAAK,gBAAgB,OAAO,MAAM;AAClC,mBAAa,KAAK,kBAAkB,IAAI,MAAM,CAAC;AAC/C,WAAK,kBAAkB,OAAO,MAAM;AACpC,iBAAW,OAAO,QAAQ,MAAO,MAAK,KAAK,GAAG;AAC9C,cAAQ,QAAQ,CAAC;AACjB,cAAQ;AAAA,IACV,CAAC;AAED,SAAK,GAAG,QAAQ,SAAO;AACrB,YAAM,MAA0B,eAAe,cAAc,MACzD,eAAe,aAAa,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU,IAC5F;AACJ,UAAI,CAAC,IAAK;AACV,iBAAW,KAAK,KAAK,UAAW,GAAE,KAAK,MAAM;AAAA,IAC/C,CAAC;AAED,SAAK,GAAG,SAAS,MAAM;AACrB,cAAQ,QAAQ;AAChB,iBAAW,KAAK,KAAK,oBAAqB,GAAE,MAAM;AAClD,WAAK,mBAAmB,MAAM;AAAA,IAChC,CAAC;AAED,SAAK,GAAG,SAAS,SAAO;AACtB,cAAQ,QAAQ;AAChB,aAAO,GAAG;AACV,WAAK,mBAAmB,MAAM;AAAA,IAChC,CAAC;AAAA,EACH;AAAA,EAEQ,mBAAmB,QAAsB;AAC/C,QAAI,CAAC,KAAK,MAAO;AACjB,UAAM,OAAO,KAAK,gBAAgB,IAAI,MAAM,KAAK,gBAAe,aAAa;AAC7E,UAAM,QAAQ,KAAK,IAAI,OAAO,GAAG,gBAAe,SAAS;AACzD,SAAK,gBAAgB,IAAI,QAAQ,KAAK;AACtC,iBAAa,KAAK,kBAAkB,IAAI,MAAM,CAAC;AAC/C,UAAM,SAAS,WAAW,MAAM;AAC9B,UAAI,KAAK,OAAO,IAAI,MAAM,GAAG,UAAU,QAAQ;AAC7C,aAAK,KAAK,QAAQ,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC1C;AAAA,IACF,GAAG,KAAK;AACR,SAAK,kBAAkB,IAAI,QAAQ,MAAM;AAAA,EAC3C;AACF;AAlIa,gBAQa,aAAa;AAR1B,gBASa,YAAa;AAThC,IAAM,iBAAN;;;AC9CP,IAAM,mBAAmB;AACzB,IAAM,WAAW;AACjB,IAAM,QAAuB,CAAC;AAE9B,SAAS,gBAA6B;AACpC,SAAO,MAAM,IAAI,KAAK,IAAI,YAAY,gBAAgB;AACxD;AAEA,SAAS,cAAc,KAAwB;AAC7C,MAAI,IAAI,eAAe,oBAAoB,MAAM,SAAS,UAAU;AAClE,UAAM,KAAK,GAAG;AAAA,EAChB;AACF;AAMO,SAAS,aACd,MACA,MACA,OACA,KACA,KACA,YACM;AACN,OAAK,SAAS,GAAG,IAAI;AACrB,OAAK,SAAS,GAAG,KAAK;AACtB,OAAK,UAAU,GAAG,KAAK,IAAI;AAC3B,OAAK,UAAU,GAAG,KAAK,IAAI;AAC3B,OAAK,UAAU,GAAG,YAAY,IAAI;AACpC;AAEO,SAAS,aAAa,MAM3B;AACA,SAAO;AAAA,IACL,MAAM,KAAK,SAAS,CAAC;AAAA,IACrB,OAAO,KAAK,SAAS,CAAC;AAAA,IACtB,KAAK,KAAK,UAAU,GAAG,IAAI;AAAA,IAC3B,KAAK,KAAK,UAAU,GAAG,IAAI;AAAA,IAC3B,YAAY,KAAK,UAAU,GAAG,IAAI;AAAA,EACpC;AACF;AAMO,SAAS,cAAc,KAA8B;AAC1D,QAAM,aAAa,IAAI,QAAQ;AAC/B,QAAM,QAAQ,cAAc;AAC5B,QAAM,MAAM,SAAS,mBAAmB,cAAc,IAAI,IAAI,YAAY,KAAK;AAC/E,eAAa,IAAI,SAAS,GAAG,GAAG,IAAI,MAAM,IAAI,OAAO,IAAI,KAAK,IAAI,KAAK,UAAU;AACjF,MAAI,WAAW,KAAK,aAAa,UAAU,EAAE,IAAI,IAAI,WAAW,IAAI,OAAO,CAAC;AAC5E,QAAM,MAAM,IAAI,MAAM,GAAG,KAAK;AAC9B,gBAAc,GAAG;AACjB,SAAO;AACT;AAEO,SAAS,cAAc,KAA8B;AAC1D,QAAM,OAAO,IAAI,SAAS,GAAG;AAC7B,QAAM,EAAE,MAAM,OAAO,KAAK,KAAK,WAAW,IAAI,aAAa,IAAI;AAC/D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,IAAI,MAAM,aAAa,cAAc,UAAU;AAAA,EAC1D;AACF;AAgBO,SAAS,mBAAmB,SAAiB,QAA+B;AACjF,QAAM,MAAM,IAAI,YAAY;AAC5B,QAAM,eAAe,IAAI,OAAO,OAAO;AAGvC,QAAM,WAAW,oBAAI,IAAkD;AACvE,aAAW,EAAE,UAAU,aAAa,SAAS,MAAM,KAAK,QAAQ;AAC9D,QAAI,SAAS,SAAS,IAAI,QAAQ;AAClC,QAAI,CAAC,QAAQ;AAAE,eAAS,oBAAI,IAAI;AAAG,eAAS,IAAI,UAAU,MAAM;AAAA,IAAE;AAClE,QAAI,SAAS,OAAO,IAAI,WAAW;AACnC,QAAI,CAAC,QAAQ;AAAE,eAAS,CAAC;AAAG,aAAO,IAAI,aAAa,MAAM;AAAA,IAAE;AAC5D,WAAO,KAAK,CAAC,SAAS,KAAK,CAAC;AAAA,EAC9B;AAGA,QAAM,UAAU,IAAI,aAAa,SAAS,IAAI,OAAO,SAAS;AAC9D,QAAM,MAAM,WAAW,mBAAmB,cAAc,IAAI,IAAI,YAAY,OAAO;AACnF,QAAM,OAAO,IAAI,SAAS,GAAG;AAC7B,MAAI,MAAM;AAEV,OAAK,SAAS,KAAK,aAAa,MAAM;AAAG,SAAO;AAChD,MAAI,WAAW,KAAK,KAAK,aAAa,MAAM,EAAE,IAAI,YAAY;AAAG,SAAO,aAAa;AACrF,OAAK,UAAU,KAAK,SAAS,MAAM,IAAI;AAAG,SAAO;AAEjD,aAAW,CAAC,KAAK,MAAM,KAAK,UAAU;AACpC,SAAK,UAAU,KAAK,KAAK,IAAI;AAAG,WAAO;AACvC,SAAK,SAAS,KAAK,OAAO,IAAI;AAAG,WAAO;AACxC,eAAW,CAAC,KAAK,MAAM,KAAK,QAAQ;AAClC,WAAK,SAAS,KAAK,GAAG;AAAG,aAAO;AAChC,WAAK,SAAS,KAAK,OAAO,MAAM;AAAG,aAAO;AAC1C,iBAAW,CAAC,KAAK,GAAG,KAAK,QAAQ;AAC/B,aAAK,SAAS,KAAK,GAAG;AAAG,eAAO;AAChC,aAAK,WAAW,KAAK,KAAK,IAAI;AAAG,eAAO;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,IAAI,MAAM,GAAG,GAAG;AAC5B,gBAAc,GAAG;AACjB,SAAO;AACT;AAEO,SAAS,mBAAmB,KAAyD;AAC1F,QAAM,OAAO,IAAI,SAAS,GAAG;AAC7B,QAAM,MAAM,IAAI,YAAY;AAC5B,MAAI,MAAM;AAEV,QAAM,aAAa,KAAK,SAAS,GAAG;AAAG,SAAO;AAC9C,QAAM,UAAU,IAAI,OAAO,IAAI,MAAM,KAAK,MAAM,UAAU,CAAC;AAAG,SAAO;AACrE,QAAM,cAAc,KAAK,UAAU,KAAK,IAAI;AAAG,SAAO;AAEtD,QAAM,SAAuB,CAAC;AAC9B,WAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,UAAM,WAAW,KAAK,UAAU,KAAK,IAAI;AAAG,WAAO;AACnD,UAAM,iBAAiB,KAAK,SAAS,GAAG;AAAG,WAAO;AAClD,aAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACvC,YAAM,cAAc,KAAK,SAAS,GAAG;AAAG,aAAO;AAC/C,YAAM,aAAa,KAAK,SAAS,GAAG;AAAG,aAAO;AAC9C,eAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,cAAM,UAAU,KAAK,SAAS,GAAG;AAAG,eAAO;AAC3C,cAAM,QAAQ,KAAK,WAAW,KAAK,IAAI;AAAG,eAAO;AACjD,eAAO,KAAK,EAAE,UAAU,aAAa,SAAS,MAAM,CAAC;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO;AAC3B;AAMA,IAAM,OAAO,IAAI,YAAY;AAC7B,IAAM,OAAO,IAAI,YAAY;AAEtB,SAAS,WAAW,KAA2C;AACpE,SAAO,KAAK,OAAO,KAAK,UAAU,GAAG,CAAC,EAAE;AAC1C;AAEO,SAAS,WAAW,KAA2C;AACpE,SAAO,KAAK,MAAM,KAAK,OAAO,GAAG,CAAC;AACpC;;;AC/KA,SAAS,MAAM,GAAmB;AAChC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAK,EAAE,WAAW,CAAC;AACnB,QAAI,KAAK,KAAK,GAAG,QAAU,MAAM;AAAA,EACnC;AACA,SAAO;AACT;AAsBO,IAAM,cAAN,MAAkB;AAAA,EAIvB,YAAY,WAA6B,UAAU;AAFnD,SAAQ,YAAY,oBAAI,IAAsB;AAG5C,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,eAAe,MAAsB;AACnC,SAAK,UAAU,IAAI,KAAK,QAAQ,IAAI;AAAA,EACtC;AAAA,EAEA,eAAe,QAAsB;AACnC,SAAK,UAAU,OAAO,MAAM;AAAA,EAC9B;AAAA,EAEA,UAAU,SAAkB,YAA8B;AACxD,QAAI,WAAW,WAAW,EAAG,OAAM,IAAI,MAAM,4BAA4B;AACzE,QAAI,WAAW,WAAW,EAAG,QAAO,WAAW,CAAC;AAEhD,YAAQ,KAAK,WAAW;AAAA,MACtB,KAAK;AAAiB,eAAO,KAAK,OAAO,UAAU;AAAA,MACnD,KAAK;AAAiB,eAAO,KAAK,UAAU,SAAS,UAAU;AAAA,MAC/D,KAAK;AAAiB,eAAO,KAAK,cAAc,UAAU;AAAA,IAC5D;AAAA,EACF;AAAA,EAEA,aAAa,SAAkB,aAAqB,YAA+B;AACjF,QAAI;AACF,aAAO,KAAK,UAAU,SAAS,UAAU,MAAM;AAAA,IACjD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,OAAO,YAA8B;AAC3C,WAAO,WAAW,OAAO,CAAC,MAAM,OAAQ,KAAK,OAAO,KAAK,IAAK;AAAA,EAChE;AAAA,EAEQ,UAAU,SAAkB,YAA8B;AAChE,UAAM,SAAS,MAAM,OAAO;AAE5B,UAAM,OAA+C,CAAC;AACtD,eAAW,UAAU,YAAY;AAC/B,eAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,aAAK,KAAK,EAAE,KAAK,MAAM,GAAG,MAAM,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC;AAAA,MACtD;AAAA,IACF;AACA,SAAK,KAAK,CAAC,GAAG,MAAO,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,CAAE;AAEhE,YAAQ,KAAK,KAAK,OAAK,EAAE,OAAO,MAAM,KAAK,KAAK,CAAC,GAAG;AAAA,EACtD;AAAA,EAEQ,cAAc,YAA8B;AAClD,QAAI,OAAO,WAAW,CAAC;AACvB,QAAI,YAAY;AAChB,eAAW,UAAU,YAAY;AAC/B,YAAM,OAAO,KAAK,UAAU,IAAI,MAAM;AAEtC,YAAM,QAAQ,OAAO,KAAK,kBAAkB,IAAI,KAAK,iBAAiB,IAAI;AAC1E,UAAI,QAAQ,WAAW;AAAE,oBAAY;AAAO,eAAO;AAAA,MAAO;AAAA,IAC5D;AACA,WAAO;AAAA,EACT;AACF;;;AC5DO,IAAM,gBAAN,MAAoB;AAAA,EAezB,YAAY,QAA6B,aAAqB;AAV9D,SAAQ,SAAS,oBAAI,IAAuB;AAC5C,SAAQ,UAAyB;AAEjC,SAAQ,UAAyB;AACjC,SAAQ,iBAAiB,oBAAI,IAAkB;AAC/C,SAAQ,WAAqB,CAAC;AAC9B,SAAQ,cAAqD;AAC7D,SAAQ,OAAO;AACf,SAAQ,OAAO;AAGb,SAAK,KAAK,OAAO;AACjB,SAAK,UAAU;AAAA,MACb,IAAI,OAAO;AAAA,MACX,aAAa,OAAO,eAAe;AAAA,MACnC,YAAY,OAAO,cAAc;AAAA,MACjC,kBAAkB,OAAO,oBAAoB;AAAA,IAC/C;AACA,SAAK,eAAe;AACpB,SAAK,WAAW,IAAI,YAAY,KAAK,QAAQ,gBAAoC;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,SAAwB;AAAE,WAAO,KAAK;AAAA,EAAQ;AAAA,EAClD,IAAI,SAAkB;AAAE,WAAO,KAAK,YAAY,KAAK;AAAA,EAAa;AAAA,EAClE,IAAI,YAAoB;AAAE,WAAO,KAAK,OAAO;AAAA,EAAK;AAAA,EAElD,QAAQ,QAAiD;AACvD,WAAO,KAAK,OAAO,IAAI,MAAM;AAAA,EAC/B;AAAA,EAEA,IAAI,UAA+B;AACjC,WAAO,IAAI,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,IAAkB;AAAE,SAAK,UAAU;AAAA,EAAG;AAAA;AAAA;AAAA;AAAA,EAMhD,QAAQ,QAAsB;AAC5B,QAAI,KAAK,OAAO,IAAI,MAAM,EAAG;AAC7B,SAAK,OAAO,IAAI,QAAQ;AAAA,MACtB;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,KAAK,IAAI;AAAA,MACnB,YAAY,KAAK,IAAI;AAAA,MACrB,OAAO;AAAA,IACT,CAAC;AACD,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,WAAW,QAAsB;AAC/B,QAAI,CAAC,KAAK,OAAO,IAAI,MAAM,EAAG;AAC9B,SAAK,OAAO,OAAO,MAAM;AACzB,SAAK,SAAS,eAAe,MAAM;AACnC,QAAI,KAAK,YAAY,QAAQ;AAC3B,WAAK,UAAU;AACf,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,QAAwB;AACpC,QAAI,OAAO,SAAS,EAAG,MAAK,SAAS,KAAK,GAAG,MAAM;AAAA,EACrD;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,SAAS,WAAW,KAAK,CAAC,KAAK,QAAS;AAEjD,UAAM,UAAU,mBAAmB,KAAK,IAAI,KAAK,QAAQ;AACzD,SAAK,WAAW,CAAC;AAEjB,UAAM,MAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF;AAEA,QAAI,KAAK,QAAQ;AAEf,iBAAW,UAAU,KAAK,OAAO,KAAK,GAAG;AACvC,aAAK,QAAQ,QAAQ,GAAG;AAAA,MAC1B;AAAA,IACF,WAAW,KAAK,YAAY,MAAM;AAEhC,WAAK,QAAQ,KAAK,SAAS,GAAG;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,KAAiB,UAAwB;AACrD,UAAM,QAAQ,KAAK,OAAO,IAAI,QAAQ;AACtC,QAAI,MAAO,OAAM,aAAa,KAAK,IAAI;AACvC,SAAK,OAAO,IAAI;AAEhB,YAAQ,IAAI,MAAM;AAAA,MAChB;AACE,aAAK,eAAe,KAAK,QAAQ;AACjC;AAAA,MACF;AACE,aAAK,YAAY,KAAK,QAAQ;AAC9B;AAAA,MACF;AACE,aAAK,aAAa,GAAG;AACrB;AAAA,IACJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,SAAmC;AACzC,SAAK,eAAe,IAAI,OAAO;AAC/B,WAAO,MAAM,KAAK,eAAe,OAAO,OAAO;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAMA,YAAkB;AAChB,QAAI,KAAK,gBAAgB,KAAM;AAC/B,SAAK,cAAc,YAAY,MAAM,KAAK,MAAM,GAAG,KAAK,QAAQ,UAAU;AAAA,EAC5E;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,gBAAgB,MAAM;AAC7B,oBAAc,KAAK,WAAW;AAC9B,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,SAAS;AACd,SAAK,eAAe,MAAM;AAC1B,SAAK,OAAO,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAiB;AACvB,QAAI,KAAK,OAAO,SAAS,GAAG;AAE1B,WAAK,UAAU,KAAK;AACpB;AAAA,IACF;AACA,UAAM,aAAa,CAAC,KAAK,cAAc,GAAG,KAAK,OAAO,KAAK,CAAC;AAC5D,UAAM,UAAU,KAAK,SAAS,UAAU,KAAK,IAAI,UAAU;AAC3D,QAAI,YAAY,KAAK,SAAS;AAC5B,WAAK,UAAU;AACf,UAAI,YAAY,KAAK,cAAc;AACjC,aAAK,oBAAoB;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,sBAA4B;AAClC,QAAI,CAAC,KAAK,QAAS;AACnB,UAAM,UAAU,WAAW,EAAE,SAAS,KAAK,IAAI,QAAQ,KAAK,aAAa,CAAC;AAC1E,UAAM,MAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF;AACA,eAAW,UAAU,KAAK,OAAO,KAAK,GAAG;AACvC,WAAK,QAAQ,QAAQ,GAAG;AAAA,IAC1B;AAAA,EACF;AAAA,EAEQ,eAAe,KAAiB,UAAwB;AAC9D,QAAI;AACJ,QAAI;AACJ,QAAI;AACF;AAAC,OAAC,EAAE,SAAS,OAAO,IAAI,mBAAmB,IAAI,OAAO;AAAA,IACxD,QAAQ;AAAE;AAAA,IAAO;AACjB,QAAI,YAAY,KAAK,GAAI;AAEzB,QAAI,KAAK,UAAU,KAAK,SAAS;AAE/B,iBAAW,UAAU,KAAK,OAAO,KAAK,GAAG;AACvC,YAAI,WAAW,SAAU,MAAK,QAAQ,QAAQ,GAAG;AAAA,MACnD;AAAA,IACF;AAEA,eAAW,KAAK,KAAK,eAAgB,GAAE,QAAQ,KAAK,IAAI,QAAQ;AAAA,EAClE;AAAA,EAEQ,YAAY,KAAiB,UAAwB;AAC3D,QAAI;AACJ,QAAI;AACJ,QAAI;AACF;AAAC,OAAC,EAAE,SAAS,OAAO,IAAI,mBAAmB,IAAI,OAAO;AAAA,IACxD,QAAQ;AAAE;AAAA,IAAO;AACjB,QAAI,YAAY,KAAK,GAAI;AAEzB,eAAW,KAAK,KAAK,eAAgB,GAAE,QAAQ,KAAK,IAAI,QAAQ;AAAA,EAClE;AAAA,EAEQ,aAAa,KAAuB;AAC1C,QAAI;AACF,YAAM,EAAE,SAAS,OAAO,IAAI,WAAW,IAAI,OAAO;AAClD,UAAI,YAAY,KAAK,MAAM,OAAO,WAAW,SAAU;AAEvD,UAAI,KAAK,OAAO,IAAI,MAAM,KAAK,WAAW,KAAK,cAAc;AAC3D,aAAK,UAAU;AACf,mBAAW,CAAC,KAAK,KAAK,KAAK,KAAK,OAAQ,OAAM,SAAS,QAAQ;AAAA,MACjE;AAAA,IACF,QAAQ;AAAA,IAAkB;AAAA,EAC5B;AAAA,EAEQ,WAAmB;AACzB,WAAQ,KAAK,OAAQ,KAAK,OAAO,IAAK;AAAA,EACxC;AACF;;;AClPO,IAAM,UAAN,MAAc;AAAA,EAYnB,YAAY,QAAuB;AAPnC,SAAQ,UAAU,oBAAI,IAA4B;AAClD,SAAQ,WAAW,IAAI,cAAc;AACrC,SAAQ,OAAO;AACf,SAAQ,OAAO;AACf,SAAQ,SAAS;AACjB,SAAQ,qBAAuE,CAAC;AAG9E,SAAK,cAAc,OAAO;AAC1B,SAAK,UAAU;AAAA,MACb,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO,YAAY;AAAA,IAC/B;AACA,SAAK,QAAQ,IAAI,eAAe;AAChC,SAAK,MAAM,OAAO,CAAC,KAAK,SAAS,KAAK,WAAW,KAAK,IAAI,CAAC;AAC3D,SAAK,MAAM,aAAa,YAAU;AAChC,iBAAW,SAAS,KAAK,QAAQ,OAAO,EAAG,OAAM,WAAW,MAAM;AAAA,IACpE,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,MAAwB;AACjC,SAAK,MAAM,QAAQ,IAAI;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAMA,kBAAkB,QAA+B;AAC/C,SAAK,SAAS,kBAAkB,MAAM;AAAA,EACxC;AAAA,EAEA,oBAAoB,aAA2B;AAC7C,SAAK,SAAS,oBAAoB,WAAW;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,QAA4C;AACtD,UAAM,WAAW,KAAK,QAAQ,IAAI,OAAO,EAAE;AAC3C,QAAI,SAAU,QAAO;AAErB,UAAM,QAAQ,IAAI,cAAc,QAAQ,KAAK,WAAW;AACxD,UAAM,UAAU,CAAC,IAAI,QAAQ,KAAK,MAAM,IAAI,GAAG,CAAC;AAChD,UAAM,UAAU;AAChB,SAAK,QAAQ,IAAI,OAAO,IAAI,KAAK;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,aAAa,SAAwB;AACnC,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,QAAQ;AACd,SAAK,QAAQ,OAAO,OAAO;AAAA,EAC7B;AAAA,EAEA,SAAS,SAA6C;AACpD,WAAO,KAAK,QAAQ,IAAI,OAAO;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAY,QAA+B;AAC/C,UAAM,KAAK,MAAM,QAAQ,MAAM;AAAA,EACjC;AAAA,EAEA,eAAe,QAAsB;AACnC,SAAK,MAAM,WAAW,MAAM;AAC5B,eAAW,SAAS,KAAK,QAAQ,OAAO,GAAG;AACzC,YAAM,WAAW,MAAM;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,iBAAiB,SAA+C;AAC9D,WAAO,KAAK,MAAM,aAAa,OAAO;AAAA,EACxC;AAAA,EAEA,cAAc,QAAgB,SAA4B;AACxD,SAAK,MAAM,QAAQ;AAAA,MACjB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,YAAY,SAAmE;AAC7E,SAAK,mBAAmB,KAAK,OAAO;AACpC,WAAO,MAAM;AACX,YAAM,MAAM,KAAK,mBAAmB,QAAQ,OAAO;AACnD,UAAI,QAAQ,GAAI,MAAK,mBAAmB,OAAO,KAAK,CAAC;AAAA,IACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,SAAkB,aAA6B;AACvD,UAAM,QAAQ,KAAK,cAAc,OAAO;AACxC,eAAW,KAAK,YAAa,OAAM,QAAQ,CAAC;AAC5C,SAAK,eAAe,SAAS,WAAW;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,SAAwB;AACjC,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,QAAQ,CAAC,GAAG,MAAM,OAAO;AAC/B,SAAK,iBAAiB,SAAS,KAAK;AACpC,eAAW,KAAK,MAAO,OAAM,WAAW,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,UAA0B;AACxC,SAAK,SAAS,UAAU,QAAQ;AAAA,EAClC;AAAA,EAEA,kBAAkB,UAA4B;AAC5C,SAAK,SAAS,eAAe,QAAQ;AAAA,EACvC;AAAA,EAEA,iBAAiB,UAA0B;AACzC,SAAK,SAAS,iBAAiB,QAAQ;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,SAAkB,UAA4B;AAC7D,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,SAAmB,KAAK,SAAS,MAAM,QAAQ;AACrD,QAAI,OAAO,SAAS,EAAG,OAAM,cAAc,MAAM;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,SAAkB,QAAgB,UAA4B;AACzE,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,SAAS,KAAK,SAAS,aAAa,QAAQ;AAClD,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,UAAU,mBAAmB,SAAS,MAAM;AAClD,SAAK,MAAM,QAAQ;AAAA,MACjB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,SAAkB,SAAmC;AAChE,WAAO,KAAK,cAAc,OAAO,EAAE,QAAQ,OAAO;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,QAAgB;AAAE,WAAO,KAAK;AAAA,EAAO;AAAA,EACzC,IAAI,mBAA2B;AAAE,WAAO,KAAK,SAAS;AAAA,EAAW;AAAA,EAEjE,KAAK,QAAsB;AACzB,UAAM,MAAM,IAAI,YAAY,CAAC;AAC7B,QAAI,SAAS,GAAG,EAAE,WAAW,GAAG,YAAY,IAAI,GAAG,IAAI;AACvD,SAAK,MAAM,QAAQ;AAAA,MACjB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACd,eAAW,SAAS,KAAK,QAAQ,OAAO,EAAG,OAAM,QAAQ;AACzD,SAAK,QAAQ,MAAM;AACnB,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAW,KAAkB,MAAoB;AACvD,QAAI;AACJ,QAAI;AAAE,YAAM,cAAc,GAAG;AAAA,IAAE,QAAQ;AAAE;AAAA,IAAO;AAChD,SAAK,OAAO,IAAI;AAEhB,YAAQ,IAAI,MAAM;AAAA,MAChB,wBAA4B;AAC1B,aAAK,aAAa,KAAK,IAAI;AAC3B;AAAA,MACF;AAAA,MACA,0BAA8B;AAC5B,aAAK,eAAe,KAAK,IAAI;AAC7B;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA,wBAA4B;AAE1B,mBAAW,SAAS,KAAK,QAAQ,OAAO,GAAG;AACzC,cAAI,MAAM,QAAQ,IAAI,IAAI,EAAG,OAAM,cAAc,KAAK,IAAI;AAAA,QAC5D;AACA;AAAA,MACF;AAAA,MACA,yBAA4B;AAC1B,mBAAW,KAAK,KAAK,mBAAoB,GAAE,IAAI,SAAS,IAAI;AAC5D;AAAA,MACF;AAAA,MACA,mBAAuB;AACrB,aAAK,MAAM,MAAM;AAAA,UACf;AAAA,UACA;AAAA,UACA,KAAK,KAAK,SAAS;AAAA,UACnB,KAAK,KAAK;AAAA,UACV,SAAS,IAAI;AAAA,QACf,CAAC;AACD;AAAA,MACF;AAAA,MACA,mBAAuB;AACrB,YAAI;AACF,gBAAM,OAAO,IAAI,SAAS,IAAI,OAAO,EAAE,WAAW,GAAG,IAAI;AACzD,gBAAM,MAAM,YAAY,IAAI,IAAI;AAChC,eAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,KAAK;AAAA,QACrD,QAAQ;AAAA,QAAkB;AAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,KAAiB,MAAoB;AACxD,QAAI;AACF,YAAM,EAAE,QAAQ,IAAI,WAAW,IAAI,OAAO;AAC1C,UAAI,OAAO,YAAY,SAAU,MAAK,QAAQ,IAAI,OAAO,GAAG,QAAQ,IAAI;AAAA,IAC1E,QAAQ;AAAA,IAAkB;AAAA,EAC5B;AAAA,EAEQ,eAAe,KAAiB,MAAoB;AAC1D,QAAI;AACF,YAAM,EAAE,QAAQ,IAAI,WAAW,IAAI,OAAO;AAC1C,UAAI,OAAO,YAAY,SAAU,MAAK,QAAQ,IAAI,OAAO,GAAG,WAAW,IAAI;AAAA,IAC7E,QAAQ;AAAA,IAAkB;AAAA,EAC5B;AAAA,EAEQ,eAAe,SAAkB,OAAuB;AAC9D,UAAM,UAAU,WAAW,EAAE,QAAQ,CAAC;AACtC,UAAM,MAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF;AACA,eAAW,KAAK,MAAO,MAAK,MAAM,GAAG,GAAG;AAAA,EAC1C;AAAA,EAEQ,iBAAiB,SAAkB,OAAuB;AAChE,UAAM,UAAU,WAAW,EAAE,QAAQ,CAAC;AACtC,UAAM,MAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,KAAK,KAAK,SAAS;AAAA,MACnB,KAAK,KAAK;AAAA,MACV;AAAA,IACF;AACA,eAAW,KAAK,MAAO,MAAK,MAAM,GAAG,GAAG;AAAA,EAC1C;AAAA,EAEQ,MAAM,IAAY,KAAuB;AAC/C,SAAK,MAAM,KAAK,IAAI,cAAc,GAAG,CAAC;AAAA,EACxC;AAAA,EAEQ,WAAmB;AACzB,WAAQ,KAAK,OAAQ,KAAK,OAAO,IAAK;AAAA,EACxC;AAAA,EAEQ,cAAc,SAAiC;AACrD,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO;AACtC,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mBAAmB,OAAO,6CAAwC;AAC9F,WAAO;AAAA,EACT;AACF;;;ACzVO,IAAM,gBAAN,MAAuB;AAAA,EAAvB;AACL,SAAQ,QAAsB,CAAC;AAC/B,SAAQ,OAAO;AAAA;AAAA,EAEf,IAAI,OAAe;AAAE,WAAO,KAAK,MAAM;AAAA,EAAO;AAAA,EAC9C,IAAI,UAAmB;AAAE,WAAO,KAAK,MAAM,WAAW;AAAA,EAAE;AAAA,EAExD,KAAK,OAAU,UAAwB;AACrC,UAAM,QAAoB,EAAE,UAAU,KAAK,KAAK,QAAQ,MAAM;AAC9D,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,UAAU,KAAK,MAAM,SAAS,CAAC;AAAA,EACtC;AAAA,EAEA,MAAqB;AACnB,QAAI,KAAK,MAAM,WAAW,EAAG,QAAO;AACpC,UAAM,MAAM,KAAK,MAAM,CAAC;AACxB,UAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,QAAI,KAAK,MAAM,SAAS,GAAG;AACzB,WAAK,MAAM,CAAC,IAAI;AAChB,WAAK,UAAU,CAAC;AAAA,IAClB;AACA,WAAO,IAAI;AAAA,EACb;AAAA,EAEA,OAAsB;AACpB,WAAO,KAAK,MAAM,CAAC,GAAG;AAAA,EACxB;AAAA,EAEA,QAAc;AACZ,SAAK,QAAQ,CAAC;AAAA,EAChB;AAAA,EAEQ,IAAI,GAAe,GAAwB;AACjD,WAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE;AAAA,EACzE;AAAA,EAEQ,UAAU,GAAiB;AACjC,WAAO,IAAI,GAAG;AACZ,YAAM,SAAU,IAAI,KAAM;AAC1B,UAAI,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,KAAK,MAAM,MAAM,CAAC,GAAG;AAC/C;AAAC,SAAC,KAAK,MAAM,CAAC,GAAG,KAAK,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,MAAM,GAAG,KAAK,MAAM,CAAC,CAAC;AACzE,YAAI;AAAA,MACN,MAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,UAAU,GAAiB;AACjC,UAAM,IAAI,KAAK,MAAM;AACrB,eAAS;AACP,UAAI,MAAM;AACV,YAAM,KAAK,KAAK,KAAK;AACrB,YAAM,IAAI,IAAI;AACd,UAAI,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,EAAG,OAAM;AAC7D,UAAI,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,EAAG,OAAM;AAC7D,UAAI,QAAQ,EAAG;AACd,OAAC,KAAK,MAAM,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,CAAC,CAAC;AACnE,UAAI;AAAA,IACN;AAAA,EACF;AACF;","names":["MessageType","MessageFlag"]}
@@ -0,0 +1,375 @@
1
+ type PeerId = string;
2
+ type GroupId = string;
3
+ type EntityId = number;
4
+ type ComponentId = number;
5
+ type FieldId = number;
6
+ type SequenceNumber = number;
7
+ type NetTypedArray = Float32Array | Float64Array | Int32Array | Uint32Array | Uint16Array | Uint8Array | Int8Array | Int16Array;
8
+ declare const HEADER_SIZE = 8;
9
+ declare const enum MessageType {
10
+ Handshake = 1,
11
+ Subscribe = 2,
12
+ Unsubscribe = 3,
13
+ DeltaUpdate = 4,
14
+ FullSync = 5,
15
+ Ack = 6,
16
+ Ping = 7,
17
+ Pong = 8,
18
+ LeadClaim = 9,
19
+ LeadYield = 10,
20
+ PeerList = 11,
21
+ GameEvent = 12
22
+ }
23
+ declare const enum MessageFlag {
24
+ None = 0,
25
+ Reliable = 1,
26
+ Compressed = 2,
27
+ Fragmented = 4
28
+ }
29
+ interface NetMessage {
30
+ type: MessageType;
31
+ flags: MessageFlag;
32
+ seq: SequenceNumber;
33
+ ack: SequenceNumber;
34
+ payload: ArrayBuffer;
35
+ }
36
+ interface FieldSchema {
37
+ id: FieldId;
38
+ name: string;
39
+ /** Direct reference to the bitecs component TypedArray (e.g. Position.x) */
40
+ array: NetTypedArray;
41
+ }
42
+ interface ComponentSchema {
43
+ id: ComponentId;
44
+ name: string;
45
+ fields: FieldSchema[];
46
+ }
47
+ interface InterestGroupConfig {
48
+ id: GroupId;
49
+ /** Max entities tracked by this group. Default 4096. */
50
+ maxEntities?: number;
51
+ /** Flush interval in ms. Default 50 (20hz). */
52
+ tickRateMs?: number;
53
+ /** Lead election strategy. Default 'min-id'. */
54
+ electionStrategy?: ElectionStrategy;
55
+ }
56
+ interface PeerNetConfig {
57
+ peerId: PeerId;
58
+ /** Exponential moving average alpha for RTT. Default 0.125. */
59
+ rttAlpha?: number;
60
+ }
61
+ interface DeltaEntry {
62
+ entityId: EntityId;
63
+ componentId: ComponentId;
64
+ fieldId: FieldId;
65
+ value: number;
66
+ }
67
+ type DeltaSet = DeltaEntry[];
68
+ type DeltaHandler = (deltas: DeltaSet, groupId: GroupId, fromPeer: PeerId) => void;
69
+ type ElectionStrategy = 'min-id' | 'hash-ring' | 'load-balanced';
70
+ interface LoadInfo {
71
+ peerId: PeerId;
72
+ connectionCount: number;
73
+ leadGroupCount: number;
74
+ }
75
+
76
+ interface PeerJSDataConnection {
77
+ readonly peer: string;
78
+ readonly open: boolean;
79
+ send(data: ArrayBuffer | Uint8Array): void;
80
+ close(): void;
81
+ on(event: 'data', cb: (data: unknown) => void): void;
82
+ on(event: 'open', cb: () => void): void;
83
+ on(event: 'close', cb: () => void): void;
84
+ on(event: 'error', cb: (err: Error) => void): void;
85
+ }
86
+ interface PeerJSPeer {
87
+ readonly id: string;
88
+ connect(peerId: string, options?: {
89
+ reliable?: boolean;
90
+ serialization?: string;
91
+ }): PeerJSDataConnection;
92
+ on(event: 'connection', cb: (conn: PeerJSDataConnection) => void): void;
93
+ on(event: 'open', cb: (id: string) => void): void;
94
+ on(event: 'error', cb: (err: Error) => void): void;
95
+ on(event: 'close', cb: () => void): void;
96
+ destroy(): void;
97
+ }
98
+ type DataHandler = (data: ArrayBuffer, from: PeerId) => void;
99
+ type DisconnectHandler = (peerId: PeerId) => void;
100
+ /**
101
+ * Manages the lifecycle of PeerJS data connections.
102
+ * Provides:
103
+ * - Lazy connection (auto-connects on first send)
104
+ * - Per-connection message queue drained on open
105
+ * - Exponential-backoff reconnect on close/error
106
+ * - Deduplication of inbound vs outbound connections to the same peer
107
+ */
108
+ declare class ConnectionPool {
109
+ private _peer;
110
+ private _conns;
111
+ private _handlers;
112
+ private _disconnectHandlers;
113
+ private _reconnectDelay;
114
+ private _reconnectHandles;
115
+ private static readonly BASE_DELAY;
116
+ private static readonly MAX_DELAY;
117
+ setPeer(peer: PeerJSPeer): void;
118
+ connect(peerId: PeerId): Promise<void>;
119
+ send(peerId: PeerId, data: ArrayBuffer): void;
120
+ isOpen(peerId: PeerId): boolean;
121
+ disconnect(peerId: PeerId): void;
122
+ onData(handler: DataHandler): () => void;
123
+ /** Fires when a connection closes unexpectedly (not from a local disconnect() call). */
124
+ onDisconnect(handler: DisconnectHandler): () => void;
125
+ dispose(): void;
126
+ private _registerConn;
127
+ private _scheduleReconnect;
128
+ }
129
+
130
+ interface PeerState {
131
+ readonly peerId: PeerId;
132
+ isLead: boolean;
133
+ joinedAt: number;
134
+ lastSeenAt: number;
135
+ rttMs: number;
136
+ }
137
+ type SendFn = (to: PeerId, msg: NetMessage) => void;
138
+ /**
139
+ * Manages one interest group: membership, lead election, and message routing.
140
+ *
141
+ * Routing rules:
142
+ * - Non-lead peers send outbound deltas → lead only
143
+ * - Lead receives deltas → re-broadcasts to all other group peers + notifies local handlers
144
+ * - Local delta handlers always fire regardless of lead status
145
+ *
146
+ * Lead election is deterministic (all peers run the same algorithm) and
147
+ * re-runs on every membership change. No election messages are needed for
148
+ * convergence; LeadClaim messages just inform remote peers of the local result.
149
+ */
150
+ declare class InterestGroup {
151
+ readonly id: GroupId;
152
+ private _config;
153
+ private _localPeerId;
154
+ private _peers;
155
+ private _leadId;
156
+ private _elector;
157
+ private _sendFn;
158
+ private _deltaHandlers;
159
+ private _pending;
160
+ private _tickHandle;
161
+ private _seq;
162
+ private _ack;
163
+ constructor(config: InterestGroupConfig, localPeerId: PeerId);
164
+ get leadId(): PeerId | null;
165
+ get isLead(): boolean;
166
+ get peerCount(): number;
167
+ getPeer(peerId: PeerId): Readonly<PeerState> | undefined;
168
+ get peerIds(): ReadonlySet<PeerId>;
169
+ setSendFn(fn: SendFn): void;
170
+ addPeer(peerId: PeerId): void;
171
+ removePeer(peerId: PeerId): void;
172
+ /** Queue deltas to be sent on the next tick flush */
173
+ publishDeltas(deltas: DeltaSet): void;
174
+ /** Send all pending deltas immediately (called by the tick interval) */
175
+ flush(): void;
176
+ handleMessage(msg: NetMessage, fromPeer: PeerId): void;
177
+ onDelta(handler: DeltaHandler): () => void;
178
+ startTick(): void;
179
+ stopTick(): void;
180
+ dispose(): void;
181
+ private _reelect;
182
+ private _broadcastLeadClaim;
183
+ private _onDeltaUpdate;
184
+ private _onFullSync;
185
+ private _onLeadClaim;
186
+ private _nextSeq;
187
+ }
188
+
189
+ /**
190
+ * PeerNet — top-level peer-to-peer netcode facade.
191
+ *
192
+ * Responsibilities:
193
+ * - Owns the ConnectionPool (PeerJS connections)
194
+ * - Owns all InterestGroups (pub/sub routing)
195
+ * - Owns the ChangeTracker (minimal delta generation)
196
+ * - Routes raw inbound buffers to the correct group
197
+ * - RTT estimation via Ping/Pong
198
+ *
199
+ * Typical usage per game tick:
200
+ * 1. ECS systems run; call net.markEntityDirty(eid) for any changed entity
201
+ * 2. Call net.flushGroupDeltas(groupId, entityList) to compute+queue deltas
202
+ * 3. InterestGroup tick fires automatically (setInterval) and sends queued data
203
+ *
204
+ * Incoming deltas from remote peers surface via net.onGroupDelta(groupId, handler).
205
+ * Apply them to your ECS TypedArrays inside that handler.
206
+ */
207
+ declare class PeerNet {
208
+ readonly localPeerId: PeerId;
209
+ private _config;
210
+ private _pool;
211
+ private _groups;
212
+ private _tracker;
213
+ private _seq;
214
+ private _ack;
215
+ private _rttMs;
216
+ private _gameEventHandlers;
217
+ constructor(config: PeerNetConfig);
218
+ /** Attach the live PeerJS Peer instance after it opens. */
219
+ attachPeer(peer: PeerJSPeer): void;
220
+ registerComponent(schema: ComponentSchema): void;
221
+ unregisterComponent(componentId: number): void;
222
+ createGroup(config: InterestGroupConfig): InterestGroup;
223
+ destroyGroup(groupId: GroupId): void;
224
+ getGroup(groupId: GroupId): InterestGroup | undefined;
225
+ connectPeer(peerId: PeerId): Promise<void>;
226
+ disconnectPeer(peerId: PeerId): void;
227
+ onPeerDisconnect(handler: (peerId: PeerId) => void): () => void;
228
+ sendGameEvent(peerId: PeerId, payload: ArrayBuffer): void;
229
+ onGameEvent(handler: (payload: ArrayBuffer, from: PeerId) => void): () => void;
230
+ /**
231
+ * Subscribe the local peer to a group and introduce remote peers.
232
+ * Sends a Subscribe control message to each remote peer.
233
+ */
234
+ joinGroup(groupId: GroupId, remotePeers: PeerId[]): void;
235
+ /**
236
+ * Leave a group: notify remote peers and remove local state.
237
+ */
238
+ leaveGroup(groupId: GroupId): void;
239
+ markEntityDirty(entityId: EntityId): void;
240
+ markEntitiesDirty(entities: EntityId[]): void;
241
+ invalidateEntity(entityId: EntityId): void;
242
+ /**
243
+ * Compute deltas for dirty entities and push them into the group's send queue.
244
+ * The group's tick interval handles the actual sending.
245
+ *
246
+ * Call once per game tick, after ECS systems run.
247
+ */
248
+ flushGroupDeltas(groupId: GroupId, entities: EntityId[]): void;
249
+ /**
250
+ * Send a full-state sync to a newly joined peer for a given group.
251
+ * Generates a snapshot of all registered component fields for all provided entities.
252
+ */
253
+ sendFullSync(groupId: GroupId, toPeer: PeerId, entities: EntityId[]): void;
254
+ onGroupDelta(groupId: GroupId, handler: DeltaHandler): () => void;
255
+ get rttMs(): number;
256
+ get dirtyEntityCount(): number;
257
+ ping(peerId: PeerId): void;
258
+ dispose(): void;
259
+ private _onRawData;
260
+ private _onSubscribe;
261
+ private _onUnsubscribe;
262
+ private _sendSubscribe;
263
+ private _sendUnsubscribe;
264
+ private _send;
265
+ private _nextSeq;
266
+ private _requireGroup;
267
+ }
268
+
269
+ /**
270
+ * Tracks the last-flushed values of registered ECS components and produces
271
+ * minimal delta sets by comparing current TypedArray state to the snapshot.
272
+ *
273
+ * Thread of ownership: single-threaded (JS). Call markDirty() whenever an
274
+ * ECS system modifies a component, then flush() once per network tick to
275
+ * collect only what changed.
276
+ */
277
+ declare class ChangeTracker {
278
+ private _schemas;
279
+ private _snapshots;
280
+ private _dirtySet;
281
+ registerComponent(schema: ComponentSchema): void;
282
+ unregisterComponent(componentId: ComponentId): void;
283
+ markDirty(entityId: EntityId): void;
284
+ markDirtyBatch(entities: EntityId[]): void;
285
+ get dirtyCount(): number;
286
+ /**
287
+ * Compute deltas for a subset of entities and update the snapshot.
288
+ * Only entities that were marked dirty are compared; clean entities are skipped.
289
+ * Clears dirty flags for all entities in the provided list.
290
+ */
291
+ flush(entities: EntityId[]): DeltaSet;
292
+ /**
293
+ * Force-flush all registered fields for the given entities, ignoring dirty state.
294
+ * Use when a new peer joins and needs a full state sync.
295
+ */
296
+ fullSnapshot(entities: EntityId[]): DeltaSet;
297
+ /**
298
+ * Invalidate the snapshot for an entity (e.g. after it is destroyed and respawned).
299
+ * Next flush will treat all fields as changed.
300
+ */
301
+ invalidateEntity(entityId: EntityId): void;
302
+ clearDirtyAll(): void;
303
+ }
304
+
305
+ /**
306
+ * Deterministic lead election for an interest group.
307
+ *
308
+ * All peers in a group run the same algorithm over the same candidate list,
309
+ * so they converge on the same lead without coordination messages. When the
310
+ * candidate set changes (peer joins/leaves), every peer re-elects locally.
311
+ *
312
+ * Strategies:
313
+ * min-id — lexicographically smallest peer ID wins. Zero coordination
314
+ * overhead, deterministic, but always assigns load to the
315
+ * same peer. Best for low-churn groups.
316
+ *
317
+ * hash-ring — consistent hashing with 20 virtual nodes per peer.
318
+ * Spreads lead responsibility across peers as groups multiply.
319
+ * A single peer join/leave only re-assigns ~1/N groups.
320
+ *
321
+ * load-balanced — weighted score from live load metrics sent by peers.
322
+ * Falls back to min-id for peers without reported metrics.
323
+ * Best when peers have heterogeneous capacity.
324
+ */
325
+ declare class LeadElector {
326
+ private _strategy;
327
+ private _loadInfo;
328
+ constructor(strategy?: ElectionStrategy);
329
+ updateLoadInfo(info: LoadInfo): void;
330
+ removeLoadInfo(peerId: PeerId): void;
331
+ electLead(groupId: GroupId, candidates: PeerId[]): PeerId;
332
+ wouldReelect(groupId: GroupId, currentLead: PeerId, candidates: PeerId[]): boolean;
333
+ private _minId;
334
+ private _hashRing;
335
+ private _loadBalanced;
336
+ }
337
+
338
+ declare function encodeHeader(view: DataView, type: MessageType, flags: MessageFlag, seq: number, ack: number, payloadLen: number): void;
339
+ declare function decodeHeader(view: DataView): {
340
+ type: MessageType;
341
+ flags: MessageFlag;
342
+ seq: number;
343
+ ack: number;
344
+ payloadLen: number;
345
+ };
346
+ declare function encodeMessage(msg: NetMessage): ArrayBuffer;
347
+ declare function decodeMessage(buf: ArrayBuffer): NetMessage;
348
+ declare function encodeDeltaPayload(groupId: string, deltas: DeltaSet): ArrayBuffer;
349
+ declare function decodeDeltaPayload(buf: ArrayBuffer): {
350
+ groupId: string;
351
+ deltas: DeltaSet;
352
+ };
353
+ declare function encodeJson(obj: Record<string, unknown>): ArrayBuffer;
354
+ declare function decodeJson(buf: ArrayBuffer): Record<string, unknown>;
355
+
356
+ interface PQEntry<T> {
357
+ readonly priority: number;
358
+ readonly seq: number;
359
+ readonly value: T;
360
+ }
361
+ declare class PriorityQueue<T> {
362
+ private _heap;
363
+ private _seq;
364
+ get size(): number;
365
+ get isEmpty(): boolean;
366
+ push(value: T, priority: number): void;
367
+ pop(): T | undefined;
368
+ peek(): T | undefined;
369
+ clear(): void;
370
+ private _lt;
371
+ private _bubbleUp;
372
+ private _siftDown;
373
+ }
374
+
375
+ export { ChangeTracker, type ComponentId, type ComponentSchema, ConnectionPool, type DeltaEntry, type DeltaHandler, type DeltaSet, type ElectionStrategy, type EntityId, type FieldId, type FieldSchema, type GroupId, HEADER_SIZE, InterestGroup, type InterestGroupConfig, LeadElector, type LoadInfo, MessageFlag, MessageType, type NetMessage, type NetTypedArray, type PQEntry, type PeerId, type PeerJSDataConnection, type PeerJSPeer, PeerNet, type PeerNetConfig, type PeerState, PriorityQueue, type SendFn, type SequenceNumber, decodeDeltaPayload, decodeHeader, decodeJson, decodeMessage, encodeDeltaPayload, encodeHeader, encodeJson, encodeMessage };