@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,1328 +0,0 @@
1
- // import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; // moved to dynamic import
2
- import { vectorEngine } from './ai.js';
3
- import { schema as defaultLens } from '../schema/lens.js';
4
- import * as Y from 'yjs';
5
- export class DashEngine {
6
- db = null;
7
- readyPromise;
8
- listeners = new Set();
9
- lens = defaultLens;
10
- currentSchemaVersion = 1;
11
- // Cloud sync state (D1)
12
- cloudConfig = null;
13
- cloudSyncTimer = null;
14
- isCloudSyncing = false;
15
- lastCloudSyncTime = 0;
16
- cloudSyncEnabled = false;
17
- syncedTables = new Set();
18
- // Relay sync state (Real-time)
19
- relayProvider = null; // HybridProvider instance
20
- relayDoc = null;
21
- relaySyncEnabled = false;
22
- // Internal tables that should never sync
23
- INTERNAL_TABLES = new Set([
24
- 'dash_metadata',
25
- 'dash_items',
26
- 'dash_vec_idx',
27
- 'dash_spatial_idx',
28
- 'dash_spatial_map',
29
- 'dash_sync_meta',
30
- 'dash_sync_queue',
31
- 'dash_sync_updates',
32
- 'sqlite_sequence',
33
- ]);
34
- constructor() {
35
- // SSR/Build safety: Only initialize in browser environments
36
- if (typeof window !== 'undefined') {
37
- this.readyPromise = this.init();
38
- }
39
- else {
40
- this.readyPromise = Promise.resolve();
41
- }
42
- }
43
- async init() {
44
- try {
45
- const sqlite3InitModule = (await import('@sqlite.org/sqlite-wasm')).default;
46
- const sqlite3 = await sqlite3InitModule();
47
- if ('opfs' in sqlite3) {
48
- this.db = new sqlite3.oo1.OpfsDb('/dash.db');
49
- console.log('Dash: SQLite OPFS database opened.');
50
- }
51
- else {
52
- console.warn('Dash: OPFS is not available. Falling back to transient storage.');
53
- this.db = new sqlite3.oo1.DB('/dash-memory.db', 'ct');
54
- }
55
- // Load Vector Extension (Simulation/Shim for now)
56
- await import('./vec_extension.js').then(m => m.loadVectorExtension(this.db));
57
- this.initializeSchema();
58
- // Auto-enable cloud sync if endpoint is available (ON BY DEFAULT)
59
- this.tryAutoEnableCloudSync();
60
- }
61
- catch (err) {
62
- console.error('Dash: Failed to initialize SQLite WASM', err);
63
- throw err;
64
- }
65
- }
66
- /**
67
- * Try to auto-enable cloud sync from environment
68
- * Cloud sync is ON BY DEFAULT when a sync endpoint is detected
69
- */
70
- tryAutoEnableCloudSync() {
71
- try {
72
- // Detect sync endpoint from various sources
73
- const syncUrl = this.detectSyncEndpoint();
74
- if (!syncUrl) {
75
- console.log('[Dash] No sync endpoint detected, cloud sync disabled');
76
- return;
77
- }
78
- // Auto-enable with default config
79
- this.enableCloudSync({
80
- baseUrl: syncUrl,
81
- getAuthToken: async () => this.getDefaultAuthToken(),
82
- syncInterval: 30000,
83
- onSyncError: (err) => {
84
- // Graceful degradation - just log, don't crash
85
- console.warn('[Dash] Cloud sync failed (graceful degradation):', err.message);
86
- },
87
- });
88
- console.log('[Dash] Cloud sync auto-enabled:', syncUrl);
89
- }
90
- catch (err) {
91
- // Graceful degradation - local-first still works
92
- console.warn('[Dash] Could not auto-enable cloud sync:', err);
93
- }
94
- }
95
- /**
96
- * Detect sync endpoint from environment variables or same-origin
97
- */
98
- detectSyncEndpoint() {
99
- // Check various env var patterns
100
- const envVars = [
101
- 'DASH_SYNC_URL',
102
- 'NEXT_PUBLIC_DASH_SYNC_URL',
103
- 'VITE_DASH_SYNC_URL',
104
- 'DASH_API_URL',
105
- 'NEXT_PUBLIC_API_URL',
106
- 'VITE_API_URL',
107
- ];
108
- // Try globalThis.process.env (Node/Next.js)
109
- if (typeof globalThis !== 'undefined' && globalThis.process?.env) {
110
- const env = globalThis.process.env;
111
- for (const key of envVars) {
112
- if (env[key])
113
- return env[key];
114
- }
115
- }
116
- // Try import.meta.env (Vite)
117
- if (typeof import.meta !== 'undefined' && import.meta.env) {
118
- const env = import.meta.env;
119
- for (const key of envVars) {
120
- if (env[key])
121
- return env[key];
122
- }
123
- }
124
- // Try window.__ENV__ (runtime injection)
125
- if (typeof window !== 'undefined' && window.__ENV__) {
126
- const env = window.__ENV__;
127
- for (const key of envVars) {
128
- if (env[key])
129
- return env[key];
130
- }
131
- }
132
- // Auto-detect same-origin sync for Cloudflare/edge deployments
133
- // If running in browser, use same origin as sync endpoint
134
- if (typeof window !== 'undefined' && window.location?.origin) {
135
- // Only auto-enable for HTTPS (production) or localhost (dev)
136
- const origin = window.location.origin;
137
- if (origin.startsWith('https://') || origin.includes('localhost') || origin.includes('127.0.0.1')) {
138
- return origin;
139
- }
140
- }
141
- return null;
142
- }
143
- /**
144
- * Detect relay endpoint from environment variables
145
- */
146
- detectRelayEndpoint() {
147
- // Check various env var patterns for relay
148
- const envVars = [
149
- 'DASH_RELAY_URL',
150
- 'NEXT_PUBLIC_DASH_RELAY_URL',
151
- 'VITE_DASH_RELAY_URL',
152
- 'DASH_RELAY_WS_URL',
153
- 'NEXT_PUBLIC_DASH_RELAY_WS_URL',
154
- 'VITE_DASH_RELAY_WS_URL',
155
- ];
156
- // Try globalThis.process.env (Node/Next.js)
157
- if (typeof globalThis !== 'undefined' && globalThis.process?.env) {
158
- const env = globalThis.process.env;
159
- for (const key of envVars) {
160
- if (env[key])
161
- return env[key];
162
- }
163
- }
164
- // Try import.meta.env (Vite)
165
- if (typeof import.meta !== 'undefined' && import.meta.env) {
166
- const env = import.meta.env;
167
- for (const key of envVars) {
168
- if (env[key])
169
- return env[key];
170
- }
171
- }
172
- // Try window.__ENV__ (runtime injection)
173
- if (typeof window !== 'undefined' && window.__ENV__) {
174
- const env = window.__ENV__;
175
- for (const key of envVars) {
176
- if (env[key])
177
- return env[key];
178
- }
179
- }
180
- // Auto-detect same-origin relay for Cloudflare/edge deployments
181
- // Convert HTTP origin to WS URL
182
- if (typeof window !== 'undefined' && window.location?.origin) {
183
- const origin = window.location.origin;
184
- if (origin.startsWith('https://')) {
185
- return origin.replace('https://', 'wss://');
186
- }
187
- else if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
188
- return origin.replace('http://', 'ws://');
189
- }
190
- }
191
- return null;
192
- }
193
- /**
194
- * Get default auth token from common auth patterns
195
- */
196
- async getDefaultAuthToken() {
197
- try {
198
- // Try localStorage token
199
- if (typeof localStorage !== 'undefined') {
200
- const token = localStorage.getItem('auth_token') ||
201
- localStorage.getItem('access_token') ||
202
- localStorage.getItem('id_token');
203
- if (token)
204
- return token;
205
- }
206
- // Try cookie-based auth (will be sent automatically)
207
- return null;
208
- }
209
- catch {
210
- return null;
211
- }
212
- }
213
- initializeSchema() {
214
- if (!this.db)
215
- return;
216
- this.db.exec(`
217
- CREATE TABLE IF NOT EXISTS dash_metadata (
218
- key TEXT PRIMARY KEY,
219
- value TEXT
220
- );
221
- CREATE TABLE IF NOT EXISTS dash_items (
222
- id TEXT PRIMARY KEY,
223
- content TEXT
224
- );
225
- -- Spatial Index (R-Tree) for 3D coordinates
226
- `);
227
- // Create Virtual Tables separately as they might fail if extensions (vec0, rtree) are missing
228
- try {
229
- this.db.exec(`
230
- CREATE VIRTUAL TABLE IF NOT EXISTS dash_vec_idx USING vec0(
231
- id TEXT PRIMARY KEY,
232
- embedding float[384]
233
- );
234
- `);
235
- }
236
- catch (e) {
237
- console.warn('Dash: Failed to create vec0 table (vector extension missing?)', e);
238
- }
239
- try {
240
- this.db.exec(`
241
- CREATE VIRTUAL TABLE IF NOT EXISTS dash_spatial_idx USING rtree(
242
- id, -- Integer Primary Key (mapped or auto)
243
- minX, maxX,
244
- minY, maxY,
245
- minZ, maxZ
246
- );
247
- `);
248
- }
249
- catch (e) {
250
- console.warn('Dash: Failed to create rtree table', e);
251
- }
252
- this.db.exec(`
253
- -- Mapping table since R-Tree requires integer IDs
254
- CREATE TABLE IF NOT EXISTS dash_spatial_map (
255
- rowid INTEGER PRIMARY KEY,
256
- item_id TEXT UNIQUE
257
- );
258
- `);
259
- }
260
- async ready() {
261
- return this.readyPromise;
262
- }
263
- tableListeners = new Map();
264
- subscribe(table, callback) {
265
- if (!this.tableListeners.has(table)) {
266
- this.tableListeners.set(table, new Set());
267
- }
268
- this.tableListeners.get(table).add(callback);
269
- return () => {
270
- const set = this.tableListeners.get(table);
271
- if (set) {
272
- set.delete(callback);
273
- if (set.size === 0)
274
- this.tableListeners.delete(table);
275
- }
276
- };
277
- }
278
- notify(table) {
279
- if (this.tableListeners.has(table)) {
280
- this.tableListeners.get(table).forEach(cb => cb());
281
- }
282
- // Also notify global listeners (optional, but good for debugging)
283
- this.listeners.forEach(cb => cb());
284
- }
285
- // Hook into SQLite updates
286
- // In a real WASM build we would use db.updateHook((type, dbName, tableName, rowid) => ...)
287
- // For this implementation effectively utilizing the "update_hook" concept via our execute wrapper
288
- // which is safer across different sqlite-wasm build versions (some minimal builds exclude hooks).
289
- notifyChanges(sql, bind) {
290
- const upper = sql.trim().toUpperCase();
291
- // Naive table parser for MVP
292
- // Matches: INSERT INTO table ...
293
- // Matches: UPDATE table ...
294
- // Matches: DELETE FROM table ...
295
- // Matches: CREATE TABLE table ...
296
- let table = '';
297
- let operation = null;
298
- let rowId = null;
299
- if (upper.startsWith('INSERT INTO')) {
300
- table = sql.split(/\s+/)[2];
301
- operation = 'create';
302
- }
303
- else if (upper.startsWith('UPDATE')) {
304
- table = sql.split(/\s+/)[1];
305
- operation = 'update';
306
- }
307
- else if (upper.startsWith('DELETE FROM')) {
308
- table = sql.split(/\s+/)[2];
309
- operation = 'delete';
310
- }
311
- else if (upper.startsWith('CREATE TABLE')) {
312
- // Extract table name from CREATE TABLE [IF NOT EXISTS] tablename
313
- const match = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
314
- if (match) {
315
- table = match[1];
316
- // Auto-setup cloud sync triggers for new tables
317
- if (this.cloudSyncEnabled && this.shouldSyncTable(table) && !this.syncedTables.has(table)) {
318
- // Defer trigger setup to after the table is created
319
- setTimeout(() => {
320
- this.setupTableSyncTriggers(table);
321
- this.syncedTables.add(table);
322
- console.log('[Dash] Auto-enabled cloud sync for new table:', table);
323
- }, 0);
324
- }
325
- }
326
- }
327
- if (table) {
328
- // cleanup quotes etc
329
- table = table.replace(/["';]/g, '');
330
- this.notify(table);
331
- // === Relay Sync: Broadcast changes in real-time ===
332
- // For Relay, we want immediate updates (not batched like D1)
333
- if (this.relaySyncEnabled && operation && this.shouldSyncTable(table)) {
334
- // Try to extract row ID from bind parameters or the SQL
335
- // For INSERT/UPDATE, bind[0] is often the id
336
- if (bind && bind.length > 0) {
337
- // Check if first param looks like an id
338
- const firstParam = bind[0];
339
- if (typeof firstParam === 'string' && firstParam.length > 0) {
340
- rowId = firstParam;
341
- }
342
- }
343
- // If we have a row ID, get the full row data and broadcast
344
- if (rowId && operation !== 'delete') {
345
- try {
346
- const row = this.execute(`SELECT * FROM "${table}" WHERE id = ?`, [rowId]);
347
- if (row.length > 0) {
348
- this.broadcastToRelay(table, rowId, operation, row[0]);
349
- }
350
- }
351
- catch {
352
- // Ignore - table might not have id column
353
- }
354
- }
355
- else if (rowId && operation === 'delete') {
356
- this.broadcastToRelay(table, rowId, 'delete', null);
357
- }
358
- }
359
- }
360
- }
361
- execute(sql, bind) {
362
- if (!this.db)
363
- throw new Error('Database not initialized');
364
- const result = [];
365
- this.db.exec({
366
- sql,
367
- bind,
368
- rowMode: 'object',
369
- callback: (row) => {
370
- result.push(row);
371
- },
372
- });
373
- this.notifyChanges(sql, bind);
374
- return result;
375
- }
376
- // Zero-Copy Binding Implementation
377
- // Returns a flat Float32Array of the results.
378
- // Ideal for passing directly to WebGL/WebGPU buffers.
379
- getFloat32(sql, bind) {
380
- if (!this.db)
381
- throw new Error('Database not initialized');
382
- const result = [];
383
- this.db.exec({
384
- sql,
385
- bind,
386
- rowMode: 'array',
387
- callback: (row) => {
388
- for (const val of row) {
389
- result.push(val);
390
- }
391
- }
392
- });
393
- return new Float32Array(result);
394
- }
395
- async addWithEmbedding(id, content, spatial) {
396
- const vector = await vectorEngine.embed(content);
397
- this.db.exec('BEGIN TRANSACTION');
398
- try {
399
- this.execute('INSERT OR REPLACE INTO dash_items (id, content) VALUES (?, ?)', [id, content]);
400
- this.execute('INSERT OR REPLACE INTO dash_vec_idx(id, embedding) VALUES (?, ?)', [id, vector]);
401
- if (spatial) {
402
- // Map text ID to integer rowid
403
- this.execute('INSERT OR IGNORE INTO dash_spatial_map (item_id) VALUES (?)', [id]);
404
- const rowMap = this.execute('SELECT rowid FROM dash_spatial_map WHERE item_id = ?', [id]);
405
- if (rowMap.length > 0) {
406
- const rid = rowMap[0].rowid;
407
- // R-Tree insert
408
- // Treat point as box with 0 size or small epsilon
409
- const r = 0.001;
410
- this.execute('INSERT OR REPLACE INTO dash_spatial_idx (id, minX, maxX, minY, maxY, minZ, maxZ) VALUES (?, ?, ?, ?, ?, ?, ?)', [rid, spatial.x - r, spatial.x + r, spatial.y - r, spatial.y + r, spatial.z - r, spatial.z + r]);
411
- }
412
- }
413
- this.db.exec('COMMIT');
414
- }
415
- catch (e) {
416
- this.db.exec('ROLLBACK');
417
- throw e;
418
- }
419
- return [];
420
- }
421
- async search(query, limit = 5) {
422
- const queryVector = await vectorEngine.embed(query);
423
- try {
424
- const rows = this.execute(`
425
- SELECT
426
- item.id,
427
- item.content,
428
- distance,
429
- dash_metadata.value as _v
430
- FROM dash_vec_idx
431
- JOIN dash_items AS item ON item.id = dash_vec_idx.id
432
- LEFT JOIN dash_metadata ON dash_metadata.key = 'schema_version_' || item.id
433
- WHERE embedding MATCH ?
434
- ORDER BY distance
435
- LIMIT ?
436
- `, [queryVector, limit]);
437
- // Normalize distance to score (assuming Cosine Distance: score = 1 - distance)
438
- return rows.map((row) => {
439
- // Apply Lens Migration if version differs
440
- // Default to v1 if no version metadata found
441
- const version = row._v ? parseInt(row._v) : 1;
442
- const migrated = this.lens.migrate(row, version, this.currentSchemaVersion);
443
- return {
444
- ...migrated,
445
- score: row.distance !== undefined ? 1 - row.distance : 0
446
- };
447
- });
448
- }
449
- catch (e) {
450
- console.warn("Vector search failed, using fallback", e);
451
- return [];
452
- }
453
- }
454
- async spatialQuery(bounds) {
455
- const rows = this.execute(`
456
- SELECT
457
- map.item_id as id,
458
- item.content,
459
- idx.minX, idx.maxX, idx.minY, idx.maxY, idx.minZ, idx.maxZ,
460
- meta.value as _v
461
- FROM dash_spatial_idx AS idx
462
- JOIN dash_spatial_map AS map ON map.rowid = idx.id
463
- JOIN dash_items AS item ON item.id = map.item_id
464
- LEFT JOIN dash_metadata AS meta ON meta.key = 'schema_version_' || item.id
465
- WHERE
466
- minX >= ? AND maxX <= ? AND
467
- minY >= ? AND maxY <= ? AND
468
- minZ >= ? AND maxZ <= ?
469
- `, [bounds.minX, bounds.maxX, bounds.minY, bounds.maxY, bounds.minZ, bounds.maxZ]);
470
- return rows.map(row => {
471
- const version = row._v ? parseInt(row._v) : 1;
472
- return this.lens.migrate(row, version, this.currentSchemaVersion);
473
- });
474
- }
475
- close() {
476
- this.stopCloudSync();
477
- this.stopRelaySync();
478
- this.db?.close();
479
- }
480
- /**
481
- * Stop Relay sync
482
- */
483
- stopRelaySync() {
484
- if (this.relayProvider) {
485
- this.relayProvider.destroy();
486
- this.relayProvider = null;
487
- }
488
- if (this.relayDoc) {
489
- this.relayDoc.destroy();
490
- this.relayDoc = null;
491
- }
492
- this.relaySyncEnabled = false;
493
- console.log('[Dash] Relay sync stopped');
494
- }
495
- // ============================================
496
- // CLOUD SYNC (D1/R2) - AUTOMATIC SYNC
497
- // ============================================
498
- /**
499
- * Enable cloud sync - changes automatically sync to D1/R2
500
- * Just call this once with your config, and sync happens magically.
501
- *
502
- * Cloud sync is ON BY DEFAULT when running in a Cloudflare environment.
503
- * Call this to customize the config or explicitly enable/disable.
504
- *
505
- * @example
506
- * ```ts
507
- * await dash.ready();
508
- * // Option 1: Use auto-detected endpoint (default)
509
- * // Cloud sync is already enabled if DASH_SYNC_URL env var is set
510
- *
511
- * // Option 2: Explicit config
512
- * dash.enableCloudSync({
513
- * baseUrl: 'https://api.example.com',
514
- * getAuthToken: async () => auth.token,
515
- * });
516
- *
517
- * // Option 3: Disable cloud sync
518
- * dash.enableCloudSync({ disabled: true });
519
- * ```
520
- */
521
- enableCloudSync(config = {}) {
522
- // Handle explicit disable
523
- if (config.disabled) {
524
- this.disableCloudSync();
525
- return;
526
- }
527
- if (!this.db) {
528
- console.warn('[Dash] Database not ready. Call enableCloudSync after ready()');
529
- return;
530
- }
531
- // Try to get baseUrl from config or auto-detect
532
- const baseUrl = config.baseUrl || this.detectSyncEndpoint();
533
- const relayUrl = config.relayUrl || this.detectRelayEndpoint();
534
- // Need at least one sync method
535
- if (!baseUrl && !relayUrl) {
536
- console.warn('[Dash] No sync endpoints available. Cloud sync disabled.');
537
- return;
538
- }
539
- this.cloudConfig = {
540
- syncInterval: 30000,
541
- ...config,
542
- baseUrl: baseUrl || undefined,
543
- relayUrl: relayUrl || undefined,
544
- getAuthToken: config.getAuthToken || (() => this.getDefaultAuthToken()),
545
- };
546
- // === D1 Sync Setup (Persistence) ===
547
- if (baseUrl && !config.disableD1) {
548
- // Create sync infrastructure tables
549
- this.initializeCloudSyncSchema();
550
- // Load last sync time
551
- const meta = this.execute("SELECT value FROM dash_sync_meta WHERE key = 'lastCloudSyncTime'");
552
- if (meta.length > 0) {
553
- this.lastCloudSyncTime = parseInt(meta[0].value, 10) || 0;
554
- }
555
- // Set up triggers for all existing user tables
556
- this.setupAllTableTriggers();
557
- this.cloudSyncEnabled = true;
558
- // Start auto-sync
559
- if (this.cloudConfig.syncInterval && this.cloudConfig.syncInterval > 0) {
560
- this.startCloudSync();
561
- }
562
- console.log('[Dash] D1 sync enabled:', baseUrl);
563
- }
564
- // === Relay Sync Setup (Real-time) ===
565
- if (relayUrl && !config.disableRelay) {
566
- this.initializeRelaySync(relayUrl, config).catch(err => {
567
- console.warn('[Dash] Relay sync failed to initialize (graceful degradation):', err.message);
568
- });
569
- }
570
- }
571
- /**
572
- * Initialize Relay sync for real-time updates
573
- */
574
- async initializeRelaySync(relayUrl, config) {
575
- try {
576
- // Dynamically import HybridProvider to avoid circular dependencies
577
- const { HybridProvider } = await import('../sync/hybrid-provider.js');
578
- // Create Yjs document for relay sync
579
- this.relayDoc = new Y.Doc();
580
- // Get room name
581
- const roomName = config.relayRoom || `dash-${config.userId || 'default'}`;
582
- // Initialize HybridProvider
583
- this.relayProvider = new HybridProvider(relayUrl, roomName, this.relayDoc, {
584
- aeonConfig: {
585
- enableDeltaSync: true,
586
- enableRichPresence: true,
587
- enableOfflineQueue: true,
588
- deltaThreshold: 64, // Compress updates > 64 bytes
589
- maxOfflineQueueSize: 1000,
590
- maxOfflineRetries: 5,
591
- },
592
- relayDiscovery: config.relayDiscovery,
593
- relayPerformance: config.relayPerformance,
594
- relayPrivacy: config.relayPrivacy,
595
- });
596
- // Listen for remote changes and apply to local SQLite
597
- this.relayDoc.on('update', (update, origin) => {
598
- if (origin === 'local')
599
- return; // Skip our own updates
600
- this.applyRelayUpdate(update);
601
- });
602
- // Enter high-frequency mode if requested
603
- if (config.relayHighFrequency) {
604
- await this.relayProvider.enterHighFrequencyMode();
605
- }
606
- this.relaySyncEnabled = true;
607
- console.log('[Dash] Relay sync enabled:', relayUrl, '(room:', roomName, ')');
608
- }
609
- catch (err) {
610
- throw err;
611
- }
612
- }
613
- /**
614
- * Apply an update from Relay to local SQLite
615
- */
616
- applyRelayUpdate(update) {
617
- if (!this.relayDoc)
618
- return;
619
- try {
620
- // Get the shared map that contains table updates
621
- const tablesMap = this.relayDoc.getMap('tables');
622
- // Process each table's updates
623
- tablesMap.forEach((tableData, tableName) => {
624
- if (!this.shouldSyncTable(tableName))
625
- return;
626
- // Apply each row update
627
- const rowsMap = tableData;
628
- rowsMap.forEach((rowData, rowId) => {
629
- this.applyServerChange(tableName, { id: rowId, ...rowData });
630
- });
631
- });
632
- }
633
- catch (err) {
634
- console.warn('[Dash] Failed to apply relay update:', err);
635
- }
636
- }
637
- /**
638
- * Broadcast a local change to Relay
639
- */
640
- broadcastToRelay(tableName, rowId, operation, data) {
641
- if (!this.relayDoc || !this.relaySyncEnabled)
642
- return;
643
- try {
644
- const tablesMap = this.relayDoc.getMap('tables');
645
- // Get or create table map
646
- let tableMap = tablesMap.get(tableName);
647
- if (!tableMap) {
648
- tableMap = new Y.Map();
649
- tablesMap.set(tableName, tableMap);
650
- }
651
- if (operation === 'delete') {
652
- tableMap.delete(rowId);
653
- }
654
- else {
655
- tableMap.set(rowId, { ...data, _syncedAt: Date.now() });
656
- }
657
- }
658
- catch (err) {
659
- console.warn('[Dash] Failed to broadcast to relay:', err);
660
- }
661
- }
662
- /**
663
- * Initialize cloud sync schema (queue and metadata tables)
664
- */
665
- initializeCloudSyncSchema() {
666
- this.db.exec(`
667
- CREATE TABLE IF NOT EXISTS dash_sync_meta (
668
- key TEXT PRIMARY KEY,
669
- value TEXT
670
- )
671
- `);
672
- this.db.exec(`
673
- CREATE TABLE IF NOT EXISTS dash_sync_queue (
674
- id INTEGER PRIMARY KEY AUTOINCREMENT,
675
- table_name TEXT NOT NULL,
676
- row_id TEXT NOT NULL,
677
- operation TEXT NOT NULL CHECK(operation IN ('create', 'update', 'delete')),
678
- data TEXT,
679
- created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000),
680
- synced INTEGER DEFAULT 0
681
- )
682
- `);
683
- this.db.exec(`
684
- CREATE INDEX IF NOT EXISTS idx_dash_sync_queue_pending
685
- ON dash_sync_queue(synced, created_at)
686
- `);
687
- }
688
- /**
689
- * Set up sync triggers for all user tables
690
- */
691
- setupAllTableTriggers() {
692
- const tables = this.execute(`
693
- SELECT name FROM sqlite_master
694
- WHERE type = 'table'
695
- AND name NOT LIKE 'sqlite_%'
696
- `);
697
- for (const t of tables) {
698
- const tableName = t.name;
699
- if (!this.shouldSyncTable(tableName))
700
- continue;
701
- this.setupTableSyncTriggers(tableName);
702
- this.syncedTables.add(tableName);
703
- }
704
- console.log('[Dash] Sync triggers set up for:', Array.from(this.syncedTables));
705
- }
706
- /**
707
- * Check if a table should be synced
708
- */
709
- shouldSyncTable(tableName) {
710
- // Skip internal tables
711
- if (this.INTERNAL_TABLES.has(tableName))
712
- return false;
713
- // Skip excluded tables
714
- if (this.cloudConfig?.excludeTables?.includes(tableName))
715
- return false;
716
- return true;
717
- }
718
- /**
719
- * Set up sync triggers for a specific table
720
- */
721
- setupTableSyncTriggers(tableName) {
722
- // Check if table has an 'id' column (required for sync)
723
- const columns = this.execute(`PRAGMA table_info('${tableName.replace(/'/g, "''")}')`);
724
- const hasId = columns.some((c) => c.name === 'id');
725
- if (!hasId) {
726
- console.warn(`[Dash] Table ${tableName} has no 'id' column, skipping sync triggers`);
727
- return;
728
- }
729
- // Drop existing triggers
730
- try {
731
- this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_insert`);
732
- this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_update`);
733
- this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_delete`);
734
- }
735
- catch {
736
- // Ignore
737
- }
738
- // INSERT trigger - capture full row as JSON
739
- this.db.exec(`
740
- CREATE TRIGGER dash_cloud_${tableName}_insert
741
- AFTER INSERT ON "${tableName}"
742
- WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
743
- BEGIN
744
- INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
745
- SELECT '${tableName}', NEW.id, 'create', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
746
- END
747
- `);
748
- // UPDATE trigger
749
- this.db.exec(`
750
- CREATE TRIGGER dash_cloud_${tableName}_update
751
- AFTER UPDATE ON "${tableName}"
752
- WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
753
- BEGIN
754
- INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
755
- SELECT '${tableName}', NEW.id, 'update', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
756
- END
757
- `);
758
- // DELETE trigger
759
- this.db.exec(`
760
- CREATE TRIGGER dash_cloud_${tableName}_delete
761
- AFTER DELETE ON "${tableName}"
762
- WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
763
- BEGIN
764
- INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
765
- VALUES ('${tableName}', OLD.id, 'delete', NULL);
766
- END
767
- `);
768
- // Enable triggers
769
- this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('cloud_enabled', '1')");
770
- }
771
- /**
772
- * Build json_object() arguments for a table's columns
773
- */
774
- buildJsonObjectArgs(tableName, columns, prefix) {
775
- return columns
776
- .map((c) => `'${c.name}', ${prefix}."${c.name}"`)
777
- .join(', ');
778
- }
779
- /**
780
- * Start automatic cloud sync
781
- */
782
- startCloudSync() {
783
- if (this.cloudSyncTimer)
784
- return;
785
- this.cloudSyncTimer = setInterval(() => {
786
- if (typeof navigator !== 'undefined' && !navigator.onLine) {
787
- return; // Skip sync when offline
788
- }
789
- this.syncToCloud().catch(console.error);
790
- }, this.cloudConfig.syncInterval);
791
- // Also do an immediate sync
792
- this.syncToCloud().catch(console.error);
793
- console.log('[Dash] Cloud auto-sync started, interval:', this.cloudConfig.syncInterval, 'ms');
794
- }
795
- /**
796
- * Stop automatic cloud sync
797
- */
798
- stopCloudSync() {
799
- if (this.cloudSyncTimer) {
800
- clearInterval(this.cloudSyncTimer);
801
- this.cloudSyncTimer = null;
802
- }
803
- this.cloudSyncEnabled = false;
804
- console.log('[Dash] Cloud sync stopped');
805
- }
806
- /**
807
- * Perform a sync to cloud (D1/R2)
808
- */
809
- async syncToCloud() {
810
- if (!this.cloudConfig || !this.cloudSyncEnabled) {
811
- return { pushed: 0, pulled: 0, errors: ['Cloud sync not enabled'], timestamp: Date.now() };
812
- }
813
- if (this.isCloudSyncing) {
814
- return { pushed: 0, pulled: 0, errors: ['Sync already in progress'], timestamp: Date.now() };
815
- }
816
- this.isCloudSyncing = true;
817
- const result = { pushed: 0, pulled: 0, errors: [], timestamp: Date.now() };
818
- try {
819
- const token = this.cloudConfig.getAuthToken
820
- ? await this.cloudConfig.getAuthToken()
821
- : await this.getDefaultAuthToken();
822
- // Token is optional - sync can work without auth for public endpoints
823
- // Get pending changes
824
- const pending = this.execute(`
825
- SELECT * FROM dash_sync_queue
826
- WHERE synced = 0
827
- ORDER BY created_at ASC
828
- LIMIT 100
829
- `);
830
- // Group by table
831
- const changesByTable = {};
832
- for (const entry of pending) {
833
- const tableName = entry.table_name;
834
- if (!changesByTable[tableName]) {
835
- changesByTable[tableName] = { creates: [], updates: [], deletes: [] };
836
- }
837
- const data = entry.data ? JSON.parse(entry.data) : null;
838
- switch (entry.operation) {
839
- case 'create':
840
- if (data)
841
- changesByTable[tableName].creates.push(data);
842
- break;
843
- case 'update':
844
- if (data)
845
- changesByTable[tableName].updates.push(data);
846
- break;
847
- case 'delete':
848
- changesByTable[tableName].deletes.push(entry.row_id);
849
- break;
850
- }
851
- }
852
- // Sync each table
853
- for (const [tableName, changes] of Object.entries(changesByTable)) {
854
- if (changes.creates.length === 0 && changes.updates.length === 0 && changes.deletes.length === 0) {
855
- continue;
856
- }
857
- try {
858
- const headers = {
859
- 'Content-Type': 'application/json',
860
- };
861
- if (token) {
862
- headers['Authorization'] = `Bearer ${token}`;
863
- }
864
- const response = await fetch(`${this.cloudConfig.baseUrl}/api/sync`, {
865
- method: 'POST',
866
- headers,
867
- body: JSON.stringify({
868
- table: tableName,
869
- creates: changes.creates,
870
- updates: changes.updates,
871
- deletes: changes.deletes,
872
- lastSyncTime: this.lastCloudSyncTime,
873
- }),
874
- });
875
- if (!response.ok) {
876
- const errorText = await response.text();
877
- result.errors.push(`${tableName}: ${response.status} ${errorText}`);
878
- continue;
879
- }
880
- const syncResponse = await response.json();
881
- result.pushed += changes.creates.length + changes.updates.length + changes.deletes.length;
882
- result.pulled += syncResponse.serverChanges?.length || 0;
883
- // Apply server changes locally (with triggers disabled)
884
- if (syncResponse.serverChanges && syncResponse.serverChanges.length > 0) {
885
- this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
886
- try {
887
- for (const change of syncResponse.serverChanges) {
888
- this.applyServerChange(tableName, change);
889
- }
890
- }
891
- finally {
892
- this.execute("UPDATE dash_sync_meta SET value = '1' WHERE key = 'cloud_enabled'");
893
- }
894
- }
895
- // Update sync time
896
- if (syncResponse.syncTime > this.lastCloudSyncTime) {
897
- this.lastCloudSyncTime = syncResponse.syncTime;
898
- this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', ?)", [String(this.lastCloudSyncTime)]);
899
- }
900
- }
901
- catch (err) {
902
- result.errors.push(`${tableName}: ${err instanceof Error ? err.message : String(err)}`);
903
- }
904
- }
905
- // Mark as synced
906
- if (pending.length > 0) {
907
- const ids = pending.map((e) => e.id).join(',');
908
- this.execute(`UPDATE dash_sync_queue SET synced = 1 WHERE id IN (${ids})`);
909
- }
910
- // Clean up old entries
911
- this.execute(`
912
- DELETE FROM dash_sync_queue
913
- WHERE synced = 1
914
- AND id NOT IN (
915
- SELECT id FROM dash_sync_queue
916
- WHERE synced = 1
917
- ORDER BY id DESC
918
- LIMIT 1000
919
- )
920
- `);
921
- this.cloudConfig.onSyncComplete?.(result);
922
- }
923
- catch (err) {
924
- const error = err instanceof Error ? err : new Error(String(err));
925
- result.errors.push(error.message);
926
- this.cloudConfig.onSyncError?.(error);
927
- }
928
- finally {
929
- this.isCloudSyncing = false;
930
- }
931
- return result;
932
- }
933
- /**
934
- * Apply a single server change locally
935
- */
936
- applyServerChange(tableName, change) {
937
- try {
938
- if (change.deleted || change._deleted) {
939
- this.execute(`DELETE FROM "${tableName}" WHERE id = ?`, [change.id]);
940
- return;
941
- }
942
- const columns = Object.keys(change).filter(k => !k.startsWith('_'));
943
- const placeholders = columns.map(() => '?').join(', ');
944
- const values = columns.map(k => {
945
- const v = change[k];
946
- if (v !== null && typeof v === 'object') {
947
- return JSON.stringify(v);
948
- }
949
- return v;
950
- });
951
- const sql = `INSERT OR REPLACE INTO "${tableName}" (${columns.join(', ')}) VALUES (${placeholders})`;
952
- this.execute(sql, values);
953
- }
954
- catch (err) {
955
- console.error(`[Dash] Failed to apply server change to ${tableName}:`, err);
956
- }
957
- }
958
- /**
959
- * Force a full cloud sync (reset last sync time)
960
- */
961
- async forceCloudSync() {
962
- this.lastCloudSyncTime = 0;
963
- this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', '0')");
964
- return this.syncToCloud();
965
- }
966
- /**
967
- * Get cloud sync status (D1 and Relay)
968
- */
969
- getCloudSyncStatus() {
970
- let pendingChanges = 0;
971
- if (this.cloudSyncEnabled && this.db) {
972
- try {
973
- const result = this.execute('SELECT COUNT(*) as count FROM dash_sync_queue WHERE synced = 0');
974
- pendingChanges = result[0]?.count || 0;
975
- }
976
- catch {
977
- // Table might not exist yet
978
- }
979
- }
980
- // Get Relay status
981
- let relayConnected = false;
982
- let relayRoomName = null;
983
- let relayPeerCount = 0;
984
- if (this.relayProvider) {
985
- try {
986
- const status = this.relayProvider.getConnectionStatus();
987
- relayConnected = status.connected;
988
- relayRoomName = status.roomName;
989
- relayPeerCount = this.relayProvider.getPeerCount();
990
- }
991
- catch {
992
- // Provider might not be ready
993
- }
994
- }
995
- return {
996
- enabled: this.cloudSyncEnabled || this.relaySyncEnabled,
997
- syncing: this.isCloudSyncing,
998
- lastSyncTime: this.lastCloudSyncTime,
999
- pendingChanges,
1000
- syncedTables: Array.from(this.syncedTables),
1001
- d1: {
1002
- enabled: this.cloudSyncEnabled,
1003
- syncing: this.isCloudSyncing,
1004
- lastSyncTime: this.lastCloudSyncTime,
1005
- pendingChanges,
1006
- },
1007
- relay: {
1008
- enabled: this.relaySyncEnabled,
1009
- connected: relayConnected,
1010
- roomName: relayRoomName,
1011
- peerCount: relayPeerCount,
1012
- },
1013
- };
1014
- }
1015
- /**
1016
- * Disable cloud sync (both D1 and Relay)
1017
- */
1018
- disableCloudSync() {
1019
- this.stopCloudSync();
1020
- this.stopRelaySync();
1021
- try {
1022
- this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
1023
- }
1024
- catch {
1025
- // Table might not exist yet
1026
- }
1027
- console.log('[Dash] Cloud sync disabled');
1028
- }
1029
- // ============================================
1030
- // INTROSPECTION METHODS FOR DASH-STUDIO
1031
- // ============================================
1032
- /**
1033
- * Get information about all tables in the database
1034
- */
1035
- getAllTables() {
1036
- if (!this.db)
1037
- return [];
1038
- const tables = this.execute(`
1039
- SELECT name, type
1040
- FROM sqlite_master
1041
- WHERE type IN ('table', 'virtual table')
1042
- AND name NOT LIKE 'sqlite_%'
1043
- ORDER BY name
1044
- `);
1045
- return tables.map((t) => {
1046
- const rowCount = this.getTableRowCount(t.name);
1047
- return {
1048
- name: t.name,
1049
- type: t.type === 'table' ? 'table' : 'virtual',
1050
- rowCount,
1051
- isVirtual: t.type !== 'table'
1052
- };
1053
- });
1054
- }
1055
- /**
1056
- * Get detailed schema information for a specific table
1057
- */
1058
- getTableSchema(tableName) {
1059
- if (!this.db)
1060
- return null;
1061
- try {
1062
- const columns = this.execute(`PRAGMA table_info('${tableName.replace(/'/g, "''")}')`);
1063
- // Get indexes for this table
1064
- const indexes = this.execute(`PRAGMA index_list('${tableName.replace(/'/g, "''")}')`);
1065
- // Get foreign keys
1066
- const foreignKeys = this.execute(`PRAGMA foreign_key_list('${tableName.replace(/'/g, "''")}')`);
1067
- return {
1068
- name: tableName,
1069
- columns: columns.map((col) => ({
1070
- cid: col.cid,
1071
- name: col.name,
1072
- type: col.type || 'ANY',
1073
- notNull: col.notnull === 1,
1074
- defaultValue: col.dflt_value,
1075
- isPrimaryKey: col.pk === 1
1076
- })),
1077
- indexes: indexes.map((idx) => ({
1078
- name: idx.name,
1079
- unique: idx.unique === 1,
1080
- origin: idx.origin
1081
- })),
1082
- foreignKeys: foreignKeys.map((fk) => ({
1083
- id: fk.id,
1084
- table: fk.table,
1085
- from: fk.from,
1086
- to: fk.to
1087
- })),
1088
- rowCount: this.getTableRowCount(tableName)
1089
- };
1090
- }
1091
- catch (e) {
1092
- console.warn(`Failed to get schema for table ${tableName}:`, e);
1093
- return null;
1094
- }
1095
- }
1096
- /**
1097
- * Get the row count for a specific table
1098
- */
1099
- getTableRowCount(tableName) {
1100
- if (!this.db)
1101
- return 0;
1102
- try {
1103
- const result = this.execute(`SELECT COUNT(*) as count FROM "${tableName.replace(/"/g, '""')}"`);
1104
- return result[0]?.count ?? 0;
1105
- }
1106
- catch (e) {
1107
- // Virtual tables may not support COUNT
1108
- return -1;
1109
- }
1110
- }
1111
- /**
1112
- * Get statistics about the vector index
1113
- */
1114
- getVectorIndexStats() {
1115
- if (!this.db) {
1116
- return { totalEmbeddings: 0, dimensions: 384, indexExists: false };
1117
- }
1118
- try {
1119
- const countResult = this.execute('SELECT COUNT(*) as count FROM dash_vec_idx');
1120
- const totalEmbeddings = countResult[0]?.count ?? 0;
1121
- return {
1122
- totalEmbeddings,
1123
- dimensions: 384, // Fixed dimension from schema
1124
- indexExists: true,
1125
- tableName: 'dash_vec_idx'
1126
- };
1127
- }
1128
- catch (e) {
1129
- return {
1130
- totalEmbeddings: 0,
1131
- dimensions: 384,
1132
- indexExists: false,
1133
- error: e instanceof Error ? e.message : 'Vector index not available'
1134
- };
1135
- }
1136
- }
1137
- /**
1138
- * Get all embeddings from the vector index
1139
- */
1140
- getAllEmbeddings(limit = 100, offset = 0) {
1141
- if (!this.db)
1142
- return [];
1143
- try {
1144
- const rows = this.execute(`
1145
- SELECT v.id, i.content, v.embedding
1146
- FROM dash_vec_idx v
1147
- LEFT JOIN dash_items i ON v.id = i.id
1148
- LIMIT ? OFFSET ?
1149
- `, [limit, offset]);
1150
- return rows.map((row) => ({
1151
- id: row.id,
1152
- content: row.content,
1153
- embedding: row.embedding ? Array.from(row.embedding) : null
1154
- }));
1155
- }
1156
- catch (e) {
1157
- console.warn('Failed to get embeddings:', e);
1158
- return [];
1159
- }
1160
- }
1161
- /**
1162
- * Search for similar vectors given a raw vector
1163
- */
1164
- searchSimilarByVector(vector, k = 5) {
1165
- if (!this.db)
1166
- return [];
1167
- try {
1168
- const rows = this.execute(`
1169
- SELECT
1170
- item.id,
1171
- item.content,
1172
- distance
1173
- FROM dash_vec_idx
1174
- JOIN dash_items AS item ON item.id = dash_vec_idx.id
1175
- WHERE embedding MATCH ?
1176
- ORDER BY distance
1177
- LIMIT ?
1178
- `, [new Float32Array(vector), k]);
1179
- return rows.map((row) => ({
1180
- id: row.id,
1181
- content: row.content,
1182
- distance: row.distance,
1183
- score: row.distance !== undefined ? 1 - row.distance : 0
1184
- }));
1185
- }
1186
- catch (e) {
1187
- console.warn('Vector search failed:', e);
1188
- return [];
1189
- }
1190
- }
1191
- /**
1192
- * Get statistics about the spatial R-Tree index
1193
- */
1194
- getSpatialIndexStats() {
1195
- if (!this.db) {
1196
- return { totalEntries: 0, indexExists: false };
1197
- }
1198
- try {
1199
- const countResult = this.execute('SELECT COUNT(*) as count FROM dash_spatial_idx');
1200
- const totalEntries = countResult[0]?.count ?? 0;
1201
- // Get bounding box of all entries
1202
- const boundsResult = this.execute(`
1203
- SELECT
1204
- MIN(minX) as minX, MAX(maxX) as maxX,
1205
- MIN(minY) as minY, MAX(maxY) as maxY,
1206
- MIN(minZ) as minZ, MAX(maxZ) as maxZ
1207
- FROM dash_spatial_idx
1208
- `);
1209
- const bounds = boundsResult[0] || {};
1210
- return {
1211
- totalEntries,
1212
- indexExists: true,
1213
- tableName: 'dash_spatial_idx',
1214
- globalBounds: totalEntries > 0 ? {
1215
- minX: bounds.minX,
1216
- maxX: bounds.maxX,
1217
- minY: bounds.minY,
1218
- maxY: bounds.maxY,
1219
- minZ: bounds.minZ,
1220
- maxZ: bounds.maxZ
1221
- } : undefined
1222
- };
1223
- }
1224
- catch (e) {
1225
- return {
1226
- totalEntries: 0,
1227
- indexExists: false,
1228
- error: e instanceof Error ? e.message : 'Spatial index not available'
1229
- };
1230
- }
1231
- }
1232
- /**
1233
- * Get all spatial bounds from the R-Tree index
1234
- */
1235
- getAllSpatialBounds(limit = 100, offset = 0) {
1236
- if (!this.db)
1237
- return [];
1238
- try {
1239
- const rows = this.execute(`
1240
- SELECT
1241
- idx.id as rowid,
1242
- map.item_id as id,
1243
- idx.minX, idx.maxX,
1244
- idx.minY, idx.maxY,
1245
- idx.minZ, idx.maxZ,
1246
- item.content
1247
- FROM dash_spatial_idx idx
1248
- JOIN dash_spatial_map map ON map.rowid = idx.id
1249
- LEFT JOIN dash_items item ON item.id = map.item_id
1250
- LIMIT ? OFFSET ?
1251
- `, [limit, offset]);
1252
- return rows.map((row) => ({
1253
- id: row.id,
1254
- rowid: row.rowid,
1255
- bounds: {
1256
- minX: row.minX,
1257
- maxX: row.maxX,
1258
- minY: row.minY,
1259
- maxY: row.maxY,
1260
- minZ: row.minZ,
1261
- maxZ: row.maxZ
1262
- },
1263
- content: row.content
1264
- }));
1265
- }
1266
- catch (e) {
1267
- console.warn('Failed to get spatial bounds:', e);
1268
- return [];
1269
- }
1270
- }
1271
- /**
1272
- * Get information about active table subscriptions
1273
- */
1274
- getActiveSubscriptions() {
1275
- const subscriptions = [];
1276
- this.tableListeners.forEach((listeners, tableName) => {
1277
- subscriptions.push({
1278
- table: tableName,
1279
- listenerCount: listeners.size
1280
- });
1281
- });
1282
- // Add global listeners
1283
- if (this.listeners.size > 0) {
1284
- subscriptions.push({
1285
- table: '*',
1286
- listenerCount: this.listeners.size
1287
- });
1288
- }
1289
- return subscriptions;
1290
- }
1291
- /**
1292
- * Get table listener counts as a map
1293
- */
1294
- getTableListenerCounts() {
1295
- const counts = {};
1296
- this.tableListeners.forEach((listeners, tableName) => {
1297
- counts[tableName] = listeners.size;
1298
- });
1299
- counts['*'] = this.listeners.size;
1300
- return counts;
1301
- }
1302
- /**
1303
- * Check if the database is ready
1304
- */
1305
- isReady() {
1306
- return this.db !== null;
1307
- }
1308
- /**
1309
- * Get database metadata
1310
- */
1311
- getDatabaseInfo() {
1312
- if (!this.db) {
1313
- return {
1314
- ready: false,
1315
- schemaVersion: this.currentSchemaVersion,
1316
- tableCount: 0
1317
- };
1318
- }
1319
- const tables = this.getAllTables();
1320
- return {
1321
- ready: true,
1322
- schemaVersion: this.currentSchemaVersion,
1323
- tableCount: tables.length,
1324
- tables: tables.map(t => t.name)
1325
- };
1326
- }
1327
- }
1328
- export const dash = new DashEngine();