@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.
- package/dist/index.cjs +1072 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +375 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +1030 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var HEADER_SIZE = 8;
|
|
3
|
+
var MessageType = /* @__PURE__ */ ((MessageType3) => {
|
|
4
|
+
MessageType3[MessageType3["Handshake"] = 1] = "Handshake";
|
|
5
|
+
MessageType3[MessageType3["Subscribe"] = 2] = "Subscribe";
|
|
6
|
+
MessageType3[MessageType3["Unsubscribe"] = 3] = "Unsubscribe";
|
|
7
|
+
MessageType3[MessageType3["DeltaUpdate"] = 4] = "DeltaUpdate";
|
|
8
|
+
MessageType3[MessageType3["FullSync"] = 5] = "FullSync";
|
|
9
|
+
MessageType3[MessageType3["Ack"] = 6] = "Ack";
|
|
10
|
+
MessageType3[MessageType3["Ping"] = 7] = "Ping";
|
|
11
|
+
MessageType3[MessageType3["Pong"] = 8] = "Pong";
|
|
12
|
+
MessageType3[MessageType3["LeadClaim"] = 9] = "LeadClaim";
|
|
13
|
+
MessageType3[MessageType3["LeadYield"] = 10] = "LeadYield";
|
|
14
|
+
MessageType3[MessageType3["PeerList"] = 11] = "PeerList";
|
|
15
|
+
MessageType3[MessageType3["GameEvent"] = 12] = "GameEvent";
|
|
16
|
+
return MessageType3;
|
|
17
|
+
})(MessageType || {});
|
|
18
|
+
var MessageFlag = /* @__PURE__ */ ((MessageFlag3) => {
|
|
19
|
+
MessageFlag3[MessageFlag3["None"] = 0] = "None";
|
|
20
|
+
MessageFlag3[MessageFlag3["Reliable"] = 1] = "Reliable";
|
|
21
|
+
MessageFlag3[MessageFlag3["Compressed"] = 2] = "Compressed";
|
|
22
|
+
MessageFlag3[MessageFlag3["Fragmented"] = 4] = "Fragmented";
|
|
23
|
+
return MessageFlag3;
|
|
24
|
+
})(MessageFlag || {});
|
|
25
|
+
|
|
26
|
+
// src/change-tracker.ts
|
|
27
|
+
var ChangeTracker = class {
|
|
28
|
+
constructor() {
|
|
29
|
+
this._schemas = /* @__PURE__ */ new Map();
|
|
30
|
+
// Flat snapshot per entity (reused across components)
|
|
31
|
+
this._snapshots = /* @__PURE__ */ new Map();
|
|
32
|
+
this._dirtySet = /* @__PURE__ */ new Set();
|
|
33
|
+
}
|
|
34
|
+
registerComponent(schema) {
|
|
35
|
+
if (this._schemas.has(schema.id)) {
|
|
36
|
+
throw new Error(`Component id ${schema.id} already registered`);
|
|
37
|
+
}
|
|
38
|
+
this._schemas.set(schema.id, schema);
|
|
39
|
+
}
|
|
40
|
+
unregisterComponent(componentId) {
|
|
41
|
+
this._schemas.delete(componentId);
|
|
42
|
+
for (const snap of this._snapshots.values()) {
|
|
43
|
+
snap.delete(componentId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
markDirty(entityId) {
|
|
47
|
+
this._dirtySet.add(entityId);
|
|
48
|
+
}
|
|
49
|
+
markDirtyBatch(entities) {
|
|
50
|
+
for (const eid of entities) this._dirtySet.add(eid);
|
|
51
|
+
}
|
|
52
|
+
get dirtyCount() {
|
|
53
|
+
return this._dirtySet.size;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute deltas for a subset of entities and update the snapshot.
|
|
57
|
+
* Only entities that were marked dirty are compared; clean entities are skipped.
|
|
58
|
+
* Clears dirty flags for all entities in the provided list.
|
|
59
|
+
*/
|
|
60
|
+
flush(entities) {
|
|
61
|
+
const deltas = [];
|
|
62
|
+
for (const eid of entities) {
|
|
63
|
+
if (!this._dirtySet.has(eid)) continue;
|
|
64
|
+
let entitySnap = this._snapshots.get(eid);
|
|
65
|
+
if (!entitySnap) {
|
|
66
|
+
entitySnap = /* @__PURE__ */ new Map();
|
|
67
|
+
this._snapshots.set(eid, entitySnap);
|
|
68
|
+
}
|
|
69
|
+
for (const [cid, schema] of this._schemas) {
|
|
70
|
+
const fieldCount = schema.fields.length;
|
|
71
|
+
let compSnap = entitySnap.get(cid);
|
|
72
|
+
if (!compSnap) {
|
|
73
|
+
compSnap = new Float64Array(fieldCount).fill(NaN);
|
|
74
|
+
entitySnap.set(cid, compSnap);
|
|
75
|
+
}
|
|
76
|
+
for (let fi = 0; fi < fieldCount; fi++) {
|
|
77
|
+
const field = schema.fields[fi];
|
|
78
|
+
const cur = field.array[eid];
|
|
79
|
+
if (!Object.is(compSnap[fi], cur)) {
|
|
80
|
+
compSnap[fi] = cur;
|
|
81
|
+
deltas.push({ entityId: eid, componentId: cid, fieldId: fi, value: cur });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this._dirtySet.delete(eid);
|
|
86
|
+
}
|
|
87
|
+
return deltas;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Force-flush all registered fields for the given entities, ignoring dirty state.
|
|
91
|
+
* Use when a new peer joins and needs a full state sync.
|
|
92
|
+
*/
|
|
93
|
+
fullSnapshot(entities) {
|
|
94
|
+
const deltas = [];
|
|
95
|
+
for (const eid of entities) {
|
|
96
|
+
for (const [cid, schema] of this._schemas) {
|
|
97
|
+
for (let fi = 0; fi < schema.fields.length; fi++) {
|
|
98
|
+
deltas.push({
|
|
99
|
+
entityId: eid,
|
|
100
|
+
componentId: cid,
|
|
101
|
+
fieldId: fi,
|
|
102
|
+
value: schema.fields[fi].array[eid]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return deltas;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Invalidate the snapshot for an entity (e.g. after it is destroyed and respawned).
|
|
111
|
+
* Next flush will treat all fields as changed.
|
|
112
|
+
*/
|
|
113
|
+
invalidateEntity(entityId) {
|
|
114
|
+
this._snapshots.delete(entityId);
|
|
115
|
+
this._dirtySet.add(entityId);
|
|
116
|
+
}
|
|
117
|
+
clearDirtyAll() {
|
|
118
|
+
this._dirtySet.clear();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/connection-pool.ts
|
|
123
|
+
var _ConnectionPool = class _ConnectionPool {
|
|
124
|
+
constructor() {
|
|
125
|
+
this._peer = null;
|
|
126
|
+
this._conns = /* @__PURE__ */ new Map();
|
|
127
|
+
this._handlers = /* @__PURE__ */ new Set();
|
|
128
|
+
this._disconnectHandlers = /* @__PURE__ */ new Set();
|
|
129
|
+
// Reconnect state: peerId → current delay ms
|
|
130
|
+
this._reconnectDelay = /* @__PURE__ */ new Map();
|
|
131
|
+
this._reconnectHandles = /* @__PURE__ */ new Map();
|
|
132
|
+
}
|
|
133
|
+
setPeer(peer) {
|
|
134
|
+
this._peer = peer;
|
|
135
|
+
peer.on("connection", (conn) => this._registerConn(conn.peer, conn));
|
|
136
|
+
}
|
|
137
|
+
async connect(peerId) {
|
|
138
|
+
const existing = this._conns.get(peerId);
|
|
139
|
+
if (existing?.state === "open") return;
|
|
140
|
+
if (existing?.state === "pending") return existing.openPromise;
|
|
141
|
+
if (!this._peer) throw new Error("Peer not initialized \u2014 call setPeer() first");
|
|
142
|
+
const conn = this._peer.connect(peerId, { reliable: false, serialization: "binary" });
|
|
143
|
+
this._registerConn(peerId, conn);
|
|
144
|
+
return this._conns.get(peerId).openPromise;
|
|
145
|
+
}
|
|
146
|
+
send(peerId, data) {
|
|
147
|
+
const managed = this._conns.get(peerId);
|
|
148
|
+
if (!managed) {
|
|
149
|
+
void this.connect(peerId).then(() => this.send(peerId, data)).catch(() => {
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (managed.state === "open") {
|
|
154
|
+
managed.conn.send(data);
|
|
155
|
+
} else {
|
|
156
|
+
managed.queue.push(data);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
isOpen(peerId) {
|
|
160
|
+
return this._conns.get(peerId)?.state === "open";
|
|
161
|
+
}
|
|
162
|
+
disconnect(peerId) {
|
|
163
|
+
clearTimeout(this._reconnectHandles.get(peerId));
|
|
164
|
+
this._reconnectHandles.delete(peerId);
|
|
165
|
+
this._reconnectDelay.delete(peerId);
|
|
166
|
+
const managed = this._conns.get(peerId);
|
|
167
|
+
if (managed) {
|
|
168
|
+
managed.conn.close();
|
|
169
|
+
this._conns.delete(peerId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
onData(handler) {
|
|
173
|
+
this._handlers.add(handler);
|
|
174
|
+
return () => this._handlers.delete(handler);
|
|
175
|
+
}
|
|
176
|
+
/** Fires when a connection closes unexpectedly (not from a local disconnect() call). */
|
|
177
|
+
onDisconnect(handler) {
|
|
178
|
+
this._disconnectHandlers.add(handler);
|
|
179
|
+
return () => this._disconnectHandlers.delete(handler);
|
|
180
|
+
}
|
|
181
|
+
dispose() {
|
|
182
|
+
for (const h of this._reconnectHandles.values()) clearTimeout(h);
|
|
183
|
+
this._reconnectHandles.clear();
|
|
184
|
+
for (const m of this._conns.values()) m.conn.close();
|
|
185
|
+
this._conns.clear();
|
|
186
|
+
this._peer?.destroy();
|
|
187
|
+
this._peer = null;
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
_registerConn(peerId, conn) {
|
|
191
|
+
const existing = this._conns.get(peerId);
|
|
192
|
+
if (existing?.state === "open") return;
|
|
193
|
+
let resolve;
|
|
194
|
+
let reject;
|
|
195
|
+
const openPromise = new Promise((res, rej) => {
|
|
196
|
+
resolve = res;
|
|
197
|
+
reject = rej;
|
|
198
|
+
});
|
|
199
|
+
const managed = { conn, state: "pending", queue: [], resolve, reject, openPromise };
|
|
200
|
+
this._conns.set(peerId, managed);
|
|
201
|
+
conn.on("open", () => {
|
|
202
|
+
managed.state = "open";
|
|
203
|
+
this._reconnectDelay.delete(peerId);
|
|
204
|
+
clearTimeout(this._reconnectHandles.get(peerId));
|
|
205
|
+
this._reconnectHandles.delete(peerId);
|
|
206
|
+
for (const buf of managed.queue) conn.send(buf);
|
|
207
|
+
managed.queue = [];
|
|
208
|
+
resolve();
|
|
209
|
+
});
|
|
210
|
+
conn.on("data", (raw) => {
|
|
211
|
+
const buf = raw instanceof ArrayBuffer ? raw : raw instanceof Uint8Array ? raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength) : null;
|
|
212
|
+
if (!buf) return;
|
|
213
|
+
for (const h of this._handlers) h(buf, peerId);
|
|
214
|
+
});
|
|
215
|
+
conn.on("close", () => {
|
|
216
|
+
managed.state = "closed";
|
|
217
|
+
for (const h of this._disconnectHandlers) h(peerId);
|
|
218
|
+
this._scheduleReconnect(peerId);
|
|
219
|
+
});
|
|
220
|
+
conn.on("error", (err) => {
|
|
221
|
+
managed.state = "error";
|
|
222
|
+
reject(err);
|
|
223
|
+
this._scheduleReconnect(peerId);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
_scheduleReconnect(peerId) {
|
|
227
|
+
if (!this._peer) return;
|
|
228
|
+
const prev = this._reconnectDelay.get(peerId) ?? _ConnectionPool.BASE_DELAY / 2;
|
|
229
|
+
const delay = Math.min(prev * 2, _ConnectionPool.MAX_DELAY);
|
|
230
|
+
this._reconnectDelay.set(peerId, delay);
|
|
231
|
+
clearTimeout(this._reconnectHandles.get(peerId));
|
|
232
|
+
const handle = setTimeout(() => {
|
|
233
|
+
if (this._conns.get(peerId)?.state !== "open") {
|
|
234
|
+
void this.connect(peerId).catch(() => {
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}, delay);
|
|
238
|
+
this._reconnectHandles.set(peerId, handle);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
_ConnectionPool.BASE_DELAY = 500;
|
|
242
|
+
_ConnectionPool.MAX_DELAY = 3e4;
|
|
243
|
+
var ConnectionPool = _ConnectionPool;
|
|
244
|
+
|
|
245
|
+
// src/message-codec.ts
|
|
246
|
+
var POOL_BUFFER_SIZE = 65536;
|
|
247
|
+
var POOL_MAX = 16;
|
|
248
|
+
var _pool = [];
|
|
249
|
+
function acquireBuffer() {
|
|
250
|
+
return _pool.pop() ?? new ArrayBuffer(POOL_BUFFER_SIZE);
|
|
251
|
+
}
|
|
252
|
+
function releaseBuffer(buf) {
|
|
253
|
+
if (buf.byteLength === POOL_BUFFER_SIZE && _pool.length < POOL_MAX) {
|
|
254
|
+
_pool.push(buf);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function encodeHeader(view, type, flags, seq, ack, payloadLen) {
|
|
258
|
+
view.setUint8(0, type);
|
|
259
|
+
view.setUint8(1, flags);
|
|
260
|
+
view.setUint16(2, seq, true);
|
|
261
|
+
view.setUint16(4, ack, true);
|
|
262
|
+
view.setUint16(6, payloadLen, true);
|
|
263
|
+
}
|
|
264
|
+
function decodeHeader(view) {
|
|
265
|
+
return {
|
|
266
|
+
type: view.getUint8(0),
|
|
267
|
+
flags: view.getUint8(1),
|
|
268
|
+
seq: view.getUint16(2, true),
|
|
269
|
+
ack: view.getUint16(4, true),
|
|
270
|
+
payloadLen: view.getUint16(6, true)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function encodeMessage(msg) {
|
|
274
|
+
const payloadLen = msg.payload.byteLength;
|
|
275
|
+
const total = HEADER_SIZE + payloadLen;
|
|
276
|
+
const buf = total <= POOL_BUFFER_SIZE ? acquireBuffer() : new ArrayBuffer(total);
|
|
277
|
+
encodeHeader(new DataView(buf), msg.type, msg.flags, msg.seq, msg.ack, payloadLen);
|
|
278
|
+
new Uint8Array(buf, HEADER_SIZE, payloadLen).set(new Uint8Array(msg.payload));
|
|
279
|
+
const out = buf.slice(0, total);
|
|
280
|
+
releaseBuffer(buf);
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function decodeMessage(buf) {
|
|
284
|
+
const view = new DataView(buf);
|
|
285
|
+
const { type, flags, seq, ack, payloadLen } = decodeHeader(view);
|
|
286
|
+
return {
|
|
287
|
+
type,
|
|
288
|
+
flags,
|
|
289
|
+
seq,
|
|
290
|
+
ack,
|
|
291
|
+
payload: buf.slice(HEADER_SIZE, HEADER_SIZE + payloadLen)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function encodeDeltaPayload(groupId, deltas) {
|
|
295
|
+
const enc = new TextEncoder();
|
|
296
|
+
const groupIdBytes = enc.encode(groupId);
|
|
297
|
+
const byEntity = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const { entityId, componentId, fieldId, value } of deltas) {
|
|
299
|
+
let byComp = byEntity.get(entityId);
|
|
300
|
+
if (!byComp) {
|
|
301
|
+
byComp = /* @__PURE__ */ new Map();
|
|
302
|
+
byEntity.set(entityId, byComp);
|
|
303
|
+
}
|
|
304
|
+
let fields = byComp.get(componentId);
|
|
305
|
+
if (!fields) {
|
|
306
|
+
fields = [];
|
|
307
|
+
byComp.set(componentId, fields);
|
|
308
|
+
}
|
|
309
|
+
fields.push([fieldId, value]);
|
|
310
|
+
}
|
|
311
|
+
const maxSize = 1 + groupIdBytes.length + 2 + deltas.length * 12;
|
|
312
|
+
const buf = maxSize <= POOL_BUFFER_SIZE ? acquireBuffer() : new ArrayBuffer(maxSize);
|
|
313
|
+
const view = new DataView(buf);
|
|
314
|
+
let off = 0;
|
|
315
|
+
view.setUint8(off, groupIdBytes.length);
|
|
316
|
+
off += 1;
|
|
317
|
+
new Uint8Array(buf, off, groupIdBytes.length).set(groupIdBytes);
|
|
318
|
+
off += groupIdBytes.length;
|
|
319
|
+
view.setUint16(off, byEntity.size, true);
|
|
320
|
+
off += 2;
|
|
321
|
+
for (const [eid, byComp] of byEntity) {
|
|
322
|
+
view.setUint32(off, eid, true);
|
|
323
|
+
off += 4;
|
|
324
|
+
view.setUint8(off, byComp.size);
|
|
325
|
+
off += 1;
|
|
326
|
+
for (const [cid, fields] of byComp) {
|
|
327
|
+
view.setUint8(off, cid);
|
|
328
|
+
off += 1;
|
|
329
|
+
view.setUint8(off, fields.length);
|
|
330
|
+
off += 1;
|
|
331
|
+
for (const [fid, val] of fields) {
|
|
332
|
+
view.setUint8(off, fid);
|
|
333
|
+
off += 1;
|
|
334
|
+
view.setFloat32(off, val, true);
|
|
335
|
+
off += 4;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const out = buf.slice(0, off);
|
|
340
|
+
releaseBuffer(buf);
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function decodeDeltaPayload(buf) {
|
|
344
|
+
const view = new DataView(buf);
|
|
345
|
+
const dec = new TextDecoder();
|
|
346
|
+
let off = 0;
|
|
347
|
+
const groupIdLen = view.getUint8(off);
|
|
348
|
+
off += 1;
|
|
349
|
+
const groupId = dec.decode(buf.slice(off, off + groupIdLen));
|
|
350
|
+
off += groupIdLen;
|
|
351
|
+
const entityCount = view.getUint16(off, true);
|
|
352
|
+
off += 2;
|
|
353
|
+
const deltas = [];
|
|
354
|
+
for (let e = 0; e < entityCount; e++) {
|
|
355
|
+
const entityId = view.getUint32(off, true);
|
|
356
|
+
off += 4;
|
|
357
|
+
const componentCount = view.getUint8(off);
|
|
358
|
+
off += 1;
|
|
359
|
+
for (let c = 0; c < componentCount; c++) {
|
|
360
|
+
const componentId = view.getUint8(off);
|
|
361
|
+
off += 1;
|
|
362
|
+
const fieldCount = view.getUint8(off);
|
|
363
|
+
off += 1;
|
|
364
|
+
for (let f = 0; f < fieldCount; f++) {
|
|
365
|
+
const fieldId = view.getUint8(off);
|
|
366
|
+
off += 1;
|
|
367
|
+
const value = view.getFloat32(off, true);
|
|
368
|
+
off += 4;
|
|
369
|
+
deltas.push({ entityId, componentId, fieldId, value });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { groupId, deltas };
|
|
374
|
+
}
|
|
375
|
+
var _enc = new TextEncoder();
|
|
376
|
+
var _dec = new TextDecoder();
|
|
377
|
+
function encodeJson(obj) {
|
|
378
|
+
return _enc.encode(JSON.stringify(obj)).buffer;
|
|
379
|
+
}
|
|
380
|
+
function decodeJson(buf) {
|
|
381
|
+
return JSON.parse(_dec.decode(buf));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/lead-election.ts
|
|
385
|
+
function fnv1a(s) {
|
|
386
|
+
let h = 2166136261;
|
|
387
|
+
for (let i = 0; i < s.length; i++) {
|
|
388
|
+
h ^= s.charCodeAt(i);
|
|
389
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
390
|
+
}
|
|
391
|
+
return h;
|
|
392
|
+
}
|
|
393
|
+
var LeadElector = class {
|
|
394
|
+
constructor(strategy = "min-id") {
|
|
395
|
+
this._loadInfo = /* @__PURE__ */ new Map();
|
|
396
|
+
this._strategy = strategy;
|
|
397
|
+
}
|
|
398
|
+
updateLoadInfo(info) {
|
|
399
|
+
this._loadInfo.set(info.peerId, info);
|
|
400
|
+
}
|
|
401
|
+
removeLoadInfo(peerId) {
|
|
402
|
+
this._loadInfo.delete(peerId);
|
|
403
|
+
}
|
|
404
|
+
electLead(groupId, candidates) {
|
|
405
|
+
if (candidates.length === 0) throw new Error("No candidates for election");
|
|
406
|
+
if (candidates.length === 1) return candidates[0];
|
|
407
|
+
switch (this._strategy) {
|
|
408
|
+
case "min-id":
|
|
409
|
+
return this._minId(candidates);
|
|
410
|
+
case "hash-ring":
|
|
411
|
+
return this._hashRing(groupId, candidates);
|
|
412
|
+
case "load-balanced":
|
|
413
|
+
return this._loadBalanced(candidates);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
wouldReelect(groupId, currentLead, candidates) {
|
|
417
|
+
try {
|
|
418
|
+
return this.electLead(groupId, candidates) !== currentLead;
|
|
419
|
+
} catch {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
_minId(candidates) {
|
|
424
|
+
return candidates.reduce((best, id) => id < best ? id : best);
|
|
425
|
+
}
|
|
426
|
+
_hashRing(groupId, candidates) {
|
|
427
|
+
const target = fnv1a(groupId);
|
|
428
|
+
const ring = [];
|
|
429
|
+
for (const peerId of candidates) {
|
|
430
|
+
for (let v = 0; v < 20; v++) {
|
|
431
|
+
ring.push({ pos: fnv1a(`${peerId}:vn${v}`), peerId });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
ring.sort((a, b) => a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : 0);
|
|
435
|
+
return (ring.find((n) => n.pos >= target) ?? ring[0]).peerId;
|
|
436
|
+
}
|
|
437
|
+
_loadBalanced(candidates) {
|
|
438
|
+
let best = candidates[0];
|
|
439
|
+
let bestScore = Infinity;
|
|
440
|
+
for (const peerId of candidates) {
|
|
441
|
+
const info = this._loadInfo.get(peerId);
|
|
442
|
+
const score = info ? info.connectionCount * 2 + info.leadGroupCount * 5 : 0;
|
|
443
|
+
if (score < bestScore) {
|
|
444
|
+
bestScore = score;
|
|
445
|
+
best = peerId;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return best;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// src/interest-group.ts
|
|
453
|
+
var InterestGroup = class {
|
|
454
|
+
constructor(config, localPeerId) {
|
|
455
|
+
this._peers = /* @__PURE__ */ new Map();
|
|
456
|
+
this._leadId = null;
|
|
457
|
+
this._sendFn = null;
|
|
458
|
+
this._deltaHandlers = /* @__PURE__ */ new Set();
|
|
459
|
+
this._pending = [];
|
|
460
|
+
this._tickHandle = null;
|
|
461
|
+
this._seq = 0;
|
|
462
|
+
this._ack = 0;
|
|
463
|
+
this.id = config.id;
|
|
464
|
+
this._config = {
|
|
465
|
+
id: config.id,
|
|
466
|
+
maxEntities: config.maxEntities ?? 4096,
|
|
467
|
+
tickRateMs: config.tickRateMs ?? 50,
|
|
468
|
+
electionStrategy: config.electionStrategy ?? "min-id"
|
|
469
|
+
};
|
|
470
|
+
this._localPeerId = localPeerId;
|
|
471
|
+
this._elector = new LeadElector(this._config.electionStrategy);
|
|
472
|
+
}
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Accessors
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
get leadId() {
|
|
477
|
+
return this._leadId;
|
|
478
|
+
}
|
|
479
|
+
get isLead() {
|
|
480
|
+
return this._leadId === this._localPeerId;
|
|
481
|
+
}
|
|
482
|
+
get peerCount() {
|
|
483
|
+
return this._peers.size;
|
|
484
|
+
}
|
|
485
|
+
getPeer(peerId) {
|
|
486
|
+
return this._peers.get(peerId);
|
|
487
|
+
}
|
|
488
|
+
get peerIds() {
|
|
489
|
+
return new Set(this._peers.keys());
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// Wiring
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
setSendFn(fn) {
|
|
495
|
+
this._sendFn = fn;
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Membership
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
addPeer(peerId) {
|
|
501
|
+
if (this._peers.has(peerId)) return;
|
|
502
|
+
this._peers.set(peerId, {
|
|
503
|
+
peerId,
|
|
504
|
+
isLead: false,
|
|
505
|
+
joinedAt: Date.now(),
|
|
506
|
+
lastSeenAt: Date.now(),
|
|
507
|
+
rttMs: 0
|
|
508
|
+
});
|
|
509
|
+
this._reelect();
|
|
510
|
+
}
|
|
511
|
+
removePeer(peerId) {
|
|
512
|
+
if (!this._peers.has(peerId)) return;
|
|
513
|
+
this._peers.delete(peerId);
|
|
514
|
+
this._elector.removeLoadInfo(peerId);
|
|
515
|
+
if (this._leadId === peerId) {
|
|
516
|
+
this._leadId = null;
|
|
517
|
+
this._reelect();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// Delta publishing
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
/** Queue deltas to be sent on the next tick flush */
|
|
524
|
+
publishDeltas(deltas) {
|
|
525
|
+
if (deltas.length > 0) this._pending.push(...deltas);
|
|
526
|
+
}
|
|
527
|
+
/** Send all pending deltas immediately (called by the tick interval) */
|
|
528
|
+
flush() {
|
|
529
|
+
if (this._pending.length === 0 || !this._sendFn) return;
|
|
530
|
+
const payload = encodeDeltaPayload(this.id, this._pending);
|
|
531
|
+
this._pending = [];
|
|
532
|
+
const msg = {
|
|
533
|
+
type: 4 /* DeltaUpdate */,
|
|
534
|
+
flags: 0 /* None */,
|
|
535
|
+
seq: this._nextSeq(),
|
|
536
|
+
ack: this._ack,
|
|
537
|
+
payload
|
|
538
|
+
};
|
|
539
|
+
if (this.isLead) {
|
|
540
|
+
for (const peerId of this._peers.keys()) {
|
|
541
|
+
this._sendFn(peerId, msg);
|
|
542
|
+
}
|
|
543
|
+
} else if (this._leadId !== null) {
|
|
544
|
+
this._sendFn(this._leadId, msg);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// Inbound message dispatch
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
handleMessage(msg, fromPeer) {
|
|
551
|
+
const state = this._peers.get(fromPeer);
|
|
552
|
+
if (state) state.lastSeenAt = Date.now();
|
|
553
|
+
this._ack = msg.seq;
|
|
554
|
+
switch (msg.type) {
|
|
555
|
+
case 4 /* DeltaUpdate */:
|
|
556
|
+
this._onDeltaUpdate(msg, fromPeer);
|
|
557
|
+
break;
|
|
558
|
+
case 5 /* FullSync */:
|
|
559
|
+
this._onFullSync(msg, fromPeer);
|
|
560
|
+
break;
|
|
561
|
+
case 9 /* LeadClaim */:
|
|
562
|
+
this._onLeadClaim(msg);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// Handler registration
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
onDelta(handler) {
|
|
570
|
+
this._deltaHandlers.add(handler);
|
|
571
|
+
return () => this._deltaHandlers.delete(handler);
|
|
572
|
+
}
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Tick control
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
startTick() {
|
|
577
|
+
if (this._tickHandle !== null) return;
|
|
578
|
+
this._tickHandle = setInterval(() => this.flush(), this._config.tickRateMs);
|
|
579
|
+
}
|
|
580
|
+
stopTick() {
|
|
581
|
+
if (this._tickHandle !== null) {
|
|
582
|
+
clearInterval(this._tickHandle);
|
|
583
|
+
this._tickHandle = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
dispose() {
|
|
587
|
+
this.stopTick();
|
|
588
|
+
this._deltaHandlers.clear();
|
|
589
|
+
this._peers.clear();
|
|
590
|
+
}
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// Private
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
_reelect() {
|
|
595
|
+
if (this._peers.size === 0) {
|
|
596
|
+
this._leadId = this._localPeerId;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const candidates = [this._localPeerId, ...this._peers.keys()];
|
|
600
|
+
const newLead = this._elector.electLead(this.id, candidates);
|
|
601
|
+
if (newLead !== this._leadId) {
|
|
602
|
+
this._leadId = newLead;
|
|
603
|
+
if (newLead === this._localPeerId) {
|
|
604
|
+
this._broadcastLeadClaim();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
_broadcastLeadClaim() {
|
|
609
|
+
if (!this._sendFn) return;
|
|
610
|
+
const payload = encodeJson({ groupId: this.id, leadId: this._localPeerId });
|
|
611
|
+
const msg = {
|
|
612
|
+
type: 9 /* LeadClaim */,
|
|
613
|
+
flags: 1 /* Reliable */,
|
|
614
|
+
seq: this._nextSeq(),
|
|
615
|
+
ack: this._ack,
|
|
616
|
+
payload
|
|
617
|
+
};
|
|
618
|
+
for (const peerId of this._peers.keys()) {
|
|
619
|
+
this._sendFn(peerId, msg);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
_onDeltaUpdate(msg, fromPeer) {
|
|
623
|
+
let groupId;
|
|
624
|
+
let deltas;
|
|
625
|
+
try {
|
|
626
|
+
;
|
|
627
|
+
({ groupId, deltas } = decodeDeltaPayload(msg.payload));
|
|
628
|
+
} catch {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (groupId !== this.id) return;
|
|
632
|
+
if (this.isLead && this._sendFn) {
|
|
633
|
+
for (const peerId of this._peers.keys()) {
|
|
634
|
+
if (peerId !== fromPeer) this._sendFn(peerId, msg);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const h of this._deltaHandlers) h(deltas, this.id, fromPeer);
|
|
638
|
+
}
|
|
639
|
+
_onFullSync(msg, fromPeer) {
|
|
640
|
+
let groupId;
|
|
641
|
+
let deltas;
|
|
642
|
+
try {
|
|
643
|
+
;
|
|
644
|
+
({ groupId, deltas } = decodeDeltaPayload(msg.payload));
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (groupId !== this.id) return;
|
|
649
|
+
for (const h of this._deltaHandlers) h(deltas, this.id, fromPeer);
|
|
650
|
+
}
|
|
651
|
+
_onLeadClaim(msg) {
|
|
652
|
+
try {
|
|
653
|
+
const { groupId, leadId } = decodeJson(msg.payload);
|
|
654
|
+
if (groupId !== this.id || typeof leadId !== "string") return;
|
|
655
|
+
if (this._peers.has(leadId) || leadId === this._localPeerId) {
|
|
656
|
+
this._leadId = leadId;
|
|
657
|
+
for (const [pid, state] of this._peers) state.isLead = pid === leadId;
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
_nextSeq() {
|
|
663
|
+
return this._seq = this._seq + 1 & 65535;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// src/peer-net.ts
|
|
668
|
+
var PeerNet = class {
|
|
669
|
+
constructor(config) {
|
|
670
|
+
this._groups = /* @__PURE__ */ new Map();
|
|
671
|
+
this._tracker = new ChangeTracker();
|
|
672
|
+
this._seq = 0;
|
|
673
|
+
this._ack = 0;
|
|
674
|
+
this._rttMs = 50;
|
|
675
|
+
this._gameEventHandlers = [];
|
|
676
|
+
this.localPeerId = config.peerId;
|
|
677
|
+
this._config = {
|
|
678
|
+
peerId: config.peerId,
|
|
679
|
+
rttAlpha: config.rttAlpha ?? 0.125
|
|
680
|
+
};
|
|
681
|
+
this._pool = new ConnectionPool();
|
|
682
|
+
this._pool.onData((buf, from) => this._onRawData(buf, from));
|
|
683
|
+
this._pool.onDisconnect((peerId) => {
|
|
684
|
+
for (const group of this._groups.values()) group.removePeer(peerId);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// ---------------------------------------------------------------------------
|
|
688
|
+
// PeerJS wiring
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
/** Attach the live PeerJS Peer instance after it opens. */
|
|
691
|
+
attachPeer(peer) {
|
|
692
|
+
this._pool.setPeer(peer);
|
|
693
|
+
}
|
|
694
|
+
// ---------------------------------------------------------------------------
|
|
695
|
+
// Component schema
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
registerComponent(schema) {
|
|
698
|
+
this._tracker.registerComponent(schema);
|
|
699
|
+
}
|
|
700
|
+
unregisterComponent(componentId) {
|
|
701
|
+
this._tracker.unregisterComponent(componentId);
|
|
702
|
+
}
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// Group management
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
createGroup(config) {
|
|
707
|
+
const existing = this._groups.get(config.id);
|
|
708
|
+
if (existing) return existing;
|
|
709
|
+
const group = new InterestGroup(config, this.localPeerId);
|
|
710
|
+
group.setSendFn((to, msg) => this._send(to, msg));
|
|
711
|
+
group.startTick();
|
|
712
|
+
this._groups.set(config.id, group);
|
|
713
|
+
return group;
|
|
714
|
+
}
|
|
715
|
+
destroyGroup(groupId) {
|
|
716
|
+
const group = this._groups.get(groupId);
|
|
717
|
+
if (!group) return;
|
|
718
|
+
group.dispose();
|
|
719
|
+
this._groups.delete(groupId);
|
|
720
|
+
}
|
|
721
|
+
getGroup(groupId) {
|
|
722
|
+
return this._groups.get(groupId);
|
|
723
|
+
}
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
// Peer connections
|
|
726
|
+
// ---------------------------------------------------------------------------
|
|
727
|
+
async connectPeer(peerId) {
|
|
728
|
+
await this._pool.connect(peerId);
|
|
729
|
+
}
|
|
730
|
+
disconnectPeer(peerId) {
|
|
731
|
+
this._pool.disconnect(peerId);
|
|
732
|
+
for (const group of this._groups.values()) {
|
|
733
|
+
group.removePeer(peerId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
onPeerDisconnect(handler) {
|
|
737
|
+
return this._pool.onDisconnect(handler);
|
|
738
|
+
}
|
|
739
|
+
sendGameEvent(peerId, payload) {
|
|
740
|
+
this._send(peerId, {
|
|
741
|
+
type: 12 /* GameEvent */,
|
|
742
|
+
flags: 0 /* None */,
|
|
743
|
+
seq: this._nextSeq(),
|
|
744
|
+
ack: this._ack,
|
|
745
|
+
payload
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
onGameEvent(handler) {
|
|
749
|
+
this._gameEventHandlers.push(handler);
|
|
750
|
+
return () => {
|
|
751
|
+
const idx = this._gameEventHandlers.indexOf(handler);
|
|
752
|
+
if (idx !== -1) this._gameEventHandlers.splice(idx, 1);
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
756
|
+
// Group subscriptions
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
/**
|
|
759
|
+
* Subscribe the local peer to a group and introduce remote peers.
|
|
760
|
+
* Sends a Subscribe control message to each remote peer.
|
|
761
|
+
*/
|
|
762
|
+
joinGroup(groupId, remotePeers) {
|
|
763
|
+
const group = this._requireGroup(groupId);
|
|
764
|
+
for (const p of remotePeers) group.addPeer(p);
|
|
765
|
+
this._sendSubscribe(groupId, remotePeers);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Leave a group: notify remote peers and remove local state.
|
|
769
|
+
*/
|
|
770
|
+
leaveGroup(groupId) {
|
|
771
|
+
const group = this._groups.get(groupId);
|
|
772
|
+
if (!group) return;
|
|
773
|
+
const peers = [...group.peerIds];
|
|
774
|
+
this._sendUnsubscribe(groupId, peers);
|
|
775
|
+
for (const p of peers) group.removePeer(p);
|
|
776
|
+
}
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
778
|
+
// Delta pipeline
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
markEntityDirty(entityId) {
|
|
781
|
+
this._tracker.markDirty(entityId);
|
|
782
|
+
}
|
|
783
|
+
markEntitiesDirty(entities) {
|
|
784
|
+
this._tracker.markDirtyBatch(entities);
|
|
785
|
+
}
|
|
786
|
+
invalidateEntity(entityId) {
|
|
787
|
+
this._tracker.invalidateEntity(entityId);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Compute deltas for dirty entities and push them into the group's send queue.
|
|
791
|
+
* The group's tick interval handles the actual sending.
|
|
792
|
+
*
|
|
793
|
+
* Call once per game tick, after ECS systems run.
|
|
794
|
+
*/
|
|
795
|
+
flushGroupDeltas(groupId, entities) {
|
|
796
|
+
const group = this._groups.get(groupId);
|
|
797
|
+
if (!group) return;
|
|
798
|
+
const deltas = this._tracker.flush(entities);
|
|
799
|
+
if (deltas.length > 0) group.publishDeltas(deltas);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Send a full-state sync to a newly joined peer for a given group.
|
|
803
|
+
* Generates a snapshot of all registered component fields for all provided entities.
|
|
804
|
+
*/
|
|
805
|
+
sendFullSync(groupId, toPeer, entities) {
|
|
806
|
+
const group = this._groups.get(groupId);
|
|
807
|
+
if (!group) return;
|
|
808
|
+
const deltas = this._tracker.fullSnapshot(entities);
|
|
809
|
+
if (deltas.length === 0) return;
|
|
810
|
+
const payload = encodeDeltaPayload(groupId, deltas);
|
|
811
|
+
this._send(toPeer, {
|
|
812
|
+
type: 5 /* FullSync */,
|
|
813
|
+
flags: 1 /* Reliable */,
|
|
814
|
+
seq: this._nextSeq(),
|
|
815
|
+
ack: this._ack,
|
|
816
|
+
payload
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
onGroupDelta(groupId, handler) {
|
|
820
|
+
return this._requireGroup(groupId).onDelta(handler);
|
|
821
|
+
}
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
// Diagnostics
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
get rttMs() {
|
|
826
|
+
return this._rttMs;
|
|
827
|
+
}
|
|
828
|
+
get dirtyEntityCount() {
|
|
829
|
+
return this._tracker.dirtyCount;
|
|
830
|
+
}
|
|
831
|
+
ping(peerId) {
|
|
832
|
+
const buf = new ArrayBuffer(8);
|
|
833
|
+
new DataView(buf).setFloat64(0, performance.now(), true);
|
|
834
|
+
this._send(peerId, {
|
|
835
|
+
type: 7 /* Ping */,
|
|
836
|
+
flags: 0 /* None */,
|
|
837
|
+
seq: this._nextSeq(),
|
|
838
|
+
ack: this._ack,
|
|
839
|
+
payload: buf
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
// ---------------------------------------------------------------------------
|
|
843
|
+
// Teardown
|
|
844
|
+
// ---------------------------------------------------------------------------
|
|
845
|
+
dispose() {
|
|
846
|
+
for (const group of this._groups.values()) group.dispose();
|
|
847
|
+
this._groups.clear();
|
|
848
|
+
this._pool.dispose();
|
|
849
|
+
}
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
// Private routing
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
_onRawData(buf, from) {
|
|
854
|
+
let msg;
|
|
855
|
+
try {
|
|
856
|
+
msg = decodeMessage(buf);
|
|
857
|
+
} catch {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
this._ack = msg.seq;
|
|
861
|
+
switch (msg.type) {
|
|
862
|
+
case 2 /* Subscribe */: {
|
|
863
|
+
this._onSubscribe(msg, from);
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
case 3 /* Unsubscribe */: {
|
|
867
|
+
this._onUnsubscribe(msg, from);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
case 4 /* DeltaUpdate */:
|
|
871
|
+
case 5 /* FullSync */:
|
|
872
|
+
case 9 /* LeadClaim */: {
|
|
873
|
+
for (const group of this._groups.values()) {
|
|
874
|
+
if (group.peerIds.has(from)) group.handleMessage(msg, from);
|
|
875
|
+
}
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
case 12 /* GameEvent */: {
|
|
879
|
+
for (const h of this._gameEventHandlers) h(msg.payload, from);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
case 7 /* Ping */: {
|
|
883
|
+
this._send(from, {
|
|
884
|
+
type: 8 /* Pong */,
|
|
885
|
+
flags: 0 /* None */,
|
|
886
|
+
seq: this._nextSeq(),
|
|
887
|
+
ack: this._ack,
|
|
888
|
+
payload: msg.payload
|
|
889
|
+
});
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
case 8 /* Pong */: {
|
|
893
|
+
try {
|
|
894
|
+
const sent = new DataView(msg.payload).getFloat64(0, true);
|
|
895
|
+
const rtt = performance.now() - sent;
|
|
896
|
+
this._rttMs += this._config.rttAlpha * (rtt - this._rttMs);
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
_onSubscribe(msg, from) {
|
|
904
|
+
try {
|
|
905
|
+
const { groupId } = decodeJson(msg.payload);
|
|
906
|
+
if (typeof groupId === "string") this._groups.get(groupId)?.addPeer(from);
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
_onUnsubscribe(msg, from) {
|
|
911
|
+
try {
|
|
912
|
+
const { groupId } = decodeJson(msg.payload);
|
|
913
|
+
if (typeof groupId === "string") this._groups.get(groupId)?.removePeer(from);
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
_sendSubscribe(groupId, peers) {
|
|
918
|
+
const payload = encodeJson({ groupId });
|
|
919
|
+
const msg = {
|
|
920
|
+
type: 2 /* Subscribe */,
|
|
921
|
+
flags: 1 /* Reliable */,
|
|
922
|
+
seq: this._nextSeq(),
|
|
923
|
+
ack: this._ack,
|
|
924
|
+
payload
|
|
925
|
+
};
|
|
926
|
+
for (const p of peers) this._send(p, msg);
|
|
927
|
+
}
|
|
928
|
+
_sendUnsubscribe(groupId, peers) {
|
|
929
|
+
const payload = encodeJson({ groupId });
|
|
930
|
+
const msg = {
|
|
931
|
+
type: 3 /* Unsubscribe */,
|
|
932
|
+
flags: 1 /* Reliable */,
|
|
933
|
+
seq: this._nextSeq(),
|
|
934
|
+
ack: this._ack,
|
|
935
|
+
payload
|
|
936
|
+
};
|
|
937
|
+
for (const p of peers) this._send(p, msg);
|
|
938
|
+
}
|
|
939
|
+
_send(to, msg) {
|
|
940
|
+
this._pool.send(to, encodeMessage(msg));
|
|
941
|
+
}
|
|
942
|
+
_nextSeq() {
|
|
943
|
+
return this._seq = this._seq + 1 & 65535;
|
|
944
|
+
}
|
|
945
|
+
_requireGroup(groupId) {
|
|
946
|
+
const group = this._groups.get(groupId);
|
|
947
|
+
if (!group) throw new Error(`Interest group "${groupId}" not found \u2014 call createGroup() first`);
|
|
948
|
+
return group;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/priority-queue.ts
|
|
953
|
+
var PriorityQueue = class {
|
|
954
|
+
constructor() {
|
|
955
|
+
this._heap = [];
|
|
956
|
+
this._seq = 0;
|
|
957
|
+
}
|
|
958
|
+
get size() {
|
|
959
|
+
return this._heap.length;
|
|
960
|
+
}
|
|
961
|
+
get isEmpty() {
|
|
962
|
+
return this._heap.length === 0;
|
|
963
|
+
}
|
|
964
|
+
push(value, priority) {
|
|
965
|
+
const entry = { priority, seq: this._seq++, value };
|
|
966
|
+
this._heap.push(entry);
|
|
967
|
+
this._bubbleUp(this._heap.length - 1);
|
|
968
|
+
}
|
|
969
|
+
pop() {
|
|
970
|
+
if (this._heap.length === 0) return void 0;
|
|
971
|
+
const top = this._heap[0];
|
|
972
|
+
const last = this._heap.pop();
|
|
973
|
+
if (this._heap.length > 0) {
|
|
974
|
+
this._heap[0] = last;
|
|
975
|
+
this._siftDown(0);
|
|
976
|
+
}
|
|
977
|
+
return top.value;
|
|
978
|
+
}
|
|
979
|
+
peek() {
|
|
980
|
+
return this._heap[0]?.value;
|
|
981
|
+
}
|
|
982
|
+
clear() {
|
|
983
|
+
this._heap = [];
|
|
984
|
+
}
|
|
985
|
+
_lt(a, b) {
|
|
986
|
+
return a.priority !== b.priority ? a.priority < b.priority : a.seq < b.seq;
|
|
987
|
+
}
|
|
988
|
+
_bubbleUp(i) {
|
|
989
|
+
while (i > 0) {
|
|
990
|
+
const parent = i - 1 >> 1;
|
|
991
|
+
if (this._lt(this._heap[i], this._heap[parent])) {
|
|
992
|
+
;
|
|
993
|
+
[this._heap[i], this._heap[parent]] = [this._heap[parent], this._heap[i]];
|
|
994
|
+
i = parent;
|
|
995
|
+
} else break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
_siftDown(i) {
|
|
999
|
+
const n = this._heap.length;
|
|
1000
|
+
for (; ; ) {
|
|
1001
|
+
let min = i;
|
|
1002
|
+
const l = (i << 1) + 1;
|
|
1003
|
+
const r = l + 1;
|
|
1004
|
+
if (l < n && this._lt(this._heap[l], this._heap[min])) min = l;
|
|
1005
|
+
if (r < n && this._lt(this._heap[r], this._heap[min])) min = r;
|
|
1006
|
+
if (min === i) break;
|
|
1007
|
+
[this._heap[i], this._heap[min]] = [this._heap[min], this._heap[i]];
|
|
1008
|
+
i = min;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
export {
|
|
1013
|
+
ChangeTracker,
|
|
1014
|
+
HEADER_SIZE,
|
|
1015
|
+
InterestGroup,
|
|
1016
|
+
LeadElector,
|
|
1017
|
+
MessageFlag,
|
|
1018
|
+
MessageType,
|
|
1019
|
+
PeerNet,
|
|
1020
|
+
PriorityQueue,
|
|
1021
|
+
decodeDeltaPayload,
|
|
1022
|
+
decodeHeader,
|
|
1023
|
+
decodeJson,
|
|
1024
|
+
decodeMessage,
|
|
1025
|
+
encodeDeltaPayload,
|
|
1026
|
+
encodeHeader,
|
|
1027
|
+
encodeJson,
|
|
1028
|
+
encodeMessage
|
|
1029
|
+
};
|
|
1030
|
+
//# sourceMappingURL=index.js.map
|