@affectively/dash 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/auth/manager.d.ts +171 -1
- package/dist/src/auth/manager.js +557 -11
- package/dist/src/engine/sqlite.d.ts +135 -0
- package/dist/src/engine/sqlite.js +298 -0
- package/dist/src/sync/aeon/config.d.ts +21 -0
- package/dist/src/sync/aeon/config.js +14 -0
- package/dist/src/sync/aeon/delta-adapter.d.ts +62 -0
- package/dist/src/sync/aeon/delta-adapter.js +98 -0
- package/dist/src/sync/aeon/index.d.ts +18 -0
- package/dist/src/sync/aeon/index.js +19 -0
- package/dist/src/sync/aeon/offline-adapter.d.ts +110 -0
- package/dist/src/sync/aeon/offline-adapter.js +223 -0
- package/dist/src/sync/aeon/presence-adapter.d.ts +114 -0
- package/dist/src/sync/aeon/presence-adapter.js +157 -0
- package/dist/src/sync/aeon/schema-adapter.d.ts +95 -0
- package/dist/src/sync/aeon/schema-adapter.js +163 -0
- package/dist/src/sync/hybrid-provider.d.ts +146 -2
- package/dist/src/sync/hybrid-provider.js +291 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Adapter for Dash
|
|
3
|
+
*
|
|
4
|
+
* Bridges Aeon's SchemaVersionManager with Dash's LensEngine.
|
|
5
|
+
* Provides migration tracking, version compatibility checks, and audit trails.
|
|
6
|
+
*/
|
|
7
|
+
import { SchemaVersionManager, MigrationEngine, MigrationTracker, } from '@affectively/aeon';
|
|
8
|
+
/**
|
|
9
|
+
* Adapter that bridges Aeon versioning with Dash's Lens system
|
|
10
|
+
*/
|
|
11
|
+
export class DashSchemaAdapter {
|
|
12
|
+
versionManager;
|
|
13
|
+
migrationEngine;
|
|
14
|
+
migrationTracker;
|
|
15
|
+
lensEngine;
|
|
16
|
+
constructor(lensEngine) {
|
|
17
|
+
this.versionManager = new SchemaVersionManager();
|
|
18
|
+
this.migrationEngine = new MigrationEngine();
|
|
19
|
+
this.migrationTracker = new MigrationTracker();
|
|
20
|
+
this.lensEngine = lensEngine;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Register a schema version with associated Lens operations
|
|
24
|
+
*/
|
|
25
|
+
registerVersion(version, description, lensOps, breaking = false) {
|
|
26
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
27
|
+
// Register with Aeon
|
|
28
|
+
this.versionManager.registerVersion({
|
|
29
|
+
major,
|
|
30
|
+
minor,
|
|
31
|
+
patch,
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
description,
|
|
34
|
+
breaking,
|
|
35
|
+
});
|
|
36
|
+
// Register Lens if provided
|
|
37
|
+
if (lensOps && major > 1) {
|
|
38
|
+
this.lensEngine.registerLens(major - 1, major, lensOps);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register a migration for a version upgrade
|
|
43
|
+
*/
|
|
44
|
+
registerMigration(id, fromVersion, toVersion, migrate, rollback) {
|
|
45
|
+
const migration = {
|
|
46
|
+
id,
|
|
47
|
+
name: `${fromVersion} -> ${toVersion}`,
|
|
48
|
+
version: toVersion,
|
|
49
|
+
description: `Migrate from ${fromVersion} to ${toVersion}`,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
up: migrate,
|
|
52
|
+
down: rollback,
|
|
53
|
+
};
|
|
54
|
+
this.migrationEngine.registerMigration(migration);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute a migration on data
|
|
58
|
+
*/
|
|
59
|
+
async executeMigration(migrationId, data) {
|
|
60
|
+
const result = await this.migrationEngine.executeMigration(migrationId, data);
|
|
61
|
+
// Track the migration
|
|
62
|
+
const record = {
|
|
63
|
+
id: `record-${Date.now()}`,
|
|
64
|
+
migrationId,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
version: result.migrationId,
|
|
67
|
+
direction: 'up',
|
|
68
|
+
status: result.success ? 'applied' : 'failed',
|
|
69
|
+
duration: result.duration,
|
|
70
|
+
itemsAffected: result.itemsAffected,
|
|
71
|
+
errorMessage: result.errors.length > 0 ? result.errors.join(', ') : undefined,
|
|
72
|
+
appliedBy: 'dash-schema-adapter',
|
|
73
|
+
};
|
|
74
|
+
this.migrationTracker.recordMigration(record);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Rollback a migration
|
|
79
|
+
*/
|
|
80
|
+
async rollbackMigration(migrationId, data) {
|
|
81
|
+
const result = await this.migrationEngine.rollbackMigration(migrationId, data);
|
|
82
|
+
// Track the rollback
|
|
83
|
+
const record = {
|
|
84
|
+
id: `record-${Date.now()}`,
|
|
85
|
+
migrationId,
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
version: result.migrationId,
|
|
88
|
+
direction: 'down',
|
|
89
|
+
status: result.success ? 'rolled-back' : 'failed',
|
|
90
|
+
duration: result.duration,
|
|
91
|
+
itemsAffected: result.itemsAffected,
|
|
92
|
+
errorMessage: result.errors.length > 0 ? result.errors.join(', ') : undefined,
|
|
93
|
+
appliedBy: 'dash-schema-adapter',
|
|
94
|
+
};
|
|
95
|
+
this.migrationTracker.recordMigration(record);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Migrate data using Lens transformations
|
|
100
|
+
*/
|
|
101
|
+
migrateWithLens(data, fromVersion, toVersion) {
|
|
102
|
+
return this.lensEngine.migrate(data, fromVersion, toVersion);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get current version
|
|
106
|
+
*/
|
|
107
|
+
getCurrentVersion() {
|
|
108
|
+
return this.versionManager.getCurrentVersion();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get a specific version by string
|
|
112
|
+
*/
|
|
113
|
+
getVersion(version) {
|
|
114
|
+
return this.versionManager.getVersion(version);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get migration by ID
|
|
118
|
+
*/
|
|
119
|
+
getMigration(id) {
|
|
120
|
+
return this.migrationEngine.getMigration(id);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if migration is needed
|
|
124
|
+
*/
|
|
125
|
+
needsMigration(fromVersion, toVersion) {
|
|
126
|
+
return this.versionManager.canMigrate(fromVersion, toVersion);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get migration path between versions
|
|
130
|
+
*/
|
|
131
|
+
getMigrationPath(fromVersion, toVersion) {
|
|
132
|
+
const from = this.versionManager.parseVersion(fromVersion);
|
|
133
|
+
const to = this.versionManager.parseVersion(toVersion);
|
|
134
|
+
return this.versionManager.getMigrationPath(from, to);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get pending migrations
|
|
138
|
+
*/
|
|
139
|
+
getPendingMigrations() {
|
|
140
|
+
return this.migrationEngine.getPendingMigrations();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get migration statistics
|
|
144
|
+
*/
|
|
145
|
+
getStatistics() {
|
|
146
|
+
return {
|
|
147
|
+
engine: this.migrationEngine.getStatistics(),
|
|
148
|
+
tracker: this.migrationTracker.getStatistics(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Format version for display
|
|
153
|
+
*/
|
|
154
|
+
formatVersion(version) {
|
|
155
|
+
return `${version.major}.${version.minor}.${version.patch}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a schema adapter from an existing LensEngine
|
|
160
|
+
*/
|
|
161
|
+
export function createSchemaAdapter(lensEngine) {
|
|
162
|
+
return new DashSchemaAdapter(lensEngine);
|
|
163
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { Observable } from 'lib0/observable';
|
|
3
3
|
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
4
|
+
import { type AeonConfig } from './aeon/config.js';
|
|
5
|
+
import { DashDeltaAdapter } from './aeon/delta-adapter.js';
|
|
6
|
+
import { DashPresenceAdapter } from './aeon/presence-adapter.js';
|
|
7
|
+
import { DashOfflineAdapter } from './aeon/offline-adapter.js';
|
|
4
8
|
export declare class HybridProvider extends Observable<string> {
|
|
5
9
|
private doc;
|
|
6
10
|
private ws;
|
|
@@ -11,8 +15,13 @@ export declare class HybridProvider extends Observable<string> {
|
|
|
11
15
|
awareness: awarenessProtocol.Awareness;
|
|
12
16
|
private writer;
|
|
13
17
|
private highFrequencyMode;
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
private aeonConfig;
|
|
19
|
+
private deltaAdapter;
|
|
20
|
+
private presenceAdapter;
|
|
21
|
+
private offlineAdapter;
|
|
22
|
+
constructor(url: string, roomName: string, doc: Y.Doc, { awareness, aeonConfig, }?: {
|
|
23
|
+
awareness?: awarenessProtocol.Awareness;
|
|
24
|
+
aeonConfig?: AeonConfig;
|
|
16
25
|
});
|
|
17
26
|
private connect;
|
|
18
27
|
private connectWebSocket;
|
|
@@ -25,4 +34,139 @@ export declare class HybridProvider extends Observable<string> {
|
|
|
25
34
|
private onDocUpdate;
|
|
26
35
|
private onAwarenessUpdate;
|
|
27
36
|
destroy(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get the delta adapter for compression stats
|
|
39
|
+
*/
|
|
40
|
+
getDeltaAdapter(): DashDeltaAdapter | null;
|
|
41
|
+
/**
|
|
42
|
+
* Get the presence adapter for rich presence features
|
|
43
|
+
*/
|
|
44
|
+
getPresenceAdapter(): DashPresenceAdapter | null;
|
|
45
|
+
/**
|
|
46
|
+
* Get the offline adapter for queue management
|
|
47
|
+
*/
|
|
48
|
+
getOfflineAdapter(): DashOfflineAdapter | null;
|
|
49
|
+
/**
|
|
50
|
+
* Get Aeon configuration
|
|
51
|
+
*/
|
|
52
|
+
getAeonConfig(): AeonConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Process offline queue when back online
|
|
55
|
+
*/
|
|
56
|
+
processOfflineQueue(): Promise<{
|
|
57
|
+
synced: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Get comprehensive connection status
|
|
62
|
+
*/
|
|
63
|
+
getConnectionStatus(): ConnectionStatus;
|
|
64
|
+
/**
|
|
65
|
+
* Get all awareness states from connected peers
|
|
66
|
+
*/
|
|
67
|
+
getAwarenessStates(): AwarenessState[];
|
|
68
|
+
/**
|
|
69
|
+
* Get the local client ID
|
|
70
|
+
*/
|
|
71
|
+
getLocalClientId(): number;
|
|
72
|
+
/**
|
|
73
|
+
* Get connected peer count (excluding local)
|
|
74
|
+
*/
|
|
75
|
+
getPeerCount(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Get document state information
|
|
78
|
+
*/
|
|
79
|
+
getDocumentState(): DocumentState;
|
|
80
|
+
/**
|
|
81
|
+
* Get a snapshot of the Yjs document for inspection
|
|
82
|
+
*/
|
|
83
|
+
getDocumentSnapshot(): DocumentSnapshot;
|
|
84
|
+
/**
|
|
85
|
+
* Set local awareness state for introspection (admin view)
|
|
86
|
+
*/
|
|
87
|
+
setLocalAwareness(state: Record<string, unknown>): void;
|
|
88
|
+
/**
|
|
89
|
+
* Get full provider status for debugging
|
|
90
|
+
*/
|
|
91
|
+
getProviderStatus(): ProviderStatus;
|
|
92
|
+
/**
|
|
93
|
+
* Get Aeon-specific status
|
|
94
|
+
*/
|
|
95
|
+
getAeonStatus(): AeonStatus;
|
|
96
|
+
}
|
|
97
|
+
export interface ConnectionStatus {
|
|
98
|
+
connected: boolean;
|
|
99
|
+
roomName: string;
|
|
100
|
+
url: string;
|
|
101
|
+
websocket: {
|
|
102
|
+
state: 'connecting' | 'open' | 'closing' | 'closed';
|
|
103
|
+
connected: boolean;
|
|
104
|
+
};
|
|
105
|
+
webTransport: {
|
|
106
|
+
connected: boolean;
|
|
107
|
+
highFrequencyMode: boolean;
|
|
108
|
+
};
|
|
109
|
+
transport: 'WebSocket' | 'WebTransport';
|
|
110
|
+
}
|
|
111
|
+
export interface AwarenessState {
|
|
112
|
+
clientId: number;
|
|
113
|
+
state: Record<string, unknown>;
|
|
114
|
+
isLocal: boolean;
|
|
115
|
+
}
|
|
116
|
+
export interface SharedTypeInfo {
|
|
117
|
+
name: string;
|
|
118
|
+
type: 'YMap' | 'YArray' | 'YText' | 'YXmlFragment' | 'unknown';
|
|
119
|
+
size: number;
|
|
120
|
+
}
|
|
121
|
+
export interface DocumentState {
|
|
122
|
+
clientId: number;
|
|
123
|
+
guid: string;
|
|
124
|
+
stateVectorSize: number;
|
|
125
|
+
updateSize: number;
|
|
126
|
+
sharedTypes: SharedTypeInfo[];
|
|
127
|
+
transactionCount: number;
|
|
128
|
+
gcEnabled: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface DocumentSnapshot {
|
|
131
|
+
timestamp: string;
|
|
132
|
+
roomName: string;
|
|
133
|
+
content: Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
export interface ProviderStatus {
|
|
136
|
+
connection: ConnectionStatus;
|
|
137
|
+
awareness: {
|
|
138
|
+
localClientId: number;
|
|
139
|
+
peerCount: number;
|
|
140
|
+
states: AwarenessState[];
|
|
141
|
+
};
|
|
142
|
+
document: DocumentState;
|
|
143
|
+
aeon: AeonStatus;
|
|
144
|
+
}
|
|
145
|
+
export interface AeonStatus {
|
|
146
|
+
enabled: {
|
|
147
|
+
deltaSync: boolean;
|
|
148
|
+
richPresence: boolean;
|
|
149
|
+
offlineQueue: boolean;
|
|
150
|
+
};
|
|
151
|
+
delta: {
|
|
152
|
+
totalOperations: number;
|
|
153
|
+
totalFull: number;
|
|
154
|
+
totalDelta: number;
|
|
155
|
+
totalOriginalSize: number;
|
|
156
|
+
totalDeltaSize: number;
|
|
157
|
+
averageReductionPercent: number;
|
|
158
|
+
} | null;
|
|
159
|
+
offline: {
|
|
160
|
+
pending: number;
|
|
161
|
+
syncing: number;
|
|
162
|
+
failed: number;
|
|
163
|
+
synced: number;
|
|
164
|
+
totalOperations: number;
|
|
165
|
+
} | null;
|
|
166
|
+
presence: {
|
|
167
|
+
total: number;
|
|
168
|
+
online: number;
|
|
169
|
+
away: number;
|
|
170
|
+
offline: number;
|
|
171
|
+
} | null;
|
|
28
172
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
1
2
|
import { Observable } from 'lib0/observable';
|
|
2
3
|
import * as encoding from 'lib0/encoding';
|
|
3
4
|
import * as decoding from 'lib0/decoding';
|
|
@@ -5,6 +6,17 @@ import * as decoding from 'lib0/decoding';
|
|
|
5
6
|
import * as syncProtocol from 'y-protocols/sync';
|
|
6
7
|
// @ts-ignore
|
|
7
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
|
+
// Message types
|
|
15
|
+
const MessageType = {
|
|
16
|
+
Sync: 0,
|
|
17
|
+
Awareness: 1,
|
|
18
|
+
Delta: 2, // New: Aeon delta-compressed sync
|
|
19
|
+
};
|
|
8
20
|
export class HybridProvider extends Observable {
|
|
9
21
|
doc;
|
|
10
22
|
ws = null;
|
|
@@ -16,12 +28,28 @@ export class HybridProvider extends Observable {
|
|
|
16
28
|
writer = null;
|
|
17
29
|
// "High Frequency" mode uses WebTransport for ephemeral data
|
|
18
30
|
highFrequencyMode = false;
|
|
19
|
-
|
|
31
|
+
// Aeon integration
|
|
32
|
+
aeonConfig;
|
|
33
|
+
deltaAdapter = null;
|
|
34
|
+
presenceAdapter = null;
|
|
35
|
+
offlineAdapter = null;
|
|
36
|
+
constructor(url, roomName, doc, { awareness = new awarenessProtocol.Awareness(doc), aeonConfig = defaultAeonConfig, } = {}) {
|
|
20
37
|
super();
|
|
21
38
|
this.url = url;
|
|
22
39
|
this.roomName = roomName;
|
|
23
40
|
this.doc = doc;
|
|
24
41
|
this.awareness = awareness;
|
|
42
|
+
this.aeonConfig = aeonConfig;
|
|
43
|
+
// Initialize Aeon adapters
|
|
44
|
+
if (aeonConfig.enableDeltaSync) {
|
|
45
|
+
this.deltaAdapter = new DashDeltaAdapter(roomName, aeonConfig.deltaThreshold);
|
|
46
|
+
}
|
|
47
|
+
if (aeonConfig.enableRichPresence) {
|
|
48
|
+
this.presenceAdapter = new DashPresenceAdapter(roomName, awareness);
|
|
49
|
+
}
|
|
50
|
+
if (aeonConfig.enableOfflineQueue) {
|
|
51
|
+
this.offlineAdapter = new DashOfflineAdapter(doc, roomName, aeonConfig.maxOfflineQueueSize, aeonConfig.maxOfflineRetries);
|
|
52
|
+
}
|
|
25
53
|
this.doc.on('update', this.onDocUpdate.bind(this));
|
|
26
54
|
this.awareness.on('update', this.onAwarenessUpdate.bind(this));
|
|
27
55
|
this.connect();
|
|
@@ -139,19 +167,28 @@ export class HybridProvider extends Observable {
|
|
|
139
167
|
}
|
|
140
168
|
}
|
|
141
169
|
handleMessage(buf) {
|
|
142
|
-
// Simple protocol: First byte is message type (0=Sync, 1=Awareness)
|
|
170
|
+
// Simple protocol: First byte is message type (0=Sync, 1=Awareness, 2=Delta)
|
|
143
171
|
// In strict binary mode, we might need a more robust header.
|
|
144
172
|
// For now assuming Y-protocol encoding.
|
|
145
173
|
// Note: The Relay DO sends raw messages back.
|
|
146
174
|
const decoder = decoding.createDecoder(buf);
|
|
147
175
|
const messageType = decoding.readVarUint(decoder);
|
|
148
176
|
switch (messageType) {
|
|
149
|
-
case
|
|
177
|
+
case MessageType.Sync:
|
|
150
178
|
syncProtocol.readSyncMessage(decoder, encoding.createEncoder(), this.doc, this);
|
|
151
179
|
break;
|
|
152
|
-
case
|
|
180
|
+
case MessageType.Awareness:
|
|
153
181
|
awarenessProtocol.applyAwarenessUpdate(this.awareness, decoding.readVarUint8Array(decoder), this);
|
|
154
182
|
break;
|
|
183
|
+
case MessageType.Delta:
|
|
184
|
+
// Handle Aeon delta-compressed sync
|
|
185
|
+
if (this.deltaAdapter) {
|
|
186
|
+
const deltaPayloadBytes = decoding.readVarUint8Array(decoder);
|
|
187
|
+
const deltaPayload = this.deltaAdapter.decodePayload(deltaPayloadBytes);
|
|
188
|
+
const update = this.deltaAdapter.unwrapDelta(deltaPayload);
|
|
189
|
+
Y.applyUpdate(this.doc, update, this);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
155
192
|
}
|
|
156
193
|
}
|
|
157
194
|
async send(message) {
|
|
@@ -169,10 +206,26 @@ export class HybridProvider extends Observable {
|
|
|
169
206
|
onDocUpdate(update, origin) {
|
|
170
207
|
if (origin === this)
|
|
171
208
|
return;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
// Check if we should queue (offline)
|
|
210
|
+
if (this.offlineAdapter?.shouldQueue()) {
|
|
211
|
+
this.offlineAdapter.queueUpdate(update);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Use delta compression if enabled
|
|
215
|
+
if (this.deltaAdapter) {
|
|
216
|
+
const deltaPayload = this.deltaAdapter.wrapUpdate(update, origin);
|
|
217
|
+
const encoder = encoding.createEncoder();
|
|
218
|
+
encoding.writeVarUint(encoder, MessageType.Delta);
|
|
219
|
+
encoding.writeVarUint8Array(encoder, this.deltaAdapter.encodePayload(deltaPayload));
|
|
220
|
+
this.send(encoding.toUint8Array(encoder));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Standard Yjs sync
|
|
224
|
+
const encoder = encoding.createEncoder();
|
|
225
|
+
encoding.writeVarUint(encoder, MessageType.Sync);
|
|
226
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
227
|
+
this.send(encoding.toUint8Array(encoder));
|
|
228
|
+
}
|
|
176
229
|
}
|
|
177
230
|
onAwarenessUpdate({ added, updated, removed }, origin) {
|
|
178
231
|
if (origin === this)
|
|
@@ -188,7 +241,237 @@ export class HybridProvider extends Observable {
|
|
|
188
241
|
this.awareness.off('update', this.onAwarenessUpdate);
|
|
189
242
|
this.ws?.close();
|
|
190
243
|
this.wt?.close();
|
|
244
|
+
// Cleanup Aeon adapters
|
|
245
|
+
this.presenceAdapter?.destroy();
|
|
246
|
+
this.offlineAdapter?.destroy();
|
|
191
247
|
this.connected = false;
|
|
192
248
|
super.destroy();
|
|
193
249
|
}
|
|
250
|
+
// ============================================
|
|
251
|
+
// AEON ADAPTER ACCESSORS
|
|
252
|
+
// ============================================
|
|
253
|
+
/**
|
|
254
|
+
* Get the delta adapter for compression stats
|
|
255
|
+
*/
|
|
256
|
+
getDeltaAdapter() {
|
|
257
|
+
return this.deltaAdapter;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get the presence adapter for rich presence features
|
|
261
|
+
*/
|
|
262
|
+
getPresenceAdapter() {
|
|
263
|
+
return this.presenceAdapter;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get the offline adapter for queue management
|
|
267
|
+
*/
|
|
268
|
+
getOfflineAdapter() {
|
|
269
|
+
return this.offlineAdapter;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get Aeon configuration
|
|
273
|
+
*/
|
|
274
|
+
getAeonConfig() {
|
|
275
|
+
return this.aeonConfig;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Process offline queue when back online
|
|
279
|
+
*/
|
|
280
|
+
async processOfflineQueue() {
|
|
281
|
+
if (!this.offlineAdapter) {
|
|
282
|
+
return { synced: 0, failed: 0 };
|
|
283
|
+
}
|
|
284
|
+
return this.offlineAdapter.processQueue(async (update) => {
|
|
285
|
+
const encoder = encoding.createEncoder();
|
|
286
|
+
if (this.deltaAdapter) {
|
|
287
|
+
const deltaPayload = this.deltaAdapter.wrapUpdate(update);
|
|
288
|
+
encoding.writeVarUint(encoder, MessageType.Delta);
|
|
289
|
+
encoding.writeVarUint8Array(encoder, this.deltaAdapter.encodePayload(deltaPayload));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
encoding.writeVarUint(encoder, MessageType.Sync);
|
|
293
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
294
|
+
}
|
|
295
|
+
await this.send(encoding.toUint8Array(encoder));
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// ============================================
|
|
299
|
+
// INTROSPECTION METHODS FOR DASH-STUDIO
|
|
300
|
+
// ============================================
|
|
301
|
+
/**
|
|
302
|
+
* Get comprehensive connection status
|
|
303
|
+
*/
|
|
304
|
+
getConnectionStatus() {
|
|
305
|
+
let wsState = 'closed';
|
|
306
|
+
if (this.ws) {
|
|
307
|
+
switch (this.ws.readyState) {
|
|
308
|
+
case WebSocket.CONNECTING:
|
|
309
|
+
wsState = 'connecting';
|
|
310
|
+
break;
|
|
311
|
+
case WebSocket.OPEN:
|
|
312
|
+
wsState = 'open';
|
|
313
|
+
break;
|
|
314
|
+
case WebSocket.CLOSING:
|
|
315
|
+
wsState = 'closing';
|
|
316
|
+
break;
|
|
317
|
+
case WebSocket.CLOSED:
|
|
318
|
+
wsState = 'closed';
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
connected: this.connected,
|
|
324
|
+
roomName: this.roomName,
|
|
325
|
+
url: this.url,
|
|
326
|
+
websocket: {
|
|
327
|
+
state: wsState,
|
|
328
|
+
connected: this.ws !== null && this.ws.readyState === WebSocket.OPEN
|
|
329
|
+
},
|
|
330
|
+
webTransport: {
|
|
331
|
+
connected: this.wt !== null,
|
|
332
|
+
highFrequencyMode: this.highFrequencyMode
|
|
333
|
+
},
|
|
334
|
+
transport: this.highFrequencyMode ? 'WebTransport' : 'WebSocket'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get all awareness states from connected peers
|
|
339
|
+
*/
|
|
340
|
+
getAwarenessStates() {
|
|
341
|
+
const states = [];
|
|
342
|
+
const awarenessStates = this.awareness.getStates();
|
|
343
|
+
awarenessStates.forEach((state, clientId) => {
|
|
344
|
+
states.push({
|
|
345
|
+
clientId,
|
|
346
|
+
state,
|
|
347
|
+
isLocal: clientId === this.awareness.clientID
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
return states;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get the local client ID
|
|
354
|
+
*/
|
|
355
|
+
getLocalClientId() {
|
|
356
|
+
return this.awareness.clientID;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Get connected peer count (excluding local)
|
|
360
|
+
*/
|
|
361
|
+
getPeerCount() {
|
|
362
|
+
const states = this.awareness.getStates();
|
|
363
|
+
return Math.max(0, states.size - 1); // Exclude self
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get document state information
|
|
367
|
+
*/
|
|
368
|
+
getDocumentState() {
|
|
369
|
+
const stateVector = Y.encodeStateVector(this.doc);
|
|
370
|
+
const update = Y.encodeStateAsUpdate(this.doc);
|
|
371
|
+
// Get shared types info
|
|
372
|
+
const sharedTypes = [];
|
|
373
|
+
this.doc.share.forEach((type, name) => {
|
|
374
|
+
let typeKind = 'unknown';
|
|
375
|
+
let size = 0;
|
|
376
|
+
if (type instanceof Y.Map) {
|
|
377
|
+
typeKind = 'YMap';
|
|
378
|
+
size = type.size;
|
|
379
|
+
}
|
|
380
|
+
else if (type instanceof Y.Array) {
|
|
381
|
+
typeKind = 'YArray';
|
|
382
|
+
size = type.length;
|
|
383
|
+
}
|
|
384
|
+
else if (type instanceof Y.Text) {
|
|
385
|
+
typeKind = 'YText';
|
|
386
|
+
size = type.length;
|
|
387
|
+
}
|
|
388
|
+
else if (type instanceof Y.XmlFragment) {
|
|
389
|
+
typeKind = 'YXmlFragment';
|
|
390
|
+
size = type.length;
|
|
391
|
+
}
|
|
392
|
+
sharedTypes.push({
|
|
393
|
+
name,
|
|
394
|
+
type: typeKind,
|
|
395
|
+
size
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
clientId: this.doc.clientID,
|
|
400
|
+
guid: this.doc.guid,
|
|
401
|
+
stateVectorSize: stateVector.byteLength,
|
|
402
|
+
updateSize: update.byteLength,
|
|
403
|
+
sharedTypes,
|
|
404
|
+
transactionCount: 0, // Yjs doesn't expose this directly
|
|
405
|
+
gcEnabled: this.doc.gc
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get a snapshot of the Yjs document for inspection
|
|
410
|
+
*/
|
|
411
|
+
getDocumentSnapshot() {
|
|
412
|
+
const content = {};
|
|
413
|
+
this.doc.share.forEach((type, name) => {
|
|
414
|
+
try {
|
|
415
|
+
if (type instanceof Y.Map) {
|
|
416
|
+
content[name] = type.toJSON();
|
|
417
|
+
}
|
|
418
|
+
else if (type instanceof Y.Array) {
|
|
419
|
+
content[name] = type.toJSON();
|
|
420
|
+
}
|
|
421
|
+
else if (type instanceof Y.Text) {
|
|
422
|
+
content[name] = type.toString();
|
|
423
|
+
}
|
|
424
|
+
else if (type instanceof Y.XmlFragment) {
|
|
425
|
+
content[name] = type.toString();
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
content[name] = '[unsupported type]';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
content[name] = '[error reading content]';
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
roomName: this.roomName,
|
|
438
|
+
content
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Set local awareness state for introspection (admin view)
|
|
443
|
+
*/
|
|
444
|
+
setLocalAwareness(state) {
|
|
445
|
+
this.awareness.setLocalState(state);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get full provider status for debugging
|
|
449
|
+
*/
|
|
450
|
+
getProviderStatus() {
|
|
451
|
+
return {
|
|
452
|
+
connection: this.getConnectionStatus(),
|
|
453
|
+
awareness: {
|
|
454
|
+
localClientId: this.awareness.clientID,
|
|
455
|
+
peerCount: this.getPeerCount(),
|
|
456
|
+
states: this.getAwarenessStates()
|
|
457
|
+
},
|
|
458
|
+
document: this.getDocumentState(),
|
|
459
|
+
aeon: this.getAeonStatus()
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Get Aeon-specific status
|
|
464
|
+
*/
|
|
465
|
+
getAeonStatus() {
|
|
466
|
+
return {
|
|
467
|
+
enabled: {
|
|
468
|
+
deltaSync: this.aeonConfig.enableDeltaSync,
|
|
469
|
+
richPresence: this.aeonConfig.enableRichPresence,
|
|
470
|
+
offlineQueue: this.aeonConfig.enableOfflineQueue,
|
|
471
|
+
},
|
|
472
|
+
delta: this.deltaAdapter?.getStats() ?? null,
|
|
473
|
+
offline: this.offlineAdapter?.getStats() ?? null,
|
|
474
|
+
presence: this.presenceAdapter?.getStats() ?? null,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
194
477
|
}
|