@affectively/dash 5.2.1 → 5.3.1

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