y-openrtc 0.1.2 → 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
@@ -12,11 +12,17 @@ pnpm add y-openrtc yjs
12
12
 
13
13
  ```ts
14
14
  import * as Y from 'yjs';
15
+ import { OpenRTC } from 'openrtc';
15
16
  import { OpenrtcProvider } from 'y-openrtc';
16
17
 
18
+ const rtc = OpenRTC({ apiKey: '<your public OpenRTC API key>' });
19
+ const room = await rtc.rooms.join('my-room', {
20
+ access: 'capability',
21
+ membership: 'ephemeral',
22
+ });
17
23
  const doc = new Y.Doc();
18
24
  const provider = new OpenrtcProvider('my-room', doc, {
19
- apiKey: '<your public OpenRTC API key>',
25
+ room,
20
26
  });
21
27
  ```
22
28
 
@@ -27,40 +33,32 @@ import * as Y from 'yjs';
27
33
  import { OpenRTC } from 'openrtc';
28
34
  import { OpenrtcProvider } from 'y-openrtc';
29
35
 
30
- const client = OpenRTC({
31
- apiKey: '<your public OpenRTC API key>',
36
+ const room = await rtc.rooms.join('private-room', {
37
+ access: 'authenticated',
38
+ auth,
32
39
  });
33
40
 
34
41
  const doc = new Y.Doc();
35
42
  const provider = new OpenrtcProvider('my-room', doc, {
36
- runtime: client,
43
+ room,
37
44
  });
38
45
  ```
39
46
 
40
47
  ## Transport and strict-mode options
41
48
 
42
- `OpenrtcProvider` forwards OpenRTC transport options when it creates its own runtime:
49
+ Configure advanced transports on the OpenRTC client, then pass the activated room:
43
50
 
44
51
  ```ts
45
- const provider = new OpenrtcProvider('private-room', doc, {
52
+ const rtc = OpenRTC({
46
53
  apiKey: '<your public OpenRTC API key>',
47
- strictMode: true,
48
54
  transports: {
49
55
  iroh: true,
50
- webrtc: {
51
- privacyMode: true,
52
- useTurn: true,
53
- },
56
+ webrtc: true,
54
57
  moq: true,
55
58
  },
56
- turnCredentialsProvider: async () => ({
57
- iceServers: [{
58
- urls: ['turn:turn.example.com:3478?transport=udp'],
59
- username: 'short-lived-user',
60
- credential: 'short-lived-credential',
61
- }],
62
- }),
63
59
  });
60
+ const room = await rtc.rooms.join('private-room', { access: 'capability' });
61
+ const provider = new OpenrtcProvider('private-room', doc, { room });
64
62
  ```
65
63
 
66
64
  Keep TURN credentials server-generated and short lived. Do not commit `.env` files or long-lived secrets.
@@ -69,6 +67,8 @@ Keep TURN credentials server-generated and short lived. Do not commit `.env` fil
69
67
 
70
68
  - `signaling` and `peerOpts` are accepted for compatibility, but ignored.
71
69
  - OpenRTC remains the source of truth for room membership and peer transport.
70
+ - Runtime connection events and room-membership changes drive peer attachment;
71
+ the provider does not poll or own base-connection retries.
72
72
  - Same-tab sync still prefers `BroadcastChannel` when available.
73
73
  - `password` encrypts provider payloads end-to-end on top of OpenRTC.
74
74
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { OpenrtcProvider, } from './provider.js';
2
2
  export { roomNameToOpenrtcRoomId } from './roomName.js';
3
- export type { OpenrtcAuthMode, OpenrtcProviderEvents, OpenrtcProviderOptions, OpenrtcTransports, PeersEventPayload, StatusEventPayload, SyncedEventPayload, } from './types.js';
3
+ export type { OpenrtcProviderEvents, OpenrtcProviderOptions, OpenrtcTransports, PeersEventPayload, StatusEventPayload, SyncedEventPayload, } from './types.js';
4
+ export { runtimeFromRoom } from './roomRuntime.js';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { OpenrtcProvider, } from './provider.js';
2
2
  export { roomNameToOpenrtcRoomId } from './roomName.js';
3
+ export { runtimeFromRoom } from './roomRuntime.js';
@@ -34,7 +34,6 @@ export declare class OpenrtcProvider {
34
34
  private localObserversAttached;
35
35
  private connectPromise;
36
36
  private reconcileTimer;
37
- private runtimeRefreshTimer;
38
37
  private readonly diagnostics;
39
38
  constructor(roomName: string, doc: Y.Doc, options?: OpenrtcProviderOptions);
40
39
  get connected(): boolean;
@@ -66,12 +65,12 @@ export declare class OpenrtcProvider {
66
65
  private hasBcNodeId;
67
66
  private isRelevantConnection;
68
67
  private refreshKnownRuntimeConnections;
69
- private startRuntimeConnectionRefresh;
70
68
  private resyncPeer;
71
69
  private sendInitialSync;
72
70
  private attachConnection;
73
71
  private sendToConnection;
74
72
  private sendControlToConnection;
73
+ private sendControlBestEffort;
75
74
  private sendBroadcastPayload;
76
75
  private broadcastProviderPayload;
77
76
  private broadcastInitialBcState;
package/dist/provider.js CHANGED
@@ -68,6 +68,9 @@ function isRoomFullError(error) {
68
68
  function isTransientStartupError(error) {
69
69
  return /relay-only endpoint ticket unavailable|failed to fetch|network|timed out/i.test(errorText(error));
70
70
  }
71
+ function isClosedConnectionSendError(error) {
72
+ return /connection is closed|closed connection|not open|writer is closed|application crypto is required but the connection is closed/i.test(errorText(error));
73
+ }
71
74
  function sleep(ms) {
72
75
  return new Promise((resolve) => setTimeout(resolve, ms));
73
76
  }
@@ -93,7 +96,6 @@ export class OpenrtcProvider {
93
96
  this.localObserversAttached = false;
94
97
  this.connectPromise = null;
95
98
  this.reconcileTimer = null;
96
- this.runtimeRefreshTimer = null;
97
99
  this.diagnostics = {
98
100
  openrtcSent: 0,
99
101
  openrtcReceived: 0,
@@ -262,9 +264,6 @@ export class OpenrtcProvider {
262
264
  async connectInternal() {
263
265
  const lease = acquireRuntimeLease({
264
266
  ...this.options,
265
- authMode: this.options.authMode ?? 'anonymous',
266
- allowAnonymousHostedDefaults: this.options.allowAnonymousHostedDefaults ?? !this.options.apiKey,
267
- space: this.options.space ?? this.options.spaceKey ?? (!this.options.apiKey ? this.roomName : undefined),
268
267
  });
269
268
  this.runtimeLease = lease;
270
269
  const runtime = await lease.ensureReady();
@@ -282,7 +281,6 @@ export class OpenrtcProvider {
282
281
  return;
283
282
  }
284
283
  this.attachConnection(connection);
285
- void this.reconcilePeers();
286
284
  });
287
285
  if (typeof runtime.onConnectionStateChange === 'function') {
288
286
  const stopRuntimeConnectionStates = runtime.onConnectionStateChange((state) => {
@@ -350,7 +348,6 @@ export class OpenrtcProvider {
350
348
  this.requestReconcile();
351
349
  });
352
350
  this.refreshKnownRuntimeConnections();
353
- this.startRuntimeConnectionRefresh();
354
351
  this.broadcastInitialBcState();
355
352
  this.requestReconcile();
356
353
  this.updateSyncedState();
@@ -508,7 +505,6 @@ export class OpenrtcProvider {
508
505
  }
509
506
  catch (error) {
510
507
  console.warn(`[y-openrtc] failed to connect to room peer ${member.nodeId}:`, error);
511
- this.requestReconcile();
512
508
  }
513
509
  finally {
514
510
  this.pendingNodeIds.delete(member.nodeId);
@@ -540,31 +536,22 @@ export class OpenrtcProvider {
540
536
  }
541
537
  }
542
538
  }
543
- startRuntimeConnectionRefresh() {
544
- if (this.runtimeRefreshTimer) {
545
- return;
546
- }
547
- this.runtimeRefreshTimer = setInterval(() => {
548
- this.refreshKnownRuntimeConnections();
549
- this.requestReconcile();
550
- }, 1000);
551
- }
552
539
  resyncPeer(remoteNodeId) {
553
540
  const record = this.openrtcConnections.get(remoteNodeId);
554
541
  if (!record) {
555
542
  return;
556
543
  }
557
544
  record.synced = false;
558
- void this.sendControlToConnection(record.connection, encodeSyncStep1(this.doc));
559
- void this.sendControlToConnection(record.connection, encodeAwarenessQuery());
545
+ this.sendControlBestEffort(record.connection, encodeSyncStep1(this.doc));
546
+ this.sendControlBestEffort(record.connection, encodeAwarenessQuery());
560
547
  this.updateSyncedState();
561
548
  }
562
549
  sendInitialSync(connection, record) {
563
- void this.sendControlToConnection(connection, encodeSyncStep1(this.doc));
564
- void this.sendControlToConnection(connection, encodeAwarenessQuery());
550
+ this.sendControlBestEffort(connection, encodeSyncStep1(this.doc));
551
+ this.sendControlBestEffort(connection, encodeAwarenessQuery());
565
552
  const awarenessStates = Array.from(this.awareness.getStates().keys());
566
553
  if (awarenessStates.length > 0) {
567
- void this.sendControlToConnection(connection, encodeAwarenessUpdate(this.awareness, awarenessStates));
554
+ this.sendControlBestEffort(connection, encodeAwarenessUpdate(this.awareness, awarenessStates));
568
555
  }
569
556
  for (const delayMs of [250, 1000, 3000]) {
570
557
  setTimeout(() => {
@@ -575,8 +562,8 @@ export class OpenrtcProvider {
575
562
  || this.openrtcConnections.get(connection.remoteNodeId)?.connection.id !== connection.id) {
576
563
  return;
577
564
  }
578
- void this.sendControlToConnection(connection, encodeSyncStep1(this.doc));
579
- void this.sendControlToConnection(connection, encodeAwarenessQuery());
565
+ this.sendControlBestEffort(connection, encodeSyncStep1(this.doc));
566
+ this.sendControlBestEffort(connection, encodeAwarenessQuery());
580
567
  }, delayMs);
581
568
  }
582
569
  }
@@ -604,7 +591,11 @@ export class OpenrtcProvider {
604
591
  return;
605
592
  }
606
593
  this.diagnostics.openrtcReceived += 1;
607
- void this.handleIncomingPayload(normalizeBinaryPayload(message), { type: 'openrtc', nodeId, connectionId: connection.id }, (reply) => this.sendControlToConnection(connection, reply), record);
594
+ void this.handleIncomingPayload(normalizeBinaryPayload(message), { type: 'openrtc', nodeId, connectionId: connection.id }, (reply) => this.sendControlToConnection(connection, reply), record).catch((error) => {
595
+ if (!isClosedConnectionSendError(error)) {
596
+ console.warn('[y-openrtc] incoming payload handler failed', error);
597
+ }
598
+ });
608
599
  });
609
600
  connection.onDisconnect(() => {
610
601
  if (!this.openrtcConnections.delete(nodeId)) {
@@ -612,7 +603,6 @@ export class OpenrtcProvider {
612
603
  }
613
604
  this.emitPeers([], [nodeId]);
614
605
  this.updateSyncedState();
615
- this.requestReconcile();
616
606
  });
617
607
  this.sendInitialSync(connection, record);
618
608
  }
@@ -642,6 +632,13 @@ export class OpenrtcProvider {
642
632
  }
643
633
  await connection.send(encrypted);
644
634
  }
635
+ sendControlBestEffort(connection, payload) {
636
+ void this.sendControlToConnection(connection, payload).catch((error) => {
637
+ if (!isClosedConnectionSendError(error)) {
638
+ console.warn('[y-openrtc] control send failed', error);
639
+ }
640
+ });
641
+ }
645
642
  async sendBroadcastPayload(payload) {
646
643
  const encrypted = await encryptFrame(payload, await this.keyPromise);
647
644
  this.diagnostics.broadcastSent += 1;
@@ -731,10 +728,6 @@ export class OpenrtcProvider {
731
728
  clearTimeout(this.reconcileTimer);
732
729
  this.reconcileTimer = null;
733
730
  }
734
- if (this.runtimeRefreshTimer) {
735
- clearInterval(this.runtimeRefreshTimer);
736
- this.runtimeRefreshTimer = null;
737
- }
738
731
  this.broadcastTransport?.disconnect();
739
732
  this.broadcastTransport = null;
740
733
  this.roomMembers.clear();
@@ -0,0 +1,4 @@
1
+ import type { OpenRTCRoomHandle } from 'openrtc';
2
+ import type { RuntimeClient } from 'openrtc/runtime';
3
+ /** One-handle adapter; cast is isolated here so provider code cannot see legacy construction. */
4
+ export declare function runtimeFromRoom(room: OpenRTCRoomHandle): RuntimeClient;
@@ -0,0 +1,63 @@
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
+ /** One-handle adapter; cast is isolated here so provider code cannot see legacy construction. */
10
+ export function runtimeFromRoom(room) {
11
+ const connections = () => room.diagnostics.connections();
12
+ const adapter = {
13
+ initialize: async () => undefined,
14
+ start: async () => undefined,
15
+ shutdown: async () => room.close(),
16
+ getNodeId: async () => {
17
+ const status = await room.diagnostics.status();
18
+ if (!status.localNodeId)
19
+ throw new Error('The activated OpenRTC room has no local peer identity.');
20
+ return status.localNodeId;
21
+ },
22
+ createRoom: async (roomId = room.id) => {
23
+ assertRoom(room, roomId);
24
+ return normalized(room.id);
25
+ },
26
+ joinRoom: async (roomId) => {
27
+ assertRoom(room, roomId);
28
+ return connections();
29
+ },
30
+ leaveRoom: async (roomId) => {
31
+ assertRoom(room, roomId);
32
+ await room.leave();
33
+ },
34
+ watchRoom: (roomId, callback) => {
35
+ assertRoom(room, roomId);
36
+ return room.peers.watch((peers) => callback(peers.map((peer) => ({
37
+ nodeId: peer.nodeId ?? peer.id,
38
+ userId: peer.userId,
39
+ ticket: peer.ticket,
40
+ joinedAt: Date.now(),
41
+ lastSeenAt: Date.now(),
42
+ }))));
43
+ },
44
+ onConnection: (callback) => {
45
+ const seen = new Set();
46
+ const publish = () => {
47
+ for (const connection of connections()) {
48
+ const id = connection.id ?? connection.remoteNodeId;
49
+ if (!id || seen.has(id))
50
+ continue;
51
+ seen.add(id);
52
+ callback(connection);
53
+ }
54
+ };
55
+ publish();
56
+ return room.peers.watch(publish);
57
+ },
58
+ onConnectionStateChange: room.diagnostics.onConnectionStateChange,
59
+ getConnections: connections,
60
+ connectByTicket: ({ ticket, timeoutMs }) => room.peers.connect({ ticket, timeoutMs }),
61
+ };
62
+ return adapter;
63
+ }
@@ -1,33 +1,5 @@
1
- import { OpenRTC } from 'openrtc';
2
- const ownedRuntimes = new Map();
1
+ import { runtimeFromRoom } from './roomRuntime.js';
3
2
  const runtimeStartPromises = new WeakMap();
4
- function stableStringify(value) {
5
- if (value === null || typeof value !== 'object') {
6
- return JSON.stringify(value);
7
- }
8
- if (Array.isArray(value)) {
9
- return `[${value.map((item) => stableStringify(item)).join(',')}]`;
10
- }
11
- const entries = Object.entries(value)
12
- .filter(([, current]) => current !== undefined)
13
- .sort(([left], [right]) => left.localeCompare(right));
14
- return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify(current)}`).join(',')}}`;
15
- }
16
- function runtimePoolKey(options) {
17
- const space = options.spaceKey ?? options.space;
18
- return stableStringify({
19
- apiKey: options.apiKey,
20
- authMode: options.authMode,
21
- allowAnonymousHostedDefaults: options.allowAnonymousHostedDefaults,
22
- space,
23
- hasSpaceTokenProvider: typeof options.spaceTokenProvider === 'function',
24
- storagePrefix: options.storagePrefix,
25
- nodeIdPersistence: options.nodeIdPersistence,
26
- transports: options.transports,
27
- strictMode: options.strictMode,
28
- hasTurnCredentialsProvider: typeof options.turnCredentialsProvider === 'function',
29
- });
30
- }
31
3
  async function ensureRuntimeReady(runtime) {
32
4
  let startPromise = runtimeStartPromises.get(runtime);
33
5
  if (!startPromise) {
@@ -41,53 +13,15 @@ async function ensureRuntimeReady(runtime) {
41
13
  return runtime;
42
14
  }
43
15
  export function acquireRuntimeLease(options) {
44
- if (options.runtime) {
45
- const runtime = options.runtime;
46
- return {
47
- runtime,
48
- owned: false,
49
- ensureReady: () => ensureRuntimeReady(runtime),
50
- release: async () => undefined,
51
- };
52
- }
53
- const key = runtimePoolKey(options);
54
- const space = options.spaceKey ?? options.space;
55
- let entry = ownedRuntimes.get(key);
56
- if (!entry) {
57
- entry = {
58
- runtime: OpenRTC({
59
- apiKey: options.apiKey,
60
- authMode: options.authMode,
61
- allowAnonymousHostedDefaults: options.allowAnonymousHostedDefaults,
62
- space,
63
- spaceTokenProvider: options.spaceTokenProvider,
64
- storagePrefix: options.storagePrefix,
65
- nodeIdPersistence: options.nodeIdPersistence,
66
- transports: options.transports,
67
- strictMode: options.strictMode,
68
- turnCredentialsProvider: options.turnCredentialsProvider,
69
- }),
70
- refs: 0,
71
- };
72
- ownedRuntimes.set(key, entry);
16
+ const runtime = options.runtime
17
+ ?? (options.room ? runtimeFromRoom(options.room) : null);
18
+ if (!runtime) {
19
+ throw new Error('y-openrtc requires an activated OpenRTC 2.0 room handle or an explicit openrtc/runtime client.');
73
20
  }
74
- entry.refs += 1;
75
21
  return {
76
- runtime: entry.runtime,
77
- owned: true,
78
- ensureReady: () => ensureRuntimeReady(entry.runtime),
79
- release: async () => {
80
- const current = ownedRuntimes.get(key);
81
- if (!current) {
82
- return;
83
- }
84
- current.refs -= 1;
85
- if (current.refs > 0) {
86
- return;
87
- }
88
- ownedRuntimes.delete(key);
89
- runtimeStartPromises.delete(current.runtime);
90
- current.runtime.stop();
91
- },
22
+ runtime,
23
+ owned: false,
24
+ ensureReady: () => ensureRuntimeReady(runtime),
25
+ release: async () => undefined,
92
26
  };
93
27
  }
package/dist/types.d.ts CHANGED
@@ -1,29 +1,15 @@
1
- import type { OpenRTC } from 'openrtc';
1
+ import type { OpenRTCOptions, OpenRTCRoomHandle } from 'openrtc';
2
2
  import type { RuntimeClient } from 'openrtc/runtime';
3
3
  import type { Awareness } from 'y-protocols/awareness';
4
- type OpenrtcClientOptions = Parameters<typeof OpenRTC>[0];
5
- export type OpenrtcTransports = OpenrtcClientOptions['transports'];
6
- export type OpenrtcAuthMode = OpenrtcClientOptions['authMode'];
7
- export type OpenrtcTurnCredentialsProvider = NonNullable<OpenrtcClientOptions>['turnCredentialsProvider'];
8
- export type OpenrtcAllowAnonymousHostedDefaults = NonNullable<OpenrtcClientOptions>['allowAnonymousHostedDefaults'];
9
- export type OpenrtcSpaceTokenProvider = NonNullable<OpenrtcClientOptions>['spaceTokenProvider'];
4
+ export type OpenrtcTransports = OpenRTCOptions['transports'];
10
5
  export interface OpenrtcProviderOptions {
11
- apiKey?: string;
12
6
  runtime?: RuntimeClient;
13
- authMode?: OpenrtcAuthMode;
14
- allowAnonymousHostedDefaults?: OpenrtcAllowAnonymousHostedDefaults;
15
- space?: string;
16
- spaceKey?: string;
17
- spaceTokenProvider?: OpenrtcSpaceTokenProvider;
7
+ /** Preferred 2.0 path: one already-activated collaborative room. */
8
+ room?: OpenRTCRoomHandle;
18
9
  awareness?: Awareness;
19
10
  password?: string | null;
20
11
  maxConns?: number;
21
12
  filterBcConns?: boolean;
22
- storagePrefix?: string;
23
- nodeIdPersistence?: 'persistent' | 'ephemeral';
24
- transports?: OpenrtcTransports;
25
- strictMode?: boolean;
26
- turnCredentialsProvider?: OpenrtcTurnCredentialsProvider;
27
13
  roomIdOverride?: string;
28
14
  /**
29
15
  * Ordered room ids to try when the preferred room is unavailable. This is
@@ -64,4 +50,3 @@ export interface OpenrtcProviderEvents {
64
50
  synced: SyncedEventPayload;
65
51
  peers: PeersEventPayload;
66
52
  }
67
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "y-openrtc",
3
- "version": "0.1.2",
3
+ "version": "2.0.0-rc.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -37,13 +37,13 @@
37
37
  "build": "rm -rf dist && tsc",
38
38
  "prepublishOnly": "pnpm run build",
39
39
  "test": "vitest run tests",
40
- "test:deterministic": "vitest run tests/TetrisSync.test.ts tests/TetrisPeerDiagnostic.test.ts tests/OpenrtcProvider.test.ts tests/PublicPackageGuard.test.ts tests/crypto.test.ts",
40
+ "test:deterministic": "vitest run tests/TetrisSync.test.ts tests/TetrisPeerDiagnostic.test.ts tests/OpenrtcProvider.test.ts tests/RoomRuntime.test.ts tests/PublicPackageGuard.test.ts tests/crypto.test.ts",
41
41
  "test:watch": "vitest",
42
42
  "test:live": "vitest run tests/OpenrtcProvider.live.test.ts"
43
43
  },
44
44
  "dependencies": {
45
45
  "lib0": "^0.2.114",
46
- "openrtc": "^0.2.0",
46
+ "openrtc": "2.0.0-rc.0",
47
47
  "y-protocols": "^1.0.6"
48
48
  },
49
49
  "peerDependencies": {