dignity.js 0.3.0 → 0.4.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.
@@ -3000,6 +3000,21 @@ var require_dignity_p2p = __commonJS({
3000
3000
  if (existing.ownerId !== this.nodeId) {
3001
3001
  throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
3002
3002
  }
3003
+ if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
3004
+ this.emitConflict({
3005
+ kind: "update",
3006
+ collection: collectionName,
3007
+ id,
3008
+ expectedVersion: options.expectedVersion,
3009
+ currentVersion: existing.version,
3010
+ phase: "local"
3011
+ });
3012
+ const error = new Error(
3013
+ `Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
3014
+ );
3015
+ error.code = "VERSION_CONFLICT";
3016
+ throw error;
3017
+ }
3003
3018
  const operation = {
3004
3019
  opId: this.idGenerator(),
3005
3020
  kind: "update",
@@ -3020,6 +3035,27 @@ var require_dignity_p2p = __commonJS({
3020
3035
  });
3021
3036
  return this.read(collectionName, id);
3022
3037
  }
3038
+ async updateWithRetry(collectionName, id, patchFn, options = {}) {
3039
+ const maxAttempts = typeof options.maxAttempts === "number" ? options.maxAttempts : 5;
3040
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3041
+ const current = this.read(collectionName, id);
3042
+ if (!current) {
3043
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3044
+ }
3045
+ const patch = await patchFn(current);
3046
+ try {
3047
+ return await this.update(collectionName, id, patch, {
3048
+ ...options,
3049
+ expectedVersion: current.version
3050
+ });
3051
+ } catch (error) {
3052
+ if (error.code !== "VERSION_CONFLICT" || attempt === maxAttempts - 1) {
3053
+ throw error;
3054
+ }
3055
+ }
3056
+ }
3057
+ throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3058
+ }
3023
3059
  async remove(collectionName, id, options = {}) {
3024
3060
  const existing = this.getCollection(collectionName).get(id);
3025
3061
  if (!existing || existing.deletedAt) {
@@ -3297,6 +3333,29 @@ var require_dignity_p2p = __commonJS({
3297
3333
  isPeerBanned(peerId) {
3298
3334
  return this.getBanInfo(peerId) !== null;
3299
3335
  }
3336
+ emitConflict(details) {
3337
+ this.emit("conflict", details);
3338
+ }
3339
+ restoreRecord(collectionName, record) {
3340
+ if (!record || !record.id) {
3341
+ return false;
3342
+ }
3343
+ const collection = this.getCollection(collectionName);
3344
+ const current = collection.get(record.id);
3345
+ if (current && current.version >= record.version) {
3346
+ return false;
3347
+ }
3348
+ collection.set(record.id, {
3349
+ id: record.id,
3350
+ ownerId: record.ownerId,
3351
+ data: { ...record.data || {} },
3352
+ createdAt: record.createdAt,
3353
+ updatedAt: record.updatedAt,
3354
+ deletedAt: record.deletedAt || null,
3355
+ version: record.version
3356
+ });
3357
+ return true;
3358
+ }
3300
3359
  applyOperation(operation) {
3301
3360
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3302
3361
  return false;
@@ -3327,6 +3386,15 @@ var require_dignity_p2p = __commonJS({
3327
3386
  return false;
3328
3387
  }
3329
3388
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3389
+ this.emitConflict({
3390
+ kind: operation.kind,
3391
+ collection: operation.collectionName,
3392
+ id: operation.id,
3393
+ expectedVersion: operation.baseVersion,
3394
+ currentVersion: current.version,
3395
+ phase: "remote",
3396
+ operation
3397
+ });
3330
3398
  return false;
3331
3399
  }
3332
3400
  if (operation.kind === "update") {
@@ -10414,6 +10482,163 @@ var require_in_memory_network = __commonJS({
10414
10482
  }
10415
10483
  });
10416
10484
 
10485
+ // src/persistence/indexeddb-persistence.js
10486
+ var require_indexeddb_persistence = __commonJS({
10487
+ "src/persistence/indexeddb-persistence.js"(exports, module) {
10488
+ var IndexedDBPersistence = class {
10489
+ constructor({
10490
+ dbName = "dignity",
10491
+ storeName = "records",
10492
+ collections = null,
10493
+ indexedDB = typeof globalThis !== "undefined" ? globalThis.indexedDB : null
10494
+ } = {}) {
10495
+ this.dbName = dbName;
10496
+ this.storeName = storeName;
10497
+ this.collections = collections;
10498
+ this.indexedDB = indexedDB;
10499
+ this.node = null;
10500
+ this.changeHandler = null;
10501
+ }
10502
+ recordKey(collection, id) {
10503
+ return `${collection}:${id}`;
10504
+ }
10505
+ shouldPersist(collection) {
10506
+ if (!this.collections) {
10507
+ return true;
10508
+ }
10509
+ return this.collections.includes(collection);
10510
+ }
10511
+ openDb() {
10512
+ if (!this.indexedDB) {
10513
+ return Promise.reject(new Error("IndexedDB is not available"));
10514
+ }
10515
+ return new Promise((resolve, reject) => {
10516
+ const request = this.indexedDB.open(this.dbName, 1);
10517
+ request.onupgradeneeded = () => {
10518
+ const db = request.result;
10519
+ if (!db.objectStoreNames.contains(this.storeName)) {
10520
+ db.createObjectStore(this.storeName, { keyPath: "key" });
10521
+ }
10522
+ };
10523
+ request.onsuccess = () => resolve(request.result);
10524
+ request.onerror = () => reject(request.error || new Error("Unable to open IndexedDB"));
10525
+ });
10526
+ }
10527
+ runTransaction(mode, handler) {
10528
+ return this.openDb().then((db) => new Promise((resolve, reject) => {
10529
+ const transaction = db.transaction(this.storeName, mode);
10530
+ const store = transaction.objectStore(this.storeName);
10531
+ Promise.resolve(handler(store)).then(resolve).catch(reject);
10532
+ transaction.oncomplete = () => db.close();
10533
+ transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
10534
+ transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
10535
+ }));
10536
+ }
10537
+ serializeRecord(collection, id) {
10538
+ const record = this.node.getCollection(collection).get(id);
10539
+ if (!record) {
10540
+ return null;
10541
+ }
10542
+ return {
10543
+ key: this.recordKey(collection, id),
10544
+ collection,
10545
+ id,
10546
+ ownerId: record.ownerId,
10547
+ data: { ...record.data },
10548
+ createdAt: record.createdAt,
10549
+ updatedAt: record.updatedAt,
10550
+ deletedAt: record.deletedAt,
10551
+ version: record.version
10552
+ };
10553
+ }
10554
+ async persistRecord(collection, id) {
10555
+ if (!this.node || !this.shouldPersist(collection)) {
10556
+ return;
10557
+ }
10558
+ const serialized = this.serializeRecord(collection, id);
10559
+ const key = this.recordKey(collection, id);
10560
+ if (!serialized) {
10561
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10562
+ const request = store.delete(key);
10563
+ request.onsuccess = () => resolve();
10564
+ request.onerror = () => reject(request.error);
10565
+ }));
10566
+ return;
10567
+ }
10568
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10569
+ const request = store.put(serialized);
10570
+ request.onsuccess = () => resolve();
10571
+ request.onerror = () => reject(request.error);
10572
+ }));
10573
+ }
10574
+ persistChange(event) {
10575
+ if (!event || !event.collection || !event.id) {
10576
+ return;
10577
+ }
10578
+ this.persistRecord(event.collection, event.id).catch((error) => {
10579
+ this.node.emit("warning", {
10580
+ type: "persistence-failed",
10581
+ collection: event.collection,
10582
+ id: event.id,
10583
+ error
10584
+ });
10585
+ });
10586
+ }
10587
+ async loadAllRecords() {
10588
+ return this.runTransaction("readonly", (store) => new Promise((resolve, reject) => {
10589
+ const request = store.getAll();
10590
+ request.onsuccess = () => resolve(request.result || []);
10591
+ request.onerror = () => reject(request.error);
10592
+ }));
10593
+ }
10594
+ async hydrate() {
10595
+ if (!this.node) {
10596
+ throw new Error("IndexedDBPersistence requires an attached node before hydrate");
10597
+ }
10598
+ const storedRecords = await this.loadAllRecords();
10599
+ for (const stored of storedRecords) {
10600
+ if (!this.shouldPersist(stored.collection)) {
10601
+ continue;
10602
+ }
10603
+ this.node.restoreRecord(stored.collection, {
10604
+ id: stored.id,
10605
+ ownerId: stored.ownerId,
10606
+ data: stored.data,
10607
+ createdAt: stored.createdAt,
10608
+ updatedAt: stored.updatedAt,
10609
+ deletedAt: stored.deletedAt,
10610
+ version: stored.version
10611
+ });
10612
+ }
10613
+ }
10614
+ async attach(node) {
10615
+ if (!node) {
10616
+ throw new Error("IndexedDBPersistence.attach requires a DignityP2P node");
10617
+ }
10618
+ this.node = node;
10619
+ await this.hydrate();
10620
+ this.changeHandler = (event) => this.persistChange(event);
10621
+ node.on("change", this.changeHandler);
10622
+ }
10623
+ async detach() {
10624
+ if (this.node && this.changeHandler) {
10625
+ this.node.off("change", this.changeHandler);
10626
+ }
10627
+ this.changeHandler = null;
10628
+ this.node = null;
10629
+ }
10630
+ async clear() {
10631
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10632
+ const request = store.clear();
10633
+ request.onsuccess = () => resolve();
10634
+ request.onerror = () => reject(request.error);
10635
+ }));
10636
+ }
10637
+ };
10638
+ module.exports = IndexedDBPersistence;
10639
+ }
10640
+ });
10641
+
10417
10642
  // src/index.js
10418
10643
  var require_index = __commonJS({
10419
10644
  "src/index.js"(exports, module) {
@@ -10426,6 +10651,7 @@ var require_index = __commonJS({
10426
10651
  InMemoryNetworkHub,
10427
10652
  InMemoryNetworkAdapter
10428
10653
  } = require_in_memory_network();
10654
+ var IndexedDBPersistence = require_indexeddb_persistence();
10429
10655
  var {
10430
10656
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10431
10657
  DEFAULT_SIGNALING_FALLBACK_URLS
@@ -10444,6 +10670,7 @@ var require_index = __commonJS({
10444
10670
  PeerJSSignalingProvider,
10445
10671
  InMemoryNetworkHub,
10446
10672
  InMemoryNetworkAdapter,
10673
+ IndexedDBPersistence,
10447
10674
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10448
10675
  DEFAULT_SIGNALING_FALLBACK_URLS,
10449
10676
  VDF,