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.
- package/README.md +60 -3
- package/dist/dignity.cjs.js +227 -0
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +227 -0
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +5 -5
- package/docs/assets/docs.js +47 -0
- package/docs/assets/highlight/github-dark.min.css +10 -0
- package/docs/assets/highlight/github.min.css +10 -0
- package/docs/assets/highlight/highlight.min.js +1244 -0
- package/docs/assets/styles.css +449 -38
- package/docs/index.html +601 -81
- package/docs/openapi-like.json +44 -6
- package/package.json +21 -3
- package/src/core/dignity-p2p.js +79 -0
- package/src/index.js +2 -0
- package/src/persistence/indexeddb-persistence.js +182 -0
- package/src/react/index.js +114 -0
package/dist/dignity.esm.js
CHANGED
|
@@ -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,
|