openrtc-netcode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ export declare class TypedEmitter<TEvents extends object> {
2
+ private readonly listeners;
3
+ on<K extends keyof TEvents>(event: K, handler: (payload: TEvents[K]) => void): () => void;
4
+ emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void;
5
+ clear(): void;
6
+ }
package/dist/events.js ADDED
@@ -0,0 +1,21 @@
1
+ export class TypedEmitter {
2
+ constructor() {
3
+ this.listeners = new Map();
4
+ }
5
+ on(event, handler) {
6
+ const listeners = this.listeners.get(event) ?? new Set();
7
+ listeners.add(handler);
8
+ this.listeners.set(event, listeners);
9
+ return () => {
10
+ listeners.delete(handler);
11
+ };
12
+ }
13
+ emit(event, payload) {
14
+ for (const listener of this.listeners.get(event) ?? []) {
15
+ listener(payload);
16
+ }
17
+ }
18
+ clear() {
19
+ this.listeners.clear();
20
+ }
21
+ }
@@ -0,0 +1,5 @@
1
+ export { OpenRtcNetcodeClient, createNetcode, shouldInitiateConnection, } from './client.js';
2
+ export { ReplicatedObjectStore } from './objects.js';
3
+ export { NETCODE_PROTOCOL, NETCODE_PROTOCOL_VERSION, decodeEnvelope, encodeEnvelope, isEnvelopeKind, } from './protocol.js';
4
+ export type { HelloBody, MetadataBody, NetcodeEnvelope, NetcodeEnvelopeKind, ObjectRemoveBody, ObjectSnapshotBody, ObjectUpsertBody, StatePatchBody, StateSnapshotBody, UserMessageBody, VoiceSignalBody, } from './protocol.js';
5
+ export type { CreateLobbyOptions, JoinLobbyOptions, NetVar, NetVarOptions, NetcodeClient, NetcodeEventHandler, NetcodeEventName, NetcodeEvents, NetcodeLobby, NetcodeMessage, NetcodeMessageHandler, NetcodeObjectChange, NetcodeObjectState, NetcodeObjectStore, NetcodeObjectUpsert, NetcodeOptions, NetcodePeer, NetcodePeerStats, NetcodePhysicsState, NetcodeQuaternion, NetcodeReliability, NetcodeRuntime, NetcodeState, NetcodeTransform, NetcodeVector3, NetcodeVoice, NetcodeVoiceEvents, NetcodeVoiceOptions, NetcodeVoicePeer, NetcodeVoiceSignal, SendOptions, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { OpenRtcNetcodeClient, createNetcode, shouldInitiateConnection, } from './client.js';
2
+ export { ReplicatedObjectStore } from './objects.js';
3
+ export { NETCODE_PROTOCOL, NETCODE_PROTOCOL_VERSION, decodeEnvelope, encodeEnvelope, isEnvelopeKind, } from './protocol.js';
@@ -0,0 +1,28 @@
1
+ import type { NetcodeObjectChange, NetcodeObjectStore, NetcodeObjectState, NetcodeObjectUpsert } from './types.js';
2
+ type ObjectListener = (change: NetcodeObjectChange) => void;
3
+ export declare class ReplicatedObjectStore implements NetcodeObjectStore {
4
+ private readonly localPeerId;
5
+ private readonly publishUpsert;
6
+ private readonly publishRemove;
7
+ private readonly objectsById;
8
+ private readonly listeners;
9
+ private nextObjectId;
10
+ constructor(localPeerId: () => string | null, publishUpsert: (object: NetcodeObjectState) => Promise<void>, publishRemove: (id: string) => Promise<void>);
11
+ list(): NetcodeObjectState[];
12
+ get(id: string): NetcodeObjectState | undefined;
13
+ snapshot(): Record<string, NetcodeObjectState>;
14
+ spawn(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
15
+ upsert(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
16
+ update(id: string, patch: Partial<Omit<NetcodeObjectState, 'id'>>): Promise<NetcodeObjectState>;
17
+ remove(id: string): Promise<void>;
18
+ onChange(listener: ObjectListener): () => void;
19
+ clear(): void;
20
+ applySnapshot(objects: Record<string, NetcodeObjectState>, from: string): void;
21
+ applyUpsert(object: NetcodeObjectState, from: string): void;
22
+ applyRemove(id: string, from: string): void;
23
+ private applyObject;
24
+ private createObjectId;
25
+ private emit;
26
+ }
27
+ export declare function isNetcodeObjectState(value: unknown): value is NetcodeObjectState;
28
+ export {};
@@ -0,0 +1,187 @@
1
+ export class ReplicatedObjectStore {
2
+ constructor(localPeerId, publishUpsert, publishRemove) {
3
+ this.localPeerId = localPeerId;
4
+ this.publishUpsert = publishUpsert;
5
+ this.publishRemove = publishRemove;
6
+ this.objectsById = new Map();
7
+ this.listeners = new Set();
8
+ this.nextObjectId = 0;
9
+ }
10
+ list() {
11
+ return Array.from(this.objectsById.values()).map(cloneObject);
12
+ }
13
+ get(id) {
14
+ const object = this.objectsById.get(id);
15
+ return object ? cloneObject(object) : undefined;
16
+ }
17
+ snapshot() {
18
+ const snapshot = {};
19
+ for (const [id, object] of this.objectsById) {
20
+ snapshot[id] = cloneObject(object);
21
+ }
22
+ return snapshot;
23
+ }
24
+ async spawn(input) {
25
+ return this.upsert({
26
+ ownerId: this.localPeerId() ?? 'local',
27
+ ...input,
28
+ id: input.id ?? this.createObjectId(input.kind),
29
+ });
30
+ }
31
+ async upsert(input) {
32
+ const previous = input.id ? this.objectsById.get(input.id) : undefined;
33
+ const object = normalizeObject({
34
+ ...previous,
35
+ ...input,
36
+ id: input.id ?? previous?.id ?? this.createObjectId(input.kind),
37
+ kind: input.kind ?? previous?.kind ?? 'object',
38
+ ownerId: input.ownerId ?? previous?.ownerId ?? this.localPeerId() ?? 'local',
39
+ transform: {
40
+ ...(previous?.transform ?? {}),
41
+ ...(input.transform ?? {}),
42
+ },
43
+ physics: {
44
+ ...(previous?.physics ?? {}),
45
+ ...(input.physics ?? {}),
46
+ },
47
+ metadata: {
48
+ ...(previous?.metadata ?? {}),
49
+ ...(input.metadata ?? {}),
50
+ },
51
+ updatedAt: input.updatedAt ?? Date.now(),
52
+ });
53
+ this.applyObject(object, 'local');
54
+ await this.publishUpsert(object);
55
+ return cloneObject(object);
56
+ }
57
+ async update(id, patch) {
58
+ const previous = this.objectsById.get(id);
59
+ if (!previous) {
60
+ throw new Error(`openrtc-netcode object does not exist: ${id}`);
61
+ }
62
+ return this.upsert({
63
+ ...patch,
64
+ id,
65
+ kind: patch.kind ?? previous.kind,
66
+ ownerId: patch.ownerId ?? previous.ownerId,
67
+ transform: {
68
+ ...previous.transform,
69
+ ...(patch.transform ?? {}),
70
+ },
71
+ physics: {
72
+ ...(previous.physics ?? {}),
73
+ ...(patch.physics ?? {}),
74
+ },
75
+ metadata: {
76
+ ...(previous.metadata ?? {}),
77
+ ...(patch.metadata ?? {}),
78
+ },
79
+ });
80
+ }
81
+ async remove(id) {
82
+ this.applyRemove(id, 'local');
83
+ await this.publishRemove(id);
84
+ }
85
+ onChange(listener) {
86
+ this.listeners.add(listener);
87
+ return () => this.listeners.delete(listener);
88
+ }
89
+ clear() {
90
+ this.objectsById.clear();
91
+ this.emit({ type: 'snapshot', objects: [], from: 'local' });
92
+ }
93
+ applySnapshot(objects, from) {
94
+ for (const object of Object.values(objects)) {
95
+ this.applyObject(object, from);
96
+ }
97
+ this.emit({ type: 'snapshot', objects: this.list(), from });
98
+ }
99
+ applyUpsert(object, from) {
100
+ this.applyObject(object, from);
101
+ }
102
+ applyRemove(id, from) {
103
+ const existed = this.objectsById.delete(id);
104
+ if (existed) {
105
+ this.emit({ type: 'remove', id, from });
106
+ }
107
+ }
108
+ applyObject(input, from) {
109
+ const object = normalizeObject(input);
110
+ const previous = this.objectsById.get(object.id);
111
+ if (previous && previous.updatedAt > object.updatedAt) {
112
+ return;
113
+ }
114
+ this.objectsById.set(object.id, object);
115
+ this.emit({ type: 'upsert', object: cloneObject(object), from });
116
+ }
117
+ createObjectId(kind = 'object') {
118
+ const peerId = this.localPeerId() ?? 'local';
119
+ this.nextObjectId += 1;
120
+ return `${peerId}:${kind}:${this.nextObjectId}`;
121
+ }
122
+ emit(change) {
123
+ for (const listener of this.listeners) {
124
+ listener(change);
125
+ }
126
+ }
127
+ }
128
+ export function isNetcodeObjectState(value) {
129
+ if (!isRecord(value)) {
130
+ return false;
131
+ }
132
+ return typeof value.id === 'string'
133
+ && typeof value.kind === 'string'
134
+ && typeof value.ownerId === 'string'
135
+ && isRecord(value.transform)
136
+ && isVector3(value.transform.position)
137
+ && typeof value.updatedAt === 'number';
138
+ }
139
+ function normalizeObject(input) {
140
+ return {
141
+ id: input.id,
142
+ kind: input.kind,
143
+ ownerId: input.ownerId,
144
+ transform: {
145
+ position: normalizeVector3(input.transform.position),
146
+ rotation: input.transform.rotation ? normalizeQuaternion(input.transform.rotation) : undefined,
147
+ scale: input.transform.scale ? normalizeVector3(input.transform.scale) : undefined,
148
+ },
149
+ physics: input.physics ? {
150
+ linearVelocity: input.physics.linearVelocity ? normalizeVector3(input.physics.linearVelocity) : undefined,
151
+ angularVelocity: input.physics.angularVelocity ? normalizeVector3(input.physics.angularVelocity) : undefined,
152
+ sleeping: !!input.physics.sleeping,
153
+ } : undefined,
154
+ metadata: input.metadata ? { ...input.metadata } : {},
155
+ updatedAt: input.updatedAt,
156
+ };
157
+ }
158
+ function normalizeVector3(value) {
159
+ return {
160
+ x: finiteNumber(value?.x),
161
+ y: finiteNumber(value?.y),
162
+ z: finiteNumber(value?.z),
163
+ };
164
+ }
165
+ function normalizeQuaternion(value) {
166
+ return {
167
+ x: finiteNumber(value?.x),
168
+ y: finiteNumber(value?.y),
169
+ z: finiteNumber(value?.z),
170
+ w: finiteNumber(value?.w, 1),
171
+ };
172
+ }
173
+ function finiteNumber(value, fallback = 0) {
174
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
175
+ }
176
+ function isVector3(value) {
177
+ return isRecord(value)
178
+ && typeof value.x === 'number'
179
+ && typeof value.y === 'number'
180
+ && typeof value.z === 'number';
181
+ }
182
+ function isRecord(value) {
183
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
184
+ }
185
+ function cloneObject(object) {
186
+ return JSON.parse(JSON.stringify(object));
187
+ }
@@ -0,0 +1,50 @@
1
+ export declare const NETCODE_PROTOCOL = "openrtc-netcode";
2
+ export declare const NETCODE_PROTOCOL_VERSION = 1;
3
+ export type NetcodeEnvelopeKind = 'hello' | 'peer' | 'user' | 'state.snapshot' | 'state.patch' | 'object.snapshot' | 'object.upsert' | 'object.remove' | 'lobby.meta' | 'member.meta' | 'voice.signal' | 'ping' | 'pong';
4
+ export interface NetcodeEnvelope<TBody = unknown> {
5
+ protocol: typeof NETCODE_PROTOCOL;
6
+ v: typeof NETCODE_PROTOCOL_VERSION;
7
+ kind: NetcodeEnvelopeKind;
8
+ lobbyId: string;
9
+ from: string;
10
+ seq: number;
11
+ sentAt: number;
12
+ target?: string;
13
+ body: TBody;
14
+ }
15
+ export interface HelloBody {
16
+ peerId: string;
17
+ displayName?: string;
18
+ }
19
+ export interface UserMessageBody {
20
+ type: string;
21
+ payload: unknown;
22
+ }
23
+ export interface StateSnapshotBody {
24
+ values: Record<string, unknown>;
25
+ owners: Record<string, string>;
26
+ }
27
+ export interface StatePatchBody {
28
+ key: string;
29
+ value: unknown;
30
+ owner: string;
31
+ }
32
+ export interface ObjectSnapshotBody {
33
+ objects: Record<string, unknown>;
34
+ }
35
+ export interface ObjectUpsertBody {
36
+ object: unknown;
37
+ }
38
+ export interface ObjectRemoveBody {
39
+ id: string;
40
+ }
41
+ export interface MetadataBody {
42
+ values: Record<string, string>;
43
+ }
44
+ export interface VoiceSignalBody {
45
+ signal: unknown;
46
+ }
47
+ export declare function encodeEnvelope(envelope: NetcodeEnvelope): string;
48
+ export declare function decodeEnvelope(value: unknown): NetcodeEnvelope | null;
49
+ export declare function isEnvelopeKind(value: unknown): value is NetcodeEnvelopeKind;
50
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,53 @@
1
+ export const NETCODE_PROTOCOL = 'openrtc-netcode';
2
+ export const NETCODE_PROTOCOL_VERSION = 1;
3
+ export function encodeEnvelope(envelope) {
4
+ return JSON.stringify(envelope);
5
+ }
6
+ export function decodeEnvelope(value) {
7
+ const candidate = typeof value === 'string' ? parseJson(value) : value;
8
+ if (!isRecord(candidate)) {
9
+ return null;
10
+ }
11
+ if (candidate.protocol !== NETCODE_PROTOCOL || candidate.v !== NETCODE_PROTOCOL_VERSION) {
12
+ return null;
13
+ }
14
+ if (!isEnvelopeKind(candidate.kind)) {
15
+ return null;
16
+ }
17
+ if (typeof candidate.lobbyId !== 'string'
18
+ || typeof candidate.from !== 'string'
19
+ || typeof candidate.seq !== 'number'
20
+ || typeof candidate.sentAt !== 'number') {
21
+ return null;
22
+ }
23
+ if ('target' in candidate && candidate.target !== undefined && typeof candidate.target !== 'string') {
24
+ return null;
25
+ }
26
+ return candidate;
27
+ }
28
+ export function isEnvelopeKind(value) {
29
+ return value === 'hello'
30
+ || value === 'peer'
31
+ || value === 'user'
32
+ || value === 'state.snapshot'
33
+ || value === 'state.patch'
34
+ || value === 'object.snapshot'
35
+ || value === 'object.upsert'
36
+ || value === 'object.remove'
37
+ || value === 'lobby.meta'
38
+ || value === 'member.meta'
39
+ || value === 'voice.signal'
40
+ || value === 'ping'
41
+ || value === 'pong';
42
+ }
43
+ export function isRecord(value) {
44
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ }
46
+ function parseJson(value) {
47
+ try {
48
+ return JSON.parse(value);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ import type { NetVar, NetVarOptions, NetcodeState } from './types.js';
2
+ export declare class ReplicatedState implements NetcodeState {
3
+ private readonly resolveOwner;
4
+ private readonly publishPatch;
5
+ private readonly entries;
6
+ constructor(resolveOwner: (options?: NetVarOptions) => string, publishPatch: (key: string, value: unknown, owner: string) => Promise<void>);
7
+ define<T>(key: string, initial: T, options?: NetVarOptions): NetVar<T>;
8
+ get<T = unknown>(key: string): T | undefined;
9
+ snapshot(): Record<string, unknown>;
10
+ owners(): Record<string, string>;
11
+ 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;
15
+ private createNetVar;
16
+ }
package/dist/state.js ADDED
@@ -0,0 +1,94 @@
1
+ export class ReplicatedState {
2
+ constructor(resolveOwner, publishPatch) {
3
+ this.resolveOwner = resolveOwner;
4
+ this.publishPatch = publishPatch;
5
+ this.entries = new Map();
6
+ }
7
+ define(key, initial, options) {
8
+ const existing = this.entries.get(key);
9
+ if (existing) {
10
+ return this.createNetVar(existing);
11
+ }
12
+ const entry = {
13
+ key,
14
+ value: initial,
15
+ owner: this.resolveOwner(options),
16
+ listeners: new Set(),
17
+ };
18
+ this.entries.set(key, entry);
19
+ return this.createNetVar(entry);
20
+ }
21
+ get(key) {
22
+ return this.entries.get(key)?.value;
23
+ }
24
+ snapshot() {
25
+ const values = {};
26
+ for (const [key, entry] of this.entries) {
27
+ values[key] = entry.value;
28
+ }
29
+ return values;
30
+ }
31
+ owners() {
32
+ const values = {};
33
+ for (const [key, entry] of this.entries) {
34
+ values[key] = entry.owner;
35
+ }
36
+ return values;
37
+ }
38
+ replaceOwner(previousOwner, nextOwner) {
39
+ for (const entry of this.entries.values()) {
40
+ if (entry.owner === previousOwner) {
41
+ entry.owner = nextOwner;
42
+ }
43
+ }
44
+ }
45
+ applySnapshot(values, owners) {
46
+ for (const [key, value] of Object.entries(values)) {
47
+ this.applyRemoteValue(key, value, owners[key] ?? this.resolveOwner());
48
+ }
49
+ }
50
+ applyPatch(key, value, owner) {
51
+ this.applyRemoteValue(key, value, owner);
52
+ }
53
+ applyRemoteValue(key, value, owner) {
54
+ const existing = this.entries.get(key);
55
+ if (!existing) {
56
+ this.entries.set(key, {
57
+ key,
58
+ value,
59
+ owner,
60
+ listeners: new Set(),
61
+ });
62
+ return;
63
+ }
64
+ const previous = existing.value;
65
+ existing.value = value;
66
+ existing.owner = owner;
67
+ for (const listener of existing.listeners) {
68
+ listener(value, previous);
69
+ }
70
+ }
71
+ createNetVar(entry) {
72
+ return {
73
+ key: entry.key,
74
+ get owner() {
75
+ return entry.owner;
76
+ },
77
+ get value() {
78
+ return entry.value;
79
+ },
80
+ set: async (value) => {
81
+ 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);
87
+ },
88
+ onChange: (callback) => {
89
+ entry.listeners.add(callback);
90
+ return () => entry.listeners.delete(callback);
91
+ },
92
+ };
93
+ }
94
+ }