@affectively/dash 5.3.0 → 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.
- package/dist/index.d.ts +62 -0
- package/dist/index.js +31 -0
- package/dist/sync/index.d.ts +6 -0
- package/dist/sync/index.js +4 -0
- package/package.json +45 -66
- package/README.md +0 -193
- package/dist/src/api/firebase/auth/index.d.ts +0 -137
- package/dist/src/api/firebase/auth/index.js +0 -352
- package/dist/src/api/firebase/auth/providers.d.ts +0 -254
- package/dist/src/api/firebase/auth/providers.js +0 -518
- package/dist/src/api/firebase/database/index.d.ts +0 -108
- package/dist/src/api/firebase/database/index.js +0 -368
- package/dist/src/api/firebase/errors.d.ts +0 -15
- package/dist/src/api/firebase/errors.js +0 -215
- package/dist/src/api/firebase/firestore/data-types.d.ts +0 -116
- package/dist/src/api/firebase/firestore/data-types.js +0 -280
- package/dist/src/api/firebase/firestore/index.d.ts +0 -7
- package/dist/src/api/firebase/firestore/index.js +0 -13
- package/dist/src/api/firebase/firestore/listeners.d.ts +0 -20
- package/dist/src/api/firebase/firestore/listeners.js +0 -50
- package/dist/src/api/firebase/firestore/operations.d.ts +0 -123
- package/dist/src/api/firebase/firestore/operations.js +0 -490
- package/dist/src/api/firebase/firestore/query.d.ts +0 -118
- package/dist/src/api/firebase/firestore/query.js +0 -418
- package/dist/src/api/firebase/index.d.ts +0 -11
- package/dist/src/api/firebase/index.js +0 -17
- package/dist/src/api/firebase/storage/index.d.ts +0 -100
- package/dist/src/api/firebase/storage/index.js +0 -286
- package/dist/src/api/firebase/types.d.ts +0 -341
- package/dist/src/api/firebase/types.js +0 -4
- package/dist/src/auth/manager.d.ts +0 -182
- package/dist/src/auth/manager.js +0 -598
- package/dist/src/engine/ai.d.ts +0 -10
- package/dist/src/engine/ai.js +0 -76
- package/dist/src/engine/sqlite.d.ts +0 -346
- package/dist/src/engine/sqlite.js +0 -1325
- package/dist/src/engine/vec_extension.d.ts +0 -5
- package/dist/src/engine/vec_extension.js +0 -10
- package/dist/src/index.d.ts +0 -15
- package/dist/src/index.js +0 -24
- package/dist/src/mcp/server.d.ts +0 -8
- package/dist/src/mcp/server.js +0 -87
- package/dist/src/reactivity/signal.d.ts +0 -3
- package/dist/src/reactivity/signal.js +0 -31
- package/dist/src/schema/lens.d.ts +0 -29
- package/dist/src/schema/lens.js +0 -122
- package/dist/src/sync/aeon/config.d.ts +0 -21
- package/dist/src/sync/aeon/config.js +0 -14
- package/dist/src/sync/aeon/delta-adapter.d.ts +0 -62
- package/dist/src/sync/aeon/delta-adapter.js +0 -98
- package/dist/src/sync/aeon/index.d.ts +0 -18
- package/dist/src/sync/aeon/index.js +0 -19
- package/dist/src/sync/aeon/offline-adapter.d.ts +0 -110
- package/dist/src/sync/aeon/offline-adapter.js +0 -227
- package/dist/src/sync/aeon/presence-adapter.d.ts +0 -114
- package/dist/src/sync/aeon/presence-adapter.js +0 -157
- package/dist/src/sync/aeon/schema-adapter.d.ts +0 -95
- package/dist/src/sync/aeon/schema-adapter.js +0 -163
- package/dist/src/sync/backup.d.ts +0 -12
- package/dist/src/sync/backup.js +0 -44
- package/dist/src/sync/connection.d.ts +0 -20
- package/dist/src/sync/connection.js +0 -50
- package/dist/src/sync/d1-provider.d.ts +0 -97
- package/dist/src/sync/d1-provider.js +0 -345
- package/dist/src/sync/hybrid-provider.d.ts +0 -172
- package/dist/src/sync/hybrid-provider.js +0 -477
- package/dist/src/sync/provider.d.ts +0 -11
- package/dist/src/sync/provider.js +0 -67
- package/dist/src/sync/verify.d.ts +0 -1
- package/dist/src/sync/verify.js +0 -23
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,1325 +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
|
-
});
|
|
593
|
-
// Listen for remote changes and apply to local SQLite
|
|
594
|
-
this.relayDoc.on('update', (update, origin) => {
|
|
595
|
-
if (origin === 'local')
|
|
596
|
-
return; // Skip our own updates
|
|
597
|
-
this.applyRelayUpdate(update);
|
|
598
|
-
});
|
|
599
|
-
// Enter high-frequency mode if requested
|
|
600
|
-
if (config.relayHighFrequency) {
|
|
601
|
-
await this.relayProvider.enterHighFrequencyMode();
|
|
602
|
-
}
|
|
603
|
-
this.relaySyncEnabled = true;
|
|
604
|
-
console.log('[Dash] Relay sync enabled:', relayUrl, '(room:', roomName, ')');
|
|
605
|
-
}
|
|
606
|
-
catch (err) {
|
|
607
|
-
throw err;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Apply an update from Relay to local SQLite
|
|
612
|
-
*/
|
|
613
|
-
applyRelayUpdate(update) {
|
|
614
|
-
if (!this.relayDoc)
|
|
615
|
-
return;
|
|
616
|
-
try {
|
|
617
|
-
// Get the shared map that contains table updates
|
|
618
|
-
const tablesMap = this.relayDoc.getMap('tables');
|
|
619
|
-
// Process each table's updates
|
|
620
|
-
tablesMap.forEach((tableData, tableName) => {
|
|
621
|
-
if (!this.shouldSyncTable(tableName))
|
|
622
|
-
return;
|
|
623
|
-
// Apply each row update
|
|
624
|
-
const rowsMap = tableData;
|
|
625
|
-
rowsMap.forEach((rowData, rowId) => {
|
|
626
|
-
this.applyServerChange(tableName, { id: rowId, ...rowData });
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
catch (err) {
|
|
631
|
-
console.warn('[Dash] Failed to apply relay update:', err);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Broadcast a local change to Relay
|
|
636
|
-
*/
|
|
637
|
-
broadcastToRelay(tableName, rowId, operation, data) {
|
|
638
|
-
if (!this.relayDoc || !this.relaySyncEnabled)
|
|
639
|
-
return;
|
|
640
|
-
try {
|
|
641
|
-
const tablesMap = this.relayDoc.getMap('tables');
|
|
642
|
-
// Get or create table map
|
|
643
|
-
let tableMap = tablesMap.get(tableName);
|
|
644
|
-
if (!tableMap) {
|
|
645
|
-
tableMap = new Y.Map();
|
|
646
|
-
tablesMap.set(tableName, tableMap);
|
|
647
|
-
}
|
|
648
|
-
if (operation === 'delete') {
|
|
649
|
-
tableMap.delete(rowId);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
tableMap.set(rowId, { ...data, _syncedAt: Date.now() });
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
catch (err) {
|
|
656
|
-
console.warn('[Dash] Failed to broadcast to relay:', err);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Initialize cloud sync schema (queue and metadata tables)
|
|
661
|
-
*/
|
|
662
|
-
initializeCloudSyncSchema() {
|
|
663
|
-
this.db.exec(`
|
|
664
|
-
CREATE TABLE IF NOT EXISTS dash_sync_meta (
|
|
665
|
-
key TEXT PRIMARY KEY,
|
|
666
|
-
value TEXT
|
|
667
|
-
)
|
|
668
|
-
`);
|
|
669
|
-
this.db.exec(`
|
|
670
|
-
CREATE TABLE IF NOT EXISTS dash_sync_queue (
|
|
671
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
672
|
-
table_name TEXT NOT NULL,
|
|
673
|
-
row_id TEXT NOT NULL,
|
|
674
|
-
operation TEXT NOT NULL CHECK(operation IN ('create', 'update', 'delete')),
|
|
675
|
-
data TEXT,
|
|
676
|
-
created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000),
|
|
677
|
-
synced INTEGER DEFAULT 0
|
|
678
|
-
)
|
|
679
|
-
`);
|
|
680
|
-
this.db.exec(`
|
|
681
|
-
CREATE INDEX IF NOT EXISTS idx_dash_sync_queue_pending
|
|
682
|
-
ON dash_sync_queue(synced, created_at)
|
|
683
|
-
`);
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Set up sync triggers for all user tables
|
|
687
|
-
*/
|
|
688
|
-
setupAllTableTriggers() {
|
|
689
|
-
const tables = this.execute(`
|
|
690
|
-
SELECT name FROM sqlite_master
|
|
691
|
-
WHERE type = 'table'
|
|
692
|
-
AND name NOT LIKE 'sqlite_%'
|
|
693
|
-
`);
|
|
694
|
-
for (const t of tables) {
|
|
695
|
-
const tableName = t.name;
|
|
696
|
-
if (!this.shouldSyncTable(tableName))
|
|
697
|
-
continue;
|
|
698
|
-
this.setupTableSyncTriggers(tableName);
|
|
699
|
-
this.syncedTables.add(tableName);
|
|
700
|
-
}
|
|
701
|
-
console.log('[Dash] Sync triggers set up for:', Array.from(this.syncedTables));
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Check if a table should be synced
|
|
705
|
-
*/
|
|
706
|
-
shouldSyncTable(tableName) {
|
|
707
|
-
// Skip internal tables
|
|
708
|
-
if (this.INTERNAL_TABLES.has(tableName))
|
|
709
|
-
return false;
|
|
710
|
-
// Skip excluded tables
|
|
711
|
-
if (this.cloudConfig?.excludeTables?.includes(tableName))
|
|
712
|
-
return false;
|
|
713
|
-
return true;
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Set up sync triggers for a specific table
|
|
717
|
-
*/
|
|
718
|
-
setupTableSyncTriggers(tableName) {
|
|
719
|
-
// Check if table has an 'id' column (required for sync)
|
|
720
|
-
const columns = this.execute(`PRAGMA table_info('${tableName.replace(/'/g, "''")}')`);
|
|
721
|
-
const hasId = columns.some((c) => c.name === 'id');
|
|
722
|
-
if (!hasId) {
|
|
723
|
-
console.warn(`[Dash] Table ${tableName} has no 'id' column, skipping sync triggers`);
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
// Drop existing triggers
|
|
727
|
-
try {
|
|
728
|
-
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_insert`);
|
|
729
|
-
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_update`);
|
|
730
|
-
this.db.exec(`DROP TRIGGER IF EXISTS dash_cloud_${tableName}_delete`);
|
|
731
|
-
}
|
|
732
|
-
catch {
|
|
733
|
-
// Ignore
|
|
734
|
-
}
|
|
735
|
-
// INSERT trigger - capture full row as JSON
|
|
736
|
-
this.db.exec(`
|
|
737
|
-
CREATE TRIGGER dash_cloud_${tableName}_insert
|
|
738
|
-
AFTER INSERT ON "${tableName}"
|
|
739
|
-
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
740
|
-
BEGIN
|
|
741
|
-
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
742
|
-
SELECT '${tableName}', NEW.id, 'create', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
|
|
743
|
-
END
|
|
744
|
-
`);
|
|
745
|
-
// UPDATE trigger
|
|
746
|
-
this.db.exec(`
|
|
747
|
-
CREATE TRIGGER dash_cloud_${tableName}_update
|
|
748
|
-
AFTER UPDATE ON "${tableName}"
|
|
749
|
-
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
750
|
-
BEGIN
|
|
751
|
-
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
752
|
-
SELECT '${tableName}', NEW.id, 'update', json_object(${this.buildJsonObjectArgs(tableName, columns, 'NEW')});
|
|
753
|
-
END
|
|
754
|
-
`);
|
|
755
|
-
// DELETE trigger
|
|
756
|
-
this.db.exec(`
|
|
757
|
-
CREATE TRIGGER dash_cloud_${tableName}_delete
|
|
758
|
-
AFTER DELETE ON "${tableName}"
|
|
759
|
-
WHEN (SELECT value FROM dash_sync_meta WHERE key = 'cloud_enabled') = '1'
|
|
760
|
-
BEGIN
|
|
761
|
-
INSERT INTO dash_sync_queue (table_name, row_id, operation, data)
|
|
762
|
-
VALUES ('${tableName}', OLD.id, 'delete', NULL);
|
|
763
|
-
END
|
|
764
|
-
`);
|
|
765
|
-
// Enable triggers
|
|
766
|
-
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('cloud_enabled', '1')");
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Build json_object() arguments for a table's columns
|
|
770
|
-
*/
|
|
771
|
-
buildJsonObjectArgs(tableName, columns, prefix) {
|
|
772
|
-
return columns
|
|
773
|
-
.map((c) => `'${c.name}', ${prefix}."${c.name}"`)
|
|
774
|
-
.join(', ');
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Start automatic cloud sync
|
|
778
|
-
*/
|
|
779
|
-
startCloudSync() {
|
|
780
|
-
if (this.cloudSyncTimer)
|
|
781
|
-
return;
|
|
782
|
-
this.cloudSyncTimer = setInterval(() => {
|
|
783
|
-
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
784
|
-
return; // Skip sync when offline
|
|
785
|
-
}
|
|
786
|
-
this.syncToCloud().catch(console.error);
|
|
787
|
-
}, this.cloudConfig.syncInterval);
|
|
788
|
-
// Also do an immediate sync
|
|
789
|
-
this.syncToCloud().catch(console.error);
|
|
790
|
-
console.log('[Dash] Cloud auto-sync started, interval:', this.cloudConfig.syncInterval, 'ms');
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Stop automatic cloud sync
|
|
794
|
-
*/
|
|
795
|
-
stopCloudSync() {
|
|
796
|
-
if (this.cloudSyncTimer) {
|
|
797
|
-
clearInterval(this.cloudSyncTimer);
|
|
798
|
-
this.cloudSyncTimer = null;
|
|
799
|
-
}
|
|
800
|
-
this.cloudSyncEnabled = false;
|
|
801
|
-
console.log('[Dash] Cloud sync stopped');
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Perform a sync to cloud (D1/R2)
|
|
805
|
-
*/
|
|
806
|
-
async syncToCloud() {
|
|
807
|
-
if (!this.cloudConfig || !this.cloudSyncEnabled) {
|
|
808
|
-
return { pushed: 0, pulled: 0, errors: ['Cloud sync not enabled'], timestamp: Date.now() };
|
|
809
|
-
}
|
|
810
|
-
if (this.isCloudSyncing) {
|
|
811
|
-
return { pushed: 0, pulled: 0, errors: ['Sync already in progress'], timestamp: Date.now() };
|
|
812
|
-
}
|
|
813
|
-
this.isCloudSyncing = true;
|
|
814
|
-
const result = { pushed: 0, pulled: 0, errors: [], timestamp: Date.now() };
|
|
815
|
-
try {
|
|
816
|
-
const token = this.cloudConfig.getAuthToken
|
|
817
|
-
? await this.cloudConfig.getAuthToken()
|
|
818
|
-
: await this.getDefaultAuthToken();
|
|
819
|
-
// Token is optional - sync can work without auth for public endpoints
|
|
820
|
-
// Get pending changes
|
|
821
|
-
const pending = this.execute(`
|
|
822
|
-
SELECT * FROM dash_sync_queue
|
|
823
|
-
WHERE synced = 0
|
|
824
|
-
ORDER BY created_at ASC
|
|
825
|
-
LIMIT 100
|
|
826
|
-
`);
|
|
827
|
-
// Group by table
|
|
828
|
-
const changesByTable = {};
|
|
829
|
-
for (const entry of pending) {
|
|
830
|
-
const tableName = entry.table_name;
|
|
831
|
-
if (!changesByTable[tableName]) {
|
|
832
|
-
changesByTable[tableName] = { creates: [], updates: [], deletes: [] };
|
|
833
|
-
}
|
|
834
|
-
const data = entry.data ? JSON.parse(entry.data) : null;
|
|
835
|
-
switch (entry.operation) {
|
|
836
|
-
case 'create':
|
|
837
|
-
if (data)
|
|
838
|
-
changesByTable[tableName].creates.push(data);
|
|
839
|
-
break;
|
|
840
|
-
case 'update':
|
|
841
|
-
if (data)
|
|
842
|
-
changesByTable[tableName].updates.push(data);
|
|
843
|
-
break;
|
|
844
|
-
case 'delete':
|
|
845
|
-
changesByTable[tableName].deletes.push(entry.row_id);
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
// Sync each table
|
|
850
|
-
for (const [tableName, changes] of Object.entries(changesByTable)) {
|
|
851
|
-
if (changes.creates.length === 0 && changes.updates.length === 0 && changes.deletes.length === 0) {
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
const headers = {
|
|
856
|
-
'Content-Type': 'application/json',
|
|
857
|
-
};
|
|
858
|
-
if (token) {
|
|
859
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
860
|
-
}
|
|
861
|
-
const response = await fetch(`${this.cloudConfig.baseUrl}/api/sync`, {
|
|
862
|
-
method: 'POST',
|
|
863
|
-
headers,
|
|
864
|
-
body: JSON.stringify({
|
|
865
|
-
table: tableName,
|
|
866
|
-
creates: changes.creates,
|
|
867
|
-
updates: changes.updates,
|
|
868
|
-
deletes: changes.deletes,
|
|
869
|
-
lastSyncTime: this.lastCloudSyncTime,
|
|
870
|
-
}),
|
|
871
|
-
});
|
|
872
|
-
if (!response.ok) {
|
|
873
|
-
const errorText = await response.text();
|
|
874
|
-
result.errors.push(`${tableName}: ${response.status} ${errorText}`);
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
const syncResponse = await response.json();
|
|
878
|
-
result.pushed += changes.creates.length + changes.updates.length + changes.deletes.length;
|
|
879
|
-
result.pulled += syncResponse.serverChanges?.length || 0;
|
|
880
|
-
// Apply server changes locally (with triggers disabled)
|
|
881
|
-
if (syncResponse.serverChanges && syncResponse.serverChanges.length > 0) {
|
|
882
|
-
this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
|
|
883
|
-
try {
|
|
884
|
-
for (const change of syncResponse.serverChanges) {
|
|
885
|
-
this.applyServerChange(tableName, change);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
finally {
|
|
889
|
-
this.execute("UPDATE dash_sync_meta SET value = '1' WHERE key = 'cloud_enabled'");
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
// Update sync time
|
|
893
|
-
if (syncResponse.syncTime > this.lastCloudSyncTime) {
|
|
894
|
-
this.lastCloudSyncTime = syncResponse.syncTime;
|
|
895
|
-
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', ?)", [String(this.lastCloudSyncTime)]);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
catch (err) {
|
|
899
|
-
result.errors.push(`${tableName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
// Mark as synced
|
|
903
|
-
if (pending.length > 0) {
|
|
904
|
-
const ids = pending.map((e) => e.id).join(',');
|
|
905
|
-
this.execute(`UPDATE dash_sync_queue SET synced = 1 WHERE id IN (${ids})`);
|
|
906
|
-
}
|
|
907
|
-
// Clean up old entries
|
|
908
|
-
this.execute(`
|
|
909
|
-
DELETE FROM dash_sync_queue
|
|
910
|
-
WHERE synced = 1
|
|
911
|
-
AND id NOT IN (
|
|
912
|
-
SELECT id FROM dash_sync_queue
|
|
913
|
-
WHERE synced = 1
|
|
914
|
-
ORDER BY id DESC
|
|
915
|
-
LIMIT 1000
|
|
916
|
-
)
|
|
917
|
-
`);
|
|
918
|
-
this.cloudConfig.onSyncComplete?.(result);
|
|
919
|
-
}
|
|
920
|
-
catch (err) {
|
|
921
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
922
|
-
result.errors.push(error.message);
|
|
923
|
-
this.cloudConfig.onSyncError?.(error);
|
|
924
|
-
}
|
|
925
|
-
finally {
|
|
926
|
-
this.isCloudSyncing = false;
|
|
927
|
-
}
|
|
928
|
-
return result;
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Apply a single server change locally
|
|
932
|
-
*/
|
|
933
|
-
applyServerChange(tableName, change) {
|
|
934
|
-
try {
|
|
935
|
-
if (change.deleted || change._deleted) {
|
|
936
|
-
this.execute(`DELETE FROM "${tableName}" WHERE id = ?`, [change.id]);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
const columns = Object.keys(change).filter(k => !k.startsWith('_'));
|
|
940
|
-
const placeholders = columns.map(() => '?').join(', ');
|
|
941
|
-
const values = columns.map(k => {
|
|
942
|
-
const v = change[k];
|
|
943
|
-
if (v !== null && typeof v === 'object') {
|
|
944
|
-
return JSON.stringify(v);
|
|
945
|
-
}
|
|
946
|
-
return v;
|
|
947
|
-
});
|
|
948
|
-
const sql = `INSERT OR REPLACE INTO "${tableName}" (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
949
|
-
this.execute(sql, values);
|
|
950
|
-
}
|
|
951
|
-
catch (err) {
|
|
952
|
-
console.error(`[Dash] Failed to apply server change to ${tableName}:`, err);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Force a full cloud sync (reset last sync time)
|
|
957
|
-
*/
|
|
958
|
-
async forceCloudSync() {
|
|
959
|
-
this.lastCloudSyncTime = 0;
|
|
960
|
-
this.execute("INSERT OR REPLACE INTO dash_sync_meta (key, value) VALUES ('lastCloudSyncTime', '0')");
|
|
961
|
-
return this.syncToCloud();
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Get cloud sync status (D1 and Relay)
|
|
965
|
-
*/
|
|
966
|
-
getCloudSyncStatus() {
|
|
967
|
-
let pendingChanges = 0;
|
|
968
|
-
if (this.cloudSyncEnabled && this.db) {
|
|
969
|
-
try {
|
|
970
|
-
const result = this.execute('SELECT COUNT(*) as count FROM dash_sync_queue WHERE synced = 0');
|
|
971
|
-
pendingChanges = result[0]?.count || 0;
|
|
972
|
-
}
|
|
973
|
-
catch {
|
|
974
|
-
// Table might not exist yet
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
// Get Relay status
|
|
978
|
-
let relayConnected = false;
|
|
979
|
-
let relayRoomName = null;
|
|
980
|
-
let relayPeerCount = 0;
|
|
981
|
-
if (this.relayProvider) {
|
|
982
|
-
try {
|
|
983
|
-
const status = this.relayProvider.getConnectionStatus();
|
|
984
|
-
relayConnected = status.connected;
|
|
985
|
-
relayRoomName = status.roomName;
|
|
986
|
-
relayPeerCount = this.relayProvider.getPeerCount();
|
|
987
|
-
}
|
|
988
|
-
catch {
|
|
989
|
-
// Provider might not be ready
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return {
|
|
993
|
-
enabled: this.cloudSyncEnabled || this.relaySyncEnabled,
|
|
994
|
-
syncing: this.isCloudSyncing,
|
|
995
|
-
lastSyncTime: this.lastCloudSyncTime,
|
|
996
|
-
pendingChanges,
|
|
997
|
-
syncedTables: Array.from(this.syncedTables),
|
|
998
|
-
d1: {
|
|
999
|
-
enabled: this.cloudSyncEnabled,
|
|
1000
|
-
syncing: this.isCloudSyncing,
|
|
1001
|
-
lastSyncTime: this.lastCloudSyncTime,
|
|
1002
|
-
pendingChanges,
|
|
1003
|
-
},
|
|
1004
|
-
relay: {
|
|
1005
|
-
enabled: this.relaySyncEnabled,
|
|
1006
|
-
connected: relayConnected,
|
|
1007
|
-
roomName: relayRoomName,
|
|
1008
|
-
peerCount: relayPeerCount,
|
|
1009
|
-
},
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Disable cloud sync (both D1 and Relay)
|
|
1014
|
-
*/
|
|
1015
|
-
disableCloudSync() {
|
|
1016
|
-
this.stopCloudSync();
|
|
1017
|
-
this.stopRelaySync();
|
|
1018
|
-
try {
|
|
1019
|
-
this.execute("UPDATE dash_sync_meta SET value = '0' WHERE key = 'cloud_enabled'");
|
|
1020
|
-
}
|
|
1021
|
-
catch {
|
|
1022
|
-
// Table might not exist yet
|
|
1023
|
-
}
|
|
1024
|
-
console.log('[Dash] Cloud sync disabled');
|
|
1025
|
-
}
|
|
1026
|
-
// ============================================
|
|
1027
|
-
// INTROSPECTION METHODS FOR DASH-STUDIO
|
|
1028
|
-
// ============================================
|
|
1029
|
-
/**
|
|
1030
|
-
* Get information about all tables in the database
|
|
1031
|
-
*/
|
|
1032
|
-
getAllTables() {
|
|
1033
|
-
if (!this.db)
|
|
1034
|
-
return [];
|
|
1035
|
-
const tables = this.execute(`
|
|
1036
|
-
SELECT name, type
|
|
1037
|
-
FROM sqlite_master
|
|
1038
|
-
WHERE type IN ('table', 'virtual table')
|
|
1039
|
-
AND name NOT LIKE 'sqlite_%'
|
|
1040
|
-
ORDER BY name
|
|
1041
|
-
`);
|
|
1042
|
-
return tables.map((t) => {
|
|
1043
|
-
const rowCount = this.getTableRowCount(t.name);
|
|
1044
|
-
return {
|
|
1045
|
-
name: t.name,
|
|
1046
|
-
type: t.type === 'table' ? 'table' : 'virtual',
|
|
1047
|
-
rowCount,
|
|
1048
|
-
isVirtual: t.type !== 'table'
|
|
1049
|
-
};
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Get detailed schema information for a specific table
|
|
1054
|
-
*/
|
|
1055
|
-
getTableSchema(tableName) {
|
|
1056
|
-
if (!this.db)
|
|
1057
|
-
return null;
|
|
1058
|
-
try {
|
|
1059
|
-
const columns = this.execute(`PRAGMA table_info('${tableName.replace(/'/g, "''")}')`);
|
|
1060
|
-
// Get indexes for this table
|
|
1061
|
-
const indexes = this.execute(`PRAGMA index_list('${tableName.replace(/'/g, "''")}')`);
|
|
1062
|
-
// Get foreign keys
|
|
1063
|
-
const foreignKeys = this.execute(`PRAGMA foreign_key_list('${tableName.replace(/'/g, "''")}')`);
|
|
1064
|
-
return {
|
|
1065
|
-
name: tableName,
|
|
1066
|
-
columns: columns.map((col) => ({
|
|
1067
|
-
cid: col.cid,
|
|
1068
|
-
name: col.name,
|
|
1069
|
-
type: col.type || 'ANY',
|
|
1070
|
-
notNull: col.notnull === 1,
|
|
1071
|
-
defaultValue: col.dflt_value,
|
|
1072
|
-
isPrimaryKey: col.pk === 1
|
|
1073
|
-
})),
|
|
1074
|
-
indexes: indexes.map((idx) => ({
|
|
1075
|
-
name: idx.name,
|
|
1076
|
-
unique: idx.unique === 1,
|
|
1077
|
-
origin: idx.origin
|
|
1078
|
-
})),
|
|
1079
|
-
foreignKeys: foreignKeys.map((fk) => ({
|
|
1080
|
-
id: fk.id,
|
|
1081
|
-
table: fk.table,
|
|
1082
|
-
from: fk.from,
|
|
1083
|
-
to: fk.to
|
|
1084
|
-
})),
|
|
1085
|
-
rowCount: this.getTableRowCount(tableName)
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
catch (e) {
|
|
1089
|
-
console.warn(`Failed to get schema for table ${tableName}:`, e);
|
|
1090
|
-
return null;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Get the row count for a specific table
|
|
1095
|
-
*/
|
|
1096
|
-
getTableRowCount(tableName) {
|
|
1097
|
-
if (!this.db)
|
|
1098
|
-
return 0;
|
|
1099
|
-
try {
|
|
1100
|
-
const result = this.execute(`SELECT COUNT(*) as count FROM "${tableName.replace(/"/g, '""')}"`);
|
|
1101
|
-
return result[0]?.count ?? 0;
|
|
1102
|
-
}
|
|
1103
|
-
catch (e) {
|
|
1104
|
-
// Virtual tables may not support COUNT
|
|
1105
|
-
return -1;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Get statistics about the vector index
|
|
1110
|
-
*/
|
|
1111
|
-
getVectorIndexStats() {
|
|
1112
|
-
if (!this.db) {
|
|
1113
|
-
return { totalEmbeddings: 0, dimensions: 384, indexExists: false };
|
|
1114
|
-
}
|
|
1115
|
-
try {
|
|
1116
|
-
const countResult = this.execute('SELECT COUNT(*) as count FROM dash_vec_idx');
|
|
1117
|
-
const totalEmbeddings = countResult[0]?.count ?? 0;
|
|
1118
|
-
return {
|
|
1119
|
-
totalEmbeddings,
|
|
1120
|
-
dimensions: 384, // Fixed dimension from schema
|
|
1121
|
-
indexExists: true,
|
|
1122
|
-
tableName: 'dash_vec_idx'
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
catch (e) {
|
|
1126
|
-
return {
|
|
1127
|
-
totalEmbeddings: 0,
|
|
1128
|
-
dimensions: 384,
|
|
1129
|
-
indexExists: false,
|
|
1130
|
-
error: e instanceof Error ? e.message : 'Vector index not available'
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Get all embeddings from the vector index
|
|
1136
|
-
*/
|
|
1137
|
-
getAllEmbeddings(limit = 100, offset = 0) {
|
|
1138
|
-
if (!this.db)
|
|
1139
|
-
return [];
|
|
1140
|
-
try {
|
|
1141
|
-
const rows = this.execute(`
|
|
1142
|
-
SELECT v.id, i.content, v.embedding
|
|
1143
|
-
FROM dash_vec_idx v
|
|
1144
|
-
LEFT JOIN dash_items i ON v.id = i.id
|
|
1145
|
-
LIMIT ? OFFSET ?
|
|
1146
|
-
`, [limit, offset]);
|
|
1147
|
-
return rows.map((row) => ({
|
|
1148
|
-
id: row.id,
|
|
1149
|
-
content: row.content,
|
|
1150
|
-
embedding: row.embedding ? Array.from(row.embedding) : null
|
|
1151
|
-
}));
|
|
1152
|
-
}
|
|
1153
|
-
catch (e) {
|
|
1154
|
-
console.warn('Failed to get embeddings:', e);
|
|
1155
|
-
return [];
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
/**
|
|
1159
|
-
* Search for similar vectors given a raw vector
|
|
1160
|
-
*/
|
|
1161
|
-
searchSimilarByVector(vector, k = 5) {
|
|
1162
|
-
if (!this.db)
|
|
1163
|
-
return [];
|
|
1164
|
-
try {
|
|
1165
|
-
const rows = this.execute(`
|
|
1166
|
-
SELECT
|
|
1167
|
-
item.id,
|
|
1168
|
-
item.content,
|
|
1169
|
-
distance
|
|
1170
|
-
FROM dash_vec_idx
|
|
1171
|
-
JOIN dash_items AS item ON item.id = dash_vec_idx.id
|
|
1172
|
-
WHERE embedding MATCH ?
|
|
1173
|
-
ORDER BY distance
|
|
1174
|
-
LIMIT ?
|
|
1175
|
-
`, [new Float32Array(vector), k]);
|
|
1176
|
-
return rows.map((row) => ({
|
|
1177
|
-
id: row.id,
|
|
1178
|
-
content: row.content,
|
|
1179
|
-
distance: row.distance,
|
|
1180
|
-
score: row.distance !== undefined ? 1 - row.distance : 0
|
|
1181
|
-
}));
|
|
1182
|
-
}
|
|
1183
|
-
catch (e) {
|
|
1184
|
-
console.warn('Vector search failed:', e);
|
|
1185
|
-
return [];
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Get statistics about the spatial R-Tree index
|
|
1190
|
-
*/
|
|
1191
|
-
getSpatialIndexStats() {
|
|
1192
|
-
if (!this.db) {
|
|
1193
|
-
return { totalEntries: 0, indexExists: false };
|
|
1194
|
-
}
|
|
1195
|
-
try {
|
|
1196
|
-
const countResult = this.execute('SELECT COUNT(*) as count FROM dash_spatial_idx');
|
|
1197
|
-
const totalEntries = countResult[0]?.count ?? 0;
|
|
1198
|
-
// Get bounding box of all entries
|
|
1199
|
-
const boundsResult = this.execute(`
|
|
1200
|
-
SELECT
|
|
1201
|
-
MIN(minX) as minX, MAX(maxX) as maxX,
|
|
1202
|
-
MIN(minY) as minY, MAX(maxY) as maxY,
|
|
1203
|
-
MIN(minZ) as minZ, MAX(maxZ) as maxZ
|
|
1204
|
-
FROM dash_spatial_idx
|
|
1205
|
-
`);
|
|
1206
|
-
const bounds = boundsResult[0] || {};
|
|
1207
|
-
return {
|
|
1208
|
-
totalEntries,
|
|
1209
|
-
indexExists: true,
|
|
1210
|
-
tableName: 'dash_spatial_idx',
|
|
1211
|
-
globalBounds: totalEntries > 0 ? {
|
|
1212
|
-
minX: bounds.minX,
|
|
1213
|
-
maxX: bounds.maxX,
|
|
1214
|
-
minY: bounds.minY,
|
|
1215
|
-
maxY: bounds.maxY,
|
|
1216
|
-
minZ: bounds.minZ,
|
|
1217
|
-
maxZ: bounds.maxZ
|
|
1218
|
-
} : undefined
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
catch (e) {
|
|
1222
|
-
return {
|
|
1223
|
-
totalEntries: 0,
|
|
1224
|
-
indexExists: false,
|
|
1225
|
-
error: e instanceof Error ? e.message : 'Spatial index not available'
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Get all spatial bounds from the R-Tree index
|
|
1231
|
-
*/
|
|
1232
|
-
getAllSpatialBounds(limit = 100, offset = 0) {
|
|
1233
|
-
if (!this.db)
|
|
1234
|
-
return [];
|
|
1235
|
-
try {
|
|
1236
|
-
const rows = this.execute(`
|
|
1237
|
-
SELECT
|
|
1238
|
-
idx.id as rowid,
|
|
1239
|
-
map.item_id as id,
|
|
1240
|
-
idx.minX, idx.maxX,
|
|
1241
|
-
idx.minY, idx.maxY,
|
|
1242
|
-
idx.minZ, idx.maxZ,
|
|
1243
|
-
item.content
|
|
1244
|
-
FROM dash_spatial_idx idx
|
|
1245
|
-
JOIN dash_spatial_map map ON map.rowid = idx.id
|
|
1246
|
-
LEFT JOIN dash_items item ON item.id = map.item_id
|
|
1247
|
-
LIMIT ? OFFSET ?
|
|
1248
|
-
`, [limit, offset]);
|
|
1249
|
-
return rows.map((row) => ({
|
|
1250
|
-
id: row.id,
|
|
1251
|
-
rowid: row.rowid,
|
|
1252
|
-
bounds: {
|
|
1253
|
-
minX: row.minX,
|
|
1254
|
-
maxX: row.maxX,
|
|
1255
|
-
minY: row.minY,
|
|
1256
|
-
maxY: row.maxY,
|
|
1257
|
-
minZ: row.minZ,
|
|
1258
|
-
maxZ: row.maxZ
|
|
1259
|
-
},
|
|
1260
|
-
content: row.content
|
|
1261
|
-
}));
|
|
1262
|
-
}
|
|
1263
|
-
catch (e) {
|
|
1264
|
-
console.warn('Failed to get spatial bounds:', e);
|
|
1265
|
-
return [];
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Get information about active table subscriptions
|
|
1270
|
-
*/
|
|
1271
|
-
getActiveSubscriptions() {
|
|
1272
|
-
const subscriptions = [];
|
|
1273
|
-
this.tableListeners.forEach((listeners, tableName) => {
|
|
1274
|
-
subscriptions.push({
|
|
1275
|
-
table: tableName,
|
|
1276
|
-
listenerCount: listeners.size
|
|
1277
|
-
});
|
|
1278
|
-
});
|
|
1279
|
-
// Add global listeners
|
|
1280
|
-
if (this.listeners.size > 0) {
|
|
1281
|
-
subscriptions.push({
|
|
1282
|
-
table: '*',
|
|
1283
|
-
listenerCount: this.listeners.size
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
return subscriptions;
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Get table listener counts as a map
|
|
1290
|
-
*/
|
|
1291
|
-
getTableListenerCounts() {
|
|
1292
|
-
const counts = {};
|
|
1293
|
-
this.tableListeners.forEach((listeners, tableName) => {
|
|
1294
|
-
counts[tableName] = listeners.size;
|
|
1295
|
-
});
|
|
1296
|
-
counts['*'] = this.listeners.size;
|
|
1297
|
-
return counts;
|
|
1298
|
-
}
|
|
1299
|
-
/**
|
|
1300
|
-
* Check if the database is ready
|
|
1301
|
-
*/
|
|
1302
|
-
isReady() {
|
|
1303
|
-
return this.db !== null;
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Get database metadata
|
|
1307
|
-
*/
|
|
1308
|
-
getDatabaseInfo() {
|
|
1309
|
-
if (!this.db) {
|
|
1310
|
-
return {
|
|
1311
|
-
ready: false,
|
|
1312
|
-
schemaVersion: this.currentSchemaVersion,
|
|
1313
|
-
tableCount: 0
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
const tables = this.getAllTables();
|
|
1317
|
-
return {
|
|
1318
|
-
ready: true,
|
|
1319
|
-
schemaVersion: this.currentSchemaVersion,
|
|
1320
|
-
tableCount: tables.length,
|
|
1321
|
-
tables: tables.map(t => t.name)
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
export const dash = new DashEngine();
|