openrtc-netcode 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -40
- package/dist/actions.d.ts +40 -0
- package/dist/actions.js +269 -0
- package/dist/client.js +1049 -294
- package/dist/codec.d.ts +74 -0
- package/dist/codec.js +167 -0
- package/dist/framing.d.ts +9 -0
- package/dist/framing.js +39 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/latestState.d.ts +4 -1
- package/dist/latestState.js +23 -19
- package/dist/motion.d.ts +9 -0
- package/dist/motion.js +12 -0
- package/dist/objects.d.ts +34 -5
- package/dist/objects.js +276 -38
- package/dist/protocol.d.ts +15 -2
- package/dist/protocol.js +20 -5
- package/dist/replication.d.ts +31 -0
- package/dist/replication.js +135 -0
- package/dist/roomRuntime.js +18 -16
- package/dist/simulation.d.ts +80 -0
- package/dist/simulation.js +269 -0
- package/dist/snapshots.d.ts +12 -0
- package/dist/snapshots.js +71 -0
- package/dist/state.d.ts +17 -4
- package/dist/state.js +162 -51
- package/dist/types.d.ts +50 -3
- package/dist/validation.d.ts +10 -0
- package/dist/validation.js +88 -0
- package/dist/voice.d.ts +2 -0
- package/dist/voice.js +81 -39
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
15
|
-
owner
|
|
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
|
-
|
|
42
|
+
const entry = this.entries.get(key);
|
|
43
|
+
return entry ? cloneJson(entry.value) : undefined;
|
|
23
44
|
}
|
|
24
45
|
snapshot() {
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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;
|