holosphere 2.0.0-alpha7 → 2.0.0-alpha9
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/CHANGELOG.md +446 -0
- package/FEATURES.md +431 -0
- package/LICENSE +29 -166
- package/LICENSE-AGPL.md +180 -0
- package/dist/cdn/holosphere.min.js +55 -0
- package/dist/cdn/holosphere.min.js.map +1 -0
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +1 -1
- package/dist/{index-C-IlLYlk.cjs → index-DDGt_V9o.cjs} +2 -2
- package/dist/{index-C-IlLYlk.cjs.map → index-DDGt_V9o.cjs.map} +1 -1
- package/dist/{index-d6f4RJBM.js → index-DJXftyvB.js} +2253 -387
- package/dist/index-DJXftyvB.js.map +1 -0
- package/dist/index-DMbdcMtK.cjs +18 -0
- package/dist/index-DMbdcMtK.cjs.map +1 -0
- package/dist/{index-jmTHEbR2.js → index-DeZ1xz_s.js} +2 -2
- package/dist/{index-jmTHEbR2.js.map → index-DeZ1xz_s.js.map} +1 -1
- package/dist/{indexeddb-storage-D8kOl0oK.js → indexeddb-storage-BFt6hMeF.js} +48 -4
- package/dist/indexeddb-storage-BFt6hMeF.js.map +1 -0
- package/dist/{indexeddb-storage-a8GipaDr.cjs → indexeddb-storage-BK5tv4Sh.cjs} +2 -2
- package/dist/indexeddb-storage-BK5tv4Sh.cjs.map +1 -0
- package/dist/{memory-storage-DBQK622V.js → memory-storage-C9HuoL2E.js} +44 -4
- package/dist/memory-storage-C9HuoL2E.js.map +1 -0
- package/dist/{memory-storage-gfRovk2O.cjs → memory-storage-Dao7jfYG.cjs} +2 -2
- package/dist/memory-storage-Dao7jfYG.cjs.map +1 -0
- package/dist/{secp256k1-BCAPF45D.cjs → secp256k1-BbKzbLtD.cjs} +2 -2
- package/dist/{secp256k1-BCAPF45D.cjs.map → secp256k1-BbKzbLtD.cjs.map} +1 -1
- package/dist/{secp256k1-DYm_CMqW.js → secp256k1-CreY7Pcl.js} +2 -2
- package/dist/{secp256k1-DYm_CMqW.js.map → secp256k1-CreY7Pcl.js.map} +1 -1
- package/docs/api/ai_aggregation.js.html +333 -0
- package/docs/api/ai_breakdown.js.html +524 -0
- package/docs/api/ai_classifier.js.html +231 -0
- package/docs/api/ai_council.js.html +246 -0
- package/docs/api/ai_embeddings.js.html +304 -0
- package/docs/api/ai_federation-ai.js.html +338 -0
- package/docs/api/ai_h3-ai.js.html +970 -0
- package/docs/api/ai_index.js.html +124 -0
- package/docs/api/ai_json-ops.js.html +241 -0
- package/docs/api/ai_llm-service.js.html +239 -0
- package/docs/api/ai_nl-query.js.html +236 -0
- package/docs/api/ai_relationships.js.html +367 -0
- package/docs/api/ai_schema-extractor.js.html +235 -0
- package/docs/api/ai_spatial.js.html +307 -0
- package/docs/api/ai_tts.js.html +214 -0
- package/docs/api/content_social-protocols.js.html +180 -0
- package/docs/api/core_holosphere.js.html +757 -0
- package/docs/api/crypto_nostr-utils.js.html +306 -0
- package/docs/api/crypto_secp256k1.js.html +267 -0
- package/docs/api/data/search.json +1 -0
- package/docs/api/federation_discovery.js.html +337 -0
- package/docs/api/federation_handshake.js.html +478 -0
- package/docs/api/federation_hologram.js.html +1053 -0
- package/docs/api/federation_registry.js.html +389 -0
- package/docs/api/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/api/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/api/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/api/global.html +3 -0
- package/docs/api/hierarchical_upcast.js.html +128 -0
- package/docs/api/index.html +265 -0
- package/docs/api/index.js.html +1868 -0
- package/docs/api/lib_ai-methods.js.html +660 -0
- package/docs/api/lib_contract-methods.js.html +445 -0
- package/docs/api/lib_errors.js.html +56 -0
- package/docs/api/lib_federation-methods.js.html +348 -0
- package/docs/api/lib_index.js.html +33 -0
- package/docs/api/module-ai.html +5 -0
- package/docs/api/module-ai_aggregation-SmartAggregation.html +6 -0
- package/docs/api/module-ai_aggregation.SmartAggregation.html +3 -0
- package/docs/api/module-ai_aggregation.html +3 -0
- package/docs/api/module-ai_breakdown-TaskBreakdown.html +5 -0
- package/docs/api/module-ai_breakdown.TaskBreakdown.html +3 -0
- package/docs/api/module-ai_breakdown.html +3 -0
- package/docs/api/module-ai_classifier-Classifier.html +6 -0
- package/docs/api/module-ai_classifier.Classifier.html +3 -0
- package/docs/api/module-ai_classifier.html +3 -0
- package/docs/api/module-ai_council-Council.html +6 -0
- package/docs/api/module-ai_council.Council.html +3 -0
- package/docs/api/module-ai_council.html +3 -0
- package/docs/api/module-ai_embeddings-Embeddings.html +5 -0
- package/docs/api/module-ai_embeddings.Embeddings.html +3 -0
- package/docs/api/module-ai_embeddings.html +3 -0
- package/docs/api/module-ai_federation-ai-FederationAdvisor.html +6 -0
- package/docs/api/module-ai_federation-ai.FederationAdvisor.html +3 -0
- package/docs/api/module-ai_federation-ai.html +3 -0
- package/docs/api/module-ai_h3-ai-H3AI.html +6 -0
- package/docs/api/module-ai_h3-ai.H3AI.html +3 -0
- package/docs/api/module-ai_h3-ai.html +3 -0
- package/docs/api/module-ai_json-ops-JSONOps.html +5 -0
- package/docs/api/module-ai_json-ops.JSONOps.html +3 -0
- package/docs/api/module-ai_json-ops.html +3 -0
- package/docs/api/module-ai_llm-service-LLMService.html +5 -0
- package/docs/api/module-ai_llm-service.LLMService.html +3 -0
- package/docs/api/module-ai_llm-service.html +3 -0
- package/docs/api/module-ai_nl-query-NLQuery.html +5 -0
- package/docs/api/module-ai_nl-query.NLQuery.html +3 -0
- package/docs/api/module-ai_nl-query.html +3 -0
- package/docs/api/module-ai_relationships-RelationshipDiscovery.html +6 -0
- package/docs/api/module-ai_relationships.RelationshipDiscovery.html +3 -0
- package/docs/api/module-ai_relationships.html +3 -0
- package/docs/api/module-ai_schema-extractor-SchemaExtractor.html +5 -0
- package/docs/api/module-ai_schema-extractor.SchemaExtractor.html +3 -0
- package/docs/api/module-ai_schema-extractor.html +3 -0
- package/docs/api/module-ai_spatial-SpatialAnalysis.html +6 -0
- package/docs/api/module-ai_spatial.SpatialAnalysis.html +3 -0
- package/docs/api/module-ai_spatial.html +3 -0
- package/docs/api/module-ai_tts-TTS.html +5 -0
- package/docs/api/module-ai_tts.TTS.html +3 -0
- package/docs/api/module-ai_tts.html +3 -0
- package/docs/api/module-content_social-protocols.html +3 -0
- package/docs/api/module-core_holosphere-HoloSphere.html +6 -0
- package/docs/api/module-core_holosphere.HoloSphere.html +3 -0
- package/docs/api/module-core_holosphere.html +3 -0
- package/docs/api/module-crypto_nostr-utils.html +3 -0
- package/docs/api/module-crypto_secp256k1.html +3 -0
- package/docs/api/module-federation_hologram.html +3 -0
- package/docs/api/module-hierarchical_upcast.html +3 -0
- package/docs/api/module-holosphere-HoloSphereBase.html +3 -0
- package/docs/api/module-holosphere.html +3 -0
- package/docs/api/module-lib_ai-methods.html +3 -0
- package/docs/api/module-lib_contract-methods.html +3 -0
- package/docs/api/module-lib_errors-AuthorizationError.html +3 -0
- package/docs/api/module-lib_errors-ValidationError.html +3 -0
- package/docs/api/module-lib_errors.AuthorizationError.html +3 -0
- package/docs/api/module-lib_errors.ValidationError.html +3 -0
- package/docs/api/module-lib_errors.html +3 -0
- package/docs/api/module-lib_federation-methods.html +3 -0
- package/docs/api/module-lib_index.html +3 -0
- package/docs/api/module-schema_validator-ValidationError.html +3 -0
- package/docs/api/module-schema_validator.ValidationError.html +3 -0
- package/docs/api/module-schema_validator.html +3 -0
- package/docs/api/module-spatial_h3-operations.html +4 -0
- package/docs/api/module-storage_backend-factory.BackendFactory.html +3 -0
- package/docs/api/module-storage_backend-factory.html +3 -0
- package/docs/api/module-storage_backend-interface-StorageBackend.html +3 -0
- package/docs/api/module-storage_backend-interface.StorageBackend.html +3 -0
- package/docs/api/module-storage_backend-interface.html +3 -0
- package/docs/api/module-storage_backends_activitypub-backend-ActivityPubBackend.html +7 -0
- package/docs/api/module-storage_backends_activitypub-backend.ActivityPubBackend.html +3 -0
- package/docs/api/module-storage_backends_activitypub-backend.html +3 -0
- package/docs/api/module-storage_backends_activitypub_server-ActivityPubServer.html +8 -0
- package/docs/api/module-storage_backends_activitypub_server.ActivityPubServer.html +3 -0
- package/docs/api/module-storage_backends_activitypub_server.html +3 -0
- package/docs/api/module-storage_backends_gundb-backend-GunDBBackend.html +7 -0
- package/docs/api/module-storage_backends_gundb-backend.GunDBBackend.html +3 -0
- package/docs/api/module-storage_backends_gundb-backend.html +3 -0
- package/docs/api/module-storage_backends_nostr-backend-NostrBackend.html +8 -0
- package/docs/api/module-storage_backends_nostr-backend.NostrBackend.html +3 -0
- package/docs/api/module-storage_backends_nostr-backend.html +3 -0
- package/docs/api/module-storage_filesystem-storage-FileSystemStorage.html +5 -0
- package/docs/api/module-storage_filesystem-storage-browser-FileSystemStorage.html +3 -0
- package/docs/api/module-storage_filesystem-storage-browser.FileSystemStorage.html +3 -0
- package/docs/api/module-storage_filesystem-storage-browser.html +3 -0
- package/docs/api/module-storage_filesystem-storage.FileSystemStorage.html +3 -0
- package/docs/api/module-storage_filesystem-storage.html +3 -0
- package/docs/api/module-storage_global-tables.html +3 -0
- package/docs/api/module-storage_gun-async.html +3 -0
- package/docs/api/module-storage_gun-auth-GunAuth.html +5 -0
- package/docs/api/module-storage_gun-auth.GunAuth.html +3 -0
- package/docs/api/module-storage_gun-auth.html +3 -0
- package/docs/api/module-storage_gun-federation.html +3 -0
- package/docs/api/module-storage_gun-references-GunReferenceHandler.html +5 -0
- package/docs/api/module-storage_gun-references.GunReferenceHandler.html +3 -0
- package/docs/api/module-storage_gun-references.html +3 -0
- package/docs/api/module-storage_gun-schema-GunSchemaValidator.html +5 -0
- package/docs/api/module-storage_gun-schema.GunSchemaValidator.html +3 -0
- package/docs/api/module-storage_gun-schema.html +3 -0
- package/docs/api/module-storage_gun-wrapper.html +11 -0
- package/docs/api/module-storage_indexeddb-storage-IndexedDBStorage.html +5 -0
- package/docs/api/module-storage_indexeddb-storage.IndexedDBStorage.html +3 -0
- package/docs/api/module-storage_indexeddb-storage.html +3 -0
- package/docs/api/module-storage_key-storage-simple.html +3 -0
- package/docs/api/module-storage_key-storage.html +4 -0
- package/docs/api/module-storage_memory-storage-MemoryStorage.html +5 -0
- package/docs/api/module-storage_memory-storage.MemoryStorage.html +3 -0
- package/docs/api/module-storage_memory-storage.html +3 -0
- package/docs/api/module-storage_migration-MigrationTool.html +6 -0
- package/docs/api/module-storage_migration.MigrationTool.html +3 -0
- package/docs/api/module-storage_migration.html +3 -0
- package/docs/api/module-storage_nostr-async.html +18 -0
- package/docs/api/module-storage_nostr-client-LRUCache.html +3 -0
- package/docs/api/module-storage_nostr-client-NostrClient.html +7 -0
- package/docs/api/module-storage_nostr-client.NostrClient.html +15 -0
- package/docs/api/module-storage_nostr-client.html +6 -0
- package/docs/api/module-storage_nostr-wrapper.html +3 -0
- package/docs/api/module-storage_outbox-queue-OutboxQueue.html +4 -0
- package/docs/api/module-storage_outbox-queue.OutboxQueue.html +3 -0
- package/docs/api/module-storage_outbox-queue.html +3 -0
- package/docs/api/module-storage_persistent-storage-PersistentStorage.html +3 -0
- package/docs/api/module-storage_persistent-storage.html +4 -0
- package/docs/api/module-storage_sync-service-SyncService.html +5 -0
- package/docs/api/module-storage_sync-service.SyncService.html +3 -0
- package/docs/api/module-storage_sync-service.html +3 -0
- package/docs/api/module-storage_unified-storage.html +3 -0
- package/docs/api/module-subscriptions_manager.SubscriptionRegistry.html +3 -0
- package/docs/api/module-subscriptions_manager.html +3 -0
- package/docs/api/schema_validator.js.html +113 -0
- package/docs/api/scripts/core.js +726 -0
- package/docs/api/scripts/core.min.js +23 -0
- package/docs/api/scripts/resize.js +90 -0
- package/docs/api/scripts/search.js +265 -0
- package/docs/api/scripts/search.min.js +6 -0
- package/docs/api/scripts/third-party/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/third-party/fuse.js +9 -0
- package/docs/api/scripts/third-party/hljs-line-num-original.js +369 -0
- package/docs/api/scripts/third-party/hljs-line-num.js +1 -0
- package/docs/api/scripts/third-party/hljs-original.js +5171 -0
- package/docs/api/scripts/third-party/hljs.js +1 -0
- package/docs/api/scripts/third-party/popper.js +5 -0
- package/docs/api/scripts/third-party/tippy.js +1 -0
- package/docs/api/scripts/third-party/tocbot.js +672 -0
- package/docs/api/scripts/third-party/tocbot.min.js +1 -0
- package/docs/api/spatial_h3-operations.js.html +129 -0
- package/docs/api/storage_backend-factory.js.html +133 -0
- package/docs/api/storage_backend-interface.js.html +164 -0
- package/docs/api/storage_backends_activitypub-backend.js.html +298 -0
- package/docs/api/storage_backends_activitypub_server.js.html +678 -0
- package/docs/api/storage_backends_gundb-backend.js.html +878 -0
- package/docs/api/storage_backends_nostr-backend.js.html +254 -0
- package/docs/api/storage_filesystem-storage-browser.js.html +83 -0
- package/docs/api/storage_filesystem-storage.js.html +207 -0
- package/docs/api/storage_global-tables.js.html +116 -0
- package/docs/api/storage_gun-async.js.html +344 -0
- package/docs/api/storage_gun-auth.js.html +376 -0
- package/docs/api/storage_gun-federation.js.html +788 -0
- package/docs/api/storage_gun-references.js.html +212 -0
- package/docs/api/storage_gun-schema.js.html +309 -0
- package/docs/api/storage_gun-wrapper.js.html +645 -0
- package/docs/api/storage_indexeddb-storage.js.html +224 -0
- package/docs/api/storage_key-storage-simple.js.html +102 -0
- package/docs/api/storage_key-storage.js.html +171 -0
- package/docs/api/storage_memory-storage.js.html +128 -0
- package/docs/api/storage_migration.js.html +354 -0
- package/docs/api/storage_nostr-async.js.html +1076 -0
- package/docs/api/storage_nostr-client.js.html +1598 -0
- package/docs/api/storage_nostr-wrapper.js.html +218 -0
- package/docs/api/storage_outbox-queue.js.html +248 -0
- package/docs/api/storage_persistent-storage.js.html +160 -0
- package/docs/api/storage_sync-service.js.html +201 -0
- package/docs/api/storage_unified-storage.js.html +157 -0
- package/docs/api/styles/clean-jsdoc-theme-base.css +1159 -0
- package/docs/api/styles/clean-jsdoc-theme-dark.css +412 -0
- package/docs/api/styles/clean-jsdoc-theme-light.css +482 -0
- package/docs/api/styles/clean-jsdoc-theme-scrollbar.css +30 -0
- package/docs/api/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
- package/docs/api/styles/clean-jsdoc-theme.min.css +1 -0
- package/docs/api/subscriptions_manager.js.html +162 -0
- package/examples/holosphere-widget.js +1242 -0
- package/examples/widget-demo.html +274 -0
- package/examples/widget.html +703 -0
- package/jsdoc.json +26 -0
- package/package.json +16 -3
- package/src/ai/aggregation.js +13 -2
- package/src/ai/breakdown.js +12 -2
- package/src/ai/classifier.js +14 -3
- package/src/ai/council.js +22 -7
- package/src/ai/embeddings.js +37 -15
- package/src/ai/federation-ai.js +13 -2
- package/src/ai/h3-ai.js +14 -2
- package/src/ai/index.js +16 -7
- package/src/ai/json-ops.js +18 -5
- package/src/ai/llm-service.js +62 -31
- package/src/ai/nl-query.js +12 -2
- package/src/ai/relationships.js +13 -2
- package/src/ai/schema-extractor.js +24 -10
- package/src/ai/spatial.js +13 -2
- package/src/ai/tts.js +25 -8
- package/src/cdn-entry.js +22 -0
- package/src/content/social-protocols.js +34 -25
- package/src/contracts/chain-manager.js +68 -40
- package/src/contracts/deployer.js +70 -42
- package/src/contracts/event-listener.js +61 -29
- package/src/contracts/holon-contracts.js +46 -31
- package/src/contracts/index.js +5 -6
- package/src/contracts/networks.js +19 -14
- package/src/contracts/operations.js +58 -41
- package/src/contracts/queries.js +70 -21
- package/src/core/holosphere.js +37 -8
- package/src/crypto/nostr-utils.js +105 -65
- package/src/crypto/secp256k1.js +7 -2
- package/src/federation/handshake.js +23 -11
- package/src/federation/hologram.js +9 -1
- package/src/hierarchical/upcast.js +34 -20
- package/src/index.js +671 -7
- package/src/lib/ai-methods.js +352 -3
- package/src/lib/contract-methods.js +152 -3
- package/src/lib/errors.js +31 -1
- package/src/lib/federation-methods.js +110 -3
- package/src/lib/index.js +9 -5
- package/src/schema/validator.js +22 -3
- package/src/spatial/h3-operations.js +17 -1
- package/src/storage/backend-factory.js +7 -2
- package/src/storage/backend-interface.js +21 -2
- package/src/storage/backends/activitypub/server.js +25 -3
- package/src/storage/backends/activitypub-backend.js +25 -2
- package/src/storage/backends/gundb-backend.js +322 -11
- package/src/storage/backends/nostr-backend.js +116 -1
- package/src/storage/filesystem-storage-browser.js +42 -2
- package/src/storage/filesystem-storage.js +72 -5
- package/src/storage/global-tables.js +7 -2
- package/src/storage/gun-async.js +20 -11
- package/src/storage/gun-auth.js +15 -4
- package/src/storage/gun-federation.js +14 -5
- package/src/storage/gun-references.js +16 -5
- package/src/storage/gun-schema.js +25 -10
- package/src/storage/gun-wrapper.js +160 -49
- package/src/storage/indexeddb-storage.js +65 -4
- package/src/storage/key-storage-simple.js +32 -9
- package/src/storage/key-storage.js +45 -13
- package/src/storage/memory-storage.js +65 -4
- package/src/storage/migration.js +20 -7
- package/src/storage/nostr-async.js +195 -90
- package/src/storage/nostr-client.js +173 -49
- package/src/storage/nostr-wrapper.js +6 -2
- package/src/storage/outbox-queue.js +55 -18
- package/src/storage/persistent-storage.js +56 -13
- package/src/storage/sync-service.js +51 -17
- package/src/storage/unified-storage.js +38 -3
- package/src/subscriptions/manager.js +33 -16
- package/vite.config.cdn.js +60 -0
- package/dist/index-Bvwyvd0T.cjs +0 -5
- package/dist/index-Bvwyvd0T.cjs.map +0 -1
- package/dist/index-d6f4RJBM.js.map +0 -1
- package/dist/indexeddb-storage-D8kOl0oK.js.map +0 -1
- package/dist/indexeddb-storage-a8GipaDr.cjs.map +0 -1
- package/dist/memory-storage-DBQK622V.js.map +0 -1
- package/dist/memory-storage-gfRovk2O.cjs.map +0 -1
- /package/{cleanup-test-data.js → scripts/cleanup-test-data.js} +0 -0
- /package/{test-ai-real-api.js → scripts/test-ai-real-api.js} +0 -0
|
@@ -0,0 +1,1598 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: storage/nostr-client.js</title><!--[if lt IE 9]>
|
|
2
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
3
|
+
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-ai.html">ai</a></div><div class="sidebar-section-children"><a href="module-ai_aggregation.html">ai/aggregation</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown.html">ai/breakdown</a></div><div class="sidebar-section-children"><a href="module-ai_classifier.html">ai/classifier</a></div><div class="sidebar-section-children"><a href="module-ai_council.html">ai/council</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings.html">ai/embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai.html">ai/federation-ai</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai.html">ai/h3-ai</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops.html">ai/json-ops</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service.html">ai/llm-service</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query.html">ai/nl-query</a></div><div class="sidebar-section-children"><a href="module-ai_relationships.html">ai/relationships</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor.html">ai/schema-extractor</a></div><div class="sidebar-section-children"><a href="module-ai_spatial.html">ai/spatial</a></div><div class="sidebar-section-children"><a href="module-ai_tts.html">ai/tts</a></div><div class="sidebar-section-children"><a href="module-content_social-protocols.html">content/social-protocols</a></div><div class="sidebar-section-children"><a href="module-core_holosphere.html">core/holosphere</a></div><div class="sidebar-section-children"><a href="module-crypto_nostr-utils.html">crypto/nostr-utils</a></div><div class="sidebar-section-children"><a href="module-crypto_secp256k1.html">crypto/secp256k1</a></div><div class="sidebar-section-children"><a href="module-federation_hologram.html">federation/hologram</a></div><div class="sidebar-section-children"><a href="module-hierarchical_upcast.html">hierarchical/upcast</a></div><div class="sidebar-section-children"><a href="module-holosphere.html">holosphere</a></div><div class="sidebar-section-children"><a href="module-lib_ai-methods.html">lib/ai-methods</a></div><div class="sidebar-section-children"><a href="module-lib_contract-methods.html">lib/contract-methods</a></div><div class="sidebar-section-children"><a href="module-lib_errors.html">lib/errors</a></div><div class="sidebar-section-children"><a href="module-lib_federation-methods.html">lib/federation-methods</a></div><div class="sidebar-section-children"><a href="module-lib_index.html">lib/index</a></div><div class="sidebar-section-children"><a href="module-schema_validator.html">schema/validator</a></div><div class="sidebar-section-children"><a href="module-spatial_h3-operations.html">spatial/h3-operations</a></div><div class="sidebar-section-children"><a href="module-storage_backend-factory.html">storage/backend-factory</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface.html">storage/backend-interface</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend.html">storage/backends/activitypub-backend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server.html">storage/backends/activitypub/server</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend.html">storage/backends/gundb-backend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend.html">storage/backends/nostr-backend</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage.html">storage/filesystem-storage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser.html">storage/filesystem-storage-browser</a></div><div class="sidebar-section-children"><a href="module-storage_global-tables.html">storage/global-tables</a></div><div class="sidebar-section-children"><a href="module-storage_gun-async.html">storage/gun-async</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth.html">storage/gun-auth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-federation.html">storage/gun-federation</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references.html">storage/gun-references</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema.html">storage/gun-schema</a></div><div class="sidebar-section-children"><a href="module-storage_gun-wrapper.html">storage/gun-wrapper</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage.html">storage/indexeddb-storage</a></div><div class="sidebar-section-children"><a href="module-storage_key-storage.html">storage/key-storage</a></div><div class="sidebar-section-children"><a href="module-storage_key-storage-simple.html">storage/key-storage-simple</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage.html">storage/memory-storage</a></div><div class="sidebar-section-children"><a href="module-storage_migration.html">storage/migration</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-async.html">storage/nostr-async</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client.html">storage/nostr-client</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-wrapper.html">storage/nostr-wrapper</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue.html">storage/outbox-queue</a></div><div class="sidebar-section-children"><a href="module-storage_persistent-storage.html">storage/persistent-storage</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service.html">storage/sync-service</a></div><div class="sidebar-section-children"><a href="module-storage_unified-storage.html">storage/unified-storage</a></div><div class="sidebar-section-children"><a href="module-subscriptions_manager.html">subscriptions/manager</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-ai_aggregation.SmartAggregation.html">SmartAggregation</a></div><div class="sidebar-section-children"><a href="module-ai_aggregation-SmartAggregation.html">SmartAggregation</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown.TaskBreakdown.html">TaskBreakdown</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown-TaskBreakdown.html">TaskBreakdown</a></div><div class="sidebar-section-children"><a href="module-ai_classifier.Classifier.html">Classifier</a></div><div class="sidebar-section-children"><a href="module-ai_classifier-Classifier.html">Classifier</a></div><div class="sidebar-section-children"><a href="module-ai_council.Council.html">Council</a></div><div class="sidebar-section-children"><a href="module-ai_council-Council.html">Council</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings.Embeddings.html">Embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings-Embeddings.html">Embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai.FederationAdvisor.html">FederationAdvisor</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai-FederationAdvisor.html">FederationAdvisor</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai.H3AI.html">H3AI</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai-H3AI.html">H3AI</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops.JSONOps.html">JSONOps</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops-JSONOps.html">JSONOps</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service.LLMService.html">LLMService</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service-LLMService.html">LLMService</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query.NLQuery.html">NLQuery</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query-NLQuery.html">NLQuery</a></div><div class="sidebar-section-children"><a href="module-ai_relationships.RelationshipDiscovery.html">RelationshipDiscovery</a></div><div class="sidebar-section-children"><a href="module-ai_relationships-RelationshipDiscovery.html">RelationshipDiscovery</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor.SchemaExtractor.html">SchemaExtractor</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor-SchemaExtractor.html">SchemaExtractor</a></div><div class="sidebar-section-children"><a href="module-ai_spatial.SpatialAnalysis.html">SpatialAnalysis</a></div><div class="sidebar-section-children"><a href="module-ai_spatial-SpatialAnalysis.html">SpatialAnalysis</a></div><div class="sidebar-section-children"><a href="module-ai_tts.TTS.html">TTS</a></div><div class="sidebar-section-children"><a href="module-ai_tts-TTS.html">TTS</a></div><div class="sidebar-section-children"><a href="module-core_holosphere.HoloSphere.html">HoloSphere</a></div><div class="sidebar-section-children"><a href="module-core_holosphere-HoloSphere.html">HoloSphere</a></div><div class="sidebar-section-children"><a href="module-holosphere-HoloSphereBase.html">HoloSphereBase</a></div><div class="sidebar-section-children"><a href="module-holosphere-HoloSphereBase.html">HoloSphereBase</a></div><div class="sidebar-section-children"><a href="module-lib_errors.AuthorizationError.html">AuthorizationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors.ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors-AuthorizationError.html">AuthorizationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors-ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-schema_validator.ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-schema_validator-ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-storage_backend-factory.BackendFactory.html">BackendFactory</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface.StorageBackend.html">StorageBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface-StorageBackend.html">StorageBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend.ActivityPubBackend.html">ActivityPubBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend-ActivityPubBackend.html">ActivityPubBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server.ActivityPubServer.html">ActivityPubServer</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server-ActivityPubServer.html">ActivityPubServer</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend.GunDBBackend.html">GunDBBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend-GunDBBackend.html">GunDBBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend.NostrBackend.html">NostrBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend-NostrBackend.html">NostrBackend</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser.FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser-FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage.FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth.GunAuth.html">GunAuth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth-GunAuth.html">GunAuth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references.GunReferenceHandler.html">GunReferenceHandler</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references-GunReferenceHandler.html">GunReferenceHandler</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema.GunSchemaValidator.html">GunSchemaValidator</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema-GunSchemaValidator.html">GunSchemaValidator</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage.IndexedDBStorage.html">IndexedDBStorage</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage-IndexedDBStorage.html">IndexedDBStorage</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage.MemoryStorage.html">MemoryStorage</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage-MemoryStorage.html">MemoryStorage</a></div><div class="sidebar-section-children"><a href="module-storage_migration.MigrationTool.html">MigrationTool</a></div><div class="sidebar-section-children"><a href="module-storage_migration-MigrationTool.html">MigrationTool</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client.NostrClient.html">NostrClient</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client-LRUCache.html">LRUCache</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client-NostrClient.html">NostrClient</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue.OutboxQueue.html">OutboxQueue</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue-OutboxQueue.html">OutboxQueue</a></div><div class="sidebar-section-children"><a href="module-storage_persistent-storage-PersistentStorage.html">PersistentStorage</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service.SyncService.html">SyncService</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service-SyncService.html">SyncService</a></div><div class="sidebar-section-children"><a href="module-subscriptions_manager.SubscriptionRegistry.html">SubscriptionRegistry</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#FederationRequestPayload">FederationRequestPayload</a></div><div class="sidebar-section-children"><a href="global.html#FederationResponsePayload">FederationResponsePayload</a></div><div class="sidebar-section-children"><a href="global.html#acceptFederationRequest">acceptFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#acceptFederationRequest">acceptFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#addFederatedPartner">addFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#cleanupExpiredCapabilities">cleanupExpiredCapabilities</a></div><div class="sidebar-section-children"><a href="global.html#createFederationRequest">createFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#createFederationResponse">createFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#declineFederationRequest">declineFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#getCapabilityForAuthor">getCapabilityForAuthor</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedAuthors">getFederatedAuthors</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedAuthorsForScope">getFederatedAuthorsForScope</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedPartner">getFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#getFederationRegistry">getFederationRegistry</a></div><div class="sidebar-section-children"><a href="global.html#getPendingFederationRequests">getPendingFederationRequests</a></div><div class="sidebar-section-children"><a href="global.html#initiateFederationHandshake">initiateFederationHandshake</a></div><div class="sidebar-section-children"><a href="global.html#isFederationRequest">isFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#isFederationResponse">isFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#rejectFederationRequest">rejectFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#removeFederatedPartner">removeFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#revokeOutboundCapability">revokeOutboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#saveFederationRegistry">saveFederationRegistry</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationRequest">sendFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationRequest">sendFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationResponse">sendFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#storeInboundCapability">storeInboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#storeOutboundCapability">storeOutboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationAcceptances">subscribeFederationAcceptances</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationDeclines">subscribeFederationDeclines</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationRequests">subscribeFederationRequests</a></div><div class="sidebar-section-children"><a href="global.html#subscribeToFederationDMs">subscribeToFederationDMs</a></div><div class="sidebar-section-children"><a href="global.html#updateDiscoverySettings">updateDiscoverySettings</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">storage_nostr-client.js</h1></header><article><pre class="prettyprint source lang-js"><code>/**
|
|
4
|
+
* @fileoverview Nostr Client - SimplePool wrapper for relay management.
|
|
5
|
+
*
|
|
6
|
+
* Provides connection pooling, relay management, event caching, persistent storage,
|
|
7
|
+
* outbox queue for guaranteed delivery, and background sync services.
|
|
8
|
+
*
|
|
9
|
+
* @module storage/nostr-client
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools';
|
|
13
|
+
import { OutboxQueue } from './outbox-queue.js';
|
|
14
|
+
import { SyncService } from './sync-service.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Global pool singleton - reuse connections across NostrClient instances.
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
let globalPool = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set of relays used by the global pool.
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
26
|
+
let globalPoolRelays = new Set();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get or create global SimplePool singleton.
|
|
30
|
+
* This ensures WebSocket connections are reused across all operations.
|
|
31
|
+
*
|
|
32
|
+
* @private
|
|
33
|
+
* @param {Object} [config={}] - Pool configuration
|
|
34
|
+
* @param {boolean} [config.enableReconnect=true] - Enable auto-reconnect
|
|
35
|
+
* @param {boolean} [config.enablePing=true] - Enable ping/pong
|
|
36
|
+
* @returns {SimplePool} The global SimplePool instance
|
|
37
|
+
*/
|
|
38
|
+
function getGlobalPool(config = {}) {
|
|
39
|
+
if (!globalPool) {
|
|
40
|
+
globalPool = new SimplePool({
|
|
41
|
+
enableReconnect: config.enableReconnect !== false,
|
|
42
|
+
enablePing: config.enablePing !== false,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return globalPool;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Global pending queries map for deduplication.
|
|
50
|
+
* Key: JSON-stringified filter, Value: { promise, timestamp }
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
const pendingQueries = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pending query timeout (5 seconds).
|
|
57
|
+
* @private
|
|
58
|
+
* @constant {number}
|
|
59
|
+
*/
|
|
60
|
+
const PENDING_QUERY_TIMEOUT = 5000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Global active subscriptions map for subscription deduplication.
|
|
64
|
+
* Key: JSON-stringified filter, Value: { subscription, callbacks: Set, refCount }
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
const activeSubscriptions = new Map();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Throttle background refreshes to avoid flooding the relay.
|
|
71
|
+
* Key: path, Value: timestamp of last refresh
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
const backgroundRefreshThrottle = new Map();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Background refresh interval (30 seconds).
|
|
78
|
+
* @private
|
|
79
|
+
* @constant {number}
|
|
80
|
+
*/
|
|
81
|
+
const BACKGROUND_REFRESH_INTERVAL = 30000;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Write debouncing for rapid updates to the same path.
|
|
85
|
+
* Key: d-tag path, Value: { event, timer, resolve, reject }
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
const pendingWrites = new Map();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Write debounce window (500ms).
|
|
92
|
+
* @private
|
|
93
|
+
* @constant {number}
|
|
94
|
+
*/
|
|
95
|
+
const WRITE_DEBOUNCE_MS = 500;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Long-lived subscription manager - keeps ONE subscription per author for real-time updates.
|
|
99
|
+
* Key: pubkey, Value: { subscription, lastEventTime, initialized }
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
const authorSubscriptions = new Map();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Author subscription initialization timeout (5 seconds).
|
|
106
|
+
* @private
|
|
107
|
+
* @constant {number}
|
|
108
|
+
*/
|
|
109
|
+
const AUTHOR_SUB_INIT_TIMEOUT = 5000;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Simple LRU Cache implementation.
|
|
113
|
+
* Automatically evicts least recently used entries when max size is reached.
|
|
114
|
+
*
|
|
115
|
+
* @private
|
|
116
|
+
* @class LRUCache
|
|
117
|
+
*/
|
|
118
|
+
class LRUCache {
|
|
119
|
+
/**
|
|
120
|
+
* Create a new LRU cache.
|
|
121
|
+
*
|
|
122
|
+
* @param {number} [maxSize=500] - Maximum number of entries
|
|
123
|
+
*/
|
|
124
|
+
constructor(maxSize = 500) {
|
|
125
|
+
this.maxSize = maxSize;
|
|
126
|
+
this.cache = new Map();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get an entry from the cache.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} key - Cache key
|
|
133
|
+
* @returns {*} Cached value or undefined
|
|
134
|
+
*/
|
|
135
|
+
get(key) {
|
|
136
|
+
if (!this.cache.has(key)) return undefined;
|
|
137
|
+
|
|
138
|
+
// Move to end (most recently used)
|
|
139
|
+
const value = this.cache.get(key);
|
|
140
|
+
this.cache.delete(key);
|
|
141
|
+
this.cache.set(key, value);
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
set(key, value) {
|
|
146
|
+
// If key exists, delete it first to update position
|
|
147
|
+
if (this.cache.has(key)) {
|
|
148
|
+
this.cache.delete(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.cache.set(key, value);
|
|
152
|
+
|
|
153
|
+
// Evict oldest entries if over capacity
|
|
154
|
+
while (this.cache.size > this.maxSize) {
|
|
155
|
+
const oldestKey = this.cache.keys().next().value;
|
|
156
|
+
this.cache.delete(oldestKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
has(key) {
|
|
161
|
+
return this.cache.has(key);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
delete(key) {
|
|
165
|
+
return this.cache.delete(key);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
clear() {
|
|
169
|
+
this.cache.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get size() {
|
|
173
|
+
return this.cache.size;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
keys() {
|
|
177
|
+
return this.cache.keys();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
values() {
|
|
181
|
+
return this.cache.values();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
entries() {
|
|
185
|
+
return this.cache.entries();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
forEach(callback) {
|
|
189
|
+
this.cache.forEach(callback);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Lazy-load WebSocket polyfill for Node.js environment
|
|
194
|
+
let webSocketPolyfillPromise = null;
|
|
195
|
+
function ensureWebSocket() {
|
|
196
|
+
if (typeof globalThis.WebSocket !== 'undefined') {
|
|
197
|
+
return Promise.resolve();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!webSocketPolyfillPromise) {
|
|
201
|
+
webSocketPolyfillPromise = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const { default: WebSocket } = await import('ws');
|
|
204
|
+
globalThis.WebSocket = WebSocket;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// WebSocket polyfill not available, will use mock pool
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return webSocketPolyfillPromise;
|
|
212
|
+
}
|
|
213
|
+
import { createPersistentStorage } from './persistent-storage.js';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* NostrClient - Manages connections to Nostr relays.
|
|
217
|
+
*
|
|
218
|
+
* Provides event publishing, querying, subscription management, persistent caching,
|
|
219
|
+
* and guaranteed delivery via outbox queue.
|
|
220
|
+
*
|
|
221
|
+
* @class NostrClient
|
|
222
|
+
* @example
|
|
223
|
+
* const client = new NostrClient({
|
|
224
|
+
* relays: ['wss://relay.example.com'],
|
|
225
|
+
* appName: 'myapp',
|
|
226
|
+
* persistence: true
|
|
227
|
+
* });
|
|
228
|
+
*/
|
|
229
|
+
export class NostrClient {
|
|
230
|
+
/**
|
|
231
|
+
* Create a new NostrClient.
|
|
232
|
+
*
|
|
233
|
+
* @param {Object} [config={}] - Configuration options
|
|
234
|
+
* @param {string[]} [config.relays=[]] - Array of relay URLs
|
|
235
|
+
* @param {string} [config.privateKey] - Private key for signing (hex format)
|
|
236
|
+
* @param {boolean} [config.enableReconnect=true] - Auto-reconnect on disconnect
|
|
237
|
+
* @param {boolean} [config.enablePing=true] - Auto-ping relays
|
|
238
|
+
* @param {string} [config.appName] - Application name for storage namespace
|
|
239
|
+
* @param {boolean} [config.persistence=true] - Enable persistent storage
|
|
240
|
+
* @param {number} [config.cacheSize=500] - Maximum cache size
|
|
241
|
+
* @param {number} [config.persistBatchMs=100] - Batch persist writes within this window
|
|
242
|
+
* @param {boolean} [config.backgroundSync=true] - Enable background sync service
|
|
243
|
+
* @param {string} [config.dataDir] - Data directory for persistent storage
|
|
244
|
+
* @throws {Error} If relays is not an array
|
|
245
|
+
*/
|
|
246
|
+
constructor(config = {}) {
|
|
247
|
+
if (config.relays && !Array.isArray(config.relays)) {
|
|
248
|
+
throw new Error('Relays must be an array');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.relays = config.relays || [];
|
|
252
|
+
|
|
253
|
+
// Generate or use provided private key
|
|
254
|
+
// IMPORTANT: If you want keys to persist across restarts, you MUST provide
|
|
255
|
+
// a privateKey in config. Otherwise, a new key is generated each time.
|
|
256
|
+
this.privateKey = config.privateKey || this._generatePrivateKey();
|
|
257
|
+
this.publicKey = getPublicKey(this.privateKey);
|
|
258
|
+
this.config = config;
|
|
259
|
+
|
|
260
|
+
this._subscriptions = new Map();
|
|
261
|
+
this._eventCache = new LRUCache(config.cacheSize || 500); // LRU cache for recent events
|
|
262
|
+
this._cacheIndex = new Map(); // Reverse index: kind -> Set of cache keys affected by that kind
|
|
263
|
+
this.persistentStorage = null;
|
|
264
|
+
|
|
265
|
+
// Batched persistent writes for better I/O performance
|
|
266
|
+
this._persistQueue = new Map(); // path -> event
|
|
267
|
+
this._persistTimer = null;
|
|
268
|
+
this._persistBatchMs = config.persistBatchMs || 100; // Batch writes within 100ms window
|
|
269
|
+
|
|
270
|
+
// Initialize pool and storage asynchronously
|
|
271
|
+
this._initReady = this._initialize();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Initialize WebSocket polyfill and pool.
|
|
276
|
+
*
|
|
277
|
+
* @private
|
|
278
|
+
* @returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
async _initialize() {
|
|
281
|
+
// Ensure WebSocket is available before initializing pool
|
|
282
|
+
await ensureWebSocket();
|
|
283
|
+
|
|
284
|
+
// Initialize SimplePool with options (only if relays exist)
|
|
285
|
+
if (this.relays.length > 0) {
|
|
286
|
+
// Use global pool singleton to reuse WebSocket connections
|
|
287
|
+
this.pool = getGlobalPool(this.config);
|
|
288
|
+
|
|
289
|
+
// Track relays used by this client
|
|
290
|
+
this.relays.forEach(r => globalPoolRelays.add(r));
|
|
291
|
+
} else {
|
|
292
|
+
// Mock pool for testing - returns mock promise that resolves immediately
|
|
293
|
+
this.pool = {
|
|
294
|
+
publish: (relays, event) => [Promise.resolve()],
|
|
295
|
+
querySync: (relays, filter, options) => Promise.resolve([]),
|
|
296
|
+
subscribeMany: (relays, filters, opts) => ({ close: () => {} }),
|
|
297
|
+
close: (relays) => {},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Initialize persistent storage
|
|
302
|
+
await this._initPersistentStorage();
|
|
303
|
+
|
|
304
|
+
// Start long-lived subscription for real-time cache updates
|
|
305
|
+
if (this.relays.length > 0) {
|
|
306
|
+
this._initLongLivedSubscription();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Initialize a long-lived subscription to keep cache fresh
|
|
312
|
+
* This replaces polling with a single persistent subscription
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
_initLongLivedSubscription() {
|
|
316
|
+
const subKey = this.publicKey;
|
|
317
|
+
|
|
318
|
+
// Check if subscription already exists for this author
|
|
319
|
+
if (authorSubscriptions.has(subKey)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const subInfo = {
|
|
324
|
+
subscription: null,
|
|
325
|
+
initialized: false,
|
|
326
|
+
initPromise: null,
|
|
327
|
+
initResolve: null,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Create promise for initial load completion
|
|
331
|
+
subInfo.initPromise = new Promise(resolve => {
|
|
332
|
+
subInfo.initResolve = resolve;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
authorSubscriptions.set(subKey, subInfo);
|
|
336
|
+
|
|
337
|
+
// Subscribe to ALL events for this author (kind 30000)
|
|
338
|
+
// This single subscription replaces all the polling queries
|
|
339
|
+
const filter = {
|
|
340
|
+
kinds: [30000],
|
|
341
|
+
authors: [this.publicKey],
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const sub = this.pool.subscribeMany(
|
|
345
|
+
this.relays,
|
|
346
|
+
[filter],
|
|
347
|
+
{
|
|
348
|
+
onevent: (event) => {
|
|
349
|
+
// Verify author (relay may not respect filter)
|
|
350
|
+
if (event.pubkey !== this.publicKey) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Cache the event - this keeps our local cache in sync
|
|
355
|
+
this._cacheEvent(event);
|
|
356
|
+
},
|
|
357
|
+
oneose: () => {
|
|
358
|
+
// End of stored events - initial load complete
|
|
359
|
+
if (!subInfo.initialized) {
|
|
360
|
+
subInfo.initialized = true;
|
|
361
|
+
subInfo.initResolve();
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
subInfo.subscription = sub;
|
|
368
|
+
|
|
369
|
+
// Set timeout for initial load in case EOSE never arrives
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
if (!subInfo.initialized) {
|
|
372
|
+
subInfo.initialized = true;
|
|
373
|
+
subInfo.initResolve();
|
|
374
|
+
}
|
|
375
|
+
}, AUTHOR_SUB_INIT_TIMEOUT);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Wait for long-lived subscription to complete initial load
|
|
380
|
+
* @private
|
|
381
|
+
*/
|
|
382
|
+
async _waitForSubscriptionInit() {
|
|
383
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
384
|
+
if (subInfo && subInfo.initPromise) {
|
|
385
|
+
await subInfo.initPromise;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Initialize persistent storage
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
async _initPersistentStorage() {
|
|
394
|
+
// Only enable persistence if explicitly requested or radisk is true
|
|
395
|
+
if (this.config.persistence !== false && (this.config.radisk || this.config.appName)) {
|
|
396
|
+
try {
|
|
397
|
+
const namespace = this.config.appName || 'holosphere_default';
|
|
398
|
+
this.persistentStorage = await createPersistentStorage(namespace, {
|
|
399
|
+
dataDir: this.config.dataDir
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Load cached events from persistent storage
|
|
403
|
+
await this._loadFromPersistentStorage();
|
|
404
|
+
|
|
405
|
+
// Initialize outbox queue for guaranteed delivery
|
|
406
|
+
this.outboxQueue = new OutboxQueue(this.persistentStorage, {
|
|
407
|
+
maxRetries: this.config.maxRetries || 5,
|
|
408
|
+
baseDelay: this.config.retryBaseDelay || 1000,
|
|
409
|
+
maxDelay: this.config.retryMaxDelay || 60000,
|
|
410
|
+
failedTTL: this.config.failedTTL || 24 * 60 * 60 * 1000, // 24 hours
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Start background sync service
|
|
414
|
+
if (this.config.backgroundSync !== false) {
|
|
415
|
+
this.syncService = new SyncService(this, {
|
|
416
|
+
interval: this.config.syncInterval || 10000, // 10 seconds
|
|
417
|
+
autoStart: true,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.warn('Failed to initialize persistent storage:', error);
|
|
422
|
+
// Continue without persistence
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Load events from persistent storage into cache
|
|
429
|
+
* @private
|
|
430
|
+
*/
|
|
431
|
+
async _loadFromPersistentStorage() {
|
|
432
|
+
if (!this.persistentStorage) return;
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
// Load all events (excluding outbox queue entries)
|
|
436
|
+
const events = await this.persistentStorage.getAll('');
|
|
437
|
+
for (const event of events) {
|
|
438
|
+
// Skip outbox queue entries
|
|
439
|
+
if (event && event.id && !event.status) {
|
|
440
|
+
// Add to cache
|
|
441
|
+
this._cacheEventSync(event);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.warn('Failed to load from persistent storage:', error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get a single event from persistent storage by path (d-tag)
|
|
451
|
+
* @param {string} path - The d-tag path to look up
|
|
452
|
+
* @returns {Promise<Object|null>} The event or null if not found
|
|
453
|
+
*/
|
|
454
|
+
async persistentGet(path) {
|
|
455
|
+
await this._initReady;
|
|
456
|
+
if (!this.persistentStorage) return null;
|
|
457
|
+
try {
|
|
458
|
+
const event = await this.persistentStorage.get(path);
|
|
459
|
+
return event || null;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.warn('[nostr] Persistent storage read failed:', error);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get all events from persistent storage matching a prefix
|
|
468
|
+
* @param {string} prefix - The path prefix to match
|
|
469
|
+
* @returns {Promise<Array>} Array of events
|
|
470
|
+
*/
|
|
471
|
+
async persistentGetAll(prefix) {
|
|
472
|
+
await this._initReady;
|
|
473
|
+
if (!this.persistentStorage) return [];
|
|
474
|
+
try {
|
|
475
|
+
const events = await this.persistentStorage.getAll(prefix);
|
|
476
|
+
// Filter out outbox queue entries and null values
|
|
477
|
+
return events.filter(e => e && e.id && !e.status);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.warn('[nostr] Persistent storage read failed:', error);
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Generate random private key
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
488
|
+
_generatePrivateKey() {
|
|
489
|
+
const bytes = new Uint8Array(32);
|
|
490
|
+
if (typeof window !== 'undefined' && window.crypto) {
|
|
491
|
+
window.crypto.getRandomValues(bytes);
|
|
492
|
+
} else {
|
|
493
|
+
// Node.js - use dynamic import
|
|
494
|
+
try {
|
|
495
|
+
// Use crypto.randomFillSync if available
|
|
496
|
+
const crypto = globalThis.crypto || global.crypto;
|
|
497
|
+
if (crypto && crypto.getRandomValues) {
|
|
498
|
+
crypto.getRandomValues(bytes);
|
|
499
|
+
} else {
|
|
500
|
+
// Fallback to Math.random (less secure, but works in testing)
|
|
501
|
+
for (let i = 0; i < 32; i++) {
|
|
502
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
} catch (e) {
|
|
506
|
+
// Fallback to Math.random
|
|
507
|
+
for (let i = 0; i < 32; i++) {
|
|
508
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return Array.from(bytes)
|
|
513
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
514
|
+
.join('');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Publish event to relays.
|
|
519
|
+
*
|
|
520
|
+
* Supports debouncing for replaceable events (kind 30000-39999) to avoid rapid updates.
|
|
521
|
+
*
|
|
522
|
+
* @param {Object} event - Unsigned event object
|
|
523
|
+
* @param {Object} [options={}] - Publish options
|
|
524
|
+
* @param {boolean} [options.waitForRelays=false] - Wait for relay confirmation
|
|
525
|
+
* @param {boolean} [options.debounce=true] - Debounce rapid writes to same d-tag
|
|
526
|
+
* @returns {Promise<Object>} Signed event with relay publish results
|
|
527
|
+
* @example
|
|
528
|
+
* const result = await client.publish({
|
|
529
|
+
* kind: 30000,
|
|
530
|
+
* tags: [['d', 'mypath']],
|
|
531
|
+
* content: JSON.stringify({ name: 'Test' })
|
|
532
|
+
* });
|
|
533
|
+
*/
|
|
534
|
+
async publish(event, options = {}) {
|
|
535
|
+
// Ensure initialization is complete
|
|
536
|
+
await this._initReady;
|
|
537
|
+
|
|
538
|
+
const waitForRelays = options.waitForRelays || false;
|
|
539
|
+
|
|
540
|
+
// For replaceable events, check if we should debounce
|
|
541
|
+
const isReplaceable = event.kind >= 30000 && event.kind < 40000;
|
|
542
|
+
const shouldDebounce = isReplaceable && options.debounce !== false && !waitForRelays;
|
|
543
|
+
|
|
544
|
+
if (shouldDebounce) {
|
|
545
|
+
const dTag = event.tags?.find(t => t[0] === 'd');
|
|
546
|
+
if (dTag && dTag[1]) {
|
|
547
|
+
return this._debouncedPublish(event, dTag[1], options);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return this._doPublish(event, options);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Debounced publish - coalesces rapid writes to the same d-tag
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
_debouncedPublish(event, dTagPath, options) {
|
|
559
|
+
return new Promise((resolve, reject) => {
|
|
560
|
+
const existing = pendingWrites.get(dTagPath);
|
|
561
|
+
|
|
562
|
+
if (existing) {
|
|
563
|
+
// Cancel previous pending write and use the new one
|
|
564
|
+
clearTimeout(existing.timer);
|
|
565
|
+
// Resolve the previous promise with the new event (it will be superseded)
|
|
566
|
+
existing.resolve({
|
|
567
|
+
event: null,
|
|
568
|
+
results: [],
|
|
569
|
+
debounced: true,
|
|
570
|
+
supersededBy: event,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Set up debounced write
|
|
575
|
+
const timer = setTimeout(async () => {
|
|
576
|
+
pendingWrites.delete(dTagPath);
|
|
577
|
+
try {
|
|
578
|
+
const result = await this._doPublish(event, options);
|
|
579
|
+
resolve(result);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
reject(err);
|
|
582
|
+
}
|
|
583
|
+
}, WRITE_DEBOUNCE_MS);
|
|
584
|
+
|
|
585
|
+
pendingWrites.set(dTagPath, { event, timer, resolve, reject });
|
|
586
|
+
|
|
587
|
+
// Cache immediately for local-first reads (even before relay publish)
|
|
588
|
+
const signedEvent = finalizeEvent(event, this.privateKey);
|
|
589
|
+
this._cacheEvent(signedEvent);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Internal publish implementation
|
|
595
|
+
* @private
|
|
596
|
+
*/
|
|
597
|
+
async _doPublish(event, options = {}) {
|
|
598
|
+
const waitForRelays = options.waitForRelays || false;
|
|
599
|
+
|
|
600
|
+
// Check if event is already signed (has id and sig)
|
|
601
|
+
// If so, use it as-is; otherwise sign it
|
|
602
|
+
const signedEvent = (event.id && event.sig)
|
|
603
|
+
? event
|
|
604
|
+
: finalizeEvent(event, this.privateKey);
|
|
605
|
+
|
|
606
|
+
// 1. Cache the event locally first (this makes reads instant)
|
|
607
|
+
await this._cacheEvent(signedEvent);
|
|
608
|
+
|
|
609
|
+
// 2. Enqueue for guaranteed delivery (if outbox queue available)
|
|
610
|
+
if (this.outboxQueue) {
|
|
611
|
+
await this.outboxQueue.enqueue(signedEvent, this.relays);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 3. Publish to relays
|
|
615
|
+
if (waitForRelays) {
|
|
616
|
+
// Wait for relay confirmation if explicitly requested
|
|
617
|
+
const deliveryResult = await this._attemptDelivery(signedEvent, this.relays);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
event: signedEvent,
|
|
621
|
+
results: deliveryResult.results,
|
|
622
|
+
queued: !!this.outboxQueue,
|
|
623
|
+
};
|
|
624
|
+
} else {
|
|
625
|
+
// Fire and forget - publish in background, return immediately
|
|
626
|
+
this._attemptDelivery(signedEvent, this.relays).catch(() => {
|
|
627
|
+
// Silent - event is cached locally and queued for retry
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Return immediately (data is in local cache and queued for delivery)
|
|
631
|
+
return {
|
|
632
|
+
event: signedEvent,
|
|
633
|
+
results: [],
|
|
634
|
+
queued: !!this.outboxQueue,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Attempt to deliver event to relays
|
|
641
|
+
* Updates outbox queue with delivery status
|
|
642
|
+
* @param {Object} event - Signed event to deliver
|
|
643
|
+
* @param {string[]} relays - Target relay URLs
|
|
644
|
+
* @returns {Promise<Object>} Delivery result with successful/failed relays
|
|
645
|
+
*/
|
|
646
|
+
async _attemptDelivery(event, relays) {
|
|
647
|
+
const results = await Promise.allSettled(
|
|
648
|
+
this.pool.publish(relays, event)
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const successful = [];
|
|
652
|
+
const failed = [];
|
|
653
|
+
const formattedResults = [];
|
|
654
|
+
|
|
655
|
+
results.forEach((r, i) => {
|
|
656
|
+
formattedResults.push({
|
|
657
|
+
relay: relays[i],
|
|
658
|
+
status: r.status,
|
|
659
|
+
value: r.value,
|
|
660
|
+
reason: r.reason,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (r.status === 'fulfilled') {
|
|
664
|
+
successful.push(relays[i]);
|
|
665
|
+
} else {
|
|
666
|
+
failed.push(relays[i]);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Update outbox queue with delivery status
|
|
671
|
+
if (this.outboxQueue) {
|
|
672
|
+
await this.outboxQueue.markSent(event.id, successful);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Log failures only at debug level (common during network issues)
|
|
676
|
+
if (failed.length > 0 && successful.length === 0) {
|
|
677
|
+
// Only warn if ALL relays failed (likely a real issue)
|
|
678
|
+
const reasons = results
|
|
679
|
+
.filter(r => r.status === 'rejected')
|
|
680
|
+
.map(f => f.reason?.message || f.reason || 'unknown')
|
|
681
|
+
.join(', ');
|
|
682
|
+
// Use debug level for timeout issues (very common)
|
|
683
|
+
if (reasons.includes('timed out')) {
|
|
684
|
+
// Silent - event is queued for retry anyway
|
|
685
|
+
} else {
|
|
686
|
+
console.warn(`[nostr] All relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { successful, failed, results: formattedResults };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Query events from relays.
|
|
695
|
+
*
|
|
696
|
+
* Uses long-lived subscription for cache updates - avoids polling.
|
|
697
|
+
*
|
|
698
|
+
* @param {Object} filter - Nostr filter object
|
|
699
|
+
* @param {Object} [options={}] - Query options
|
|
700
|
+
* @param {number} [options.timeout=30000] - Query timeout in ms (set to 0 for no timeout)
|
|
701
|
+
* @param {boolean} [options.localFirst=true] - Return local cache immediately, refresh in background
|
|
702
|
+
* @param {boolean} [options.forceRelay=false] - Force relay query even if subscription cache is available
|
|
703
|
+
* @returns {Promise<Array>} Array of events
|
|
704
|
+
* @example
|
|
705
|
+
* const events = await client.query({
|
|
706
|
+
* kinds: [30000],
|
|
707
|
+
* authors: [client.publicKey],
|
|
708
|
+
* '#d': ['mypath']
|
|
709
|
+
* });
|
|
710
|
+
*/
|
|
711
|
+
async query(filter, options = {}) {
|
|
712
|
+
// Ensure initialization is complete
|
|
713
|
+
await this._initReady;
|
|
714
|
+
|
|
715
|
+
const timeout = options.timeout !== undefined ? options.timeout : 30000;
|
|
716
|
+
const localFirst = options.localFirst !== false; // Default to true for speed
|
|
717
|
+
const forceRelay = options.forceRelay === true;
|
|
718
|
+
|
|
719
|
+
// If no relays, query from cache only
|
|
720
|
+
if (this.relays.length === 0) {
|
|
721
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
722
|
+
return matchingEvents;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Check if this query can be served from the long-lived subscription cache
|
|
726
|
+
// The subscription keeps ALL events for this author in cache, updated in real-time
|
|
727
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
728
|
+
const isOwnDataQuery = filter.authors &&
|
|
729
|
+
filter.authors.length === 1 &&
|
|
730
|
+
filter.authors[0] === this.publicKey;
|
|
731
|
+
|
|
732
|
+
// If we have an initialized subscription for our own data, use cache
|
|
733
|
+
if (!forceRelay && isOwnDataQuery && subInfo && subInfo.initialized) {
|
|
734
|
+
// Return matching events from cache - no relay query needed!
|
|
735
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
736
|
+
return matchingEvents;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// For first query before subscription initializes, wait briefly
|
|
740
|
+
if (isOwnDataQuery && subInfo && !subInfo.initialized) {
|
|
741
|
+
// Wait for subscription to initialize (up to timeout)
|
|
742
|
+
await Promise.race([
|
|
743
|
+
subInfo.initPromise,
|
|
744
|
+
new Promise(resolve => setTimeout(resolve, Math.min(timeout, AUTHOR_SUB_INIT_TIMEOUT)))
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
// Now try cache again
|
|
748
|
+
if (subInfo.initialized) {
|
|
749
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
750
|
+
return matchingEvents;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Check d-tag cache first for single-item queries (most common case)
|
|
755
|
+
// This ensures recently written data is returned immediately
|
|
756
|
+
if (filter['#d'] && filter['#d'].length === 1 && filter.kinds && filter.kinds.length === 1) {
|
|
757
|
+
const dTagKey = `d:${filter.kinds[0]}:${filter['#d'][0]}`;
|
|
758
|
+
const dTagCached = this._eventCache.get(dTagKey);
|
|
759
|
+
if (dTagCached && Date.now() - dTagCached.timestamp < 5000) {
|
|
760
|
+
return dTagCached.events;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const cacheKey = JSON.stringify(filter);
|
|
765
|
+
const cached = this._eventCache.get(cacheKey);
|
|
766
|
+
|
|
767
|
+
// Return fresh cache immediately
|
|
768
|
+
if (cached && Date.now() - cached.timestamp < 5000) {
|
|
769
|
+
return cached.events;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// If we have stale cache and localFirst is enabled, return it immediately
|
|
773
|
+
// and refresh in background
|
|
774
|
+
if (localFirst && cached) {
|
|
775
|
+
// Refresh cache in background
|
|
776
|
+
this._refreshCacheInBackground(filter, cacheKey, timeout);
|
|
777
|
+
return cached.events;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// No cache available or localFirst disabled - must query relays
|
|
781
|
+
return this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Query relays and update cache
|
|
786
|
+
* Uses global pending queries map to deduplicate identical concurrent queries
|
|
787
|
+
* @private
|
|
788
|
+
*/
|
|
789
|
+
async _queryRelaysAndCache(filter, cacheKey, timeout) {
|
|
790
|
+
// Check if there's already a pending query for this exact filter
|
|
791
|
+
const pending = pendingQueries.get(cacheKey);
|
|
792
|
+
if (pending && Date.now() - pending.timestamp < PENDING_QUERY_TIMEOUT) {
|
|
793
|
+
// Reuse the pending query promise instead of creating a new one
|
|
794
|
+
return pending.promise;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Create the query promise
|
|
798
|
+
const queryPromise = (async () => {
|
|
799
|
+
try {
|
|
800
|
+
let events = await this.pool.querySync(this.relays, filter, { timeout });
|
|
801
|
+
|
|
802
|
+
// CRITICAL: Filter out events from other authors (relay may not respect filter)
|
|
803
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
804
|
+
events = events.filter(event => filter.authors.includes(event.pubkey));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Cache results
|
|
808
|
+
this._eventCache.set(cacheKey, {
|
|
809
|
+
events,
|
|
810
|
+
timestamp: Date.now(),
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// Update reverse index for fast invalidation
|
|
814
|
+
this._indexCacheEntry(cacheKey, filter);
|
|
815
|
+
|
|
816
|
+
return events;
|
|
817
|
+
} finally {
|
|
818
|
+
// Clean up pending query after completion
|
|
819
|
+
pendingQueries.delete(cacheKey);
|
|
820
|
+
}
|
|
821
|
+
})();
|
|
822
|
+
|
|
823
|
+
// Store the pending query
|
|
824
|
+
pendingQueries.set(cacheKey, {
|
|
825
|
+
promise: queryPromise,
|
|
826
|
+
timestamp: Date.now(),
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return queryPromise;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Limit cache size (called after cache operations)
|
|
834
|
+
* Note: LRU cache handles this automatically, kept for API compatibility
|
|
835
|
+
* @private
|
|
836
|
+
*/
|
|
837
|
+
_limitCacheSize() {
|
|
838
|
+
// LRU cache handles size limiting automatically
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Add cache entry to reverse index for fast invalidation
|
|
843
|
+
* @private
|
|
844
|
+
*/
|
|
845
|
+
_indexCacheEntry(cacheKey, filter) {
|
|
846
|
+
// Index by kinds for fast lookup during invalidation
|
|
847
|
+
if (filter.kinds) {
|
|
848
|
+
for (const kind of filter.kinds) {
|
|
849
|
+
if (!this._cacheIndex.has(kind)) {
|
|
850
|
+
this._cacheIndex.set(kind, new Set());
|
|
851
|
+
}
|
|
852
|
+
this._cacheIndex.get(kind).add(cacheKey);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Remove cache entry from reverse index
|
|
859
|
+
* @private
|
|
860
|
+
*/
|
|
861
|
+
_unindexCacheEntry(cacheKey) {
|
|
862
|
+
// Try to parse the filter from the cache key to remove from index
|
|
863
|
+
if (!cacheKey.startsWith('{')) return;
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const filter = JSON.parse(cacheKey);
|
|
867
|
+
if (filter.kinds) {
|
|
868
|
+
for (const kind of filter.kinds) {
|
|
869
|
+
const indexSet = this._cacheIndex.get(kind);
|
|
870
|
+
if (indexSet) {
|
|
871
|
+
indexSet.delete(cacheKey);
|
|
872
|
+
if (indexSet.size === 0) {
|
|
873
|
+
this._cacheIndex.delete(kind);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} catch {
|
|
879
|
+
// Not a valid filter key, skip
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Refresh cache in background (fire and forget)
|
|
885
|
+
* @private
|
|
886
|
+
*/
|
|
887
|
+
_refreshCacheInBackground(filter, cacheKey, timeout) {
|
|
888
|
+
this._queryRelaysAndCache(filter, cacheKey, timeout).catch(err => {
|
|
889
|
+
// Silently ignore background refresh errors - we have stale cache as fallback
|
|
890
|
+
console.debug('[nostr] Background cache refresh failed:', err.message);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Refresh a single path from relays in the background
|
|
896
|
+
* Used for persistent-first reads to update local data
|
|
897
|
+
* @param {string} path - The d-tag path to refresh
|
|
898
|
+
* @param {number} kind - Event kind (default: 30000)
|
|
899
|
+
* @param {Object} options - Query options
|
|
900
|
+
*/
|
|
901
|
+
refreshPathInBackground(path, kind = 30000, options = {}) {
|
|
902
|
+
this._doBackgroundPathRefresh(path, kind, options).catch(err => {
|
|
903
|
+
console.debug('[nostr] Background path refresh failed:', err.message);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Internal method to refresh a path from relays
|
|
909
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
910
|
+
* @private
|
|
911
|
+
*/
|
|
912
|
+
async _doBackgroundPathRefresh(path, kind, options) {
|
|
913
|
+
if (this.relays.length === 0) return;
|
|
914
|
+
|
|
915
|
+
// Throttle: Skip if we've refreshed this path recently
|
|
916
|
+
const lastRefresh = backgroundRefreshThrottle.get(path);
|
|
917
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
918
|
+
return; // Skip - recently refreshed
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Mark as refreshed
|
|
922
|
+
backgroundRefreshThrottle.set(path, Date.now());
|
|
923
|
+
|
|
924
|
+
// Clean up old throttle entries periodically (keep map from growing)
|
|
925
|
+
if (backgroundRefreshThrottle.size > 1000) {
|
|
926
|
+
const cutoff = Date.now() - BACKGROUND_REFRESH_INTERVAL;
|
|
927
|
+
for (const [key, timestamp] of backgroundRefreshThrottle) {
|
|
928
|
+
if (timestamp < cutoff) {
|
|
929
|
+
backgroundRefreshThrottle.delete(key);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const filter = {
|
|
935
|
+
kinds: [kind],
|
|
936
|
+
authors: options.authors || [this.publicKey],
|
|
937
|
+
'#d': [path],
|
|
938
|
+
limit: 1,
|
|
939
|
+
};
|
|
940
|
+
const cacheKey = JSON.stringify(filter);
|
|
941
|
+
|
|
942
|
+
// Use our query deduplication by calling query() instead of pool.querySync() directly
|
|
943
|
+
const timeout = options.timeout || 30000;
|
|
944
|
+
const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
945
|
+
|
|
946
|
+
// Filter by author (relays may not respect filter)
|
|
947
|
+
const authorFiltered = events.filter(e =>
|
|
948
|
+
(options.authors || [this.publicKey]).includes(e.pubkey)
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
if (authorFiltered.length > 0) {
|
|
952
|
+
// Update cache and persistent storage
|
|
953
|
+
await this._cacheEvent(authorFiltered[0]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Refresh all paths with a prefix from relays in the background
|
|
959
|
+
* Used for persistent-first collection reads
|
|
960
|
+
* @param {string} prefix - The d-tag prefix to refresh
|
|
961
|
+
* @param {number} kind - Event kind (default: 30000)
|
|
962
|
+
* @param {Object} options - Query options
|
|
963
|
+
*/
|
|
964
|
+
refreshPrefixInBackground(prefix, kind = 30000, options = {}) {
|
|
965
|
+
this._doBackgroundPrefixRefresh(prefix, kind, options).catch(err => {
|
|
966
|
+
console.debug('[nostr] Background prefix refresh failed:', err.message);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Internal method to refresh a prefix from relays
|
|
972
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
973
|
+
* @private
|
|
974
|
+
*/
|
|
975
|
+
async _doBackgroundPrefixRefresh(prefix, kind, options) {
|
|
976
|
+
if (this.relays.length === 0) return;
|
|
977
|
+
|
|
978
|
+
// Throttle: Skip if we've refreshed this prefix recently
|
|
979
|
+
const throttleKey = `prefix:${prefix}`;
|
|
980
|
+
const lastRefresh = backgroundRefreshThrottle.get(throttleKey);
|
|
981
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
982
|
+
return; // Skip - recently refreshed
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Mark as refreshed
|
|
986
|
+
backgroundRefreshThrottle.set(throttleKey, Date.now());
|
|
987
|
+
|
|
988
|
+
// Query with wildcard-ish filter (relays handle d-tag prefix matching)
|
|
989
|
+
const filter = {
|
|
990
|
+
kinds: [kind],
|
|
991
|
+
authors: options.authors || [this.publicKey],
|
|
992
|
+
limit: options.limit || 1000,
|
|
993
|
+
};
|
|
994
|
+
const cacheKey = JSON.stringify(filter);
|
|
995
|
+
|
|
996
|
+
// Use our query deduplication
|
|
997
|
+
const timeout = options.timeout || 30000;
|
|
998
|
+
let events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
999
|
+
|
|
1000
|
+
// Filter by author (already done by _queryRelaysAndCache, but double-check)
|
|
1001
|
+
events = events.filter(e =>
|
|
1002
|
+
(options.authors || [this.publicKey]).includes(e.pubkey)
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
// Filter by d-tag prefix (client-side since relays don't support prefix matching)
|
|
1006
|
+
events = events.filter(e => {
|
|
1007
|
+
const dTag = e.tags.find(t => t[0] === 'd');
|
|
1008
|
+
return dTag && dTag[1] && dTag[1].startsWith(prefix);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// Update cache and persistent storage for each event
|
|
1012
|
+
for (const event of events) {
|
|
1013
|
+
await this._cacheEvent(event);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Hybrid query: Check local cache first, then query relays for missing data
|
|
1019
|
+
* Merges local and relay results, preferring newer events
|
|
1020
|
+
* @param {Object} filter - Nostr filter object
|
|
1021
|
+
* @param {Object} options - Query options
|
|
1022
|
+
* @param {number} options.timeout - Query timeout in ms (default: 30000)
|
|
1023
|
+
* @returns {Promise<Array>} Array of events (merged from local + relay)
|
|
1024
|
+
*/
|
|
1025
|
+
async queryHybrid(filter, options = {}) {
|
|
1026
|
+
// Ensure initialization is complete
|
|
1027
|
+
await this._initReady;
|
|
1028
|
+
|
|
1029
|
+
const timeout = options.timeout !== undefined ? options.timeout : 30000;
|
|
1030
|
+
|
|
1031
|
+
// Step 1: Get events from local cache
|
|
1032
|
+
const localEvents = this._getMatchingCachedEvents(filter);
|
|
1033
|
+
|
|
1034
|
+
// If no relays configured, return local only
|
|
1035
|
+
if (this.relays.length === 0) {
|
|
1036
|
+
return localEvents;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Step 2: Query relays
|
|
1040
|
+
let relayEvents = [];
|
|
1041
|
+
try {
|
|
1042
|
+
relayEvents = await this.pool.querySync(this.relays, filter, { timeout });
|
|
1043
|
+
|
|
1044
|
+
// Filter out events from other authors
|
|
1045
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
1046
|
+
relayEvents = relayEvents.filter(event => filter.authors.includes(event.pubkey));
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
console.warn('Relay query failed, using local cache only:', error.message);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Step 3: Merge results (prefer newer events for replaceable events)
|
|
1053
|
+
const merged = new Map();
|
|
1054
|
+
|
|
1055
|
+
// Add local events first
|
|
1056
|
+
for (const event of localEvents) {
|
|
1057
|
+
const key = this._getEventKey(event);
|
|
1058
|
+
merged.set(key, event);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Add or update with relay events (relay events are authoritative)
|
|
1062
|
+
for (const event of relayEvents) {
|
|
1063
|
+
const key = this._getEventKey(event);
|
|
1064
|
+
const existing = merged.get(key);
|
|
1065
|
+
|
|
1066
|
+
// For replaceable events, prefer newer
|
|
1067
|
+
if (event.kind >= 30000 && event.kind < 40000 && existing) {
|
|
1068
|
+
if (event.created_at >= existing.created_at) {
|
|
1069
|
+
merged.set(key, event);
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
// For non-replaceable or if no existing, just add
|
|
1073
|
+
merged.set(key, event);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const result = Array.from(merged.values());
|
|
1078
|
+
|
|
1079
|
+
// Cache the merged results
|
|
1080
|
+
const cacheKey = JSON.stringify(filter);
|
|
1081
|
+
this._eventCache.set(cacheKey, {
|
|
1082
|
+
events: result,
|
|
1083
|
+
timestamp: Date.now(),
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
return result;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Get unique key for event (for merging)
|
|
1091
|
+
* @private
|
|
1092
|
+
*/
|
|
1093
|
+
_getEventKey(event) {
|
|
1094
|
+
// For replaceable events with d-tag, use kind:dtag
|
|
1095
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1096
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1097
|
+
if (dTag && dTag[1]) {
|
|
1098
|
+
return `${event.kind}:${dTag[1]}`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Otherwise use event id
|
|
1102
|
+
return event.id;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Subscribe to events.
|
|
1107
|
+
*
|
|
1108
|
+
* Uses subscription deduplication to avoid creating multiple identical subscriptions.
|
|
1109
|
+
*
|
|
1110
|
+
* @param {Object} filter - Nostr filter object
|
|
1111
|
+
* @param {Function} onEvent - Callback for each event
|
|
1112
|
+
* @param {Object} [options={}] - Subscription options
|
|
1113
|
+
* @param {Function} [options.onEOSE] - Callback when End Of Stored Events is received
|
|
1114
|
+
* @returns {Promise<Object>} Subscription object with unsubscribe method and id
|
|
1115
|
+
* @example
|
|
1116
|
+
* const sub = await client.subscribe(
|
|
1117
|
+
* { kinds: [30000], authors: [client.publicKey] },
|
|
1118
|
+
* (event) => console.log('Event:', event)
|
|
1119
|
+
* );
|
|
1120
|
+
* // Later: sub.unsubscribe();
|
|
1121
|
+
*/
|
|
1122
|
+
async subscribe(filter, onEvent, options = {}) {
|
|
1123
|
+
// Ensure initialization is complete
|
|
1124
|
+
await this._initReady;
|
|
1125
|
+
|
|
1126
|
+
const subId = `sub-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1127
|
+
const filterKey = JSON.stringify(filter);
|
|
1128
|
+
|
|
1129
|
+
// If no relays, check cache for matching events and trigger callbacks
|
|
1130
|
+
if (this.relays.length === 0) {
|
|
1131
|
+
// Trigger with cached events that match the filter
|
|
1132
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
1133
|
+
|
|
1134
|
+
// Create a mock subscription that watches for new cache entries
|
|
1135
|
+
const mockSub = {
|
|
1136
|
+
filter,
|
|
1137
|
+
onEvent,
|
|
1138
|
+
active: true
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
this._subscriptions.set(subId, mockSub);
|
|
1142
|
+
|
|
1143
|
+
// Trigger callbacks asynchronously to mimic real subscription behavior
|
|
1144
|
+
// Check active flag to avoid triggering if unsubscribed immediately
|
|
1145
|
+
setTimeout(() => {
|
|
1146
|
+
if (mockSub.active) {
|
|
1147
|
+
matchingEvents.forEach(event => onEvent(event));
|
|
1148
|
+
if (options.onEOSE) options.onEOSE();
|
|
1149
|
+
}
|
|
1150
|
+
}, 10);
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
id: subId,
|
|
1154
|
+
unsubscribe: () => {
|
|
1155
|
+
mockSub.active = false;
|
|
1156
|
+
this._subscriptions.delete(subId);
|
|
1157
|
+
// Clear the subscription's onEvent to prevent any further calls
|
|
1158
|
+
mockSub.onEvent = () => {};
|
|
1159
|
+
// Note: active flag is checked before triggering callbacks
|
|
1160
|
+
},
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Check if we already have an active subscription for this filter
|
|
1165
|
+
const existing = activeSubscriptions.get(filterKey);
|
|
1166
|
+
if (existing) {
|
|
1167
|
+
// Add callback to existing subscription
|
|
1168
|
+
existing.callbacks.add(onEvent);
|
|
1169
|
+
existing.refCount++;
|
|
1170
|
+
|
|
1171
|
+
// Return wrapper that removes this specific callback on unsubscribe
|
|
1172
|
+
return {
|
|
1173
|
+
id: subId,
|
|
1174
|
+
unsubscribe: () => {
|
|
1175
|
+
existing.callbacks.delete(onEvent);
|
|
1176
|
+
existing.refCount--;
|
|
1177
|
+
|
|
1178
|
+
// Only close actual subscription when no more callbacks
|
|
1179
|
+
if (existing.refCount === 0) {
|
|
1180
|
+
if (existing.subscription && existing.subscription.close) {
|
|
1181
|
+
existing.subscription.close();
|
|
1182
|
+
}
|
|
1183
|
+
activeSubscriptions.delete(filterKey);
|
|
1184
|
+
}
|
|
1185
|
+
this._subscriptions.delete(subId);
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Create new subscription with shared callback dispatcher
|
|
1191
|
+
const callbacks = new Set([onEvent]);
|
|
1192
|
+
const subscriptionInfo = {
|
|
1193
|
+
callbacks,
|
|
1194
|
+
refCount: 1,
|
|
1195
|
+
subscription: null,
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// Store before creating subscription to handle race conditions
|
|
1199
|
+
activeSubscriptions.set(filterKey, subscriptionInfo);
|
|
1200
|
+
|
|
1201
|
+
const sub = this.pool.subscribeMany(
|
|
1202
|
+
this.relays,
|
|
1203
|
+
[filter],
|
|
1204
|
+
{
|
|
1205
|
+
onevent: (event) => {
|
|
1206
|
+
// CRITICAL: Filter out events from other authors immediately
|
|
1207
|
+
// Many relays don't respect the authors filter, so we must do it client-side
|
|
1208
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
1209
|
+
if (!filter.authors.includes(event.pubkey)) {
|
|
1210
|
+
// Silently reject events from other authors
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
this._cacheEvent(event);
|
|
1216
|
+
|
|
1217
|
+
// Dispatch to ALL registered callbacks for this subscription
|
|
1218
|
+
const subInfo = activeSubscriptions.get(filterKey);
|
|
1219
|
+
if (subInfo) {
|
|
1220
|
+
for (const cb of subInfo.callbacks) {
|
|
1221
|
+
try {
|
|
1222
|
+
cb(event);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
console.warn('[nostr] Subscription callback error:', err.message);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
oneose: () => {
|
|
1230
|
+
if (options.onEOSE) options.onEOSE();
|
|
1231
|
+
},
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
// Store the actual subscription object
|
|
1236
|
+
subscriptionInfo.subscription = sub;
|
|
1237
|
+
this._subscriptions.set(subId, sub);
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
id: subId,
|
|
1241
|
+
unsubscribe: () => {
|
|
1242
|
+
callbacks.delete(onEvent);
|
|
1243
|
+
subscriptionInfo.refCount--;
|
|
1244
|
+
|
|
1245
|
+
// Only close actual subscription when no more callbacks
|
|
1246
|
+
if (subscriptionInfo.refCount === 0) {
|
|
1247
|
+
if (sub.close) sub.close();
|
|
1248
|
+
activeSubscriptions.delete(filterKey);
|
|
1249
|
+
}
|
|
1250
|
+
this._subscriptions.delete(subId);
|
|
1251
|
+
},
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Cache event in memory (synchronous version for loading)
|
|
1257
|
+
* @private
|
|
1258
|
+
*/
|
|
1259
|
+
_cacheEventSync(event) {
|
|
1260
|
+
const key = event.id;
|
|
1261
|
+
this._eventCache.set(key, {
|
|
1262
|
+
events: [event],
|
|
1263
|
+
timestamp: Date.now(),
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// For replaceable events (kind 30000-39999), also cache by d-tag
|
|
1267
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1268
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1269
|
+
if (dTag && dTag[1]) {
|
|
1270
|
+
const dTagKey = `d:${event.kind}:${dTag[1]}`;
|
|
1271
|
+
const existing = this._eventCache.get(dTagKey);
|
|
1272
|
+
|
|
1273
|
+
// Replace if newer, or if same timestamp but different event ID (for events in same second)
|
|
1274
|
+
const shouldReplace = !existing || !existing.events[0] ||
|
|
1275
|
+
event.created_at > existing.events[0].created_at ||
|
|
1276
|
+
(event.created_at === existing.events[0].created_at && event.id !== existing.events[0].id);
|
|
1277
|
+
|
|
1278
|
+
if (shouldReplace) {
|
|
1279
|
+
this._eventCache.set(dTagKey, {
|
|
1280
|
+
events: [event],
|
|
1281
|
+
timestamp: Date.now(),
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
// Remove old event from cache if it exists
|
|
1285
|
+
if (existing && existing.events[0]) {
|
|
1286
|
+
this._eventCache.delete(existing.events[0].id);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Invalidate any query caches that might contain stale data for this d-tag
|
|
1290
|
+
// Query caches are JSON-stringified filters, so we look for any that match this event
|
|
1291
|
+
this._invalidateQueryCachesForEvent(event);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Invalidate query caches that might be affected by a new event
|
|
1299
|
+
* Uses reverse index for O(1) lookup instead of O(n) scan
|
|
1300
|
+
* @private
|
|
1301
|
+
*/
|
|
1302
|
+
_invalidateQueryCachesForEvent(event) {
|
|
1303
|
+
// Use reverse index for fast lookup - only check caches that could match this event's kind
|
|
1304
|
+
const indexedKeys = this._cacheIndex.get(event.kind);
|
|
1305
|
+
if (!indexedKeys || indexedKeys.size === 0) {
|
|
1306
|
+
return; // No cached queries for this kind
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const keysToDelete = [];
|
|
1310
|
+
|
|
1311
|
+
// Only iterate over cache entries that match the event's kind
|
|
1312
|
+
for (const cacheKey of indexedKeys) {
|
|
1313
|
+
const cached = this._eventCache.get(cacheKey);
|
|
1314
|
+
if (!cached) {
|
|
1315
|
+
// Cache entry was evicted, clean up index
|
|
1316
|
+
indexedKeys.delete(cacheKey);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
try {
|
|
1321
|
+
const filter = JSON.parse(cacheKey);
|
|
1322
|
+
// Check if this event would match the filter
|
|
1323
|
+
if (this._eventMatchesFilter(event, filter)) {
|
|
1324
|
+
keysToDelete.push(cacheKey);
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
// Not a valid JSON key, clean up index
|
|
1328
|
+
indexedKeys.delete(cacheKey);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
for (const key of keysToDelete) {
|
|
1333
|
+
this._eventCache.delete(key);
|
|
1334
|
+
this._unindexCacheEntry(key);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Cache event in memory and persist (batched)
|
|
1340
|
+
* @private
|
|
1341
|
+
*/
|
|
1342
|
+
async _cacheEvent(event) {
|
|
1343
|
+
// Cache in memory (synchronous - immediate for local-first reads)
|
|
1344
|
+
this._cacheEventSync(event);
|
|
1345
|
+
|
|
1346
|
+
// Queue for batched persistence (async - batches writes for I/O efficiency)
|
|
1347
|
+
if (this.persistentStorage) {
|
|
1348
|
+
// For replaceable events, use d-tag as key
|
|
1349
|
+
let storageKey = event.id;
|
|
1350
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1351
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1352
|
+
if (dTag && dTag[1]) {
|
|
1353
|
+
storageKey = dTag[1]; // Use d-tag as key for replaceable events
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Queue for batched write (overwrites previous if same key)
|
|
1358
|
+
this._persistQueue.set(storageKey, event);
|
|
1359
|
+
|
|
1360
|
+
// Schedule batch flush if not already scheduled
|
|
1361
|
+
if (!this._persistTimer) {
|
|
1362
|
+
this._persistTimer = setTimeout(() => this._flushPersistQueue(), this._persistBatchMs);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Trigger any active subscriptions that match this event
|
|
1367
|
+
if (this.relays.length === 0) {
|
|
1368
|
+
for (const sub of this._subscriptions.values()) {
|
|
1369
|
+
if (sub.active && this._eventMatchesFilter(event, sub.filter)) {
|
|
1370
|
+
// Trigger callback asynchronously, but check active status again
|
|
1371
|
+
setTimeout(() => {
|
|
1372
|
+
if (sub.active) {
|
|
1373
|
+
sub.onEvent(event);
|
|
1374
|
+
}
|
|
1375
|
+
}, 10);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Flush batched persistent writes
|
|
1383
|
+
* @private
|
|
1384
|
+
*/
|
|
1385
|
+
async _flushPersistQueue() {
|
|
1386
|
+
this._persistTimer = null;
|
|
1387
|
+
|
|
1388
|
+
if (!this.persistentStorage || this._persistQueue.size === 0) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Take snapshot of current queue and clear it
|
|
1393
|
+
const toWrite = Array.from(this._persistQueue.entries());
|
|
1394
|
+
this._persistQueue.clear();
|
|
1395
|
+
|
|
1396
|
+
// Write all queued events
|
|
1397
|
+
const writePromises = toWrite.map(async ([key, event]) => {
|
|
1398
|
+
try {
|
|
1399
|
+
await this.persistentStorage.put(key, event);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
console.warn(`Failed to persist event ${key}:`, error.message);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// Wait for all writes to complete
|
|
1406
|
+
await Promise.all(writePromises);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Get cached events matching a filter
|
|
1411
|
+
* @private
|
|
1412
|
+
*/
|
|
1413
|
+
_getMatchingCachedEvents(filter) {
|
|
1414
|
+
const matchingEvents = [];
|
|
1415
|
+
const seenDTags = new Map(); // For deduplicating replaceable events
|
|
1416
|
+
|
|
1417
|
+
for (const cached of this._eventCache.values()) {
|
|
1418
|
+
for (const event of (cached.events || [])) {
|
|
1419
|
+
if (this._eventMatchesFilter(event, filter)) {
|
|
1420
|
+
// For replaceable events (kind 30000-39999), keep only the newest
|
|
1421
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1422
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1423
|
+
if (dTag && dTag[1]) {
|
|
1424
|
+
const dTagKey = `${event.kind}:${dTag[1]}`;
|
|
1425
|
+
const existing = seenDTags.get(dTagKey);
|
|
1426
|
+
|
|
1427
|
+
// Replace if newer, or if same timestamp with _deleted flag (tombstones)
|
|
1428
|
+
const shouldReplace = !existing ||
|
|
1429
|
+
event.created_at > existing.created_at ||
|
|
1430
|
+
(event.created_at === existing.created_at &&
|
|
1431
|
+
JSON.parse(event.content)?._deleted &&
|
|
1432
|
+
!JSON.parse(existing.content)?._deleted);
|
|
1433
|
+
|
|
1434
|
+
if (shouldReplace) {
|
|
1435
|
+
// Remove old event if exists
|
|
1436
|
+
if (existing) {
|
|
1437
|
+
const idx = matchingEvents.indexOf(existing);
|
|
1438
|
+
if (idx > -1) matchingEvents.splice(idx, 1);
|
|
1439
|
+
}
|
|
1440
|
+
seenDTags.set(dTagKey, event);
|
|
1441
|
+
matchingEvents.push(event);
|
|
1442
|
+
}
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Regular events or replaceable events without d-tag
|
|
1448
|
+
matchingEvents.push(event);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return matchingEvents;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Check if event matches filter
|
|
1458
|
+
* @private
|
|
1459
|
+
*/
|
|
1460
|
+
_eventMatchesFilter(event, filter) {
|
|
1461
|
+
// Check kinds
|
|
1462
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Check IDs
|
|
1467
|
+
if (filter.ids && !filter.ids.includes(event.id)) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Check authors
|
|
1472
|
+
if (filter.authors && !filter.authors.includes(event.pubkey)) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Check #d tag (for replaceable events)
|
|
1477
|
+
if (filter['#d']) {
|
|
1478
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1479
|
+
if (!dTag || !filter['#d'].includes(dTag[1])) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Check since/until
|
|
1485
|
+
if (filter.since && event.created_at < filter.since) {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
if (filter.until && event.created_at > filter.until) {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Get single event by ID
|
|
1497
|
+
* @param {string} eventId - Event ID (hex)
|
|
1498
|
+
* @returns {Promise<Object|null>} Event or null
|
|
1499
|
+
*/
|
|
1500
|
+
async getEvent(eventId) {
|
|
1501
|
+
const events = await this.query({ ids: [eventId] });
|
|
1502
|
+
return events.length > 0 ? events[0] : null;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Clear the event cache (or specific entries matching a pattern)
|
|
1507
|
+
* @param {string} [pattern] - Optional d-tag pattern to match (clears all if not provided)
|
|
1508
|
+
*/
|
|
1509
|
+
clearCache(pattern = null) {
|
|
1510
|
+
if (!pattern) {
|
|
1511
|
+
this._eventCache.clear();
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Clear entries matching the pattern
|
|
1516
|
+
for (const key of this._eventCache.keys()) {
|
|
1517
|
+
if (key.includes(pattern)) {
|
|
1518
|
+
this._eventCache.delete(key);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Close all connections and subscriptions.
|
|
1525
|
+
*
|
|
1526
|
+
* @param {Object} [options={}] - Close options
|
|
1527
|
+
* @param {boolean} [options.flush=true] - Flush pending writes before closing
|
|
1528
|
+
* @returns {Promise<void>}
|
|
1529
|
+
*/
|
|
1530
|
+
async close(options = {}) {
|
|
1531
|
+
const shouldFlush = options.flush !== false;
|
|
1532
|
+
|
|
1533
|
+
// Flush pending persistent writes before closing
|
|
1534
|
+
if (shouldFlush && this._persistTimer) {
|
|
1535
|
+
clearTimeout(this._persistTimer);
|
|
1536
|
+
await this._flushPersistQueue();
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Stop background sync service
|
|
1540
|
+
if (this.syncService) {
|
|
1541
|
+
this.syncService.stop();
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Close long-lived author subscription
|
|
1545
|
+
const authorSub = authorSubscriptions.get(this.publicKey);
|
|
1546
|
+
if (authorSub && authorSub.subscription) {
|
|
1547
|
+
if (authorSub.subscription.close) {
|
|
1548
|
+
authorSub.subscription.close();
|
|
1549
|
+
}
|
|
1550
|
+
authorSubscriptions.delete(this.publicKey);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Close all subscriptions
|
|
1554
|
+
for (const sub of this._subscriptions.values()) {
|
|
1555
|
+
if (sub.close) {
|
|
1556
|
+
sub.close();
|
|
1557
|
+
} else if (sub.active !== undefined) {
|
|
1558
|
+
// Mock subscription
|
|
1559
|
+
sub.active = false;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
this._subscriptions.clear();
|
|
1563
|
+
|
|
1564
|
+
// Close pool
|
|
1565
|
+
this.pool.close(this.relays);
|
|
1566
|
+
|
|
1567
|
+
// Clear cache and index
|
|
1568
|
+
this._eventCache.clear();
|
|
1569
|
+
this._cacheIndex.clear();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Get relay status
|
|
1574
|
+
* @returns {Array} Relay connection statuses
|
|
1575
|
+
*/
|
|
1576
|
+
getRelayStatus() {
|
|
1577
|
+
return this.relays.map(relay => ({
|
|
1578
|
+
url: relay,
|
|
1579
|
+
connected: true, // SimplePool manages this internally
|
|
1580
|
+
}));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Create NostrClient instance.
|
|
1586
|
+
*
|
|
1587
|
+
* @param {Object} config - Configuration options
|
|
1588
|
+
* @returns {NostrClient} Client instance
|
|
1589
|
+
* @example
|
|
1590
|
+
* const client = createClient({
|
|
1591
|
+
* relays: ['wss://relay.example.com'],
|
|
1592
|
+
* appName: 'myapp'
|
|
1593
|
+
* });
|
|
1594
|
+
*/
|
|
1595
|
+
export function createClient(config) {
|
|
1596
|
+
return new NostrClient(config);
|
|
1597
|
+
}
|
|
1598
|
+
</code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><div class="mobile-nav-links"></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-ai.html">ai</a></div><div class="sidebar-section-children"><a href="module-ai_aggregation.html">ai/aggregation</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown.html">ai/breakdown</a></div><div class="sidebar-section-children"><a href="module-ai_classifier.html">ai/classifier</a></div><div class="sidebar-section-children"><a href="module-ai_council.html">ai/council</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings.html">ai/embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai.html">ai/federation-ai</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai.html">ai/h3-ai</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops.html">ai/json-ops</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service.html">ai/llm-service</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query.html">ai/nl-query</a></div><div class="sidebar-section-children"><a href="module-ai_relationships.html">ai/relationships</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor.html">ai/schema-extractor</a></div><div class="sidebar-section-children"><a href="module-ai_spatial.html">ai/spatial</a></div><div class="sidebar-section-children"><a href="module-ai_tts.html">ai/tts</a></div><div class="sidebar-section-children"><a href="module-content_social-protocols.html">content/social-protocols</a></div><div class="sidebar-section-children"><a href="module-core_holosphere.html">core/holosphere</a></div><div class="sidebar-section-children"><a href="module-crypto_nostr-utils.html">crypto/nostr-utils</a></div><div class="sidebar-section-children"><a href="module-crypto_secp256k1.html">crypto/secp256k1</a></div><div class="sidebar-section-children"><a href="module-federation_hologram.html">federation/hologram</a></div><div class="sidebar-section-children"><a href="module-hierarchical_upcast.html">hierarchical/upcast</a></div><div class="sidebar-section-children"><a href="module-holosphere.html">holosphere</a></div><div class="sidebar-section-children"><a href="module-lib_ai-methods.html">lib/ai-methods</a></div><div class="sidebar-section-children"><a href="module-lib_contract-methods.html">lib/contract-methods</a></div><div class="sidebar-section-children"><a href="module-lib_errors.html">lib/errors</a></div><div class="sidebar-section-children"><a href="module-lib_federation-methods.html">lib/federation-methods</a></div><div class="sidebar-section-children"><a href="module-lib_index.html">lib/index</a></div><div class="sidebar-section-children"><a href="module-schema_validator.html">schema/validator</a></div><div class="sidebar-section-children"><a href="module-spatial_h3-operations.html">spatial/h3-operations</a></div><div class="sidebar-section-children"><a href="module-storage_backend-factory.html">storage/backend-factory</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface.html">storage/backend-interface</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend.html">storage/backends/activitypub-backend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server.html">storage/backends/activitypub/server</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend.html">storage/backends/gundb-backend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend.html">storage/backends/nostr-backend</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage.html">storage/filesystem-storage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser.html">storage/filesystem-storage-browser</a></div><div class="sidebar-section-children"><a href="module-storage_global-tables.html">storage/global-tables</a></div><div class="sidebar-section-children"><a href="module-storage_gun-async.html">storage/gun-async</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth.html">storage/gun-auth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-federation.html">storage/gun-federation</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references.html">storage/gun-references</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema.html">storage/gun-schema</a></div><div class="sidebar-section-children"><a href="module-storage_gun-wrapper.html">storage/gun-wrapper</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage.html">storage/indexeddb-storage</a></div><div class="sidebar-section-children"><a href="module-storage_key-storage.html">storage/key-storage</a></div><div class="sidebar-section-children"><a href="module-storage_key-storage-simple.html">storage/key-storage-simple</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage.html">storage/memory-storage</a></div><div class="sidebar-section-children"><a href="module-storage_migration.html">storage/migration</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-async.html">storage/nostr-async</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client.html">storage/nostr-client</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-wrapper.html">storage/nostr-wrapper</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue.html">storage/outbox-queue</a></div><div class="sidebar-section-children"><a href="module-storage_persistent-storage.html">storage/persistent-storage</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service.html">storage/sync-service</a></div><div class="sidebar-section-children"><a href="module-storage_unified-storage.html">storage/unified-storage</a></div><div class="sidebar-section-children"><a href="module-subscriptions_manager.html">subscriptions/manager</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-ai_aggregation.SmartAggregation.html">SmartAggregation</a></div><div class="sidebar-section-children"><a href="module-ai_aggregation-SmartAggregation.html">SmartAggregation</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown.TaskBreakdown.html">TaskBreakdown</a></div><div class="sidebar-section-children"><a href="module-ai_breakdown-TaskBreakdown.html">TaskBreakdown</a></div><div class="sidebar-section-children"><a href="module-ai_classifier.Classifier.html">Classifier</a></div><div class="sidebar-section-children"><a href="module-ai_classifier-Classifier.html">Classifier</a></div><div class="sidebar-section-children"><a href="module-ai_council.Council.html">Council</a></div><div class="sidebar-section-children"><a href="module-ai_council-Council.html">Council</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings.Embeddings.html">Embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_embeddings-Embeddings.html">Embeddings</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai.FederationAdvisor.html">FederationAdvisor</a></div><div class="sidebar-section-children"><a href="module-ai_federation-ai-FederationAdvisor.html">FederationAdvisor</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai.H3AI.html">H3AI</a></div><div class="sidebar-section-children"><a href="module-ai_h3-ai-H3AI.html">H3AI</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops.JSONOps.html">JSONOps</a></div><div class="sidebar-section-children"><a href="module-ai_json-ops-JSONOps.html">JSONOps</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service.LLMService.html">LLMService</a></div><div class="sidebar-section-children"><a href="module-ai_llm-service-LLMService.html">LLMService</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query.NLQuery.html">NLQuery</a></div><div class="sidebar-section-children"><a href="module-ai_nl-query-NLQuery.html">NLQuery</a></div><div class="sidebar-section-children"><a href="module-ai_relationships.RelationshipDiscovery.html">RelationshipDiscovery</a></div><div class="sidebar-section-children"><a href="module-ai_relationships-RelationshipDiscovery.html">RelationshipDiscovery</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor.SchemaExtractor.html">SchemaExtractor</a></div><div class="sidebar-section-children"><a href="module-ai_schema-extractor-SchemaExtractor.html">SchemaExtractor</a></div><div class="sidebar-section-children"><a href="module-ai_spatial.SpatialAnalysis.html">SpatialAnalysis</a></div><div class="sidebar-section-children"><a href="module-ai_spatial-SpatialAnalysis.html">SpatialAnalysis</a></div><div class="sidebar-section-children"><a href="module-ai_tts.TTS.html">TTS</a></div><div class="sidebar-section-children"><a href="module-ai_tts-TTS.html">TTS</a></div><div class="sidebar-section-children"><a href="module-core_holosphere.HoloSphere.html">HoloSphere</a></div><div class="sidebar-section-children"><a href="module-core_holosphere-HoloSphere.html">HoloSphere</a></div><div class="sidebar-section-children"><a href="module-holosphere-HoloSphereBase.html">HoloSphereBase</a></div><div class="sidebar-section-children"><a href="module-holosphere-HoloSphereBase.html">HoloSphereBase</a></div><div class="sidebar-section-children"><a href="module-lib_errors.AuthorizationError.html">AuthorizationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors.ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors-AuthorizationError.html">AuthorizationError</a></div><div class="sidebar-section-children"><a href="module-lib_errors-ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-schema_validator.ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-schema_validator-ValidationError.html">ValidationError</a></div><div class="sidebar-section-children"><a href="module-storage_backend-factory.BackendFactory.html">BackendFactory</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface.StorageBackend.html">StorageBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backend-interface-StorageBackend.html">StorageBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend.ActivityPubBackend.html">ActivityPubBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub-backend-ActivityPubBackend.html">ActivityPubBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server.ActivityPubServer.html">ActivityPubServer</a></div><div class="sidebar-section-children"><a href="module-storage_backends_activitypub_server-ActivityPubServer.html">ActivityPubServer</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend.GunDBBackend.html">GunDBBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_gundb-backend-GunDBBackend.html">GunDBBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend.NostrBackend.html">NostrBackend</a></div><div class="sidebar-section-children"><a href="module-storage_backends_nostr-backend-NostrBackend.html">NostrBackend</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser.FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-browser-FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage.FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_filesystem-storage-FileSystemStorage.html">FileSystemStorage</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth.GunAuth.html">GunAuth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-auth-GunAuth.html">GunAuth</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references.GunReferenceHandler.html">GunReferenceHandler</a></div><div class="sidebar-section-children"><a href="module-storage_gun-references-GunReferenceHandler.html">GunReferenceHandler</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema.GunSchemaValidator.html">GunSchemaValidator</a></div><div class="sidebar-section-children"><a href="module-storage_gun-schema-GunSchemaValidator.html">GunSchemaValidator</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage.IndexedDBStorage.html">IndexedDBStorage</a></div><div class="sidebar-section-children"><a href="module-storage_indexeddb-storage-IndexedDBStorage.html">IndexedDBStorage</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage.MemoryStorage.html">MemoryStorage</a></div><div class="sidebar-section-children"><a href="module-storage_memory-storage-MemoryStorage.html">MemoryStorage</a></div><div class="sidebar-section-children"><a href="module-storage_migration.MigrationTool.html">MigrationTool</a></div><div class="sidebar-section-children"><a href="module-storage_migration-MigrationTool.html">MigrationTool</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client.NostrClient.html">NostrClient</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client-LRUCache.html">LRUCache</a></div><div class="sidebar-section-children"><a href="module-storage_nostr-client-NostrClient.html">NostrClient</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue.OutboxQueue.html">OutboxQueue</a></div><div class="sidebar-section-children"><a href="module-storage_outbox-queue-OutboxQueue.html">OutboxQueue</a></div><div class="sidebar-section-children"><a href="module-storage_persistent-storage-PersistentStorage.html">PersistentStorage</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service.SyncService.html">SyncService</a></div><div class="sidebar-section-children"><a href="module-storage_sync-service-SyncService.html">SyncService</a></div><div class="sidebar-section-children"><a href="module-subscriptions_manager.SubscriptionRegistry.html">SubscriptionRegistry</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#FederationRequestPayload">FederationRequestPayload</a></div><div class="sidebar-section-children"><a href="global.html#FederationResponsePayload">FederationResponsePayload</a></div><div class="sidebar-section-children"><a href="global.html#acceptFederationRequest">acceptFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#acceptFederationRequest">acceptFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#addFederatedPartner">addFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#cleanupExpiredCapabilities">cleanupExpiredCapabilities</a></div><div class="sidebar-section-children"><a href="global.html#createFederationRequest">createFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#createFederationResponse">createFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#declineFederationRequest">declineFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#getCapabilityForAuthor">getCapabilityForAuthor</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedAuthors">getFederatedAuthors</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedAuthorsForScope">getFederatedAuthorsForScope</a></div><div class="sidebar-section-children"><a href="global.html#getFederatedPartner">getFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#getFederationRegistry">getFederationRegistry</a></div><div class="sidebar-section-children"><a href="global.html#getPendingFederationRequests">getPendingFederationRequests</a></div><div class="sidebar-section-children"><a href="global.html#initiateFederationHandshake">initiateFederationHandshake</a></div><div class="sidebar-section-children"><a href="global.html#isFederationRequest">isFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#isFederationResponse">isFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#rejectFederationRequest">rejectFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#removeFederatedPartner">removeFederatedPartner</a></div><div class="sidebar-section-children"><a href="global.html#revokeOutboundCapability">revokeOutboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#saveFederationRegistry">saveFederationRegistry</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationRequest">sendFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationRequest">sendFederationRequest</a></div><div class="sidebar-section-children"><a href="global.html#sendFederationResponse">sendFederationResponse</a></div><div class="sidebar-section-children"><a href="global.html#storeInboundCapability">storeInboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#storeOutboundCapability">storeOutboundCapability</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationAcceptances">subscribeFederationAcceptances</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationDeclines">subscribeFederationDeclines</a></div><div class="sidebar-section-children"><a href="global.html#subscribeFederationRequests">subscribeFederationRequests</a></div><div class="sidebar-section-children"><a href="global.html#subscribeToFederationDMs">subscribeToFederationDMs</a></div><div class="sidebar-section-children"><a href="global.html#updateDiscoverySettings">updateDiscoverySettings</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>
|