@affectively/dash 5.4.1 → 5.4.5

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.
Files changed (92) hide show
  1. package/README.md +8 -189
  2. package/dist/automerge_wasm_bg-4hg5vg2g.wasm +0 -0
  3. package/dist/engine/sqlite.d.ts +30 -0
  4. package/dist/engine/vec_extension.d.ts +2 -0
  5. package/dist/index.d.ts +73 -0
  6. package/dist/index.js +53895 -0
  7. package/dist/middleware/errorHandler.d.ts +60 -0
  8. package/dist/{src/sync → sync}/AeonDurableSync.d.ts +7 -9
  9. package/dist/sync/AeonDurableSync.js +1984 -0
  10. package/dist/{src/sync → sync}/AutomergeProvider.d.ts +8 -8
  11. package/dist/sync/AutomergeProvider.js +4421 -0
  12. package/dist/sync/HybridProvider.d.ts +124 -0
  13. package/dist/sync/HybridProvider.js +8328 -0
  14. package/dist/sync/connection/WebRTCConnection.d.ts +23 -0
  15. package/dist/sync/connection/WebRTCConnection.js +59 -0
  16. package/dist/sync/index.d.ts +13 -0
  17. package/dist/sync/index.js +12773 -0
  18. package/dist/sync/provider/YjsSqliteProvider.d.ts +17 -0
  19. package/dist/sync/provider/YjsSqliteProvider.js +54 -0
  20. package/dist/sync/types.d.ts +74 -0
  21. package/dist/sync/webtransport/WebTransportProvider.d.ts +16 -0
  22. package/dist/sync/webtransport/WebTransportProvider.js +55 -0
  23. package/package.json +62 -70
  24. package/dist/src/api/firebase/auth/index.d.ts +0 -137
  25. package/dist/src/api/firebase/auth/index.js +0 -352
  26. package/dist/src/api/firebase/auth/providers.d.ts +0 -254
  27. package/dist/src/api/firebase/auth/providers.js +0 -518
  28. package/dist/src/api/firebase/database/index.d.ts +0 -108
  29. package/dist/src/api/firebase/database/index.js +0 -368
  30. package/dist/src/api/firebase/errors.d.ts +0 -15
  31. package/dist/src/api/firebase/errors.js +0 -215
  32. package/dist/src/api/firebase/firestore/data-types.d.ts +0 -116
  33. package/dist/src/api/firebase/firestore/data-types.js +0 -280
  34. package/dist/src/api/firebase/firestore/index.d.ts +0 -7
  35. package/dist/src/api/firebase/firestore/index.js +0 -13
  36. package/dist/src/api/firebase/firestore/listeners.d.ts +0 -20
  37. package/dist/src/api/firebase/firestore/listeners.js +0 -50
  38. package/dist/src/api/firebase/firestore/operations.d.ts +0 -123
  39. package/dist/src/api/firebase/firestore/operations.js +0 -490
  40. package/dist/src/api/firebase/firestore/query.d.ts +0 -118
  41. package/dist/src/api/firebase/firestore/query.js +0 -418
  42. package/dist/src/api/firebase/index.d.ts +0 -11
  43. package/dist/src/api/firebase/index.js +0 -17
  44. package/dist/src/api/firebase/storage/index.d.ts +0 -100
  45. package/dist/src/api/firebase/storage/index.js +0 -286
  46. package/dist/src/api/firebase/types.d.ts +0 -341
  47. package/dist/src/api/firebase/types.js +0 -4
  48. package/dist/src/auth/manager.d.ts +0 -182
  49. package/dist/src/auth/manager.js +0 -598
  50. package/dist/src/engine/ai.js +0 -76
  51. package/dist/src/engine/sqlite.d.ts +0 -353
  52. package/dist/src/engine/sqlite.js +0 -1328
  53. package/dist/src/engine/vec_extension.d.ts +0 -5
  54. package/dist/src/engine/vec_extension.js +0 -10
  55. package/dist/src/index.d.ts +0 -21
  56. package/dist/src/index.js +0 -26
  57. package/dist/src/mcp/server.js +0 -87
  58. package/dist/src/reactivity/signal.js +0 -31
  59. package/dist/src/schema/lens.d.ts +0 -29
  60. package/dist/src/schema/lens.js +0 -122
  61. package/dist/src/sync/AeonDurableSync.js +0 -133
  62. package/dist/src/sync/AutomergeProvider.js +0 -153
  63. package/dist/src/sync/aeon/config.d.ts +0 -21
  64. package/dist/src/sync/aeon/config.js +0 -14
  65. package/dist/src/sync/aeon/delta-adapter.d.ts +0 -62
  66. package/dist/src/sync/aeon/delta-adapter.js +0 -98
  67. package/dist/src/sync/aeon/index.d.ts +0 -18
  68. package/dist/src/sync/aeon/index.js +0 -19
  69. package/dist/src/sync/aeon/offline-adapter.d.ts +0 -110
  70. package/dist/src/sync/aeon/offline-adapter.js +0 -227
  71. package/dist/src/sync/aeon/presence-adapter.d.ts +0 -114
  72. package/dist/src/sync/aeon/presence-adapter.js +0 -157
  73. package/dist/src/sync/aeon/schema-adapter.d.ts +0 -95
  74. package/dist/src/sync/aeon/schema-adapter.js +0 -163
  75. package/dist/src/sync/backup.d.ts +0 -12
  76. package/dist/src/sync/backup.js +0 -44
  77. package/dist/src/sync/connection.d.ts +0 -20
  78. package/dist/src/sync/connection.js +0 -50
  79. package/dist/src/sync/d1-provider.d.ts +0 -103
  80. package/dist/src/sync/d1-provider.js +0 -418
  81. package/dist/src/sync/hybrid-provider.d.ts +0 -307
  82. package/dist/src/sync/hybrid-provider.js +0 -1353
  83. package/dist/src/sync/provider.d.ts +0 -11
  84. package/dist/src/sync/provider.js +0 -67
  85. package/dist/src/sync/types.d.ts +0 -32
  86. package/dist/src/sync/types.js +0 -4
  87. package/dist/src/sync/verify.d.ts +0 -1
  88. package/dist/src/sync/verify.js +0 -23
  89. package/dist/tsconfig.tsbuildinfo +0 -1
  90. /package/dist/{src/engine → engine}/ai.d.ts +0 -0
  91. /package/dist/{src/mcp → mcp}/server.d.ts +0 -0
  92. /package/dist/{src/reactivity → reactivity}/signal.d.ts +0 -0
@@ -1,1353 +0,0 @@
1
- import * as Y from 'yjs';
2
- import { Observable } from 'lib0/observable';
3
- import * as encoding from 'lib0/encoding';
4
- import * as decoding from 'lib0/decoding';
5
- // @ts-ignore
6
- import * as syncProtocol from 'y-protocols/sync';
7
- // @ts-ignore
8
- import * as awarenessProtocol from 'y-protocols/awareness';
9
- // Aeon integration
10
- import { defaultAeonConfig } from './aeon/config.js';
11
- import { DashDeltaAdapter } from './aeon/delta-adapter.js';
12
- import { DashPresenceAdapter } from './aeon/presence-adapter.js';
13
- import { DashOfflineAdapter } from './aeon/offline-adapter.js';
14
- import { CompressionEngine, AdaptiveCompressionOptimizer, BatchTimingOptimizer, } from '@affectively/aeon';
15
- // Message types
16
- const MessageType = {
17
- Sync: 0,
18
- Awareness: 1,
19
- Delta: 2, // New: Aeon delta-compressed sync
20
- Compressed: 3,
21
- Encrypted: 4,
22
- Batch: 5,
23
- Capabilities: 6,
24
- };
25
- const CapabilityFlag = {
26
- Compression: 1,
27
- Encryption: 2,
28
- Batch: 4,
29
- };
30
- export class HybridProvider extends Observable {
31
- doc;
32
- ws = null;
33
- wt = null;
34
- connected = false;
35
- url;
36
- activeRelayUrl;
37
- roomName;
38
- awareness;
39
- writer = null;
40
- // "High Frequency" mode uses WebTransport for ephemeral data
41
- highFrequencyMode = false;
42
- // Connection state machine - prevents race conditions
43
- connectionState = 'disconnected';
44
- // Exponential backoff state
45
- reconnectAttempts = 0;
46
- maxReconnectDelay = 30000; // 30s max
47
- baseReconnectDelay = 1000; // 1s base
48
- reconnectTimeout = null;
49
- // Relay discovery state
50
- relayDiscovery;
51
- discoveredRelayUrls = [];
52
- relayDiscoveryCursor = 0;
53
- lastRelayDiscoveryAt = null;
54
- relayHealth = new Map();
55
- // Performance pipeline (Aeon optimization)
56
- relayPerformance;
57
- compressionEngine = null;
58
- adaptiveCompression = null;
59
- batchTiming = null;
60
- outboundQueue = [];
61
- outboundQueueBytes = 0;
62
- batchFlushTimeout = null;
63
- flushingBatch = false;
64
- lastCompressionRatio = null;
65
- // Privacy pipeline (room-scoped encryption)
66
- relayPrivacy;
67
- roomCryptoKey = null;
68
- roomCryptoKeyReady = null;
69
- localCapabilitiesBitmask = 0;
70
- peerCapabilities = new Map();
71
- protocolVersion = 2;
72
- lastCapabilityAdvertisedAt = null;
73
- // Aeon integration
74
- aeonConfig;
75
- deltaAdapter = null;
76
- presenceAdapter = null;
77
- offlineAdapter = null;
78
- constructor(url, roomName, doc, { awareness = new awarenessProtocol.Awareness(doc), aeonConfig = defaultAeonConfig, relayDiscovery = {}, relayPerformance = {}, relayPrivacy = {}, } = {}) {
79
- super();
80
- this.url = url;
81
- this.activeRelayUrl = url;
82
- this.roomName = roomName;
83
- this.doc = doc;
84
- this.awareness = awareness;
85
- this.aeonConfig = aeonConfig;
86
- this.relayDiscovery = {
87
- enabled: false,
88
- bootstrapUrls: [],
89
- discoveryPath: '/discovery',
90
- refreshIntervalMs: 60_000,
91
- requestTimeoutMs: 2_500,
92
- maxCandidates: 8,
93
- dhtQueryEnabled: true,
94
- ...relayDiscovery,
95
- };
96
- this.relayPerformance = {
97
- enableAdaptiveCompression: true,
98
- enableBatching: true,
99
- compressionThresholdBytes: 512,
100
- maxBatchDelayMs: 12,
101
- maxBatchSizeBytes: 64 * 1024,
102
- ...relayPerformance,
103
- };
104
- this.relayPrivacy = {
105
- enabled: false,
106
- ...relayPrivacy,
107
- };
108
- if (this.relayPerformance.enableAdaptiveCompression) {
109
- this.compressionEngine = new CompressionEngine('gzip');
110
- this.adaptiveCompression = new AdaptiveCompressionOptimizer();
111
- this.batchTiming = new BatchTimingOptimizer();
112
- this.seedAdaptiveNetworkProfile();
113
- }
114
- if (this.relayPrivacy.enabled) {
115
- this.roomCryptoKeyReady = this.initializeRoomCryptoKey();
116
- }
117
- this.localCapabilitiesBitmask = this.computeLocalCapabilities();
118
- // Initialize Aeon adapters
119
- if (aeonConfig.enableDeltaSync) {
120
- this.deltaAdapter = new DashDeltaAdapter(roomName, aeonConfig.deltaThreshold);
121
- }
122
- if (aeonConfig.enableRichPresence) {
123
- this.presenceAdapter = new DashPresenceAdapter(roomName, awareness);
124
- }
125
- if (aeonConfig.enableOfflineQueue) {
126
- this.offlineAdapter = new DashOfflineAdapter(doc, roomName, aeonConfig.maxOfflineQueueSize, aeonConfig.maxOfflineRetries);
127
- }
128
- this.doc.on('update', this.onDocUpdate.bind(this));
129
- this.awareness.on('update', this.onAwarenessUpdate.bind(this));
130
- this.connect();
131
- }
132
- connect() {
133
- this.connectWebSocket();
134
- }
135
- connectWebSocket() {
136
- // Guard: prevent multiple simultaneous connection attempts
137
- if (this.connectionState === 'connecting' || this.connectionState === 'connected') {
138
- return;
139
- }
140
- this.connectionState = 'connecting';
141
- this.emit('status', [{ status: 'connecting' }]);
142
- const connectionPromise = Promise.all([
143
- this.getAuthToken(),
144
- this.resolveRelayUrl(),
145
- ]);
146
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Auth token timeout (10s)')), 10000));
147
- Promise.race([connectionPromise, timeoutPromise])
148
- .then(result => {
149
- if (!result) {
150
- throw new Error('Connection setup timed out');
151
- }
152
- const [token, relayUrl] = result;
153
- // Validate token was obtained
154
- if (!token) {
155
- throw new Error('Auth token is empty or undefined');
156
- }
157
- // Double-check we weren't destroyed while waiting
158
- if (this.connectionState === 'disconnected') {
159
- return;
160
- }
161
- this.activeRelayUrl = relayUrl;
162
- this.peerCapabilities.clear();
163
- const wsUrl = relayUrl.replace(/^http/, 'ws') + '/sync/' + this.roomName + '?token=' + token;
164
- const connectionStartedAt = Date.now();
165
- this.ws = new WebSocket(wsUrl);
166
- this.ws.binaryType = 'arraybuffer';
167
- this.ws.onopen = () => {
168
- this.recordRelaySuccess(this.activeRelayUrl, Date.now() - connectionStartedAt);
169
- this.connectionState = 'connected';
170
- this.connected = true;
171
- this.reconnectAttempts = 0; // Reset backoff on successful connection
172
- this.emit('status', [{ status: 'connected' }]);
173
- this.sendSyncStep1();
174
- this.sendCapabilitiesAdvertisement(true).catch((err) => {
175
- console.warn('[Dash] Failed to advertise capabilities:', err);
176
- });
177
- // Process any queued offline updates
178
- if (this.offlineAdapter) {
179
- this.processOfflineQueue().catch(err => {
180
- console.warn('[Dash] Failed to process offline queue:', err);
181
- });
182
- }
183
- };
184
- this.ws.onmessage = (event) => {
185
- this.handleMessage(new Uint8Array(event.data)).catch((err) => {
186
- console.error('[Dash] Failed to handle message:', err);
187
- });
188
- };
189
- this.ws.onclose = (event) => {
190
- if (event.code !== 1000) {
191
- this.recordRelayFailure(this.activeRelayUrl);
192
- }
193
- this.connected = false;
194
- this.connectionState = 'disconnected';
195
- this.ws = null;
196
- this.emit('status', [{ status: 'disconnected', code: event.code, reason: event.reason }]);
197
- // Schedule reconnect with exponential backoff + jitter
198
- this.scheduleReconnect();
199
- };
200
- this.ws.onerror = (err) => {
201
- this.recordRelayFailure(this.activeRelayUrl);
202
- console.error('[Dash] WebSocket error:', err);
203
- this.emit('error', [{ type: 'websocket', error: err }]);
204
- // Note: onerror is always followed by onclose, so reconnect happens there
205
- };
206
- })
207
- .catch(err => {
208
- console.error('[Dash] Connection failed:', err.message);
209
- this.connectionState = 'disconnected';
210
- this.emit('error', [{ type: 'auth', error: err }]);
211
- // Schedule reconnect even on auth failure (token might refresh)
212
- this.scheduleReconnect();
213
- });
214
- }
215
- /**
216
- * Schedule reconnection with exponential backoff + jitter
217
- * Prevents thundering herd on network flapping
218
- */
219
- scheduleReconnect() {
220
- // Clear any existing timeout
221
- if (this.reconnectTimeout) {
222
- clearTimeout(this.reconnectTimeout);
223
- }
224
- this.reconnectAttempts++;
225
- // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max)
226
- const exponentialDelay = Math.min(this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
227
- // Add jitter: ±25% to prevent thundering herd
228
- const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
229
- const delay = Math.round(exponentialDelay + jitter);
230
- console.log(`[Dash] Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
231
- this.connectionState = 'reconnecting';
232
- this.emit('status', [{ status: 'reconnecting', attempt: this.reconnectAttempts, delay }]);
233
- this.reconnectTimeout = setTimeout(() => {
234
- this.connectionState = 'disconnected'; // Reset for new attempt
235
- this.connectWebSocket();
236
- }, delay);
237
- }
238
- seedAdaptiveNetworkProfile() {
239
- if (!this.adaptiveCompression || typeof navigator === 'undefined') {
240
- return;
241
- }
242
- const nav = navigator;
243
- const downlinkMbps = nav.connection?.downlink ?? 5;
244
- const rtt = nav.connection?.rtt ?? 50;
245
- this.adaptiveCompression.updateNetworkConditions(Math.round(downlinkMbps * 1000), rtt, true);
246
- this.batchTiming?.recordNetworkMeasurement(rtt, downlinkMbps);
247
- }
248
- async initializeRoomCryptoKey() {
249
- if (!this.relayPrivacy.enabled) {
250
- this.roomCryptoKey = null;
251
- return;
252
- }
253
- if (typeof crypto === 'undefined' || !crypto.subtle) {
254
- console.warn('[Dash] crypto.subtle unavailable; disabling relay privacy');
255
- this.relayPrivacy.enabled = false;
256
- this.roomCryptoKey = null;
257
- this.localCapabilitiesBitmask = this.computeLocalCapabilities();
258
- return;
259
- }
260
- const source = this.relayPrivacy.roomKey;
261
- if (!source) {
262
- console.warn('[Dash] relayPrivacy.enabled requires roomKey; disabling privacy');
263
- this.relayPrivacy.enabled = false;
264
- this.roomCryptoKey = null;
265
- this.localCapabilitiesBitmask = this.computeLocalCapabilities();
266
- return;
267
- }
268
- const keyBytes = await deriveRoomKeyBytes(source);
269
- this.roomCryptoKey = await crypto.subtle.importKey('raw', toArrayBuffer(keyBytes), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
270
- this.localCapabilitiesBitmask = this.computeLocalCapabilities();
271
- }
272
- async getRoomCryptoKey() {
273
- if (!this.relayPrivacy.enabled) {
274
- return null;
275
- }
276
- if (this.roomCryptoKey) {
277
- return this.roomCryptoKey;
278
- }
279
- if (!this.roomCryptoKeyReady) {
280
- this.roomCryptoKeyReady = this.initializeRoomCryptoKey();
281
- }
282
- await this.roomCryptoKeyReady;
283
- return this.roomCryptoKey;
284
- }
285
- computeLocalCapabilities() {
286
- let bitmask = 0;
287
- if (this.relayPerformance.enableAdaptiveCompression) {
288
- bitmask |= CapabilityFlag.Compression;
289
- }
290
- if (this.relayPrivacy.enabled) {
291
- bitmask |= CapabilityFlag.Encryption;
292
- }
293
- if (this.relayPerformance.enableBatching) {
294
- bitmask |= CapabilityFlag.Batch;
295
- }
296
- return bitmask;
297
- }
298
- getKnownRemoteClientIds() {
299
- const ids = [];
300
- this.awareness.getStates().forEach((_state, clientId) => {
301
- if (clientId !== this.awareness.clientID) {
302
- ids.push(clientId);
303
- }
304
- });
305
- return ids;
306
- }
307
- isCapabilityEnabled(flag) {
308
- if ((this.localCapabilitiesBitmask & flag) === 0) {
309
- return false;
310
- }
311
- const remoteIds = this.getKnownRemoteClientIds();
312
- if (remoteIds.length === 0) {
313
- return false;
314
- }
315
- return remoteIds.every((clientId) => {
316
- const capabilities = this.peerCapabilities.get(clientId) ?? 0;
317
- return (capabilities & flag) !== 0;
318
- });
319
- }
320
- capabilityListFromBitmask(bitmask) {
321
- const list = [];
322
- if ((bitmask & CapabilityFlag.Compression) !== 0)
323
- list.push('compression');
324
- if ((bitmask & CapabilityFlag.Encryption) !== 0)
325
- list.push('encryption');
326
- if ((bitmask & CapabilityFlag.Batch) !== 0)
327
- list.push('batch');
328
- return list;
329
- }
330
- async sendCapabilitiesAdvertisement(force = false) {
331
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
332
- return;
333
- }
334
- const now = Date.now();
335
- if (!force && this.lastCapabilityAdvertisedAt && now - this.lastCapabilityAdvertisedAt < 5_000) {
336
- return;
337
- }
338
- const encoder = encoding.createEncoder();
339
- encoding.writeVarUint(encoder, MessageType.Capabilities);
340
- encoding.writeVarUint(encoder, this.protocolVersion);
341
- encoding.writeVarUint(encoder, this.awareness.clientID);
342
- encoding.writeVarUint(encoder, this.localCapabilitiesBitmask);
343
- const payload = encoding.toUint8Array(encoder);
344
- await this.sendSingleFrame(payload, []);
345
- this.lastCapabilityAdvertisedAt = now;
346
- }
347
- reconcilePeerCapabilities(removed = []) {
348
- for (const clientId of removed) {
349
- this.peerCapabilities.delete(clientId);
350
- }
351
- const livePeers = new Set(this.getKnownRemoteClientIds());
352
- for (const clientId of this.peerCapabilities.keys()) {
353
- if (!livePeers.has(clientId)) {
354
- this.peerCapabilities.delete(clientId);
355
- }
356
- }
357
- }
358
- async getAuthToken() {
359
- try {
360
- // Lazy load auth to avoid circular dependency issues if any
361
- const { auth } = await import('../auth/manager.js');
362
- // For MVP, we assume the Relay allows any DID to connect if they sign it.
363
- // In prod, you'd know the Relay's DID.
364
- const token = await auth.issueRoomToken("did:web:relay.buley.dev", this.roomName);
365
- return token || 'dash-anonymous-token';
366
- }
367
- catch {
368
- // Graceful fallback for environments that don't initialize auth yet.
369
- return 'dash-anonymous-token';
370
- }
371
- }
372
- async resolveRelayUrl() {
373
- if (!this.relayDiscovery.enabled) {
374
- return this.activeRelayUrl;
375
- }
376
- const refreshMs = this.relayDiscovery.refreshIntervalMs ?? 60_000;
377
- const shouldRefresh = this.discoveredRelayUrls.length === 0 ||
378
- this.lastRelayDiscoveryAt === null ||
379
- Date.now() - this.lastRelayDiscoveryAt > refreshMs;
380
- if (shouldRefresh) {
381
- this.discoveredRelayUrls = await this.discoverRelayUrls();
382
- this.relayDiscoveryCursor = 0;
383
- this.lastRelayDiscoveryAt = Date.now();
384
- }
385
- if (this.discoveredRelayUrls.length === 0) {
386
- return this.activeRelayUrl;
387
- }
388
- const rankedRelays = this.rankRelayUrls(this.discoveredRelayUrls);
389
- const idx = this.relayDiscoveryCursor % rankedRelays.length;
390
- this.relayDiscoveryCursor += 1;
391
- return rankedRelays[idx];
392
- }
393
- async discoverRelayUrls() {
394
- const seedUrls = [
395
- this.activeRelayUrl,
396
- ...(this.relayDiscovery.bootstrapUrls ?? []),
397
- ];
398
- const uniqueSeeds = dedupeRelayUrls(seedUrls);
399
- const discoveryPath = (this.relayDiscovery.discoveryPath || '/discovery').replace(/\/+$/, '');
400
- const targetPeerId = normalizePeerId(this.relayDiscovery.targetPeerId || this.roomName);
401
- const discoveryRequests = uniqueSeeds.flatMap(seed => {
402
- const requests = [
403
- this.fetchRelayCandidates(`${seed}${discoveryPath}/bootstrap`),
404
- ];
405
- if (this.relayDiscovery.dhtQueryEnabled !== false) {
406
- requests.push(this.fetchRelayCandidates(`${seed}${discoveryPath}/peers?target=${encodeURIComponent(targetPeerId)}&k=${this.relayDiscovery.maxCandidates ?? 8}`));
407
- }
408
- return requests;
409
- });
410
- const results = await Promise.allSettled(discoveryRequests);
411
- const discovered = [this.activeRelayUrl];
412
- for (const result of results) {
413
- if (result.status === 'fulfilled') {
414
- discovered.push(...result.value);
415
- }
416
- }
417
- return dedupeRelayUrls(discovered).slice(0, this.relayDiscovery.maxCandidates ?? 8);
418
- }
419
- async fetchRelayCandidates(url) {
420
- const timeoutMs = this.relayDiscovery.requestTimeoutMs ?? 2_500;
421
- const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
422
- const timeout = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
423
- const startedAt = Date.now();
424
- try {
425
- const response = await fetch(url, {
426
- method: 'GET',
427
- signal: controller?.signal,
428
- });
429
- if (!response.ok) {
430
- const seedRelay = getRelayOriginFromDiscoveryUrl(url);
431
- if (seedRelay)
432
- this.recordRelayFailure(seedRelay);
433
- return [];
434
- }
435
- const payload = await response.json();
436
- const seedRelay = getRelayOriginFromDiscoveryUrl(url);
437
- if (seedRelay) {
438
- this.recordRelaySuccess(seedRelay, Date.now() - startedAt);
439
- }
440
- return extractRelayUrls(payload);
441
- }
442
- catch {
443
- const seedRelay = getRelayOriginFromDiscoveryUrl(url);
444
- if (seedRelay)
445
- this.recordRelayFailure(seedRelay);
446
- return [];
447
- }
448
- finally {
449
- if (timeout) {
450
- clearTimeout(timeout);
451
- }
452
- }
453
- }
454
- rankRelayUrls(relays) {
455
- return [...relays].sort((left, right) => this.scoreRelay(left) - this.scoreRelay(right));
456
- }
457
- scoreRelay(relayUrl) {
458
- const health = this.relayHealth.get(relayUrl);
459
- if (!health) {
460
- return 500;
461
- }
462
- const failurePenalty = health.failures * 250;
463
- const successBonus = Math.min(100, health.successes * 10);
464
- return health.avgLatencyMs + failurePenalty - successBonus;
465
- }
466
- recordRelaySuccess(relayUrl, latencyMs) {
467
- const current = this.relayHealth.get(relayUrl);
468
- if (!current) {
469
- this.relayHealth.set(relayUrl, {
470
- successes: 1,
471
- failures: 0,
472
- avgLatencyMs: Math.max(1, latencyMs),
473
- lastUpdatedAt: Date.now(),
474
- });
475
- return;
476
- }
477
- const nextSuccesses = current.successes + 1;
478
- const nextAvg = Math.round((current.avgLatencyMs * current.successes + latencyMs) / nextSuccesses);
479
- this.relayHealth.set(relayUrl, {
480
- successes: nextSuccesses,
481
- failures: current.failures,
482
- avgLatencyMs: Math.max(1, nextAvg),
483
- lastUpdatedAt: Date.now(),
484
- });
485
- }
486
- recordRelayFailure(relayUrl) {
487
- const current = this.relayHealth.get(relayUrl);
488
- if (!current) {
489
- this.relayHealth.set(relayUrl, {
490
- successes: 0,
491
- failures: 1,
492
- avgLatencyMs: 800,
493
- lastUpdatedAt: Date.now(),
494
- });
495
- return;
496
- }
497
- this.relayHealth.set(relayUrl, {
498
- ...current,
499
- failures: current.failures + 1,
500
- avgLatencyMs: Math.min(5_000, current.avgLatencyMs + 100),
501
- lastUpdatedAt: Date.now(),
502
- });
503
- }
504
- async enterHighFrequencyMode() {
505
- if (this.highFrequencyMode)
506
- return;
507
- try {
508
- // Connect WebTransport
509
- // Just an example URL construction, depends on server routing
510
- const wtUrl = this.activeRelayUrl + '/sync/' + this.roomName;
511
- this.wt = new WebTransport(wtUrl);
512
- await this.wt.ready;
513
- const stream = await this.wt.createBidirectionalStream();
514
- this.writer = stream.writable;
515
- this.readLoop(stream.readable);
516
- this.highFrequencyMode = true;
517
- console.log('Upgraded to WebTransport (High Frequency Mode)');
518
- }
519
- catch (e) {
520
- console.error('Failed to upgrade to WebTransport', e);
521
- // Fallback: stay on WebSocket
522
- }
523
- }
524
- sendSyncStep1() {
525
- const encoder = encoding.createEncoder();
526
- encoding.writeVarUint(encoder, 0); // MessageType.Sync
527
- syncProtocol.writeSyncStep1(encoder, this.doc);
528
- this.send(encoding.toUint8Array(encoder));
529
- }
530
- /**
531
- * Read loop for WebTransport with dynamic buffer sizing
532
- * Handles payloads larger than initial buffer
533
- */
534
- async readLoop(readable) {
535
- let reader;
536
- const isByob = typeof ReadableStreamBYOBReader !== 'undefined';
537
- try {
538
- // @ts-ignore
539
- reader = isByob ? readable.getReader({ mode: 'byob' }) : readable.getReader();
540
- }
541
- catch (e) {
542
- reader = readable.getReader();
543
- }
544
- // Start with 64KB, grow dynamically if needed (max 16MB)
545
- const MIN_BUFFER_SIZE = 65536; // 64KB
546
- const MAX_BUFFER_SIZE = 16777216; // 16MB
547
- let bufferSize = MIN_BUFFER_SIZE;
548
- let buffer = new Uint8Array(bufferSize);
549
- let consecutiveLargeReads = 0;
550
- try {
551
- while (true) {
552
- let result;
553
- if (isByob && reader.read.length > 0) {
554
- // BYOB Reader - pre-allocate buffer
555
- result = await reader.read(new Uint8Array(buffer.buffer, 0, buffer.byteLength));
556
- if (result.value) {
557
- buffer = result.value;
558
- // Dynamic buffer growth: if we're consistently filling the buffer, grow it
559
- if (result.value.byteLength >= bufferSize * 0.9) {
560
- consecutiveLargeReads++;
561
- if (consecutiveLargeReads >= 3 && bufferSize < MAX_BUFFER_SIZE) {
562
- bufferSize = Math.min(bufferSize * 2, MAX_BUFFER_SIZE);
563
- buffer = new Uint8Array(bufferSize);
564
- console.log(`[Dash] Grew WebTransport buffer to ${bufferSize / 1024}KB`);
565
- }
566
- }
567
- else {
568
- consecutiveLargeReads = 0;
569
- }
570
- }
571
- }
572
- else {
573
- // Default Reader
574
- result = await reader.read();
575
- }
576
- if (result.done) {
577
- console.log('[Dash] WebTransport stream ended gracefully');
578
- break;
579
- }
580
- if (result.value) {
581
- this.handleMessage(result.value).catch((err) => {
582
- console.error('[Dash] Failed to process WebTransport message:', err);
583
- });
584
- }
585
- }
586
- }
587
- catch (e) {
588
- // Check if this is a recoverable error
589
- const errorMessage = e.message || '';
590
- if (errorMessage.includes('aborted') || errorMessage.includes('closed')) {
591
- console.log('[Dash] WebTransport connection closed');
592
- }
593
- else {
594
- console.error('[Dash] WebTransport read loop error:', e);
595
- this.emit('error', [{ type: 'webtransport', error: e }]);
596
- }
597
- }
598
- finally {
599
- this.highFrequencyMode = false;
600
- this.writer = null;
601
- this.wt = null;
602
- // If we were in high-frequency mode and it failed, fall back to WebSocket
603
- if (this.connectionState === 'connected') {
604
- console.log('[Dash] WebTransport closed, continuing with WebSocket');
605
- }
606
- }
607
- }
608
- /**
609
- * Handle incoming message with protocol validation
610
- * Never silently ignores malformed messages
611
- */
612
- async handleMessage(buf) {
613
- // Validate minimum message size
614
- if (buf.byteLength < 1) {
615
- console.warn('[Dash] Received empty message, ignoring');
616
- return;
617
- }
618
- try {
619
- const decoder = decoding.createDecoder(buf);
620
- // Check if we can read the message type (decoder has remaining bytes)
621
- if (decoder.pos >= decoder.arr.length) {
622
- console.warn('[Dash] Message too short to decode type');
623
- return;
624
- }
625
- const messageType = decoding.readVarUint(decoder);
626
- switch (messageType) {
627
- case MessageType.Sync:
628
- syncProtocol.readSyncMessage(decoder, encoding.createEncoder(), this.doc, this);
629
- break;
630
- case MessageType.Awareness:
631
- awarenessProtocol.applyAwarenessUpdate(this.awareness, decoding.readVarUint8Array(decoder), this);
632
- break;
633
- case MessageType.Delta:
634
- // Handle Aeon delta-compressed sync
635
- if (this.deltaAdapter) {
636
- const deltaPayloadBytes = decoding.readVarUint8Array(decoder);
637
- const deltaPayload = this.deltaAdapter.decodePayload(deltaPayloadBytes);
638
- const update = this.deltaAdapter.unwrapDelta(deltaPayload);
639
- Y.applyUpdate(this.doc, update, this);
640
- }
641
- else {
642
- console.warn('[Dash] Received Delta message but delta adapter not enabled');
643
- }
644
- break;
645
- case MessageType.Compressed: {
646
- const algorithmCode = decoding.readVarUint(decoder);
647
- const compressedPayload = decoding.readVarUint8Array(decoder);
648
- const decompressed = await this.decompressPayload(compressedPayload, algorithmCode === 1 ? 'gzip' : algorithmCode === 2 ? 'deflate' : 'none');
649
- await this.handleMessage(decompressed);
650
- break;
651
- }
652
- case MessageType.Encrypted: {
653
- const iv = decoding.readVarUint8Array(decoder);
654
- const ciphertext = decoding.readVarUint8Array(decoder);
655
- const plaintext = await this.decryptPayload(iv, ciphertext);
656
- if (plaintext) {
657
- await this.handleMessage(plaintext);
658
- }
659
- break;
660
- }
661
- case MessageType.Batch: {
662
- const count = decoding.readVarUint(decoder);
663
- for (let i = 0; i < count; i++) {
664
- const frame = decoding.readVarUint8Array(decoder);
665
- await this.handleMessage(frame);
666
- }
667
- break;
668
- }
669
- case MessageType.Capabilities: {
670
- const remoteProtocolVersion = decoding.readVarUint(decoder);
671
- const remoteClientId = decoding.readVarUint(decoder);
672
- const remoteCapabilities = decoding.readVarUint(decoder);
673
- this.peerCapabilities.set(remoteClientId, remoteCapabilities);
674
- this.emit('status', [{
675
- status: 'capabilities',
676
- protocolVersion: remoteProtocolVersion,
677
- clientId: remoteClientId,
678
- capabilities: this.capabilityListFromBitmask(remoteCapabilities),
679
- }]);
680
- break;
681
- }
682
- default:
683
- // CRITICAL: Don't silently ignore unknown message types
684
- console.warn(`[Dash] Unknown message type: ${messageType}. Protocol mismatch possible.`);
685
- this.emit('error', [{
686
- type: 'protocol',
687
- error: new Error(`Unknown message type: ${messageType}`),
688
- messageType
689
- }]);
690
- break;
691
- }
692
- }
693
- catch (err) {
694
- // Don't let malformed messages crash the sync loop
695
- console.error('[Dash] Failed to decode message:', err);
696
- this.emit('error', [{
697
- type: 'decode',
698
- error: err,
699
- bufferSize: buf.byteLength
700
- }]);
701
- }
702
- }
703
- /**
704
- * Send message with error handling and offline queue fallback
705
- * Never fails silently - either sends, queues, or throws
706
- */
707
- async send(message) {
708
- if (this.isCapabilityEnabled(CapabilityFlag.Batch)) {
709
- this.enqueueOutbound(message);
710
- return;
711
- }
712
- const processed = await this.applyOutboundPipeline(message);
713
- await this.sendSingleFrame(processed, [message]);
714
- }
715
- enqueueOutbound(message) {
716
- this.outboundQueue.push(message);
717
- this.outboundQueueBytes += message.byteLength;
718
- if (this.outboundQueueBytes >= (this.relayPerformance.maxBatchSizeBytes ?? 64 * 1024)) {
719
- this.flushOutboundQueue().catch((err) => {
720
- console.warn('[Dash] Failed to flush outbound queue:', err);
721
- });
722
- return;
723
- }
724
- this.scheduleBatchFlush();
725
- }
726
- scheduleBatchFlush() {
727
- if (this.batchFlushTimeout || this.flushingBatch) {
728
- return;
729
- }
730
- const maxDelay = this.relayPerformance.maxBatchDelayMs ?? 12;
731
- const decision = this.batchTiming?.getSchedulingDecision(this.outboundQueueBytes, 'normal', false);
732
- const delay = Math.max(0, Math.min(maxDelay, decision?.recommendedDelay ?? maxDelay));
733
- this.batchFlushTimeout = setTimeout(() => {
734
- this.batchFlushTimeout = null;
735
- this.flushOutboundQueue().catch((err) => {
736
- console.warn('[Dash] Failed to flush outbound queue:', err);
737
- });
738
- }, delay);
739
- }
740
- async flushOutboundQueue() {
741
- if (this.flushingBatch || this.outboundQueue.length === 0) {
742
- return;
743
- }
744
- this.flushingBatch = true;
745
- try {
746
- const batch = this.outboundQueue.splice(0, this.outboundQueue.length);
747
- this.outboundQueueBytes = 0;
748
- const frame = batch.length === 1 ? batch[0] : this.encodeBatchFrame(batch);
749
- const processed = await this.applyOutboundPipeline(frame);
750
- await this.sendSingleFrame(processed, batch);
751
- }
752
- finally {
753
- this.flushingBatch = false;
754
- }
755
- }
756
- encodeBatchFrame(messages) {
757
- const encoder = encoding.createEncoder();
758
- encoding.writeVarUint(encoder, MessageType.Batch);
759
- encoding.writeVarUint(encoder, messages.length);
760
- for (const message of messages) {
761
- encoding.writeVarUint8Array(encoder, message);
762
- }
763
- return encoding.toUint8Array(encoder);
764
- }
765
- async applyOutboundPipeline(message) {
766
- let payload = message;
767
- payload = await this.maybeCompressPayload(payload);
768
- payload = await this.maybeEncryptPayload(payload);
769
- return payload;
770
- }
771
- async maybeCompressPayload(message) {
772
- if (!this.compressionEngine ||
773
- !this.relayPerformance.enableAdaptiveCompression ||
774
- !this.isCapabilityEnabled(CapabilityFlag.Compression)) {
775
- return message;
776
- }
777
- if (message.byteLength < (this.relayPerformance.compressionThresholdBytes ?? 512)) {
778
- return message;
779
- }
780
- const recommendation = this.adaptiveCompression?.getRecommendedLevel();
781
- if (recommendation && recommendation.recommendedLevel <= 3) {
782
- return message;
783
- }
784
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
785
- const compressed = await this.compressionEngine.compress(message);
786
- const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt;
787
- this.adaptiveCompression?.recordCompressionPerformance(recommendation?.recommendedLevel ?? 6, elapsed, compressed.compressionRatio);
788
- this.adaptiveCompression?.applyRecommendation();
789
- this.lastCompressionRatio = compressed.compressionRatio;
790
- if (compressed.algorithm === 'none') {
791
- return message;
792
- }
793
- // Keep payload uncompressed if ratio is poor.
794
- if (compressed.compressedSize >= message.byteLength * 0.97) {
795
- return message;
796
- }
797
- const encoder = encoding.createEncoder();
798
- encoding.writeVarUint(encoder, MessageType.Compressed);
799
- encoding.writeVarUint(encoder, compressed.algorithm === 'gzip' ? 1 : 2);
800
- encoding.writeVarUint8Array(encoder, compressed.compressed);
801
- return encoding.toUint8Array(encoder);
802
- }
803
- async decompressPayload(compressedPayload, algorithm) {
804
- if (!this.compressionEngine || algorithm === 'none') {
805
- return compressedPayload;
806
- }
807
- return this.compressionEngine.decompress({
808
- id: `incoming-${Date.now()}-${Math.random().toString(36).slice(2)}`,
809
- compressed: compressedPayload,
810
- originalSize: compressedPayload.byteLength,
811
- compressedSize: compressedPayload.byteLength,
812
- compressionRatio: 0,
813
- algorithm,
814
- timestamp: Date.now(),
815
- });
816
- }
817
- async maybeEncryptPayload(message) {
818
- if (!this.relayPrivacy.enabled || !this.isCapabilityEnabled(CapabilityFlag.Encryption)) {
819
- return message;
820
- }
821
- const key = await this.getRoomCryptoKey();
822
- if (!key) {
823
- return message;
824
- }
825
- if (typeof crypto === 'undefined' || !crypto.subtle) {
826
- return message;
827
- }
828
- const iv = crypto.getRandomValues(new Uint8Array(12));
829
- const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, key, toArrayBuffer(message));
830
- const encoder = encoding.createEncoder();
831
- encoding.writeVarUint(encoder, MessageType.Encrypted);
832
- encoding.writeVarUint8Array(encoder, iv);
833
- encoding.writeVarUint8Array(encoder, new Uint8Array(encrypted));
834
- return encoding.toUint8Array(encoder);
835
- }
836
- async decryptPayload(iv, ciphertext) {
837
- const key = await this.getRoomCryptoKey();
838
- if (!key) {
839
- console.warn('[Dash] Received encrypted payload but no room key is configured');
840
- return null;
841
- }
842
- if (typeof crypto === 'undefined' || !crypto.subtle) {
843
- return null;
844
- }
845
- try {
846
- const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, key, toArrayBuffer(ciphertext));
847
- return new Uint8Array(decrypted);
848
- }
849
- catch (err) {
850
- this.emit('error', [{ type: 'privacy', error: err }]);
851
- console.warn('[Dash] Failed to decrypt relay payload:', err);
852
- return null;
853
- }
854
- }
855
- async sendSingleFrame(payload, originalMessages) {
856
- // Try WebTransport first (high frequency mode)
857
- if (this.writer && this.highFrequencyMode) {
858
- try {
859
- const writer = this.writer.getWriter();
860
- await writer.write(payload);
861
- writer.releaseLock();
862
- return;
863
- }
864
- catch (err) {
865
- console.warn('[Dash] WebTransport send failed, falling back to WebSocket:', err);
866
- }
867
- }
868
- // Try WebSocket
869
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
870
- try {
871
- if (this.ws.bufferedAmount > 1024 * 1024) {
872
- console.warn('[Dash] WebSocket buffer full, queueing message');
873
- for (const message of originalMessages) {
874
- this.queueForOffline(message);
875
- }
876
- return;
877
- }
878
- this.ws.send(payload);
879
- return;
880
- }
881
- catch (err) {
882
- console.warn('[Dash] WebSocket send failed:', err);
883
- }
884
- }
885
- // Neither transport available - queue for later
886
- for (const message of originalMessages) {
887
- this.queueForOffline(message);
888
- }
889
- }
890
- /**
891
- * Queue message for offline sync
892
- */
893
- queueForOffline(message) {
894
- if (this.offlineAdapter) {
895
- // Extract the update from the message for proper queuing
896
- // The message includes protocol header, but offline adapter expects raw update
897
- try {
898
- const decoder = decoding.createDecoder(message);
899
- const messageType = decoding.readVarUint(decoder);
900
- if (messageType === MessageType.Sync || messageType === MessageType.Delta) {
901
- const updateBytes = decoding.readVarUint8Array(decoder);
902
- this.offlineAdapter.queueUpdate(updateBytes);
903
- console.log('[Dash] Message queued for offline sync');
904
- }
905
- }
906
- catch (err) {
907
- console.error('[Dash] Failed to queue message:', err);
908
- }
909
- }
910
- else {
911
- console.warn('[Dash] Message dropped - no transport and no offline queue');
912
- this.emit('error', [{ type: 'send', error: new Error('No transport available and offline queue disabled') }]);
913
- }
914
- }
915
- onDocUpdate(update, origin) {
916
- if (origin === this)
917
- return;
918
- // Check if we should queue (offline)
919
- if (this.offlineAdapter?.shouldQueue()) {
920
- this.offlineAdapter.queueUpdate(update);
921
- return;
922
- }
923
- // Use delta compression if enabled
924
- if (this.deltaAdapter) {
925
- const deltaPayload = this.deltaAdapter.wrapUpdate(update, origin);
926
- const encoder = encoding.createEncoder();
927
- encoding.writeVarUint(encoder, MessageType.Delta);
928
- encoding.writeVarUint8Array(encoder, this.deltaAdapter.encodePayload(deltaPayload));
929
- this.send(encoding.toUint8Array(encoder));
930
- }
931
- else {
932
- // Standard Yjs sync
933
- const encoder = encoding.createEncoder();
934
- encoding.writeVarUint(encoder, MessageType.Sync);
935
- syncProtocol.writeUpdate(encoder, update);
936
- this.send(encoding.toUint8Array(encoder));
937
- }
938
- }
939
- onAwarenessUpdate({ added, updated, removed }, origin) {
940
- if (origin === this) {
941
- this.reconcilePeerCapabilities(removed);
942
- if (Array.isArray(added) && added.length > 0) {
943
- this.sendCapabilitiesAdvertisement(false).catch((err) => {
944
- console.warn('[Dash] Failed to re-advertise capabilities:', err);
945
- });
946
- }
947
- return;
948
- }
949
- const changedClients = added.concat(updated).concat(removed);
950
- const encoder = encoding.createEncoder();
951
- encoding.writeVarUint(encoder, 1); // MessageType.Awareness
952
- encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
953
- this.send(encoding.toUint8Array(encoder));
954
- }
955
- destroy() {
956
- // Cancel any pending reconnect
957
- if (this.reconnectTimeout) {
958
- clearTimeout(this.reconnectTimeout);
959
- this.reconnectTimeout = null;
960
- }
961
- if (this.batchFlushTimeout) {
962
- clearTimeout(this.batchFlushTimeout);
963
- this.batchFlushTimeout = null;
964
- }
965
- this.outboundQueue = [];
966
- this.outboundQueueBytes = 0;
967
- this.peerCapabilities.clear();
968
- // Mark as disconnected to prevent reconnect attempts
969
- this.connectionState = 'disconnected';
970
- this.doc.off('update', this.onDocUpdate);
971
- this.awareness.off('update', this.onAwarenessUpdate);
972
- this.ws?.close();
973
- this.wt?.close();
974
- // Cleanup Aeon adapters
975
- this.presenceAdapter?.destroy();
976
- this.offlineAdapter?.destroy();
977
- this.connected = false;
978
- super.destroy();
979
- }
980
- // ============================================
981
- // AEON ADAPTER ACCESSORS
982
- // ============================================
983
- /**
984
- * Get the delta adapter for compression stats
985
- */
986
- getDeltaAdapter() {
987
- return this.deltaAdapter;
988
- }
989
- /**
990
- * Get the presence adapter for rich presence features
991
- */
992
- getPresenceAdapter() {
993
- return this.presenceAdapter;
994
- }
995
- /**
996
- * Get the offline adapter for queue management
997
- */
998
- getOfflineAdapter() {
999
- return this.offlineAdapter;
1000
- }
1001
- /**
1002
- * Get Aeon configuration
1003
- */
1004
- getAeonConfig() {
1005
- return this.aeonConfig;
1006
- }
1007
- /**
1008
- * Process offline queue when back online
1009
- */
1010
- async processOfflineQueue() {
1011
- if (!this.offlineAdapter) {
1012
- return { synced: 0, failed: 0 };
1013
- }
1014
- return this.offlineAdapter.processQueue(async (update) => {
1015
- const encoder = encoding.createEncoder();
1016
- if (this.deltaAdapter) {
1017
- const deltaPayload = this.deltaAdapter.wrapUpdate(update);
1018
- encoding.writeVarUint(encoder, MessageType.Delta);
1019
- encoding.writeVarUint8Array(encoder, this.deltaAdapter.encodePayload(deltaPayload));
1020
- }
1021
- else {
1022
- encoding.writeVarUint(encoder, MessageType.Sync);
1023
- syncProtocol.writeUpdate(encoder, update);
1024
- }
1025
- await this.send(encoding.toUint8Array(encoder));
1026
- });
1027
- }
1028
- // ============================================
1029
- // INTROSPECTION METHODS FOR DASH-STUDIO
1030
- // ============================================
1031
- /**
1032
- * Get comprehensive connection status
1033
- */
1034
- getConnectionStatus() {
1035
- let wsState = 'closed';
1036
- if (this.ws) {
1037
- switch (this.ws.readyState) {
1038
- case WebSocket.CONNECTING:
1039
- wsState = 'connecting';
1040
- break;
1041
- case WebSocket.OPEN:
1042
- wsState = 'open';
1043
- break;
1044
- case WebSocket.CLOSING:
1045
- wsState = 'closing';
1046
- break;
1047
- case WebSocket.CLOSED:
1048
- wsState = 'closed';
1049
- break;
1050
- }
1051
- }
1052
- return {
1053
- connected: this.connected,
1054
- connectionState: this.connectionState,
1055
- roomName: this.roomName,
1056
- url: this.activeRelayUrl,
1057
- reconnectAttempts: this.reconnectAttempts,
1058
- websocket: {
1059
- state: wsState,
1060
- connected: this.ws !== null && this.ws.readyState === WebSocket.OPEN,
1061
- bufferedAmount: this.ws?.bufferedAmount ?? 0
1062
- },
1063
- webTransport: {
1064
- connected: this.wt !== null,
1065
- highFrequencyMode: this.highFrequencyMode
1066
- },
1067
- discovery: {
1068
- enabled: this.relayDiscovery.enabled === true,
1069
- knownRelays: this.discoveredRelayUrls.length,
1070
- lastDiscoveryAt: this.lastRelayDiscoveryAt,
1071
- targetPeerId: normalizePeerId(this.relayDiscovery.targetPeerId || this.roomName),
1072
- },
1073
- performance: {
1074
- batching: this.relayPerformance.enableBatching === true,
1075
- adaptiveCompression: this.relayPerformance.enableAdaptiveCompression === true,
1076
- activeBatching: this.isCapabilityEnabled(CapabilityFlag.Batch),
1077
- activeCompression: this.isCapabilityEnabled(CapabilityFlag.Compression),
1078
- queuedFrames: this.outboundQueue.length,
1079
- lastCompressionRatio: this.lastCompressionRatio,
1080
- },
1081
- privacy: {
1082
- enabled: this.relayPrivacy.enabled === true,
1083
- keyLoaded: this.roomCryptoKey !== null,
1084
- activeEncryption: this.isCapabilityEnabled(CapabilityFlag.Encryption),
1085
- },
1086
- protocol: {
1087
- version: this.protocolVersion,
1088
- localCapabilities: this.capabilityListFromBitmask(this.localCapabilitiesBitmask),
1089
- knownPeers: this.peerCapabilities.size,
1090
- },
1091
- transport: this.highFrequencyMode ? 'WebTransport' : 'WebSocket'
1092
- };
1093
- }
1094
- /**
1095
- * Force immediate reconnection (resets backoff)
1096
- * Use when user explicitly requests reconnect
1097
- */
1098
- forceReconnect() {
1099
- if (this.reconnectTimeout) {
1100
- clearTimeout(this.reconnectTimeout);
1101
- this.reconnectTimeout = null;
1102
- }
1103
- // Close existing connections
1104
- this.ws?.close();
1105
- this.wt?.close();
1106
- this.ws = null;
1107
- this.wt = null;
1108
- // Reset backoff
1109
- this.reconnectAttempts = 0;
1110
- this.connectionState = 'disconnected';
1111
- // Connect immediately
1112
- this.connectWebSocket();
1113
- }
1114
- /**
1115
- * Get all awareness states from connected peers
1116
- */
1117
- getAwarenessStates() {
1118
- const states = [];
1119
- const awarenessStates = this.awareness.getStates();
1120
- awarenessStates.forEach((state, clientId) => {
1121
- states.push({
1122
- clientId,
1123
- state,
1124
- isLocal: clientId === this.awareness.clientID
1125
- });
1126
- });
1127
- return states;
1128
- }
1129
- /**
1130
- * Get the local client ID
1131
- */
1132
- getLocalClientId() {
1133
- return this.awareness.clientID;
1134
- }
1135
- /**
1136
- * Get connected peer count (excluding local)
1137
- */
1138
- getPeerCount() {
1139
- const states = this.awareness.getStates();
1140
- return Math.max(0, states.size - 1); // Exclude self
1141
- }
1142
- /**
1143
- * Get document state information
1144
- */
1145
- getDocumentState() {
1146
- const stateVector = Y.encodeStateVector(this.doc);
1147
- const update = Y.encodeStateAsUpdate(this.doc);
1148
- // Get shared types info
1149
- const sharedTypes = [];
1150
- this.doc.share.forEach((type, name) => {
1151
- let typeKind = 'unknown';
1152
- let size = 0;
1153
- if (type instanceof Y.Map) {
1154
- typeKind = 'YMap';
1155
- size = type.size;
1156
- }
1157
- else if (type instanceof Y.Array) {
1158
- typeKind = 'YArray';
1159
- size = type.length;
1160
- }
1161
- else if (type instanceof Y.Text) {
1162
- typeKind = 'YText';
1163
- size = type.length;
1164
- }
1165
- else if (type instanceof Y.XmlFragment) {
1166
- typeKind = 'YXmlFragment';
1167
- size = type.length;
1168
- }
1169
- sharedTypes.push({
1170
- name,
1171
- type: typeKind,
1172
- size
1173
- });
1174
- });
1175
- return {
1176
- clientId: this.doc.clientID,
1177
- guid: this.doc.guid,
1178
- stateVectorSize: stateVector.byteLength,
1179
- updateSize: update.byteLength,
1180
- sharedTypes,
1181
- transactionCount: 0, // Yjs doesn't expose this directly
1182
- gcEnabled: this.doc.gc
1183
- };
1184
- }
1185
- /**
1186
- * Get a snapshot of the Yjs document for inspection
1187
- */
1188
- getDocumentSnapshot() {
1189
- const content = {};
1190
- this.doc.share.forEach((type, name) => {
1191
- try {
1192
- if (type instanceof Y.Map) {
1193
- content[name] = type.toJSON();
1194
- }
1195
- else if (type instanceof Y.Array) {
1196
- content[name] = type.toJSON();
1197
- }
1198
- else if (type instanceof Y.Text) {
1199
- content[name] = type.toString();
1200
- }
1201
- else if (type instanceof Y.XmlFragment) {
1202
- content[name] = type.toString();
1203
- }
1204
- else {
1205
- content[name] = '[unsupported type]';
1206
- }
1207
- }
1208
- catch (e) {
1209
- content[name] = '[error reading content]';
1210
- }
1211
- });
1212
- return {
1213
- timestamp: new Date().toISOString(),
1214
- roomName: this.roomName,
1215
- content
1216
- };
1217
- }
1218
- /**
1219
- * Set local awareness state for introspection (admin view)
1220
- */
1221
- setLocalAwareness(state) {
1222
- this.awareness.setLocalState(state);
1223
- }
1224
- /**
1225
- * Get full provider status for debugging
1226
- */
1227
- getProviderStatus() {
1228
- return {
1229
- connection: this.getConnectionStatus(),
1230
- awareness: {
1231
- localClientId: this.awareness.clientID,
1232
- peerCount: this.getPeerCount(),
1233
- states: this.getAwarenessStates()
1234
- },
1235
- document: this.getDocumentState(),
1236
- aeon: this.getAeonStatus()
1237
- };
1238
- }
1239
- /**
1240
- * Get Aeon-specific status
1241
- */
1242
- getAeonStatus() {
1243
- return {
1244
- enabled: {
1245
- deltaSync: this.aeonConfig.enableDeltaSync,
1246
- richPresence: this.aeonConfig.enableRichPresence,
1247
- offlineQueue: this.aeonConfig.enableOfflineQueue,
1248
- },
1249
- delta: this.deltaAdapter?.getStats() ?? null,
1250
- offline: this.offlineAdapter?.getStats() ?? null,
1251
- presence: this.presenceAdapter?.getStats() ?? null,
1252
- compression: this.adaptiveCompression?.getStats() ?? null,
1253
- };
1254
- }
1255
- }
1256
- function normalizePeerId(value) {
1257
- const trimmed = value.trim().toLowerCase();
1258
- if (/^[0-9a-f]+$/i.test(trimmed)) {
1259
- return trimmed;
1260
- }
1261
- let hash = 2166136261;
1262
- for (let i = 0; i < trimmed.length; i++) {
1263
- hash ^= trimmed.charCodeAt(i);
1264
- hash = Math.imul(hash, 16777619);
1265
- }
1266
- return (hash >>> 0).toString(16).padStart(8, '0');
1267
- }
1268
- function normalizeRelayUrl(url) {
1269
- try {
1270
- const parsed = new URL(url);
1271
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
1272
- return null;
1273
- }
1274
- return parsed.origin;
1275
- }
1276
- catch {
1277
- return null;
1278
- }
1279
- }
1280
- function dedupeRelayUrls(urls) {
1281
- const deduped = new Set();
1282
- for (const url of urls) {
1283
- const normalized = normalizeRelayUrl(url);
1284
- if (normalized)
1285
- deduped.add(normalized);
1286
- }
1287
- return Array.from(deduped);
1288
- }
1289
- function extractRelayUrls(payload) {
1290
- if (!payload || typeof payload !== 'object') {
1291
- return [];
1292
- }
1293
- const data = payload;
1294
- const urls = [];
1295
- const collect = (value) => {
1296
- if (typeof value === 'string') {
1297
- urls.push(value);
1298
- return;
1299
- }
1300
- if (!value || typeof value !== 'object') {
1301
- return;
1302
- }
1303
- const record = value;
1304
- if (typeof record.relayUrl === 'string') {
1305
- urls.push(record.relayUrl);
1306
- }
1307
- else if (typeof record.url === 'string') {
1308
- urls.push(record.url);
1309
- }
1310
- };
1311
- const candidates = ['bootstrapRelays', 'relays', 'peers'];
1312
- for (const key of candidates) {
1313
- const value = data[key];
1314
- if (Array.isArray(value)) {
1315
- for (const item of value) {
1316
- collect(item);
1317
- }
1318
- }
1319
- }
1320
- return dedupeRelayUrls(urls);
1321
- }
1322
- function getRelayOriginFromDiscoveryUrl(url) {
1323
- try {
1324
- const parsed = new URL(url);
1325
- return `${parsed.protocol}//${parsed.host}`;
1326
- }
1327
- catch {
1328
- return null;
1329
- }
1330
- }
1331
- async function deriveRoomKeyBytes(input) {
1332
- if (input instanceof Uint8Array) {
1333
- if (input.byteLength === 32) {
1334
- return input;
1335
- }
1336
- if (typeof crypto !== 'undefined' && crypto.subtle) {
1337
- const digest = await crypto.subtle.digest('SHA-256', toArrayBuffer(input));
1338
- return new Uint8Array(digest);
1339
- }
1340
- return input;
1341
- }
1342
- const encoded = new TextEncoder().encode(input);
1343
- if (typeof crypto !== 'undefined' && crypto.subtle) {
1344
- const digest = await crypto.subtle.digest('SHA-256', toArrayBuffer(encoded));
1345
- return new Uint8Array(digest);
1346
- }
1347
- return encoded;
1348
- }
1349
- function toArrayBuffer(data) {
1350
- const clone = new Uint8Array(data.byteLength);
1351
- clone.set(data);
1352
- return clone.buffer;
1353
- }