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.
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +1 -1
- package/dist/index-BtKHqqet.cjs +5 -0
- package/dist/index-BtKHqqet.cjs.map +1 -0
- package/dist/{index-CBitK71M.cjs → index-CmzkI7SI.cjs} +2 -2
- package/dist/{index-CBitK71M.cjs.map → index-CmzkI7SI.cjs.map} +1 -1
- package/dist/{index-Cz-PLCUR.js → index-JFz-dW43.js} +2 -2
- package/dist/{index-Cz-PLCUR.js.map → index-JFz-dW43.js.map} +1 -1
- package/dist/{index-CV0eOogK.js → index-NOravBLu.js} +733 -164
- package/dist/index-NOravBLu.js.map +1 -0
- package/dist/{indexeddb-storage-CRsZyB2f.cjs → indexeddb-storage-C4HsulhA.cjs} +2 -2
- package/dist/{indexeddb-storage-CRsZyB2f.cjs.map → indexeddb-storage-C4HsulhA.cjs.map} +1 -1
- package/dist/{indexeddb-storage-DZaGlY_a.js → indexeddb-storage-OtSAVDZY.js} +2 -2
- package/dist/{indexeddb-storage-DZaGlY_a.js.map → indexeddb-storage-OtSAVDZY.js.map} +1 -1
- package/dist/{memory-storage-BkUi6sZG.js → memory-storage-ChpcYvxA.js} +2 -2
- package/dist/{memory-storage-BkUi6sZG.js.map → memory-storage-ChpcYvxA.js.map} +1 -1
- package/dist/{memory-storage-C0DuUsdY.cjs → memory-storage-MD6ED00P.cjs} +2 -2
- package/dist/{memory-storage-C0DuUsdY.cjs.map → memory-storage-MD6ED00P.cjs.map} +1 -1
- package/dist/{secp256k1-0kPdAVkK.cjs → secp256k1-DcTYQrqC.cjs} +2 -2
- package/dist/{secp256k1-0kPdAVkK.cjs.map → secp256k1-DcTYQrqC.cjs.map} +1 -1
- package/dist/{secp256k1-DN4FVXcv.js → secp256k1-PfNOEI7a.js} +2 -2
- package/dist/{secp256k1-DN4FVXcv.js.map → secp256k1-PfNOEI7a.js.map} +1 -1
- package/package.json +1 -1
- package/src/contracts/abis/Bundle.json +1438 -1435
- package/src/contracts/deployer.js +32 -3
- package/src/federation/handshake.js +13 -5
- package/src/index.js +9 -1
- package/src/storage/gun-async.js +55 -6
- package/src/storage/gun-auth.js +81 -30
- package/src/storage/gun-wrapper.js +56 -48
- package/src/storage/nostr-async.js +149 -14
- package/src/storage/nostr-client.js +574 -48
- package/dist/index-BB_vVJgv.cjs +0 -5
- package/dist/index-BB_vVJgv.cjs.map +0 -1
- 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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
+
// EOSE received
|
|
519
639
|
},
|
|
520
640
|
}
|
|
521
641
|
);
|
|
522
642
|
|
|
523
|
-
|
|
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
|
/**
|