holosphere 2.0.0-alpha1 → 2.0.0-alpha10
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 +473 -0
- package/FEATURES.md +431 -0
- package/LICENSE +29 -166
- package/LICENSE-AGPL.md +180 -0
- package/README.md +97 -16
- package/dist/2019-D2OG2idw.js +6680 -0
- package/dist/2019-D2OG2idw.js.map +1 -0
- package/dist/2019-EION3wKo.cjs +8 -0
- package/dist/2019-EION3wKo.cjs.map +1 -0
- package/dist/_commonjsHelpers-C37NGDzP.cjs +2 -0
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +1 -0
- package/dist/_commonjsHelpers-CUmg6egw.js +7 -0
- package/dist/_commonjsHelpers-CUmg6egw.js.map +1 -0
- package/dist/browser-BSniCNqO.js +3058 -0
- package/dist/browser-BSniCNqO.js.map +1 -0
- package/dist/browser-Cq59Ij19.cjs +2 -0
- package/dist/browser-Cq59Ij19.cjs.map +1 -0
- package/dist/cdn/holosphere.min.js +55 -0
- package/dist/cdn/holosphere.min.js.map +1 -0
- package/dist/cjs/holosphere.cjs +2 -0
- package/dist/cjs/holosphere.cjs.map +1 -0
- package/dist/esm/holosphere.js +53 -0
- package/dist/esm/holosphere.js.map +1 -0
- package/dist/index-DDGt_V9o.cjs +12 -0
- package/dist/index-DDGt_V9o.cjs.map +1 -0
- package/dist/index-DJXftyvB.js +39841 -0
- 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-DeZ1xz_s.js +15104 -0
- package/dist/index-DeZ1xz_s.js.map +1 -0
- package/dist/indexeddb-storage-BFt6hMeF.js +176 -0
- package/dist/indexeddb-storage-BFt6hMeF.js.map +1 -0
- package/dist/indexeddb-storage-BK5tv4Sh.cjs +2 -0
- package/dist/indexeddb-storage-BK5tv4Sh.cjs.map +1 -0
- package/dist/memory-storage-C9HuoL2E.js +91 -0
- package/dist/memory-storage-C9HuoL2E.js.map +1 -0
- package/dist/memory-storage-Dao7jfYG.cjs +2 -0
- package/dist/memory-storage-Dao7jfYG.cjs.map +1 -0
- package/dist/secp256k1-BbKzbLtD.cjs +12 -0
- package/dist/secp256k1-BbKzbLtD.cjs.map +1 -0
- package/dist/secp256k1-CreY7Pcl.js +1890 -0
- package/dist/secp256k1-CreY7Pcl.js.map +1 -0
- package/docs/CONTRACTS.md +797 -0
- package/docs/FOSDEM_PROPOSAL.md +388 -0
- package/docs/LOCALFIRST.md +266 -0
- 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/docs/contracts/api-interface.md +793 -0
- package/docs/data-model.md +476 -0
- package/docs/gun-async-usage.md +338 -0
- package/docs/plan.md +349 -0
- package/docs/quickstart.md +674 -0
- package/docs/research.md +362 -0
- package/docs/spec.md +244 -0
- package/docs/storage-backends.md +326 -0
- package/docs/tasks.md +947 -0
- package/examples/demo.html +47 -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 +25 -7
- 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/abis/Appreciative.json +1280 -0
- package/src/contracts/abis/AppreciativeFactory.json +101 -0
- package/src/contracts/abis/Bundle.json +1438 -0
- package/src/contracts/abis/BundleFactory.json +106 -0
- package/src/contracts/abis/Holon.json +881 -0
- package/src/contracts/abis/Holons.json +330 -0
- package/src/contracts/abis/Managed.json +1262 -0
- package/src/contracts/abis/ManagedFactory.json +149 -0
- package/src/contracts/abis/Membrane.json +261 -0
- package/src/contracts/abis/Splitter.json +1624 -0
- package/src/contracts/abis/SplitterFactory.json +220 -0
- package/src/contracts/abis/TestToken.json +321 -0
- package/src/contracts/abis/Zoned.json +1461 -0
- package/src/contracts/abis/ZonedFactory.json +154 -0
- package/src/contracts/chain-manager.js +403 -0
- package/src/contracts/deployer.js +500 -0
- package/src/contracts/event-listener.js +539 -0
- package/src/contracts/holon-contracts.js +359 -0
- package/src/contracts/index.js +82 -0
- package/src/contracts/networks.js +229 -0
- package/src/contracts/operations.js +687 -0
- package/src/contracts/queries.js +638 -0
- package/src/core/holosphere.js +487 -6
- package/src/crypto/nostr-utils.js +303 -0
- package/src/crypto/secp256k1.js +7 -2
- package/src/federation/handshake.js +475 -0
- package/src/federation/hologram.js +117 -3
- package/src/hierarchical/upcast.js +40 -25
- package/src/index.js +1501 -1909
- package/src/lib/ai-methods.js +657 -0
- package/src/lib/contract-methods.js +442 -0
- package/src/lib/errors.js +53 -0
- package/src/lib/federation-methods.js +345 -0
- package/src/lib/index.js +30 -0
- package/src/schema/validator.js +22 -3
- package/src/spatial/h3-operations.js +19 -3
- 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 +692 -50
- 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 +35 -3
- package/src/storage/gun-async.js +75 -15
- package/src/storage/gun-auth.js +373 -0
- package/src/storage/gun-federation.js +785 -0
- package/src/storage/gun-references.js +209 -0
- package/src/storage/gun-schema.js +306 -0
- package/src/storage/gun-wrapper.js +475 -54
- package/src/storage/indexeddb-storage.js +112 -13
- package/src/storage/key-storage-simple.js +32 -9
- package/src/storage/key-storage.js +45 -13
- package/src/storage/memory-storage.js +68 -2
- package/src/storage/migration.js +20 -7
- package/src/storage/nostr-async.js +412 -122
- package/src/storage/nostr-client.js +749 -76
- package/src/storage/nostr-wrapper.js +6 -2
- package/src/storage/outbox-queue.js +55 -18
- package/src/storage/persistent-storage.js +62 -14
- package/src/storage/sync-service.js +51 -17
- package/src/storage/unified-storage.js +154 -0
- package/src/subscriptions/manager.js +34 -17
- package/types/index.d.ts +133 -0
- package/vite.config.cdn.js +60 -0
- package/tests/unit/ai/aggregation.test.js +0 -295
- package/tests/unit/ai/breakdown.test.js +0 -446
- package/tests/unit/ai/classifier.test.js +0 -294
- package/tests/unit/ai/council.test.js +0 -262
- package/tests/unit/ai/embeddings.test.js +0 -384
- package/tests/unit/ai/federation-ai.test.js +0 -344
- package/tests/unit/ai/h3-ai.test.js +0 -458
- package/tests/unit/ai/index.test.js +0 -304
- package/tests/unit/ai/json-ops.test.js +0 -307
- package/tests/unit/ai/llm-service.test.js +0 -390
- package/tests/unit/ai/nl-query.test.js +0 -383
- package/tests/unit/ai/relationships.test.js +0 -311
- package/tests/unit/ai/schema-extractor.test.js +0 -384
- package/tests/unit/ai/spatial.test.js +0 -279
- package/tests/unit/ai/tts.test.js +0 -279
- package/tests/unit/content.test.js +0 -332
- package/tests/unit/contract/core.test.js +0 -88
- package/tests/unit/contract/crypto.test.js +0 -198
- package/tests/unit/contract/data.test.js +0 -223
- package/tests/unit/contract/federation.test.js +0 -181
- package/tests/unit/contract/hierarchical.test.js +0 -113
- package/tests/unit/contract/schema.test.js +0 -114
- package/tests/unit/contract/social.test.js +0 -217
- package/tests/unit/contract/spatial.test.js +0 -110
- package/tests/unit/contract/subscriptions.test.js +0 -128
- package/tests/unit/contract/utils.test.js +0 -159
- package/tests/unit/core.test.js +0 -152
- package/tests/unit/crypto.test.js +0 -328
- package/tests/unit/federation.test.js +0 -234
- package/tests/unit/gun-async.test.js +0 -252
- package/tests/unit/hierarchical.test.js +0 -399
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +0 -74
- package/tests/unit/integration/scenario-02-federation.test.js +0 -76
- package/tests/unit/integration/scenario-03-subscriptions.test.js +0 -102
- package/tests/unit/integration/scenario-04-validation.test.js +0 -129
- package/tests/unit/integration/scenario-05-hierarchy.test.js +0 -125
- package/tests/unit/integration/scenario-06-social.test.js +0 -135
- package/tests/unit/integration/scenario-07-persistence.test.js +0 -130
- package/tests/unit/integration/scenario-08-authorization.test.js +0 -161
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +0 -139
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +0 -357
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +0 -410
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +0 -719
- package/tests/unit/performance/benchmark.test.js +0 -85
- package/tests/unit/schema.test.js +0 -213
- package/tests/unit/spatial.test.js +0 -158
- package/tests/unit/storage.test.js +0 -195
- package/tests/unit/subscriptions.test.js +0 -328
- package/tests/unit/test-data-permanence-debug.js +0 -197
- package/tests/unit/test-data-permanence.js +0 -340
- package/tests/unit/test-key-persistence-fixed.js +0 -148
- package/tests/unit/test-key-persistence.js +0 -172
- package/tests/unit/test-relay-permanence.js +0 -376
- package/tests/unit/test-second-node.js +0 -95
- package/tests/unit/test-simple-write.js +0 -89
- /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
|
@@ -1,12 +1,193 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nostr Client - SimplePool wrapper for relay management
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Nostr Client - SimplePool wrapper for relay management.
|
|
3
|
+
*
|
|
4
|
+
* Provides connection pooling, relay management, event caching, persistent storage,
|
|
5
|
+
* outbox queue for guaranteed delivery, and background sync services.
|
|
6
|
+
*
|
|
7
|
+
* @module storage/nostr-client
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools';
|
|
7
11
|
import { OutboxQueue } from './outbox-queue.js';
|
|
8
12
|
import { SyncService } from './sync-service.js';
|
|
9
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Global pool singleton - reuse connections across NostrClient instances.
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
let globalPool = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set of relays used by the global pool.
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
let globalPoolRelays = new Set();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get or create global SimplePool singleton.
|
|
28
|
+
* This ensures WebSocket connections are reused across all operations.
|
|
29
|
+
*
|
|
30
|
+
* @private
|
|
31
|
+
* @param {Object} [config={}] - Pool configuration
|
|
32
|
+
* @param {boolean} [config.enableReconnect=true] - Enable auto-reconnect
|
|
33
|
+
* @param {boolean} [config.enablePing=true] - Enable ping/pong
|
|
34
|
+
* @returns {SimplePool} The global SimplePool instance
|
|
35
|
+
*/
|
|
36
|
+
function getGlobalPool(config = {}) {
|
|
37
|
+
if (!globalPool) {
|
|
38
|
+
globalPool = new SimplePool({
|
|
39
|
+
enableReconnect: config.enableReconnect !== false,
|
|
40
|
+
enablePing: config.enablePing !== false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return globalPool;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Global pending queries map for deduplication.
|
|
48
|
+
* Key: JSON-stringified filter, Value: { promise, timestamp }
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
const pendingQueries = new Map();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pending query timeout (5 seconds).
|
|
55
|
+
* @private
|
|
56
|
+
* @constant {number}
|
|
57
|
+
*/
|
|
58
|
+
const PENDING_QUERY_TIMEOUT = 5000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Global active subscriptions map for subscription deduplication.
|
|
62
|
+
* Key: JSON-stringified filter, Value: { subscription, callbacks: Set, refCount }
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
const activeSubscriptions = new Map();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Throttle background refreshes to avoid flooding the relay.
|
|
69
|
+
* Key: path, Value: timestamp of last refresh
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
const backgroundRefreshThrottle = new Map();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Background refresh interval (30 seconds).
|
|
76
|
+
* @private
|
|
77
|
+
* @constant {number}
|
|
78
|
+
*/
|
|
79
|
+
const BACKGROUND_REFRESH_INTERVAL = 30000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Write debouncing for rapid updates to the same path.
|
|
83
|
+
* Key: d-tag path, Value: { event, timer, resolve, reject }
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
const pendingWrites = new Map();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Write debounce window (500ms).
|
|
90
|
+
* @private
|
|
91
|
+
* @constant {number}
|
|
92
|
+
*/
|
|
93
|
+
const WRITE_DEBOUNCE_MS = 500;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Long-lived subscription manager - keeps ONE subscription per author for real-time updates.
|
|
97
|
+
* Key: pubkey, Value: { subscription, lastEventTime, initialized }
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
const authorSubscriptions = new Map();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Author subscription initialization timeout (5 seconds).
|
|
104
|
+
* @private
|
|
105
|
+
* @constant {number}
|
|
106
|
+
*/
|
|
107
|
+
const AUTHOR_SUB_INIT_TIMEOUT = 5000;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Simple LRU Cache implementation.
|
|
111
|
+
* Automatically evicts least recently used entries when max size is reached.
|
|
112
|
+
*
|
|
113
|
+
* @private
|
|
114
|
+
* @class LRUCache
|
|
115
|
+
*/
|
|
116
|
+
class LRUCache {
|
|
117
|
+
/**
|
|
118
|
+
* Create a new LRU cache.
|
|
119
|
+
*
|
|
120
|
+
* @param {number} [maxSize=500] - Maximum number of entries
|
|
121
|
+
*/
|
|
122
|
+
constructor(maxSize = 500) {
|
|
123
|
+
this.maxSize = maxSize;
|
|
124
|
+
this.cache = new Map();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get an entry from the cache.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} key - Cache key
|
|
131
|
+
* @returns {*} Cached value or undefined
|
|
132
|
+
*/
|
|
133
|
+
get(key) {
|
|
134
|
+
if (!this.cache.has(key)) return undefined;
|
|
135
|
+
|
|
136
|
+
// Move to end (most recently used)
|
|
137
|
+
const value = this.cache.get(key);
|
|
138
|
+
this.cache.delete(key);
|
|
139
|
+
this.cache.set(key, value);
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set(key, value) {
|
|
144
|
+
// If key exists, delete it first to update position
|
|
145
|
+
if (this.cache.has(key)) {
|
|
146
|
+
this.cache.delete(key);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.cache.set(key, value);
|
|
150
|
+
|
|
151
|
+
// Evict oldest entries if over capacity
|
|
152
|
+
while (this.cache.size > this.maxSize) {
|
|
153
|
+
const oldestKey = this.cache.keys().next().value;
|
|
154
|
+
this.cache.delete(oldestKey);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
has(key) {
|
|
159
|
+
return this.cache.has(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
delete(key) {
|
|
163
|
+
return this.cache.delete(key);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
clear() {
|
|
167
|
+
this.cache.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get size() {
|
|
171
|
+
return this.cache.size;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
keys() {
|
|
175
|
+
return this.cache.keys();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
values() {
|
|
179
|
+
return this.cache.values();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
entries() {
|
|
183
|
+
return this.cache.entries();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
forEach(callback) {
|
|
187
|
+
this.cache.forEach(callback);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
10
191
|
// Lazy-load WebSocket polyfill for Node.js environment
|
|
11
192
|
let webSocketPolyfillPromise = null;
|
|
12
193
|
function ensureWebSocket() {
|
|
@@ -30,17 +211,35 @@ function ensureWebSocket() {
|
|
|
30
211
|
import { createPersistentStorage } from './persistent-storage.js';
|
|
31
212
|
|
|
32
213
|
/**
|
|
33
|
-
* NostrClient - Manages connections to Nostr relays
|
|
214
|
+
* NostrClient - Manages connections to Nostr relays.
|
|
215
|
+
*
|
|
216
|
+
* Provides event publishing, querying, subscription management, persistent caching,
|
|
217
|
+
* and guaranteed delivery via outbox queue.
|
|
218
|
+
*
|
|
219
|
+
* @class NostrClient
|
|
220
|
+
* @example
|
|
221
|
+
* const client = new NostrClient({
|
|
222
|
+
* relays: ['wss://relay.example.com'],
|
|
223
|
+
* appName: 'myapp',
|
|
224
|
+
* persistence: true
|
|
225
|
+
* });
|
|
34
226
|
*/
|
|
35
227
|
export class NostrClient {
|
|
36
228
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param {
|
|
40
|
-
* @param {
|
|
41
|
-
* @param {
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {boolean} config.
|
|
229
|
+
* Create a new NostrClient.
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} [config={}] - Configuration options
|
|
232
|
+
* @param {string[]} [config.relays=[]] - Array of relay URLs
|
|
233
|
+
* @param {string} [config.privateKey] - Private key for signing (hex format)
|
|
234
|
+
* @param {boolean} [config.enableReconnect=true] - Auto-reconnect on disconnect
|
|
235
|
+
* @param {boolean} [config.enablePing=true] - Auto-ping relays
|
|
236
|
+
* @param {string} [config.appName] - Application name for storage namespace
|
|
237
|
+
* @param {boolean} [config.persistence=true] - Enable persistent storage
|
|
238
|
+
* @param {number} [config.cacheSize=500] - Maximum cache size
|
|
239
|
+
* @param {number} [config.persistBatchMs=100] - Batch persist writes within this window
|
|
240
|
+
* @param {boolean} [config.backgroundSync=true] - Enable background sync service
|
|
241
|
+
* @param {string} [config.dataDir] - Data directory for persistent storage
|
|
242
|
+
* @throws {Error} If relays is not an array
|
|
44
243
|
*/
|
|
45
244
|
constructor(config = {}) {
|
|
46
245
|
if (config.relays && !Array.isArray(config.relays)) {
|
|
@@ -57,16 +256,24 @@ export class NostrClient {
|
|
|
57
256
|
this.config = config;
|
|
58
257
|
|
|
59
258
|
this._subscriptions = new Map();
|
|
60
|
-
this._eventCache = new
|
|
259
|
+
this._eventCache = new LRUCache(config.cacheSize || 500); // LRU cache for recent events
|
|
260
|
+
this._cacheIndex = new Map(); // Reverse index: kind -> Set of cache keys affected by that kind
|
|
61
261
|
this.persistentStorage = null;
|
|
62
262
|
|
|
263
|
+
// Batched persistent writes for better I/O performance
|
|
264
|
+
this._persistQueue = new Map(); // path -> event
|
|
265
|
+
this._persistTimer = null;
|
|
266
|
+
this._persistBatchMs = config.persistBatchMs || 100; // Batch writes within 100ms window
|
|
267
|
+
|
|
63
268
|
// Initialize pool and storage asynchronously
|
|
64
269
|
this._initReady = this._initialize();
|
|
65
270
|
}
|
|
66
271
|
|
|
67
272
|
/**
|
|
68
|
-
* Initialize WebSocket polyfill and pool
|
|
273
|
+
* Initialize WebSocket polyfill and pool.
|
|
274
|
+
*
|
|
69
275
|
* @private
|
|
276
|
+
* @returns {Promise<void>}
|
|
70
277
|
*/
|
|
71
278
|
async _initialize() {
|
|
72
279
|
// Ensure WebSocket is available before initializing pool
|
|
@@ -74,10 +281,11 @@ export class NostrClient {
|
|
|
74
281
|
|
|
75
282
|
// Initialize SimplePool with options (only if relays exist)
|
|
76
283
|
if (this.relays.length > 0) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
284
|
+
// Use global pool singleton to reuse WebSocket connections
|
|
285
|
+
this.pool = getGlobalPool(this.config);
|
|
286
|
+
|
|
287
|
+
// Track relays used by this client
|
|
288
|
+
this.relays.forEach(r => globalPoolRelays.add(r));
|
|
81
289
|
} else {
|
|
82
290
|
// Mock pool for testing - returns mock promise that resolves immediately
|
|
83
291
|
this.pool = {
|
|
@@ -90,6 +298,90 @@ export class NostrClient {
|
|
|
90
298
|
|
|
91
299
|
// Initialize persistent storage
|
|
92
300
|
await this._initPersistentStorage();
|
|
301
|
+
|
|
302
|
+
// Start long-lived subscription for real-time cache updates
|
|
303
|
+
if (this.relays.length > 0) {
|
|
304
|
+
this._initLongLivedSubscription();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Initialize a long-lived subscription to keep cache fresh
|
|
310
|
+
* This replaces polling with a single persistent subscription
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
313
|
+
_initLongLivedSubscription() {
|
|
314
|
+
const subKey = this.publicKey;
|
|
315
|
+
|
|
316
|
+
// Check if subscription already exists for this author
|
|
317
|
+
if (authorSubscriptions.has(subKey)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const subInfo = {
|
|
322
|
+
subscription: null,
|
|
323
|
+
initialized: false,
|
|
324
|
+
initPromise: null,
|
|
325
|
+
initResolve: null,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Create promise for initial load completion
|
|
329
|
+
subInfo.initPromise = new Promise(resolve => {
|
|
330
|
+
subInfo.initResolve = resolve;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
authorSubscriptions.set(subKey, subInfo);
|
|
334
|
+
|
|
335
|
+
// Subscribe to ALL events for this author (kind 30000)
|
|
336
|
+
// This single subscription replaces all the polling queries
|
|
337
|
+
const filter = {
|
|
338
|
+
kinds: [30000],
|
|
339
|
+
authors: [this.publicKey],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const sub = this.pool.subscribeMany(
|
|
343
|
+
this.relays,
|
|
344
|
+
[filter],
|
|
345
|
+
{
|
|
346
|
+
onevent: (event) => {
|
|
347
|
+
// Verify author (relay may not respect filter)
|
|
348
|
+
if (event.pubkey !== this.publicKey) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Cache the event - this keeps our local cache in sync
|
|
353
|
+
this._cacheEvent(event);
|
|
354
|
+
},
|
|
355
|
+
oneose: () => {
|
|
356
|
+
// End of stored events - initial load complete
|
|
357
|
+
if (!subInfo.initialized) {
|
|
358
|
+
subInfo.initialized = true;
|
|
359
|
+
subInfo.initResolve();
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
subInfo.subscription = sub;
|
|
366
|
+
|
|
367
|
+
// Set timeout for initial load in case EOSE never arrives
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
if (!subInfo.initialized) {
|
|
370
|
+
subInfo.initialized = true;
|
|
371
|
+
subInfo.initResolve();
|
|
372
|
+
}
|
|
373
|
+
}, AUTHOR_SUB_INIT_TIMEOUT);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Wait for long-lived subscription to complete initial load
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
async _waitForSubscriptionInit() {
|
|
381
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
382
|
+
if (subInfo && subInfo.initPromise) {
|
|
383
|
+
await subInfo.initPromise;
|
|
384
|
+
}
|
|
93
385
|
}
|
|
94
386
|
|
|
95
387
|
/**
|
|
@@ -169,6 +461,23 @@ export class NostrClient {
|
|
|
169
461
|
}
|
|
170
462
|
}
|
|
171
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Get event from LRU cache by d-tag path (for local-first reads).
|
|
466
|
+
* This is always up-to-date because writes update the LRU cache synchronously,
|
|
467
|
+
* unlike persistent storage which uses batched writes.
|
|
468
|
+
* @param {string} path - The d-tag path
|
|
469
|
+
* @param {number} [kind=30000] - Event kind
|
|
470
|
+
* @returns {Object|null} The cached event or null
|
|
471
|
+
*/
|
|
472
|
+
getCachedByPath(path, kind = 30000) {
|
|
473
|
+
const dTagKey = `d:${kind}:${path}`;
|
|
474
|
+
const cached = this._eventCache.get(dTagKey);
|
|
475
|
+
if (cached && cached.events && cached.events[0]) {
|
|
476
|
+
return cached.events[0];
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
172
481
|
/**
|
|
173
482
|
* Get all events from persistent storage matching a prefix
|
|
174
483
|
* @param {string} prefix - The path prefix to match
|
|
@@ -221,11 +530,21 @@ export class NostrClient {
|
|
|
221
530
|
}
|
|
222
531
|
|
|
223
532
|
/**
|
|
224
|
-
* Publish event to relays
|
|
533
|
+
* Publish event to relays.
|
|
534
|
+
*
|
|
535
|
+
* Supports debouncing for replaceable events (kind 30000-39999) to avoid rapid updates.
|
|
536
|
+
*
|
|
225
537
|
* @param {Object} event - Unsigned event object
|
|
226
|
-
* @param {Object} options - Publish options
|
|
227
|
-
* @param {boolean} options.waitForRelays - Wait for relay confirmation
|
|
538
|
+
* @param {Object} [options={}] - Publish options
|
|
539
|
+
* @param {boolean} [options.waitForRelays=false] - Wait for relay confirmation
|
|
540
|
+
* @param {boolean} [options.debounce=true] - Debounce rapid writes to same d-tag
|
|
228
541
|
* @returns {Promise<Object>} Signed event with relay publish results
|
|
542
|
+
* @example
|
|
543
|
+
* const result = await client.publish({
|
|
544
|
+
* kind: 30000,
|
|
545
|
+
* tags: [['d', 'mypath']],
|
|
546
|
+
* content: JSON.stringify({ name: 'Test' })
|
|
547
|
+
* });
|
|
229
548
|
*/
|
|
230
549
|
async publish(event, options = {}) {
|
|
231
550
|
// Ensure initialization is complete
|
|
@@ -233,8 +552,71 @@ export class NostrClient {
|
|
|
233
552
|
|
|
234
553
|
const waitForRelays = options.waitForRelays || false;
|
|
235
554
|
|
|
236
|
-
//
|
|
237
|
-
const
|
|
555
|
+
// For replaceable events, check if we should debounce
|
|
556
|
+
const isReplaceable = event.kind >= 30000 && event.kind < 40000;
|
|
557
|
+
const shouldDebounce = isReplaceable && options.debounce !== false && !waitForRelays;
|
|
558
|
+
|
|
559
|
+
if (shouldDebounce) {
|
|
560
|
+
const dTag = event.tags?.find(t => t[0] === 'd');
|
|
561
|
+
if (dTag && dTag[1]) {
|
|
562
|
+
return this._debouncedPublish(event, dTag[1], options);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return this._doPublish(event, options);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Debounced publish - coalesces rapid writes to the same d-tag
|
|
571
|
+
* @private
|
|
572
|
+
*/
|
|
573
|
+
_debouncedPublish(event, dTagPath, options) {
|
|
574
|
+
return new Promise((resolve, reject) => {
|
|
575
|
+
const existing = pendingWrites.get(dTagPath);
|
|
576
|
+
|
|
577
|
+
if (existing) {
|
|
578
|
+
// Cancel previous pending write and use the new one
|
|
579
|
+
clearTimeout(existing.timer);
|
|
580
|
+
// Resolve the previous promise with the new event (it will be superseded)
|
|
581
|
+
existing.resolve({
|
|
582
|
+
event: null,
|
|
583
|
+
results: [],
|
|
584
|
+
debounced: true,
|
|
585
|
+
supersededBy: event,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Set up debounced write
|
|
590
|
+
const timer = setTimeout(async () => {
|
|
591
|
+
pendingWrites.delete(dTagPath);
|
|
592
|
+
try {
|
|
593
|
+
const result = await this._doPublish(event, options);
|
|
594
|
+
resolve(result);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
reject(err);
|
|
597
|
+
}
|
|
598
|
+
}, WRITE_DEBOUNCE_MS);
|
|
599
|
+
|
|
600
|
+
pendingWrites.set(dTagPath, { event, timer, resolve, reject });
|
|
601
|
+
|
|
602
|
+
// Cache immediately for local-first reads (even before relay publish)
|
|
603
|
+
const signedEvent = finalizeEvent(event, this.privateKey);
|
|
604
|
+
this._cacheEvent(signedEvent);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Internal publish implementation
|
|
610
|
+
* @private
|
|
611
|
+
*/
|
|
612
|
+
async _doPublish(event, options = {}) {
|
|
613
|
+
const waitForRelays = options.waitForRelays || false;
|
|
614
|
+
|
|
615
|
+
// Check if event is already signed (has id and sig)
|
|
616
|
+
// If so, use it as-is; otherwise sign it
|
|
617
|
+
const signedEvent = (event.id && event.sig)
|
|
618
|
+
? event
|
|
619
|
+
: finalizeEvent(event, this.privateKey);
|
|
238
620
|
|
|
239
621
|
// 1. Cache the event locally first (this makes reads instant)
|
|
240
622
|
await this._cacheEvent(signedEvent);
|
|
@@ -256,8 +638,8 @@ export class NostrClient {
|
|
|
256
638
|
};
|
|
257
639
|
} else {
|
|
258
640
|
// Fire and forget - publish in background, return immediately
|
|
259
|
-
this._attemptDelivery(signedEvent, this.relays).catch(
|
|
260
|
-
|
|
641
|
+
this._attemptDelivery(signedEvent, this.relays).catch(() => {
|
|
642
|
+
// Silent - event is cached locally and queued for retry
|
|
261
643
|
});
|
|
262
644
|
|
|
263
645
|
// Return immediately (data is in local cache and queued for delivery)
|
|
@@ -305,25 +687,41 @@ export class NostrClient {
|
|
|
305
687
|
await this.outboxQueue.markSent(event.id, successful);
|
|
306
688
|
}
|
|
307
689
|
|
|
308
|
-
// Log failures
|
|
309
|
-
if (failed.length > 0) {
|
|
690
|
+
// Log failures only at debug level (common during network issues)
|
|
691
|
+
if (failed.length > 0 && successful.length === 0) {
|
|
692
|
+
// Only warn if ALL relays failed (likely a real issue)
|
|
310
693
|
const reasons = results
|
|
311
694
|
.filter(r => r.status === 'rejected')
|
|
312
695
|
.map(f => f.reason?.message || f.reason || 'unknown')
|
|
313
696
|
.join(', ');
|
|
314
|
-
|
|
697
|
+
// Use debug level for timeout issues (very common)
|
|
698
|
+
if (reasons.includes('timed out')) {
|
|
699
|
+
// Silent - event is queued for retry anyway
|
|
700
|
+
} else {
|
|
701
|
+
console.warn(`[nostr] All relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
|
|
702
|
+
}
|
|
315
703
|
}
|
|
316
704
|
|
|
317
705
|
return { successful, failed, results: formattedResults };
|
|
318
706
|
}
|
|
319
707
|
|
|
320
708
|
/**
|
|
321
|
-
* Query events from relays
|
|
709
|
+
* Query events from relays.
|
|
710
|
+
*
|
|
711
|
+
* Uses long-lived subscription for cache updates - avoids polling.
|
|
712
|
+
*
|
|
322
713
|
* @param {Object} filter - Nostr filter object
|
|
323
|
-
* @param {Object} options - Query options
|
|
324
|
-
* @param {number} options.timeout - Query timeout in ms (
|
|
325
|
-
* @param {boolean} options.localFirst - Return local cache immediately, refresh in background
|
|
714
|
+
* @param {Object} [options={}] - Query options
|
|
715
|
+
* @param {number} [options.timeout=30000] - Query timeout in ms (set to 0 for no timeout)
|
|
716
|
+
* @param {boolean} [options.localFirst=true] - Return local cache immediately, refresh in background
|
|
717
|
+
* @param {boolean} [options.forceRelay=false] - Force relay query even if subscription cache is available
|
|
326
718
|
* @returns {Promise<Array>} Array of events
|
|
719
|
+
* @example
|
|
720
|
+
* const events = await client.query({
|
|
721
|
+
* kinds: [30000],
|
|
722
|
+
* authors: [client.publicKey],
|
|
723
|
+
* '#d': ['mypath']
|
|
724
|
+
* });
|
|
327
725
|
*/
|
|
328
726
|
async query(filter, options = {}) {
|
|
329
727
|
// Ensure initialization is complete
|
|
@@ -331,6 +729,7 @@ export class NostrClient {
|
|
|
331
729
|
|
|
332
730
|
const timeout = options.timeout !== undefined ? options.timeout : 30000;
|
|
333
731
|
const localFirst = options.localFirst !== false; // Default to true for speed
|
|
732
|
+
const forceRelay = options.forceRelay === true;
|
|
334
733
|
|
|
335
734
|
// If no relays, query from cache only
|
|
336
735
|
if (this.relays.length === 0) {
|
|
@@ -338,6 +737,35 @@ export class NostrClient {
|
|
|
338
737
|
return matchingEvents;
|
|
339
738
|
}
|
|
340
739
|
|
|
740
|
+
// Check if this query can be served from the long-lived subscription cache
|
|
741
|
+
// The subscription keeps ALL events for this author in cache, updated in real-time
|
|
742
|
+
const subInfo = authorSubscriptions.get(this.publicKey);
|
|
743
|
+
const isOwnDataQuery = filter.authors &&
|
|
744
|
+
filter.authors.length === 1 &&
|
|
745
|
+
filter.authors[0] === this.publicKey;
|
|
746
|
+
|
|
747
|
+
// If we have an initialized subscription for our own data, use cache
|
|
748
|
+
if (!forceRelay && isOwnDataQuery && subInfo && subInfo.initialized) {
|
|
749
|
+
// Return matching events from cache - no relay query needed!
|
|
750
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
751
|
+
return matchingEvents;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// For first query before subscription initializes, wait briefly
|
|
755
|
+
if (isOwnDataQuery && subInfo && !subInfo.initialized) {
|
|
756
|
+
// Wait for subscription to initialize (up to timeout)
|
|
757
|
+
await Promise.race([
|
|
758
|
+
subInfo.initPromise,
|
|
759
|
+
new Promise(resolve => setTimeout(resolve, Math.min(timeout, AUTHOR_SUB_INIT_TIMEOUT)))
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
// Now try cache again
|
|
763
|
+
if (subInfo.initialized) {
|
|
764
|
+
const matchingEvents = this._getMatchingCachedEvents(filter);
|
|
765
|
+
return matchingEvents;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
341
769
|
// Check d-tag cache first for single-item queries (most common case)
|
|
342
770
|
// This ensures recently written data is returned immediately
|
|
343
771
|
if (filter['#d'] && filter['#d'].length === 1 && filter.kinds && filter.kinds.length === 1) {
|
|
@@ -370,29 +798,101 @@ export class NostrClient {
|
|
|
370
798
|
|
|
371
799
|
/**
|
|
372
800
|
* Query relays and update cache
|
|
801
|
+
* Uses global pending queries map to deduplicate identical concurrent queries
|
|
373
802
|
* @private
|
|
374
803
|
*/
|
|
375
804
|
async _queryRelaysAndCache(filter, cacheKey, timeout) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
805
|
+
// Check if there's already a pending query for this exact filter
|
|
806
|
+
const pending = pendingQueries.get(cacheKey);
|
|
807
|
+
if (pending && Date.now() - pending.timestamp < PENDING_QUERY_TIMEOUT) {
|
|
808
|
+
// Reuse the pending query promise instead of creating a new one
|
|
809
|
+
return pending.promise;
|
|
381
810
|
}
|
|
382
811
|
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
812
|
+
// Create the query promise
|
|
813
|
+
const queryPromise = (async () => {
|
|
814
|
+
try {
|
|
815
|
+
let events = await this.pool.querySync(this.relays, filter, { timeout });
|
|
816
|
+
|
|
817
|
+
// CRITICAL: Filter out events from other authors (relay may not respect filter)
|
|
818
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
819
|
+
events = events.filter(event => filter.authors.includes(event.pubkey));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Cache results
|
|
823
|
+
this._eventCache.set(cacheKey, {
|
|
824
|
+
events,
|
|
825
|
+
timestamp: Date.now(),
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Update reverse index for fast invalidation
|
|
829
|
+
this._indexCacheEntry(cacheKey, filter);
|
|
830
|
+
|
|
831
|
+
return events;
|
|
832
|
+
} finally {
|
|
833
|
+
// Clean up pending query after completion
|
|
834
|
+
pendingQueries.delete(cacheKey);
|
|
835
|
+
}
|
|
836
|
+
})();
|
|
837
|
+
|
|
838
|
+
// Store the pending query
|
|
839
|
+
pendingQueries.set(cacheKey, {
|
|
840
|
+
promise: queryPromise,
|
|
386
841
|
timestamp: Date.now(),
|
|
387
842
|
});
|
|
388
843
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
844
|
+
return queryPromise;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Limit cache size (called after cache operations)
|
|
849
|
+
* Note: LRU cache handles this automatically, kept for API compatibility
|
|
850
|
+
* @private
|
|
851
|
+
*/
|
|
852
|
+
_limitCacheSize() {
|
|
853
|
+
// LRU cache handles size limiting automatically
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Add cache entry to reverse index for fast invalidation
|
|
858
|
+
* @private
|
|
859
|
+
*/
|
|
860
|
+
_indexCacheEntry(cacheKey, filter) {
|
|
861
|
+
// Index by kinds for fast lookup during invalidation
|
|
862
|
+
if (filter.kinds) {
|
|
863
|
+
for (const kind of filter.kinds) {
|
|
864
|
+
if (!this._cacheIndex.has(kind)) {
|
|
865
|
+
this._cacheIndex.set(kind, new Set());
|
|
866
|
+
}
|
|
867
|
+
this._cacheIndex.get(kind).add(cacheKey);
|
|
868
|
+
}
|
|
393
869
|
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Remove cache entry from reverse index
|
|
874
|
+
* @private
|
|
875
|
+
*/
|
|
876
|
+
_unindexCacheEntry(cacheKey) {
|
|
877
|
+
// Try to parse the filter from the cache key to remove from index
|
|
878
|
+
if (!cacheKey.startsWith('{')) return;
|
|
394
879
|
|
|
395
|
-
|
|
880
|
+
try {
|
|
881
|
+
const filter = JSON.parse(cacheKey);
|
|
882
|
+
if (filter.kinds) {
|
|
883
|
+
for (const kind of filter.kinds) {
|
|
884
|
+
const indexSet = this._cacheIndex.get(kind);
|
|
885
|
+
if (indexSet) {
|
|
886
|
+
indexSet.delete(cacheKey);
|
|
887
|
+
if (indexSet.size === 0) {
|
|
888
|
+
this._cacheIndex.delete(kind);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
// Not a valid filter key, skip
|
|
895
|
+
}
|
|
396
896
|
}
|
|
397
897
|
|
|
398
898
|
/**
|
|
@@ -421,20 +921,42 @@ export class NostrClient {
|
|
|
421
921
|
|
|
422
922
|
/**
|
|
423
923
|
* Internal method to refresh a path from relays
|
|
924
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
424
925
|
* @private
|
|
425
926
|
*/
|
|
426
927
|
async _doBackgroundPathRefresh(path, kind, options) {
|
|
427
928
|
if (this.relays.length === 0) return;
|
|
428
929
|
|
|
930
|
+
// Throttle: Skip if we've refreshed this path recently
|
|
931
|
+
const lastRefresh = backgroundRefreshThrottle.get(path);
|
|
932
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
933
|
+
return; // Skip - recently refreshed
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Mark as refreshed
|
|
937
|
+
backgroundRefreshThrottle.set(path, Date.now());
|
|
938
|
+
|
|
939
|
+
// Clean up old throttle entries periodically (keep map from growing)
|
|
940
|
+
if (backgroundRefreshThrottle.size > 1000) {
|
|
941
|
+
const cutoff = Date.now() - BACKGROUND_REFRESH_INTERVAL;
|
|
942
|
+
for (const [key, timestamp] of backgroundRefreshThrottle) {
|
|
943
|
+
if (timestamp < cutoff) {
|
|
944
|
+
backgroundRefreshThrottle.delete(key);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
429
949
|
const filter = {
|
|
430
950
|
kinds: [kind],
|
|
431
951
|
authors: options.authors || [this.publicKey],
|
|
432
952
|
'#d': [path],
|
|
433
953
|
limit: 1,
|
|
434
954
|
};
|
|
955
|
+
const cacheKey = JSON.stringify(filter);
|
|
435
956
|
|
|
957
|
+
// Use our query deduplication by calling query() instead of pool.querySync() directly
|
|
436
958
|
const timeout = options.timeout || 30000;
|
|
437
|
-
const events = await this.
|
|
959
|
+
const events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
438
960
|
|
|
439
961
|
// Filter by author (relays may not respect filter)
|
|
440
962
|
const authorFiltered = events.filter(e =>
|
|
@@ -462,22 +984,35 @@ export class NostrClient {
|
|
|
462
984
|
|
|
463
985
|
/**
|
|
464
986
|
* Internal method to refresh a prefix from relays
|
|
987
|
+
* Throttled to avoid flooding the relay with repeated requests
|
|
465
988
|
* @private
|
|
466
989
|
*/
|
|
467
990
|
async _doBackgroundPrefixRefresh(prefix, kind, options) {
|
|
468
991
|
if (this.relays.length === 0) return;
|
|
469
992
|
|
|
993
|
+
// Throttle: Skip if we've refreshed this prefix recently
|
|
994
|
+
const throttleKey = `prefix:${prefix}`;
|
|
995
|
+
const lastRefresh = backgroundRefreshThrottle.get(throttleKey);
|
|
996
|
+
if (lastRefresh && Date.now() - lastRefresh < BACKGROUND_REFRESH_INTERVAL) {
|
|
997
|
+
return; // Skip - recently refreshed
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Mark as refreshed
|
|
1001
|
+
backgroundRefreshThrottle.set(throttleKey, Date.now());
|
|
1002
|
+
|
|
470
1003
|
// Query with wildcard-ish filter (relays handle d-tag prefix matching)
|
|
471
1004
|
const filter = {
|
|
472
1005
|
kinds: [kind],
|
|
473
1006
|
authors: options.authors || [this.publicKey],
|
|
474
1007
|
limit: options.limit || 1000,
|
|
475
1008
|
};
|
|
1009
|
+
const cacheKey = JSON.stringify(filter);
|
|
476
1010
|
|
|
1011
|
+
// Use our query deduplication
|
|
477
1012
|
const timeout = options.timeout || 30000;
|
|
478
|
-
let events = await this.
|
|
1013
|
+
let events = await this._queryRelaysAndCache(filter, cacheKey, timeout);
|
|
479
1014
|
|
|
480
|
-
// Filter by author
|
|
1015
|
+
// Filter by author (already done by _queryRelaysAndCache, but double-check)
|
|
481
1016
|
events = events.filter(e =>
|
|
482
1017
|
(options.authors || [this.publicKey]).includes(e.pubkey)
|
|
483
1018
|
);
|
|
@@ -583,17 +1118,28 @@ export class NostrClient {
|
|
|
583
1118
|
}
|
|
584
1119
|
|
|
585
1120
|
/**
|
|
586
|
-
* Subscribe to events
|
|
1121
|
+
* Subscribe to events.
|
|
1122
|
+
*
|
|
1123
|
+
* Uses subscription deduplication to avoid creating multiple identical subscriptions.
|
|
1124
|
+
*
|
|
587
1125
|
* @param {Object} filter - Nostr filter object
|
|
588
1126
|
* @param {Function} onEvent - Callback for each event
|
|
589
|
-
* @param {Object} options - Subscription options
|
|
590
|
-
* @
|
|
1127
|
+
* @param {Object} [options={}] - Subscription options
|
|
1128
|
+
* @param {Function} [options.onEOSE] - Callback when End Of Stored Events is received
|
|
1129
|
+
* @returns {Promise<Object>} Subscription object with unsubscribe method and id
|
|
1130
|
+
* @example
|
|
1131
|
+
* const sub = await client.subscribe(
|
|
1132
|
+
* { kinds: [30000], authors: [client.publicKey] },
|
|
1133
|
+
* (event) => console.log('Event:', event)
|
|
1134
|
+
* );
|
|
1135
|
+
* // Later: sub.unsubscribe();
|
|
591
1136
|
*/
|
|
592
1137
|
async subscribe(filter, onEvent, options = {}) {
|
|
593
1138
|
// Ensure initialization is complete
|
|
594
1139
|
await this._initReady;
|
|
595
1140
|
|
|
596
|
-
const subId = `sub-${Date.now()}-${Math.random().toString(36).
|
|
1141
|
+
const subId = `sub-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1142
|
+
const filterKey = JSON.stringify(filter);
|
|
597
1143
|
|
|
598
1144
|
// If no relays, check cache for matching events and trigger callbacks
|
|
599
1145
|
if (this.relays.length === 0) {
|
|
@@ -630,6 +1176,43 @@ export class NostrClient {
|
|
|
630
1176
|
};
|
|
631
1177
|
}
|
|
632
1178
|
|
|
1179
|
+
// Check if we already have an active subscription for this filter
|
|
1180
|
+
const existing = activeSubscriptions.get(filterKey);
|
|
1181
|
+
if (existing) {
|
|
1182
|
+
// Add callback to existing subscription
|
|
1183
|
+
existing.callbacks.add(onEvent);
|
|
1184
|
+
existing.refCount++;
|
|
1185
|
+
|
|
1186
|
+
// Return wrapper that removes this specific callback on unsubscribe
|
|
1187
|
+
return {
|
|
1188
|
+
id: subId,
|
|
1189
|
+
unsubscribe: () => {
|
|
1190
|
+
existing.callbacks.delete(onEvent);
|
|
1191
|
+
existing.refCount--;
|
|
1192
|
+
|
|
1193
|
+
// Only close actual subscription when no more callbacks
|
|
1194
|
+
if (existing.refCount === 0) {
|
|
1195
|
+
if (existing.subscription && existing.subscription.close) {
|
|
1196
|
+
existing.subscription.close();
|
|
1197
|
+
}
|
|
1198
|
+
activeSubscriptions.delete(filterKey);
|
|
1199
|
+
}
|
|
1200
|
+
this._subscriptions.delete(subId);
|
|
1201
|
+
},
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Create new subscription with shared callback dispatcher
|
|
1206
|
+
const callbacks = new Set([onEvent]);
|
|
1207
|
+
const subscriptionInfo = {
|
|
1208
|
+
callbacks,
|
|
1209
|
+
refCount: 1,
|
|
1210
|
+
subscription: null,
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// Store before creating subscription to handle race conditions
|
|
1214
|
+
activeSubscriptions.set(filterKey, subscriptionInfo);
|
|
1215
|
+
|
|
633
1216
|
const sub = this.pool.subscribeMany(
|
|
634
1217
|
this.relays,
|
|
635
1218
|
[filter],
|
|
@@ -645,7 +1228,18 @@ export class NostrClient {
|
|
|
645
1228
|
}
|
|
646
1229
|
|
|
647
1230
|
this._cacheEvent(event);
|
|
648
|
-
|
|
1231
|
+
|
|
1232
|
+
// Dispatch to ALL registered callbacks for this subscription
|
|
1233
|
+
const subInfo = activeSubscriptions.get(filterKey);
|
|
1234
|
+
if (subInfo) {
|
|
1235
|
+
for (const cb of subInfo.callbacks) {
|
|
1236
|
+
try {
|
|
1237
|
+
cb(event);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
console.warn('[nostr] Subscription callback error:', err.message);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
649
1243
|
},
|
|
650
1244
|
oneose: () => {
|
|
651
1245
|
if (options.onEOSE) options.onEOSE();
|
|
@@ -653,12 +1247,21 @@ export class NostrClient {
|
|
|
653
1247
|
}
|
|
654
1248
|
);
|
|
655
1249
|
|
|
1250
|
+
// Store the actual subscription object
|
|
1251
|
+
subscriptionInfo.subscription = sub;
|
|
656
1252
|
this._subscriptions.set(subId, sub);
|
|
657
1253
|
|
|
658
1254
|
return {
|
|
659
1255
|
id: subId,
|
|
660
1256
|
unsubscribe: () => {
|
|
661
|
-
|
|
1257
|
+
callbacks.delete(onEvent);
|
|
1258
|
+
subscriptionInfo.refCount--;
|
|
1259
|
+
|
|
1260
|
+
// Only close actual subscription when no more callbacks
|
|
1261
|
+
if (subscriptionInfo.refCount === 0) {
|
|
1262
|
+
if (sub.close) sub.close();
|
|
1263
|
+
activeSubscriptions.delete(filterKey);
|
|
1264
|
+
}
|
|
662
1265
|
this._subscriptions.delete(subId);
|
|
663
1266
|
},
|
|
664
1267
|
};
|
|
@@ -708,16 +1311,26 @@ export class NostrClient {
|
|
|
708
1311
|
|
|
709
1312
|
/**
|
|
710
1313
|
* Invalidate query caches that might be affected by a new event
|
|
1314
|
+
* Uses reverse index for O(1) lookup instead of O(n) scan
|
|
711
1315
|
* @private
|
|
712
1316
|
*/
|
|
713
1317
|
_invalidateQueryCachesForEvent(event) {
|
|
714
|
-
//
|
|
715
|
-
|
|
1318
|
+
// Use reverse index for fast lookup - only check caches that could match this event's kind
|
|
1319
|
+
const indexedKeys = this._cacheIndex.get(event.kind);
|
|
1320
|
+
if (!indexedKeys || indexedKeys.size === 0) {
|
|
1321
|
+
return; // No cached queries for this kind
|
|
1322
|
+
}
|
|
1323
|
+
|
|
716
1324
|
const keysToDelete = [];
|
|
717
1325
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1326
|
+
// Only iterate over cache entries that match the event's kind
|
|
1327
|
+
for (const cacheKey of indexedKeys) {
|
|
1328
|
+
const cached = this._eventCache.get(cacheKey);
|
|
1329
|
+
if (!cached) {
|
|
1330
|
+
// Cache entry was evicted, clean up index
|
|
1331
|
+
indexedKeys.delete(cacheKey);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
721
1334
|
|
|
722
1335
|
try {
|
|
723
1336
|
const filter = JSON.parse(cacheKey);
|
|
@@ -726,38 +1339,42 @@ export class NostrClient {
|
|
|
726
1339
|
keysToDelete.push(cacheKey);
|
|
727
1340
|
}
|
|
728
1341
|
} catch {
|
|
729
|
-
// Not a JSON key,
|
|
1342
|
+
// Not a valid JSON key, clean up index
|
|
1343
|
+
indexedKeys.delete(cacheKey);
|
|
730
1344
|
}
|
|
731
1345
|
}
|
|
732
1346
|
|
|
733
1347
|
for (const key of keysToDelete) {
|
|
734
1348
|
this._eventCache.delete(key);
|
|
1349
|
+
this._unindexCacheEntry(key);
|
|
735
1350
|
}
|
|
736
1351
|
}
|
|
737
1352
|
|
|
738
1353
|
/**
|
|
739
|
-
* Cache event in memory and persist
|
|
1354
|
+
* Cache event in memory and persist (batched)
|
|
740
1355
|
* @private
|
|
741
1356
|
*/
|
|
742
1357
|
async _cacheEvent(event) {
|
|
743
|
-
// Cache in memory
|
|
1358
|
+
// Cache in memory (synchronous - immediate for local-first reads)
|
|
744
1359
|
this._cacheEventSync(event);
|
|
745
1360
|
|
|
746
|
-
//
|
|
1361
|
+
// Queue for batched persistence (async - batches writes for I/O efficiency)
|
|
747
1362
|
if (this.persistentStorage) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
storageKey = dTag[1]; // Use d-tag as key for replaceable events
|
|
755
|
-
}
|
|
1363
|
+
// For replaceable events, use d-tag as key
|
|
1364
|
+
let storageKey = event.id;
|
|
1365
|
+
if (event.kind >= 30000 && event.kind < 40000) {
|
|
1366
|
+
const dTag = event.tags.find(t => t[0] === 'd');
|
|
1367
|
+
if (dTag && dTag[1]) {
|
|
1368
|
+
storageKey = dTag[1]; // Use d-tag as key for replaceable events
|
|
756
1369
|
}
|
|
1370
|
+
}
|
|
757
1371
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1372
|
+
// Queue for batched write (overwrites previous if same key)
|
|
1373
|
+
this._persistQueue.set(storageKey, event);
|
|
1374
|
+
|
|
1375
|
+
// Schedule batch flush if not already scheduled
|
|
1376
|
+
if (!this._persistTimer) {
|
|
1377
|
+
this._persistTimer = setTimeout(() => this._flushPersistQueue(), this._persistBatchMs);
|
|
761
1378
|
}
|
|
762
1379
|
}
|
|
763
1380
|
|
|
@@ -776,6 +1393,34 @@ export class NostrClient {
|
|
|
776
1393
|
}
|
|
777
1394
|
}
|
|
778
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Flush batched persistent writes
|
|
1398
|
+
* @private
|
|
1399
|
+
*/
|
|
1400
|
+
async _flushPersistQueue() {
|
|
1401
|
+
this._persistTimer = null;
|
|
1402
|
+
|
|
1403
|
+
if (!this.persistentStorage || this._persistQueue.size === 0) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Take snapshot of current queue and clear it
|
|
1408
|
+
const toWrite = Array.from(this._persistQueue.entries());
|
|
1409
|
+
this._persistQueue.clear();
|
|
1410
|
+
|
|
1411
|
+
// Write all queued events
|
|
1412
|
+
const writePromises = toWrite.map(async ([key, event]) => {
|
|
1413
|
+
try {
|
|
1414
|
+
await this.persistentStorage.put(key, event);
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
console.warn(`Failed to persist event ${key}:`, error.message);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Wait for all writes to complete
|
|
1421
|
+
await Promise.all(writePromises);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
779
1424
|
/**
|
|
780
1425
|
* Get cached events matching a filter
|
|
781
1426
|
* @private
|
|
@@ -891,14 +1536,35 @@ export class NostrClient {
|
|
|
891
1536
|
}
|
|
892
1537
|
|
|
893
1538
|
/**
|
|
894
|
-
* Close all connections and subscriptions
|
|
1539
|
+
* Close all connections and subscriptions.
|
|
1540
|
+
*
|
|
1541
|
+
* @param {Object} [options={}] - Close options
|
|
1542
|
+
* @param {boolean} [options.flush=true] - Flush pending writes before closing
|
|
1543
|
+
* @returns {Promise<void>}
|
|
895
1544
|
*/
|
|
896
|
-
close() {
|
|
1545
|
+
async close(options = {}) {
|
|
1546
|
+
const shouldFlush = options.flush !== false;
|
|
1547
|
+
|
|
1548
|
+
// Flush pending persistent writes before closing
|
|
1549
|
+
if (shouldFlush && this._persistTimer) {
|
|
1550
|
+
clearTimeout(this._persistTimer);
|
|
1551
|
+
await this._flushPersistQueue();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
897
1554
|
// Stop background sync service
|
|
898
1555
|
if (this.syncService) {
|
|
899
1556
|
this.syncService.stop();
|
|
900
1557
|
}
|
|
901
1558
|
|
|
1559
|
+
// Close long-lived author subscription
|
|
1560
|
+
const authorSub = authorSubscriptions.get(this.publicKey);
|
|
1561
|
+
if (authorSub && authorSub.subscription) {
|
|
1562
|
+
if (authorSub.subscription.close) {
|
|
1563
|
+
authorSub.subscription.close();
|
|
1564
|
+
}
|
|
1565
|
+
authorSubscriptions.delete(this.publicKey);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
902
1568
|
// Close all subscriptions
|
|
903
1569
|
for (const sub of this._subscriptions.values()) {
|
|
904
1570
|
if (sub.close) {
|
|
@@ -913,8 +1579,9 @@ export class NostrClient {
|
|
|
913
1579
|
// Close pool
|
|
914
1580
|
this.pool.close(this.relays);
|
|
915
1581
|
|
|
916
|
-
// Clear cache
|
|
1582
|
+
// Clear cache and index
|
|
917
1583
|
this._eventCache.clear();
|
|
1584
|
+
this._cacheIndex.clear();
|
|
918
1585
|
}
|
|
919
1586
|
|
|
920
1587
|
/**
|
|
@@ -930,9 +1597,15 @@ export class NostrClient {
|
|
|
930
1597
|
}
|
|
931
1598
|
|
|
932
1599
|
/**
|
|
933
|
-
* Create NostrClient instance
|
|
1600
|
+
* Create NostrClient instance.
|
|
1601
|
+
*
|
|
934
1602
|
* @param {Object} config - Configuration options
|
|
935
1603
|
* @returns {NostrClient} Client instance
|
|
1604
|
+
* @example
|
|
1605
|
+
* const client = createClient({
|
|
1606
|
+
* relays: ['wss://relay.example.com'],
|
|
1607
|
+
* appName: 'myapp'
|
|
1608
|
+
* });
|
|
936
1609
|
*/
|
|
937
1610
|
export function createClient(config) {
|
|
938
1611
|
return new NostrClient(config);
|