@carverjs/multiplayer 0.0.1 → 0.0.2
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/InputBuffer-J6XT_Tt0.d.mts +61 -0
- package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
- package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
- package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
- package/dist/{chunk-UD6FDZMX.mjs → chunk-CBTAOVXP.mjs} +34 -3
- package/dist/chunk-CBTAOVXP.mjs.map +1 -0
- package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
- package/dist/chunk-Q25TJEY4.mjs.map +1 -0
- package/dist/{chunk-3KT73N2S.mjs → chunk-UKEFWQ76.mjs} +0 -0
- package/dist/chunk-UKEFWQ76.mjs.map +1 -0
- package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
- package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
- package/dist/index.d.mts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +744 -245
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -37
- package/dist/index.mjs.map +1 -1
- package/dist/strategy.d.mts +2 -2
- package/dist/strategy.d.ts +2 -2
- package/dist/strategy.js +33 -2
- package/dist/strategy.js.map +1 -1
- package/dist/strategy.mjs +1 -1
- package/dist/sync.d.mts +134 -50
- package/dist/sync.d.ts +134 -50
- package/dist/sync.js +499 -205
- package/dist/sync.js.map +1 -1
- package/dist/sync.mjs +15 -3
- package/dist/transport.d.mts +0 -0
- package/dist/transport.d.ts +0 -0
- package/dist/transport.js +0 -0
- package/dist/transport.js.map +1 -1
- package/dist/transport.mjs +2 -2
- package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
- package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
- package/dist/types.d.mts +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +26 -5
- package/dist/chunk-3KT73N2S.mjs.map +0 -1
- package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
- package/dist/chunk-UD6FDZMX.mjs.map +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { P as PlayerInput } from './NetworkManager-D-DxFgdM.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* InputBuffer — unified ring-buffer for local and peer inputs.
|
|
5
|
+
*
|
|
6
|
+
* Ported from LumberNet's LumberInputBuffer. Stores:
|
|
7
|
+
* - local player tick-keyed inputs (storeTick / getTick / hasTick)
|
|
8
|
+
* - last-received input per remote peer (setRemote / getRemote / allRemotes)
|
|
9
|
+
* - per-peer tick-keyed inputs for accurate rollback resimulation (getRemoteAtTick)
|
|
10
|
+
*
|
|
11
|
+
* Generic over per-tick payload I. Caller supplies the neutral payload.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare class InputBuffer<I extends PlayerInput = PlayerInput> {
|
|
15
|
+
private readonly _historySize;
|
|
16
|
+
private readonly _neutral;
|
|
17
|
+
/** Local player tick-keyed inputs (ring buffer). */
|
|
18
|
+
private readonly _local;
|
|
19
|
+
/** Last-received input per remote peer. */
|
|
20
|
+
private readonly _remotes;
|
|
21
|
+
/** Per-peer tick-keyed inputs for accurate rollback re-simulation. */
|
|
22
|
+
private readonly _peerTicks;
|
|
23
|
+
constructor(neutralInput: I, historySize?: number);
|
|
24
|
+
/** Record a snapshot of the local input at the given tick. Evicts the entry exactly historySize back. */
|
|
25
|
+
storeTick(tick: number, input: I): void;
|
|
26
|
+
/** Return the local input at `tick`, or a neutral copy if out of range. */
|
|
27
|
+
getTick(tick: number): I;
|
|
28
|
+
/** True if we have a stored local input for this tick (used to avoid spurious justPressed after snap/rejoin). */
|
|
29
|
+
hasTick(tick: number): boolean;
|
|
30
|
+
/** Neutral payload with every boolean field forced false (use for justPressed when prev tick is unknown). */
|
|
31
|
+
getJustPressedZero(): I;
|
|
32
|
+
/**
|
|
33
|
+
* Record a remote peer's input. If `tick` is given (the sender's local tick),
|
|
34
|
+
* also store it in the per-peer ring buffer for rollback.
|
|
35
|
+
*/
|
|
36
|
+
setRemote(peerId: string, input: I, tick?: number): void;
|
|
37
|
+
/** Last-known input for a peer, or a neutral copy if never received. */
|
|
38
|
+
getRemote(peerId: string): I;
|
|
39
|
+
/** Snapshot of all remote peers' last-known inputs (shallow copy of the map). */
|
|
40
|
+
allRemotes(): Map<string, I>;
|
|
41
|
+
/**
|
|
42
|
+
* Return a peer's exact input at the given tick (for rollback accuracy),
|
|
43
|
+
* falling back to their last-known input when history does not reach that far.
|
|
44
|
+
*/
|
|
45
|
+
getRemoteAtTick(peerId: string, tick: number): I;
|
|
46
|
+
/** Override the last-known input for a peer (does NOT touch tick history). */
|
|
47
|
+
overrideRemote(peerId: string, input: I): void;
|
|
48
|
+
/** Number of currently tracked remote peers. */
|
|
49
|
+
get peerCount(): number;
|
|
50
|
+
/** Iterate tracked peer IDs. */
|
|
51
|
+
peerIds(): IterableIterator<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Keep only these peer IDs; remove any other remotes (e.g. after leave/rejoin).
|
|
54
|
+
* Call when the room's peer list changes so stale peers stop receiving input.
|
|
55
|
+
*/
|
|
56
|
+
setPeerIds(peerIds: ReadonlySet<string> | string[]): void;
|
|
57
|
+
/** Clear local history, remote last-known inputs, and per-peer tick history. */
|
|
58
|
+
clear(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { InputBuffer as I };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { P as PlayerInput } from './NetworkManager-DH9uGVMg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* InputBuffer — unified ring-buffer for local and peer inputs.
|
|
5
|
+
*
|
|
6
|
+
* Ported from LumberNet's LumberInputBuffer. Stores:
|
|
7
|
+
* - local player tick-keyed inputs (storeTick / getTick / hasTick)
|
|
8
|
+
* - last-received input per remote peer (setRemote / getRemote / allRemotes)
|
|
9
|
+
* - per-peer tick-keyed inputs for accurate rollback resimulation (getRemoteAtTick)
|
|
10
|
+
*
|
|
11
|
+
* Generic over per-tick payload I. Caller supplies the neutral payload.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare class InputBuffer<I extends PlayerInput = PlayerInput> {
|
|
15
|
+
private readonly _historySize;
|
|
16
|
+
private readonly _neutral;
|
|
17
|
+
/** Local player tick-keyed inputs (ring buffer). */
|
|
18
|
+
private readonly _local;
|
|
19
|
+
/** Last-received input per remote peer. */
|
|
20
|
+
private readonly _remotes;
|
|
21
|
+
/** Per-peer tick-keyed inputs for accurate rollback re-simulation. */
|
|
22
|
+
private readonly _peerTicks;
|
|
23
|
+
constructor(neutralInput: I, historySize?: number);
|
|
24
|
+
/** Record a snapshot of the local input at the given tick. Evicts the entry exactly historySize back. */
|
|
25
|
+
storeTick(tick: number, input: I): void;
|
|
26
|
+
/** Return the local input at `tick`, or a neutral copy if out of range. */
|
|
27
|
+
getTick(tick: number): I;
|
|
28
|
+
/** True if we have a stored local input for this tick (used to avoid spurious justPressed after snap/rejoin). */
|
|
29
|
+
hasTick(tick: number): boolean;
|
|
30
|
+
/** Neutral payload with every boolean field forced false (use for justPressed when prev tick is unknown). */
|
|
31
|
+
getJustPressedZero(): I;
|
|
32
|
+
/**
|
|
33
|
+
* Record a remote peer's input. If `tick` is given (the sender's local tick),
|
|
34
|
+
* also store it in the per-peer ring buffer for rollback.
|
|
35
|
+
*/
|
|
36
|
+
setRemote(peerId: string, input: I, tick?: number): void;
|
|
37
|
+
/** Last-known input for a peer, or a neutral copy if never received. */
|
|
38
|
+
getRemote(peerId: string): I;
|
|
39
|
+
/** Snapshot of all remote peers' last-known inputs (shallow copy of the map). */
|
|
40
|
+
allRemotes(): Map<string, I>;
|
|
41
|
+
/**
|
|
42
|
+
* Return a peer's exact input at the given tick (for rollback accuracy),
|
|
43
|
+
* falling back to their last-known input when history does not reach that far.
|
|
44
|
+
*/
|
|
45
|
+
getRemoteAtTick(peerId: string, tick: number): I;
|
|
46
|
+
/** Override the last-known input for a peer (does NOT touch tick history). */
|
|
47
|
+
overrideRemote(peerId: string, input: I): void;
|
|
48
|
+
/** Number of currently tracked remote peers. */
|
|
49
|
+
get peerCount(): number;
|
|
50
|
+
/** Iterate tracked peer IDs. */
|
|
51
|
+
peerIds(): IterableIterator<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Keep only these peer IDs; remove any other remotes (e.g. after leave/rejoin).
|
|
54
|
+
* Call when the room's peer list changes so stale peers stop receiving input.
|
|
55
|
+
*/
|
|
56
|
+
setPeerIds(peerIds: ReadonlySet<string> | string[]): void;
|
|
57
|
+
/** Clear local history, remote last-known inputs, and per-peer tick history. */
|
|
58
|
+
clear(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { InputBuffer as I };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalingStrategy } from './types-
|
|
1
|
+
import { S as SignalingStrategy } from './types-hNfCIBzj.mjs';
|
|
2
2
|
|
|
3
3
|
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'migrating' | 'reconnecting';
|
|
4
4
|
type RoomState = 'lobby' | 'playing' | 'ended';
|
|
@@ -108,15 +108,68 @@ interface EntityState3D {
|
|
|
108
108
|
c?: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
110
|
type EntityState = EntityState2D | EntityState3D;
|
|
111
|
+
/** Flat per-tick input payload. Booleans get edge detection; numbers pass through. */
|
|
112
|
+
type PlayerInput = Record<string, boolean | number | undefined>;
|
|
113
|
+
/**
|
|
114
|
+
* Game simulation callback for prediction mode.
|
|
115
|
+
* Keys of both maps are peer ids (the local player appears under transport.peerId).
|
|
116
|
+
* Invoked once per fixed tick (isRollback=false) and once per resimulated tick (isRollback=true).
|
|
117
|
+
*/
|
|
118
|
+
type PhysicsStepCallback = (inputs: Map<string, PlayerInput>, justPressed: Map<string, PlayerInput>, tick: number, isRollback: boolean, dt: number) => void;
|
|
119
|
+
/** Per-entity visual error offset produced by rollback. 2D uses x/y/a (z=0, q=identity); 3D uses x/y/z + quaternion (a=0). */
|
|
120
|
+
interface ErrorOffset {
|
|
121
|
+
x: number;
|
|
122
|
+
y: number;
|
|
123
|
+
z: number;
|
|
124
|
+
a: number;
|
|
125
|
+
qx: number;
|
|
126
|
+
qy: number;
|
|
127
|
+
qz: number;
|
|
128
|
+
qw: number;
|
|
129
|
+
}
|
|
130
|
+
/** World access used by PredictionSync for forward stepping and rollback. */
|
|
131
|
+
interface PredictionWorldDriver {
|
|
132
|
+
/** Read current state of every networked entity (raw physics, no error offsets). */
|
|
133
|
+
captureState(): Map<string, EntityState>;
|
|
134
|
+
/** Hard-apply states (position, rotation, velocities) to actors and rigid bodies, waking them. Skips tombstones. */
|
|
135
|
+
applyState(entities: Iterable<EntityState>): void;
|
|
136
|
+
/** Optional: step the physics world one fixed tick. If omitted, the game steps inside onPhysicsStep. */
|
|
137
|
+
stepWorld?(): void;
|
|
138
|
+
}
|
|
139
|
+
interface PredictionSyncOptions {
|
|
140
|
+
/** Max |localTick - (serverTick + driftTargetTicks)| before hard tick snap (no resim). Default 15. */
|
|
141
|
+
maxRewindTicks?: number;
|
|
142
|
+
/** Per-axis positional jump (units) above which a rollback error vector is suppressed (intentional teleport). Default 150. */
|
|
143
|
+
snapThreshold?: number;
|
|
144
|
+
/** Multiplicative error decay per render frame. Default 0.85. */
|
|
145
|
+
errorDecay?: number;
|
|
146
|
+
/** Max positional correction magnitude applied per render frame. 0 = disabled (full decaying error applied). Default 0. */
|
|
147
|
+
maxErrorPerFrame?: number;
|
|
148
|
+
/** Neutral input payload used as fallback for unknown ticks/peers. Default {}. */
|
|
149
|
+
neutralInput?: PlayerInput;
|
|
150
|
+
/** Tick-history ring size for local and per-peer inputs. Default 120. */
|
|
151
|
+
inputHistorySize?: number;
|
|
152
|
+
/** Rollback snap target offset: snap target = serverTick + driftTargetTicks. Default 4. */
|
|
153
|
+
driftTargetTicks?: number;
|
|
154
|
+
}
|
|
155
|
+
/** Fired by ClientReceiver after each snapshot is merged into the full world state. */
|
|
156
|
+
type SnapshotListener = (tick: number, entities: Map<string, EntityState>, hostInput: PlayerInput | undefined) => void;
|
|
157
|
+
/** Minimal structural source of merged snapshots (implemented by SnapshotSync). */
|
|
158
|
+
interface SnapshotSource {
|
|
159
|
+
onSnapshot(cb: SnapshotListener): void;
|
|
160
|
+
}
|
|
111
161
|
interface SnapshotPacket {
|
|
112
162
|
t: number;
|
|
113
163
|
b: number;
|
|
114
164
|
s: Uint8Array;
|
|
115
|
-
hi?:
|
|
165
|
+
hi?: PlayerInput;
|
|
116
166
|
}
|
|
117
167
|
interface InputPacket {
|
|
168
|
+
/** Sender's local tick. */
|
|
118
169
|
t: number;
|
|
119
|
-
|
|
170
|
+
/** Per-tick input payload. */
|
|
171
|
+
i: PlayerInput;
|
|
172
|
+
/** Sender peerId (informational; receivers MUST key by transport-provided peerId). */
|
|
120
173
|
p: string;
|
|
121
174
|
}
|
|
122
175
|
interface EventPacket {
|
|
@@ -173,13 +226,7 @@ interface UseMultiplayerOptions {
|
|
|
173
226
|
velocity?: number;
|
|
174
227
|
custom?: 'strict' | number;
|
|
175
228
|
};
|
|
176
|
-
prediction?:
|
|
177
|
-
maxRewindTicks?: number;
|
|
178
|
-
errorSmoothingDecay?: number;
|
|
179
|
-
maxErrorPerFrame?: number;
|
|
180
|
-
snapThreshold?: number;
|
|
181
|
-
lagCompensation?: boolean;
|
|
182
|
-
};
|
|
229
|
+
prediction?: PredictionSyncOptions;
|
|
183
230
|
interpolation?: {
|
|
184
231
|
bufferSize?: number;
|
|
185
232
|
method?: 'hermite' | 'linear';
|
|
@@ -197,7 +244,9 @@ interface UseMultiplayerOptions {
|
|
|
197
244
|
simulatedPacketLoss?: number;
|
|
198
245
|
logLevel?: 'none' | 'error' | 'warn' | 'verbose';
|
|
199
246
|
};
|
|
200
|
-
|
|
247
|
+
/** Optional: step the physics world one fixed tick. Used for both forward sim and rollback resim. */
|
|
248
|
+
stepWorld?: () => void;
|
|
249
|
+
onPhysicsStep?: PhysicsStepCallback;
|
|
201
250
|
}
|
|
202
251
|
interface MultiplayerContextValue {
|
|
203
252
|
appId: string;
|
|
@@ -241,6 +290,8 @@ declare class TickKeeper {
|
|
|
241
290
|
get drift(): number;
|
|
242
291
|
/** Update server tick from received snapshot */
|
|
243
292
|
setServerTick(serverTick: number): void;
|
|
293
|
+
/** Hard-set the local tick (prediction rollback snap). Does not touch the accumulator. */
|
|
294
|
+
snapTick(tick: number): void;
|
|
244
295
|
/**
|
|
245
296
|
* Accumulate time and return the number of fixed ticks to process.
|
|
246
297
|
* Call this once per render frame with the raw frame delta.
|
|
@@ -302,13 +353,14 @@ declare class Codec {
|
|
|
302
353
|
* Returns null if nothing changed.
|
|
303
354
|
*/
|
|
304
355
|
computeDelta(current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined): EntityState[] | null;
|
|
305
|
-
/** Serialize a delta snapshot packet */
|
|
306
|
-
serializeDelta(tick: number, baseTick: number, current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined): Uint8Array | null;
|
|
356
|
+
/** Serialize a delta snapshot packet (optionally embedding the host's own input as `hi`) */
|
|
357
|
+
serializeDelta(tick: number, baseTick: number, current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined, hostInput?: PlayerInput): Uint8Array | null;
|
|
307
358
|
/** Deserialize a snapshot packet */
|
|
308
359
|
deserializePacket(data: Uint8Array): {
|
|
309
360
|
tick: number;
|
|
310
361
|
baseTick: number;
|
|
311
362
|
entities: EntityState[];
|
|
363
|
+
hostInput: PlayerInput | undefined;
|
|
312
364
|
};
|
|
313
365
|
private _hasChanged;
|
|
314
366
|
private _quantizeEntity;
|
|
@@ -366,4 +418,4 @@ declare class NetworkManager {
|
|
|
366
418
|
private _notifyRoomListeners;
|
|
367
419
|
}
|
|
368
420
|
|
|
369
|
-
export { type CarverTransport as C, type EntityState as E, type InputPacket as I, type JoinOptions as J, type MultiplayerContextValue as M, type NetworkQuality as N, type
|
|
421
|
+
export { type CarverTransport as C, type EntityState as E, type InputPacket as I, type JoinOptions as J, type MultiplayerContextValue as M, type NetworkQuality as N, type PlayerInput as P, type Room as R, SnapshotBuffer as S, type TransportConfig as T, type UseRoomOptions as U, type Player as a, type ChannelOptions as b, type CarverChannel as c, type RoomState as d, Codec as e, type SnapshotListener as f, type SnapshotSource as g, TickKeeper as h, type PredictionSyncOptions as i, type PhysicsStepCallback as j, type PredictionWorldDriver as k, type ErrorOffset as l, type EntityState2D as m, type EntityState3D as n, type EventPacket as o, type SnapshotPacket as p, type SyncMode as q, NetworkManager as r, type ConnectionState as s, type CarverMultiplayerError as t, type UseLobbyOptions as u, type RoomConfig as v, type UseMultiplayerOptions as w, type CarverErrorCode as x };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalingStrategy } from './types-
|
|
1
|
+
import { S as SignalingStrategy } from './types-hNfCIBzj.js';
|
|
2
2
|
|
|
3
3
|
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'migrating' | 'reconnecting';
|
|
4
4
|
type RoomState = 'lobby' | 'playing' | 'ended';
|
|
@@ -108,15 +108,68 @@ interface EntityState3D {
|
|
|
108
108
|
c?: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
110
|
type EntityState = EntityState2D | EntityState3D;
|
|
111
|
+
/** Flat per-tick input payload. Booleans get edge detection; numbers pass through. */
|
|
112
|
+
type PlayerInput = Record<string, boolean | number | undefined>;
|
|
113
|
+
/**
|
|
114
|
+
* Game simulation callback for prediction mode.
|
|
115
|
+
* Keys of both maps are peer ids (the local player appears under transport.peerId).
|
|
116
|
+
* Invoked once per fixed tick (isRollback=false) and once per resimulated tick (isRollback=true).
|
|
117
|
+
*/
|
|
118
|
+
type PhysicsStepCallback = (inputs: Map<string, PlayerInput>, justPressed: Map<string, PlayerInput>, tick: number, isRollback: boolean, dt: number) => void;
|
|
119
|
+
/** Per-entity visual error offset produced by rollback. 2D uses x/y/a (z=0, q=identity); 3D uses x/y/z + quaternion (a=0). */
|
|
120
|
+
interface ErrorOffset {
|
|
121
|
+
x: number;
|
|
122
|
+
y: number;
|
|
123
|
+
z: number;
|
|
124
|
+
a: number;
|
|
125
|
+
qx: number;
|
|
126
|
+
qy: number;
|
|
127
|
+
qz: number;
|
|
128
|
+
qw: number;
|
|
129
|
+
}
|
|
130
|
+
/** World access used by PredictionSync for forward stepping and rollback. */
|
|
131
|
+
interface PredictionWorldDriver {
|
|
132
|
+
/** Read current state of every networked entity (raw physics, no error offsets). */
|
|
133
|
+
captureState(): Map<string, EntityState>;
|
|
134
|
+
/** Hard-apply states (position, rotation, velocities) to actors and rigid bodies, waking them. Skips tombstones. */
|
|
135
|
+
applyState(entities: Iterable<EntityState>): void;
|
|
136
|
+
/** Optional: step the physics world one fixed tick. If omitted, the game steps inside onPhysicsStep. */
|
|
137
|
+
stepWorld?(): void;
|
|
138
|
+
}
|
|
139
|
+
interface PredictionSyncOptions {
|
|
140
|
+
/** Max |localTick - (serverTick + driftTargetTicks)| before hard tick snap (no resim). Default 15. */
|
|
141
|
+
maxRewindTicks?: number;
|
|
142
|
+
/** Per-axis positional jump (units) above which a rollback error vector is suppressed (intentional teleport). Default 150. */
|
|
143
|
+
snapThreshold?: number;
|
|
144
|
+
/** Multiplicative error decay per render frame. Default 0.85. */
|
|
145
|
+
errorDecay?: number;
|
|
146
|
+
/** Max positional correction magnitude applied per render frame. 0 = disabled (full decaying error applied). Default 0. */
|
|
147
|
+
maxErrorPerFrame?: number;
|
|
148
|
+
/** Neutral input payload used as fallback for unknown ticks/peers. Default {}. */
|
|
149
|
+
neutralInput?: PlayerInput;
|
|
150
|
+
/** Tick-history ring size for local and per-peer inputs. Default 120. */
|
|
151
|
+
inputHistorySize?: number;
|
|
152
|
+
/** Rollback snap target offset: snap target = serverTick + driftTargetTicks. Default 4. */
|
|
153
|
+
driftTargetTicks?: number;
|
|
154
|
+
}
|
|
155
|
+
/** Fired by ClientReceiver after each snapshot is merged into the full world state. */
|
|
156
|
+
type SnapshotListener = (tick: number, entities: Map<string, EntityState>, hostInput: PlayerInput | undefined) => void;
|
|
157
|
+
/** Minimal structural source of merged snapshots (implemented by SnapshotSync). */
|
|
158
|
+
interface SnapshotSource {
|
|
159
|
+
onSnapshot(cb: SnapshotListener): void;
|
|
160
|
+
}
|
|
111
161
|
interface SnapshotPacket {
|
|
112
162
|
t: number;
|
|
113
163
|
b: number;
|
|
114
164
|
s: Uint8Array;
|
|
115
|
-
hi?:
|
|
165
|
+
hi?: PlayerInput;
|
|
116
166
|
}
|
|
117
167
|
interface InputPacket {
|
|
168
|
+
/** Sender's local tick. */
|
|
118
169
|
t: number;
|
|
119
|
-
|
|
170
|
+
/** Per-tick input payload. */
|
|
171
|
+
i: PlayerInput;
|
|
172
|
+
/** Sender peerId (informational; receivers MUST key by transport-provided peerId). */
|
|
120
173
|
p: string;
|
|
121
174
|
}
|
|
122
175
|
interface EventPacket {
|
|
@@ -173,13 +226,7 @@ interface UseMultiplayerOptions {
|
|
|
173
226
|
velocity?: number;
|
|
174
227
|
custom?: 'strict' | number;
|
|
175
228
|
};
|
|
176
|
-
prediction?:
|
|
177
|
-
maxRewindTicks?: number;
|
|
178
|
-
errorSmoothingDecay?: number;
|
|
179
|
-
maxErrorPerFrame?: number;
|
|
180
|
-
snapThreshold?: number;
|
|
181
|
-
lagCompensation?: boolean;
|
|
182
|
-
};
|
|
229
|
+
prediction?: PredictionSyncOptions;
|
|
183
230
|
interpolation?: {
|
|
184
231
|
bufferSize?: number;
|
|
185
232
|
method?: 'hermite' | 'linear';
|
|
@@ -197,7 +244,9 @@ interface UseMultiplayerOptions {
|
|
|
197
244
|
simulatedPacketLoss?: number;
|
|
198
245
|
logLevel?: 'none' | 'error' | 'warn' | 'verbose';
|
|
199
246
|
};
|
|
200
|
-
|
|
247
|
+
/** Optional: step the physics world one fixed tick. Used for both forward sim and rollback resim. */
|
|
248
|
+
stepWorld?: () => void;
|
|
249
|
+
onPhysicsStep?: PhysicsStepCallback;
|
|
201
250
|
}
|
|
202
251
|
interface MultiplayerContextValue {
|
|
203
252
|
appId: string;
|
|
@@ -241,6 +290,8 @@ declare class TickKeeper {
|
|
|
241
290
|
get drift(): number;
|
|
242
291
|
/** Update server tick from received snapshot */
|
|
243
292
|
setServerTick(serverTick: number): void;
|
|
293
|
+
/** Hard-set the local tick (prediction rollback snap). Does not touch the accumulator. */
|
|
294
|
+
snapTick(tick: number): void;
|
|
244
295
|
/**
|
|
245
296
|
* Accumulate time and return the number of fixed ticks to process.
|
|
246
297
|
* Call this once per render frame with the raw frame delta.
|
|
@@ -302,13 +353,14 @@ declare class Codec {
|
|
|
302
353
|
* Returns null if nothing changed.
|
|
303
354
|
*/
|
|
304
355
|
computeDelta(current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined): EntityState[] | null;
|
|
305
|
-
/** Serialize a delta snapshot packet */
|
|
306
|
-
serializeDelta(tick: number, baseTick: number, current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined): Uint8Array | null;
|
|
356
|
+
/** Serialize a delta snapshot packet (optionally embedding the host's own input as `hi`) */
|
|
357
|
+
serializeDelta(tick: number, baseTick: number, current: Map<string, EntityState>, baseline: Map<string, EntityState> | undefined, hostInput?: PlayerInput): Uint8Array | null;
|
|
307
358
|
/** Deserialize a snapshot packet */
|
|
308
359
|
deserializePacket(data: Uint8Array): {
|
|
309
360
|
tick: number;
|
|
310
361
|
baseTick: number;
|
|
311
362
|
entities: EntityState[];
|
|
363
|
+
hostInput: PlayerInput | undefined;
|
|
312
364
|
};
|
|
313
365
|
private _hasChanged;
|
|
314
366
|
private _quantizeEntity;
|
|
@@ -366,4 +418,4 @@ declare class NetworkManager {
|
|
|
366
418
|
private _notifyRoomListeners;
|
|
367
419
|
}
|
|
368
420
|
|
|
369
|
-
export { type CarverTransport as C, type EntityState as E, type InputPacket as I, type JoinOptions as J, type MultiplayerContextValue as M, type NetworkQuality as N, type
|
|
421
|
+
export { type CarverTransport as C, type EntityState as E, type InputPacket as I, type JoinOptions as J, type MultiplayerContextValue as M, type NetworkQuality as N, type PlayerInput as P, type Room as R, SnapshotBuffer as S, type TransportConfig as T, type UseRoomOptions as U, type Player as a, type ChannelOptions as b, type CarverChannel as c, type RoomState as d, Codec as e, type SnapshotListener as f, type SnapshotSource as g, TickKeeper as h, type PredictionSyncOptions as i, type PhysicsStepCallback as j, type PredictionWorldDriver as k, type ErrorOffset as l, type EntityState2D as m, type EntityState3D as n, type EventPacket as o, type SnapshotPacket as p, type SyncMode as q, NetworkManager as r, type ConnectionState as s, type CarverMultiplayerError as t, type UseLobbyOptions as u, type RoomConfig as v, type UseMultiplayerOptions as w, type CarverErrorCode as x };
|
|
@@ -76,6 +76,7 @@ var MqttStrategy = class {
|
|
|
76
76
|
this._peerExpiryTimer = null;
|
|
77
77
|
this._lobbySubscribed = false;
|
|
78
78
|
this._destroyed = false;
|
|
79
|
+
this._lastAnnouncement = null;
|
|
79
80
|
this.selfId = generatePeerId();
|
|
80
81
|
this._appId = appId;
|
|
81
82
|
this._config = config;
|
|
@@ -141,9 +142,19 @@ var MqttStrategy = class {
|
|
|
141
142
|
removeFromArray(this._onLobby, cb);
|
|
142
143
|
};
|
|
143
144
|
}
|
|
145
|
+
updateRoomOccupancy(roomId, playerCount, state) {
|
|
146
|
+
const ann = this._lastAnnouncement;
|
|
147
|
+
if (!ann || ann.roomId !== roomId || !this._client) return;
|
|
148
|
+
ann.playerCount = playerCount;
|
|
149
|
+
if (state) ann.state = state;
|
|
150
|
+
ann.lastSeen = Date.now();
|
|
151
|
+
const topic = mqttTopics(this._appId, roomId, "").roomLobbyEntry;
|
|
152
|
+
this._client.publish(topic, JSON.stringify(ann), { retain: true, qos: 1 });
|
|
153
|
+
}
|
|
144
154
|
announceRoom(announcement) {
|
|
145
155
|
if (!this._client) return;
|
|
146
156
|
const topic = mqttTopics(this._appId, announcement.roomId, "").roomLobbyEntry;
|
|
157
|
+
this._lastAnnouncement = announcement;
|
|
147
158
|
announcement.lastSeen = Date.now();
|
|
148
159
|
this._client.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });
|
|
149
160
|
if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);
|
|
@@ -341,6 +352,8 @@ var FirebaseStrategy = class {
|
|
|
341
352
|
// State
|
|
342
353
|
this._knownPeers = /* @__PURE__ */ new Set();
|
|
343
354
|
this._lobbyAnnounceTimer = null;
|
|
355
|
+
this._lastAnnouncement = null;
|
|
356
|
+
this._lobbyWired = false;
|
|
344
357
|
this._destroyed = false;
|
|
345
358
|
this.selfId = generatePeerId();
|
|
346
359
|
this._appId = appId;
|
|
@@ -429,6 +442,12 @@ var FirebaseStrategy = class {
|
|
|
429
442
|
}
|
|
430
443
|
subscribeToLobby(cb) {
|
|
431
444
|
this._onLobby.push(cb);
|
|
445
|
+
if (this._lobbyWired) {
|
|
446
|
+
return () => {
|
|
447
|
+
removeFromArray(this._onLobby, cb);
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
this._lobbyWired = true;
|
|
432
451
|
this._ensureInit().then(() => {
|
|
433
452
|
if (!this._db || !this._fb || this._destroyed) return;
|
|
434
453
|
const { ref, onValue } = this._fb;
|
|
@@ -456,14 +475,25 @@ var FirebaseStrategy = class {
|
|
|
456
475
|
if (!this._db || !this._fb) return;
|
|
457
476
|
const { ref, set } = this._fb;
|
|
458
477
|
const paths = firebasePaths(this._appId, announcement.roomId, "");
|
|
478
|
+
this._lastAnnouncement = announcement;
|
|
459
479
|
announcement.lastSeen = Date.now();
|
|
460
|
-
set(ref(this._db, paths.roomLobbyEntry), announcement);
|
|
480
|
+
set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));
|
|
461
481
|
if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);
|
|
462
482
|
this._lobbyAnnounceTimer = setInterval(() => {
|
|
463
483
|
announcement.lastSeen = Date.now();
|
|
464
|
-
set(ref(this._db, paths.roomLobbyEntry), announcement);
|
|
484
|
+
set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));
|
|
465
485
|
}, ROOM_ANNOUNCE_INTERVAL_MS);
|
|
466
486
|
}
|
|
487
|
+
updateRoomOccupancy(roomId, playerCount, state) {
|
|
488
|
+
const ann = this._lastAnnouncement;
|
|
489
|
+
if (!ann || ann.roomId !== roomId || !this._db || !this._fb) return;
|
|
490
|
+
ann.playerCount = playerCount;
|
|
491
|
+
if (state) ann.state = state;
|
|
492
|
+
ann.lastSeen = Date.now();
|
|
493
|
+
const { ref, set } = this._fb;
|
|
494
|
+
const paths = firebasePaths(this._appId, roomId, "");
|
|
495
|
+
set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(ann));
|
|
496
|
+
}
|
|
467
497
|
removeRoomAnnouncement(roomId) {
|
|
468
498
|
if (!this._db || !this._fb) return;
|
|
469
499
|
const { ref, remove } = this._fb;
|
|
@@ -566,6 +596,7 @@ function sanitizeForFirebase(obj) {
|
|
|
566
596
|
if (typeof obj === "object" && obj !== null) {
|
|
567
597
|
const result = {};
|
|
568
598
|
for (const [key, value] of Object.entries(obj)) {
|
|
599
|
+
if (value === void 0) continue;
|
|
569
600
|
result[key] = value === null ? "__null__" : sanitizeForFirebase(value);
|
|
570
601
|
}
|
|
571
602
|
return result;
|
|
@@ -578,4 +609,4 @@ export {
|
|
|
578
609
|
MqttStrategy,
|
|
579
610
|
FirebaseStrategy
|
|
580
611
|
};
|
|
581
|
-
//# sourceMappingURL=chunk-
|
|
612
|
+
//# sourceMappingURL=chunk-CBTAOVXP.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transport/strategy/utils.ts","../src/transport/strategy/mqtt.ts","../src/transport/strategy/firebase.ts"],"sourcesContent":["/** Generate a random peer ID (20 chars, URL-safe) */\nexport function generatePeerId(): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n const bytes = new Uint8Array(20);\n crypto.getRandomValues(bytes);\n let id = '';\n for (let i = 0; i < 20; i++) {\n id += chars[bytes[i] % chars.length];\n }\n return id;\n}\n\n/** Build MQTT topic paths for a given appId + roomId */\nexport function mqttTopics(appId: string, roomId: string, peerId?: string) {\n const base = `carver/${appId}`;\n return {\n /** Lobby wildcard: subscribe to discover all room announcements */\n lobbyWildcard: `${base}/lobby/+`,\n /** Single room lobby entry */\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n /** Wildcard for all peer presence in a room */\n roomPresenceWildcard: `${base}/room/${roomId}/presence/+`,\n /** This peer's presence topic */\n peerPresence: peerId ? `${base}/room/${roomId}/presence/${peerId}` : '',\n /** This peer's signal inbox */\n peerSignalInbox: peerId ? `${base}/room/${roomId}/signal/${peerId}` : '',\n };\n}\n\n/** Build Firebase RTDB paths for a given appId + roomId */\nexport function firebasePaths(appId: string, roomId: string, peerId?: string) {\n const base = `${appId}/__carver__`;\n return {\n lobby: `${base}/lobby`,\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n peers: `${base}/rooms/${roomId}/peers`,\n peerPresence: peerId ? `${base}/rooms/${roomId}/peers/${peerId}` : '',\n peerSignalInbox: peerId ? `${base}/rooms/${roomId}/signals/${peerId}` : '',\n };\n}\n\n/** Default MQTT brokers (WebSocket endpoints, free/public) */\nexport const DEFAULT_MQTT_BROKERS = [\n 'wss://broker.emqx.io:8084/mqtt',\n 'wss://test.mosquitto.org:8081/mqtt',\n];\n\n/** Room announcement expiry time (30s without heartbeat = stale) */\nexport const ROOM_ANNOUNCE_EXPIRY_MS = 30_000;\n\n/** Room announcement heartbeat interval */\nexport const ROOM_ANNOUNCE_INTERVAL_MS = 10_000;\n\n/** Peer presence heartbeat interval */\nexport const PRESENCE_HEARTBEAT_MS = 5_000;\n\n/** Peer expiry: 3 missed heartbeats */\nexport const PEER_EXPIRY_MS = PRESENCE_HEARTBEAT_MS * 3;\n\n/** Rapid warmup announces for faster initial peer discovery */\nexport const PRESENCE_WARMUP_DELAYS_MS = [200, 500, 1500];\n\n/** Remove an item from an array by reference. Returns true if found. */\nexport function removeFromArray<T>(arr: T[], item: T): boolean {\n const idx = arr.indexOf(item);\n if (idx >= 0) {\n arr.splice(idx, 1);\n return true;\n }\n return false;\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n MqttStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n mqttTopics,\n removeFromArray,\n DEFAULT_MQTT_BROKERS,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n PRESENCE_HEARTBEAT_MS,\n PEER_EXPIRY_MS,\n PRESENCE_WARMUP_DELAYS_MS,\n} from \"./utils\";\n\n// mqtt.MqttClient type (avoid hard import at module level)\ntype MqttClient = {\n on(event: string, cb: (...args: any[]) => void): void;\n subscribe(topic: string | string[], opts: Record<string, unknown>, cb?: (err: Error | null) => void): void;\n unsubscribe(topic: string | string[]): void;\n publish(topic: string, payload: string | Buffer, opts?: Record<string, unknown>): void;\n end(force?: boolean): void;\n};\n\n/**\n * MQTT-based signaling strategy.\n *\n * Connects to public MQTT brokers over WebSocket. Peer discovery uses\n * retained presence messages with periodic heartbeats. SDP/ICE relay\n * uses per-peer signal topics. Room discovery uses retained lobby topics.\n *\n * Zero infrastructure cost -- uses free public brokers by default.\n */\nexport class MqttStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: MqttStrategyConfig;\n private _client: MqttClient | null = null;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Map<string, { meta: PeerMetadata; lastSeen: number }>();\n private _lobbyRooms = new Map<string, RoomAnnouncement>();\n private _presenceTimer: ReturnType<typeof setInterval> | null = null;\n private _warmupTimers: ReturnType<typeof setTimeout>[] = [];\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _peerExpiryTimer: ReturnType<typeof setInterval> | null = null;\n private _lobbySubscribed = false;\n private _destroyed = false;\n\n constructor(appId: string, config: MqttStrategyConfig = { type: 'mqtt' }) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._client) throw new Error('MQTT client not available');\n\n this._joinGeneration++;\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n\n const topics = mqttTopics(this._appId, roomId, this.selfId);\n\n // Subscribe to room presence (discover peers) and own signal inbox\n await new Promise<void>((resolve, reject) => {\n this._client!.subscribe(\n [topics.roomPresenceWildcard, topics.peerSignalInbox],\n { qos: 1 },\n (err: Error | null) => (err ? reject(err) : resolve()),\n );\n });\n\n // Publish retained presence\n this._publishPresence();\n\n // Rapid warmup announces for fast peer discovery\n for (const delay of PRESENCE_WARMUP_DELAYS_MS) {\n this._warmupTimers.push(setTimeout(() => this._publishPresence(), delay));\n }\n\n // Periodic heartbeat\n this._presenceTimer = setInterval(() => this._publishPresence(), PRESENCE_HEARTBEAT_MS);\n\n // Periodic peer expiry check\n this._peerExpiryTimer = setInterval(() => this._checkPeerExpiry(), PRESENCE_HEARTBEAT_MS);\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._client || !this._roomId) return;\n\n const generation = this._joinGeneration;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._clearRoomTimers();\n\n // Clear retained presence\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n\n // Unsubscribe from room topics\n this._client.unsubscribe([topics.roomPresenceWildcard, topics.peerSignalInbox]);\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._client || !this._roomId) return;\n const targetTopic = `carver/${this._appId}/room/${this._roomId}/signal/${targetPeerId}`;\n this._client.publish(\n targetTopic,\n JSON.stringify({ from: this.selfId, data, ts: Date.now() }),\n { qos: 1 },\n );\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n // Subscribe to lobby topic (lazy -- waits for init)\n if (!this._lobbySubscribed) {\n this._lobbySubscribed = true;\n this._ensureInit().then(() => {\n if (this._client && !this._destroyed) {\n const lobbyTopic = mqttTopics(this._appId, '', '').lobbyWildcard;\n this._client.subscribe(lobbyTopic, { qos: 0 });\n }\n });\n }\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n private _lastAnnouncement: RoomAnnouncement | null = null;\n\n updateRoomOccupancy(roomId: string, playerCount: number, state?: 'lobby' | 'playing' | 'ended'): void {\n const ann = this._lastAnnouncement;\n if (!ann || ann.roomId !== roomId || !this._client) return;\n ann.playerCount = playerCount;\n if (state) ann.state = state;\n ann.lastSeen = Date.now();\n const topic = mqttTopics(this._appId, roomId, '').roomLobbyEntry;\n this._client.publish(topic, JSON.stringify(ann), { retain: true, qos: 1 });\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, announcement.roomId, '').roomLobbyEntry;\n\n this._lastAnnouncement = announcement;\n announcement.lastSeen = Date.now();\n this._client.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n this._client?.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, roomId, '').roomLobbyEntry;\n this._client.publish(topic, '', { retain: true, qos: 1 });\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n this._clearRoomTimers();\n\n // Clean up retained presence\n if (this._client && this._roomId) {\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n }\n\n this._client?.end(true);\n this._client = null;\n this._knownPeers.clear();\n this._lobbyRooms.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const mqtt = await import('mqtt');\n const brokers = this._config.brokerUrls ?? DEFAULT_MQTT_BROKERS;\n // Pick a random broker from the available pool\n const brokerUrl = brokers[Math.floor(Math.random() * brokers.length)];\n\n return new Promise<void>((resolve, reject) => {\n const connectFn = mqtt.default?.connect ?? mqtt.connect;\n this._client = connectFn(brokerUrl, {\n clientId: `carver_${this.selfId}`,\n clean: true,\n connectTimeout: 10_000,\n keepalive: 30,\n }) as MqttClient;\n\n this._client.on('connect', () => {\n if (!this._destroyed) resolve();\n });\n\n this._client.on('error', (err: Error) => {\n if (!this._initPromise) return; // already resolved\n reject(err);\n });\n\n this._client.on('message', (topic: string, payload: Buffer) => {\n this._handleMessage(topic, payload);\n });\n });\n }\n\n private _publishPresence(): void {\n if (!this._client || !this._roomId) return;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(\n topics.peerPresence,\n JSON.stringify({ peerId: this.selfId, meta: this._peerMeta, ts: Date.now() }),\n { retain: true, qos: 1 },\n );\n }\n\n private _handleMessage(topic: string, payload: Buffer): void {\n const raw = payload.toString();\n\n // ── Presence message ──\n const presenceMatch = topic.match(/\\/room\\/[^/]+\\/presence\\/([^/]+)$/);\n if (presenceMatch) {\n const peerId = presenceMatch[1];\n if (peerId === this.selfId) return;\n\n if (!raw) {\n // Empty retained = peer left\n if (this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n return;\n }\n try {\n const msg = JSON.parse(raw);\n const isNew = !this._knownPeers.has(peerId);\n this._knownPeers.set(peerId, { meta: msg.meta ?? {}, lastSeen: msg.ts ?? Date.now() });\n if (isNew) {\n for (const cb of this._onPeerDiscovered) cb(peerId, msg.meta ?? {});\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Signal message (SDP / ICE) ──\n const signalMatch = topic.match(/\\/room\\/[^/]+\\/signal\\/([^/]+)$/);\n if (signalMatch) {\n try {\n const msg = JSON.parse(raw);\n if (msg.from && msg.from !== this.selfId) {\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Lobby announcement ──\n const lobbyMatch = topic.match(/\\/lobby\\/([^/]+)$/);\n if (lobbyMatch) {\n const roomId = lobbyMatch[1];\n if (!raw) {\n this._lobbyRooms.delete(roomId);\n } else {\n try {\n const ann = JSON.parse(raw) as RoomAnnouncement;\n if (Date.now() - ann.lastSeen < ROOM_ANNOUNCE_EXPIRY_MS) {\n this._lobbyRooms.set(roomId, ann);\n } else {\n this._lobbyRooms.delete(roomId);\n }\n } catch { /* ignore */ }\n }\n const rooms = Array.from(this._lobbyRooms.values());\n for (const cb of this._onLobby) cb(rooms);\n }\n }\n\n private _checkPeerExpiry(): void {\n const now = Date.now();\n for (const [peerId, data] of this._knownPeers) {\n if (now - data.lastSeen > PEER_EXPIRY_MS) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n }\n }\n\n private _clearRoomTimers(): void {\n if (this._presenceTimer) { clearInterval(this._presenceTimer); this._presenceTimer = null; }\n for (const t of this._warmupTimers) clearTimeout(t);\n this._warmupTimers = [];\n if (this._lobbyAnnounceTimer) { clearInterval(this._lobbyAnnounceTimer); this._lobbyAnnounceTimer = null; }\n if (this._peerExpiryTimer) { clearInterval(this._peerExpiryTimer); this._peerExpiryTimer = null; }\n }\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n FirebaseStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n firebasePaths,\n removeFromArray,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n} from \"./utils\";\n\n/**\n * Firebase Realtime Database signaling strategy.\n *\n * Requires the `firebase` package as a peer dependency.\n * Pass either a `databaseURL` (auto-creates a namespaced Firebase app)\n * or an existing `firebaseApp` instance.\n *\n * Presence cleanup is automatic via Firebase onDisconnect().\n */\nexport class FirebaseStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: FirebaseStrategyConfig;\n private _db: any = null;\n private _firebaseApp: any = null;\n private _ownApp = false;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Firebase module references (filled after dynamic import)\n private _fb: {\n ref: any;\n set: any;\n push: any;\n remove: any;\n onValue: any;\n onChildAdded: any;\n onChildRemoved: any;\n onDisconnect: any;\n } | null = null;\n\n // Unsubscribe handles for Firebase listeners\n private _listeners: (() => void)[] = [];\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Set<string>();\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _lastAnnouncement: RoomAnnouncement | null = null;\n private _lobbyWired = false;\n private _destroyed = false;\n\n constructor(appId: string, config: FirebaseStrategyConfig) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._db || !this._fb) throw new Error('Firebase not initialized');\n\n // Bump generation so any in-flight leaveRoom from a prior call won't\n // null out _roomId after we set it here (React StrictMode race fix).\n this._joinGeneration++;\n\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n const { ref, set, onChildAdded, onChildRemoved, onDisconnect, remove } = this._fb;\n const paths = firebasePaths(this._appId, roomId, this.selfId);\n\n // Clean stale signals from our inbox before listening (prevents\n // replaying SDP from a previous session that wasn't cleaned up).\n await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n\n // 1. Write presence with auto-cleanup on disconnect\n const presenceRef = ref(this._db, paths.peerPresence);\n await set(presenceRef, {\n peerId: this.selfId,\n meta: peerMeta,\n ts: Date.now(),\n });\n onDisconnect(presenceRef).remove();\n\n // 2. Listen for peers joining\n const peersRef = ref(this._db, paths.peers);\n const addedUnsub = onChildAdded(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data || data.peerId === this.selfId) return;\n if (!this._knownPeers.has(data.peerId)) {\n this._knownPeers.add(data.peerId);\n for (const cb of this._onPeerDiscovered) cb(data.peerId, data.meta ?? {});\n }\n });\n this._listeners.push(() => addedUnsub());\n\n // 3. Listen for peers leaving\n const removedUnsub = onChildRemoved(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n const peerId = data?.peerId ?? snapshot.key;\n if (peerId && this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n });\n this._listeners.push(() => removedUnsub());\n\n // 4. Listen for signals addressed to us\n const signalRef = ref(this._db, paths.peerSignalInbox);\n const signalUnsub = onChildAdded(signalRef, (snapshot: any) => {\n const msg = snapshot.val();\n if (!msg || msg.from === this.selfId) return;\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n // Remove processed signal to keep the inbox clean\n remove(snapshot.ref);\n });\n this._listeners.push(() => signalUnsub());\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._db || !this._fb || !this._roomId) return;\n\n // Capture current state so async cleanup targets the correct room\n // even if joinRoom() is called concurrently (React StrictMode).\n const leavingRoomId = this._roomId;\n const generation = this._joinGeneration;\n\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, leavingRoomId, this.selfId);\n\n // Detach listeners\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n // Remove presence and signal inbox\n await Promise.all([\n remove(ref(this._db, paths.peerPresence)),\n remove(ref(this._db, paths.peerSignalInbox)),\n ]).catch(() => {});\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n // This prevents the StrictMode race: old leaveRoom completing after\n // new joinRoom already set _roomId to the fresh value.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._db || !this._fb || !this._roomId) return;\n const { ref, push } = this._fb;\n\n // Atomic push: single operation writes the key + data together.\n // Avoids the push() + set() two-step that can cause onChildAdded to\n // fire with null if the listener catches the intermediate state.\n const inboxPath = firebasePaths(this._appId, this._roomId, targetPeerId).peerSignalInbox;\n push(ref(this._db, inboxPath), {\n from: this.selfId,\n data: sanitizeForFirebase(data),\n ts: Date.now(),\n });\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n // Wire the underlying RTDB listener exactly once — repeated subscribe/\n // unsubscribe cycles (React StrictMode, route changes) must not stack\n // duplicate onValue listeners.\n if (this._lobbyWired) {\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n this._lobbyWired = true;\n\n this._ensureInit().then(() => {\n if (!this._db || !this._fb || this._destroyed) return;\n const { ref, onValue } = this._fb;\n const paths = firebasePaths(this._appId, '', '');\n const lobbyRef = ref(this._db, paths.lobby);\n\n const unsub = onValue(lobbyRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data) {\n for (const lcb of this._onLobby) lcb([]);\n return;\n }\n const now = Date.now();\n const rooms: RoomAnnouncement[] = Object.values(data).filter(\n (r: any) => r && now - (r.lastSeen ?? 0) < ROOM_ANNOUNCE_EXPIRY_MS,\n ) as RoomAnnouncement[];\n for (const lcb of this._onLobby) lcb(rooms);\n });\n this._listeners.push(() => unsub());\n });\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._db || !this._fb) return;\n const { ref, set } = this._fb;\n const paths = firebasePaths(this._appId, announcement.roomId, '');\n\n this._lastAnnouncement = announcement;\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n updateRoomOccupancy(roomId: string, playerCount: number, state?: 'lobby' | 'playing' | 'ended'): void {\n const ann = this._lastAnnouncement;\n if (!ann || ann.roomId !== roomId || !this._db || !this._fb) return;\n ann.playerCount = playerCount;\n if (state) ann.state = state;\n ann.lastSeen = Date.now();\n const { ref, set } = this._fb;\n const paths = firebasePaths(this._appId, roomId, '');\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(ann));\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._db || !this._fb) return;\n const { ref, remove } = this._fb;\n remove(ref(this._db, firebasePaths(this._appId, roomId, '').roomLobbyEntry));\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n // Best-effort cleanup\n if (this._db && this._fb && this._roomId) {\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, this._roomId, this.selfId);\n remove(ref(this._db, paths.peerPresence)).catch(() => {});\n remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n }\n\n // Delete own Firebase app if we created it\n if (this._ownApp && this._firebaseApp) {\n import('firebase/app').then(({ deleteApp }) => {\n deleteApp(this._firebaseApp).catch(() => {});\n });\n }\n\n this._db = null;\n this._firebaseApp = null;\n this._fb = null;\n this._knownPeers.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const { initializeApp, getApps } = await import('firebase/app');\n const {\n getDatabase,\n ref,\n set,\n push,\n remove,\n onValue,\n onChildAdded,\n onChildRemoved,\n onDisconnect,\n } = await import('firebase/database');\n\n this._fb = { ref, set, push, remove, onValue, onChildAdded, onChildRemoved, onDisconnect };\n\n if (this._config.firebaseApp) {\n this._firebaseApp = this._config.firebaseApp;\n this._ownApp = false;\n } else {\n const appName = `carver_${this._appId}`;\n const existing = getApps().find((a: any) => a.name === appName);\n if (existing) {\n this._firebaseApp = existing;\n this._ownApp = false;\n } else {\n this._firebaseApp = initializeApp({ databaseURL: this._config.databaseURL }, appName);\n this._ownApp = true;\n }\n }\n\n this._db = getDatabase(this._firebaseApp);\n }\n}\n\n/**\n * Firebase RTDB deletes any key whose value is `null` (treats null as \"remove\").\n * ICE candidates from `toJSON()` can contain `null` fields (e.g. usernameFragment).\n * We recursively replace `null` with a sentinel so Firebase preserves the key.\n * On the receiving end, `_handleSignal` doesn't need to reverse this because\n * `new RTCIceCandidate()` / `new RTCSessionDescription()` accept missing fields.\n */\nfunction sanitizeForFirebase(obj: unknown): unknown {\n if (obj === null) return '__null__';\n if (Array.isArray(obj)) return obj.map(sanitizeForFirebase);\n if (typeof obj === 'object' && obj !== null) {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (value === undefined) continue; // RTDB rejects undefined anywhere in the payload\n result[key] = value === null ? '__null__' : sanitizeForFirebase(value);\n }\n return result;\n }\n return obj;\n}\n"],"mappings":"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","names":[]}
|