@peterddod/phop 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/README.md +84 -0
- package/dist/index.d.mts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +506 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +474 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Room: () => Room,
|
|
24
|
+
RoomContext: () => RoomContext,
|
|
25
|
+
createLamportStrategy: () => createLamportStrategy,
|
|
26
|
+
createLastWriteWinsStrategy: () => createLastWriteWinsStrategy,
|
|
27
|
+
useRoom: () => useRoom,
|
|
28
|
+
useSharedState: () => useSharedState
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/context/Room.tsx
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
|
|
35
|
+
// src/core/PeerConnection.ts
|
|
36
|
+
var _PeerConnection = class _PeerConnection {
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.dataChannel = null;
|
|
39
|
+
this.selfPeerId = opts.localPeerId;
|
|
40
|
+
this.remotePeerId = opts.remotePeerId;
|
|
41
|
+
this.signalingClient = opts.signalingClient;
|
|
42
|
+
this.onChannelMessage = opts.onChannelMessage;
|
|
43
|
+
this.onChannelOpen = opts.onChannelOpen;
|
|
44
|
+
this.pc = new RTCPeerConnection(
|
|
45
|
+
opts.rtcConfig ?? { iceServers: _PeerConnection.DEFAULT_ICE_SERVERS }
|
|
46
|
+
);
|
|
47
|
+
this.setupConnection();
|
|
48
|
+
if (this.initiator) {
|
|
49
|
+
this.initiate();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
get initiator() {
|
|
53
|
+
return this.selfPeerId > this.remotePeerId;
|
|
54
|
+
}
|
|
55
|
+
setupConnection() {
|
|
56
|
+
this.pc.onicecandidate = (event) => {
|
|
57
|
+
if (event.candidate) {
|
|
58
|
+
this.signalingClient.send({
|
|
59
|
+
type: "signal",
|
|
60
|
+
to: this.remotePeerId,
|
|
61
|
+
data: {
|
|
62
|
+
type: "ice-candidate",
|
|
63
|
+
candidate: event.candidate
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this.pc.ondatachannel = (event) => {
|
|
69
|
+
this.dataChannel = event.channel;
|
|
70
|
+
this.setupDataChannel(event.channel);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async initiate() {
|
|
74
|
+
this.dataChannel = this.pc.createDataChannel("game-sync");
|
|
75
|
+
this.setupDataChannel(this.dataChannel);
|
|
76
|
+
const offer = await this.pc.createOffer();
|
|
77
|
+
await this.pc.setLocalDescription(offer);
|
|
78
|
+
this.signalingClient.send({
|
|
79
|
+
type: "signal",
|
|
80
|
+
to: this.remotePeerId,
|
|
81
|
+
data: {
|
|
82
|
+
type: "offer",
|
|
83
|
+
sdp: offer
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async handleSignal(data) {
|
|
88
|
+
switch (data.type) {
|
|
89
|
+
case "offer": {
|
|
90
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
|
|
91
|
+
const answer = await this.pc.createAnswer();
|
|
92
|
+
await this.pc.setLocalDescription(answer);
|
|
93
|
+
this.signalingClient.send({
|
|
94
|
+
type: "signal",
|
|
95
|
+
to: this.remotePeerId,
|
|
96
|
+
data: {
|
|
97
|
+
type: "answer",
|
|
98
|
+
sdp: answer
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "answer":
|
|
104
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
|
|
105
|
+
break;
|
|
106
|
+
case "ice-candidate":
|
|
107
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
setupDataChannel(channel) {
|
|
112
|
+
channel.onopen = () => {
|
|
113
|
+
console.log(`Data channel open to ${this.remotePeerId}`);
|
|
114
|
+
this.onChannelOpen?.(this.remotePeerId);
|
|
115
|
+
};
|
|
116
|
+
channel.onclose = () => {
|
|
117
|
+
console.log(`Data channel closed to ${this.remotePeerId}`);
|
|
118
|
+
};
|
|
119
|
+
channel.onerror = (error) => {
|
|
120
|
+
console.error(`Data channel error with ${this.remotePeerId}:`, error);
|
|
121
|
+
};
|
|
122
|
+
channel.onmessage = (event) => {
|
|
123
|
+
const message = JSON.parse(event.data);
|
|
124
|
+
this.onChannelMessage?.(this.remotePeerId, message);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
send(message) {
|
|
128
|
+
if (this.dataChannel?.readyState === "open") {
|
|
129
|
+
this.dataChannel.send(JSON.stringify(message));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
close() {
|
|
133
|
+
this.dataChannel?.close();
|
|
134
|
+
this.pc.close();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
_PeerConnection.DEFAULT_ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
138
|
+
var PeerConnection = _PeerConnection;
|
|
139
|
+
|
|
140
|
+
// src/core/SignalingClient.ts
|
|
141
|
+
var SignalingClient = class {
|
|
142
|
+
constructor(serverUrl, roomId) {
|
|
143
|
+
this.serverUrl = serverUrl;
|
|
144
|
+
this.roomId = roomId;
|
|
145
|
+
this.ws = null;
|
|
146
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
147
|
+
this.peerId = "";
|
|
148
|
+
}
|
|
149
|
+
connect() {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this.ws = new WebSocket(this.serverUrl);
|
|
152
|
+
this.ws.onopen = () => {
|
|
153
|
+
this.send({
|
|
154
|
+
type: "join",
|
|
155
|
+
roomId: this.roomId
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
this.ws.onmessage = (event) => {
|
|
159
|
+
const message = JSON.parse(event.data);
|
|
160
|
+
if (message.type === "joined") {
|
|
161
|
+
this.peerId = message.peerId;
|
|
162
|
+
resolve(message.peerId);
|
|
163
|
+
}
|
|
164
|
+
this.emit(message);
|
|
165
|
+
};
|
|
166
|
+
this.ws.onerror = (error) => {
|
|
167
|
+
reject(error);
|
|
168
|
+
};
|
|
169
|
+
this.ws.onclose = () => {
|
|
170
|
+
console.log("Disconnected from signaling server");
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
send(message) {
|
|
175
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
176
|
+
this.ws.send(JSON.stringify(message));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
on(eventType, handler) {
|
|
180
|
+
if (!this.eventHandlers.has(eventType)) {
|
|
181
|
+
this.eventHandlers.set(eventType, []);
|
|
182
|
+
}
|
|
183
|
+
this.eventHandlers.get(eventType)?.push(handler);
|
|
184
|
+
}
|
|
185
|
+
off(eventType, handler) {
|
|
186
|
+
const handlers = this.eventHandlers.get(eventType);
|
|
187
|
+
if (handlers) {
|
|
188
|
+
const index = handlers.indexOf(handler);
|
|
189
|
+
if (index > -1) {
|
|
190
|
+
handlers.splice(index, 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
emit(event) {
|
|
195
|
+
const handlers = this.eventHandlers.get(event.type);
|
|
196
|
+
if (handlers) {
|
|
197
|
+
handlers.forEach((handler) => {
|
|
198
|
+
handler(event);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const wildcardHandlers = this.eventHandlers.get("*");
|
|
202
|
+
if (wildcardHandlers) {
|
|
203
|
+
wildcardHandlers.forEach((handler) => {
|
|
204
|
+
handler(event);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
disconnect() {
|
|
209
|
+
this.ws?.close();
|
|
210
|
+
this.ws = null;
|
|
211
|
+
}
|
|
212
|
+
getPeerId() {
|
|
213
|
+
return this.peerId;
|
|
214
|
+
}
|
|
215
|
+
isConnected() {
|
|
216
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/context/Room.tsx
|
|
221
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
222
|
+
var RoomContext = (0, import_react.createContext)(null);
|
|
223
|
+
function Room({ children, signallingServerUrl, roomId }) {
|
|
224
|
+
const [peerId, setPeerId] = (0, import_react.useState)("");
|
|
225
|
+
const [peers, setPeers] = (0, import_react.useState)([]);
|
|
226
|
+
const [isConnected, setIsConnected] = (0, import_react.useState)(false);
|
|
227
|
+
const signalingClientRef = (0, import_react.useRef)(null);
|
|
228
|
+
const connectionsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
229
|
+
const handlersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
230
|
+
const peerConnectedHandlersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
231
|
+
(0, import_react.useEffect)(
|
|
232
|
+
function initializeSignalingClient() {
|
|
233
|
+
const client = new SignalingClient(signallingServerUrl, roomId);
|
|
234
|
+
signalingClientRef.current = client;
|
|
235
|
+
client.on("joined", (event) => {
|
|
236
|
+
if (event.peerId) {
|
|
237
|
+
setPeerId(event.peerId);
|
|
238
|
+
setIsConnected(true);
|
|
239
|
+
}
|
|
240
|
+
if (event.peers) {
|
|
241
|
+
setPeers(event.peers);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
client.on("peer-list", (event) => {
|
|
245
|
+
if (event.peers) {
|
|
246
|
+
setPeers(event.peers);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
client.on("peer-joined", (event) => {
|
|
250
|
+
if (event.peers) {
|
|
251
|
+
setPeers(event.peers);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
client.on("peer-left", (event) => {
|
|
255
|
+
if (event.peerId) {
|
|
256
|
+
const connection = connectionsRef.current.get(event.peerId);
|
|
257
|
+
if (connection) {
|
|
258
|
+
connection.close();
|
|
259
|
+
connectionsRef.current.delete(event.peerId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (event.peers) {
|
|
263
|
+
setPeers(event.peers);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
client.on("signal", (event) => {
|
|
267
|
+
if (event.from && event.data) {
|
|
268
|
+
const connection = connectionsRef.current.get(event.from);
|
|
269
|
+
connection?.handleSignal(event.data);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
client.on("disconnected", () => {
|
|
273
|
+
setIsConnected(false);
|
|
274
|
+
connectionsRef.current.forEach((conn) => {
|
|
275
|
+
conn.close();
|
|
276
|
+
});
|
|
277
|
+
connectionsRef.current.clear();
|
|
278
|
+
});
|
|
279
|
+
client.connect().catch((error) => {
|
|
280
|
+
console.error("Failed to connect to signaling server:", error);
|
|
281
|
+
setIsConnected(false);
|
|
282
|
+
});
|
|
283
|
+
return () => {
|
|
284
|
+
connectionsRef.current.forEach((conn) => {
|
|
285
|
+
conn.close();
|
|
286
|
+
});
|
|
287
|
+
connectionsRef.current.clear();
|
|
288
|
+
client.disconnect();
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
[signallingServerUrl, roomId]
|
|
292
|
+
);
|
|
293
|
+
(0, import_react.useEffect)(
|
|
294
|
+
function createPeerConnections() {
|
|
295
|
+
if (!peerId || !signalingClientRef.current) return;
|
|
296
|
+
const signalingClient = signalingClientRef.current;
|
|
297
|
+
peers.forEach((remotePeerId) => {
|
|
298
|
+
if (remotePeerId === peerId) return;
|
|
299
|
+
if (connectionsRef.current.has(remotePeerId)) return;
|
|
300
|
+
const connection = new PeerConnection({
|
|
301
|
+
localPeerId: peerId,
|
|
302
|
+
remotePeerId,
|
|
303
|
+
signalingClient,
|
|
304
|
+
onChannelMessage: (fromPeerId, raw) => {
|
|
305
|
+
const message = {
|
|
306
|
+
senderId: fromPeerId,
|
|
307
|
+
data: raw && typeof raw === "object" && "data" in raw && raw.data !== void 0 ? raw.data : raw,
|
|
308
|
+
timestamp: raw && typeof raw === "object" && "timestamp" in raw && typeof raw.timestamp === "number" ? raw.timestamp : Date.now()
|
|
309
|
+
};
|
|
310
|
+
handlersRef.current.forEach((h) => void h(message));
|
|
311
|
+
},
|
|
312
|
+
onChannelOpen: (connectedRemotePeerId) => {
|
|
313
|
+
peerConnectedHandlersRef.current.forEach((h) => void h(connectedRemotePeerId));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
connectionsRef.current.set(remotePeerId, connection);
|
|
317
|
+
});
|
|
318
|
+
connectionsRef.current.forEach((connection, remotePeerId) => {
|
|
319
|
+
if (!peers.includes(remotePeerId)) {
|
|
320
|
+
connection.close();
|
|
321
|
+
connectionsRef.current.delete(remotePeerId);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
[peers, peerId]
|
|
326
|
+
);
|
|
327
|
+
const broadcast = (0, import_react.useCallback)((message) => {
|
|
328
|
+
connectionsRef.current.forEach((connection) => {
|
|
329
|
+
connection.send(message);
|
|
330
|
+
});
|
|
331
|
+
}, []);
|
|
332
|
+
const sendToPeer = (0, import_react.useCallback)(
|
|
333
|
+
(targetPeerId, message) => {
|
|
334
|
+
const connection = connectionsRef.current.get(targetPeerId);
|
|
335
|
+
connection?.send(message);
|
|
336
|
+
},
|
|
337
|
+
[]
|
|
338
|
+
);
|
|
339
|
+
const onMessage = (0, import_react.useCallback)(
|
|
340
|
+
(handler) => {
|
|
341
|
+
handlersRef.current.add(handler);
|
|
342
|
+
return () => {
|
|
343
|
+
handlersRef.current.delete(handler);
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
[]
|
|
347
|
+
);
|
|
348
|
+
const onPeerConnected = (0, import_react.useCallback)((handler) => {
|
|
349
|
+
peerConnectedHandlersRef.current.add(handler);
|
|
350
|
+
return () => {
|
|
351
|
+
peerConnectedHandlersRef.current.delete(handler);
|
|
352
|
+
};
|
|
353
|
+
}, []);
|
|
354
|
+
const contextValue = {
|
|
355
|
+
roomId,
|
|
356
|
+
peerId,
|
|
357
|
+
peers,
|
|
358
|
+
isConnected,
|
|
359
|
+
broadcast,
|
|
360
|
+
sendToPeer,
|
|
361
|
+
onMessage,
|
|
362
|
+
onPeerConnected
|
|
363
|
+
};
|
|
364
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RoomContext.Provider, { value: contextValue, children });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/core/merge-strategies/lamport.ts
|
|
368
|
+
function createLamportStrategy(getPeerId) {
|
|
369
|
+
let localClock = 0;
|
|
370
|
+
return {
|
|
371
|
+
initialMeta: { clock: 0, tiebreaker: getPeerId() },
|
|
372
|
+
createMeta() {
|
|
373
|
+
return { clock: ++localClock, tiebreaker: getPeerId() };
|
|
374
|
+
},
|
|
375
|
+
merge(_currentState, currentMeta, incomingState, incomingMeta, senderId) {
|
|
376
|
+
localClock = Math.max(localClock, incomingMeta.clock);
|
|
377
|
+
const clockWins = incomingMeta.clock > currentMeta.clock;
|
|
378
|
+
const incomingTiebreaker = incomingMeta.tiebreaker || senderId;
|
|
379
|
+
const currentTiebreaker = currentMeta.tiebreaker || getPeerId();
|
|
380
|
+
const tiebreakWins = incomingMeta.clock === currentMeta.clock && incomingTiebreaker > currentTiebreaker;
|
|
381
|
+
return clockWins || tiebreakWins ? { state: incomingState, meta: { ...incomingMeta, tiebreaker: incomingTiebreaker } } : null;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/core/merge-strategies/lastWriteWins.ts
|
|
387
|
+
function createLastWriteWinsStrategy(getPeerId) {
|
|
388
|
+
return {
|
|
389
|
+
initialMeta: { timestamp: 0, tiebreaker: getPeerId() },
|
|
390
|
+
createMeta() {
|
|
391
|
+
return { timestamp: Date.now(), tiebreaker: getPeerId() };
|
|
392
|
+
},
|
|
393
|
+
merge(_currentState, currentMeta, incomingState, incomingMeta, senderId) {
|
|
394
|
+
const incomingTiebreaker = incomingMeta.tiebreaker || senderId;
|
|
395
|
+
const currentTiebreaker = currentMeta.tiebreaker || getPeerId();
|
|
396
|
+
const timestampWins = incomingMeta.timestamp > currentMeta.timestamp;
|
|
397
|
+
const tiebreakWins = incomingMeta.timestamp === currentMeta.timestamp && incomingTiebreaker > currentTiebreaker;
|
|
398
|
+
return timestampWins || tiebreakWins ? { state: incomingState, meta: { ...incomingMeta, tiebreaker: incomingTiebreaker } } : null;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/hooks/useRoom.ts
|
|
404
|
+
var import_react2 = require("react");
|
|
405
|
+
function useRoom() {
|
|
406
|
+
const context = (0, import_react2.useContext)(RoomContext);
|
|
407
|
+
if (!context) {
|
|
408
|
+
throw new Error("useRoom must be used within a Room provider");
|
|
409
|
+
}
|
|
410
|
+
return context;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/hooks/useSharedState.ts
|
|
414
|
+
var import_react3 = require("react");
|
|
415
|
+
function isStateRequest(data) {
|
|
416
|
+
return typeof data === "object" && data !== null && "type" in data && data.type === "state-request";
|
|
417
|
+
}
|
|
418
|
+
function useSharedState(key, initialState, strategy) {
|
|
419
|
+
const { broadcast, onMessage, onPeerConnected, peerId, sendToPeer } = useRoom();
|
|
420
|
+
const peerIdRef = (0, import_react3.useRef)(peerId);
|
|
421
|
+
peerIdRef.current = peerId;
|
|
422
|
+
const lamportStrategyRef = (0, import_react3.useRef)(
|
|
423
|
+
createLamportStrategy(() => peerIdRef.current)
|
|
424
|
+
);
|
|
425
|
+
const effectiveStrategy = strategy !== void 0 ? strategy : lamportStrategyRef.current;
|
|
426
|
+
const [rawState, setRawState] = (0, import_react3.useState)(initialState);
|
|
427
|
+
const rawStateRef = (0, import_react3.useRef)(initialState);
|
|
428
|
+
const rawStateMetaRef = (0, import_react3.useRef)(effectiveStrategy.initialMeta);
|
|
429
|
+
const strategyRef = (0, import_react3.useRef)(effectiveStrategy);
|
|
430
|
+
rawStateRef.current = rawState;
|
|
431
|
+
(0, import_react3.useEffect)(() => {
|
|
432
|
+
strategyRef.current = effectiveStrategy;
|
|
433
|
+
}, [effectiveStrategy]);
|
|
434
|
+
(0, import_react3.useEffect)(
|
|
435
|
+
function handleMessage() {
|
|
436
|
+
const unsubscribe = onMessage(
|
|
437
|
+
({ senderId, data }) => {
|
|
438
|
+
if (senderId === peerId) return;
|
|
439
|
+
if (isStateRequest(data)) {
|
|
440
|
+
if (data.key === key) {
|
|
441
|
+
sendToPeer(senderId, {
|
|
442
|
+
senderId: peerId,
|
|
443
|
+
data: { key, state: rawStateRef.current, meta: rawStateMetaRef.current },
|
|
444
|
+
timestamp: Date.now()
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (data.key !== key) return;
|
|
450
|
+
const result = strategyRef.current.merge(
|
|
451
|
+
rawStateRef.current,
|
|
452
|
+
rawStateMetaRef.current,
|
|
453
|
+
data.state,
|
|
454
|
+
data.meta,
|
|
455
|
+
senderId
|
|
456
|
+
);
|
|
457
|
+
if (result) {
|
|
458
|
+
rawStateRef.current = result.state;
|
|
459
|
+
rawStateMetaRef.current = result.meta;
|
|
460
|
+
setRawState(result.state);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
return unsubscribe;
|
|
465
|
+
},
|
|
466
|
+
[key, onMessage, peerId, sendToPeer]
|
|
467
|
+
);
|
|
468
|
+
(0, import_react3.useEffect)(
|
|
469
|
+
function pushStateOnConnect() {
|
|
470
|
+
const unsubscribe = onPeerConnected((remotePeerId) => {
|
|
471
|
+
sendToPeer(remotePeerId, {
|
|
472
|
+
senderId: peerId,
|
|
473
|
+
data: { key, state: rawStateRef.current, meta: rawStateMetaRef.current },
|
|
474
|
+
timestamp: Date.now()
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
return unsubscribe;
|
|
478
|
+
},
|
|
479
|
+
[key, onPeerConnected, peerId, sendToPeer]
|
|
480
|
+
);
|
|
481
|
+
const setState = (0, import_react3.useCallback)(
|
|
482
|
+
(next) => {
|
|
483
|
+
const meta = strategyRef.current.createMeta();
|
|
484
|
+
rawStateRef.current = next;
|
|
485
|
+
rawStateMetaRef.current = meta;
|
|
486
|
+
setRawState(next);
|
|
487
|
+
broadcast({
|
|
488
|
+
senderId: peerId,
|
|
489
|
+
data: { key, state: next, meta },
|
|
490
|
+
timestamp: Date.now()
|
|
491
|
+
});
|
|
492
|
+
},
|
|
493
|
+
[broadcast, key, peerId]
|
|
494
|
+
);
|
|
495
|
+
return [rawState, setState];
|
|
496
|
+
}
|
|
497
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
498
|
+
0 && (module.exports = {
|
|
499
|
+
Room,
|
|
500
|
+
RoomContext,
|
|
501
|
+
createLamportStrategy,
|
|
502
|
+
createLastWriteWinsStrategy,
|
|
503
|
+
useRoom,
|
|
504
|
+
useSharedState
|
|
505
|
+
});
|
|
506
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context/Room.tsx","../src/core/PeerConnection.ts","../src/core/SignalingClient.ts","../src/core/merge-strategies/lamport.ts","../src/core/merge-strategies/lastWriteWins.ts","../src/hooks/useRoom.ts","../src/hooks/useSharedState.ts"],"sourcesContent":["export { Room, RoomContext, type RoomContextValue } from './context';\nexport {\n createLamportStrategy,\n createLastWriteWinsStrategy,\n type LamportMeta,\n type LastWriteWinsMeta,\n type MergeMeta,\n type MergeStrategy,\n} from './core/merge-strategies';\nexport { useRoom, useSharedState } from './hooks';\nexport type { JSONSerializable, Message, MessageHandler } from './types';\n","import { createContext, useCallback, useEffect, useRef, useState } from 'react';\nimport { PeerConnection } from '../core/PeerConnection';\nimport { SignalingClient } from '../core/SignalingClient';\nimport type { JSONSerializable, Message, MessageHandler } from '../types';\n\nexport interface RoomContextValue {\n roomId: string;\n peerId: string;\n peers: string[];\n isConnected: boolean;\n broadcast: <TData extends JSONSerializable = JSONSerializable>(message: Message<TData>) => void;\n sendToPeer: <TData extends JSONSerializable = JSONSerializable>(\n peerId: string,\n message: Message<TData>\n ) => void;\n onMessage: <TData extends JSONSerializable = JSONSerializable>(\n handler: MessageHandler<TData>\n ) => () => void;\n onPeerConnected: (handler: (remotePeerId: string) => void) => () => void;\n}\n\nexport const RoomContext = createContext<RoomContextValue | null>(null);\n\ninterface RoomProps extends React.PropsWithChildren {\n signallingServerUrl: string;\n roomId: string;\n}\n\nexport function Room({ children, signallingServerUrl, roomId }: RoomProps) {\n const [peerId, setPeerId] = useState<string>('');\n const [peers, setPeers] = useState<string[]>([]);\n const [isConnected, setIsConnected] = useState(false);\n\n const signalingClientRef = useRef<SignalingClient | null>(null);\n const connectionsRef = useRef<Map<string, PeerConnection>>(new Map());\n const handlersRef = useRef<Set<MessageHandler>>(new Set());\n const peerConnectedHandlersRef = useRef<Set<(remotePeerId: string) => void>>(new Set());\n\n useEffect(\n function initializeSignalingClient() {\n const client = new SignalingClient(signallingServerUrl, roomId);\n signalingClientRef.current = client;\n\n client.on('joined', (event) => {\n if (event.peerId) {\n setPeerId(event.peerId);\n setIsConnected(true);\n }\n if (event.peers) {\n setPeers(event.peers);\n }\n });\n\n client.on('peer-list', (event) => {\n if (event.peers) {\n setPeers(event.peers);\n }\n });\n\n client.on('peer-joined', (event) => {\n if (event.peers) {\n setPeers(event.peers);\n }\n });\n\n client.on('peer-left', (event) => {\n if (event.peerId) {\n const connection = connectionsRef.current.get(event.peerId);\n if (connection) {\n connection.close();\n connectionsRef.current.delete(event.peerId);\n }\n }\n if (event.peers) {\n setPeers(event.peers);\n }\n });\n\n client.on('signal', (event) => {\n if (event.from && event.data) {\n const connection = connectionsRef.current.get(event.from);\n connection?.handleSignal(event.data);\n }\n });\n\n client.on('disconnected', () => {\n setIsConnected(false);\n connectionsRef.current.forEach((conn) => {\n conn.close();\n });\n connectionsRef.current.clear();\n });\n\n client.connect().catch((error) => {\n console.error('Failed to connect to signaling server:', error);\n setIsConnected(false);\n });\n\n return () => {\n connectionsRef.current.forEach((conn) => {\n conn.close();\n });\n connectionsRef.current.clear();\n client.disconnect();\n };\n },\n [signallingServerUrl, roomId]\n );\n\n useEffect(\n function createPeerConnections() {\n if (!peerId || !signalingClientRef.current) return;\n\n const signalingClient = signalingClientRef.current;\n\n peers.forEach((remotePeerId) => {\n if (remotePeerId === peerId) return;\n\n if (connectionsRef.current.has(remotePeerId)) return;\n\n const connection = new PeerConnection({\n localPeerId: peerId,\n remotePeerId,\n signalingClient,\n onChannelMessage: (fromPeerId, raw) => {\n const message: Message<JSONSerializable> = {\n senderId: fromPeerId,\n data:\n raw && typeof raw === 'object' && 'data' in raw && raw.data !== undefined\n ? (raw.data as JSONSerializable)\n : (raw as JSONSerializable),\n timestamp:\n raw &&\n typeof raw === 'object' &&\n 'timestamp' in raw &&\n typeof (raw as { timestamp?: number }).timestamp === 'number'\n ? (raw as { timestamp: number }).timestamp\n : Date.now(),\n };\n handlersRef.current.forEach((h) => void h(message));\n },\n onChannelOpen: (connectedRemotePeerId) => {\n peerConnectedHandlersRef.current.forEach((h) => void h(connectedRemotePeerId));\n },\n });\n\n connectionsRef.current.set(remotePeerId, connection);\n });\n\n connectionsRef.current.forEach((connection, remotePeerId) => {\n if (!peers.includes(remotePeerId)) {\n connection.close();\n connectionsRef.current.delete(remotePeerId);\n }\n });\n },\n [peers, peerId]\n );\n\n const broadcast = useCallback(<TData extends JSONSerializable>(message: Message<TData>): void => {\n connectionsRef.current.forEach((connection) => {\n connection.send(message);\n });\n }, []);\n\n const sendToPeer = useCallback(\n <TData extends JSONSerializable>(targetPeerId: string, message: Message<TData>): void => {\n const connection = connectionsRef.current.get(targetPeerId);\n connection?.send(message);\n },\n []\n );\n\n const onMessage = useCallback(\n <TData extends JSONSerializable = JSONSerializable>(\n handler: MessageHandler<TData>\n ): (() => void) => {\n handlersRef.current.add(handler as MessageHandler);\n return () => {\n handlersRef.current.delete(handler as MessageHandler);\n };\n },\n []\n );\n\n const onPeerConnected = useCallback((handler: (remotePeerId: string) => void): (() => void) => {\n peerConnectedHandlersRef.current.add(handler);\n return () => {\n peerConnectedHandlersRef.current.delete(handler);\n };\n }, []);\n\n const contextValue: RoomContextValue = {\n roomId,\n peerId,\n peers,\n isConnected,\n broadcast,\n sendToPeer,\n onMessage,\n onPeerConnected,\n };\n\n return <RoomContext.Provider value={contextValue}>{children}</RoomContext.Provider>;\n}\n","import type { SignalingClient } from './SignalingClient';\n\nexport type SignalData =\n | { type: 'offer'; sdp: RTCSessionDescriptionInit }\n | { type: 'answer'; sdp: RTCSessionDescriptionInit }\n | { type: 'ice-candidate'; candidate: RTCIceCandidateInit };\n\nexport interface PeerConnectionOptions {\n localPeerId: string;\n remotePeerId: string;\n signalingClient: SignalingClient;\n rtcConfig?: RTCConfiguration;\n onChannelMessage?: (peerId: string, message: Record<string, unknown>) => void;\n onChannelOpen?: (remotePeerId: string) => void;\n}\n\nclass PeerConnection {\n private static readonly DEFAULT_ICE_SERVERS = [{ urls: 'stun:stun.l.google.com:19302' }];\n\n private pc: RTCPeerConnection;\n private dataChannel: RTCDataChannel | null = null;\n private selfPeerId: string;\n private remotePeerId: string;\n private signalingClient: SignalingClient;\n private onChannelMessage?: (peerId: string, message: Record<string, unknown>) => void;\n private onChannelOpen?: (remotePeerId: string) => void;\n\n constructor(opts: PeerConnectionOptions) {\n this.selfPeerId = opts.localPeerId;\n this.remotePeerId = opts.remotePeerId;\n this.signalingClient = opts.signalingClient;\n this.onChannelMessage = opts.onChannelMessage;\n this.onChannelOpen = opts.onChannelOpen;\n\n this.pc = new RTCPeerConnection(\n opts.rtcConfig ?? { iceServers: PeerConnection.DEFAULT_ICE_SERVERS }\n );\n\n this.setupConnection();\n\n if (this.initiator) {\n this.initiate();\n }\n }\n\n get initiator(): boolean {\n return this.selfPeerId > this.remotePeerId;\n }\n\n private setupConnection() {\n this.pc.onicecandidate = (event) => {\n if (event.candidate) {\n this.signalingClient.send({\n type: 'signal',\n to: this.remotePeerId,\n data: {\n type: 'ice-candidate',\n candidate: event.candidate,\n },\n });\n }\n };\n\n this.pc.ondatachannel = (event) => {\n this.dataChannel = event.channel;\n this.setupDataChannel(event.channel);\n };\n }\n\n private async initiate() {\n this.dataChannel = this.pc.createDataChannel('game-sync');\n this.setupDataChannel(this.dataChannel);\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n\n this.signalingClient.send({\n type: 'signal',\n to: this.remotePeerId,\n data: {\n type: 'offer',\n sdp: offer,\n },\n });\n }\n\n async handleSignal(data: SignalData) {\n switch (data.type) {\n case 'offer': {\n await this.pc.setRemoteDescription(new RTCSessionDescription(data.sdp));\n const answer = await this.pc.createAnswer();\n await this.pc.setLocalDescription(answer);\n\n this.signalingClient.send({\n type: 'signal',\n to: this.remotePeerId,\n data: {\n type: 'answer',\n sdp: answer,\n },\n });\n break;\n }\n\n case 'answer':\n await this.pc.setRemoteDescription(new RTCSessionDescription(data.sdp));\n break;\n\n case 'ice-candidate':\n await this.pc.addIceCandidate(new RTCIceCandidate(data.candidate));\n break;\n }\n }\n\n private setupDataChannel(channel: RTCDataChannel) {\n channel.onopen = () => {\n console.log(`Data channel open to ${this.remotePeerId}`);\n this.onChannelOpen?.(this.remotePeerId);\n };\n\n channel.onclose = () => {\n console.log(`Data channel closed to ${this.remotePeerId}`);\n };\n\n channel.onerror = (error) => {\n console.error(`Data channel error with ${this.remotePeerId}:`, error);\n };\n\n channel.onmessage = (event) => {\n const message = JSON.parse(event.data);\n this.onChannelMessage?.(this.remotePeerId, message);\n };\n }\n\n send(message: Record<string, unknown>) {\n if (this.dataChannel?.readyState === 'open') {\n this.dataChannel.send(JSON.stringify(message));\n }\n }\n\n close() {\n this.dataChannel?.close();\n this.pc.close();\n }\n}\n\nexport { PeerConnection };\n","import type { SignalData } from './PeerConnection';\n\ntype SignalingEventHandler = (event: SignalingEvent) => void;\n\ninterface SignalingEvent {\n type: 'joined' | 'peer-list' | 'peer-joined' | 'peer-left' | 'signal' | 'disconnected';\n peerId?: string;\n peers?: string[];\n from?: string;\n data?: SignalData;\n}\n\nclass SignalingClient {\n private ws: WebSocket | null = null;\n private eventHandlers: Map<string, SignalingEventHandler[]> = new Map();\n private peerId: string = '';\n\n constructor(\n private serverUrl: string,\n private roomId: string\n ) {}\n\n connect(): Promise<string> {\n return new Promise((resolve, reject) => {\n this.ws = new WebSocket(this.serverUrl);\n\n this.ws.onopen = () => {\n this.send({\n type: 'join',\n roomId: this.roomId,\n });\n };\n\n this.ws.onmessage = (event) => {\n const message = JSON.parse(event.data);\n\n if (message.type === 'joined') {\n this.peerId = message.peerId;\n resolve(message.peerId);\n }\n\n this.emit(message);\n };\n\n this.ws.onerror = (error) => {\n reject(error);\n };\n\n this.ws.onclose = () => {\n console.log('Disconnected from signaling server');\n };\n });\n }\n\n send(message: Record<string, unknown>): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(message));\n }\n }\n\n on(eventType: string, handler: SignalingEventHandler): void {\n if (!this.eventHandlers.has(eventType)) {\n this.eventHandlers.set(eventType, []);\n }\n this.eventHandlers.get(eventType)?.push(handler);\n }\n\n off(eventType: string, handler: SignalingEventHandler): void {\n const handlers = this.eventHandlers.get(eventType);\n if (handlers) {\n const index = handlers.indexOf(handler);\n if (index > -1) {\n handlers.splice(index, 1);\n }\n }\n }\n\n private emit(event: SignalingEvent): void {\n const handlers = this.eventHandlers.get(event.type);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(event);\n });\n }\n\n const wildcardHandlers = this.eventHandlers.get('*');\n if (wildcardHandlers) {\n wildcardHandlers.forEach((handler) => {\n handler(event);\n });\n }\n }\n\n disconnect(): void {\n this.ws?.close();\n this.ws = null;\n }\n\n getPeerId(): string {\n return this.peerId;\n }\n\n isConnected(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n}\n\nexport { SignalingClient };\n","import type { JSONSerializable } from '../../types';\nimport type { MergeStrategy } from './types';\n\n/**\n * Metadata carried by the Lamport clock strategy.\n * `clock` is a monotonically increasing logical counter; `tiebreaker` is the\n * writing peer's ID, used to deterministically resolve equal-clock conflicts.\n */\nexport type LamportMeta = { clock: number; tiebreaker: string };\n\n/**\n * Creates a Lamport logical-clock merge strategy.\n *\n * Unlike wall-clock timestamps, Lamport clocks do not rely on peers having\n * synchronised system clocks. The clock advances monotonically: it increments\n * on every local write and is fast-forwarded to `max(local, incoming)` on\n * every receive, so causal ordering is preserved across all peers.\n *\n * Concurrent writes (same clock value) are broken deterministically by\n * comparing the writing peer's ID as a string, so every peer reaches the same\n * result independently.\n *\n * @param getPeerId - A function that returns the local peer's ID at call time.\n * Pass a ref-backed getter so the strategy stays stable even before the\n * signalling handshake completes.\n */\nexport function createLamportStrategy(\n getPeerId: () => string\n): MergeStrategy<JSONSerializable, LamportMeta> {\n let localClock = 0;\n\n return {\n initialMeta: { clock: 0, tiebreaker: getPeerId() },\n\n createMeta(): LamportMeta {\n return { clock: ++localClock, tiebreaker: getPeerId() };\n },\n\n merge(\n _currentState,\n currentMeta,\n incomingState,\n incomingMeta,\n senderId\n ): { state: JSONSerializable | null; meta: LamportMeta } | null {\n localClock = Math.max(localClock, incomingMeta.clock);\n\n const clockWins = incomingMeta.clock > currentMeta.clock;\n const incomingTiebreaker = incomingMeta.tiebreaker || senderId;\n const currentTiebreaker = currentMeta.tiebreaker || getPeerId();\n\n const tiebreakWins =\n incomingMeta.clock === currentMeta.clock && incomingTiebreaker > currentTiebreaker;\n\n return clockWins || tiebreakWins\n ? { state: incomingState, meta: { ...incomingMeta, tiebreaker: incomingTiebreaker } }\n : null;\n },\n };\n}\n","import type { JSONSerializable } from '../../types';\nimport type { MergeStrategy } from './types';\n\n/**\n * Metadata carried by the Last Write Wins strategy.\n * `timestamp` is a wall-clock time in milliseconds; `tiebreaker` is the\n * writing peer's ID, used to deterministically resolve same-millisecond conflicts.\n */\nexport type LastWriteWinsMeta = { timestamp: number; tiebreaker: string };\n\n/**\n * Creates a Last Write Wins merge strategy.\n *\n * Accepts the incoming state whenever its wall-clock timestamp is strictly\n * greater than the current one. Ties (same millisecond) are broken\n * deterministically by comparing peer ID strings, so every peer converges to\n * the same result independently without coordination.\n *\n * Note: this strategy relies on peers having reasonably synchronised system\n * clocks. It is appropriate for use cases where approximate recency is\n * sufficient (e.g. presence indicators, cursor positions, UI state), but\n * should not be used where causal ordering must be guaranteed — prefer\n * `createLamportStrategy` in that case.\n *\n * @param getPeerId - A function that returns the local peer's ID at call time.\n * Pass a ref-backed getter so the strategy stays stable even before the\n * signalling handshake completes.\n */\nexport function createLastWriteWinsStrategy(\n getPeerId: () => string\n): MergeStrategy<JSONSerializable, LastWriteWinsMeta> {\n return {\n initialMeta: { timestamp: 0, tiebreaker: getPeerId() },\n\n createMeta(): LastWriteWinsMeta {\n return { timestamp: Date.now(), tiebreaker: getPeerId() };\n },\n\n merge(\n _currentState,\n currentMeta,\n incomingState,\n incomingMeta,\n senderId\n ): { state: JSONSerializable | null; meta: LastWriteWinsMeta } | null {\n const incomingTiebreaker = incomingMeta.tiebreaker || senderId;\n const currentTiebreaker = currentMeta.tiebreaker || getPeerId();\n\n const timestampWins = incomingMeta.timestamp > currentMeta.timestamp;\n const tiebreakWins =\n incomingMeta.timestamp === currentMeta.timestamp && incomingTiebreaker > currentTiebreaker;\n\n return timestampWins || tiebreakWins\n ? { state: incomingState, meta: { ...incomingMeta, tiebreaker: incomingTiebreaker } }\n : null;\n },\n };\n}\n","import { useContext } from 'react';\nimport { RoomContext, type RoomContextValue } from '../context';\n\nexport function useRoom(): RoomContextValue {\n const context = useContext(RoomContext);\n if (!context) {\n throw new Error('useRoom must be used within a Room provider');\n }\n return context;\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n createLamportStrategy,\n type LamportMeta,\n type MergeMeta,\n type MergeStrategy,\n} from '../core/merge-strategies';\nimport type { JSONSerializable } from '../types';\nimport { useRoom } from './useRoom';\n\ntype SharedStatePayload<TState extends JSONSerializable, TMeta extends MergeMeta = MergeMeta> = {\n key: string;\n state: TState | null;\n meta: TMeta;\n};\n\ntype StateRequestPayload = {\n type: 'state-request';\n key: string;\n};\n\nfunction isStateRequest(\n data: SharedStatePayload<JSONSerializable> | StateRequestPayload\n): data is StateRequestPayload {\n return (\n typeof data === 'object' && data !== null && 'type' in data && data.type === 'state-request'\n );\n}\n\n/**\n * A hook that allows you to share state between multiple peers.\n *\n * Works hostlessly by:\n *\n * - Broadcasting updates to all peers under the given key; everyone keeps the\n * causally latest state according to a Lamport logical clock.\n * - Late joiners: when a data channel opens to a new peer, both sides push\n * their current state to each other; the merge strategy keeps the winner.\n *\n * @param key - A string key that namespaces this shared state slice. Multiple calls with the same key share state; different keys are independent.\n * @param initialState - The initial state of the shared state (used only before any sync or update).\n * @returns A tuple containing the current state and a function to update the state.\n */\nexport function useSharedState<TState extends JSONSerializable>(\n key: string,\n initialState: TState | null\n): [state: TState | null, setState: (next: TState | null) => void];\n\n/**\n * A hook that allows you to share state between multiple peers with a custom merge strategy.\n *\n * @param key - A string key that namespaces this shared state slice.\n * @param initialState - The initial state (used only before any sync or update).\n * @param strategy - A merge strategy controlling how incoming state is reconciled.\n * @returns A tuple containing the current state and a function to update the state.\n */\nexport function useSharedState<TState extends JSONSerializable, TMeta extends MergeMeta>(\n key: string,\n initialState: TState | null,\n strategy: MergeStrategy<TState, TMeta>\n): [state: TState | null, setState: (next: TState | null) => void];\n\nexport function useSharedState<\n TState extends JSONSerializable,\n TMeta extends MergeMeta = LamportMeta,\n>(\n key: string,\n initialState: TState | null,\n strategy?: MergeStrategy<TState, TMeta>\n): [state: TState | null, setState: (next: TState | null) => void] {\n const { broadcast, onMessage, onPeerConnected, peerId, sendToPeer } = useRoom();\n\n const peerIdRef = useRef(peerId);\n peerIdRef.current = peerId;\n\n const lamportStrategyRef = useRef<MergeStrategy<JSONSerializable, LamportMeta>>(\n createLamportStrategy(() => peerIdRef.current)\n );\n\n const effectiveStrategy: MergeStrategy<TState, TMeta> =\n strategy !== undefined\n ? strategy\n : (lamportStrategyRef.current as unknown as MergeStrategy<TState, TMeta>);\n\n const [rawState, setRawState] = useState<TState | null>(initialState);\n const rawStateRef = useRef<TState | null>(initialState);\n const rawStateMetaRef = useRef<TMeta>(effectiveStrategy.initialMeta);\n\n const strategyRef = useRef(effectiveStrategy);\n\n rawStateRef.current = rawState;\n\n useEffect(() => {\n strategyRef.current = effectiveStrategy;\n }, [effectiveStrategy]);\n\n useEffect(\n function handleMessage() {\n const unsubscribe = onMessage<SharedStatePayload<TState, TMeta> | StateRequestPayload>(\n ({ senderId, data }) => {\n if (senderId === peerId) return;\n\n if (isStateRequest(data)) {\n if (data.key === key) {\n sendToPeer(senderId, {\n senderId: peerId,\n data: { key, state: rawStateRef.current, meta: rawStateMetaRef.current },\n timestamp: Date.now(),\n });\n }\n return;\n }\n\n if (data.key !== key) return;\n\n const result = strategyRef.current.merge(\n rawStateRef.current,\n rawStateMetaRef.current,\n data.state,\n data.meta,\n senderId\n );\n if (result) {\n rawStateRef.current = result.state;\n rawStateMetaRef.current = result.meta;\n setRawState(result.state);\n }\n }\n );\n return unsubscribe;\n },\n [key, onMessage, peerId, sendToPeer]\n );\n\n useEffect(\n function pushStateOnConnect() {\n const unsubscribe = onPeerConnected((remotePeerId) => {\n sendToPeer(remotePeerId, {\n senderId: peerId,\n data: { key, state: rawStateRef.current, meta: rawStateMetaRef.current },\n timestamp: Date.now(),\n });\n });\n return unsubscribe;\n },\n [key, onPeerConnected, peerId, sendToPeer]\n );\n\n const setState = useCallback(\n (next: TState | null): void => {\n const meta = strategyRef.current.createMeta();\n rawStateRef.current = next;\n rawStateMetaRef.current = meta;\n setRawState(next);\n broadcast<SharedStatePayload<TState, TMeta>>({\n senderId: peerId,\n data: { key, state: next, meta },\n timestamp: Date.now(),\n });\n },\n [broadcast, key, peerId]\n );\n\n return [rawState, setState];\n}\n"],"mappings":"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","names":["import_react","import_react"]}
|