y-openrtc 0.1.1 → 1.0.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
@@ -69,6 +69,8 @@ Keep TURN credentials server-generated and short lived. Do not commit `.env` fil
69
69
 
70
70
  - `signaling` and `peerOpts` are accepted for compatibility, but ignored.
71
71
  - OpenRTC remains the source of truth for room membership and peer transport.
72
+ - Runtime connection events and room-membership changes drive peer attachment;
73
+ the provider does not poll or own base-connection retries.
72
74
  - Same-tab sync still prefers `BroadcastChannel` when available.
73
75
  - `password` encrypts provider payloads end-to-end on top of OpenRTC.
74
76
 
@@ -79,7 +81,6 @@ The live test at `tests/OpenrtcProvider.live.test.ts` reads package-local env va
79
81
  Create `openrtc/packages/y-openrtc/.env.local` with:
80
82
 
81
83
  ```bash
82
- OPENRTC_LIVE_TESTS=1
83
84
  OPENRTC_Y_OPENRTC_API_KEY=<your public OpenRTC API key>
84
85
  ```
85
86
 
@@ -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;
@@ -49,7 +48,9 @@ export declare class OpenrtcProvider {
49
48
  private warnCompatibilityOptions;
50
49
  private emitStatus;
51
50
  private emitSynced;
51
+ private currentPeersPayload;
52
52
  private emitPeers;
53
+ private replayEvent;
53
54
  private attachLocalObservers;
54
55
  private detachLocalObservers;
55
56
  private connectInternal;
@@ -64,12 +65,12 @@ export declare class OpenrtcProvider {
64
65
  private hasBcNodeId;
65
66
  private isRelevantConnection;
66
67
  private refreshKnownRuntimeConnections;
67
- private startRuntimeConnectionRefresh;
68
68
  private resyncPeer;
69
69
  private sendInitialSync;
70
70
  private attachConnection;
71
71
  private sendToConnection;
72
72
  private sendControlToConnection;
73
+ private sendControlBestEffort;
73
74
  private sendBroadcastPayload;
74
75
  private broadcastProviderPayload;
75
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,
@@ -129,6 +131,9 @@ export class OpenrtcProvider {
129
131
  }
130
132
  const clients = added.concat(updated, removed);
131
133
  void this.broadcastProviderPayload(encodeAwarenessUpdate(this.awareness, clients));
134
+ if (added.concat(removed).some((clientId) => clientId !== this.doc.clientID)) {
135
+ this.emitPeers([], []);
136
+ }
132
137
  };
133
138
  this.destroyHandler = () => {
134
139
  void this.destroyInternal();
@@ -144,6 +149,7 @@ export class OpenrtcProvider {
144
149
  const listeners = this.listeners.get(name) ?? new Set();
145
150
  listeners.add(listener);
146
151
  this.listeners.set(name, listeners);
152
+ this.replayEvent(name, listener);
147
153
  return listener;
148
154
  }
149
155
  off(name, listener) {
@@ -154,7 +160,9 @@ export class OpenrtcProvider {
154
160
  this.off(name, wrapped);
155
161
  listener(payload);
156
162
  });
157
- this.on(name, wrapped);
163
+ const listeners = this.listeners.get(name) ?? new Set();
164
+ listeners.add(wrapped);
165
+ this.listeners.set(name, listeners);
158
166
  }
159
167
  emit(name, payload) {
160
168
  for (const listener of this.listeners.get(name) ?? []) {
@@ -206,16 +214,37 @@ export class OpenrtcProvider {
206
214
  this.synced = nextSynced;
207
215
  this.emit('synced', { synced: nextSynced });
208
216
  }
209
- emitPeers(added, removed) {
210
- const payload = {
217
+ currentPeersPayload(added = [], removed = []) {
218
+ return {
211
219
  added,
212
220
  removed,
213
221
  openrtcPeers: Array.from(this.openrtcConnections.keys()),
214
222
  webrtcPeers: Array.from(this.openrtcConnections.keys()),
215
223
  bcPeers: Array.from(this.bcPeers.keys()),
224
+ awarenessPeers: Array.from(this.awareness.getStates().keys())
225
+ .filter((clientId) => clientId !== this.doc.clientID),
216
226
  };
227
+ }
228
+ emitPeers(added, removed) {
229
+ const payload = this.currentPeersPayload(added, removed);
217
230
  this.emit('peers', payload);
218
231
  }
232
+ replayEvent(name, listener) {
233
+ queueMicrotask(() => {
234
+ if (this.destroyed || !this.listeners.get(name)?.has(listener)) {
235
+ return;
236
+ }
237
+ if (name === 'status') {
238
+ listener({ connected: this.connected });
239
+ return;
240
+ }
241
+ if (name === 'synced') {
242
+ listener({ synced: this.synced });
243
+ return;
244
+ }
245
+ listener(this.currentPeersPayload());
246
+ });
247
+ }
219
248
  attachLocalObservers() {
220
249
  if (this.localObserversAttached) {
221
250
  return;
@@ -255,7 +284,6 @@ export class OpenrtcProvider {
255
284
  return;
256
285
  }
257
286
  this.attachConnection(connection);
258
- void this.reconcilePeers();
259
287
  });
260
288
  if (typeof runtime.onConnectionStateChange === 'function') {
261
289
  const stopRuntimeConnectionStates = runtime.onConnectionStateChange((state) => {
@@ -323,7 +351,6 @@ export class OpenrtcProvider {
323
351
  this.requestReconcile();
324
352
  });
325
353
  this.refreshKnownRuntimeConnections();
326
- this.startRuntimeConnectionRefresh();
327
354
  this.broadcastInitialBcState();
328
355
  this.requestReconcile();
329
356
  this.updateSyncedState();
@@ -481,7 +508,6 @@ export class OpenrtcProvider {
481
508
  }
482
509
  catch (error) {
483
510
  console.warn(`[y-openrtc] failed to connect to room peer ${member.nodeId}:`, error);
484
- this.requestReconcile();
485
511
  }
486
512
  finally {
487
513
  this.pendingNodeIds.delete(member.nodeId);
@@ -513,31 +539,22 @@ export class OpenrtcProvider {
513
539
  }
514
540
  }
515
541
  }
516
- startRuntimeConnectionRefresh() {
517
- if (this.runtimeRefreshTimer) {
518
- return;
519
- }
520
- this.runtimeRefreshTimer = setInterval(() => {
521
- this.refreshKnownRuntimeConnections();
522
- this.requestReconcile();
523
- }, 1000);
524
- }
525
542
  resyncPeer(remoteNodeId) {
526
543
  const record = this.openrtcConnections.get(remoteNodeId);
527
544
  if (!record) {
528
545
  return;
529
546
  }
530
547
  record.synced = false;
531
- void this.sendControlToConnection(record.connection, encodeSyncStep1(this.doc));
532
- void this.sendControlToConnection(record.connection, encodeAwarenessQuery());
548
+ this.sendControlBestEffort(record.connection, encodeSyncStep1(this.doc));
549
+ this.sendControlBestEffort(record.connection, encodeAwarenessQuery());
533
550
  this.updateSyncedState();
534
551
  }
535
552
  sendInitialSync(connection, record) {
536
- void this.sendControlToConnection(connection, encodeSyncStep1(this.doc));
537
- void this.sendControlToConnection(connection, encodeAwarenessQuery());
553
+ this.sendControlBestEffort(connection, encodeSyncStep1(this.doc));
554
+ this.sendControlBestEffort(connection, encodeAwarenessQuery());
538
555
  const awarenessStates = Array.from(this.awareness.getStates().keys());
539
556
  if (awarenessStates.length > 0) {
540
- void this.sendControlToConnection(connection, encodeAwarenessUpdate(this.awareness, awarenessStates));
557
+ this.sendControlBestEffort(connection, encodeAwarenessUpdate(this.awareness, awarenessStates));
541
558
  }
542
559
  for (const delayMs of [250, 1000, 3000]) {
543
560
  setTimeout(() => {
@@ -548,8 +565,8 @@ export class OpenrtcProvider {
548
565
  || this.openrtcConnections.get(connection.remoteNodeId)?.connection.id !== connection.id) {
549
566
  return;
550
567
  }
551
- void this.sendControlToConnection(connection, encodeSyncStep1(this.doc));
552
- void this.sendControlToConnection(connection, encodeAwarenessQuery());
568
+ this.sendControlBestEffort(connection, encodeSyncStep1(this.doc));
569
+ this.sendControlBestEffort(connection, encodeAwarenessQuery());
553
570
  }, delayMs);
554
571
  }
555
572
  }
@@ -577,7 +594,11 @@ export class OpenrtcProvider {
577
594
  return;
578
595
  }
579
596
  this.diagnostics.openrtcReceived += 1;
580
- void this.handleIncomingPayload(normalizeBinaryPayload(message), { type: 'openrtc', nodeId, connectionId: connection.id }, (reply) => this.sendControlToConnection(connection, reply), record);
597
+ void this.handleIncomingPayload(normalizeBinaryPayload(message), { type: 'openrtc', nodeId, connectionId: connection.id }, (reply) => this.sendControlToConnection(connection, reply), record).catch((error) => {
598
+ if (!isClosedConnectionSendError(error)) {
599
+ console.warn('[y-openrtc] incoming payload handler failed', error);
600
+ }
601
+ });
581
602
  });
582
603
  connection.onDisconnect(() => {
583
604
  if (!this.openrtcConnections.delete(nodeId)) {
@@ -585,7 +606,6 @@ export class OpenrtcProvider {
585
606
  }
586
607
  this.emitPeers([], [nodeId]);
587
608
  this.updateSyncedState();
588
- this.requestReconcile();
589
609
  });
590
610
  this.sendInitialSync(connection, record);
591
611
  }
@@ -615,6 +635,13 @@ export class OpenrtcProvider {
615
635
  }
616
636
  await connection.send(encrypted);
617
637
  }
638
+ sendControlBestEffort(connection, payload) {
639
+ void this.sendControlToConnection(connection, payload).catch((error) => {
640
+ if (!isClosedConnectionSendError(error)) {
641
+ console.warn('[y-openrtc] control send failed', error);
642
+ }
643
+ });
644
+ }
618
645
  async sendBroadcastPayload(payload) {
619
646
  const encrypted = await encryptFrame(payload, await this.keyPromise);
620
647
  this.diagnostics.broadcastSent += 1;
@@ -704,10 +731,6 @@ export class OpenrtcProvider {
704
731
  clearTimeout(this.reconcileTimer);
705
732
  this.reconcileTimer = null;
706
733
  }
707
- if (this.runtimeRefreshTimer) {
708
- clearInterval(this.runtimeRefreshTimer);
709
- this.runtimeRefreshTimer = null;
710
- }
711
734
  this.broadcastTransport?.disconnect();
712
735
  this.broadcastTransport = null;
713
736
  this.roomMembers.clear();
package/dist/types.d.ts CHANGED
@@ -52,6 +52,12 @@ export interface PeersEventPayload {
52
52
  openrtcPeers: string[];
53
53
  webrtcPeers: string[];
54
54
  bcPeers: string[];
55
+ /**
56
+ * Remote Yjs awareness client ids currently visible to this provider.
57
+ * This is the best source for participant counts in collaborative UIs,
58
+ * because OpenRTC may use a mesh subset or BroadcastChannel for transport.
59
+ */
60
+ awarenessPeers: number[];
55
61
  }
56
62
  export interface OpenrtcProviderEvents {
57
63
  status: StatusEventPayload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "y-openrtc",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,14 +34,16 @@
34
34
  "./package.json": "./package.json"
35
35
  },
36
36
  "scripts": {
37
- "build": "tsc",
37
+ "build": "rm -rf dist && tsc",
38
+ "prepublishOnly": "pnpm run build",
38
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",
39
41
  "test:watch": "vitest",
40
- "test:live": "OPENRTC_LIVE_TESTS=1 vitest run tests/OpenrtcProvider.live.test.ts"
42
+ "test:live": "vitest run tests/OpenrtcProvider.live.test.ts"
41
43
  },
42
44
  "dependencies": {
43
45
  "lib0": "^0.2.114",
44
- "openrtc": "workspace:*",
46
+ "openrtc": "^1.0.0",
45
47
  "y-protocols": "^1.0.6"
46
48
  },
47
49
  "peerDependencies": {