@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.
Files changed (42) hide show
  1. package/dist/InputBuffer-J6XT_Tt0.d.mts +61 -0
  2. package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
  3. package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
  4. package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
  5. package/dist/{chunk-UD6FDZMX.mjs → chunk-CBTAOVXP.mjs} +34 -3
  6. package/dist/chunk-CBTAOVXP.mjs.map +1 -0
  7. package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
  8. package/dist/chunk-Q25TJEY4.mjs.map +1 -0
  9. package/dist/{chunk-3KT73N2S.mjs → chunk-UKEFWQ76.mjs} +0 -0
  10. package/dist/chunk-UKEFWQ76.mjs.map +1 -0
  11. package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
  12. package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
  13. package/dist/index.d.mts +27 -6
  14. package/dist/index.d.ts +27 -6
  15. package/dist/index.js +744 -245
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +172 -37
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/strategy.d.mts +2 -2
  20. package/dist/strategy.d.ts +2 -2
  21. package/dist/strategy.js +33 -2
  22. package/dist/strategy.js.map +1 -1
  23. package/dist/strategy.mjs +1 -1
  24. package/dist/sync.d.mts +134 -50
  25. package/dist/sync.d.ts +134 -50
  26. package/dist/sync.js +499 -205
  27. package/dist/sync.js.map +1 -1
  28. package/dist/sync.mjs +15 -3
  29. package/dist/transport.d.mts +0 -0
  30. package/dist/transport.d.ts +0 -0
  31. package/dist/transport.js +0 -0
  32. package/dist/transport.js.map +1 -1
  33. package/dist/transport.mjs +2 -2
  34. package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
  35. package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
  36. package/dist/types.d.mts +2 -2
  37. package/dist/types.d.ts +2 -2
  38. package/dist/types.js.map +1 -1
  39. package/package.json +26 -5
  40. package/dist/chunk-3KT73N2S.mjs.map +0 -1
  41. package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
  42. 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-5LHBOW08.js';
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?: unknown;
165
+ hi?: PlayerInput;
116
166
  }
117
167
  interface InputPacket {
168
+ /** Sender's local tick. */
118
169
  t: number;
119
- i: unknown;
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
- onPhysicsStep?: (inputs: Map<string, unknown>, tick: number, isRollback: boolean) => void;
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 Player as P, type Room as R, SnapshotBuffer as S, type TransportConfig as T, type UseRoomOptions as U, type ChannelOptions as a, type CarverChannel as b, type RoomState as c, Codec as d, TickKeeper as e, type EntityState2D as f, type EntityState3D as g, type EventPacket as h, type SnapshotPacket as i, type SyncMode as j, NetworkManager as k, type ConnectionState as l, type CarverMultiplayerError as m, type UseLobbyOptions as n, type RoomConfig as o, type UseMultiplayerOptions as p, type CarverErrorCode as q };
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-5LHBOW08.mjs';
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?: unknown;
165
+ hi?: PlayerInput;
116
166
  }
117
167
  interface InputPacket {
168
+ /** Sender's local tick. */
118
169
  t: number;
119
- i: unknown;
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
- onPhysicsStep?: (inputs: Map<string, unknown>, tick: number, isRollback: boolean) => void;
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 Player as P, type Room as R, SnapshotBuffer as S, type TransportConfig as T, type UseRoomOptions as U, type ChannelOptions as a, type CarverChannel as b, type RoomState as c, Codec as d, TickKeeper as e, type EntityState2D as f, type EntityState3D as g, type EventPacket as h, type SnapshotPacket as i, type SyncMode as j, NetworkManager as k, type ConnectionState as l, type CarverMultiplayerError as m, type UseLobbyOptions as n, type RoomConfig as o, type UseMultiplayerOptions as p, type CarverErrorCode as q };
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-UD6FDZMX.mjs.map
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":[]}