@affectively/dash 5.3.1 → 5.4.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.
Files changed (77) hide show
  1. package/README.md +193 -0
  2. package/dist/src/api/firebase/auth/index.d.ts +137 -0
  3. package/dist/src/api/firebase/auth/index.js +352 -0
  4. package/dist/src/api/firebase/auth/providers.d.ts +254 -0
  5. package/dist/src/api/firebase/auth/providers.js +518 -0
  6. package/dist/src/api/firebase/database/index.d.ts +108 -0
  7. package/dist/src/api/firebase/database/index.js +368 -0
  8. package/dist/src/api/firebase/errors.d.ts +15 -0
  9. package/dist/src/api/firebase/errors.js +215 -0
  10. package/dist/src/api/firebase/firestore/data-types.d.ts +116 -0
  11. package/dist/src/api/firebase/firestore/data-types.js +280 -0
  12. package/dist/src/api/firebase/firestore/index.d.ts +7 -0
  13. package/dist/src/api/firebase/firestore/index.js +13 -0
  14. package/dist/src/api/firebase/firestore/listeners.d.ts +20 -0
  15. package/dist/src/api/firebase/firestore/listeners.js +50 -0
  16. package/dist/src/api/firebase/firestore/operations.d.ts +123 -0
  17. package/dist/src/api/firebase/firestore/operations.js +490 -0
  18. package/dist/src/api/firebase/firestore/query.d.ts +118 -0
  19. package/dist/src/api/firebase/firestore/query.js +418 -0
  20. package/dist/src/api/firebase/index.d.ts +11 -0
  21. package/dist/src/api/firebase/index.js +17 -0
  22. package/dist/src/api/firebase/storage/index.d.ts +100 -0
  23. package/dist/src/api/firebase/storage/index.js +286 -0
  24. package/dist/src/api/firebase/types.d.ts +341 -0
  25. package/dist/src/api/firebase/types.js +4 -0
  26. package/dist/src/auth/manager.d.ts +182 -0
  27. package/dist/src/auth/manager.js +598 -0
  28. package/dist/src/engine/ai.d.ts +10 -0
  29. package/dist/src/engine/ai.js +76 -0
  30. package/dist/src/engine/sqlite.d.ts +353 -0
  31. package/dist/src/engine/sqlite.js +1328 -0
  32. package/dist/src/engine/vec_extension.d.ts +5 -0
  33. package/dist/src/engine/vec_extension.js +10 -0
  34. package/dist/src/index.d.ts +21 -0
  35. package/dist/src/index.js +26 -0
  36. package/dist/src/mcp/server.d.ts +8 -0
  37. package/dist/src/mcp/server.js +87 -0
  38. package/dist/src/reactivity/signal.d.ts +3 -0
  39. package/dist/src/reactivity/signal.js +31 -0
  40. package/dist/src/schema/lens.d.ts +29 -0
  41. package/dist/src/schema/lens.js +122 -0
  42. package/dist/src/sync/AeonDurableSync.d.ts +26 -0
  43. package/dist/src/sync/AeonDurableSync.js +67 -0
  44. package/dist/src/sync/AutomergeProvider.d.ts +45 -0
  45. package/dist/src/sync/AutomergeProvider.js +153 -0
  46. package/dist/src/sync/aeon/config.d.ts +21 -0
  47. package/dist/src/sync/aeon/config.js +14 -0
  48. package/dist/src/sync/aeon/delta-adapter.d.ts +62 -0
  49. package/dist/src/sync/aeon/delta-adapter.js +98 -0
  50. package/dist/src/sync/aeon/index.d.ts +18 -0
  51. package/dist/src/sync/aeon/index.js +19 -0
  52. package/dist/src/sync/aeon/offline-adapter.d.ts +110 -0
  53. package/dist/src/sync/aeon/offline-adapter.js +227 -0
  54. package/dist/src/sync/aeon/presence-adapter.d.ts +114 -0
  55. package/dist/src/sync/aeon/presence-adapter.js +157 -0
  56. package/dist/src/sync/aeon/schema-adapter.d.ts +95 -0
  57. package/dist/src/sync/aeon/schema-adapter.js +163 -0
  58. package/dist/src/sync/backup.d.ts +12 -0
  59. package/dist/src/sync/backup.js +44 -0
  60. package/dist/src/sync/connection.d.ts +20 -0
  61. package/dist/src/sync/connection.js +50 -0
  62. package/dist/src/sync/d1-provider.d.ts +103 -0
  63. package/dist/src/sync/d1-provider.js +418 -0
  64. package/dist/src/sync/hybrid-provider.d.ts +307 -0
  65. package/dist/src/sync/hybrid-provider.js +1353 -0
  66. package/dist/src/sync/provider.d.ts +11 -0
  67. package/dist/src/sync/provider.js +67 -0
  68. package/dist/src/sync/types.d.ts +32 -0
  69. package/dist/src/sync/types.js +4 -0
  70. package/dist/src/sync/verify.d.ts +1 -0
  71. package/dist/src/sync/verify.js +23 -0
  72. package/dist/tsconfig.tsbuildinfo +1 -0
  73. package/package.json +77 -43
  74. package/dist/index.d.ts +0 -62
  75. package/dist/index.js +0 -31
  76. package/dist/sync/index.d.ts +0 -6
  77. package/dist/sync/index.js +0 -4
@@ -0,0 +1,5 @@
1
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
2
+ type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
3
+ type Database = Sqlite3Module['oo1']['OpfsDb'] | Sqlite3Module['oo1']['DB'];
4
+ export declare function loadVectorExtension(db: Database): Promise<void>;
5
+ export {};
@@ -0,0 +1,10 @@
1
+ export async function loadVectorExtension(db) {
2
+ // Hypothetical API for loading WASM extension if sqlite-wasm supports it.
3
+ // Currently sqlite-wasm static builds don't easily support dynamic extension loading
4
+ // without a custom build.
5
+ // For this 'cutting edge' roadmap implementation, we will simulate the
6
+ // extension's presence or use a polyfill pattern if the raw WASM isn't present.
7
+ console.log("Loading sqlite-vec extension...");
8
+ // register native functions if possible
9
+ // db.createFunction(...)
10
+ }
@@ -0,0 +1,21 @@
1
+ export { dash } from './engine/sqlite.js';
2
+ export type { CloudConfig, CloudSyncResult } from './engine/sqlite.js';
3
+ export { liveQuery, signal, effect, computed } from './reactivity/signal.js';
4
+ export { mcpServer } from './mcp/server.js';
5
+ export { YjsSqliteProvider } from './sync/provider.js';
6
+ export { backup, restore, generateKey, exportKey, importKey } from './sync/backup.js';
7
+ export type { CloudStorageAdapter } from './sync/backup.js';
8
+ export { HybridProvider } from './sync/hybrid-provider.js';
9
+ export type { RelayDiscoveryConfig, RelayPerformanceConfig, RelayPrivacyConfig, ConnectionStatus, ProviderStatus, } from './sync/hybrid-provider.js';
10
+ export { AutomergeProvider } from './sync/AutomergeProvider.js';
11
+ export type { AutomergeProviderOptions } from './sync/AutomergeProvider.js';
12
+ export type { AutomergeProviderConfig } from './sync/types.js';
13
+ export { AeonDurableSyncRuntime } from './sync/AeonDurableSync.js';
14
+ export type { AeonDurableSyncConfig } from './sync/AeonDurableSync.js';
15
+ export { D1SyncProvider, getD1SyncProvider, resetD1SyncProvider } from './sync/d1-provider.js';
16
+ export type { D1SyncConfig, SyncResult, SyncQueueEntry } from './sync/d1-provider.js';
17
+ export * as firebase from './api/firebase/index.js';
18
+ export { collection, doc, query, where, orderBy, limit, limitToLast, startAt, startAfter, endAt, endBefore, offset, Timestamp, GeoPoint, serverTimestamp, deleteField, arrayUnion, arrayRemove, increment, getDoc, getDocs, setDoc, updateDoc, deleteDoc, writeBatch, runTransaction, onSnapshot, } from './api/firebase/index.js';
19
+ export { ref, child, set, update, remove, push, get, onDisconnect, onValue, off, } from './api/firebase/index.js';
20
+ export { createUserWithEmailAndPassword, signInWithEmailAndPassword, signInAnonymously, signOut, getAuth, onAuthStateChanged, updateUserProfile, updateUserEmail, updateUserPassword, deleteUser, sendPasswordResetEmail, sendEmailVerification, GoogleAuthProvider, GithubAuthProvider, FacebookAuthProvider, TwitterAuthProvider, DiscordAuthProvider, TotpMultiFactorGenerator, PhoneMultiFactorGenerator, setPersistence, browserLocalPersistence, browserSessionPersistence, inMemoryPersistence, } from './api/firebase/index.js';
21
+ export { ref as storageRef, refFromURL, child as storageChild, uploadBytes, uploadBytesResumable, uploadString, getBytes, getDownloadURL, getMetadata, updateMetadata, deleteObject, list as listFiles, listAll, } from './api/firebase/storage/index.js';
@@ -0,0 +1,26 @@
1
+ export { dash } from './engine/sqlite.js';
2
+ export { liveQuery, signal, effect, computed } from './reactivity/signal.js';
3
+ export { mcpServer } from './mcp/server.js';
4
+ // Sync exports
5
+ export { YjsSqliteProvider } from './sync/provider.js';
6
+ export { backup, restore, generateKey, exportKey, importKey } from './sync/backup.js';
7
+ export { HybridProvider } from './sync/hybrid-provider.js';
8
+ export { AutomergeProvider } from './sync/AutomergeProvider.js';
9
+ export { AeonDurableSyncRuntime } from './sync/AeonDurableSync.js';
10
+ // D1 HTTP Sync (legacy - prefer dash.enableCloudSync())
11
+ export { D1SyncProvider, getD1SyncProvider, resetD1SyncProvider } from './sync/d1-provider.js';
12
+ // Firebase Compatibility API exports
13
+ export * as firebase from './api/firebase/index.js';
14
+ // Firestore
15
+ export { collection, doc, query, where, orderBy, limit, limitToLast, startAt, startAfter, endAt, endBefore, offset, serverTimestamp, deleteField, arrayUnion, arrayRemove, increment, getDoc, getDocs, setDoc, updateDoc, deleteDoc, writeBatch, runTransaction, onSnapshot, } from './api/firebase/index.js';
16
+ // Realtime Database
17
+ export { ref, child, set, update, remove, push, get, onDisconnect, onValue,
18
+ // onChildAdded,
19
+ // onChildChanged,
20
+ // onChildRemoved,
21
+ // onChildMoved,
22
+ off, } from './api/firebase/index.js';
23
+ // Authentication
24
+ export { createUserWithEmailAndPassword, signInWithEmailAndPassword, signInAnonymously, signOut, getAuth, onAuthStateChanged, updateUserProfile, updateUserEmail, updateUserPassword, deleteUser, sendPasswordResetEmail, sendEmailVerification, GoogleAuthProvider, GithubAuthProvider, FacebookAuthProvider, TwitterAuthProvider, DiscordAuthProvider, TotpMultiFactorGenerator, PhoneMultiFactorGenerator, setPersistence, browserLocalPersistence, browserSessionPersistence, inMemoryPersistence, } from './api/firebase/index.js';
25
+ // Storage
26
+ export { ref as storageRef, refFromURL, child as storageChild, uploadBytes, uploadBytesResumable, uploadString, getBytes, getDownloadURL, getMetadata, updateMetadata, deleteObject, list as listFiles, listAll, } from './api/firebase/storage/index.js';
@@ -0,0 +1,8 @@
1
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
2
+ export declare class DashMCPServer {
3
+ private server;
4
+ constructor();
5
+ private setupHandlers;
6
+ connect(transport: SSEServerTransport): Promise<void>;
7
+ }
8
+ export declare const mcpServer: DashMCPServer;
@@ -0,0 +1,87 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { dash } from '../engine/sqlite.js';
4
+ export class DashMCPServer {
5
+ server;
6
+ constructor() {
7
+ this.server = new Server({
8
+ name: 'dash-local-db',
9
+ version: '2.0.0',
10
+ }, {
11
+ capabilities: {
12
+ resources: {},
13
+ tools: {},
14
+ },
15
+ });
16
+ this.setupHandlers();
17
+ }
18
+ setupHandlers() {
19
+ // List Resources: Expose all tables as resources
20
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
21
+ await dash.ready();
22
+ // Get all table names
23
+ const tables = dash.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
24
+ return {
25
+ resources: tables.map((t) => ({
26
+ uri: `mcp://local/dash/${t.name}`,
27
+ name: t.name,
28
+ mimeType: 'application/json',
29
+ description: `Table: ${t.name}`,
30
+ })),
31
+ };
32
+ });
33
+ // Read Resource: Return table content
34
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
35
+ const uri = request.params.uri;
36
+ const tableName = uri.split('/').pop();
37
+ if (!tableName)
38
+ throw new Error('Invalid URI');
39
+ const rows = dash.execute(`SELECT * FROM ${tableName} LIMIT 100`);
40
+ return {
41
+ contents: [{
42
+ uri: uri,
43
+ mimeType: 'application/json',
44
+ text: JSON.stringify(rows, null, 2),
45
+ }],
46
+ };
47
+ });
48
+ // List Tools: Expose Semantic Search
49
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
50
+ return {
51
+ tools: [{
52
+ name: 'semantic_search',
53
+ description: 'Search the Dash database using vector similarity (semantic search).',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ query: { type: 'string', description: 'The search query string.' },
58
+ },
59
+ required: ['query'],
60
+ },
61
+ }],
62
+ };
63
+ });
64
+ // Call Tool: Execute Semantic Search
65
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
+ if (request.params.name === 'semantic_search') {
67
+ const query = request.params.arguments?.query;
68
+ if (!query)
69
+ throw new Error('Missing query argument');
70
+ const results = await dash.search(query);
71
+ return {
72
+ content: [{
73
+ type: 'text',
74
+ text: JSON.stringify(results, null, 2),
75
+ }],
76
+ };
77
+ }
78
+ throw new Error('Tool not found');
79
+ });
80
+ }
81
+ // Method to connect to a transport (e.g., SSE for web)
82
+ async connect(transport) {
83
+ await this.server.connect(transport);
84
+ console.log('Dash MCP Server Connected');
85
+ }
86
+ }
87
+ export const mcpServer = new DashMCPServer();
@@ -0,0 +1,3 @@
1
+ import { Signal } from '@preact/signals-core';
2
+ export declare function liveQuery<T = any>(sql: string, bind?: any[]): Signal<T[]>;
3
+ export { signal, effect, computed } from '@preact/signals-core';
@@ -0,0 +1,31 @@
1
+ import { signal } from '@preact/signals-core';
2
+ import { dash } from '../engine/sqlite.js';
3
+ export function liveQuery(sql, bind) {
4
+ const data = signal([]);
5
+ const run = async () => {
6
+ await dash.ready();
7
+ try {
8
+ const result = dash.execute(sql, bind);
9
+ data.value = result;
10
+ }
11
+ catch (e) {
12
+ console.error('Dash LiveQuery Error:', e);
13
+ }
14
+ };
15
+ // Run immediately
16
+ run();
17
+ // Subscribe to database changes
18
+ // Parse table name naively
19
+ const match = sql.match(/FROM\s+([a-zA-Z0-9_]+)/i);
20
+ const table = match ? match[1] : null;
21
+ if (table) {
22
+ dash.subscribe(table, () => {
23
+ run();
24
+ });
25
+ }
26
+ else {
27
+ console.warn('Dash: Could not identify table for liveQuery', sql);
28
+ }
29
+ return data;
30
+ }
31
+ export { signal, effect, computed } from '@preact/signals-core';
@@ -0,0 +1,29 @@
1
+ export type LensOp = {
2
+ kind: 'rename';
3
+ source: string;
4
+ target: string;
5
+ } | {
6
+ kind: 'hoist';
7
+ host: string;
8
+ name: string;
9
+ } | {
10
+ kind: 'wrap';
11
+ name: string;
12
+ };
13
+ export interface SchemaLens {
14
+ from: number;
15
+ to: number;
16
+ ops: LensOp[];
17
+ }
18
+ export declare class LensEngine {
19
+ private lenses;
20
+ private currentVersion;
21
+ constructor(currentVersion: number);
22
+ registerLens(from: number, to: number, ops: LensOp[]): void;
23
+ private findPath;
24
+ migrate(data: any, from: number, to?: number): any;
25
+ private normalizeOps;
26
+ private invertOp;
27
+ private applyOp;
28
+ }
29
+ export declare const schema: LensEngine;
@@ -0,0 +1,122 @@
1
+ // Native Bidirectional Lens Implementation
2
+ // Inspired by Project Cambria
3
+ export class LensEngine {
4
+ lenses = [];
5
+ currentVersion;
6
+ constructor(currentVersion) {
7
+ this.currentVersion = currentVersion;
8
+ }
9
+ registerLens(from, to, ops) {
10
+ this.lenses.push({ from, to, ops });
11
+ }
12
+ // Find path from V_start to V_end
13
+ findPath(from, to) {
14
+ // BFS to find shortest path
15
+ const queue = [{ version: from, path: [] }];
16
+ const visited = new Set([from]);
17
+ while (queue.length > 0) {
18
+ const { version, path } = queue.shift();
19
+ if (version === to)
20
+ return path;
21
+ // Find neighbors
22
+ // Forward lenses
23
+ const forward = this.lenses.filter(l => l.from === version);
24
+ for (const lens of forward) {
25
+ if (!visited.has(lens.to)) {
26
+ visited.add(lens.to);
27
+ queue.push({ version: lens.to, path: [...path, lens] });
28
+ }
29
+ }
30
+ // Reverse lenses (Implicit)
31
+ const backward = this.lenses.filter(l => l.to === version);
32
+ for (const lens of backward) {
33
+ if (!visited.has(lens.from)) {
34
+ visited.add(lens.from);
35
+ // Create synthetic reverse lens
36
+ const reverseLens = {
37
+ from: lens.to,
38
+ to: lens.from,
39
+ ops: [...lens.ops].reverse() // Ops must be reversed!
40
+ };
41
+ queue.push({ version: lens.from, path: [...path, reverseLens] });
42
+ }
43
+ }
44
+ }
45
+ throw new Error(`No translation path found from v${from} to v${to}`);
46
+ }
47
+ migrate(data, from, to = this.currentVersion) {
48
+ if (from === to)
49
+ return data;
50
+ const path = this.findPath(from, to);
51
+ let doc = JSON.parse(JSON.stringify(data)); // Deep clone to avoid mutation
52
+ for (const step of path) {
53
+ const isForward = step.from < step.to; // Heuristic: Assuming localized version jumps
54
+ // Actually we should just apply the ops in the step direction.
55
+ // But we need to know if the step is a "Forward Lens" or a "Reverse Lens"
56
+ // because ops like 'rename' need to swap source/target if reversed.
57
+ // The path finding creates "synthetic" lenses for reverse steps where from > to.
58
+ // So we just need to apply 'step.ops' carefully.
59
+ // But wait! in findPath, I just reversed the ARRAY of ops.
60
+ // I ALSO need to invert the OPERATORS themselves.
61
+ const opsToApply = this.normalizeOps(step);
62
+ for (const op of opsToApply) {
63
+ doc = this.applyOp(doc, op);
64
+ }
65
+ }
66
+ return doc;
67
+ }
68
+ normalizeOps(step) {
69
+ // If this lens was registered as from=1, to=2
70
+ // And we are using it as from=1, to=2. No change.
71
+ // If we are traversing 2->1.
72
+ // The findPath constructed a lens { from: 2, to: 1, ops: ops.reverse() }
73
+ // But the ops inside are still { kind: 'rename', source: 'old', target: 'new' }
74
+ // We need to invert them to { kind: 'rename', source: 'new', target: 'old' }
75
+ // We need to know the *registred* direction to know if inversion is needed?
76
+ // Actually findPath constructs a fresh object. We can check if it exists in this.lenses.
77
+ const registered = this.lenses.find(l => l.from === step.from && l.to === step.to);
78
+ if (registered) {
79
+ return step.ops; // Direct match, use as is
80
+ }
81
+ // Must be a reverse step constructed by findPath
82
+ return step.ops.map(op => this.invertOp(op));
83
+ }
84
+ invertOp(op) {
85
+ switch (op.kind) {
86
+ case 'rename': return { kind: 'rename', source: op.target, target: op.source };
87
+ // Invert Hoist: Wrap?
88
+ // Hoist: host='meta', name='key' (moves doc.meta.key -> doc.key)
89
+ // Inverse: Wrap (moves doc.key -> doc.meta.key)
90
+ // Use 'wrap' as simplified inverse of simple hoist
91
+ case 'hoist': return { kind: 'wrap', name: op.name }; // Simplified
92
+ case 'wrap': return { kind: 'hoist', host: 'container', name: op.name }; // Naive inverse
93
+ default: return op;
94
+ }
95
+ }
96
+ applyOp(doc, op) {
97
+ switch (op.kind) {
98
+ case 'rename': {
99
+ if (doc[op.source] !== undefined) {
100
+ doc[op.target] = doc[op.source];
101
+ delete doc[op.source];
102
+ }
103
+ return doc;
104
+ }
105
+ case 'hoist': {
106
+ // Extract property from scalar host? Or object?
107
+ // Assume simple object flattening: { data: { val: 1 } } -> hoist 'data', 'val' -> { val: 1 }
108
+ // doc[op.host][op.name] -> doc[op.name]
109
+ if (doc[op.host] && doc[op.host][op.name] !== undefined) {
110
+ doc[op.name] = doc[op.host][op.name];
111
+ delete doc[op.host][op.name];
112
+ if (Object.keys(doc[op.host]).length === 0)
113
+ delete doc[op.host];
114
+ }
115
+ return doc;
116
+ }
117
+ default: return doc;
118
+ }
119
+ }
120
+ }
121
+ export const schema = new LensEngine(1);
122
+ // Default to v1 until configured
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AeonDurableSync - Durable telemetry runtime for sync operations
3
+ *
4
+ * Records transport selection, discovery events, and sync protocol messages
5
+ * using Aeon's OfflineOperationQueue, SyncProtocol, and ReplicationManager.
6
+ *
7
+ * Operates in-memory by default. Persistence requires @affectively/aeon/persistence
8
+ * (available in a future release).
9
+ */
10
+ type TransportType = 'websocket' | 'webtransport' | null;
11
+ export interface AeonDurableSyncConfig {
12
+ enabled?: boolean;
13
+ roomName: string;
14
+ persistenceKeyPrefix?: string;
15
+ }
16
+ export declare class AeonDurableSyncRuntime {
17
+ private readonly queue;
18
+ private readonly protocol;
19
+ private readonly replication;
20
+ constructor(config: AeonDurableSyncConfig);
21
+ initialize(): Promise<void>;
22
+ recordTransportSelection(roomName: string, transport: TransportType, relay: string | null): void;
23
+ recordDiscovery(roomName: string, relays: string[]): void;
24
+ getPendingOperations(): number;
25
+ }
26
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * AeonDurableSync - Durable telemetry runtime for sync operations
3
+ *
4
+ * Records transport selection, discovery events, and sync protocol messages
5
+ * using Aeon's OfflineOperationQueue, SyncProtocol, and ReplicationManager.
6
+ *
7
+ * Operates in-memory by default. Persistence requires @affectively/aeon/persistence
8
+ * (available in a future release).
9
+ */
10
+ import { OfflineOperationQueue, SyncProtocol, ReplicationManager, } from '@affectively/aeon';
11
+ export class AeonDurableSyncRuntime {
12
+ queue;
13
+ protocol;
14
+ replication;
15
+ constructor(config) {
16
+ // Construct without persistence options — in-memory only until
17
+ // @affectively/aeon/persistence ships.
18
+ this.queue = new OfflineOperationQueue();
19
+ this.protocol = new SyncProtocol();
20
+ this.replication = new ReplicationManager();
21
+ }
22
+ async initialize() {
23
+ // No-op until persistence module is available.
24
+ // When @affectively/aeon/persistence ships, this will hydrate from storage.
25
+ }
26
+ recordTransportSelection(roomName, transport, relay) {
27
+ if (!transport) {
28
+ return;
29
+ }
30
+ try {
31
+ this.queue.enqueue('update', {
32
+ roomName,
33
+ event: 'transport-selected',
34
+ transport,
35
+ relay,
36
+ ts: Date.now(),
37
+ }, roomName, 'normal');
38
+ this.protocol.createSyncRequestMessage(roomName, relay ?? 'unknown-relay', `transport-${Date.now()}`, '0.0.0', '1.1.0', {
39
+ transport,
40
+ relay,
41
+ });
42
+ }
43
+ catch {
44
+ // Telemetry durability must never impact foreground sync operations.
45
+ }
46
+ }
47
+ recordDiscovery(roomName, relays) {
48
+ const sanitizedRelays = relays
49
+ .filter((relay) => typeof relay === 'string' && relay.length > 0)
50
+ .slice(0, 32);
51
+ try {
52
+ this.queue.enqueue('sync', {
53
+ roomName,
54
+ event: 'relay-discovery',
55
+ count: sanitizedRelays.length,
56
+ relays: sanitizedRelays,
57
+ ts: Date.now(),
58
+ }, roomName, 'low');
59
+ }
60
+ catch {
61
+ // Telemetry durability must never impact foreground sync operations.
62
+ }
63
+ }
64
+ getPendingOperations() {
65
+ return this.queue.getPendingCount();
66
+ }
67
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * AutomergeProvider - Automerge sync over WebSocket relay
3
+ *
4
+ * Mirrors HybridProvider's transport selection and discovery logic
5
+ * but uses Automerge's built-in sync protocol instead of Yjs.
6
+ * The relay server is CRDT-agnostic — it relays opaque binary frames.
7
+ *
8
+ * @automerge/automerge is dynamically imported so users who only use
9
+ * Yjs never pay the bundle cost.
10
+ */
11
+ import type { AutomergeProviderConfig } from './types.js';
12
+ export interface AutomergeProviderOptions<T> {
13
+ onChange?: (doc: T) => void;
14
+ }
15
+ export declare class AutomergeProvider<T = unknown> {
16
+ private config;
17
+ private _doc;
18
+ private _onChange?;
19
+ private _ws;
20
+ private _syncState;
21
+ private _automerge;
22
+ private _ready;
23
+ private _destroyed;
24
+ private _reconnectTimer;
25
+ private _reconnectDelayMs;
26
+ private readonly _maxReconnectDelayMs;
27
+ constructor(doc: T, config: AutomergeProviderConfig, options?: AutomergeProviderOptions<T>);
28
+ private initialize;
29
+ private getRelayUrl;
30
+ private connectWebSocket;
31
+ private scheduleReconnect;
32
+ private sendSyncMessage;
33
+ private handleMessage;
34
+ /**
35
+ * Update the local document. Required because Automerge documents are immutable —
36
+ * after calling Automerge.change(), pass the new doc here to sync it.
37
+ */
38
+ updateDoc(newDoc: T): void;
39
+ get doc(): T;
40
+ get ready(): Promise<void>;
41
+ get roomName(): string;
42
+ get connected(): boolean;
43
+ disconnect(): void;
44
+ destroy(): Promise<void>;
45
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * AutomergeProvider - Automerge sync over WebSocket relay
3
+ *
4
+ * Mirrors HybridProvider's transport selection and discovery logic
5
+ * but uses Automerge's built-in sync protocol instead of Yjs.
6
+ * The relay server is CRDT-agnostic — it relays opaque binary frames.
7
+ *
8
+ * @automerge/automerge is dynamically imported so users who only use
9
+ * Yjs never pay the bundle cost.
10
+ */
11
+ export class AutomergeProvider {
12
+ config;
13
+ _doc;
14
+ _onChange;
15
+ _ws = null;
16
+ _syncState = null;
17
+ _automerge = null;
18
+ _ready;
19
+ _destroyed = false;
20
+ _reconnectTimer = null;
21
+ _reconnectDelayMs = 1000;
22
+ _maxReconnectDelayMs = 30_000;
23
+ constructor(doc, config, options) {
24
+ this._doc = doc;
25
+ this.config = config;
26
+ this._onChange = options?.onChange;
27
+ this._ready = this.initialize();
28
+ }
29
+ async initialize() {
30
+ const am = await import('@automerge/automerge');
31
+ this._automerge = am;
32
+ this._syncState = am.initSyncState();
33
+ this.connectWebSocket();
34
+ }
35
+ getRelayUrl() {
36
+ if (this.config.websocket?.url) {
37
+ return toWsUrl(this.config.websocket.url);
38
+ }
39
+ if (this.config.webtransport?.url) {
40
+ return toWsUrl(this.config.webtransport.url);
41
+ }
42
+ return null;
43
+ }
44
+ connectWebSocket() {
45
+ if (this._destroyed)
46
+ return;
47
+ const relayUrl = this.getRelayUrl();
48
+ if (!relayUrl)
49
+ return;
50
+ const separator = relayUrl.includes('?') ? '&' : '?';
51
+ const params = new URLSearchParams({ room: this.config.roomName });
52
+ if (this.config.apiKey) {
53
+ params.set('apiKey', this.config.apiKey);
54
+ }
55
+ const url = `${relayUrl}${separator}${params.toString()}`;
56
+ try {
57
+ this._ws = new WebSocket(url);
58
+ this._ws.binaryType = 'arraybuffer';
59
+ }
60
+ catch {
61
+ this.scheduleReconnect();
62
+ return;
63
+ }
64
+ this._ws.addEventListener('open', () => {
65
+ this._reconnectDelayMs = 1000;
66
+ this.sendSyncMessage();
67
+ });
68
+ this._ws.addEventListener('message', (event) => {
69
+ this.handleMessage(event.data);
70
+ });
71
+ this._ws.addEventListener('close', () => {
72
+ this._ws = null;
73
+ this.scheduleReconnect();
74
+ });
75
+ this._ws.addEventListener('error', () => {
76
+ // close event will follow and trigger reconnect
77
+ });
78
+ }
79
+ scheduleReconnect() {
80
+ if (this._destroyed || this._reconnectTimer)
81
+ return;
82
+ this._reconnectTimer = setTimeout(() => {
83
+ this._reconnectTimer = null;
84
+ this.connectWebSocket();
85
+ }, this._reconnectDelayMs);
86
+ this._reconnectDelayMs = Math.min(this._reconnectDelayMs * 2, this._maxReconnectDelayMs);
87
+ }
88
+ sendSyncMessage() {
89
+ const am = this._automerge;
90
+ if (!am || !this._ws || this._ws.readyState !== WebSocket.OPEN)
91
+ return;
92
+ const [nextSyncState, message] = am.generateSyncMessage(this._doc, this._syncState);
93
+ this._syncState = nextSyncState;
94
+ if (message) {
95
+ this._ws.send(message);
96
+ }
97
+ }
98
+ handleMessage(data) {
99
+ const am = this._automerge;
100
+ if (!am)
101
+ return;
102
+ const message = new Uint8Array(data);
103
+ const [nextDoc, nextSyncState] = am.receiveSyncMessage(this._doc, this._syncState, message);
104
+ this._doc = nextDoc;
105
+ this._syncState = nextSyncState;
106
+ this._onChange?.(this._doc);
107
+ // After receiving, we may need to send our own changes
108
+ this.sendSyncMessage();
109
+ }
110
+ /**
111
+ * Update the local document. Required because Automerge documents are immutable —
112
+ * after calling Automerge.change(), pass the new doc here to sync it.
113
+ */
114
+ updateDoc(newDoc) {
115
+ this._doc = newDoc;
116
+ this.sendSyncMessage();
117
+ }
118
+ get doc() {
119
+ return this._doc;
120
+ }
121
+ get ready() {
122
+ return this._ready;
123
+ }
124
+ get roomName() {
125
+ return this.config.roomName;
126
+ }
127
+ get connected() {
128
+ return this._ws !== null && this._ws.readyState === WebSocket.OPEN;
129
+ }
130
+ disconnect() {
131
+ if (this._reconnectTimer) {
132
+ clearTimeout(this._reconnectTimer);
133
+ this._reconnectTimer = null;
134
+ }
135
+ if (this._ws) {
136
+ this._ws.close();
137
+ this._ws = null;
138
+ }
139
+ }
140
+ async destroy() {
141
+ this._destroyed = true;
142
+ this.disconnect();
143
+ }
144
+ }
145
+ function toWsUrl(url) {
146
+ if (url.startsWith('wss://') || url.startsWith('ws://'))
147
+ return url;
148
+ if (url.startsWith('https://'))
149
+ return `wss://${url.slice('https://'.length)}`;
150
+ if (url.startsWith('http://'))
151
+ return `ws://${url.slice('http://'.length)}`;
152
+ return url;
153
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Aeon Integration Configuration
3
+ *
4
+ * Configuration for Aeon sync features in Dash.
5
+ * All features enabled by default.
6
+ */
7
+ export interface AeonConfig {
8
+ /** Enable delta sync optimization (70-80% bandwidth reduction) */
9
+ enableDeltaSync: boolean;
10
+ /** Enable rich presence tracking (cursors, sections, activity) */
11
+ enableRichPresence: boolean;
12
+ /** Enable offline operation queuing */
13
+ enableOfflineQueue: boolean;
14
+ /** Bytes threshold before falling back to full sync instead of delta */
15
+ deltaThreshold: number;
16
+ /** Maximum operations to hold in offline queue */
17
+ maxOfflineQueueSize: number;
18
+ /** Maximum retries for failed offline operations */
19
+ maxOfflineRetries: number;
20
+ }
21
+ export declare const defaultAeonConfig: AeonConfig;