openrtc-netcode 3.0.1 → 3.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/state.d.ts CHANGED
@@ -1,16 +1,29 @@
1
+ import type { StateSnapshotBody } from './protocol.js';
1
2
  import type { NetVar, NetVarOptions, NetcodeState } from './types.js';
2
3
  export declare class ReplicatedState implements NetcodeState {
3
4
  private readonly resolveOwner;
4
5
  private readonly publishPatch;
6
+ private readonly capacity;
7
+ private readonly canWrite;
8
+ private readonly awaitsInitialHost;
5
9
  private readonly entries;
6
- constructor(resolveOwner: (options?: NetVarOptions) => string, publishPatch: (key: string, value: unknown, owner: string) => Promise<void>);
10
+ private readonly subscriptions;
11
+ private bytes;
12
+ constructor(resolveOwner: (options?: NetVarOptions) => string, publishPatch: (key: string, value: unknown, owner: string, revision: number) => Promise<void>, capacity?: number, canWrite?: (owner: string) => boolean, awaitsInitialHost?: (options?: NetVarOptions) => boolean);
7
13
  define<T>(key: string, initial: T, options?: NetVarOptions): NetVar<T>;
8
14
  get<T = unknown>(key: string): T | undefined;
9
15
  snapshot(): Record<string, unknown>;
10
16
  owners(): Record<string, string>;
17
+ revisions(): Record<string, number>;
18
+ /** Watches a key across match resets and replacement owner incarnations. */
19
+ subscribe(key: string, listener: () => void): () => void;
20
+ clear(): void;
21
+ removeOwner(owner: string): void;
11
22
  replaceOwner(previousOwner: string, nextOwner: string): void;
12
- applySnapshot(values: Record<string, unknown>, owners: Record<string, string>): void;
13
- applyPatch(key: string, value: unknown, owner: string): void;
14
- private applyRemoteValue;
23
+ /** Bind only declarations made before the first authoritative room roster. */
24
+ bindInitialHost(owner: string, localPeerId: string): void;
25
+ applySnapshot(values: Record<string, unknown>, owners: Record<string, string>, revisions: Record<string, number>, from: string): StateSnapshotBody;
26
+ applyPatch(key: string, value: unknown, owner: string, revision: number, from: string): boolean;
27
+ private notify;
15
28
  private createNetVar;
16
29
  }
package/dist/state.js CHANGED
@@ -1,89 +1,200 @@
1
+ import { canonicalJson, cloneJson, isCounter, isId, jsonByteLength, MAX_VALUE_BYTES } from './validation.js';
2
+ const MAX_STATE_BYTES = 4 * 1024 * 1024;
1
3
  export class ReplicatedState {
2
- constructor(resolveOwner, publishPatch) {
4
+ constructor(resolveOwner, publishPatch, capacity = 4096, canWrite = (owner) => owner === this.resolveOwner(), awaitsInitialHost = () => false) {
3
5
  this.resolveOwner = resolveOwner;
4
6
  this.publishPatch = publishPatch;
7
+ this.capacity = capacity;
8
+ this.canWrite = canWrite;
9
+ this.awaitsInitialHost = awaitsInitialHost;
5
10
  this.entries = new Map();
11
+ this.subscriptions = new Map();
12
+ this.bytes = 0;
6
13
  }
7
14
  define(key, initial, options) {
15
+ if (!isId(key))
16
+ throw new Error('Invalid NetVar key.');
8
17
  const existing = this.entries.get(key);
9
- if (existing) {
18
+ if (existing)
10
19
  return this.createNetVar(existing);
11
- }
20
+ if (this.entries.size >= this.capacity)
21
+ throw new Error('NetVar capacity exceeded.');
22
+ const owner = this.resolveOwner(options);
23
+ if (!isId(owner))
24
+ throw new Error('Invalid NetVar owner.');
25
+ const value = cloneJson(initial, MAX_VALUE_BYTES);
26
+ const revision = owner === 'local' || this.canWrite(owner) ? 1 : 0;
27
+ const bytes = jsonByteLength([key, value, owner, revision]);
28
+ if (this.bytes + bytes > MAX_STATE_BYTES)
29
+ throw new Error('NetVar byte capacity exceeded.');
12
30
  const entry = {
13
- key,
14
- value: initial,
15
- owner: this.resolveOwner(options),
16
- listeners: new Set(),
31
+ key, value, owner, bytes,
32
+ // A remote declaration is a display placeholder until its owner
33
+ // publishes. It must not compete with that owner's first version.
34
+ revision, awaitingHost: this.awaitsInitialHost(options), listeners: new Set(),
17
35
  };
36
+ this.bytes += bytes;
18
37
  this.entries.set(key, entry);
38
+ this.notify(key);
19
39
  return this.createNetVar(entry);
20
40
  }
21
41
  get(key) {
22
- return this.entries.get(key)?.value;
42
+ const entry = this.entries.get(key);
43
+ return entry ? cloneJson(entry.value) : undefined;
23
44
  }
24
45
  snapshot() {
25
- const values = {};
26
- for (const [key, entry] of this.entries) {
27
- values[key] = entry.value;
28
- }
29
- return values;
46
+ return Object.fromEntries(Array.from(this.entries, ([key, entry]) => [key, cloneJson(entry.value)]));
30
47
  }
31
48
  owners() {
32
- const values = {};
33
- for (const [key, entry] of this.entries) {
34
- values[key] = entry.owner;
35
- }
36
- return values;
49
+ return Object.fromEntries(Array.from(this.entries, ([key, entry]) => [key, entry.owner]));
37
50
  }
38
- replaceOwner(previousOwner, nextOwner) {
39
- for (const entry of this.entries.values()) {
40
- if (entry.owner === previousOwner) {
41
- entry.owner = nextOwner;
51
+ revisions() {
52
+ return Object.fromEntries(Array.from(this.entries, ([key, entry]) => [key, entry.revision]));
53
+ }
54
+ /** Watches a key across match resets and replacement owner incarnations. */
55
+ subscribe(key, listener) {
56
+ if (!isId(key))
57
+ throw new Error('Invalid NetVar key.');
58
+ const listeners = this.subscriptions.get(key) ?? new Set();
59
+ listeners.add(listener);
60
+ this.subscriptions.set(key, listeners);
61
+ return () => {
62
+ listeners.delete(listener);
63
+ if (!listeners.size)
64
+ this.subscriptions.delete(key);
65
+ };
66
+ }
67
+ clear() {
68
+ const keys = [...this.entries.keys()];
69
+ this.entries.clear();
70
+ this.bytes = 0;
71
+ for (const key of keys)
72
+ this.notify(key);
73
+ }
74
+ removeOwner(owner) {
75
+ for (const [key, entry] of this.entries)
76
+ if (entry.owner === owner) {
77
+ this.entries.delete(key);
78
+ this.bytes -= entry.bytes;
79
+ this.notify(key);
42
80
  }
81
+ }
82
+ replaceOwner(previousOwner, nextOwner) {
83
+ if (!isId(nextOwner))
84
+ throw new Error('Invalid NetVar owner.');
85
+ const replacements = Array.from(this.entries.values()).filter((entry) => entry.owner === previousOwner)
86
+ .map((entry) => ({ entry, bytes: jsonByteLength([entry.key, entry.value, nextOwner, entry.revision]) }));
87
+ const total = replacements.reduce((sum, replacement) => sum + replacement.bytes - replacement.entry.bytes, this.bytes);
88
+ if (total > MAX_STATE_BYTES)
89
+ throw new Error('NetVar byte capacity exceeded.');
90
+ for (const { entry, bytes } of replacements) {
91
+ entry.owner = nextOwner;
92
+ entry.bytes = bytes;
43
93
  }
94
+ this.bytes = total;
44
95
  }
45
- applySnapshot(values, owners) {
46
- for (const [key, value] of Object.entries(values)) {
47
- this.applyRemoteValue(key, value, owners[key] ?? this.resolveOwner());
96
+ /** Bind only declarations made before the first authoritative room roster. */
97
+ bindInitialHost(owner, localPeerId) {
98
+ if (!isId(owner) || !isId(localPeerId))
99
+ throw new Error('Invalid NetVar owner.');
100
+ const replacements = Array.from(this.entries.values()).filter((entry) => entry.awaitingHost)
101
+ .map((entry) => {
102
+ const revision = owner === localPeerId ? Math.max(1, entry.revision) : 0;
103
+ return { entry, revision, bytes: jsonByteLength([entry.key, entry.value, owner, revision]) };
104
+ });
105
+ const total = replacements.reduce((sum, replacement) => sum + replacement.bytes - replacement.entry.bytes, this.bytes);
106
+ if (total > MAX_STATE_BYTES)
107
+ throw new Error('NetVar byte capacity exceeded.');
108
+ for (const { entry, revision, bytes } of replacements) {
109
+ entry.owner = owner;
110
+ entry.revision = revision;
111
+ entry.bytes = bytes;
112
+ entry.awaitingHost = false;
48
113
  }
114
+ this.bytes = total;
115
+ for (const { entry } of replacements)
116
+ this.notify(entry.key);
49
117
  }
50
- applyPatch(key, value, owner) {
51
- this.applyRemoteValue(key, value, owner);
118
+ applySnapshot(values, owners, revisions, from) {
119
+ const accepted = { values: Object.create(null), owners: Object.create(null), revisions: Object.create(null) };
120
+ for (const [key, value] of Object.entries(values))
121
+ if (this.applyPatch(key, value, owners[key], revisions[key], from)) {
122
+ accepted.values[key] = this.get(key);
123
+ accepted.owners[key] = owners[key];
124
+ accepted.revisions[key] = revisions[key];
125
+ }
126
+ return accepted;
52
127
  }
53
- applyRemoteValue(key, value, owner) {
128
+ applyPatch(key, value, owner, revision, from) {
129
+ if (!isId(key) || !isId(owner) || owner !== from || !isCounter(revision) || revision === 0)
130
+ return false;
54
131
  const existing = this.entries.get(key);
132
+ if (existing && (existing.owner !== from || existing.revision > revision))
133
+ return false;
134
+ if (!existing && this.entries.size >= this.capacity)
135
+ return false;
136
+ let next;
137
+ try {
138
+ next = cloneJson(value, MAX_VALUE_BYTES);
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ if (existing?.revision === revision && canonicalJson(existing.value) >= canonicalJson(next))
144
+ return false;
145
+ let bytes;
146
+ try {
147
+ bytes = jsonByteLength([key, next, owner, revision]);
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ if (this.bytes - (existing?.bytes ?? 0) + bytes > MAX_STATE_BYTES)
153
+ return false;
154
+ this.bytes += bytes - (existing?.bytes ?? 0);
55
155
  if (!existing) {
56
- this.entries.set(key, {
57
- key,
58
- value,
59
- owner,
60
- listeners: new Set(),
61
- });
62
- return;
156
+ this.entries.set(key, { key, value: next, owner, revision, bytes, listeners: new Set() });
63
157
  }
64
- const previous = existing.value;
65
- existing.value = value;
66
- existing.owner = owner;
67
- for (const listener of existing.listeners) {
68
- listener(value, previous);
158
+ else {
159
+ const previous = existing.value;
160
+ existing.value = next;
161
+ existing.revision = revision;
162
+ existing.bytes = bytes;
163
+ for (const listener of existing.listeners)
164
+ listener(cloneJson(next), cloneJson(previous));
69
165
  }
166
+ this.notify(key);
167
+ return true;
168
+ }
169
+ notify(key) {
170
+ for (const listener of this.subscriptions.get(key) ?? [])
171
+ listener();
70
172
  }
71
173
  createNetVar(entry) {
72
174
  return {
73
175
  key: entry.key,
74
- get owner() {
75
- return entry.owner;
76
- },
77
- get value() {
78
- return entry.value;
79
- },
176
+ get owner() { return entry.owner; },
177
+ get value() { return cloneJson(entry.value); },
80
178
  set: async (value) => {
179
+ if (this.entries.get(entry.key) !== entry)
180
+ throw new Error('NetVar belongs to a retired match.');
181
+ if (!this.canWrite(entry.owner))
182
+ throw new Error('NetVar writes require the local owner.');
183
+ if (!isCounter(entry.revision + 1))
184
+ throw new Error('NetVar revision exhausted.');
185
+ const next = cloneJson(value, MAX_VALUE_BYTES);
186
+ const bytes = jsonByteLength([entry.key, next, entry.owner, entry.revision + 1]);
187
+ if (this.bytes - entry.bytes + bytes > MAX_STATE_BYTES)
188
+ throw new Error('NetVar byte capacity exceeded.');
81
189
  const previous = entry.value;
82
- entry.value = value;
83
- for (const listener of entry.listeners) {
84
- listener(value, previous);
85
- }
86
- await this.publishPatch(entry.key, value, entry.owner);
190
+ this.bytes += bytes - entry.bytes;
191
+ entry.value = next;
192
+ entry.bytes = bytes;
193
+ entry.revision++;
194
+ for (const listener of entry.listeners)
195
+ listener(cloneJson(entry.value), cloneJson(previous));
196
+ this.notify(entry.key);
197
+ await this.publishPatch(entry.key, cloneJson(entry.value), entry.owner, entry.revision);
87
198
  },
88
199
  onChange: (callback) => {
89
200
  entry.listeners.add(callback);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { ReadinessOptions as OpenRtcApplicationPayloadReadinessOptions, IncomingStream, ScopedChannelOptions, ScopedLogicalChannel } from 'openrtc/runtime';
2
2
  import type { Room } from 'openrtc';
3
+ import type { ActionHandler } from './actions.js';
4
+ import type { SimulationClock } from './simulation.js';
5
+ import type { MotionThresholds } from './motion.js';
6
+ import type { ReplicationOptions, ReplicationStats } from './replication.js';
3
7
  import type { MetadataBody, NetcodeEnvelope, ObjectRemoveBody, ObjectSnapshotBody, ObjectUpsertBody, StatePatchBody, StateSnapshotBody, UserMessageBody } from './protocol.js';
4
8
  export interface NetcodeRuntime {
5
9
  /**
@@ -16,7 +20,11 @@ export interface NetcodeRuntime {
16
20
  initialize?(): Promise<void> | void;
17
21
  start?(): Promise<void> | void;
18
22
  stop?(): Promise<void> | void;
23
+ /** Releases only adapter-owned observers; never stops a borrowed runtime. */
24
+ dispose?(): Promise<void> | void;
19
25
  getNodeId(): Promise<string>;
26
+ /** Synchronous capability fence for a borrowed activated room. */
27
+ isRoomActive?(roomId: string): boolean;
20
28
  createRoom(roomId?: string): Promise<string>;
21
29
  joinRoom(roomId: string, options?: {
22
30
  bootstrapPeers?: boolean;
@@ -38,7 +46,17 @@ export interface NetcodeOptions {
38
46
  room?: Room;
39
47
  preferredTransports?: ReadinessOptions['preferredTransports'];
40
48
  displayName?: string;
41
- localFallback?: boolean;
49
+ authority?: {
50
+ mode: 'owner' | 'host';
51
+ hostId?: string;
52
+ hostOwnedKinds?: readonly string[];
53
+ };
54
+ tickRate?: number;
55
+ replication?: ReplicationOptions;
56
+ codec?: 'json' | 'quantized';
57
+ motionThresholds?: MotionThresholds;
58
+ interest?: (peerId: string, object: Readonly<NetcodeObjectState>) => boolean;
59
+ priority?: (peerId: string, object: Readonly<NetcodeObjectState>) => number;
42
60
  }
43
61
  export interface CreateLobbyOptions {
44
62
  id?: string;
@@ -87,6 +105,8 @@ export interface NetcodeState {
87
105
  define<T>(key: string, initial: T, options?: NetVarOptions): NetVar<T>;
88
106
  get<T = unknown>(key: string): T | undefined;
89
107
  snapshot(): Record<string, unknown>;
108
+ /** Observe the current key, including removal and recreation in a later match. */
109
+ subscribe(key: string, listener: () => void): () => void;
90
110
  }
91
111
  export interface NetcodeVector3 {
92
112
  x: number;
@@ -116,9 +136,15 @@ export interface NetcodeObjectState<TMetadata extends Record<string, unknown> =
116
136
  transform: NetcodeTransform;
117
137
  physics?: NetcodePhysicsState;
118
138
  metadata?: TMetadata;
139
+ /** Owner-issued monotonic version. Wall clocks do not determine ordering. */
140
+ revision: number;
141
+ incarnation: string;
142
+ tick: number;
143
+ teleport?: boolean;
144
+ /** Informational sender wall-clock time only. */
119
145
  updatedAt: number;
120
146
  }
121
- export type NetcodeObjectUpsert<TMetadata extends Record<string, unknown> = Record<string, unknown>> = Partial<Omit<NetcodeObjectState<TMetadata>, 'transform'>> & {
147
+ export type NetcodeObjectUpsert<TMetadata extends Record<string, unknown> = Record<string, unknown>> = Partial<Omit<NetcodeObjectState<TMetadata>, 'transform' | 'revision' | 'incarnation'>> & {
122
148
  kind?: string;
123
149
  transform: Partial<NetcodeTransform> & {
124
150
  position: NetcodeVector3;
@@ -138,12 +164,15 @@ export type NetcodeObjectChange = {
138
164
  from: string;
139
165
  };
140
166
  export interface NetcodeObjectStore {
167
+ read(id: string): Readonly<NetcodeObjectState> | undefined;
168
+ ids(kind?: string): readonly string[];
169
+ onStructureChange(listener: () => void): () => void;
141
170
  list(): NetcodeObjectState[];
142
171
  get(id: string): NetcodeObjectState | undefined;
143
172
  snapshot(): Record<string, NetcodeObjectState>;
144
173
  spawn(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
145
174
  upsert(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
146
- update(id: string, patch: Partial<Omit<NetcodeObjectState, 'id'>>): Promise<NetcodeObjectState>;
175
+ update(id: string, patch: Partial<Omit<NetcodeObjectState, 'id' | 'revision' | 'incarnation'>>): Promise<NetcodeObjectState>;
147
176
  remove(id: string): Promise<void>;
148
177
  onChange(listener: (change: NetcodeObjectChange) => void): () => void;
149
178
  }
@@ -181,6 +210,10 @@ export interface NetcodeVoice {
181
210
  on<K extends keyof NetcodeVoiceEvents>(event: K, handler: (payload: NetcodeVoiceEvents[K]) => void): () => void;
182
211
  }
183
212
  export interface NetcodeEvents {
213
+ 'protocol:error': {
214
+ peerId: string;
215
+ reason: string;
216
+ };
184
217
  lobby: NetcodeLobby | null;
185
218
  'peer:joined': NetcodePeer;
186
219
  'peer:left': NetcodePeer;
@@ -217,6 +250,16 @@ export interface NetcodeClient {
217
250
  readonly state: NetcodeState;
218
251
  readonly objects: NetcodeObjectStore;
219
252
  readonly voice: NetcodeVoice;
253
+ readonly clock: SimulationClock;
254
+ handleAction(type: string, handler: ActionHandler): () => void;
255
+ request(peerId: string, type: string, payload: unknown, options?: {
256
+ id?: string;
257
+ timeoutMs?: number;
258
+ }): Promise<unknown>;
259
+ waitForPeer(peerId: string, timeoutMs?: number): Promise<void>;
260
+ refreshInterest(): Promise<void>;
261
+ flushReplication(): void;
262
+ replicationStats(peerId: string): ReplicationStats;
220
263
  start(): Promise<void>;
221
264
  stop(): Promise<void>;
222
265
  createLobby(options?: CreateLobbyOptions): Promise<NetcodeLobby>;
@@ -256,6 +299,10 @@ export type RuntimeMediaTrackEventLike = {
256
299
  readonly streams: readonly MediaStream[];
257
300
  };
258
301
  export type RuntimeMediaConnectionLike = {
302
+ /** Current receiver tracks, supplied by the public OpenRTC media owner. */
303
+ receivers?(): readonly {
304
+ readonly track: MediaStreamTrack;
305
+ }[];
259
306
  addTrack(track: MediaStreamTrack, options?: {
260
307
  streams?: readonly MediaStream[];
261
308
  codec?: string;
@@ -0,0 +1,10 @@
1
+ export declare const MAX_FRAME_BYTES: number;
2
+ export declare const MAX_VALUE_BYTES: number;
3
+ export declare function isCounter(value: unknown): value is number;
4
+ export declare function isId(value: unknown): value is string;
5
+ export declare function boundedJson(value: unknown, maxNodes?: number, maxDepth?: number): boolean;
6
+ /** Check aggregate encoded size before allocating a complete JSON string. */
7
+ export declare function jsonByteLength(value: unknown, limit?: number, maxNodes?: number, maxDepth?: number): number;
8
+ export declare function cloneJson<T>(value: T, limit?: number, maxNodes?: number, maxDepth?: number): T;
9
+ export declare function canonicalJson(value: unknown): string;
10
+ export declare function freezeJson<T>(value: T): T;
@@ -0,0 +1,88 @@
1
+ export const MAX_FRAME_BYTES = 256 * 1024;
2
+ export const MAX_VALUE_BYTES = 240 * 1024;
3
+ const encoder = new TextEncoder();
4
+ export function isCounter(value) {
5
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
6
+ }
7
+ export function isId(value) {
8
+ return typeof value === 'string' && value.length > 0 && value.length <= 256;
9
+ }
10
+ export function boundedJson(value, maxNodes = 20000, maxDepth = 24) {
11
+ let nodes = 0;
12
+ const visit = (item, depth) => {
13
+ if (++nodes > maxNodes || depth > maxDepth)
14
+ return false;
15
+ if (item === null || typeof item === 'boolean')
16
+ return true;
17
+ if (typeof item === 'number')
18
+ return Number.isFinite(item);
19
+ if (typeof item === 'string')
20
+ return item.length <= MAX_FRAME_BYTES;
21
+ if (Array.isArray(item)) {
22
+ for (let index = 0; index < item.length; index++)
23
+ if (!(index in item) || !visit(item[index], depth + 1))
24
+ return false;
25
+ return true;
26
+ }
27
+ if (typeof item !== 'object' || !item)
28
+ return false;
29
+ return Object.entries(item).every(([key, entry]) => key.length <= 256 && (entry === undefined || visit(entry, depth + 1)));
30
+ };
31
+ return visit(value, 0);
32
+ }
33
+ /** Check aggregate encoded size before allocating a complete JSON string. */
34
+ export function jsonByteLength(value, limit = MAX_FRAME_BYTES, maxNodes = 20000, maxDepth = 24) {
35
+ if (!boundedJson(value, maxNodes, maxDepth))
36
+ throw new Error('Netcode value exceeds the JSON schema or resource limits.');
37
+ let bytes = 0;
38
+ const add = (count) => {
39
+ bytes += count;
40
+ if (bytes > limit)
41
+ throw new Error('Netcode value exceeds the byte limit.');
42
+ };
43
+ const visit = (item) => {
44
+ if (Array.isArray(item)) {
45
+ add(2 + Math.max(0, item.length - 1));
46
+ for (const entry of item)
47
+ visit(entry);
48
+ }
49
+ else if (item && typeof item === 'object') {
50
+ add(2);
51
+ let count = 0;
52
+ for (const [key, entry] of Object.entries(item)) {
53
+ if (entry === undefined)
54
+ continue;
55
+ add((count++ ? 1 : 0) + encoder.encode(JSON.stringify(key)).byteLength + 1);
56
+ visit(entry);
57
+ }
58
+ }
59
+ else
60
+ add(encoder.encode(JSON.stringify(item)).byteLength);
61
+ };
62
+ visit(value);
63
+ return bytes;
64
+ }
65
+ export function cloneJson(value, limit = MAX_FRAME_BYTES, maxNodes = 20000, maxDepth = 24) {
66
+ jsonByteLength(value, limit, maxNodes, maxDepth);
67
+ const encoded = JSON.stringify(value);
68
+ if (encoder.encode(encoded).byteLength > limit)
69
+ throw new Error('Netcode value exceeds the byte limit.');
70
+ return JSON.parse(encoded);
71
+ }
72
+ export function canonicalJson(value) {
73
+ if (Array.isArray(value))
74
+ return `[${value.map(canonicalJson).join(',')}]`;
75
+ if (value && typeof value === 'object') {
76
+ const record = value;
77
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
78
+ }
79
+ return JSON.stringify(value);
80
+ }
81
+ export function freezeJson(value) {
82
+ if (value && typeof value === 'object') {
83
+ for (const item of Object.values(value))
84
+ freezeJson(item);
85
+ Object.freeze(value);
86
+ }
87
+ return value;
88
+ }
package/dist/voice.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare class NetcodeVoiceController implements NetcodeVoice {
10
10
  private localMediaStream;
11
11
  private options;
12
12
  private active;
13
+ private generation;
14
+ private starting;
13
15
  private muted;
14
16
  constructor(localPeerId: () => string | null, peers: () => NetcodePeer[], connectionForPeer: (peerId: string) => RuntimeConnectionLike | null);
15
17
  get enabled(): boolean;