openrtc-netcode 0.1.0 → 2.0.0-rc.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.
package/README.md CHANGED
@@ -6,16 +6,31 @@ and exposes reliable typed messages, lightweight replicated state, networked
6
6
  objects, and browser voice-chat signaling.
7
7
 
8
8
  Gameplay payloads are sent over OpenRTC peer data paths only: Iroh
9
- relay/direct, WebRTC DataChannel upgrades, and MoQ routes when enabled. Firebase
10
- and Firestore remain part of OpenRTC's control plane for identity, discovery,
11
- rooms, tickets, and browser signaling metadata; `openrtc-netcode` does not use
12
- Firestore as a state or message relay.
9
+ relay/direct, WebRTC DataChannel upgrades, and MoQ routes when enabled. Live
10
+ membership and signaling use the OpenRTC coordination gateway; neither core
11
+ netcode nor its payload path uses Firebase, Firestore, or RTDB.
12
+
13
+ `preferredPayloadTransports` filters OpenRTC's typed application-readiness
14
+ query; it does not request promotion. OpenRTC owns optional transport promotion
15
+ and recovery, while netcode only observes route events. Netcode consumes one
16
+ already-activated room handle, so it never retains an unused user or space
17
+ avenue.
13
18
 
14
19
  ```ts
20
+ import { OpenRTC } from 'openrtc';
15
21
  import { createNetcode } from 'openrtc-netcode';
16
22
 
17
- const netcode = createNetcode({ apiKey: 'pk_live_...' });
18
- await netcode.createLobby({ id: 'duel-room' });
23
+ const rtc = OpenRTC({
24
+ apiKey: 'pk_live_...',
25
+ transports: { webrtc: true },
26
+ });
27
+ const room = await rtc.rooms.join('duel-room', {
28
+ access: 'capability',
29
+ membership: 'ephemeral',
30
+ maxPeers: 4,
31
+ });
32
+ const netcode = createNetcode({ room });
33
+ await netcode.joinLobby('duel-room');
19
34
 
20
35
  netcode.onMessage('chat', (message) => {
21
36
  console.log(message.from, message.payload);
package/dist/client.d.ts CHANGED
@@ -5,7 +5,6 @@ import type { CreateLobbyOptions, JoinLobbyOptions, NetcodeClient as NetcodeClie
5
5
  export declare class OpenRtcNetcodeClient implements NetcodeClientInterface {
6
6
  private readonly options;
7
7
  private runtime;
8
- private ownsRuntime;
9
8
  private localPeerId;
10
9
  private currentLobby;
11
10
  private currentRoomId;
@@ -29,10 +28,10 @@ export declare class OpenRtcNetcodeClient implements NetcodeClientInterface {
29
28
  private readonly connectionIds;
30
29
  private readonly pendingPeerIds;
31
30
  private readonly directSendWarnings;
32
- private readonly requestedWebRtcUpgradeConnectionIds;
33
31
  private readonly introPeerIds;
34
32
  private readonly seenEnvelopeIds;
35
33
  private readonly seenEnvelopeOrder;
34
+ private readonly latestStateQueues;
36
35
  private readonly stateStore;
37
36
  private readonly objectStore;
38
37
  private readonly voiceController;
@@ -81,8 +80,10 @@ export declare class OpenRtcNetcodeClient implements NetcodeClientInterface {
81
80
  private sendObjectSnapshot;
82
81
  private broadcastMemberMetadata;
83
82
  private startPeerPingLoop;
83
+ private refreshPeerPayloadState;
84
84
  private stopPeerPingLoop;
85
85
  private broadcastEnvelope;
86
+ private broadcastLatestState;
86
87
  private sendEnvelope;
87
88
  private sendDirectEnvelope;
88
89
  private warnDirectSendFailure;
@@ -101,7 +102,6 @@ export declare class OpenRtcNetcodeClient implements NetcodeClientInterface {
101
102
  private resolvePeerTransport;
102
103
  private isPeerPayloadReady;
103
104
  private resolvePayloadConnection;
104
- private requestPreferredTransportUpgrade;
105
105
  private payloadReadinessOptions;
106
106
  private refreshLobby;
107
107
  private requireRuntime;
package/dist/client.js CHANGED
@@ -1,9 +1,10 @@
1
- import { OpenRTC } from 'openrtc';
2
1
  import { TypedEmitter } from './events.js';
3
2
  import { decodeEnvelope, encodeEnvelope, isRecord, NETCODE_PROTOCOL, NETCODE_PROTOCOL_VERSION, } from './protocol.js';
4
3
  import { isNetcodeObjectState, ReplicatedObjectStore } from './objects.js';
5
4
  import { ReplicatedState } from './state.js';
6
5
  import { NetcodeVoiceController } from './voice.js';
6
+ import { runtimeFromRoom } from './roomRuntime.js';
7
+ import { LatestStateQueue } from './latestState.js';
7
8
  const DEFAULT_MAX_PEERS = 8;
8
9
  const NETCODE_CHANNEL_ID = 'openrtc-netcode/v1';
9
10
  const PING_INTERVAL_MS = 2000;
@@ -16,7 +17,6 @@ export class OpenRtcNetcodeClient {
16
17
  constructor(options = {}) {
17
18
  this.options = options;
18
19
  this.runtime = null;
19
- this.ownsRuntime = false;
20
20
  this.localPeerId = null;
21
21
  this.currentLobby = null;
22
22
  this.currentRoomId = null;
@@ -40,12 +40,12 @@ export class OpenRtcNetcodeClient {
40
40
  this.connectionIds = new Set();
41
41
  this.pendingPeerIds = new Set();
42
42
  this.directSendWarnings = new Set();
43
- this.requestedWebRtcUpgradeConnectionIds = new Set();
44
43
  this.introPeerIds = new Set();
45
44
  this.seenEnvelopeIds = new Set();
46
45
  this.seenEnvelopeOrder = [];
47
- this.stateStore = new ReplicatedState((options) => this.resolveStateOwner(options), (key, value, owner) => this.broadcastEnvelope('state.patch', { key, value, owner }));
48
- this.objectStore = new ReplicatedObjectStore(() => this.localPeerId, (object) => this.broadcastEnvelope('object.upsert', { object }), (id) => this.broadcastEnvelope('object.remove', { id }));
46
+ this.latestStateQueues = new Map();
47
+ this.stateStore = new ReplicatedState((options) => this.resolveStateOwner(options), (key, value, owner) => this.broadcastLatestState(`state:${key}`, 'state.patch', { key, value, owner }));
48
+ this.objectStore = new ReplicatedObjectStore(() => this.localPeerId, (object) => this.broadcastLatestState(`object:${object.id}`, 'object.upsert', { object }), (id) => this.broadcastEnvelope('object.remove', { id }));
49
49
  this.voiceController = new NetcodeVoiceController(() => this.localPeerId, () => this.peers, shouldInitiateConnection, (peerId, signal) => this.sendEnvelope(peerId, 'voice.signal', { signal }));
50
50
  }
51
51
  get peerId() {
@@ -75,23 +75,10 @@ export class OpenRtcNetcodeClient {
75
75
  }
76
76
  this.started = true;
77
77
  this.stopped = false;
78
- this.runtime = this.options.runtime ?? OpenRTC({
79
- apiKey: this.options.apiKey,
80
- projectId: this.options.projectId,
81
- authMode: this.options.authMode,
82
- discoveryMode: this.options.discoveryMode,
83
- allowAnonymousHostedDefaults: this.options.allowAnonymousHostedDefaults,
84
- space: this.options.space,
85
- spaceKey: this.options.spaceKey,
86
- spaceTokenProvider: this.options.spaceTokenProvider,
87
- storagePrefix: this.options.storagePrefix,
88
- nodeIdPersistence: this.options.nodeIdPersistence,
89
- transports: this.options.transports,
90
- transportPriority: this.options.transportPriority,
91
- strictMode: this.options.strictMode,
92
- turnCredentialsProvider: this.options.turnCredentialsProvider,
93
- });
94
- this.ownsRuntime = !this.options.runtime;
78
+ if (!this.options.runtime && !this.options.room) {
79
+ throw new Error('openrtc-netcode requires an activated OpenRTC 2.0 room adapter or an explicit openrtc/runtime client.');
80
+ }
81
+ this.runtime = this.options.runtime ?? runtimeFromRoom(this.options.room);
95
82
  await callOptional(this.runtime, 'initialize');
96
83
  await callOptional(this.runtime, 'start');
97
84
  this.localPeerId = await this.runtime.getNodeId();
@@ -120,20 +107,17 @@ export class OpenRtcNetcodeClient {
120
107
  this.connectionIds.clear();
121
108
  this.pendingPeerIds.clear();
122
109
  this.directSendWarnings.clear();
123
- this.requestedWebRtcUpgradeConnectionIds.clear();
124
110
  this.introPeerIds.clear();
125
111
  this.peerStatsById.clear();
126
112
  this.seenEnvelopeIds.clear();
127
113
  this.seenEnvelopeOrder.length = 0;
114
+ this.latestStateQueues.clear();
128
115
  this.stopPeerPingLoop();
129
116
  this.peersById.clear();
130
117
  this.objectStore.clear();
131
118
  await this.voiceController.stop();
132
119
  this.emitter.clear();
133
120
  this.messageListeners.clear();
134
- if (this.ownsRuntime) {
135
- await callOptional(this.runtime, 'stop');
136
- }
137
121
  this.runtime = null;
138
122
  this.started = false;
139
123
  }
@@ -239,6 +223,7 @@ export class OpenRtcNetcodeClient {
239
223
  this.peerStatsById.clear();
240
224
  this.seenEnvelopeIds.clear();
241
225
  this.seenEnvelopeOrder.length = 0;
226
+ this.latestStateQueues.clear();
242
227
  this.objectStore.clear();
243
228
  await this.voiceController.stop();
244
229
  if (runtime && roomId) {
@@ -312,13 +297,9 @@ export class OpenRtcNetcodeClient {
312
297
  if (this.fallbackChannel) {
313
298
  continue;
314
299
  }
315
- const connection = this.connectionsByPeer.get(peerId);
316
300
  this.peersById.delete(peerId);
317
301
  this.connectionsByPeer.delete(peerId);
318
302
  this.pendingPeerIds.delete(peerId);
319
- if (connection?.id) {
320
- this.requestedWebRtcUpgradeConnectionIds.delete(connection.id);
321
- }
322
303
  this.introPeerIds.delete(peerId);
323
304
  this.peerStatsById.delete(peerId);
324
305
  this.emitter.emit('peer:left', clonePeer(peer));
@@ -332,6 +313,14 @@ export class OpenRtcNetcodeClient {
332
313
  if (!this.runtime || !this.localPeerId) {
333
314
  return;
334
315
  }
316
+ // An OpenRTC 2.0 room capability is the sole owner of its peer
317
+ // lifecycle. Calling connectByTicket here races that owner and can
318
+ // restart reciprocal application-key agreement on an already-admitted
319
+ // physical connection. Explicit legacy/test runtimes retain the
320
+ // deterministic mesh dial below.
321
+ if (this.runtime.managedConnectionLifecycle) {
322
+ return;
323
+ }
335
324
  const maxPeers = this.options.maxPeers ?? DEFAULT_MAX_PEERS;
336
325
  for (const peer of this.peersById.values()) {
337
326
  if (this.connectionsByPeer.size >= maxPeers) {
@@ -370,7 +359,6 @@ export class OpenRtcNetcodeClient {
370
359
  this.connectionIds.add(connectionId);
371
360
  }
372
361
  this.connectionsByPeer.set(peerId, connection);
373
- this.requestPreferredTransportUpgrade(connection);
374
362
  const payloadReady = this.isPeerPayloadReady(peerId);
375
363
  const peer = this.upsertPeer(peerId, { connected: payloadReady });
376
364
  this.updatePeerStats(peerId, {
@@ -384,13 +372,15 @@ export class OpenRtcNetcodeClient {
384
372
  connection.onMessage((message) => {
385
373
  this.handleConnectionMessage(peerId, message);
386
374
  });
375
+ connection.onUpgradeStateChange?.(() => {
376
+ this.refreshPeerPayloadState(peerId);
377
+ });
387
378
  connection.onDisconnect(() => {
388
379
  if (this.connectionsByPeer.get(peerId) === connection) {
389
380
  this.connectionsByPeer.delete(peerId);
390
381
  }
391
382
  if (connectionId) {
392
383
  this.connectionIds.delete(connectionId);
393
- this.requestedWebRtcUpgradeConnectionIds.delete(connectionId);
394
384
  }
395
385
  const disconnected = this.upsertPeer(peerId, { connected: false });
396
386
  this.updatePeerStats(peerId, {
@@ -653,29 +643,28 @@ export class OpenRtcNetcodeClient {
653
643
  return;
654
644
  }
655
645
  for (const peer of this.peersById.values()) {
656
- const payloadReady = this.isPeerPayloadReady(peer.id);
657
- if (!payloadReady) {
658
- const connection = this.connectionsByPeer.get(peer.id);
659
- if (connection) {
660
- this.requestPreferredTransportUpgrade(connection);
661
- }
662
- }
663
- if (payloadReady && !peer.connected) {
664
- const connected = this.upsertPeer(peer.id, { connected: true });
665
- this.emitter.emit('peer:connected', clonePeer(connected));
666
- void this.voiceController.handlePeerConnected(peer.id);
667
- this.queuePeerIntro(peer.id, true);
668
- }
646
+ const payloadReady = this.refreshPeerPayloadState(peer.id);
669
647
  if (payloadReady) {
670
648
  void this.sendEnvelope(peer.id, 'ping', {});
671
649
  }
672
- this.updatePeerStats(peer.id, {
673
- connected: payloadReady,
674
- transport: this.resolvePeerTransport(peer.id),
675
- });
676
650
  }
677
651
  }, PING_INTERVAL_MS);
678
652
  }
653
+ refreshPeerPayloadState(peerId) {
654
+ const payloadReady = this.isPeerPayloadReady(peerId);
655
+ const peer = this.peersById.get(peerId);
656
+ if (payloadReady && peer && !peer.connected) {
657
+ const connected = this.upsertPeer(peerId, { connected: true });
658
+ this.emitter.emit('peer:connected', clonePeer(connected));
659
+ void this.voiceController.handlePeerConnected(peerId);
660
+ this.queuePeerIntro(peerId, true);
661
+ }
662
+ this.updatePeerStats(peerId, {
663
+ connected: payloadReady,
664
+ transport: this.resolvePeerTransport(peerId),
665
+ });
666
+ return payloadReady;
667
+ }
679
668
  stopPeerPingLoop() {
680
669
  if (this.peerPingTimer) {
681
670
  clearInterval(this.peerPingTimer);
@@ -690,6 +679,14 @@ export class OpenRtcNetcodeClient {
690
679
  const sends = Array.from(this.connectionsByPeer.keys()).map((peerId) => this.sendDirectEnvelope(peerId, envelope));
691
680
  await Promise.all(sends);
692
681
  }
682
+ broadcastLatestState(key, kind, body) {
683
+ let queue = this.latestStateQueues.get(key);
684
+ if (!queue) {
685
+ queue = new LatestStateQueue((state) => this.broadcastEnvelope(state.kind, state.body));
686
+ this.latestStateQueues.set(key, queue);
687
+ }
688
+ return queue.push({ kind, body });
689
+ }
693
690
  async sendEnvelope(peerId, kind, body) {
694
691
  const envelope = this.createEnvelope(kind, body, peerId);
695
692
  if (this.fallbackChannel) {
@@ -974,28 +971,8 @@ export class OpenRtcNetcodeClient {
974
971
  }
975
972
  return this.connectionsByPeer.get(peerId) ?? null;
976
973
  }
977
- requestPreferredTransportUpgrade(connection) {
978
- const preferredTransports = this.payloadReadinessOptions().preferredTransports ?? [];
979
- const wantsWebRtc = preferredTransports.some((transport) => transport === 'webrtc' || transport === 'webrtc-lan');
980
- if (!wantsWebRtc || typeof connection.requestWebRTCUpgrade !== 'function') {
981
- return;
982
- }
983
- const remotePeerId = connection.remoteNodeId ?? connection.deviceId;
984
- if (this.localPeerId && remotePeerId && this.localPeerId.localeCompare(remotePeerId) <= 0) {
985
- return;
986
- }
987
- const upgradeKey = connection.id ?? connection.remoteNodeId ?? connection.deviceId;
988
- if (upgradeKey && this.requestedWebRtcUpgradeConnectionIds.has(upgradeKey)) {
989
- return;
990
- }
991
- if (upgradeKey) {
992
- this.requestedWebRtcUpgradeConnectionIds.add(upgradeKey);
993
- }
994
- connection.requestWebRTCUpgrade('openrtc-netcode-preferred-payload');
995
- }
996
974
  payloadReadinessOptions() {
997
- const preferredTransports = this.options.preferredPayloadTransports
998
- ?? derivePreferredPayloadTransports(this.options.transports, this.options.transportPriority);
975
+ const preferredTransports = this.options.preferredPayloadTransports ?? [];
999
976
  if (preferredTransports.length === 0) {
1000
977
  return {};
1001
978
  }
@@ -1150,21 +1127,6 @@ function isRoomAdmissionTransientError(error) {
1150
1127
  return isRoomNotFoundError(error)
1151
1128
  || /permission[_ -]?denied|permission denied|forbidden|code["']?\s*:\s*403|status["']?\s*:\s*["']?permission_denied/i.test(text);
1152
1129
  }
1153
- function derivePreferredPayloadTransports(transports, transportPriority) {
1154
- const configuredPriority = (transportPriority ?? [])
1155
- .filter((transport) => UPGRADED_PAYLOAD_TRANSPORTS.has(transport));
1156
- if (configuredPriority.length > 0) {
1157
- return [...new Set(configuredPriority)];
1158
- }
1159
- const preferred = [];
1160
- if (transports?.webrtc) {
1161
- preferred.push('webrtc-lan', 'webrtc');
1162
- }
1163
- if (transports?.moq) {
1164
- preferred.push('moq');
1165
- }
1166
- return preferred;
1167
- }
1168
1130
  function delay(ms) {
1169
1131
  return new Promise((resolve) => setTimeout(resolve, ms));
1170
1132
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { OpenRtcNetcodeClient, createNetcode, shouldInitiateConnection, } from './client.js';
2
+ export { runtimeFromRoom } from './roomRuntime.js';
2
3
  export { ReplicatedObjectStore } from './objects.js';
3
4
  export { NETCODE_PROTOCOL, NETCODE_PROTOCOL_VERSION, decodeEnvelope, encodeEnvelope, isEnvelopeKind, } from './protocol.js';
4
5
  export type { HelloBody, MetadataBody, NetcodeEnvelope, NetcodeEnvelopeKind, ObjectRemoveBody, ObjectSnapshotBody, ObjectUpsertBody, StatePatchBody, StateSnapshotBody, UserMessageBody, VoiceSignalBody, } from './protocol.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { OpenRtcNetcodeClient, createNetcode, shouldInitiateConnection, } from './client.js';
2
+ export { runtimeFromRoom } from './roomRuntime.js';
2
3
  export { ReplicatedObjectStore } from './objects.js';
3
4
  export { NETCODE_PROTOCOL, NETCODE_PROTOCOL_VERSION, decodeEnvelope, encodeEnvelope, isEnvelopeKind, } from './protocol.js';
@@ -0,0 +1,9 @@
1
+ /** Coalesces only queued values; the in-flight state is allowed to finish. */
2
+ export declare class LatestStateQueue<T> {
3
+ private readonly transmit;
4
+ private pending;
5
+ private running;
6
+ constructor(transmit: (value: T) => Promise<void>);
7
+ push(value: T): Promise<void>;
8
+ private pump;
9
+ }
@@ -0,0 +1,44 @@
1
+ /** Coalesces only queued values; the in-flight state is allowed to finish. */
2
+ export class LatestStateQueue {
3
+ constructor(transmit) {
4
+ this.transmit = transmit;
5
+ this.pending = null;
6
+ this.running = false;
7
+ }
8
+ push(value) {
9
+ return new Promise((resolve, reject) => {
10
+ if (this.pending) {
11
+ this.pending.value = value;
12
+ this.pending.waiters.push({ resolve, reject });
13
+ }
14
+ else {
15
+ this.pending = { value, waiters: [{ resolve, reject }] };
16
+ }
17
+ this.pump();
18
+ });
19
+ }
20
+ pump() {
21
+ if (this.running)
22
+ return;
23
+ this.running = true;
24
+ void (async () => {
25
+ while (this.pending) {
26
+ const current = this.pending;
27
+ this.pending = null;
28
+ try {
29
+ await this.transmit(current.value);
30
+ for (const waiter of current.waiters)
31
+ waiter.resolve();
32
+ }
33
+ catch (error) {
34
+ for (const waiter of current.waiters)
35
+ waiter.reject(error);
36
+ }
37
+ }
38
+ })().finally(() => {
39
+ this.running = false;
40
+ if (this.pending)
41
+ this.pump();
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ import type { OpenRTCRoomHandle } from 'openrtc';
2
+ import type { NetcodeRuntime } from './types.js';
3
+ /**
4
+ * Adapts one OpenRTC 2.0 capability handle to netcode's transport-only runtime
5
+ * contract. It never creates a second room, user scope, or base space.
6
+ */
7
+ export declare function runtimeFromRoom(room: OpenRTCRoomHandle): NetcodeRuntime;
@@ -0,0 +1,88 @@
1
+ function normalized(value) {
2
+ return value.trim().toUpperCase();
3
+ }
4
+ function assertRoom(room, requested) {
5
+ if (normalized(room.id) !== normalized(requested)) {
6
+ throw new Error(`Activated OpenRTC room '${room.id}' cannot join '${requested}'.`);
7
+ }
8
+ }
9
+ function peerMember(peer) {
10
+ const peerId = String(peer.nodeId ?? peer.id ?? peer.deviceId ?? '');
11
+ return {
12
+ nodeId: peerId,
13
+ userId: typeof peer.userId === 'string' ? peer.userId : undefined,
14
+ ticket: typeof peer.ticket === 'string' ? peer.ticket : undefined,
15
+ joinedAt: typeof peer.connectedAt === 'number' ? peer.connectedAt : Date.now(),
16
+ lastSeenAt: Date.now(),
17
+ };
18
+ }
19
+ /**
20
+ * Adapts one OpenRTC 2.0 capability handle to netcode's transport-only runtime
21
+ * contract. It never creates a second room, user scope, or base space.
22
+ */
23
+ export function runtimeFromRoom(room) {
24
+ const connections = () => room.diagnostics.connections();
25
+ return {
26
+ managedConnectionLifecycle: true,
27
+ channels: room.channels,
28
+ initialize: async () => undefined,
29
+ start: async () => undefined,
30
+ stop: async () => undefined,
31
+ getNodeId: async () => {
32
+ const status = await room.diagnostics.status();
33
+ const nodeId = typeof status.localNodeId === 'string' ? status.localNodeId.trim() : '';
34
+ if (!nodeId)
35
+ throw new Error('The activated OpenRTC room has no local peer identity yet.');
36
+ return nodeId;
37
+ },
38
+ createRoom: async (roomId = room.id) => {
39
+ assertRoom(room, roomId);
40
+ return normalized(room.id);
41
+ },
42
+ joinRoom: async (roomId) => {
43
+ assertRoom(room, roomId);
44
+ return connections();
45
+ },
46
+ leaveRoom: async (roomId) => {
47
+ assertRoom(room, roomId);
48
+ await room.leave();
49
+ },
50
+ watchRoom: (roomId, callback) => {
51
+ assertRoom(room, roomId);
52
+ return room.peers.watch((peers) => callback(peers.map((peer) => peerMember(peer))));
53
+ },
54
+ getRoomMembers: async (roomId) => {
55
+ assertRoom(room, roomId);
56
+ return room.peers.listConnected()
57
+ .map((peer) => peerMember(peer));
58
+ },
59
+ onConnection: (callback) => {
60
+ const seen = new Set();
61
+ const publish = (connection) => {
62
+ const id = String(connection.id ?? connection.remoteNodeId ?? '');
63
+ if (!id || seen.has(id))
64
+ return;
65
+ seen.add(id);
66
+ callback(connection);
67
+ };
68
+ connections().forEach(publish);
69
+ return room.diagnostics.onConnection((connection) => {
70
+ publish(connection);
71
+ });
72
+ },
73
+ getConnections: connections,
74
+ connectByTicket: ({ ticket, timeoutMs }) => room.peers.connect({ ticket, timeoutMs }),
75
+ // The room capability already authorized the peer route. A protocol
76
+ // channel is not another admission avenue and must not replace that
77
+ // lifecycle scope with the channel identifier (which would trigger a
78
+ // second scoped dial on every send while the new scope is absent).
79
+ connectScopedChannel: (options) => room.channels.connectScoped({
80
+ ...options,
81
+ scope: `v2:room:${room.id}`,
82
+ existingRouteOnly: true,
83
+ }),
84
+ onIncomingChannelStream: (channelId, callback) => room.channels.onIncomingChannel(channelId, callback),
85
+ getApplicationReadyConnections: () => connections().filter((connection) => typeof connection.isReadyForApplicationPayload !== 'function'
86
+ || connection.isReadyForApplicationPayload()),
87
+ };
88
+ }
package/dist/types.d.ts CHANGED
@@ -1,26 +1,41 @@
1
- import type { OpenRTC } from 'openrtc';
2
- import type { RuntimeClient } from 'openrtc/runtime';
1
+ import type { RuntimeApplicationPayloadReadinessOptions as OpenRtcApplicationPayloadReadinessOptions, RuntimeIncomingStream, RuntimeScopedChannelOptions, RuntimeScopedLogicalChannel } from 'openrtc/runtime';
2
+ import type { OpenRTCRoomHandle } from 'openrtc';
3
3
  import type { MetadataBody, NetcodeEnvelope, ObjectRemoveBody, ObjectSnapshotBody, ObjectUpsertBody, StatePatchBody, StateSnapshotBody, UserMessageBody, VoiceSignalBody } from './protocol.js';
4
- type OpenrtcClientOptions = Parameters<typeof OpenRTC>[0];
5
- export type NetcodeRuntime = RuntimeClient;
4
+ export interface NetcodeRuntime {
5
+ /**
6
+ * The activated capability owns discovery and physical peer lifecycle.
7
+ * Protocol layers must observe its connections instead of issuing a
8
+ * second ticket dial for the same avenue.
9
+ */
10
+ managedConnectionLifecycle?: boolean;
11
+ initialize?(): Promise<void> | void;
12
+ start?(): Promise<void> | void;
13
+ stop?(): Promise<void> | void;
14
+ getNodeId(): Promise<string>;
15
+ createRoom(roomId?: string): Promise<string>;
16
+ joinRoom(roomId: string, options?: {
17
+ bootstrapPeers?: boolean;
18
+ }): Promise<RuntimeConnectionLike[]>;
19
+ leaveRoom(roomId: string): Promise<void>;
20
+ watchRoom(roomId: string, callback: (members: RuntimeMemberLike[]) => void): () => void;
21
+ getRoomMembers(roomId: string): Promise<RuntimeMemberLike[]>;
22
+ onConnection(callback: (connection: RuntimeConnectionLike) => void): () => void;
23
+ getConnections(): RuntimeConnectionLike[];
24
+ connectByTicket(target: {
25
+ ticket: string;
26
+ timeoutMs?: number;
27
+ }): Promise<RuntimeConnectionLike>;
28
+ connectScopedChannel(options: RuntimeScopedChannelOptions): Promise<RuntimeScopedLogicalChannel>;
29
+ onIncomingChannelStream(channelId: string, callback: (stream: RuntimeIncomingStream) => boolean | void): () => void;
30
+ getApplicationReadyConnections(options?: RuntimeApplicationPayloadReadinessOptions): RuntimeConnectionLike[];
31
+ channels?: OpenRTCRoomHandle['channels'];
32
+ }
6
33
  export type NetcodeReliability = 'reliable';
7
34
  export interface NetcodeOptions {
8
35
  runtime?: NetcodeRuntime;
9
- apiKey?: OpenrtcClientOptions['apiKey'];
10
- projectId?: OpenrtcClientOptions['projectId'];
11
- authMode?: OpenrtcClientOptions['authMode'];
12
- discoveryMode?: OpenrtcClientOptions['discoveryMode'];
13
- allowAnonymousHostedDefaults?: OpenrtcClientOptions['allowAnonymousHostedDefaults'];
14
- space?: OpenrtcClientOptions['space'];
15
- spaceKey?: OpenrtcClientOptions['spaceKey'];
16
- spaceTokenProvider?: NonNullable<OpenrtcClientOptions>['spaceTokenProvider'];
17
- storagePrefix?: OpenrtcClientOptions['storagePrefix'];
18
- nodeIdPersistence?: OpenrtcClientOptions['nodeIdPersistence'];
19
- transports?: OpenrtcClientOptions['transports'];
20
- transportPriority?: OpenrtcClientOptions['transportPriority'];
36
+ /** Preferred 2.0 path: one already-activated ephemeral match room. */
37
+ room?: OpenRTCRoomHandle;
21
38
  preferredPayloadTransports?: RuntimeApplicationPayloadReadinessOptions['preferredTransports'];
22
- strictMode?: OpenrtcClientOptions['strictMode'];
23
- turnCredentialsProvider?: NonNullable<OpenrtcClientOptions>['turnCredentialsProvider'];
24
39
  displayName?: string;
25
40
  maxPeers?: number;
26
41
  localFallback?: boolean;
@@ -235,16 +250,15 @@ export type RuntimeConnectionLike = {
235
250
  parallelTransport?: string | null;
236
251
  };
237
252
  getAvailableTransports?(): string[];
253
+ isReadyForApplicationPayload?(options?: RuntimeApplicationPayloadReadinessOptions): boolean;
254
+ onUpgradeStateChange?(callback: (state: 'none' | 'upgrading' | 'upgraded' | 'failed') => void): void;
238
255
  requestWebRTCUpgrade?(reason?: string): void;
239
256
  send(message: unknown): Promise<void> | void;
240
257
  onMessage(callback: (message: unknown) => void): void | (() => void);
241
258
  onDisconnect(callback: () => void): void | (() => void);
242
259
  disconnect?(): Promise<void> | void;
243
260
  };
244
- export type RuntimeApplicationPayloadReadinessOptions = {
245
- preferredTransports?: string[];
246
- allowFallbackAfterPreferredTransportFailure?: boolean;
247
- };
261
+ export type RuntimeApplicationPayloadReadinessOptions = OpenRtcApplicationPayloadReadinessOptions;
248
262
  export type RuntimeMemberLike = {
249
263
  nodeId?: string;
250
264
  userId?: string;
@@ -255,4 +269,3 @@ export type RuntimeMemberLike = {
255
269
  metadata?: string | Record<string, unknown> | null;
256
270
  };
257
271
  export type RuntimeEnvelopeBody = UserMessageBody | StateSnapshotBody | StatePatchBody | ObjectSnapshotBody | ObjectUpsertBody | ObjectRemoveBody | MetadataBody | VoiceSignalBody | Record<string, unknown>;
258
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrtc-netcode",
3
- "version": "0.1.0",
3
+ "version": "2.0.0-rc.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -42,7 +42,7 @@
42
42
  "test:watch": "vitest"
43
43
  },
44
44
  "dependencies": {
45
- "openrtc": "^0.2.0"
45
+ "openrtc": "2.0.0-rc.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^24.10.1",