@affectively/dash 5.1.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/api/firebase/database/index.d.ts +4 -0
- package/dist/src/api/firebase/database/index.js +17 -0
- package/dist/src/api/firebase/firestore/operations.d.ts +1 -1
- package/dist/src/api/firebase/firestore/operations.js +3 -3
- package/dist/src/api/firebase/index.d.ts +1 -1
- package/dist/src/api/firebase/index.js +2 -1
- package/dist/src/api/firebase/types.d.ts +1 -0
- package/dist/src/engine/sqlite.d.ts +129 -0
- package/dist/src/engine/sqlite.js +525 -0
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +9 -2
- package/dist/src/sync/d1-provider.d.ts +97 -0
- package/dist/src/sync/d1-provider.js +345 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,25 @@ export class DashEngine {
|
|
|
7
7
|
listeners = new Set();
|
|
8
8
|
lens = defaultLens;
|
|
9
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
|
+
]);
|
|
10
29
|
constructor() {
|
|
11
30
|
// SSR/Build safety: Only initialize in browser environments
|
|
12
31
|
if (typeof window !== 'undefined') {
|
|
@@ -31,12 +50,102 @@ export class DashEngine {
|
|
|
31
50
|
// Load Vector Extension (Simulation/Shim for now)
|
|
32
51
|
await import('./vec_extension.js').then(m => m.loadVectorExtension(this.db));
|
|
33
52
|
this.initializeSchema();
|
|
53
|
+
// Auto-enable cloud sync if endpoint is available (ON BY DEFAULT)
|
|
54
|
+
this.tryAutoEnableCloudSync();
|
|
34
55
|
}
|
|
35
56
|
catch (err) {
|
|
36
57
|
console.error('Dash: Failed to initialize SQLite WASM', err);
|
|
37
58
|
throw err;
|
|
38
59
|
}
|
|
39
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
|
|
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
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get default auth token from common auth patterns
|
|
131
|
+
*/
|
|
132
|
+
async getDefaultAuthToken() {
|
|
133
|
+
try {
|
|
134
|
+
// Try localStorage token
|
|
135
|
+
if (typeof localStorage !== 'undefined') {
|
|
136
|
+
const token = localStorage.getItem('auth_token') ||
|
|
137
|
+
localStorage.getItem('access_token') ||
|
|
138
|
+
localStorage.getItem('id_token');
|
|
139
|
+
if (token)
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
// Try cookie-based auth (will be sent automatically)
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
40
149
|
initializeSchema() {
|
|
41
150
|
if (!this.db)
|
|
42
151
|
return;
|
|
@@ -119,6 +228,7 @@ export class DashEngine {
|
|
|
119
228
|
// Matches: INSERT INTO table ...
|
|
120
229
|
// Matches: UPDATE table ...
|
|
121
230
|
// Matches: DELETE FROM table ...
|
|
231
|
+
// Matches: CREATE TABLE table ...
|
|
122
232
|
let table = '';
|
|
123
233
|
if (upper.startsWith('INSERT INTO')) {
|
|
124
234
|
table = sql.split(/\s+/)[2];
|
|
@@ -129,6 +239,22 @@ export class DashEngine {
|
|
|
129
239
|
else if (upper.startsWith('DELETE FROM')) {
|
|
130
240
|
table = sql.split(/\s+/)[2];
|
|
131
241
|
}
|
|
242
|
+
else if (upper.startsWith('CREATE TABLE')) {
|
|
243
|
+
// Extract table name from CREATE TABLE [IF NOT EXISTS] tablename
|
|
244
|
+
const match = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
|
|
245
|
+
if (match) {
|
|
246
|
+
table = match[1];
|
|
247
|
+
// Auto-setup cloud sync triggers for new tables
|
|
248
|
+
if (this.cloudSyncEnabled && this.shouldSyncTable(table) && !this.syncedTables.has(table)) {
|
|
249
|
+
// Defer trigger setup to after the table is created
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
this.setupTableSyncTriggers(table);
|
|
252
|
+
this.syncedTables.add(table);
|
|
253
|
+
console.log('[Dash] Auto-enabled cloud sync for new table:', table);
|
|
254
|
+
}, 0);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
132
258
|
if (table) {
|
|
133
259
|
// cleanup quotes etc
|
|
134
260
|
table = table.replace(/["';]/g, '');
|
|
@@ -250,9 +376,408 @@ export class DashEngine {
|
|
|
250
376
|
});
|
|
251
377
|
}
|
|
252
378
|
close() {
|
|
379
|
+
this.stopCloudSync();
|
|
253
380
|
this.db?.close();
|
|
254
381
|
}
|
|
255
382
|
// ============================================
|
|
383
|
+
// CLOUD SYNC (D1/R2) - AUTOMATIC SYNC
|
|
384
|
+
// ============================================
|
|
385
|
+
/**
|
|
386
|
+
* Enable cloud sync - changes automatically sync to D1/R2
|
|
387
|
+
* Just call this once with your config, and sync happens magically.
|
|
388
|
+
*
|
|
389
|
+
* Cloud sync is ON BY DEFAULT when running in a Cloudflare environment.
|
|
390
|
+
* Call this to customize the config or explicitly enable/disable.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* await dash.ready();
|
|
395
|
+
* // Option 1: Use auto-detected endpoint (default)
|
|
396
|
+
* // Cloud sync is already enabled if DASH_SYNC_URL env var is set
|
|
397
|
+
*
|
|
398
|
+
* // Option 2: Explicit config
|
|
399
|
+
* dash.enableCloudSync({
|
|
400
|
+
* baseUrl: 'https://api.example.com',
|
|
401
|
+
* getAuthToken: async () => auth.token,
|
|
402
|
+
* });
|
|
403
|
+
*
|
|
404
|
+
* // Option 3: Disable cloud sync
|
|
405
|
+
* dash.enableCloudSync({ disabled: true });
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
enableCloudSync(config = {}) {
|
|
409
|
+
// Handle explicit disable
|
|
410
|
+
if (config.disabled) {
|
|
411
|
+
this.disableCloudSync();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!this.db) {
|
|
415
|
+
console.warn('[Dash] Database not ready. Call enableCloudSync after ready()');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Try to get baseUrl from config or auto-detect
|
|
419
|
+
const baseUrl = config.baseUrl || this.detectSyncEndpoint();
|
|
420
|
+
if (!baseUrl) {
|
|
421
|
+
console.warn('[Dash] No sync endpoint available. Cloud sync disabled.');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.cloudConfig = {
|
|
425
|
+
syncInterval: 30000,
|
|
426
|
+
...config,
|
|
427
|
+
baseUrl,
|
|
428
|
+
getAuthToken: config.getAuthToken || (() => this.getDefaultAuthToken()),
|
|
429
|
+
};
|
|
430
|
+
// Create sync infrastructure tables
|
|
431
|
+
this.initializeCloudSyncSchema();
|
|
432
|
+
// Load last sync time
|
|
433
|
+
const meta = this.execute("SELECT value FROM dash_sync_meta WHERE key = 'lastCloudSyncTime'");
|
|
434
|
+
if (meta.length > 0) {
|
|
435
|
+
this.lastCloudSyncTime = parseInt(meta[0].value, 10) || 0;
|
|
436
|
+
}
|
|
437
|
+
// Set up triggers for all existing user tables
|
|
438
|
+
this.setupAllTableTriggers();
|
|
439
|
+
this.cloudSyncEnabled = true;
|
|
440
|
+
// Start auto-sync
|
|
441
|
+
if (this.cloudConfig.syncInterval && this.cloudConfig.syncInterval > 0) {
|
|
442
|
+
this.startCloudSync();
|
|
443
|
+
}
|
|
444
|
+
console.log('[Dash] Cloud sync enabled');
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Initialize cloud sync schema (queue and metadata tables)
|
|
448
|
+
*/
|
|
449
|
+
initializeCloudSyncSchema() {
|
|
450
|
+
this.db.exec(`
|
|
451
|
+
CREATE TABLE IF NOT EXISTS dash_sync_meta (
|
|
452
|
+
key TEXT PRIMARY KEY,
|
|
453
|
+
value TEXT
|
|
454
|
+
)
|
|
455
|
+
`);
|
|
456
|
+
this.db.exec(`
|
|
457
|
+
CREATE TABLE IF NOT EXISTS dash_sync_queue (
|
|
458
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
459
|
+
table_name TEXT NOT NULL,
|
|
460
|
+
row_id TEXT NOT NULL,
|
|
461
|
+
operation TEXT NOT NULL CHECK(operation IN ('create', 'update', 'delete')),
|
|
462
|
+
data TEXT,
|
|
463
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000),
|
|
464
|
+
synced INTEGER DEFAULT 0
|
|
465
|
+
)
|
|
466
|
+
`);
|
|
467
|
+
this.db.exec(`
|
|
468
|
+
CREATE INDEX IF NOT EXISTS idx_dash_sync_queue_pending
|
|
469
|
+
ON dash_sync_queue(synced, created_at)
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Set up sync triggers for all user tables
|
|
474
|
+
*/
|
|
475
|
+
setupAllTableTriggers() {
|
|
476
|
+
const tables = this.execute(`
|
|
477
|
+
SELECT name FROM sqlite_master
|
|
478
|
+
WHERE type = 'table'
|
|
479
|
+
AND name NOT LIKE 'sqlite_%'
|
|
480
|
+
`);
|
|
481
|
+
for (const t of tables) {
|
|
482
|
+
const tableName = t.name;
|
|
483
|
+
if (!this.shouldSyncTable(tableName))
|
|
484
|
+
continue;
|
|
485
|
+
this.setupTableSyncTriggers(tableName);
|
|
486
|
+
this.syncedTables.add(tableName);
|
|
487
|
+
}
|
|
488
|
+
console.log('[Dash] Sync triggers set up for:', Array.from(this.syncedTables));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Check if a table should be synced
|
|
492
|
+
*/
|
|
493
|
+
shouldSyncTable(tableName) {
|
|
494
|
+
// Skip internal tables
|
|
495
|
+
if (this.INTERNAL_TABLES.has(tableName))
|
|
496
|
+
return false;
|
|
497
|
+
// Skip excluded tables
|
|
498
|
+
if (this.cloudConfig?.excludeTables?.includes(tableName))
|
|
499
|
+
return false;
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Set up sync triggers for a specific table
|
|
504
|
+
*/
|
|
505
|
+
setupTableSyncTriggers(tableName) {
|
|
506
|
+
// Check if table has an 'id' column (required for sync)
|
|
507
|
+
const columns = this.execute(`PRAGMA table_info('${tableName.replace(/'/g, "''")}')`);
|
|
508
|
+
const hasId = columns.some((c) => c.name === 'id');
|
|
509
|
+
if (!hasId) {
|
|
510
|
+
console.warn(`[Dash] Table ${tableName} has no 'id' column, skipping sync triggers`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// Drop existing triggers
|
|
514
|
+
try {
|
|
515
|
+
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_insert`);
|
|
516
|
+
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_update`);
|
|
517
|
+
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_delete`);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// Ignore
|
|
521
|
+
}
|
|
522
|
+
// INSERT trigger - capture full row as JSON
|
|
523
|
+
this.db.exec(`
|
|
524
|
+
CREATE TRIGGER dash_cloud_${tableName}_insert
|
|
525
|
+
AFTER INSERT ON "${tableName}"
|
|
526
|
+
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
527
|
+
BEGIN
|
|
528
|
+
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
529
|
+
SELECT '${tableName}', NEW.id, 'create', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
|
|
530
|
+
END
|
|
531
|
+
`);
|
|
532
|
+
// UPDATE trigger
|
|
533
|
+
this.db.exec(`
|
|
534
|
+
CREATE TRIGGER dash_cloud_${tableName}_update
|
|
535
|
+
AFTER UPDATE ON "${tableName}"
|
|
536
|
+
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
537
|
+
BEGIN
|
|
538
|
+
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
539
|
+
SELECT '${tableName}', NEW.id, 'update', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
|
|
540
|
+
END
|
|
541
|
+
`);
|
|
542
|
+
// DELETE trigger
|
|
543
|
+
this.db.exec(`
|
|
544
|
+
CREATE TRIGGER dash_cloud_${tableName}_delete
|
|
545
|
+
AFTER DELETE ON "${tableName}"
|
|
546
|
+
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
547
|
+
BEGIN
|
|
548
|
+
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
549
|
+
VALUES ('${tableName}', OLD.id, 'delete', NULL);
|
|
550
|
+
END
|
|
551
|
+
`);
|
|
552
|
+
// Enable triggers
|
|
553
|
+
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('cloud_enabled', '1')");
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Build json_object() arguments for a table's columns
|
|
557
|
+
*/
|
|
558
|
+
buildJsonObjectArgs(tableName, columns, prefix) {
|
|
559
|
+
return columns
|
|
560
|
+
.map((c) => `'${c.name}', ${prefix}."${c.name}"`)
|
|
561
|
+
.join(', ');
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Start automatic cloud sync
|
|
565
|
+
*/
|
|
566
|
+
startCloudSync() {
|
|
567
|
+
if (this.cloudSyncTimer)
|
|
568
|
+
return;
|
|
569
|
+
this.cloudSyncTimer = setInterval(() => {
|
|
570
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
571
|
+
return; // Skip sync when offline
|
|
572
|
+
}
|
|
573
|
+
this.syncToCloud().catch(console.error);
|
|
574
|
+
}, this.cloudConfig.syncInterval);
|
|
575
|
+
// Also do an immediate sync
|
|
576
|
+
this.syncToCloud().catch(console.error);
|
|
577
|
+
console.log('[Dash] Cloud auto-sync started, interval:', this.cloudConfig.syncInterval, 'ms');
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Stop automatic cloud sync
|
|
581
|
+
*/
|
|
582
|
+
stopCloudSync() {
|
|
583
|
+
if (this.cloudSyncTimer) {
|
|
584
|
+
clearInterval(this.cloudSyncTimer);
|
|
585
|
+
this.cloudSyncTimer = null;
|
|
586
|
+
}
|
|
587
|
+
this.cloudSyncEnabled = false;
|
|
588
|
+
console.log('[Dash] Cloud sync stopped');
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Perform a sync to cloud (D1/R2)
|
|
592
|
+
*/
|
|
593
|
+
async syncToCloud() {
|
|
594
|
+
if (!this.cloudConfig || !this.cloudSyncEnabled) {
|
|
595
|
+
return { pushed: 0, pulled: 0, errors: ['Cloud sync not enabled'], timestamp: Date.now() };
|
|
596
|
+
}
|
|
597
|
+
if (this.isCloudSyncing) {
|
|
598
|
+
return { pushed: 0, pulled: 0, errors: ['Sync already in progress'], timestamp: Date.now() };
|
|
599
|
+
}
|
|
600
|
+
this.isCloudSyncing = true;
|
|
601
|
+
const result = { pushed: 0, pulled: 0, errors: [], timestamp: Date.now() };
|
|
602
|
+
try {
|
|
603
|
+
const token = this.cloudConfig.getAuthToken
|
|
604
|
+
? await this.cloudConfig.getAuthToken()
|
|
605
|
+
: await this.getDefaultAuthToken();
|
|
606
|
+
// Token is optional - sync can work without auth for public endpoints
|
|
607
|
+
// Get pending changes
|
|
608
|
+
const pending = this.execute(`
|
|
609
|
+
SELECT * FROM dash_sync_queue
|
|
610
|
+
WHERE synced = 0
|
|
611
|
+
ORDER BY created_at ASC
|
|
612
|
+
LIMIT 100
|
|
613
|
+
`);
|
|
614
|
+
// Group by table
|
|
615
|
+
const changesByTable = {};
|
|
616
|
+
for (const entry of pending) {
|
|
617
|
+
const tableName = entry.table_name;
|
|
618
|
+
if (!changesByTable[tableName]) {
|
|
619
|
+
changesByTable[tableName] = { creates: [], updates: [], deletes: [] };
|
|
620
|
+
}
|
|
621
|
+
const data = entry.data ? JSON.parse(entry.data) : null;
|
|
622
|
+
switch (entry.operation) {
|
|
623
|
+
case 'create':
|
|
624
|
+
if (data)
|
|
625
|
+
changesByTable[tableName].creates.push(data);
|
|
626
|
+
break;
|
|
627
|
+
case 'update':
|
|
628
|
+
if (data)
|
|
629
|
+
changesByTable[tableName].updates.push(data);
|
|
630
|
+
break;
|
|
631
|
+
case 'delete':
|
|
632
|
+
changesByTable[tableName].deletes.push(entry.row_id);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Sync each table
|
|
637
|
+
for (const [tableName, changes] of Object.entries(changesByTable)) {
|
|
638
|
+
if (changes.creates.length === 0 && changes.updates.length === 0 && changes.deletes.length === 0) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const headers = {
|
|
643
|
+
'Content-Type': 'application/json',
|
|
644
|
+
};
|
|
645
|
+
if (token) {
|
|
646
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
647
|
+
}
|
|
648
|
+
const response = await fetch(`${this.cloudConfig.baseUrl}/api/sync`, {
|
|
649
|
+
method: 'POST',
|
|
650
|
+
headers,
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
table: tableName,
|
|
653
|
+
creates: changes.creates,
|
|
654
|
+
updates: changes.updates,
|
|
655
|
+
deletes: changes.deletes,
|
|
656
|
+
lastSyncTime: this.lastCloudSyncTime,
|
|
657
|
+
}),
|
|
658
|
+
});
|
|
659
|
+
if (!response.ok) {
|
|
660
|
+
const errorText = await response.text();
|
|
661
|
+
result.errors.push(`${tableName}: ${response.status} ${errorText}`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const syncResponse = await response.json();
|
|
665
|
+
result.pushed += changes.creates.length + changes.updates.length + changes.deletes.length;
|
|
666
|
+
result.pulled += syncResponse.serverChanges?.length || 0;
|
|
667
|
+
// Apply server changes locally (with triggers disabled)
|
|
668
|
+
if (syncResponse.serverChanges && syncResponse.serverChanges.length > 0) {
|
|
669
|
+
this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
|
|
670
|
+
try {
|
|
671
|
+
for (const change of syncResponse.serverChanges) {
|
|
672
|
+
this.applyServerChange(tableName, change);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
this.execute("UPDATE dash_sync_meta SET value = '1' WHERE key = 'cloud_enabled'");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Update sync time
|
|
680
|
+
if (syncResponse.syncTime > this.lastCloudSyncTime) {
|
|
681
|
+
this.lastCloudSyncTime = syncResponse.syncTime;
|
|
682
|
+
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', ?)", [String(this.lastCloudSyncTime)]);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
result.errors.push(`${tableName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Mark as synced
|
|
690
|
+
if (pending.length > 0) {
|
|
691
|
+
const ids = pending.map((e) => e.id).join(',');
|
|
692
|
+
this.execute(`UPDATE dash_sync_queue SET synced = 1 WHERE id IN (${ids})`);
|
|
693
|
+
}
|
|
694
|
+
// Clean up old entries
|
|
695
|
+
this.execute(`
|
|
696
|
+
DELETE FROM dash_sync_queue
|
|
697
|
+
WHERE synced = 1
|
|
698
|
+
AND id NOT IN (
|
|
699
|
+
SELECT id FROM dash_sync_queue
|
|
700
|
+
WHERE synced = 1
|
|
701
|
+
ORDER BY id DESC
|
|
702
|
+
LIMIT 1000
|
|
703
|
+
)
|
|
704
|
+
`);
|
|
705
|
+
this.cloudConfig.onSyncComplete?.(result);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
709
|
+
result.errors.push(error.message);
|
|
710
|
+
this.cloudConfig.onSyncError?.(error);
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
this.isCloudSyncing = false;
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Apply a single server change locally
|
|
719
|
+
*/
|
|
720
|
+
applyServerChange(tableName, change) {
|
|
721
|
+
try {
|
|
722
|
+
if (change.deleted || change._deleted) {
|
|
723
|
+
this.execute(`DELETE FROM "${tableName}" WHERE id = ?`, [change.id]);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const columns = Object.keys(change).filter(k => !k.startsWith('_'));
|
|
727
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
728
|
+
const values = columns.map(k => {
|
|
729
|
+
const v = change[k];
|
|
730
|
+
if (v !== null && typeof v === 'object') {
|
|
731
|
+
return JSON.stringify(v);
|
|
732
|
+
}
|
|
733
|
+
return v;
|
|
734
|
+
});
|
|
735
|
+
const sql = `INSERT OR REPLACE INTO "${tableName}" (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
736
|
+
this.execute(sql, values);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
console.error(`[Dash] Failed to apply server change to ${tableName}:`, err);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Force a full cloud sync (reset last sync time)
|
|
744
|
+
*/
|
|
745
|
+
async forceCloudSync() {
|
|
746
|
+
this.lastCloudSyncTime = 0;
|
|
747
|
+
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', '0')");
|
|
748
|
+
return this.syncToCloud();
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Get cloud sync status
|
|
752
|
+
*/
|
|
753
|
+
getCloudSyncStatus() {
|
|
754
|
+
let pendingChanges = 0;
|
|
755
|
+
if (this.cloudSyncEnabled && this.db) {
|
|
756
|
+
try {
|
|
757
|
+
const result = this.execute('SELECT COUNT(*) as count FROM dash_sync_queue WHERE synced = 0');
|
|
758
|
+
pendingChanges = result[0]?.count || 0;
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// Table might not exist yet
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
enabled: this.cloudSyncEnabled,
|
|
766
|
+
syncing: this.isCloudSyncing,
|
|
767
|
+
lastSyncTime: this.lastCloudSyncTime,
|
|
768
|
+
pendingChanges,
|
|
769
|
+
syncedTables: Array.from(this.syncedTables),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Disable cloud sync
|
|
774
|
+
*/
|
|
775
|
+
disableCloudSync() {
|
|
776
|
+
this.stopCloudSync();
|
|
777
|
+
this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
|
|
778
|
+
console.log('[Dash] Cloud sync disabled');
|
|
779
|
+
}
|
|
780
|
+
// ============================================
|
|
256
781
|
// INTROSPECTION METHODS FOR DASH-STUDIO
|
|
257
782
|
// ============================================
|
|
258
783
|
/**
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { dash } from './engine/sqlite.js';
|
|
2
|
+
export type { CloudConfig, CloudSyncResult } from './engine/sqlite.js';
|
|
2
3
|
export { liveQuery, signal, effect, computed } from './reactivity/signal.js';
|
|
3
4
|
export { mcpServer } from './mcp/server.js';
|
|
4
5
|
export { YjsSqliteProvider } from './sync/provider.js';
|
|
5
6
|
export { backup, restore, generateKey, exportKey, importKey } from './sync/backup.js';
|
|
6
7
|
export type { CloudStorageAdapter } from './sync/backup.js';
|
|
7
8
|
export { HybridProvider } from './sync/hybrid-provider.js';
|
|
9
|
+
export { D1SyncProvider, getD1SyncProvider, resetD1SyncProvider } from './sync/d1-provider.js';
|
|
10
|
+
export type { D1SyncConfig, SyncResult, SyncQueueEntry } from './sync/d1-provider.js';
|
|
8
11
|
export * as firebase from './api/firebase/index.js';
|
|
9
12
|
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';
|
|
10
|
-
export { ref, child, set, update, remove, push, get, onDisconnect, onValue,
|
|
13
|
+
export { ref, child, set, update, remove, push, get, onDisconnect, onValue, off, } from './api/firebase/index.js';
|
|
11
14
|
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';
|
|
12
|
-
export { ref as storageRef, refFromURL, child as storageChild, uploadBytes, uploadBytesResumable, uploadString, getBytes, getDownloadURL, getMetadata, updateMetadata, deleteObject, list as listFiles, listAll, } from './api/firebase/index.js';
|
|
15
|
+
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';
|
package/dist/src/index.js
CHANGED
|
@@ -5,13 +5,20 @@ export { mcpServer } from './mcp/server.js';
|
|
|
5
5
|
export { YjsSqliteProvider } from './sync/provider.js';
|
|
6
6
|
export { backup, restore, generateKey, exportKey, importKey } from './sync/backup.js';
|
|
7
7
|
export { HybridProvider } from './sync/hybrid-provider.js';
|
|
8
|
+
// D1 HTTP Sync (legacy - prefer dash.enableCloudSync())
|
|
9
|
+
export { D1SyncProvider, getD1SyncProvider, resetD1SyncProvider } from './sync/d1-provider.js';
|
|
8
10
|
// Firebase Compatibility API exports
|
|
9
11
|
export * as firebase from './api/firebase/index.js';
|
|
10
12
|
// Firestore
|
|
11
13
|
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';
|
|
12
14
|
// Realtime Database
|
|
13
|
-
export { ref, child, set, update, remove, push, get, onDisconnect, onValue,
|
|
15
|
+
export { ref, child, set, update, remove, push, get, onDisconnect, onValue,
|
|
16
|
+
// onChildAdded,
|
|
17
|
+
// onChildChanged,
|
|
18
|
+
// onChildRemoved,
|
|
19
|
+
// onChildMoved,
|
|
20
|
+
off, } from './api/firebase/index.js';
|
|
14
21
|
// Authentication
|
|
15
22
|
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';
|
|
16
23
|
// Storage
|
|
17
|
-
export { ref as storageRef, refFromURL, child as storageChild, uploadBytes, uploadBytesResumable, uploadString, getBytes, getDownloadURL, getMetadata, updateMetadata, deleteObject, list as listFiles, listAll, } from './api/firebase/index.js';
|
|
24
|
+
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';
|