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/docs/openapi-like.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "REST-like object API over peer-to-peer replication",
|
|
5
5
|
"resources": {
|
|
6
6
|
"collections/{collection}/{id}": {
|
|
@@ -12,8 +12,16 @@
|
|
|
12
12
|
"method": "read(collection, id)"
|
|
13
13
|
},
|
|
14
14
|
"update": {
|
|
15
|
-
"method": "update(collection, id, patch)",
|
|
16
|
-
"authorization": "owner-only"
|
|
15
|
+
"method": "update(collection, id, patch, options)",
|
|
16
|
+
"authorization": "owner-only",
|
|
17
|
+
"options": {
|
|
18
|
+
"expectedVersion": "optional number; throws VERSION_CONFLICT when mismatched",
|
|
19
|
+
"broadcastScope": "optional scoped broadcast password namespace"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"updateWithRetry": {
|
|
23
|
+
"method": "updateWithRetry(collection, id, patchFn, options)",
|
|
24
|
+
"description": "read-modify-write helper with automatic retry on version conflicts"
|
|
17
25
|
},
|
|
18
26
|
"delete": {
|
|
19
27
|
"method": "remove(collection, id)",
|
|
@@ -26,6 +34,31 @@
|
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
},
|
|
37
|
+
"events": {
|
|
38
|
+
"change": "object create/update/delete applied",
|
|
39
|
+
"conflict": "local or remote version mismatch",
|
|
40
|
+
"peerdiscovered": "peer joined discovery scope",
|
|
41
|
+
"peerleft": "peer left or timed out",
|
|
42
|
+
"message": "custom decrypted message received"
|
|
43
|
+
},
|
|
44
|
+
"persistence": {
|
|
45
|
+
"IndexedDBPersistence": {
|
|
46
|
+
"method": "attach(node)",
|
|
47
|
+
"options": [
|
|
48
|
+
"dbName",
|
|
49
|
+
"storeName",
|
|
50
|
+
"collections"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"react": {
|
|
55
|
+
"entrypoint": "dignity.js/react",
|
|
56
|
+
"hooks": [
|
|
57
|
+
"useDignity",
|
|
58
|
+
"useCollection",
|
|
59
|
+
"usePeers"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
29
62
|
"signaling": {
|
|
30
63
|
"defaults": {
|
|
31
64
|
"cloudflare": "enabled by default",
|
|
@@ -45,13 +78,18 @@
|
|
|
45
78
|
"signingEnabled": true,
|
|
46
79
|
"encryptionEnabled": true,
|
|
47
80
|
"powEnabled": true,
|
|
48
|
-
"
|
|
81
|
+
"powSteps": 22,
|
|
82
|
+
"powTargetMs": 1000,
|
|
83
|
+
"kdfIterations": 100000,
|
|
84
|
+
"banDurationMs": 172800000
|
|
49
85
|
},
|
|
50
86
|
"broadcast": {
|
|
51
|
-
"encryption": "
|
|
87
|
+
"encryption": "AES secretbox with PBKDF2-SHA256 derived key",
|
|
88
|
+
"scopePasswords": "broadcastPasswords map keyed by broadcastScope",
|
|
89
|
+
"legacyKdf": "single-hash fallback accepted for older peers"
|
|
52
90
|
},
|
|
53
91
|
"direct": {
|
|
54
|
-
"encryption": "
|
|
92
|
+
"encryption": "NaCl box (X25519 + XSalsa20-Poly1305) to recipient public key"
|
|
55
93
|
},
|
|
56
94
|
"pow": {
|
|
57
95
|
"algorithm": "Sloth VDF",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "P2P object API for decentralized JavaScript applications",
|
|
5
5
|
"main": "dist/dignity.cjs.js",
|
|
6
6
|
"module": "dist/dignity.esm.js",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"require": "./dist/dignity.cjs.js",
|
|
13
13
|
"import": "./dist/dignity.esm.js",
|
|
14
14
|
"default": "./dist/dignity.esm.js"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"require": "./src/react/index.js",
|
|
18
|
+
"import": "./src/react/index.js",
|
|
19
|
+
"default": "./src/react/index.js"
|
|
15
20
|
}
|
|
16
21
|
},
|
|
17
22
|
"files": [
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
"test:pow-calibrate": "jest tests/unit/sloth-vdf-timing.test.js --runInBand",
|
|
28
33
|
"build": "node scripts/build.js",
|
|
29
34
|
"docs:serve": "npx http-server docs -p 4173 -o",
|
|
30
|
-
"docs:check": "node -e \"require('fs')
|
|
35
|
+
"docs:check": "node -e \"const fs=require('fs');['docs/index.html','docs/assets/highlight/highlight.min.js','docs/assets/highlight/github.min.css','docs/assets/highlight/github-dark.min.css'].forEach(p=>fs.accessSync(p));\"",
|
|
31
36
|
"example:tictactoe": "node examples/decentralized-tictactoe.js",
|
|
32
37
|
"example:chess": "node examples/decentralized-chess-lite.js",
|
|
33
38
|
"prepublishOnly": "npm test && npm run build"
|
|
@@ -40,9 +45,22 @@
|
|
|
40
45
|
"objects"
|
|
41
46
|
],
|
|
42
47
|
"license": "Apache 2.0",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"react": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
43
56
|
"devDependencies": {
|
|
57
|
+
"@testing-library/react": "^16.3.0",
|
|
44
58
|
"esbuild": "^0.28.0",
|
|
45
|
-
"
|
|
59
|
+
"fake-indexeddb": "^6.0.0",
|
|
60
|
+
"jest": "^29.7.0",
|
|
61
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
62
|
+
"react": "^19.1.0",
|
|
63
|
+
"react-dom": "^19.1.0"
|
|
46
64
|
},
|
|
47
65
|
"dependencies": {
|
|
48
66
|
"peerjs": "^1.5.5",
|
package/src/core/dignity-p2p.js
CHANGED
|
@@ -178,6 +178,23 @@ class DignityP2P extends EventEmitter {
|
|
|
178
178
|
throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
if (typeof options.expectedVersion === 'number' && existing.version !== options.expectedVersion) {
|
|
182
|
+
this.emitConflict({
|
|
183
|
+
kind: 'update',
|
|
184
|
+
collection: collectionName,
|
|
185
|
+
id,
|
|
186
|
+
expectedVersion: options.expectedVersion,
|
|
187
|
+
currentVersion: existing.version,
|
|
188
|
+
phase: 'local'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const error = new Error(
|
|
192
|
+
`Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
|
|
193
|
+
);
|
|
194
|
+
error.code = 'VERSION_CONFLICT';
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
const operation = {
|
|
182
199
|
opId: this.idGenerator(),
|
|
183
200
|
kind: 'update',
|
|
@@ -201,6 +218,31 @@ class DignityP2P extends EventEmitter {
|
|
|
201
218
|
return this.read(collectionName, id);
|
|
202
219
|
}
|
|
203
220
|
|
|
221
|
+
async updateWithRetry(collectionName, id, patchFn, options = {}) {
|
|
222
|
+
const maxAttempts = typeof options.maxAttempts === 'number' ? options.maxAttempts : 5;
|
|
223
|
+
|
|
224
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
225
|
+
const current = this.read(collectionName, id);
|
|
226
|
+
if (!current) {
|
|
227
|
+
throw new Error(`Object ${id} does not exist in ${collectionName}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const patch = await patchFn(current);
|
|
231
|
+
try {
|
|
232
|
+
return await this.update(collectionName, id, patch, {
|
|
233
|
+
...options,
|
|
234
|
+
expectedVersion: current.version
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error.code !== 'VERSION_CONFLICT' || attempt === maxAttempts - 1) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
|
|
244
|
+
}
|
|
245
|
+
|
|
204
246
|
async remove(collectionName, id, options = {}) {
|
|
205
247
|
const existing = this.getCollection(collectionName).get(id);
|
|
206
248
|
|
|
@@ -534,6 +576,34 @@ class DignityP2P extends EventEmitter {
|
|
|
534
576
|
return this.getBanInfo(peerId) !== null;
|
|
535
577
|
}
|
|
536
578
|
|
|
579
|
+
emitConflict(details) {
|
|
580
|
+
this.emit('conflict', details);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
restoreRecord(collectionName, record) {
|
|
584
|
+
if (!record || !record.id) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const collection = this.getCollection(collectionName);
|
|
589
|
+
const current = collection.get(record.id);
|
|
590
|
+
if (current && current.version >= record.version) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
collection.set(record.id, {
|
|
595
|
+
id: record.id,
|
|
596
|
+
ownerId: record.ownerId,
|
|
597
|
+
data: { ...(record.data || {}) },
|
|
598
|
+
createdAt: record.createdAt,
|
|
599
|
+
updatedAt: record.updatedAt,
|
|
600
|
+
deletedAt: record.deletedAt || null,
|
|
601
|
+
version: record.version
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
537
607
|
applyOperation(operation) {
|
|
538
608
|
if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
|
|
539
609
|
return false;
|
|
@@ -571,6 +641,15 @@ class DignityP2P extends EventEmitter {
|
|
|
571
641
|
}
|
|
572
642
|
|
|
573
643
|
if (typeof operation.baseVersion === 'number' && operation.baseVersion !== current.version) {
|
|
644
|
+
this.emitConflict({
|
|
645
|
+
kind: operation.kind,
|
|
646
|
+
collection: operation.collectionName,
|
|
647
|
+
id: operation.id,
|
|
648
|
+
expectedVersion: operation.baseVersion,
|
|
649
|
+
currentVersion: current.version,
|
|
650
|
+
phase: 'remote',
|
|
651
|
+
operation
|
|
652
|
+
});
|
|
574
653
|
return false;
|
|
575
654
|
}
|
|
576
655
|
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
InMemoryNetworkHub,
|
|
16
16
|
InMemoryNetworkAdapter
|
|
17
17
|
} = require('./network/in-memory-network');
|
|
18
|
+
const IndexedDBPersistence = require('./persistence/indexeddb-persistence');
|
|
18
19
|
const {
|
|
19
20
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
20
21
|
DEFAULT_SIGNALING_FALLBACK_URLS
|
|
@@ -34,6 +35,7 @@ module.exports = {
|
|
|
34
35
|
PeerJSSignalingProvider,
|
|
35
36
|
InMemoryNetworkHub,
|
|
36
37
|
InMemoryNetworkAdapter,
|
|
38
|
+
IndexedDBPersistence,
|
|
37
39
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
38
40
|
DEFAULT_SIGNALING_FALLBACK_URLS,
|
|
39
41
|
VDF,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
class IndexedDBPersistence {
|
|
2
|
+
constructor({
|
|
3
|
+
dbName = 'dignity',
|
|
4
|
+
storeName = 'records',
|
|
5
|
+
collections = null,
|
|
6
|
+
indexedDB = typeof globalThis !== 'undefined' ? globalThis.indexedDB : null
|
|
7
|
+
} = {}) {
|
|
8
|
+
this.dbName = dbName;
|
|
9
|
+
this.storeName = storeName;
|
|
10
|
+
this.collections = collections;
|
|
11
|
+
this.indexedDB = indexedDB;
|
|
12
|
+
this.node = null;
|
|
13
|
+
this.changeHandler = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
recordKey(collection, id) {
|
|
17
|
+
return `${collection}:${id}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
shouldPersist(collection) {
|
|
21
|
+
if (!this.collections) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.collections.includes(collection);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
openDb() {
|
|
29
|
+
if (!this.indexedDB) {
|
|
30
|
+
return Promise.reject(new Error('IndexedDB is not available'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const request = this.indexedDB.open(this.dbName, 1);
|
|
35
|
+
|
|
36
|
+
request.onupgradeneeded = () => {
|
|
37
|
+
const db = request.result;
|
|
38
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
39
|
+
db.createObjectStore(this.storeName, { keyPath: 'key' });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
request.onsuccess = () => resolve(request.result);
|
|
44
|
+
request.onerror = () => reject(request.error || new Error('Unable to open IndexedDB'));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runTransaction(mode, handler) {
|
|
49
|
+
return this.openDb().then((db) => new Promise((resolve, reject) => {
|
|
50
|
+
const transaction = db.transaction(this.storeName, mode);
|
|
51
|
+
const store = transaction.objectStore(this.storeName);
|
|
52
|
+
|
|
53
|
+
Promise.resolve(handler(store))
|
|
54
|
+
.then(resolve)
|
|
55
|
+
.catch(reject);
|
|
56
|
+
|
|
57
|
+
transaction.oncomplete = () => db.close();
|
|
58
|
+
transaction.onerror = () => reject(transaction.error || new Error('IndexedDB transaction failed'));
|
|
59
|
+
transaction.onabort = () => reject(transaction.error || new Error('IndexedDB transaction aborted'));
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
serializeRecord(collection, id) {
|
|
64
|
+
const record = this.node.getCollection(collection).get(id);
|
|
65
|
+
if (!record) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
key: this.recordKey(collection, id),
|
|
71
|
+
collection,
|
|
72
|
+
id,
|
|
73
|
+
ownerId: record.ownerId,
|
|
74
|
+
data: { ...record.data },
|
|
75
|
+
createdAt: record.createdAt,
|
|
76
|
+
updatedAt: record.updatedAt,
|
|
77
|
+
deletedAt: record.deletedAt,
|
|
78
|
+
version: record.version
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async persistRecord(collection, id) {
|
|
83
|
+
if (!this.node || !this.shouldPersist(collection)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const serialized = this.serializeRecord(collection, id);
|
|
88
|
+
const key = this.recordKey(collection, id);
|
|
89
|
+
|
|
90
|
+
if (!serialized) {
|
|
91
|
+
await this.runTransaction('readwrite', (store) => new Promise((resolve, reject) => {
|
|
92
|
+
const request = store.delete(key);
|
|
93
|
+
request.onsuccess = () => resolve();
|
|
94
|
+
request.onerror = () => reject(request.error);
|
|
95
|
+
}));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await this.runTransaction('readwrite', (store) => new Promise((resolve, reject) => {
|
|
100
|
+
const request = store.put(serialized);
|
|
101
|
+
request.onsuccess = () => resolve();
|
|
102
|
+
request.onerror = () => reject(request.error);
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
persistChange(event) {
|
|
107
|
+
if (!event || !event.collection || !event.id) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.persistRecord(event.collection, event.id).catch((error) => {
|
|
112
|
+
this.node.emit('warning', {
|
|
113
|
+
type: 'persistence-failed',
|
|
114
|
+
collection: event.collection,
|
|
115
|
+
id: event.id,
|
|
116
|
+
error
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async loadAllRecords() {
|
|
122
|
+
return this.runTransaction('readonly', (store) => new Promise((resolve, reject) => {
|
|
123
|
+
const request = store.getAll();
|
|
124
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
125
|
+
request.onerror = () => reject(request.error);
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async hydrate() {
|
|
130
|
+
if (!this.node) {
|
|
131
|
+
throw new Error('IndexedDBPersistence requires an attached node before hydrate');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const storedRecords = await this.loadAllRecords();
|
|
135
|
+
for (const stored of storedRecords) {
|
|
136
|
+
if (!this.shouldPersist(stored.collection)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.node.restoreRecord(stored.collection, {
|
|
141
|
+
id: stored.id,
|
|
142
|
+
ownerId: stored.ownerId,
|
|
143
|
+
data: stored.data,
|
|
144
|
+
createdAt: stored.createdAt,
|
|
145
|
+
updatedAt: stored.updatedAt,
|
|
146
|
+
deletedAt: stored.deletedAt,
|
|
147
|
+
version: stored.version
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async attach(node) {
|
|
153
|
+
if (!node) {
|
|
154
|
+
throw new Error('IndexedDBPersistence.attach requires a DignityP2P node');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.node = node;
|
|
158
|
+
await this.hydrate();
|
|
159
|
+
|
|
160
|
+
this.changeHandler = (event) => this.persistChange(event);
|
|
161
|
+
node.on('change', this.changeHandler);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async detach() {
|
|
165
|
+
if (this.node && this.changeHandler) {
|
|
166
|
+
this.node.off('change', this.changeHandler);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.changeHandler = null;
|
|
170
|
+
this.node = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async clear() {
|
|
174
|
+
await this.runTransaction('readwrite', (store) => new Promise((resolve, reject) => {
|
|
175
|
+
const request = store.clear();
|
|
176
|
+
request.onsuccess = () => resolve();
|
|
177
|
+
request.onerror = () => reject(request.error);
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = IndexedDBPersistence;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { useCallback, useEffect, useState } = require('react');
|
|
2
|
+
const DignityP2P = require('../core/dignity-p2p');
|
|
3
|
+
|
|
4
|
+
function useDignity(config) {
|
|
5
|
+
const [node, setNode] = useState(null);
|
|
6
|
+
const [status, setStatus] = useState('idle');
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!config) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
const instance = new DignityP2P(config);
|
|
16
|
+
setNode(instance);
|
|
17
|
+
setStatus('starting');
|
|
18
|
+
setError(null);
|
|
19
|
+
|
|
20
|
+
instance.start()
|
|
21
|
+
.then(() => {
|
|
22
|
+
if (!cancelled) {
|
|
23
|
+
setStatus('running');
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.catch((startError) => {
|
|
27
|
+
if (!cancelled) {
|
|
28
|
+
setError(startError);
|
|
29
|
+
setStatus('error');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
instance.stop()
|
|
36
|
+
.catch(() => undefined)
|
|
37
|
+
.finally(() => {
|
|
38
|
+
setStatus('stopped');
|
|
39
|
+
setNode(null);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}, [config]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
node,
|
|
46
|
+
status,
|
|
47
|
+
error
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function useCollection(node, collectionName) {
|
|
52
|
+
const [records, setRecords] = useState([]);
|
|
53
|
+
|
|
54
|
+
const refresh = useCallback(() => {
|
|
55
|
+
if (!node || !collectionName) {
|
|
56
|
+
setRecords([]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setRecords(node.list(collectionName));
|
|
61
|
+
}, [node, collectionName]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
refresh();
|
|
65
|
+
|
|
66
|
+
if (!node) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
node.on('change', refresh);
|
|
71
|
+
return () => node.off('change', refresh);
|
|
72
|
+
}, [node, refresh]);
|
|
73
|
+
|
|
74
|
+
return records;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function usePeers(node, scope = 'main', options = {}) {
|
|
78
|
+
const includeSelf = options.includeSelf !== false;
|
|
79
|
+
const [peers, setPeers] = useState([]);
|
|
80
|
+
|
|
81
|
+
const refresh = useCallback(() => {
|
|
82
|
+
if (!node) {
|
|
83
|
+
setPeers([]);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setPeers(node.listPeers(scope, { includeSelf }));
|
|
88
|
+
}, [node, scope, includeSelf]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
refresh();
|
|
92
|
+
|
|
93
|
+
if (!node) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handlePresenceChange = () => refresh();
|
|
98
|
+
node.on('peerdiscovered', handlePresenceChange);
|
|
99
|
+
node.on('peerleft', handlePresenceChange);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
node.off('peerdiscovered', handlePresenceChange);
|
|
103
|
+
node.off('peerleft', handlePresenceChange);
|
|
104
|
+
};
|
|
105
|
+
}, [node, refresh]);
|
|
106
|
+
|
|
107
|
+
return peers;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
useDignity,
|
|
112
|
+
useCollection,
|
|
113
|
+
usePeers
|
|
114
|
+
};
|