zeno-db
Version:
A lightweight, offline-first client-side database with automatic sync capabilities
298 lines (297 loc) • 12.6 kB
JavaScript
"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;