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,21 +1,52 @@
1
1
  /**
2
- * Nostr Async Utilities
3
- * Provides Promise-based wrappers and async patterns for Nostr operations
2
+ * @fileoverview Nostr Async Utilities.
3
+ *
4
+ * Provides Promise-based wrappers and async patterns for Nostr operations.
5
+ * Includes local-first data access, query deduplication, subscription management,
6
+ * and background refresh capabilities for optimal performance.
7
+ *
8
+ * @module storage/nostr-async
4
9
  */
5
10
 
6
11
  /**
7
- * Global subscription manager to prevent duplicate subscriptions
8
- * Maps: pathPrefix -> subscription object
12
+ * Global subscription manager to prevent duplicate subscriptions.
13
+ * Maps: subscriptionKey -> subscription object
14
+ * @private
9
15
  */
10
16
  const globalSubscriptions = new Map();
11
17
 
12
18
  /**
13
- * Write data as Nostr event (parameterized replaceable event)
19
+ * Single-path subscription manager (for nostrSubscribe).
20
+ * Maps: subscriptionKey -> { subscription, callbacks: [] }
21
+ * @private
22
+ */
23
+ const singlePathSubscriptions = new Map();
24
+
25
+ /**
26
+ * Query deduplication for nostrGet - prevents duplicate relay queries.
27
+ * Maps: queryKey -> { promise, timestamp, callbacks: [] }
28
+ * @private
29
+ */
30
+ const pendingQueries = new Map();
31
+
32
+ /**
33
+ * Time window for query deduplication (2 seconds).
34
+ * @private
35
+ * @constant {number}
36
+ */
37
+ const QUERY_DEDUP_WINDOW = 2000;
38
+
39
+ /**
40
+ * Write data as Nostr event (parameterized replaceable event).
41
+ *
14
42
  * @param {Object} client - NostrClient instance
15
43
  * @param {string} path - Path identifier (encoded in d-tag)
16
44
  * @param {Object} data - Data to store
17
- * @param {number} kind - Event kind (default: 30000 for parameterized replaceable)
18
- * @returns {Promise<Object>} Published event result
45
+ * @param {number} [kind=30000] - Event kind (default: 30000 for parameterized replaceable)
46
+ * @returns {Promise<Object>} Published event result with relay responses
47
+ * @example
48
+ * const result = await nostrPut(client, 'myapp/holon123/items/item1', { name: 'Test' });
49
+ * console.log(result.event.id); // Event ID
19
50
  */
20
51
  export async function nostrPut(client, path, data, kind = 30000) {
21
52
  const dataEvent = {
@@ -32,52 +63,137 @@ export async function nostrPut(client, path, data, kind = 30000) {
32
63
  }
33
64
 
34
65
  /**
35
- * Read data from Nostr (query by d-tag)
36
- * LOCAL-FIRST: Checks persistent storage first, never blocks on network
66
+ * Read data from Nostr (query by d-tag).
67
+ *
68
+ * LOCAL-FIRST: Checks persistent storage first, never blocks on network.
69
+ * Uses query deduplication to prevent duplicate relay queries within a time window.
70
+ *
37
71
  * @param {Object} client - NostrClient instance
38
72
  * @param {string} path - Path identifier
39
- * @param {number} kind - Event kind (default: 30000)
40
- * @param {Object} options - Query options
41
- * @param {string[]} options.authors - Array of public keys to query (default: [client.publicKey])
42
- * @param {boolean} options.includeAuthor - If true, adds _author field to returned data
43
- * @param {boolean} options.skipPersistent - If true, skip persistent storage check (default: false)
73
+ * @param {number} [kind=30000] - Event kind (default: 30000)
74
+ * @param {Object} [options={}] - Query options
75
+ * @param {string[]} [options.authors] - Array of public keys to query (default: [client.publicKey])
76
+ * @param {boolean} [options.includeAuthor=false] - If true, adds _author field to returned data
77
+ * @param {boolean} [options.skipPersistent=false] - If true, skip persistent storage check
78
+ * @param {number} [options.timeout=30000] - Query timeout in milliseconds
44
79
  * @returns {Promise<Object|null>} Data or null if not found
80
+ * @example
81
+ * const data = await nostrGet(client, 'myapp/holon1/items/item1');
82
+ * if (data) {
83
+ * console.log(data.name);
84
+ * }
45
85
  */
46
86
  export async function nostrGet(client, path, kind = 30000, options = {}) {
47
87
  const timeout = options.timeout !== undefined ? options.timeout : 30000;
48
88
  const authors = options.authors || [client.publicKey];
49
89
 
50
- // LOCAL-FIRST: Check persistent storage FIRST (never blocks on network)
90
+ // CHECK LRU CACHE FIRST (always up-to-date, even before batched persist)
91
+ // This ensures immediate reads after writes return fresh data
92
+ if (!options.skipCache && client.getCachedByPath) {
93
+ const cachedEvent = client.getCachedByPath(path, kind);
94
+ if (cachedEvent && cachedEvent.content) {
95
+ // Verify author is in requested authors list
96
+ if (authors.includes(cachedEvent.pubkey)) {
97
+ try {
98
+ const data = JSON.parse(cachedEvent.content);
99
+
100
+ // Skip null/undefined or deleted items
101
+ if (!data || data._deleted) {
102
+ return null;
103
+ }
104
+
105
+ // Optionally include author information
106
+ if (options.includeAuthor) {
107
+ data._author = cachedEvent.pubkey;
108
+ }
109
+
110
+ return data;
111
+ } catch (error) {
112
+ // Fall through if parsing fails
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // LOCAL-FIRST: Check persistent storage (may be slightly stale due to batched writes)
51
119
  if (!options.skipPersistent && client.persistentGet) {
52
120
  const persistedEvent = await client.persistentGet(path);
53
121
  if (persistedEvent && persistedEvent.content) {
54
- try {
55
- const data = JSON.parse(persistedEvent.content);
122
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
123
+ if (!authors.includes(persistedEvent.pubkey)) {
124
+ // Author mismatch - fall through to relay query
125
+ } else {
126
+ try {
127
+ const data = JSON.parse(persistedEvent.content);
56
128
 
57
- // Skip deleted items
58
- if (data._deleted) {
59
- return null;
60
- }
129
+ // Skip null/undefined or deleted items
130
+ if (!data || data._deleted) {
131
+ return null;
132
+ }
61
133
 
62
- // Optionally include author information
63
- if (options.includeAuthor) {
64
- data._author = persistedEvent.pubkey;
65
- }
134
+ // Optionally include author information
135
+ if (options.includeAuthor) {
136
+ data._author = persistedEvent.pubkey;
137
+ }
66
138
 
67
- // Trigger background refresh from relays (fire-and-forget)
68
- if (client.refreshPathInBackground) {
69
- client.refreshPathInBackground(path, kind, { authors, timeout });
70
- }
139
+ // Trigger background refresh from relays (fire-and-forget)
140
+ if (client.refreshPathInBackground) {
141
+ client.refreshPathInBackground(path, kind, { authors, timeout });
142
+ }
71
143
 
72
- return data;
73
- } catch (error) {
74
- // Fall through to relay query if parsing fails
75
- console.warn('[nostrGet] Failed to parse persisted event:', error);
144
+ return data;
145
+ } catch (error) {
146
+ // Fall through to relay query if parsing fails
147
+ console.warn('[nostrGet] Failed to parse persisted event:', error);
148
+ }
76
149
  }
77
150
  }
78
151
  }
79
152
 
80
- // Fallback to relay query (existing logic)
153
+ // Query deduplication: Check if same query is already pending
154
+ const queryKey = `get:${client.publicKey}:${kind}:${path}:${authors.join(',')}`;
155
+ const pending = pendingQueries.get(queryKey);
156
+
157
+ if (pending && Date.now() - pending.timestamp < QUERY_DEDUP_WINDOW) {
158
+ // Reuse pending query result
159
+ return pending.promise;
160
+ }
161
+
162
+ // Create new query with deduplication
163
+ const queryPromise = _executeNostrGet(client, path, kind, authors, timeout, options);
164
+
165
+ pendingQueries.set(queryKey, {
166
+ promise: queryPromise,
167
+ timestamp: Date.now(),
168
+ });
169
+
170
+ // Clean up after promise resolves
171
+ queryPromise.finally(() => {
172
+ // Remove from pending after a short delay to allow coalescing
173
+ setTimeout(() => {
174
+ const current = pendingQueries.get(queryKey);
175
+ if (current && current.promise === queryPromise) {
176
+ pendingQueries.delete(queryKey);
177
+ }
178
+ }, QUERY_DEDUP_WINDOW);
179
+ });
180
+
181
+ return queryPromise;
182
+ }
183
+
184
+ /**
185
+ * Internal function to execute nostrGet query.
186
+ *
187
+ * @private
188
+ * @param {Object} client - NostrClient instance
189
+ * @param {string} path - Path identifier
190
+ * @param {number} kind - Event kind
191
+ * @param {string[]} authors - Array of author public keys
192
+ * @param {number} timeout - Query timeout
193
+ * @param {Object} options - Query options
194
+ * @returns {Promise<Object|null>} Data or null
195
+ */
196
+ async function _executeNostrGet(client, path, kind, authors, timeout, options) {
81
197
  const filter = {
82
198
  kinds: [kind],
83
199
  authors: authors, // Support multiple authors for cross-holosphere queries
@@ -87,37 +203,52 @@ export async function nostrGet(client, path, kind = 30000, options = {}) {
87
203
 
88
204
  const events = await client.query(filter, { timeout });
89
205
 
90
- if (events.length === 0) {
206
+ // Filter by author (relays may not respect authors filter)
207
+ const authoredEvents = events.filter(event => authors.includes(event.pubkey));
208
+
209
+ if (authoredEvents.length === 0) {
91
210
  return null;
92
211
  }
93
212
 
94
- // Get most recent event (across all authors)
95
- const event = events.sort((a, b) => b.created_at - a.created_at)[0];
213
+ // Get most recent event (across allowed authors)
214
+ const event = authoredEvents.sort((a, b) => b.created_at - a.created_at)[0];
96
215
 
97
216
  try {
98
217
  const data = JSON.parse(event.content);
218
+ // Skip null/undefined or deleted items
219
+ if (!data || data._deleted) {
220
+ return null;
221
+ }
99
222
  // Optionally include author information
100
223
  if (options.includeAuthor) {
101
224
  data._author = event.pubkey;
102
225
  }
103
226
  return data;
104
227
  } catch (error) {
105
- console.error('Failed to parse event content:', error);
228
+ // Silent - return null for unparseable events
106
229
  return null;
107
230
  }
108
231
  }
109
232
 
110
233
  /**
111
- * Query all events under a path prefix
112
- * LOCAL-FIRST: Checks persistent storage first, never blocks on network
234
+ * Query all events under a path prefix.
235
+ *
236
+ * LOCAL-FIRST: Checks persistent storage first, never blocks on network.
237
+ * Uses query deduplication to prevent duplicate relay queries within a time window.
238
+ *
113
239
  * @param {Object} client - NostrClient instance
114
240
  * @param {string} pathPrefix - Path prefix to match
115
- * @param {number} kind - Event kind (default: 30000)
116
- * @param {Object} options - Query options
117
- * @param {string[]} options.authors - Array of public keys to query (default: [client.publicKey])
118
- * @param {boolean} options.includeAuthor - If true, adds _author field to returned data
119
- * @param {boolean} options.skipPersistent - If true, skip persistent storage check (default: false)
241
+ * @param {number} [kind=30000] - Event kind (default: 30000)
242
+ * @param {Object} [options={}] - Query options
243
+ * @param {string[]} [options.authors] - Array of public keys to query (default: [client.publicKey])
244
+ * @param {boolean} [options.includeAuthor=false] - If true, adds _author field to returned data
245
+ * @param {boolean} [options.skipPersistent=false] - If true, skip persistent storage check
246
+ * @param {number} [options.timeout=30000] - Query timeout in milliseconds
247
+ * @param {number} [options.limit=1000] - Maximum number of events to retrieve
120
248
  * @returns {Promise<Array>} Array of data objects
249
+ * @example
250
+ * const items = await nostrGetAll(client, 'myapp/holon1/items/');
251
+ * console.log(`Found ${items.length} items`);
121
252
  */
122
253
  export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}) {
123
254
  const timeout = options.timeout !== undefined ? options.timeout : 30000;
@@ -133,6 +264,9 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
133
264
  for (const event of persistedEvents) {
134
265
  if (!event || !event.tags) continue;
135
266
 
267
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
268
+ if (!authors.includes(event.pubkey)) continue;
269
+
136
270
  const dTag = event.tags.find(t => t[0] === 'd');
137
271
  if (!dTag || !dTag[1] || !dTag[1].startsWith(pathPrefix)) continue;
138
272
 
@@ -143,8 +277,11 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
143
277
  try {
144
278
  const data = JSON.parse(event.content);
145
279
 
146
- // Skip deleted items
147
- if (data._deleted) continue;
280
+ // Handle null/undefined or deleted items - remove from map if this is newer
281
+ if (!data || data._deleted) {
282
+ byPath.delete(path);
283
+ continue;
284
+ }
148
285
 
149
286
  // Optionally include author information
150
287
  if (options.includeAuthor) {
@@ -167,7 +304,50 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
167
304
  }
168
305
  }
169
306
 
170
- // Fallback to relay query (existing logic)
307
+ // Query deduplication: Check if same query is already pending
308
+ const queryKey = `getAll:${client.publicKey}:${kind}:${pathPrefix}:${authors.join(',')}`;
309
+ const pending = pendingQueries.get(queryKey);
310
+
311
+ if (pending && Date.now() - pending.timestamp < QUERY_DEDUP_WINDOW) {
312
+ // Reuse pending query result
313
+ return pending.promise;
314
+ }
315
+
316
+ // Create new query with deduplication
317
+ const queryPromise = _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, limit, options);
318
+
319
+ pendingQueries.set(queryKey, {
320
+ promise: queryPromise,
321
+ timestamp: Date.now(),
322
+ });
323
+
324
+ // Clean up after promise resolves
325
+ queryPromise.finally(() => {
326
+ setTimeout(() => {
327
+ const current = pendingQueries.get(queryKey);
328
+ if (current && current.promise === queryPromise) {
329
+ pendingQueries.delete(queryKey);
330
+ }
331
+ }, QUERY_DEDUP_WINDOW);
332
+ });
333
+
334
+ return queryPromise;
335
+ }
336
+
337
+ /**
338
+ * Internal function to execute nostrGetAll query.
339
+ *
340
+ * @private
341
+ * @param {Object} client - NostrClient instance
342
+ * @param {string} pathPrefix - Path prefix to match
343
+ * @param {number} kind - Event kind
344
+ * @param {string[]} authors - Array of author public keys
345
+ * @param {number} timeout - Query timeout
346
+ * @param {number} limit - Maximum results
347
+ * @param {Object} options - Query options
348
+ * @returns {Promise<Array>} Array of data objects
349
+ */
350
+ async function _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, limit, options) {
171
351
  const filter = {
172
352
  kinds: [kind],
173
353
  authors: authors,
@@ -176,10 +356,12 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
176
356
 
177
357
  const events = await client.query(filter, { timeout });
178
358
 
179
- // Filter by path prefix in application layer
359
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
180
360
  const matching = events.filter(event => {
181
361
  const dTag = event.tags.find(t => t[0] === 'd');
182
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
362
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
363
+ const authorAllowed = authors.includes(event.pubkey);
364
+ return pathMatches && authorAllowed;
183
365
  });
184
366
 
185
367
  // Parse content and group by d-tag (keep latest only, across all authors)
@@ -192,8 +374,11 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
192
374
  try {
193
375
  const data = JSON.parse(event.content);
194
376
 
195
- // Skip deleted items
196
- if (data._deleted) continue;
377
+ // Handle null/undefined or deleted items - remove from map if this is newer
378
+ if (!data || data._deleted) {
379
+ byPath.delete(dTag);
380
+ continue;
381
+ }
197
382
 
198
383
  // Optionally include author information
199
384
  if (options.includeAuthor) {
@@ -201,7 +386,7 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
201
386
  }
202
387
  byPath.set(dTag, { data, created_at: event.created_at });
203
388
  } catch (error) {
204
- console.error('Failed to parse event content:', error);
389
+ // Silent - skip unparseable events
205
390
  }
206
391
  }
207
392
  }
@@ -211,14 +396,18 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
211
396
  }
212
397
 
213
398
  /**
214
- * Query all events under a path prefix (HYBRID MODE - local + relay)
215
- * Checks local cache first, then merges with relay data
399
+ * Query all events under a path prefix (HYBRID MODE - local + relay).
400
+ *
401
+ * Checks local cache first, then merges with relay data.
402
+ *
216
403
  * @param {Object} client - NostrClient instance
217
404
  * @param {string} pathPrefix - Path prefix to match
218
- * @param {number} kind - Event kind (default: 30000)
219
- * @param {Object} options - Query options
220
- * @param {string[]} options.authors - Array of public keys to query (default: [client.publicKey])
221
- * @param {boolean} options.includeAuthor - If true, adds _author field to returned data
405
+ * @param {number} [kind=30000] - Event kind (default: 30000)
406
+ * @param {Object} [options={}] - Query options
407
+ * @param {string[]} [options.authors] - Array of public keys to query (default: [client.publicKey])
408
+ * @param {boolean} [options.includeAuthor=false] - If true, adds _author field to returned data
409
+ * @param {number} [options.timeout=30000] - Query timeout in milliseconds
410
+ * @param {number} [options.limit=1000] - Maximum number of events to retrieve
222
411
  * @returns {Promise<Array>} Array of data objects (merged from local + relay)
223
412
  */
224
413
  export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, options = {}) {
@@ -237,10 +426,12 @@ export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, option
237
426
 
238
427
  const events = await queryMethod.call(client, filter, { timeout });
239
428
 
240
- // Filter by path prefix in application layer
429
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
241
430
  const matching = events.filter(event => {
242
431
  const dTag = event.tags.find(t => t[0] === 'd');
243
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
432
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
433
+ const authorAllowed = authors.includes(event.pubkey);
434
+ return pathMatches && authorAllowed;
244
435
  });
245
436
 
246
437
  // Parse content and group by d-tag (keep latest only)
@@ -252,13 +443,22 @@ export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, option
252
443
  if (!existing || event.created_at > existing.created_at) {
253
444
  try {
254
445
  const data = JSON.parse(event.content);
446
+ // Skip null or non-object content
447
+ if (data === null || typeof data !== 'object') {
448
+ continue;
449
+ }
450
+ // Handle deleted items - remove from map if this is newer
451
+ if (data._deleted) {
452
+ byPath.delete(dTag);
453
+ continue;
454
+ }
255
455
  // Optionally include author information
256
456
  if (options.includeAuthor) {
257
457
  data._author = event.pubkey;
258
458
  }
259
459
  byPath.set(dTag, { data, created_at: event.created_at });
260
460
  } catch (error) {
261
- console.error('Failed to parse event content:', error);
461
+ // Silent - skip unparseable events
262
462
  }
263
463
  }
264
464
  }
@@ -268,11 +468,19 @@ export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, option
268
468
  }
269
469
 
270
470
  /**
271
- * Delete data (publish deletion event - NIP-09)
471
+ * Delete data (publish deletion event - NIP-09).
472
+ *
473
+ * Publishes a tombstone event and a NIP-09 deletion event to mark data as deleted.
474
+ *
272
475
  * @param {Object} client - NostrClient instance
273
476
  * @param {string} path - Path identifier
274
- * @param {number} kind - Original event kind (default: 30000)
275
- * @returns {Promise<Object>} Deletion event result
477
+ * @param {number} [kind=30000] - Original event kind (default: 30000)
478
+ * @returns {Promise<Object>} Deletion event result with status
479
+ * @example
480
+ * const result = await nostrDelete(client, 'myapp/holon1/items/item1');
481
+ * if (result.reason !== 'not_found') {
482
+ * console.log('Item deleted successfully');
483
+ * }
276
484
  */
277
485
  export async function nostrDelete(client, path, kind = 30000) {
278
486
  // Read existing data first
@@ -289,7 +497,7 @@ export async function nostrDelete(client, path, kind = 30000) {
289
497
  };
290
498
 
291
499
  // Publish tombstone as replacement event
292
- // This ensures reads will see _deleted flag
500
+ // This ensures reads will see _deleted flag (also updates LRU cache via _cacheEvent)
293
501
  const result = await nostrPut(client, path, tombstone, kind);
294
502
 
295
503
  // Also delete from persistent storage to prevent resurrection on restart
@@ -301,11 +509,6 @@ export async function nostrDelete(client, path, kind = 30000) {
301
509
  }
302
510
  }
303
511
 
304
- // Clear from memory cache
305
- if (client.clearCache) {
306
- client.clearCache(path);
307
- }
308
-
309
512
  // Also publish NIP-09 deletion event for relay compliance
310
513
  const coordinate = `${kind}:${client.publicKey}:${path}`;
311
514
  const deletionEvent = {
@@ -319,25 +522,29 @@ export async function nostrDelete(client, path, kind = 30000) {
319
522
 
320
523
  // Publish deletion event (best effort, don't block on failure)
321
524
  try {
322
- const publishResult = await client.publish(deletionEvent);
323
- console.log(`✅ NIP-09 deletion event published for ${path}:`, {
324
- coordinate,
325
- relays: publishResult?.relays?.length || 0,
326
- successful: publishResult?.successful || []
327
- });
525
+ await client.publish(deletionEvent);
526
+ // Silent success - deletion queued
328
527
  } catch (error) {
329
- console.error(`❌ Failed to publish NIP-09 deletion event for ${path}:`, error.message);
528
+ // Silent - deletion will be retried
330
529
  }
331
530
 
332
531
  return result;
333
532
  }
334
533
 
534
+ // Rate limit delay between bulk delete operations (ms)
535
+ const DELETE_RATE_LIMIT_MS = 100;
536
+
335
537
  /**
336
- * Delete all data with path prefix (publish deletion events - NIP-09)
538
+ * Delete all data with path prefix (publish deletion events - NIP-09).
539
+ * Includes built-in rate limiting to avoid overwhelming relays.
540
+ *
337
541
  * @param {Object} client - NostrClient instance
338
542
  * @param {string} pathPrefix - Path prefix to delete all items under
339
- * @param {number} kind - Original event kind (default: 30000)
340
- * @returns {Promise<Object>} Deletion results
543
+ * @param {number} [kind=30000] - Original event kind (default: 30000)
544
+ * @returns {Promise<Object>} Deletion results with success count
545
+ * @example
546
+ * const result = await nostrDeleteAll(client, 'myapp/holon1/items/');
547
+ * console.log(`Deleted ${result.count} items`);
341
548
  */
342
549
  export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
343
550
  // Query events from relay
@@ -388,9 +595,10 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
388
595
  return { success: true, count: 0, results: [] };
389
596
  }
390
597
 
391
- // Publish tombstone events for each matching item
598
+ // Publish tombstone events for each matching item (with rate limiting)
392
599
  const tombstoneResults = [];
393
- for (const event of matching) {
600
+ for (let i = 0; i < matching.length; i++) {
601
+ const event = matching[i];
394
602
  const dTag = event.tags.find(t => t[0] === 'd')[1];
395
603
 
396
604
  try {
@@ -400,7 +608,7 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
400
608
  _deletedAt: Date.now()
401
609
  };
402
610
 
403
- // Publish tombstone replacement
611
+ // Publish tombstone replacement (this also updates LRU cache via _cacheEvent)
404
612
  const result = await nostrPut(client, dTag, tombstone, kind);
405
613
  tombstoneResults.push(result);
406
614
 
@@ -413,9 +621,9 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
413
621
  }
414
622
  }
415
623
 
416
- // Clear from memory cache
417
- if (client.clearCache) {
418
- client.clearCache(dTag);
624
+ // Rate limit: add delay between deletes to avoid overwhelming relays
625
+ if (i < matching.length - 1 && DELETE_RATE_LIMIT_MS > 0) {
626
+ await new Promise(resolve => setTimeout(resolve, DELETE_RATE_LIMIT_MS));
419
627
  }
420
628
  } catch (error) {
421
629
  console.error(`Failed to delete item ${dTag}:`, error);
@@ -438,15 +646,10 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
438
646
  content: '', // Optional deletion reason/note
439
647
  };
440
648
 
441
- const publishResult = await client.publish(deletionEvent);
442
- console.log(`✅ NIP-09 bulk deletion event published for ${pathPrefix}:`, {
443
- itemsDeleted: matching.length,
444
- coordinates: coordinates.length,
445
- relays: publishResult?.relays?.length || 0,
446
- successful: publishResult?.successful || []
447
- });
649
+ await client.publish(deletionEvent);
650
+ // Silent success - bulk deletion queued
448
651
  } catch (error) {
449
- console.error(`❌ Failed to publish NIP-09 bulk deletion event for ${pathPrefix}:`, error.message);
652
+ // Silent - bulk deletion will be retried
450
653
  }
451
654
 
452
655
  return {
@@ -457,16 +660,60 @@ export async function nostrDeleteAll(client, pathPrefix, kind = 30000) {
457
660
  }
458
661
 
459
662
  /**
460
- * Subscribe to path changes
663
+ * Subscribe to path changes.
664
+ *
665
+ * Uses subscription deduplication - multiple subscribers to same path share one relay subscription.
666
+ *
461
667
  * @param {Object} client - NostrClient instance
462
668
  * @param {string} path - Path to subscribe to
463
669
  * @param {Function} callback - Callback function (data, event) => void
464
- * @param {Object} options - Subscription options
670
+ * @param {Object} [options={}] - Subscription options
671
+ * @param {number} [options.kind=30000] - Event kind to subscribe to
465
672
  * @returns {Object} Subscription object with unsubscribe method
673
+ * @example
674
+ * const sub = nostrSubscribe(client, 'myapp/holon1/items/item1', (data, event) => {
675
+ * console.log('Item updated:', data);
676
+ * });
677
+ * // Later: sub.unsubscribe();
466
678
  */
467
679
  export function nostrSubscribe(client, path, callback, options = {}) {
468
680
  const kind = options.kind || 30000;
469
- const includeInitial = options.includeInitial !== false;
681
+
682
+ // Create unique key for this subscription
683
+ const subscriptionKey = `single:${client.publicKey}:${kind}:${path}`;
684
+
685
+ // Check if we already have an active subscription for this path
686
+ const existing = singlePathSubscriptions.get(subscriptionKey);
687
+ if (existing) {
688
+ // Add callback to existing subscription
689
+ existing.callbacks.push(callback);
690
+
691
+ // Return wrapper that removes only this callback on unsubscribe
692
+ return {
693
+ unsubscribe: () => {
694
+ const idx = existing.callbacks.indexOf(callback);
695
+ if (idx > -1) {
696
+ existing.callbacks.splice(idx, 1);
697
+ }
698
+
699
+ // If no more callbacks, unsubscribe the whole thing
700
+ if (existing.callbacks.length === 0) {
701
+ existing.subscription.unsubscribe();
702
+ singlePathSubscriptions.delete(subscriptionKey);
703
+ }
704
+ }
705
+ };
706
+ }
707
+
708
+ // Create new subscription
709
+ const callbacks = [callback];
710
+ const subscriptionInfo = {
711
+ callbacks,
712
+ subscription: null,
713
+ };
714
+
715
+ // Store before creating subscription to handle race conditions
716
+ singlePathSubscriptions.set(subscriptionKey, subscriptionInfo);
470
717
 
471
718
  const filter = {
472
719
  kinds: [kind],
@@ -475,40 +722,61 @@ export function nostrSubscribe(client, path, callback, options = {}) {
475
722
  limit: 10, // Limit results for single item subscription
476
723
  };
477
724
 
478
- let initialLoadComplete = false;
479
-
480
725
  const subscription = client.subscribe(
481
726
  filter,
482
727
  (event) => {
483
728
  // Verify event is from our public key (relay may not respect author filter)
484
729
  if (event.pubkey !== client.publicKey) {
485
- console.warn('[nostrSubscribe] Rejecting event from different author:', {
486
- expected: client.publicKey,
487
- received: event.pubkey,
488
- eventId: event.id
489
- });
490
730
  return;
491
731
  }
492
732
 
493
733
  try {
494
734
  const data = JSON.parse(event.content);
495
- callback(data, event);
735
+
736
+ // Skip null/undefined or deleted items - don't send tombstones to subscribers
737
+ if (!data || data._deleted) {
738
+ return;
739
+ }
740
+
741
+ // Dispatch to all registered callbacks
742
+ for (const cb of callbacks) {
743
+ try {
744
+ cb(data, event);
745
+ } catch (err) {
746
+ console.error('Subscription callback error:', err);
747
+ }
748
+ }
496
749
  } catch (error) {
497
750
  console.error('Failed to parse event in subscription:', error);
498
751
  }
499
752
  },
500
753
  {
501
754
  onEOSE: () => {
502
- initialLoadComplete = true;
755
+ // EOSE received
503
756
  },
504
757
  }
505
758
  );
506
759
 
507
- return subscription;
760
+ subscriptionInfo.subscription = subscription;
761
+
762
+ return {
763
+ unsubscribe: () => {
764
+ const idx = callbacks.indexOf(callback);
765
+ if (idx > -1) {
766
+ callbacks.splice(idx, 1);
767
+ }
768
+
769
+ // If no more callbacks, unsubscribe the whole thing
770
+ if (callbacks.length === 0) {
771
+ subscription.unsubscribe();
772
+ singlePathSubscriptions.delete(subscriptionKey);
773
+ }
774
+ }
775
+ };
508
776
  }
509
777
 
510
778
  /**
511
- * Subscribe to path prefix (multiple paths)
779
+ * Subscribe to path prefix (multiple paths).
512
780
  *
513
781
  * Subscribes to data events and uses Page Visibility API to refresh when tab becomes visible.
514
782
  * Note: Nostr relays do not broadcast replaceable event updates to active subscriptions.
@@ -516,8 +784,14 @@ export function nostrSubscribe(client, path, callback, options = {}) {
516
784
  * @param {Object} client - NostrClient instance
517
785
  * @param {string} pathPrefix - Path prefix to match
518
786
  * @param {Function} callback - Callback function (data, path, event) => void
519
- * @param {Object} options - Subscription options
787
+ * @param {Object} [options={}] - Subscription options
788
+ * @param {number} [options.kind=30000] - Event kind to subscribe to
520
789
  * @returns {Promise<Object>} Subscription object with unsubscribe method
790
+ * @example
791
+ * const sub = await nostrSubscribeMany(client, 'myapp/holon1/items/', (data, path, event) => {
792
+ * console.log('Item event:', data);
793
+ * });
794
+ * // Later: sub.unsubscribe();
521
795
  */
522
796
  export async function nostrSubscribeMany(client, pathPrefix, callback, options = {}) {
523
797
  const kind = options.kind || 30000;
@@ -589,9 +863,15 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
589
863
 
590
864
  // Client-side prefix filter
591
865
  if (dTag && dTag.startsWith(pathPrefix)) {
592
- acceptedCount++;
593
866
  try {
594
867
  const data = JSON.parse(event.content);
868
+
869
+ // Skip null/undefined data or deleted items - don't send tombstones to subscribers
870
+ if (!data || data._deleted) {
871
+ return;
872
+ }
873
+
874
+ acceptedCount++;
595
875
  // Notify all registered callbacks
596
876
  for (const cb of callbacks) {
597
877
  cb(data, dTag, event);
@@ -672,12 +952,16 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
672
952
  }
673
953
 
674
954
  /**
675
- * Update data (merge with existing)
955
+ * Update data (merge with existing).
956
+ *
676
957
  * @param {Object} client - NostrClient instance
677
958
  * @param {string} path - Path identifier
678
959
  * @param {Object} updates - Fields to update
679
- * @param {number} kind - Event kind (default: 30000)
960
+ * @param {number} [kind=30000] - Event kind (default: 30000)
680
961
  * @returns {Promise<Object>} Updated event result
962
+ * @throws {Error} If no data found at path
963
+ * @example
964
+ * await nostrUpdate(client, 'myapp/holon1/items/item1', { status: 'completed' });
681
965
  */
682
966
  export async function nostrUpdate(client, path, updates, kind = 30000) {
683
967
  // Read existing data
@@ -695,10 +979,11 @@ export async function nostrUpdate(client, path, updates, kind = 30000) {
695
979
  }
696
980
 
697
981
  /**
698
- * Batch read multiple paths
982
+ * Batch read multiple paths.
983
+ *
699
984
  * @param {Object} client - NostrClient instance
700
985
  * @param {string[]} paths - Array of paths
701
- * @param {number} kind - Event kind (default: 30000)
986
+ * @param {number} [kind=30000] - Event kind (default: 30000)
702
987
  * @returns {Promise<Object>} Object mapping paths to data
703
988
  */
704
989
  export async function nostrBatchGet(client, paths, kind = 30000) {
@@ -739,10 +1024,11 @@ export async function nostrBatchGet(client, paths, kind = 30000) {
739
1024
  }
740
1025
 
741
1026
  /**
742
- * Batch write multiple paths
1027
+ * Batch write multiple paths.
1028
+ *
743
1029
  * @param {Object} client - NostrClient instance
744
1030
  * @param {Object} pathDataMap - Object mapping paths to data
745
- * @param {number} kind - Event kind (default: 30000)
1031
+ * @param {number} [kind=30000] - Event kind (default: 30000)
746
1032
  * @returns {Promise<Array>} Array of publish results
747
1033
  */
748
1034
  export async function nostrBatchPut(client, pathDataMap, kind = 30000) {
@@ -754,11 +1040,13 @@ export async function nostrBatchPut(client, pathDataMap, kind = 30000) {
754
1040
  }
755
1041
 
756
1042
  /**
757
- * Retry operation with exponential backoff
1043
+ * Retry operation with exponential backoff.
1044
+ *
758
1045
  * @param {Function} operation - Async function to retry
759
- * @param {number} maxRetries - Max retry attempts (default: 3)
760
- * @param {number} baseDelay - Base delay in ms (default: 100ms)
1046
+ * @param {number} [maxRetries=3] - Max retry attempts
1047
+ * @param {number} [baseDelay=100] - Base delay in ms
761
1048
  * @returns {Promise<any>} Promise resolving to operation result
1049
+ * @throws {Error} Last error if all retries fail
762
1050
  */
763
1051
  export async function nostrRetry(operation, maxRetries = 3, baseDelay = 100) {
764
1052
  let lastError;
@@ -779,12 +1067,14 @@ export async function nostrRetry(operation, maxRetries = 3, baseDelay = 100) {
779
1067
  }
780
1068
 
781
1069
  /**
782
- * Wait for specific condition on data
1070
+ * Wait for specific condition on data.
1071
+ *
783
1072
  * @param {Object} client - NostrClient instance
784
1073
  * @param {string} path - Path to watch
785
1074
  * @param {Function} predicate - Condition function (data) => boolean
786
- * @param {number} timeout - Timeout in ms (default: 5000ms)
1075
+ * @param {number} [timeout=5000] - Timeout in ms
787
1076
  * @returns {Promise<any>} Promise resolving when condition is met
1077
+ * @throws {Error} If timeout occurs before condition is met
788
1078
  */
789
1079
  export async function nostrWaitFor(client, path, predicate, timeout = 5000) {
790
1080
  return new Promise((resolve, reject) => {