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.
Files changed (418) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/FEATURES.md +431 -0
  3. package/LICENSE +29 -166
  4. package/LICENSE-AGPL.md +180 -0
  5. package/README.md +97 -16
  6. package/dist/2019-D2OG2idw.js +6680 -0
  7. package/dist/2019-D2OG2idw.js.map +1 -0
  8. package/dist/2019-EION3wKo.cjs +8 -0
  9. package/dist/2019-EION3wKo.cjs.map +1 -0
  10. package/dist/_commonjsHelpers-C37NGDzP.cjs +2 -0
  11. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +1 -0
  12. package/dist/_commonjsHelpers-CUmg6egw.js +7 -0
  13. package/dist/_commonjsHelpers-CUmg6egw.js.map +1 -0
  14. package/dist/browser-BSniCNqO.js +3058 -0
  15. package/dist/browser-BSniCNqO.js.map +1 -0
  16. package/dist/browser-Cq59Ij19.cjs +2 -0
  17. package/dist/browser-Cq59Ij19.cjs.map +1 -0
  18. package/dist/cdn/holosphere.min.js +55 -0
  19. package/dist/cdn/holosphere.min.js.map +1 -0
  20. package/dist/cjs/holosphere.cjs +2 -0
  21. package/dist/cjs/holosphere.cjs.map +1 -0
  22. package/dist/esm/holosphere.js +53 -0
  23. package/dist/esm/holosphere.js.map +1 -0
  24. package/dist/index-DDGt_V9o.cjs +12 -0
  25. package/dist/index-DDGt_V9o.cjs.map +1 -0
  26. package/dist/index-DJXftyvB.js +39841 -0
  27. package/dist/index-DJXftyvB.js.map +1 -0
  28. package/dist/index-DMbdcMtK.cjs +18 -0
  29. package/dist/index-DMbdcMtK.cjs.map +1 -0
  30. package/dist/index-DeZ1xz_s.js +15104 -0
  31. package/dist/index-DeZ1xz_s.js.map +1 -0
  32. package/dist/indexeddb-storage-BFt6hMeF.js +176 -0
  33. package/dist/indexeddb-storage-BFt6hMeF.js.map +1 -0
  34. package/dist/indexeddb-storage-BK5tv4Sh.cjs +2 -0
  35. package/dist/indexeddb-storage-BK5tv4Sh.cjs.map +1 -0
  36. package/dist/memory-storage-C9HuoL2E.js +91 -0
  37. package/dist/memory-storage-C9HuoL2E.js.map +1 -0
  38. package/dist/memory-storage-Dao7jfYG.cjs +2 -0
  39. package/dist/memory-storage-Dao7jfYG.cjs.map +1 -0
  40. package/dist/secp256k1-BbKzbLtD.cjs +12 -0
  41. package/dist/secp256k1-BbKzbLtD.cjs.map +1 -0
  42. package/dist/secp256k1-CreY7Pcl.js +1890 -0
  43. package/dist/secp256k1-CreY7Pcl.js.map +1 -0
  44. package/docs/CONTRACTS.md +797 -0
  45. package/docs/FOSDEM_PROPOSAL.md +388 -0
  46. package/docs/LOCALFIRST.md +266 -0
  47. package/docs/api/ai_aggregation.js.html +333 -0
  48. package/docs/api/ai_breakdown.js.html +524 -0
  49. package/docs/api/ai_classifier.js.html +231 -0
  50. package/docs/api/ai_council.js.html +246 -0
  51. package/docs/api/ai_embeddings.js.html +304 -0
  52. package/docs/api/ai_federation-ai.js.html +338 -0
  53. package/docs/api/ai_h3-ai.js.html +970 -0
  54. package/docs/api/ai_index.js.html +124 -0
  55. package/docs/api/ai_json-ops.js.html +241 -0
  56. package/docs/api/ai_llm-service.js.html +239 -0
  57. package/docs/api/ai_nl-query.js.html +236 -0
  58. package/docs/api/ai_relationships.js.html +367 -0
  59. package/docs/api/ai_schema-extractor.js.html +235 -0
  60. package/docs/api/ai_spatial.js.html +307 -0
  61. package/docs/api/ai_tts.js.html +214 -0
  62. package/docs/api/content_social-protocols.js.html +180 -0
  63. package/docs/api/core_holosphere.js.html +757 -0
  64. package/docs/api/crypto_nostr-utils.js.html +306 -0
  65. package/docs/api/crypto_secp256k1.js.html +267 -0
  66. package/docs/api/data/search.json +1 -0
  67. package/docs/api/federation_discovery.js.html +337 -0
  68. package/docs/api/federation_handshake.js.html +478 -0
  69. package/docs/api/federation_hologram.js.html +1053 -0
  70. package/docs/api/federation_registry.js.html +389 -0
  71. package/docs/api/fonts/Inconsolata-Regular.ttf +0 -0
  72. package/docs/api/fonts/OpenSans-Regular.ttf +0 -0
  73. package/docs/api/fonts/WorkSans-Bold.ttf +0 -0
  74. package/docs/api/global.html +3 -0
  75. package/docs/api/hierarchical_upcast.js.html +128 -0
  76. package/docs/api/index.html +265 -0
  77. package/docs/api/index.js.html +1868 -0
  78. package/docs/api/lib_ai-methods.js.html +660 -0
  79. package/docs/api/lib_contract-methods.js.html +445 -0
  80. package/docs/api/lib_errors.js.html +56 -0
  81. package/docs/api/lib_federation-methods.js.html +348 -0
  82. package/docs/api/lib_index.js.html +33 -0
  83. package/docs/api/module-ai.html +5 -0
  84. package/docs/api/module-ai_aggregation-SmartAggregation.html +6 -0
  85. package/docs/api/module-ai_aggregation.SmartAggregation.html +3 -0
  86. package/docs/api/module-ai_aggregation.html +3 -0
  87. package/docs/api/module-ai_breakdown-TaskBreakdown.html +5 -0
  88. package/docs/api/module-ai_breakdown.TaskBreakdown.html +3 -0
  89. package/docs/api/module-ai_breakdown.html +3 -0
  90. package/docs/api/module-ai_classifier-Classifier.html +6 -0
  91. package/docs/api/module-ai_classifier.Classifier.html +3 -0
  92. package/docs/api/module-ai_classifier.html +3 -0
  93. package/docs/api/module-ai_council-Council.html +6 -0
  94. package/docs/api/module-ai_council.Council.html +3 -0
  95. package/docs/api/module-ai_council.html +3 -0
  96. package/docs/api/module-ai_embeddings-Embeddings.html +5 -0
  97. package/docs/api/module-ai_embeddings.Embeddings.html +3 -0
  98. package/docs/api/module-ai_embeddings.html +3 -0
  99. package/docs/api/module-ai_federation-ai-FederationAdvisor.html +6 -0
  100. package/docs/api/module-ai_federation-ai.FederationAdvisor.html +3 -0
  101. package/docs/api/module-ai_federation-ai.html +3 -0
  102. package/docs/api/module-ai_h3-ai-H3AI.html +6 -0
  103. package/docs/api/module-ai_h3-ai.H3AI.html +3 -0
  104. package/docs/api/module-ai_h3-ai.html +3 -0
  105. package/docs/api/module-ai_json-ops-JSONOps.html +5 -0
  106. package/docs/api/module-ai_json-ops.JSONOps.html +3 -0
  107. package/docs/api/module-ai_json-ops.html +3 -0
  108. package/docs/api/module-ai_llm-service-LLMService.html +5 -0
  109. package/docs/api/module-ai_llm-service.LLMService.html +3 -0
  110. package/docs/api/module-ai_llm-service.html +3 -0
  111. package/docs/api/module-ai_nl-query-NLQuery.html +5 -0
  112. package/docs/api/module-ai_nl-query.NLQuery.html +3 -0
  113. package/docs/api/module-ai_nl-query.html +3 -0
  114. package/docs/api/module-ai_relationships-RelationshipDiscovery.html +6 -0
  115. package/docs/api/module-ai_relationships.RelationshipDiscovery.html +3 -0
  116. package/docs/api/module-ai_relationships.html +3 -0
  117. package/docs/api/module-ai_schema-extractor-SchemaExtractor.html +5 -0
  118. package/docs/api/module-ai_schema-extractor.SchemaExtractor.html +3 -0
  119. package/docs/api/module-ai_schema-extractor.html +3 -0
  120. package/docs/api/module-ai_spatial-SpatialAnalysis.html +6 -0
  121. package/docs/api/module-ai_spatial.SpatialAnalysis.html +3 -0
  122. package/docs/api/module-ai_spatial.html +3 -0
  123. package/docs/api/module-ai_tts-TTS.html +5 -0
  124. package/docs/api/module-ai_tts.TTS.html +3 -0
  125. package/docs/api/module-ai_tts.html +3 -0
  126. package/docs/api/module-content_social-protocols.html +3 -0
  127. package/docs/api/module-core_holosphere-HoloSphere.html +6 -0
  128. package/docs/api/module-core_holosphere.HoloSphere.html +3 -0
  129. package/docs/api/module-core_holosphere.html +3 -0
  130. package/docs/api/module-crypto_nostr-utils.html +3 -0
  131. package/docs/api/module-crypto_secp256k1.html +3 -0
  132. package/docs/api/module-federation_hologram.html +3 -0
  133. package/docs/api/module-hierarchical_upcast.html +3 -0
  134. package/docs/api/module-holosphere-HoloSphereBase.html +3 -0
  135. package/docs/api/module-holosphere.html +3 -0
  136. package/docs/api/module-lib_ai-methods.html +3 -0
  137. package/docs/api/module-lib_contract-methods.html +3 -0
  138. package/docs/api/module-lib_errors-AuthorizationError.html +3 -0
  139. package/docs/api/module-lib_errors-ValidationError.html +3 -0
  140. package/docs/api/module-lib_errors.AuthorizationError.html +3 -0
  141. package/docs/api/module-lib_errors.ValidationError.html +3 -0
  142. package/docs/api/module-lib_errors.html +3 -0
  143. package/docs/api/module-lib_federation-methods.html +3 -0
  144. package/docs/api/module-lib_index.html +3 -0
  145. package/docs/api/module-schema_validator-ValidationError.html +3 -0
  146. package/docs/api/module-schema_validator.ValidationError.html +3 -0
  147. package/docs/api/module-schema_validator.html +3 -0
  148. package/docs/api/module-spatial_h3-operations.html +4 -0
  149. package/docs/api/module-storage_backend-factory.BackendFactory.html +3 -0
  150. package/docs/api/module-storage_backend-factory.html +3 -0
  151. package/docs/api/module-storage_backend-interface-StorageBackend.html +3 -0
  152. package/docs/api/module-storage_backend-interface.StorageBackend.html +3 -0
  153. package/docs/api/module-storage_backend-interface.html +3 -0
  154. package/docs/api/module-storage_backends_activitypub-backend-ActivityPubBackend.html +7 -0
  155. package/docs/api/module-storage_backends_activitypub-backend.ActivityPubBackend.html +3 -0
  156. package/docs/api/module-storage_backends_activitypub-backend.html +3 -0
  157. package/docs/api/module-storage_backends_activitypub_server-ActivityPubServer.html +8 -0
  158. package/docs/api/module-storage_backends_activitypub_server.ActivityPubServer.html +3 -0
  159. package/docs/api/module-storage_backends_activitypub_server.html +3 -0
  160. package/docs/api/module-storage_backends_gundb-backend-GunDBBackend.html +7 -0
  161. package/docs/api/module-storage_backends_gundb-backend.GunDBBackend.html +3 -0
  162. package/docs/api/module-storage_backends_gundb-backend.html +3 -0
  163. package/docs/api/module-storage_backends_nostr-backend-NostrBackend.html +8 -0
  164. package/docs/api/module-storage_backends_nostr-backend.NostrBackend.html +3 -0
  165. package/docs/api/module-storage_backends_nostr-backend.html +3 -0
  166. package/docs/api/module-storage_filesystem-storage-FileSystemStorage.html +5 -0
  167. package/docs/api/module-storage_filesystem-storage-browser-FileSystemStorage.html +3 -0
  168. package/docs/api/module-storage_filesystem-storage-browser.FileSystemStorage.html +3 -0
  169. package/docs/api/module-storage_filesystem-storage-browser.html +3 -0
  170. package/docs/api/module-storage_filesystem-storage.FileSystemStorage.html +3 -0
  171. package/docs/api/module-storage_filesystem-storage.html +3 -0
  172. package/docs/api/module-storage_global-tables.html +3 -0
  173. package/docs/api/module-storage_gun-async.html +3 -0
  174. package/docs/api/module-storage_gun-auth-GunAuth.html +5 -0
  175. package/docs/api/module-storage_gun-auth.GunAuth.html +3 -0
  176. package/docs/api/module-storage_gun-auth.html +3 -0
  177. package/docs/api/module-storage_gun-federation.html +3 -0
  178. package/docs/api/module-storage_gun-references-GunReferenceHandler.html +5 -0
  179. package/docs/api/module-storage_gun-references.GunReferenceHandler.html +3 -0
  180. package/docs/api/module-storage_gun-references.html +3 -0
  181. package/docs/api/module-storage_gun-schema-GunSchemaValidator.html +5 -0
  182. package/docs/api/module-storage_gun-schema.GunSchemaValidator.html +3 -0
  183. package/docs/api/module-storage_gun-schema.html +3 -0
  184. package/docs/api/module-storage_gun-wrapper.html +11 -0
  185. package/docs/api/module-storage_indexeddb-storage-IndexedDBStorage.html +5 -0
  186. package/docs/api/module-storage_indexeddb-storage.IndexedDBStorage.html +3 -0
  187. package/docs/api/module-storage_indexeddb-storage.html +3 -0
  188. package/docs/api/module-storage_key-storage-simple.html +3 -0
  189. package/docs/api/module-storage_key-storage.html +4 -0
  190. package/docs/api/module-storage_memory-storage-MemoryStorage.html +5 -0
  191. package/docs/api/module-storage_memory-storage.MemoryStorage.html +3 -0
  192. package/docs/api/module-storage_memory-storage.html +3 -0
  193. package/docs/api/module-storage_migration-MigrationTool.html +6 -0
  194. package/docs/api/module-storage_migration.MigrationTool.html +3 -0
  195. package/docs/api/module-storage_migration.html +3 -0
  196. package/docs/api/module-storage_nostr-async.html +18 -0
  197. package/docs/api/module-storage_nostr-client-LRUCache.html +3 -0
  198. package/docs/api/module-storage_nostr-client-NostrClient.html +7 -0
  199. package/docs/api/module-storage_nostr-client.NostrClient.html +15 -0
  200. package/docs/api/module-storage_nostr-client.html +6 -0
  201. package/docs/api/module-storage_nostr-wrapper.html +3 -0
  202. package/docs/api/module-storage_outbox-queue-OutboxQueue.html +4 -0
  203. package/docs/api/module-storage_outbox-queue.OutboxQueue.html +3 -0
  204. package/docs/api/module-storage_outbox-queue.html +3 -0
  205. package/docs/api/module-storage_persistent-storage-PersistentStorage.html +3 -0
  206. package/docs/api/module-storage_persistent-storage.html +4 -0
  207. package/docs/api/module-storage_sync-service-SyncService.html +5 -0
  208. package/docs/api/module-storage_sync-service.SyncService.html +3 -0
  209. package/docs/api/module-storage_sync-service.html +3 -0
  210. package/docs/api/module-storage_unified-storage.html +3 -0
  211. package/docs/api/module-subscriptions_manager.SubscriptionRegistry.html +3 -0
  212. package/docs/api/module-subscriptions_manager.html +3 -0
  213. package/docs/api/schema_validator.js.html +113 -0
  214. package/docs/api/scripts/core.js +726 -0
  215. package/docs/api/scripts/core.min.js +23 -0
  216. package/docs/api/scripts/resize.js +90 -0
  217. package/docs/api/scripts/search.js +265 -0
  218. package/docs/api/scripts/search.min.js +6 -0
  219. package/docs/api/scripts/third-party/Apache-License-2.0.txt +202 -0
  220. package/docs/api/scripts/third-party/fuse.js +9 -0
  221. package/docs/api/scripts/third-party/hljs-line-num-original.js +369 -0
  222. package/docs/api/scripts/third-party/hljs-line-num.js +1 -0
  223. package/docs/api/scripts/third-party/hljs-original.js +5171 -0
  224. package/docs/api/scripts/third-party/hljs.js +1 -0
  225. package/docs/api/scripts/third-party/popper.js +5 -0
  226. package/docs/api/scripts/third-party/tippy.js +1 -0
  227. package/docs/api/scripts/third-party/tocbot.js +672 -0
  228. package/docs/api/scripts/third-party/tocbot.min.js +1 -0
  229. package/docs/api/spatial_h3-operations.js.html +129 -0
  230. package/docs/api/storage_backend-factory.js.html +133 -0
  231. package/docs/api/storage_backend-interface.js.html +164 -0
  232. package/docs/api/storage_backends_activitypub-backend.js.html +298 -0
  233. package/docs/api/storage_backends_activitypub_server.js.html +678 -0
  234. package/docs/api/storage_backends_gundb-backend.js.html +878 -0
  235. package/docs/api/storage_backends_nostr-backend.js.html +254 -0
  236. package/docs/api/storage_filesystem-storage-browser.js.html +83 -0
  237. package/docs/api/storage_filesystem-storage.js.html +207 -0
  238. package/docs/api/storage_global-tables.js.html +116 -0
  239. package/docs/api/storage_gun-async.js.html +344 -0
  240. package/docs/api/storage_gun-auth.js.html +376 -0
  241. package/docs/api/storage_gun-federation.js.html +788 -0
  242. package/docs/api/storage_gun-references.js.html +212 -0
  243. package/docs/api/storage_gun-schema.js.html +309 -0
  244. package/docs/api/storage_gun-wrapper.js.html +645 -0
  245. package/docs/api/storage_indexeddb-storage.js.html +224 -0
  246. package/docs/api/storage_key-storage-simple.js.html +102 -0
  247. package/docs/api/storage_key-storage.js.html +171 -0
  248. package/docs/api/storage_memory-storage.js.html +128 -0
  249. package/docs/api/storage_migration.js.html +354 -0
  250. package/docs/api/storage_nostr-async.js.html +1076 -0
  251. package/docs/api/storage_nostr-client.js.html +1598 -0
  252. package/docs/api/storage_nostr-wrapper.js.html +218 -0
  253. package/docs/api/storage_outbox-queue.js.html +248 -0
  254. package/docs/api/storage_persistent-storage.js.html +160 -0
  255. package/docs/api/storage_sync-service.js.html +201 -0
  256. package/docs/api/storage_unified-storage.js.html +157 -0
  257. package/docs/api/styles/clean-jsdoc-theme-base.css +1159 -0
  258. package/docs/api/styles/clean-jsdoc-theme-dark.css +412 -0
  259. package/docs/api/styles/clean-jsdoc-theme-light.css +482 -0
  260. package/docs/api/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  261. package/docs/api/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  262. package/docs/api/styles/clean-jsdoc-theme.min.css +1 -0
  263. package/docs/api/subscriptions_manager.js.html +162 -0
  264. package/docs/contracts/api-interface.md +793 -0
  265. package/docs/data-model.md +476 -0
  266. package/docs/gun-async-usage.md +338 -0
  267. package/docs/plan.md +349 -0
  268. package/docs/quickstart.md +674 -0
  269. package/docs/research.md +362 -0
  270. package/docs/spec.md +244 -0
  271. package/docs/storage-backends.md +326 -0
  272. package/docs/tasks.md +947 -0
  273. package/examples/demo.html +47 -0
  274. package/examples/holosphere-widget.js +1242 -0
  275. package/examples/widget-demo.html +274 -0
  276. package/examples/widget.html +703 -0
  277. package/jsdoc.json +26 -0
  278. package/package.json +25 -7
  279. package/src/ai/aggregation.js +13 -2
  280. package/src/ai/breakdown.js +12 -2
  281. package/src/ai/classifier.js +14 -3
  282. package/src/ai/council.js +22 -7
  283. package/src/ai/embeddings.js +37 -15
  284. package/src/ai/federation-ai.js +13 -2
  285. package/src/ai/h3-ai.js +14 -2
  286. package/src/ai/index.js +16 -7
  287. package/src/ai/json-ops.js +18 -5
  288. package/src/ai/llm-service.js +62 -31
  289. package/src/ai/nl-query.js +12 -2
  290. package/src/ai/relationships.js +13 -2
  291. package/src/ai/schema-extractor.js +24 -10
  292. package/src/ai/spatial.js +13 -2
  293. package/src/ai/tts.js +25 -8
  294. package/src/cdn-entry.js +22 -0
  295. package/src/content/social-protocols.js +34 -25
  296. package/src/contracts/abis/Appreciative.json +1280 -0
  297. package/src/contracts/abis/AppreciativeFactory.json +101 -0
  298. package/src/contracts/abis/Bundle.json +1438 -0
  299. package/src/contracts/abis/BundleFactory.json +106 -0
  300. package/src/contracts/abis/Holon.json +881 -0
  301. package/src/contracts/abis/Holons.json +330 -0
  302. package/src/contracts/abis/Managed.json +1262 -0
  303. package/src/contracts/abis/ManagedFactory.json +149 -0
  304. package/src/contracts/abis/Membrane.json +261 -0
  305. package/src/contracts/abis/Splitter.json +1624 -0
  306. package/src/contracts/abis/SplitterFactory.json +220 -0
  307. package/src/contracts/abis/TestToken.json +321 -0
  308. package/src/contracts/abis/Zoned.json +1461 -0
  309. package/src/contracts/abis/ZonedFactory.json +154 -0
  310. package/src/contracts/chain-manager.js +403 -0
  311. package/src/contracts/deployer.js +500 -0
  312. package/src/contracts/event-listener.js +539 -0
  313. package/src/contracts/holon-contracts.js +359 -0
  314. package/src/contracts/index.js +82 -0
  315. package/src/contracts/networks.js +229 -0
  316. package/src/contracts/operations.js +687 -0
  317. package/src/contracts/queries.js +638 -0
  318. package/src/core/holosphere.js +487 -6
  319. package/src/crypto/nostr-utils.js +303 -0
  320. package/src/crypto/secp256k1.js +7 -2
  321. package/src/federation/handshake.js +475 -0
  322. package/src/federation/hologram.js +117 -3
  323. package/src/hierarchical/upcast.js +40 -25
  324. package/src/index.js +1501 -1909
  325. package/src/lib/ai-methods.js +657 -0
  326. package/src/lib/contract-methods.js +442 -0
  327. package/src/lib/errors.js +53 -0
  328. package/src/lib/federation-methods.js +345 -0
  329. package/src/lib/index.js +30 -0
  330. package/src/schema/validator.js +22 -3
  331. package/src/spatial/h3-operations.js +19 -3
  332. package/src/storage/backend-factory.js +7 -2
  333. package/src/storage/backend-interface.js +21 -2
  334. package/src/storage/backends/activitypub/server.js +25 -3
  335. package/src/storage/backends/activitypub-backend.js +25 -2
  336. package/src/storage/backends/gundb-backend.js +692 -50
  337. package/src/storage/backends/nostr-backend.js +116 -1
  338. package/src/storage/filesystem-storage-browser.js +42 -2
  339. package/src/storage/filesystem-storage.js +72 -5
  340. package/src/storage/global-tables.js +35 -3
  341. package/src/storage/gun-async.js +75 -15
  342. package/src/storage/gun-auth.js +373 -0
  343. package/src/storage/gun-federation.js +785 -0
  344. package/src/storage/gun-references.js +209 -0
  345. package/src/storage/gun-schema.js +306 -0
  346. package/src/storage/gun-wrapper.js +475 -54
  347. package/src/storage/indexeddb-storage.js +112 -13
  348. package/src/storage/key-storage-simple.js +32 -9
  349. package/src/storage/key-storage.js +45 -13
  350. package/src/storage/memory-storage.js +68 -2
  351. package/src/storage/migration.js +20 -7
  352. package/src/storage/nostr-async.js +412 -122
  353. package/src/storage/nostr-client.js +749 -76
  354. package/src/storage/nostr-wrapper.js +6 -2
  355. package/src/storage/outbox-queue.js +55 -18
  356. package/src/storage/persistent-storage.js +62 -14
  357. package/src/storage/sync-service.js +51 -17
  358. package/src/storage/unified-storage.js +154 -0
  359. package/src/subscriptions/manager.js +34 -17
  360. package/types/index.d.ts +133 -0
  361. package/vite.config.cdn.js +60 -0
  362. package/tests/unit/ai/aggregation.test.js +0 -295
  363. package/tests/unit/ai/breakdown.test.js +0 -446
  364. package/tests/unit/ai/classifier.test.js +0 -294
  365. package/tests/unit/ai/council.test.js +0 -262
  366. package/tests/unit/ai/embeddings.test.js +0 -384
  367. package/tests/unit/ai/federation-ai.test.js +0 -344
  368. package/tests/unit/ai/h3-ai.test.js +0 -458
  369. package/tests/unit/ai/index.test.js +0 -304
  370. package/tests/unit/ai/json-ops.test.js +0 -307
  371. package/tests/unit/ai/llm-service.test.js +0 -390
  372. package/tests/unit/ai/nl-query.test.js +0 -383
  373. package/tests/unit/ai/relationships.test.js +0 -311
  374. package/tests/unit/ai/schema-extractor.test.js +0 -384
  375. package/tests/unit/ai/spatial.test.js +0 -279
  376. package/tests/unit/ai/tts.test.js +0 -279
  377. package/tests/unit/content.test.js +0 -332
  378. package/tests/unit/contract/core.test.js +0 -88
  379. package/tests/unit/contract/crypto.test.js +0 -198
  380. package/tests/unit/contract/data.test.js +0 -223
  381. package/tests/unit/contract/federation.test.js +0 -181
  382. package/tests/unit/contract/hierarchical.test.js +0 -113
  383. package/tests/unit/contract/schema.test.js +0 -114
  384. package/tests/unit/contract/social.test.js +0 -217
  385. package/tests/unit/contract/spatial.test.js +0 -110
  386. package/tests/unit/contract/subscriptions.test.js +0 -128
  387. package/tests/unit/contract/utils.test.js +0 -159
  388. package/tests/unit/core.test.js +0 -152
  389. package/tests/unit/crypto.test.js +0 -328
  390. package/tests/unit/federation.test.js +0 -234
  391. package/tests/unit/gun-async.test.js +0 -252
  392. package/tests/unit/hierarchical.test.js +0 -399
  393. package/tests/unit/integration/scenario-01-geographic-storage.test.js +0 -74
  394. package/tests/unit/integration/scenario-02-federation.test.js +0 -76
  395. package/tests/unit/integration/scenario-03-subscriptions.test.js +0 -102
  396. package/tests/unit/integration/scenario-04-validation.test.js +0 -129
  397. package/tests/unit/integration/scenario-05-hierarchy.test.js +0 -125
  398. package/tests/unit/integration/scenario-06-social.test.js +0 -135
  399. package/tests/unit/integration/scenario-07-persistence.test.js +0 -130
  400. package/tests/unit/integration/scenario-08-authorization.test.js +0 -161
  401. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +0 -139
  402. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +0 -357
  403. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +0 -410
  404. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +0 -719
  405. package/tests/unit/performance/benchmark.test.js +0 -85
  406. package/tests/unit/schema.test.js +0 -213
  407. package/tests/unit/spatial.test.js +0 -158
  408. package/tests/unit/storage.test.js +0 -195
  409. package/tests/unit/subscriptions.test.js +0 -328
  410. package/tests/unit/test-data-permanence-debug.js +0 -197
  411. package/tests/unit/test-data-permanence.js +0 -340
  412. package/tests/unit/test-key-persistence-fixed.js +0 -148
  413. package/tests/unit/test-key-persistence.js +0 -172
  414. package/tests/unit/test-relay-permanence.js +0 -376
  415. package/tests/unit/test-second-node.js +0 -95
  416. package/tests/unit/test-simple-write.js +0 -89
  417. /package/{cleanup-test-data.js → scripts/cleanup-test-data.js} +0 -0
  418. /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
- * Provides connection pooling and relay management
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
- * @param {Object} config - Configuration options
38
- * @param {string[]} config.relays - Array of relay URLs
39
- * @param {string} config.privateKey - Private key for signing (hex format)
40
- * @param {boolean} config.enableReconnect - Auto-reconnect on disconnect (default: true)
41
- * @param {boolean} config.enablePing - Auto-ping relays (default: true)
42
- * @param {string} config.appName - Application name for storage namespace
43
- * @param {boolean} config.persistence - Enable persistent storage (default: true)
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 Map(); // In-memory cache for recent events
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
- this.pool = new SimplePool({
78
- enableReconnect: this.config.enableReconnect !== false,
79
- enablePing: this.config.enablePing !== false,
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 (default: false for speed)
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
- // Sign the event
237
- const signedEvent = finalizeEvent(event, this.privateKey);
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(err => {
260
- console.warn('[nostr] Immediate delivery failed, queued for retry:', err.message);
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 for debugging
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
- console.warn(`[nostr] ${failed.length}/${relays.length} relays failed for ${event.id.slice(0, 8)}: ${reasons}`);
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 (default: 30000, set to 0 for no timeout)
325
- * @param {boolean} options.localFirst - Return local cache immediately, refresh in background (default: true)
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
- let events = await this.pool.querySync(this.relays, filter, { timeout });
377
-
378
- // CRITICAL: Filter out events from other authors (relay may not respect filter)
379
- if (filter.authors && filter.authors.length > 0) {
380
- events = events.filter(event => filter.authors.includes(event.pubkey));
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
- // Cache results
384
- this._eventCache.set(cacheKey, {
385
- events,
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
- // Limit cache size
390
- if (this._eventCache.size > 100) {
391
- const firstKey = this._eventCache.keys().next().value;
392
- this._eventCache.delete(firstKey);
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
- return events;
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.pool.querySync(this.relays, filter, { timeout });
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.pool.querySync(this.relays, filter, { timeout });
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
- * @returns {Object} Subscription object with unsubscribe method
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).substr(2, 9)}`;
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
- onEvent(event);
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
- if (sub.close) sub.close();
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
- // Find and remove query cache entries that could match this event
715
- // Query cache keys are JSON-stringified filters
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
- for (const [cacheKey, cached] of this._eventCache.entries()) {
719
- // Skip non-query caches (event IDs and d-tag keys)
720
- if (!cacheKey.startsWith('{')) continue;
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, skip
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
- // Persist to storage
1361
+ // Queue for batched persistence (async - batches writes for I/O efficiency)
747
1362
  if (this.persistentStorage) {
748
- try {
749
- // For replaceable events, use d-tag as key
750
- let storageKey = event.id;
751
- if (event.kind >= 30000 && event.kind < 40000) {
752
- const dTag = event.tags.find(t => t[0] === 'd');
753
- if (dTag && dTag[1]) {
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
- await this.persistentStorage.put(storageKey, event);
759
- } catch (error) {
760
- console.warn('Failed to persist event:', error);
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);