UNPKG

zeno-db

Version:

A lightweight, offline-first client-side database with automatic sync capabilities

298 lines (297 loc) 12.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZenoDB = void 0; const indexedDBAdapter_1 = require("./storage/indexedDBAdapter"); const websocketSync_1 = require("./sync/websocketSync"); class ZenoDB { constructor(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; this.syncAdapter = null; this.syncSubscribers = []; this.syncStatus = { isOnline: false, isSyncing: false, syncProgress: 0, pendingChangesCount: 0 }; this.isInitialized = false; // Generate clientId if not provided const clientId = config.clientId || `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; if (!config.sync.url) { throw new Error('sync.url is required'); } this.config = Object.assign(Object.assign({}, config), { clientId, softDelete: { enabled: (_b = (_a = config.softDelete) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : false, fieldName: (_d = (_c = config.softDelete) === null || _c === void 0 ? void 0 : _c.fieldName) !== null && _d !== void 0 ? _d : 'deletedAt', permanentDeleteAfter: (_f = (_e = config.softDelete) === null || _e === void 0 ? void 0 : _e.permanentDeleteAfter) !== null && _f !== void 0 ? _f : 30 } }); if (config.storage.type !== 'indexeddb') { throw new Error('Only IndexedDB is supported in the browser'); } this.storageAdapter = new indexedDBAdapter_1.IndexedDBAdapter(config.storage); if (config.sync.type === 'websocket') { this.syncAdapter = new websocketSync_1.WebSocketSync({ url: config.sync.url }); } this.softDeleteConfig = { enabled: (_h = (_g = config.softDelete) === null || _g === void 0 ? void 0 : _g.enabled) !== null && _h !== void 0 ? _h : false, fieldName: (_k = (_j = config.softDelete) === null || _j === void 0 ? void 0 : _j.fieldName) !== null && _k !== void 0 ? _k : 'deletedAt', permanentDeleteAfter: (_m = (_l = config.softDelete) === null || _l === void 0 ? void 0 : _l.permanentDeleteAfter) !== null && _m !== void 0 ? _m : 30 }; } updateSyncStatus(updates) { this.syncStatus = Object.assign(Object.assign({}, this.syncStatus), updates); this.notifySyncSubscribers('connection_change'); } notifySyncSubscribers(type) { const event = { type, status: this.syncStatus }; this.syncSubscribers.forEach(callback => callback(event)); } subscribe(callback) { this.syncSubscribers.push(callback); // Return unsubscribe function return () => { this.syncSubscribers = this.syncSubscribers.filter(cb => cb !== callback); }; } getSyncStatus() { return Object.assign({}, this.syncStatus); } init() { return __awaiter(this, void 0, void 0, function* () { yield this.storageAdapter.init(); if (this.syncAdapter) { this.syncAdapter.onConnected(() => __awaiter(this, void 0, void 0, function* () { this.updateSyncStatus({ isOnline: true }); yield this.syncOfflineChanges(); })); this.syncAdapter.onDisconnected(() => { this.updateSyncStatus({ isOnline: false }); }); yield this.syncAdapter.connect(); } this.isInitialized = true; }); } syncOfflineChanges() { return __awaiter(this, void 0, void 0, function* () { if (!this.syncAdapter) return; const offlineChanges = this.storageAdapter.getChanges(); if (offlineChanges.length > 0) { this.updateSyncStatus({ isSyncing: true, pendingChangesCount: offlineChanges.length }); this.notifySyncSubscribers('sync_started'); for (let i = 0; i < offlineChanges.length; i++) { const change = offlineChanges[i]; yield this.syncAdapter.sendChange(change); const progress = Math.round((i + 1) / offlineChanges.length * 100); this.updateSyncStatus({ syncProgress: progress, pendingChangesCount: offlineChanges.length - (i + 1) }); this.notifySyncSubscribers('sync_progress'); } this.storageAdapter.clearChanges(); this.updateSyncStatus({ isSyncing: false, syncProgress: 0, pendingChangesCount: 0 }); this.notifySyncSubscribers('sync_completed'); } }); } get(key_1) { return __awaiter(this, arguments, void 0, function* (key, includeDeleted = false) { var _a; const value = yield this.storageAdapter.get(key); if (!value) return null; if (this.softDeleteConfig.enabled && !includeDeleted) { const softDeleteField = (_a = this.softDeleteConfig.fieldName) !== null && _a !== void 0 ? _a : 'deletedAt'; const val = value; if (val[softDeleteField]) { return null; } } return value; }); } set(key, value) { return __awaiter(this, void 0, void 0, function* () { yield this.storageAdapter.set(key, value); if (this.syncAdapter) { const change = { key, value, timestamp: Date.now(), clientId: this.config.clientId, operation: 'set' }; yield this.syncAdapter.sendChange(change); } }); } delete(key_1) { return __awaiter(this, arguments, void 0, function* (key, permanent = false) { if (!this.isInitialized) { throw new Error('Database not initialized'); } if (this.softDeleteConfig.enabled && !permanent) { // Soft delete const item = yield this.get(key); if (item) { const softDeleteField = this.softDeleteConfig.fieldName; if (softDeleteField) { item[softDeleteField] = new Date().toISOString(); yield this.set(key, item); } } } else { // Permanent delete yield this.storageAdapter.delete(key); if (this.syncAdapter) { yield this.syncAdapter.sendChange({ key, value: null, timestamp: Date.now(), clientId: this.config.clientId }); } } }); } keys() { return __awaiter(this, void 0, void 0, function* () { return yield this.storageAdapter.keys(); }); } close() { return __awaiter(this, void 0, void 0, function* () { if (this.syncAdapter) { yield this.syncAdapter.disconnect(); } }); } getAll() { return __awaiter(this, arguments, void 0, function* (includeDeleted = false) { if (!this.isInitialized) { throw new Error('Database not initialized'); } const items = yield this.storageAdapter.getAll(); const itemsArray = Array.isArray(items) ? items : Object.values(items); if (!this.softDeleteConfig.enabled || includeDeleted) { return itemsArray; } const softDeleteField = this.softDeleteConfig.fieldName; return itemsArray.filter((item) => !softDeleteField || !item[softDeleteField]); }); } restore(key) { return __awaiter(this, void 0, void 0, function* () { if (!this.isInitialized) { throw new Error('Database not initialized'); } if (!this.softDeleteConfig.enabled) { throw new Error('Soft delete is not enabled'); } const item = yield this.get(key, true); const softDeleteField = this.softDeleteConfig.fieldName; if (item && softDeleteField && item[softDeleteField]) { delete item[softDeleteField]; yield this.set(key, item); } }); } purgeDeleted() { return __awaiter(this, void 0, void 0, function* () { var _a; if (!this.isInitialized) { throw new Error('Database not initialized'); } if (!this.softDeleteConfig.enabled) { throw new Error('Soft delete is not enabled'); } const items = yield this.getAll(true); const now = new Date(); const daysToKeep = (_a = this.softDeleteConfig.permanentDeleteAfter) !== null && _a !== void 0 ? _a : 30; const cutoffDate = new Date(now.setDate(now.getDate() - daysToKeep)); const softDeleteField = this.softDeleteConfig.fieldName; for (const item of items) { if (softDeleteField && item[softDeleteField]) { const deletedAt = new Date(item[softDeleteField]); if (deletedAt < cutoffDate && item.id) { yield this.delete(item.id, true); } } } }); } /** * Batch get multiple items by keys. Returns an array of values in the same order as keys. */ getMany(keys, includeDeleted) { return __awaiter(this, void 0, void 0, function* () { if (this.storageAdapter.getMany) { const values = yield this.storageAdapter.getMany(keys); if (!this.softDeleteConfig.enabled || includeDeleted) { return values; } const softDeleteField = this.softDeleteConfig.fieldName; return values.map((value) => { if (!value) return null; if (softDeleteField && value[softDeleteField]) return null; return value; }); } else { // Fallback: fetch one by one const results = yield Promise.all(keys.map((key) => this.get(key, includeDeleted))); return results; } }); } /** * Batch set multiple items. Each item must have an 'id' property. */ setMany(items) { return __awaiter(this, void 0, void 0, function* () { if (this.storageAdapter.setMany) { yield this.storageAdapter.setMany(items); } else { yield Promise.all(items.map((item) => this.set(item.id, item))); } if (this.syncAdapter) { for (const item of items) { const change = { key: item.id, value: item, timestamp: Date.now(), clientId: this.config.clientId, operation: 'set', }; yield this.syncAdapter.sendChange(change); } } }); } } exports.ZenoDB = ZenoDB;