holosphere 2.0.0-alpha21 → 2.0.0-alpha23

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.
Files changed (69) hide show
  1. package/README.md +1 -2
  2. package/dist/cjs/holosphere.cjs +1 -1
  3. package/dist/esm/holosphere.js +61 -58
  4. package/dist/{index-B6-8KAQm.js → index-BEkCLOwI.js} +2 -2
  5. package/dist/{index-B6-8KAQm.js.map → index-BEkCLOwI.js.map} +1 -1
  6. package/dist/{index-D2WstuZJ.js → index-BEvX6DxG.js} +2 -2
  7. package/dist/{index-D2WstuZJ.js.map → index-BEvX6DxG.js.map} +1 -1
  8. package/dist/{index--QsHG_gD.cjs → index-BGTOiJ2Y.cjs} +2 -2
  9. package/dist/{index--QsHG_gD.cjs.map → index-BGTOiJ2Y.cjs.map} +1 -1
  10. package/dist/{index-COpLk9gL.cjs → index-BH1woZXL.cjs} +2 -2
  11. package/dist/{index-COpLk9gL.cjs.map → index-BH1woZXL.cjs.map} +1 -1
  12. package/dist/{index-BHptWysv.js → index-Cvxov2jv.js} +2970 -7753
  13. package/dist/index-Cvxov2jv.js.map +1 -0
  14. package/dist/index-vTKI_BAX.cjs +29 -0
  15. package/dist/index-vTKI_BAX.cjs.map +1 -0
  16. package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-BmnCNnSg.cjs} +2 -2
  17. package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-BmnCNnSg.cjs.map} +1 -1
  18. package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-MIFisaPy.js} +2 -2
  19. package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-MIFisaPy.js.map} +1 -1
  20. package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-BJjK3F4r.cjs} +2 -2
  21. package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-BJjK3F4r.cjs.map} +1 -1
  22. package/dist/{memory-storage-DnXCSbBl.js → memory-storage-DhHXdKQ-.js} +2 -2
  23. package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-DhHXdKQ-.js.map} +1 -1
  24. package/examples/demo.html +2 -29
  25. package/package.json +3 -8
  26. package/src/content/social-protocols.js +3 -59
  27. package/src/core/holosphere.js +16 -554
  28. package/src/crypto/nostr-utils.js +98 -1
  29. package/src/crypto/secp256k1.js +4 -393
  30. package/src/federation/discovery.js +7 -75
  31. package/src/federation/handshake.js +69 -202
  32. package/src/federation/hologram.js +222 -298
  33. package/src/federation/index.js +2 -9
  34. package/src/federation/registry.js +67 -1257
  35. package/src/federation/request-card.js +21 -35
  36. package/src/hierarchical/upcast.js +4 -9
  37. package/src/index.js +145 -296
  38. package/src/lib/federation-methods.js +370 -909
  39. package/src/storage/global-tables.js +1 -1
  40. package/src/storage/nostr-wrapper.js +9 -5
  41. package/src/subscriptions/manager.js +1 -1
  42. package/types/index.d.ts +145 -37
  43. package/bin/holosphere-activitypub.js +0 -158
  44. package/dist/2019-BzVkRcax.js +0 -6680
  45. package/dist/2019-BzVkRcax.js.map +0 -1
  46. package/dist/2019-C1hPR_Os.cjs +0 -8
  47. package/dist/2019-C1hPR_Os.cjs.map +0 -1
  48. package/dist/browser-BcmACE3G.js +0 -3058
  49. package/dist/browser-BcmACE3G.js.map +0 -1
  50. package/dist/browser-DaqYUTcG.cjs +0 -2
  51. package/dist/browser-DaqYUTcG.cjs.map +0 -1
  52. package/dist/index-BHptWysv.js.map +0 -1
  53. package/dist/index-CDlhzxT2.cjs +0 -29
  54. package/dist/index-CDlhzxT2.cjs.map +0 -1
  55. package/src/federation/capabilities.js +0 -46
  56. package/src/storage/backend-factory.js +0 -130
  57. package/src/storage/backend-interface.js +0 -161
  58. package/src/storage/backends/activitypub/server.js +0 -675
  59. package/src/storage/backends/activitypub-backend.js +0 -295
  60. package/src/storage/backends/gundb-backend.js +0 -875
  61. package/src/storage/backends/nostr-backend.js +0 -251
  62. package/src/storage/gun-async.js +0 -341
  63. package/src/storage/gun-auth.js +0 -373
  64. package/src/storage/gun-federation.js +0 -785
  65. package/src/storage/gun-references.js +0 -209
  66. package/src/storage/gun-schema.js +0 -306
  67. package/src/storage/gun-wrapper.js +0 -642
  68. package/src/storage/migration.js +0 -351
  69. package/src/storage/unified-storage.js +0 -161
@@ -1,875 +0,0 @@
1
- /**
2
- * @fileoverview GunDB storage backend with authentication, schema validation, references, and global tables.
3
- * Provides full-featured P2P data storage using GunDB with write caching for immediate consistency.
4
- * @module storage/backends/gundb-backend
5
- */
6
-
7
- import { StorageBackend } from '../backend-interface.js';
8
- import * as wrapper from '../gun-wrapper.js';
9
- import { gunPromise, gunPut, gunMap, gunCollect } from '../gun-async.js';
10
- import { GunReferenceHandler } from '../gun-references.js';
11
- import { GunAuth } from '../gun-auth.js';
12
- import { GunSchemaValidator } from '../gun-schema.js';
13
-
14
- /**
15
- * GunDB storage backend implementation with advanced features.
16
- * Supports authentication, schema validation, references, global tables,
17
- * and write caching for immediate consistency.
18
- *
19
- * @class GunDBBackend
20
- * @extends StorageBackend
21
- * @example
22
- * const backend = await BackendFactory.create('gundb', {
23
- * appName: 'myapp',
24
- * peers: ['wss://gun-server.example.com/gun'],
25
- * radisk: true
26
- * });
27
- */
28
- export class GunDBBackend extends StorageBackend {
29
- /**
30
- * Create a new GunDB backend instance.
31
- * @param {Object} config - Backend configuration
32
- * @param {string} [config.appName='holosphere'] - Application namespace
33
- * @param {string[]} [config.peers=[]] - Array of peer URLs
34
- * @param {boolean} [config.radisk=true] - Enable radisk persistence
35
- * @param {boolean} [config.localStorage=true] - Enable localStorage
36
- * @param {string} [config.dataDir='radata'] - Data directory for Node.js
37
- * @param {string} [config.privateKey] - Optional existing private key
38
- * @param {string} [config.publicKey] - Optional existing public key
39
- * @param {boolean} [config.strict=false] - Enable strict schema validation
40
- */
41
- constructor(config) {
42
- super(config);
43
- this.gun = null;
44
- this.keyPair = null;
45
- this.appName = config.appName || 'holosphere';
46
-
47
- // New modules
48
- this.references = null;
49
- this.auth = null;
50
- this.schemaValidator = null;
51
-
52
- // Subscription tracking
53
- this.subscriptions = new Map();
54
- this.subscriptionCounter = 0;
55
-
56
- // Write cache for immediate consistency
57
- // Gun's readAll/map().once() doesn't immediately see new writes - this cache bridges that gap
58
- this.writeCache = new Map(); // path -> Map(id -> {data, timestamp})
59
- this.writeCacheTTL = 300000; // Cache entries expire after 5 minutes (increased from 30s)
60
-
61
- // Pending writes queue for retry
62
- this.pendingWrites = new Map(); // path -> {data, retries, lastAttempt}
63
- this.maxWriteRetries = 5;
64
- this.writeRetryInterval = 10000; // 10 seconds between retries
65
- }
66
-
67
- async init() {
68
- // Dynamically import Gun to avoid issues if not installed
69
- let Gun;
70
- try {
71
- console.log('[gundb-backend] Importing Gun...');
72
- const gunModule = await import('gun');
73
- Gun = gunModule.default || gunModule;
74
- console.log('[gundb-backend] Gun imported:', typeof Gun);
75
- } catch (error) {
76
- throw new Error(
77
- 'GunDB backend requires the "gun" package. Install it with: npm install gun'
78
- );
79
- }
80
-
81
- // In Node.js, Gun needs a file path for radisk persistence
82
- // Default to 'radata' if not specified
83
- const dataDir = this.config.dataDir || 'radata';
84
-
85
- const gunConfig = {
86
- peers: this.config.peers || [],
87
- radisk: this.config.radisk !== false,
88
- localStorage: this.config.localStorage !== false,
89
- file: dataDir,
90
- };
91
-
92
- console.log('[gundb-backend] Gun config:', JSON.stringify(gunConfig));
93
-
94
- this.gun = Gun(gunConfig);
95
- console.log('[gundb-backend] Gun instance created:', !!this.gun);
96
-
97
- // Test basic Gun functionality
98
- try {
99
- const testKey = `_test_${Date.now()}`;
100
- this.gun.get(testKey).put({ test: true }, (ack) => {
101
- console.log('[gundb-backend] Gun test write ack:', ack);
102
- });
103
- console.log('[gundb-backend] Gun test write initiated');
104
- } catch (e) {
105
- console.error('[gundb-backend] Gun test write failed:', e.message);
106
- }
107
-
108
- // Add a small delay to let Gun initialize
109
- await new Promise(resolve => setTimeout(resolve, 500));
110
-
111
- // Generate or use provided key pair using Gun's SEA
112
- try {
113
- const SEA = Gun.SEA;
114
- if (SEA) {
115
- if (this.config.privateKey) {
116
- // Try to restore from existing key
117
- this.keyPair = { priv: this.config.privateKey, pub: this.config.publicKey };
118
- this.publicKey = this.config.publicKey || this.config.privateKey.substring(0, 32);
119
- } else {
120
- // Generate new key pair
121
- this.keyPair = await SEA.pair();
122
- this.publicKey = this.keyPair.pub;
123
- }
124
- } else {
125
- // SEA not available, use simple identifier
126
- this.publicKey = this.appName;
127
- }
128
- } catch (error) {
129
- // SEA might not be loaded
130
- this.publicKey = this.appName;
131
- }
132
-
133
- // Initialize modules
134
- this.references = new GunReferenceHandler(this.appName);
135
- this.auth = new GunAuth(this.gun, this.appName);
136
- this.schemaValidator = new GunSchemaValidator({
137
- strict: this.config.strict || false,
138
- cacheMaxAge: this.config.schemaCacheMaxAge || 3600000
139
- });
140
-
141
- // Try to initialize schema validator (may fail if AJV not installed)
142
- await this.schemaValidator.init();
143
- }
144
-
145
- // ============================================================================
146
- // WRITE CACHE (for immediate consistency)
147
- // ============================================================================
148
-
149
- /**
150
- * Add item to the local write cache for immediate consistency
151
- * @private
152
- */
153
- _addToWriteCache(path, data) {
154
- if (!data || !data.id) return;
155
-
156
- // Extract the parent path (without the data id)
157
- const parentPath = path.substring(0, path.lastIndexOf('/'));
158
- if (!parentPath) return;
159
-
160
- if (!this.writeCache.has(parentPath)) {
161
- this.writeCache.set(parentPath, new Map());
162
- }
163
-
164
- const pathCache = this.writeCache.get(parentPath);
165
- pathCache.set(data.id.toString(), {
166
- data: data,
167
- timestamp: Date.now()
168
- });
169
- }
170
-
171
- /**
172
- * Remove item from write cache (e.g., on delete)
173
- * @private
174
- */
175
- _removeFromWriteCache(path, id) {
176
- // Extract the parent path
177
- const parentPath = path.substring(0, path.lastIndexOf('/'));
178
- const pathCache = this.writeCache.get(parentPath);
179
- if (pathCache) {
180
- pathCache.delete(id.toString());
181
- }
182
- }
183
-
184
- /**
185
- * Get cached writes for a path, filtering out expired entries
186
- * @private
187
- */
188
- _getWriteCacheEntries(path) {
189
- const pathCache = this.writeCache.get(path);
190
- if (!pathCache) return [];
191
-
192
- const now = Date.now();
193
- const validEntries = [];
194
-
195
- for (const [id, entry] of pathCache) {
196
- if (now - entry.timestamp < this.writeCacheTTL) {
197
- validEntries.push(entry.data);
198
- } else {
199
- // Clean up expired entries
200
- pathCache.delete(id);
201
- }
202
- }
203
-
204
- return validEntries;
205
- }
206
-
207
- /**
208
- * Merge cached writes with results from Gun
209
- * @private
210
- */
211
- _mergeWithWriteCache(path, gunResults) {
212
- const cachedWrites = this._getWriteCacheEntries(path);
213
- if (cachedWrites.length === 0) return gunResults;
214
-
215
- // Create a map by ID for deduplication
216
- const resultMap = new Map();
217
-
218
- // Add Gun results first
219
- for (const item of gunResults) {
220
- if (item && item.id) {
221
- resultMap.set(item.id.toString(), item);
222
- }
223
- }
224
-
225
- // Overlay cached writes (they're more recent)
226
- for (const item of cachedWrites) {
227
- if (item && item.id && !item._deleted) {
228
- resultMap.set(item.id.toString(), item);
229
- } else if (item && item.id && item._deleted) {
230
- // Remove deleted items
231
- resultMap.delete(item.id.toString());
232
- }
233
- }
234
-
235
- return Array.from(resultMap.values());
236
- }
237
-
238
- /**
239
- * Clear write cache for a specific path or all paths
240
- * @param {string} [path] - Optional path to clear, clears all if not provided
241
- */
242
- clearWriteCache(path = null) {
243
- if (path) {
244
- this.writeCache.delete(path);
245
- } else {
246
- this.writeCache.clear();
247
- }
248
- }
249
-
250
- // ============================================================================
251
- // PATH BUILDING
252
- // ============================================================================
253
-
254
- buildPath(appName, holonId, lensName, key = null) {
255
- return wrapper.buildPath(appName, holonId, lensName, key);
256
- }
257
-
258
- buildGlobalPath(tableName, key = null) {
259
- return wrapper.buildGlobalPath(this.appName, tableName, key);
260
- }
261
-
262
- // ============================================================================
263
- // BASIC CRUD OPERATIONS
264
- // ============================================================================
265
-
266
- async write(path, data, options = {}) {
267
- // Validate against schema if strict mode
268
- if (this.schemaValidator && this.schemaValidator.isStrict()) {
269
- const pathParts = path.split('/');
270
- if (pathParts.length >= 3) {
271
- const lens = pathParts[2];
272
- // Skip validation for references
273
- if (!this.isReference(data)) {
274
- const result = await this.schemaValidator.validateData(
275
- this.gun, this.appName, lens, data
276
- );
277
- if (!result.valid) {
278
- throw new Error(`Schema validation failed: ${JSON.stringify(result.errors)}`);
279
- }
280
- }
281
- }
282
- }
283
-
284
- // Add to write cache FIRST for immediate consistency
285
- // This ensures readAll returns the new data even if Gun write is slow/times out
286
- this._addToWriteCache(path, data);
287
-
288
- // Try to write to Gun with retry on timeout
289
- const result = await this._writeWithRetry(path, data);
290
-
291
- return result;
292
- }
293
-
294
- /**
295
- * Write to Gun with automatic retry on timeout
296
- * @private
297
- */
298
- async _writeWithRetry(path, data, attempt = 0) {
299
- const result = await wrapper.write(this.gun, path, data);
300
-
301
- if (result.timeout && attempt < this.maxWriteRetries) {
302
- // Queue for background retry
303
- this.pendingWrites.set(path, {
304
- data,
305
- retries: attempt + 1,
306
- lastAttempt: Date.now()
307
- });
308
-
309
- // Schedule retry
310
- this._scheduleRetry(path);
311
- } else if (!result.timeout) {
312
- // Success - remove from pending
313
- this.pendingWrites.delete(path);
314
- }
315
-
316
- return result;
317
- }
318
-
319
- /**
320
- * Schedule a background retry for a pending write
321
- * @private
322
- */
323
- _scheduleRetry(path) {
324
- setTimeout(async () => {
325
- const pending = this.pendingWrites.get(path);
326
- if (!pending) return;
327
-
328
- if (pending.retries >= this.maxWriteRetries) {
329
- console.warn(`[gundb-backend] Max retries reached for: ${path}`);
330
- this.pendingWrites.delete(path);
331
- return;
332
- }
333
-
334
- console.log(`[gundb-backend] Retrying write (attempt ${pending.retries + 1}): ${path}`);
335
- await this._writeWithRetry(path, pending.data, pending.retries);
336
- }, this.writeRetryInterval);
337
- }
338
-
339
- async read(path, options = {}) {
340
- const data = await wrapper.read(this.gun, path);
341
-
342
- // Resolve references if requested
343
- if (data && options.resolveReferences && this.isReference(data)) {
344
- return this.resolveReference(data);
345
- }
346
-
347
- return data;
348
- }
349
-
350
- async readAll(path, options = {}) {
351
- const gunItems = await wrapper.readAll(this.gun, path);
352
-
353
- // Merge with write cache for immediate consistency
354
- const items = this._mergeWithWriteCache(path, gunItems);
355
-
356
- // Resolve references if requested
357
- if (options.resolveReferences) {
358
- const resolved = [];
359
- for (const item of items) {
360
- if (this.isReference(item)) {
361
- const resolvedItem = await this.resolveReference(item);
362
- if (resolvedItem) {
363
- resolved.push(resolvedItem);
364
- }
365
- } else {
366
- resolved.push(item);
367
- }
368
- }
369
- return resolved;
370
- }
371
-
372
- return items;
373
- }
374
-
375
- async update(path, updates) {
376
- return wrapper.update(this.gun, path, updates);
377
- }
378
-
379
- async delete(path) {
380
- const result = await wrapper.deleteData(this.gun, path);
381
-
382
- // Extract id from path and mark as deleted in cache
383
- const pathParts = path.split('/');
384
- const id = pathParts[pathParts.length - 1];
385
- if (id) {
386
- // Add tombstone to cache so readAll doesn't return this item
387
- const parentPath = path.substring(0, path.lastIndexOf('/'));
388
- if (!this.writeCache.has(parentPath)) {
389
- this.writeCache.set(parentPath, new Map());
390
- }
391
- this.writeCache.get(parentPath).set(id.toString(), {
392
- data: { id, _deleted: true },
393
- timestamp: Date.now()
394
- });
395
- }
396
-
397
- return result;
398
- }
399
-
400
- async deleteAll(path) {
401
- const items = await this.readAll(path);
402
- let count = 0;
403
-
404
- for (const item of items) {
405
- if (item && item.id) {
406
- const itemPath = `${path}/${item.id}`;
407
- await wrapper.deleteData(this.gun, itemPath);
408
- count++;
409
- }
410
- }
411
-
412
- return { success: true, count };
413
- }
414
-
415
- // ============================================================================
416
- // GLOBAL TABLE OPERATIONS
417
- // ============================================================================
418
-
419
- async writeGlobal(tableName, data) {
420
- // Add to write cache FIRST for immediate consistency
421
- // This ensures readAllGlobal returns the new data even if Gun write is slow/times out
422
- const path = this.buildGlobalPath(tableName, data.id);
423
- this._addToWriteCache(path, data);
424
-
425
- // Try to write to Gun with retry on timeout
426
- const result = await this._writeGlobalWithRetry(tableName, data);
427
-
428
- return result;
429
- }
430
-
431
- /**
432
- * Write to global table with automatic retry on timeout
433
- * @private
434
- */
435
- async _writeGlobalWithRetry(tableName, data, attempt = 0) {
436
- const path = this.buildGlobalPath(tableName, data.id);
437
- const result = await wrapper.writeGlobal(this.gun, this.appName, tableName, data);
438
-
439
- if (result.timeout && attempt < this.maxWriteRetries) {
440
- // Queue for background retry
441
- this.pendingWrites.set(path, {
442
- data: { tableName, data },
443
- isGlobal: true,
444
- retries: attempt + 1,
445
- lastAttempt: Date.now()
446
- });
447
-
448
- // Schedule retry
449
- this._scheduleGlobalRetry(path, tableName);
450
- } else if (!result.timeout) {
451
- // Success - remove from pending
452
- this.pendingWrites.delete(path);
453
- }
454
-
455
- return result;
456
- }
457
-
458
- /**
459
- * Schedule a background retry for a pending global write
460
- * @private
461
- */
462
- _scheduleGlobalRetry(path, tableName) {
463
- setTimeout(async () => {
464
- const pending = this.pendingWrites.get(path);
465
- if (!pending || !pending.isGlobal) return;
466
-
467
- if (pending.retries >= this.maxWriteRetries) {
468
- console.warn(`[gundb-backend] Max retries reached for global: ${path}`);
469
- this.pendingWrites.delete(path);
470
- return;
471
- }
472
-
473
- console.log(`[gundb-backend] Retrying global write (attempt ${pending.retries + 1}): ${path}`);
474
- await this._writeGlobalWithRetry(tableName, pending.data.data, pending.retries);
475
- }, this.writeRetryInterval);
476
- }
477
-
478
- async readGlobal(tableName, key) {
479
- return wrapper.readGlobal(this.gun, this.appName, tableName, key);
480
- }
481
-
482
- async readAllGlobal(tableName, timeout = 5000) {
483
- const gunItems = await wrapper.readAllGlobal(this.gun, this.appName, tableName, timeout);
484
-
485
- // Merge with write cache for immediate consistency
486
- const path = this.buildGlobalPath(tableName);
487
- return this._mergeWithWriteCache(path, gunItems);
488
- }
489
-
490
- async deleteGlobal(tableName, key) {
491
- const result = await wrapper.deleteGlobal(this.gun, this.appName, tableName, key);
492
-
493
- // Mark as deleted in cache
494
- const path = this.buildGlobalPath(tableName);
495
- if (!this.writeCache.has(path)) {
496
- this.writeCache.set(path, new Map());
497
- }
498
- this.writeCache.get(path).set(key.toString(), {
499
- data: { id: key, _deleted: true },
500
- timestamp: Date.now()
501
- });
502
-
503
- return result;
504
- }
505
-
506
- async deleteAllGlobal(tableName) {
507
- return wrapper.deleteAllGlobal(this.gun, this.appName, tableName);
508
- }
509
-
510
- // ============================================================================
511
- // REFERENCE OPERATIONS
512
- // ============================================================================
513
-
514
- /**
515
- * Create a reference to data
516
- */
517
- createReference(holon, lens, data) {
518
- return this.references.createReference(holon, lens, data);
519
- }
520
-
521
- /**
522
- * Check if data is a reference
523
- */
524
- isReference(data) {
525
- return this.references.isReference(data);
526
- }
527
-
528
- /**
529
- * Resolve a reference to get actual data
530
- */
531
- async resolveReference(reference, options = {}) {
532
- return this.references.resolveReference(this.gun, reference, options);
533
- }
534
-
535
- /**
536
- * Parse a soul path
537
- */
538
- parseSoulPath(soul) {
539
- return this.references.parseSoulPath(soul);
540
- }
541
-
542
- /**
543
- * Get soul for data
544
- */
545
- getSoul(holon, lens, key) {
546
- return this.references.getSoul(holon, lens, key);
547
- }
548
-
549
- // ============================================================================
550
- // AUTHENTICATION OPERATIONS
551
- // ============================================================================
552
-
553
- /**
554
- * Authenticate for a holon
555
- */
556
- async authenticate(holonId, password) {
557
- return this.auth.authenticate(holonId, password);
558
- }
559
-
560
- /**
561
- * Create a new user
562
- */
563
- async createUser(holonId, password) {
564
- return this.auth.createUser(holonId, password);
565
- }
566
-
567
- /**
568
- * Logout current user
569
- */
570
- logout() {
571
- this.auth.logout();
572
- }
573
-
574
- /**
575
- * Check if authenticated
576
- */
577
- isAuthenticated() {
578
- return this.auth.isAuthenticated();
579
- }
580
-
581
- /**
582
- * Write to private storage
583
- */
584
- async writePrivate(lens, key, data) {
585
- return this.auth.writePrivate(lens, key, data);
586
- }
587
-
588
- /**
589
- * Read from private storage
590
- */
591
- async readPrivate(lens, key) {
592
- return this.auth.readPrivate(lens, key);
593
- }
594
-
595
- /**
596
- * Read all from private lens
597
- */
598
- async readAllPrivate(lens, timeout = 2000) {
599
- return this.auth.readAllPrivate(lens, timeout);
600
- }
601
-
602
- /**
603
- * Delete from private storage
604
- */
605
- async deletePrivate(lens, key) {
606
- return this.auth.deletePrivate(lens, key);
607
- }
608
-
609
- // ============================================================================
610
- // SCHEMA OPERATIONS
611
- // ============================================================================
612
-
613
- /**
614
- * Set a schema for a lens
615
- */
616
- async setSchema(lens, schema) {
617
- return this.schemaValidator.setSchema(this.gun, this.appName, lens, schema);
618
- }
619
-
620
- /**
621
- * Get a schema for a lens
622
- */
623
- async getSchema(lens, options = {}) {
624
- return this.schemaValidator.getSchema(this.gun, this.appName, lens, options);
625
- }
626
-
627
- /**
628
- * Get all schemas
629
- */
630
- async getAllSchemas() {
631
- return this.schemaValidator.getAllSchemas(this.gun, this.appName);
632
- }
633
-
634
- /**
635
- * Delete a schema
636
- */
637
- async deleteSchema(lens) {
638
- return this.schemaValidator.deleteSchema(this.gun, this.appName, lens);
639
- }
640
-
641
- /**
642
- * Validate data against a schema
643
- */
644
- validate(schema, data) {
645
- return this.schemaValidator.validate(schema, data);
646
- }
647
-
648
- /**
649
- * Validate data against a lens schema
650
- */
651
- async validateData(lens, data) {
652
- return this.schemaValidator.validateData(this.gun, this.appName, lens, data);
653
- }
654
-
655
- /**
656
- * Clear schema cache
657
- */
658
- clearSchemaCache(lens = null) {
659
- this.schemaValidator.clearCache(lens);
660
- }
661
-
662
- // ============================================================================
663
- // SUBSCRIPTIONS
664
- // ============================================================================
665
-
666
- async subscribe(path, callback, options = {}) {
667
- const subscriptionId = ++this.subscriptionCounter;
668
- const pathParts = path.split('/');
669
- const isPrefix = pathParts.length <= 3;
670
-
671
- const subscription = {
672
- id: subscriptionId,
673
- path,
674
- active: true,
675
- gunRef: null
676
- };
677
-
678
- if (isPrefix) {
679
- const ref = this.gun.get(path);
680
- subscription.gunRef = ref;
681
-
682
- ref.map().on(async (data, key) => {
683
- if (!this.subscriptions.get(subscriptionId)?.active) return;
684
- if (!data || key.startsWith('_') || data._deleted) return;
685
-
686
- // Parse data
687
- let parsed = wrapper.parse(data);
688
- if (!parsed) return;
689
-
690
- // Resolve references if requested
691
- if (options.resolveReferences && this.isReference(parsed)) {
692
- parsed = await this.resolveReference(parsed);
693
- }
694
-
695
- if (parsed) {
696
- callback(parsed, key);
697
- }
698
- });
699
- } else {
700
- const ref = this.gun.get(path);
701
- subscription.gunRef = ref;
702
-
703
- ref.on(async (data, key) => {
704
- if (!this.subscriptions.get(subscriptionId)?.active) return;
705
- if (!data) return;
706
-
707
- let parsed = wrapper.parse(data);
708
- if (!parsed || parsed._deleted) return;
709
-
710
- if (options.resolveReferences && this.isReference(parsed)) {
711
- parsed = await this.resolveReference(parsed);
712
- }
713
-
714
- if (parsed) {
715
- callback(parsed, key);
716
- }
717
- });
718
- }
719
-
720
- this.subscriptions.set(subscriptionId, subscription);
721
-
722
- return {
723
- id: subscriptionId,
724
- unsubscribe: () => {
725
- const sub = this.subscriptions.get(subscriptionId);
726
- if (sub) {
727
- sub.active = false;
728
- if (sub.gunRef) {
729
- try {
730
- sub.gunRef.off();
731
- } catch (e) {}
732
- }
733
- this.subscriptions.delete(subscriptionId);
734
- }
735
- }
736
- };
737
- }
738
-
739
- // ============================================================================
740
- // EXPORT/IMPORT
741
- // ============================================================================
742
-
743
- async exportData(pathPrefix = '') {
744
- const records = [];
745
- const basePath = pathPrefix || this.appName;
746
-
747
- try {
748
- const items = await gunMap(this.gun.get(basePath), 2000);
749
-
750
- for (const [key, data] of Object.entries(items)) {
751
- if (!key.startsWith('_') && data && typeof data === 'object' && !data._deleted) {
752
- await this._collectRecords(basePath, key, data, records);
753
- }
754
- }
755
- } catch (error) {
756
- console.warn('Export data error:', error);
757
- }
758
-
759
- return records;
760
- }
761
-
762
- async _collectRecords(basePath, key, data, records) {
763
- const currentPath = `${basePath}/${key}`;
764
-
765
- if (data.id) {
766
- records.push({
767
- path: currentPath,
768
- data: this._cleanGunData(data),
769
- timestamp: data._meta?.timestamp || Date.now(),
770
- });
771
- } else {
772
- try {
773
- const nested = await gunMap(this.gun.get(currentPath), 500);
774
- for (const [nestedKey, nestedData] of Object.entries(nested)) {
775
- if (!nestedKey.startsWith('_') && nestedData) {
776
- await this._collectRecords(currentPath, nestedKey, nestedData, records);
777
- }
778
- }
779
- } catch (e) {}
780
- }
781
- }
782
-
783
- _cleanGunData(data) {
784
- const cleaned = { ...data };
785
- delete cleaned['_'];
786
- return cleaned;
787
- }
788
-
789
- async importData(records, options = {}) {
790
- const results = { success: 0, failed: 0, errors: [] };
791
-
792
- for (const record of records) {
793
- try {
794
- await wrapper.write(this.gun, record.path, record.data);
795
- results.success++;
796
- } catch (error) {
797
- results.failed++;
798
- results.errors.push({ path: record.path, error: error.message });
799
- }
800
- }
801
-
802
- return results;
803
- }
804
-
805
- // ============================================================================
806
- // LIFECYCLE
807
- // ============================================================================
808
-
809
- close() {
810
- // 1. Unsubscribe all active subscriptions
811
- for (const [id, subscription] of this.subscriptions) {
812
- if (subscription.active) {
813
- subscription.active = false;
814
- if (subscription.gunRef) {
815
- try {
816
- subscription.gunRef.off();
817
- } catch (e) {}
818
- }
819
- }
820
- }
821
- this.subscriptions.clear();
822
-
823
- // 2. Clear schema cache
824
- if (this.schemaValidator) {
825
- this.schemaValidator.clearCache();
826
- }
827
-
828
- // 3. Clear write cache
829
- this.writeCache.clear();
830
-
831
- // 4. Logout auth
832
- if (this.auth) {
833
- this.auth.logout();
834
- }
835
-
836
- // 5. Clean up Gun connections
837
- if (this.gun) {
838
- try {
839
- // Clean up mesh connections
840
- const mesh = this.gun.back('opt.mesh');
841
- if (mesh) {
842
- if (mesh.way) {
843
- Object.values(mesh.way).forEach(conn => {
844
- if (conn?.wire?.close) conn.wire.close();
845
- });
846
- }
847
- if (mesh.opt?.peers) mesh.opt.peers = {};
848
- }
849
-
850
- // Close web server if present
851
- const server = this.gun.back('opt.web');
852
- if (server?.close) server.close();
853
-
854
- // Turn off all listeners
855
- this.gun.off();
856
- } catch (error) {
857
- console.warn('Error during Gun cleanup:', error);
858
- }
859
- }
860
- }
861
-
862
- getStatus() {
863
- return {
864
- type: 'gundb',
865
- publicKey: this.publicKey,
866
- peers: this.config.peers || [],
867
- connected: !!this.gun,
868
- authenticated: this.isAuthenticated(),
869
- subscriptionCount: this.subscriptions.size,
870
- schemaValidationEnabled: this.schemaValidator?.isStrict() || false
871
- };
872
- }
873
- }
874
-
875
- export default GunDBBackend;