holosphere 2.0.0-alpha4 → 2.0.0-alpha6

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 (35) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/index-BtKHqqet.cjs +5 -0
  4. package/dist/index-BtKHqqet.cjs.map +1 -0
  5. package/dist/{index-CBitK71M.cjs → index-CmzkI7SI.cjs} +2 -2
  6. package/dist/{index-CBitK71M.cjs.map → index-CmzkI7SI.cjs.map} +1 -1
  7. package/dist/{index-Cz-PLCUR.js → index-JFz-dW43.js} +2 -2
  8. package/dist/{index-Cz-PLCUR.js.map → index-JFz-dW43.js.map} +1 -1
  9. package/dist/{index-CV0eOogK.js → index-NOravBLu.js} +733 -164
  10. package/dist/index-NOravBLu.js.map +1 -0
  11. package/dist/{indexeddb-storage-CRsZyB2f.cjs → indexeddb-storage-C4HsulhA.cjs} +2 -2
  12. package/dist/{indexeddb-storage-CRsZyB2f.cjs.map → indexeddb-storage-C4HsulhA.cjs.map} +1 -1
  13. package/dist/{indexeddb-storage-DZaGlY_a.js → indexeddb-storage-OtSAVDZY.js} +2 -2
  14. package/dist/{indexeddb-storage-DZaGlY_a.js.map → indexeddb-storage-OtSAVDZY.js.map} +1 -1
  15. package/dist/{memory-storage-BkUi6sZG.js → memory-storage-ChpcYvxA.js} +2 -2
  16. package/dist/{memory-storage-BkUi6sZG.js.map → memory-storage-ChpcYvxA.js.map} +1 -1
  17. package/dist/{memory-storage-C0DuUsdY.cjs → memory-storage-MD6ED00P.cjs} +2 -2
  18. package/dist/{memory-storage-C0DuUsdY.cjs.map → memory-storage-MD6ED00P.cjs.map} +1 -1
  19. package/dist/{secp256k1-0kPdAVkK.cjs → secp256k1-DcTYQrqC.cjs} +2 -2
  20. package/dist/{secp256k1-0kPdAVkK.cjs.map → secp256k1-DcTYQrqC.cjs.map} +1 -1
  21. package/dist/{secp256k1-DN4FVXcv.js → secp256k1-PfNOEI7a.js} +2 -2
  22. package/dist/{secp256k1-DN4FVXcv.js.map → secp256k1-PfNOEI7a.js.map} +1 -1
  23. package/package.json +1 -1
  24. package/src/contracts/abis/Bundle.json +1438 -1435
  25. package/src/contracts/deployer.js +32 -3
  26. package/src/federation/handshake.js +13 -5
  27. package/src/index.js +9 -1
  28. package/src/storage/gun-async.js +55 -6
  29. package/src/storage/gun-auth.js +81 -30
  30. package/src/storage/gun-wrapper.js +56 -48
  31. package/src/storage/nostr-async.js +149 -14
  32. package/src/storage/nostr-client.js +574 -48
  33. package/dist/index-BB_vVJgv.cjs +0 -5
  34. package/dist/index-BB_vVJgv.cjs.map +0 -1
  35. package/dist/index-CV0eOogK.js.map +0 -1
@@ -5,10 +5,23 @@
5
5
 
6
6
  /**
7
7
  * Global subscription manager to prevent duplicate subscriptions
8
- * Maps: pathPrefix -> subscription object
8
+ * Maps: subscriptionKey -> subscription object
9
9
  */
10
10
  const globalSubscriptions = new Map();
11
11
 
12
+ /**
13
+ * Single-path subscription manager (for nostrSubscribe)
14
+ * Maps: subscriptionKey -> { subscription, callbacks: [] }
15
+ */
16
+ const singlePathSubscriptions = new Map();
17
+
18
+ /**
19
+ * Query deduplication for nostrGet - prevents duplicate relay queries
20
+ * Maps: queryKey -> { promise, timestamp, callbacks: [] }
21
+ */
22
+ const pendingQueries = new Map();
23
+ const QUERY_DEDUP_WINDOW = 2000; // 2 second window for deduplication
24
+
12
25
  /**
13
26
  * Write data as Nostr event (parameterized replaceable event)
14
27
  * @param {Object} client - NostrClient instance
@@ -34,6 +47,7 @@ export async function nostrPut(client, path, data, kind = 30000) {
34
47
  /**
35
48
  * Read data from Nostr (query by d-tag)
36
49
  * LOCAL-FIRST: Checks persistent storage first, never blocks on network
50
+ * Uses query deduplication to prevent duplicate relay queries within a time window
37
51
  * @param {Object} client - NostrClient instance
38
52
  * @param {string} path - Path identifier
39
53
  * @param {number} kind - Event kind (default: 30000)
@@ -77,7 +91,42 @@ export async function nostrGet(client, path, kind = 30000, options = {}) {
77
91
  }
78
92
  }
79
93
 
80
- // Fallback to relay query (existing logic)
94
+ // Query deduplication: Check if same query is already pending
95
+ const queryKey = `get:${client.publicKey}:${kind}:${path}:${authors.join(',')}`;
96
+ const pending = pendingQueries.get(queryKey);
97
+
98
+ if (pending && Date.now() - pending.timestamp < QUERY_DEDUP_WINDOW) {
99
+ // Reuse pending query result
100
+ return pending.promise;
101
+ }
102
+
103
+ // Create new query with deduplication
104
+ const queryPromise = _executeNostrGet(client, path, kind, authors, timeout, options);
105
+
106
+ pendingQueries.set(queryKey, {
107
+ promise: queryPromise,
108
+ timestamp: Date.now(),
109
+ });
110
+
111
+ // Clean up after promise resolves
112
+ queryPromise.finally(() => {
113
+ // Remove from pending after a short delay to allow coalescing
114
+ setTimeout(() => {
115
+ const current = pendingQueries.get(queryKey);
116
+ if (current && current.promise === queryPromise) {
117
+ pendingQueries.delete(queryKey);
118
+ }
119
+ }, QUERY_DEDUP_WINDOW);
120
+ });
121
+
122
+ return queryPromise;
123
+ }
124
+
125
+ /**
126
+ * Internal function to execute nostrGet query
127
+ * @private
128
+ */
129
+ async function _executeNostrGet(client, path, kind, authors, timeout, options) {
81
130
  const filter = {
82
131
  kinds: [kind],
83
132
  authors: authors, // Support multiple authors for cross-holosphere queries
@@ -114,6 +163,7 @@ export async function nostrGet(client, path, kind = 30000, options = {}) {
114
163
  /**
115
164
  * Query all events under a path prefix
116
165
  * LOCAL-FIRST: Checks persistent storage first, never blocks on network
166
+ * Uses query deduplication to prevent duplicate relay queries within a time window
117
167
  * @param {Object} client - NostrClient instance
118
168
  * @param {string} pathPrefix - Path prefix to match
119
169
  * @param {number} kind - Event kind (default: 30000)
@@ -174,7 +224,41 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
174
224
  }
175
225
  }
176
226
 
177
- // Fallback to relay query (existing logic)
227
+ // Query deduplication: Check if same query is already pending
228
+ const queryKey = `getAll:${client.publicKey}:${kind}:${pathPrefix}:${authors.join(',')}`;
229
+ const pending = pendingQueries.get(queryKey);
230
+
231
+ if (pending && Date.now() - pending.timestamp < QUERY_DEDUP_WINDOW) {
232
+ // Reuse pending query result
233
+ return pending.promise;
234
+ }
235
+
236
+ // Create new query with deduplication
237
+ const queryPromise = _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, limit, options);
238
+
239
+ pendingQueries.set(queryKey, {
240
+ promise: queryPromise,
241
+ timestamp: Date.now(),
242
+ });
243
+
244
+ // Clean up after promise resolves
245
+ queryPromise.finally(() => {
246
+ setTimeout(() => {
247
+ const current = pendingQueries.get(queryKey);
248
+ if (current && current.promise === queryPromise) {
249
+ pendingQueries.delete(queryKey);
250
+ }
251
+ }, QUERY_DEDUP_WINDOW);
252
+ });
253
+
254
+ return queryPromise;
255
+ }
256
+
257
+ /**
258
+ * Internal function to execute nostrGetAll query
259
+ * @private
260
+ */
261
+ async function _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, limit, options) {
178
262
  const filter = {
179
263
  kinds: [kind],
180
264
  authors: authors,
@@ -468,6 +552,7 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
468
552
 
469
553
  /**
470
554
  * Subscribe to path changes
555
+ * Uses subscription deduplication - multiple subscribers to same path share one relay subscription
471
556
  * @param {Object} client - NostrClient instance
472
557
  * @param {string} path - Path to subscribe to
473
558
  * @param {Function} callback - Callback function (data, event) => void
@@ -476,7 +561,42 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
476
561
  */
477
562
  export function nostrSubscribe(client, path, callback, options = {}) {
478
563
  const kind = options.kind || 30000;
479
- const includeInitial = options.includeInitial !== false;
564
+
565
+ // Create unique key for this subscription
566
+ const subscriptionKey = `single:${client.publicKey}:${kind}:${path}`;
567
+
568
+ // Check if we already have an active subscription for this path
569
+ const existing = singlePathSubscriptions.get(subscriptionKey);
570
+ if (existing) {
571
+ // Add callback to existing subscription
572
+ existing.callbacks.push(callback);
573
+
574
+ // Return wrapper that removes only this callback on unsubscribe
575
+ return {
576
+ unsubscribe: () => {
577
+ const idx = existing.callbacks.indexOf(callback);
578
+ if (idx > -1) {
579
+ existing.callbacks.splice(idx, 1);
580
+ }
581
+
582
+ // If no more callbacks, unsubscribe the whole thing
583
+ if (existing.callbacks.length === 0) {
584
+ existing.subscription.unsubscribe();
585
+ singlePathSubscriptions.delete(subscriptionKey);
586
+ }
587
+ }
588
+ };
589
+ }
590
+
591
+ // Create new subscription
592
+ const callbacks = [callback];
593
+ const subscriptionInfo = {
594
+ callbacks,
595
+ subscription: null,
596
+ };
597
+
598
+ // Store before creating subscription to handle race conditions
599
+ singlePathSubscriptions.set(subscriptionKey, subscriptionInfo);
480
600
 
481
601
  const filter = {
482
602
  kinds: [kind],
@@ -485,18 +605,11 @@ export function nostrSubscribe(client, path, callback, options = {}) {
485
605
  limit: 10, // Limit results for single item subscription
486
606
  };
487
607
 
488
- let initialLoadComplete = false;
489
-
490
608
  const subscription = client.subscribe(
491
609
  filter,
492
610
  (event) => {
493
611
  // Verify event is from our public key (relay may not respect author filter)
494
612
  if (event.pubkey !== client.publicKey) {
495
- console.warn('[nostrSubscribe] Rejecting event from different author:', {
496
- expected: client.publicKey,
497
- received: event.pubkey,
498
- eventId: event.id
499
- });
500
613
  return;
501
614
  }
502
615
 
@@ -508,19 +621,41 @@ export function nostrSubscribe(client, path, callback, options = {}) {
508
621
  return;
509
622
  }
510
623
 
511
- callback(data, event);
624
+ // Dispatch to all registered callbacks
625
+ for (const cb of callbacks) {
626
+ try {
627
+ cb(data, event);
628
+ } catch (err) {
629
+ console.error('Subscription callback error:', err);
630
+ }
631
+ }
512
632
  } catch (error) {
513
633
  console.error('Failed to parse event in subscription:', error);
514
634
  }
515
635
  },
516
636
  {
517
637
  onEOSE: () => {
518
- initialLoadComplete = true;
638
+ // EOSE received
519
639
  },
520
640
  }
521
641
  );
522
642
 
523
- return subscription;
643
+ subscriptionInfo.subscription = subscription;
644
+
645
+ return {
646
+ unsubscribe: () => {
647
+ const idx = callbacks.indexOf(callback);
648
+ if (idx > -1) {
649
+ callbacks.splice(idx, 1);
650
+ }
651
+
652
+ // If no more callbacks, unsubscribe the whole thing
653
+ if (callbacks.length === 0) {
654
+ subscription.unsubscribe();
655
+ singlePathSubscriptions.delete(subscriptionKey);
656
+ }
657
+ }
658
+ };
524
659
  }
525
660
 
526
661
  /**