@swal/edge-mesh 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +128 -0
- package/NOTICE +37 -0
- package/README.md +470 -2
- package/dist/adapters/gpu-agent/index.d.ts +61 -0
- package/dist/adapters/gpu-agent/index.d.ts.map +1 -0
- package/dist/adapters/gpu-agent/index.js +364 -0
- package/dist/adapters/gpu-agent/index.js.map +1 -0
- package/dist/adapters/maloca-salud/citas-distribuidas.d.ts +19 -0
- package/dist/adapters/maloca-salud/citas-distribuidas.d.ts.map +1 -0
- package/dist/adapters/maloca-salud/citas-distribuidas.js +51 -0
- package/dist/adapters/maloca-salud/citas-distribuidas.js.map +1 -0
- package/dist/adapters/maloca-salud/eps-bridge.d.ts +15 -0
- package/dist/adapters/maloca-salud/eps-bridge.d.ts.map +1 -0
- package/dist/adapters/maloca-salud/eps-bridge.js +27 -0
- package/dist/adapters/maloca-salud/eps-bridge.js.map +1 -0
- package/dist/adapters/maloca-salud/patient-profile.d.ts +20 -0
- package/dist/adapters/maloca-salud/patient-profile.d.ts.map +1 -0
- package/dist/adapters/maloca-salud/patient-profile.js +36 -0
- package/dist/adapters/maloca-salud/patient-profile.js.map +1 -0
- package/dist/adapters/maloca-salud/types.d.ts +40 -0
- package/dist/adapters/maloca-salud/types.d.ts.map +1 -0
- package/dist/adapters/maloca-salud/types.js +2 -0
- package/dist/adapters/maloca-salud/types.js.map +1 -0
- package/dist/adapters/maloca-veeduria/contract-bridge.d.ts +31 -0
- package/dist/adapters/maloca-veeduria/contract-bridge.d.ts.map +1 -0
- package/dist/adapters/maloca-veeduria/contract-bridge.js +103 -0
- package/dist/adapters/maloca-veeduria/contract-bridge.js.map +1 -0
- package/dist/adapters/maloca-veeduria/evidentia-veeduria.d.ts +14 -0
- package/dist/adapters/maloca-veeduria/evidentia-veeduria.d.ts.map +1 -0
- package/dist/adapters/maloca-veeduria/evidentia-veeduria.js +40 -0
- package/dist/adapters/maloca-veeduria/evidentia-veeduria.js.map +1 -0
- package/dist/adapters/maloca-veeduria/licitante-profile.d.ts +21 -0
- package/dist/adapters/maloca-veeduria/licitante-profile.d.ts.map +1 -0
- package/dist/adapters/maloca-veeduria/licitante-profile.js +58 -0
- package/dist/adapters/maloca-veeduria/licitante-profile.js.map +1 -0
- package/dist/adapters/maloca-veeduria/types.d.ts +30 -0
- package/dist/adapters/maloca-veeduria/types.d.ts.map +1 -0
- package/dist/adapters/maloca-veeduria/types.js +2 -0
- package/dist/adapters/maloca-veeduria/types.js.map +1 -0
- package/dist/adapters/maloca-xavier/agent-profile.d.ts +32 -0
- package/dist/adapters/maloca-xavier/agent-profile.d.ts.map +1 -0
- package/dist/adapters/maloca-xavier/agent-profile.js +56 -0
- package/dist/adapters/maloca-xavier/agent-profile.js.map +1 -0
- package/dist/adapters/maloca-xavier/codegraph-bridge.d.ts +27 -0
- package/dist/adapters/maloca-xavier/codegraph-bridge.d.ts.map +1 -0
- package/dist/adapters/maloca-xavier/codegraph-bridge.js +58 -0
- package/dist/adapters/maloca-xavier/codegraph-bridge.js.map +1 -0
- package/dist/adapters/maloca-xavier/llm-router.d.ts +25 -0
- package/dist/adapters/maloca-xavier/llm-router.d.ts.map +1 -0
- package/dist/adapters/maloca-xavier/llm-router.js +58 -0
- package/dist/adapters/maloca-xavier/llm-router.js.map +1 -0
- package/dist/authz/index.d.ts +81 -0
- package/dist/authz/index.d.ts.map +1 -0
- package/dist/authz/index.js +271 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/chat/index.d.ts +138 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +394 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/offline-queue.d.ts +32 -0
- package/dist/chat/offline-queue.d.ts.map +1 -0
- package/dist/chat/offline-queue.js +93 -0
- package/dist/chat/offline-queue.js.map +1 -0
- package/dist/core/DefaultEdgeMeshNode.d.ts +17 -0
- package/dist/core/DefaultEdgeMeshNode.d.ts.map +1 -0
- package/dist/core/DefaultEdgeMeshNode.js +57 -0
- package/dist/core/DefaultEdgeMeshNode.js.map +1 -0
- package/dist/core/node.d.ts +26 -0
- package/dist/core/node.d.ts.map +1 -0
- package/dist/core/node.js +16 -0
- package/dist/core/node.js.map +1 -0
- package/dist/edge-mesh.d.ts +139 -0
- package/dist/edge-mesh.d.ts.map +1 -0
- package/dist/edge-mesh.js +1146 -0
- package/dist/edge-mesh.js.map +1 -0
- package/dist/gos/GosBridge.d.ts +49 -0
- package/dist/gos/GosBridge.d.ts.map +1 -0
- package/dist/gos/GosBridge.js +133 -0
- package/dist/gos/GosBridge.js.map +1 -0
- package/dist/governance/authority.d.ts +44 -0
- package/dist/governance/authority.d.ts.map +1 -0
- package/dist/governance/authority.js +123 -0
- package/dist/governance/authority.js.map +1 -0
- package/dist/governance/index.d.ts +66 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +210 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/merge.d.ts +31 -0
- package/dist/governance/merge.d.ts.map +1 -0
- package/dist/governance/merge.js +167 -0
- package/dist/governance/merge.js.map +1 -0
- package/dist/identity/index.d.ts +199 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +415 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/maloca/backoffice.d.ts +31 -0
- package/dist/maloca/backoffice.d.ts.map +1 -0
- package/dist/maloca/backoffice.js +40 -0
- package/dist/maloca/backoffice.js.map +1 -0
- package/dist/maloca/event-bus.d.ts +60 -0
- package/dist/maloca/event-bus.d.ts.map +1 -0
- package/dist/maloca/event-bus.js +175 -0
- package/dist/maloca/event-bus.js.map +1 -0
- package/dist/maloca/evidentia.d.ts +136 -0
- package/dist/maloca/evidentia.d.ts.map +1 -0
- package/dist/maloca/evidentia.js +447 -0
- package/dist/maloca/evidentia.js.map +1 -0
- package/dist/maloca/gateway/api.d.ts +146 -0
- package/dist/maloca/gateway/api.d.ts.map +1 -0
- package/dist/maloca/gateway/api.js +228 -0
- package/dist/maloca/gateway/api.js.map +1 -0
- package/dist/maloca/gateway/auth.d.ts +20 -0
- package/dist/maloca/gateway/auth.d.ts.map +1 -0
- package/dist/maloca/gateway/auth.js +115 -0
- package/dist/maloca/gateway/auth.js.map +1 -0
- package/dist/maloca/gateway/dashboard.d.ts +42 -0
- package/dist/maloca/gateway/dashboard.d.ts.map +1 -0
- package/dist/maloca/gateway/dashboard.js +67 -0
- package/dist/maloca/gateway/dashboard.js.map +1 -0
- package/dist/maloca/gateway/websocket.d.ts +34 -0
- package/dist/maloca/gateway/websocket.d.ts.map +1 -0
- package/dist/maloca/gateway/websocket.js +71 -0
- package/dist/maloca/gateway/websocket.js.map +1 -0
- package/dist/maloca/karma.d.ts +165 -0
- package/dist/maloca/karma.d.ts.map +1 -0
- package/dist/maloca/karma.js +307 -0
- package/dist/maloca/karma.js.map +1 -0
- package/dist/maloca/kernel.d.ts +20 -0
- package/dist/maloca/kernel.d.ts.map +1 -0
- package/dist/maloca/kernel.js +76 -0
- package/dist/maloca/kernel.js.map +1 -0
- package/dist/maloca/metadata.d.ts +30 -0
- package/dist/maloca/metadata.d.ts.map +1 -0
- package/dist/maloca/metadata.js +80 -0
- package/dist/maloca/metadata.js.map +1 -0
- package/dist/maloca/perfil.d.ts +57 -0
- package/dist/maloca/perfil.d.ts.map +1 -0
- package/dist/maloca/perfil.js +113 -0
- package/dist/maloca/perfil.js.map +1 -0
- package/dist/maloca/plugin-registry.d.ts +27 -0
- package/dist/maloca/plugin-registry.d.ts.map +1 -0
- package/dist/maloca/plugin-registry.js +76 -0
- package/dist/maloca/plugin-registry.js.map +1 -0
- package/dist/maloca/polygon-bridge.d.ts +63 -0
- package/dist/maloca/polygon-bridge.d.ts.map +1 -0
- package/dist/maloca/polygon-bridge.js +198 -0
- package/dist/maloca/polygon-bridge.js.map +1 -0
- package/dist/maloca/types.d.ts +44 -0
- package/dist/maloca/types.d.ts.map +1 -0
- package/dist/maloca/types.js +2 -0
- package/dist/maloca/types.js.map +1 -0
- package/dist/medico/aval.d.ts +149 -0
- package/dist/medico/aval.d.ts.map +1 -0
- package/dist/medico/aval.js +281 -0
- package/dist/medico/aval.js.map +1 -0
- package/dist/medico/canonica.d.ts +96 -0
- package/dist/medico/canonica.d.ts.map +1 -0
- package/dist/medico/canonica.js +148 -0
- package/dist/medico/canonica.js.map +1 -0
- package/dist/medico/credencial.d.ts +160 -0
- package/dist/medico/credencial.d.ts.map +1 -0
- package/dist/medico/credencial.js +240 -0
- package/dist/medico/credencial.js.map +1 -0
- package/dist/medico/index.d.ts +36 -0
- package/dist/medico/index.d.ts.map +1 -0
- package/dist/medico/index.js +39 -0
- package/dist/medico/index.js.map +1 -0
- package/dist/medico/malla.d.ts +218 -0
- package/dist/medico/malla.d.ts.map +1 -0
- package/dist/medico/malla.js +347 -0
- package/dist/medico/malla.js.map +1 -0
- package/dist/medico/registro-profesional.d.ts +84 -0
- package/dist/medico/registro-profesional.d.ts.map +1 -0
- package/dist/medico/registro-profesional.js +135 -0
- package/dist/medico/registro-profesional.js.map +1 -0
- package/dist/medico/registro.d.ts +149 -0
- package/dist/medico/registro.d.ts.map +1 -0
- package/dist/medico/registro.js +173 -0
- package/dist/medico/registro.js.map +1 -0
- package/dist/medico/types.d.ts +287 -0
- package/dist/medico/types.d.ts.map +1 -0
- package/dist/medico/types.js +135 -0
- package/dist/medico/types.js.map +1 -0
- package/dist/medico/verificacion-registro.d.ts +300 -0
- package/dist/medico/verificacion-registro.d.ts.map +1 -0
- package/dist/medico/verificacion-registro.js +319 -0
- package/dist/medico/verificacion-registro.js.map +1 -0
- package/dist/mesh/index.d.ts +457 -0
- package/dist/mesh/index.d.ts.map +1 -0
- package/dist/mesh/index.js +929 -0
- package/dist/mesh/index.js.map +1 -0
- package/dist/namespaces/encrypted-plugin.d.ts +56 -0
- package/dist/namespaces/encrypted-plugin.d.ts.map +1 -0
- package/dist/namespaces/encrypted-plugin.js +173 -0
- package/dist/namespaces/encrypted-plugin.js.map +1 -0
- package/dist/namespaces/index.d.ts +52 -0
- package/dist/namespaces/index.d.ts.map +1 -0
- package/dist/namespaces/index.js +134 -0
- package/dist/namespaces/index.js.map +1 -0
- package/dist/namespaces/ivn-proofs.d.ts +54 -0
- package/dist/namespaces/ivn-proofs.d.ts.map +1 -0
- package/dist/namespaces/ivn-proofs.js +162 -0
- package/dist/namespaces/ivn-proofs.js.map +1 -0
- package/dist/namespaces/offers-gossip.d.ts +95 -0
- package/dist/namespaces/offers-gossip.d.ts.map +1 -0
- package/dist/namespaces/offers-gossip.js +213 -0
- package/dist/namespaces/offers-gossip.js.map +1 -0
- package/dist/node-memory/idb-store.d.ts +22 -0
- package/dist/node-memory/idb-store.d.ts.map +1 -0
- package/dist/node-memory/idb-store.js +157 -0
- package/dist/node-memory/idb-store.js.map +1 -0
- package/dist/node-memory/index.d.ts +14 -0
- package/dist/node-memory/index.d.ts.map +1 -0
- package/dist/node-memory/index.js +159 -0
- package/dist/node-memory/index.js.map +1 -0
- package/dist/node-memory/sync-flush.d.ts +11 -0
- package/dist/node-memory/sync-flush.d.ts.map +1 -0
- package/dist/node-memory/sync-flush.js +37 -0
- package/dist/node-memory/sync-flush.js.map +1 -0
- package/dist/node-memory/types.d.ts +34 -0
- package/dist/node-memory/types.d.ts.map +1 -0
- package/dist/node-memory/types.js +2 -0
- package/dist/node-memory/types.js.map +1 -0
- package/dist/node-memory/xavier-store.d.ts +12 -0
- package/dist/node-memory/xavier-store.d.ts.map +1 -0
- package/dist/node-memory/xavier-store.js +75 -0
- package/dist/node-memory/xavier-store.js.map +1 -0
- package/dist/op-log/index.d.ts +49 -0
- package/dist/op-log/index.d.ts.map +1 -0
- package/dist/op-log/index.js +190 -0
- package/dist/op-log/index.js.map +1 -0
- package/dist/presence/health.d.ts +54 -0
- package/dist/presence/health.d.ts.map +1 -0
- package/dist/presence/health.js +139 -0
- package/dist/presence/health.js.map +1 -0
- package/dist/presence/index.d.ts +85 -0
- package/dist/presence/index.d.ts.map +1 -0
- package/dist/presence/index.js +255 -0
- package/dist/presence/index.js.map +1 -0
- package/dist/presence/peer-health.d.ts +40 -0
- package/dist/presence/peer-health.d.ts.map +1 -0
- package/dist/presence/peer-health.js +118 -0
- package/dist/presence/peer-health.js.map +1 -0
- package/dist/protocol/canonical.d.ts +10 -0
- package/dist/protocol/canonical.d.ts.map +1 -0
- package/dist/protocol/canonical.js +32 -0
- package/dist/protocol/canonical.js.map +1 -0
- package/dist/protocol/crypto.d.ts +299 -0
- package/dist/protocol/crypto.d.ts.map +1 -0
- package/dist/protocol/crypto.js +739 -0
- package/dist/protocol/crypto.js.map +1 -0
- package/dist/protocol/index.d.ts +30 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +116 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/utils.d.ts +5 -0
- package/dist/protocol/utils.d.ts.map +1 -0
- package/dist/protocol/utils.js +27 -0
- package/dist/protocol/utils.js.map +1 -0
- package/dist/salones/manager.d.ts +100 -0
- package/dist/salones/manager.d.ts.map +1 -0
- package/dist/salones/manager.js +240 -0
- package/dist/salones/manager.js.map +1 -0
- package/dist/security/rate-limiter.d.ts +13 -0
- package/dist/security/rate-limiter.d.ts.map +1 -0
- package/dist/security/rate-limiter.js +32 -0
- package/dist/security/rate-limiter.js.map +1 -0
- package/dist/snapshot/index.d.ts +90 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +189 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/storage/index.d.ts +49 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +192 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/optimizer.d.ts +28 -0
- package/dist/storage/optimizer.d.ts.map +1 -0
- package/dist/storage/optimizer.js +71 -0
- package/dist/storage/optimizer.js.map +1 -0
- package/dist/storage/ydoc-persistence.d.ts +27 -0
- package/dist/storage/ydoc-persistence.d.ts.map +1 -0
- package/dist/storage/ydoc-persistence.js +128 -0
- package/dist/storage/ydoc-persistence.js.map +1 -0
- package/dist/sync/ChunkSyncManager.d.ts +41 -0
- package/dist/sync/ChunkSyncManager.d.ts.map +1 -0
- package/dist/sync/ChunkSyncManager.js +115 -0
- package/dist/sync/ChunkSyncManager.js.map +1 -0
- package/dist/sync/conflict-resolver.d.ts +33 -0
- package/dist/sync/conflict-resolver.d.ts.map +1 -0
- package/dist/sync/conflict-resolver.js +54 -0
- package/dist/sync/conflict-resolver.js.map +1 -0
- package/dist/sync/engine.d.ts +168 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +315 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/transport/TorTransportAdapter.d.ts +70 -0
- package/dist/transport/TorTransportAdapter.d.ts.map +1 -0
- package/dist/transport/TorTransportAdapter.js +192 -0
- package/dist/transport/TorTransportAdapter.js.map +1 -0
- package/dist/transport/memory.d.ts +31 -0
- package/dist/transport/memory.d.ts.map +1 -0
- package/dist/transport/memory.js +114 -0
- package/dist/transport/memory.js.map +1 -0
- package/dist/transport/peerjs.d.ts +52 -0
- package/dist/transport/peerjs.d.ts.map +1 -0
- package/dist/transport/peerjs.js +336 -0
- package/dist/transport/peerjs.js.map +1 -0
- package/dist/transport/pqc-handshake.d.ts +72 -0
- package/dist/transport/pqc-handshake.d.ts.map +1 -0
- package/dist/transport/pqc-handshake.js +129 -0
- package/dist/transport/pqc-handshake.js.map +1 -0
- package/dist/transport/relay-config.d.ts +11 -0
- package/dist/transport/relay-config.d.ts.map +1 -0
- package/dist/transport/relay-config.js +48 -0
- package/dist/transport/relay-config.js.map +1 -0
- package/dist/transport/relay-server.d.ts +37 -0
- package/dist/transport/relay-server.d.ts.map +1 -0
- package/dist/transport/relay-server.js +246 -0
- package/dist/transport/relay-server.js.map +1 -0
- package/dist/transport/tor.d.ts +47 -0
- package/dist/transport/tor.d.ts.map +1 -0
- package/dist/transport/tor.js +158 -0
- package/dist/transport/tor.js.map +1 -0
- package/dist/transport/types.d.ts +34 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +2 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/types/index.d.ts +293 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +73 -0
- package/dist/types/index.js.map +1 -0
- package/dist/web/admission.d.ts +24 -0
- package/dist/web/admission.d.ts.map +1 -0
- package/dist/web/admission.js +41 -0
- package/dist/web/admission.js.map +1 -0
- package/dist/web/crypto.d.ts +18 -0
- package/dist/web/crypto.d.ts.map +1 -0
- package/dist/web/crypto.js +78 -0
- package/dist/web/crypto.js.map +1 -0
- package/dist/web/exchange.d.ts +23 -0
- package/dist/web/exchange.d.ts.map +1 -0
- package/dist/web/exchange.js +54 -0
- package/dist/web/exchange.js.map +1 -0
- package/dist/web/fragment.d.ts +58 -0
- package/dist/web/fragment.d.ts.map +1 -0
- package/dist/web/fragment.js +198 -0
- package/dist/web/fragment.js.map +1 -0
- package/dist/web/index.d.ts +25 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +14 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/merge/index.d.ts +104 -0
- package/dist/web/merge/index.d.ts.map +1 -0
- package/dist/web/merge/index.js +236 -0
- package/dist/web/merge/index.js.map +1 -0
- package/dist/web/oplog/hlc.d.ts +19 -0
- package/dist/web/oplog/hlc.d.ts.map +1 -0
- package/dist/web/oplog/hlc.js +39 -0
- package/dist/web/oplog/hlc.js.map +1 -0
- package/dist/web/oplog/index.d.ts +10 -0
- package/dist/web/oplog/index.d.ts.map +1 -0
- package/dist/web/oplog/index.js +6 -0
- package/dist/web/oplog/index.js.map +1 -0
- package/dist/web/oplog/log.d.ts +182 -0
- package/dist/web/oplog/log.d.ts.map +1 -0
- package/dist/web/oplog/log.js +693 -0
- package/dist/web/oplog/log.js.map +1 -0
- package/dist/web/oplog/store.d.ts +40 -0
- package/dist/web/oplog/store.d.ts.map +1 -0
- package/dist/web/oplog/store.js +85 -0
- package/dist/web/oplog/store.js.map +1 -0
- package/dist/web/oplog/sync.d.ts +54 -0
- package/dist/web/oplog/sync.d.ts.map +1 -0
- package/dist/web/oplog/sync.js +162 -0
- package/dist/web/oplog/sync.js.map +1 -0
- package/dist/web/oplog/types.d.ts +119 -0
- package/dist/web/oplog/types.d.ts.map +1 -0
- package/dist/web/oplog/types.js +2 -0
- package/dist/web/oplog/types.js.map +1 -0
- package/dist/web/pairing.d.ts +186 -0
- package/dist/web/pairing.d.ts.map +1 -0
- package/dist/web/pairing.js +395 -0
- package/dist/web/pairing.js.map +1 -0
- package/dist/web/pq.d.ts +36 -0
- package/dist/web/pq.d.ts.map +1 -0
- package/dist/web/pq.js +59 -0
- package/dist/web/pq.js.map +1 -0
- package/dist/web/provider.d.ts +188 -0
- package/dist/web/provider.d.ts.map +1 -0
- package/dist/web/provider.js +2042 -0
- package/dist/web/provider.js.map +1 -0
- package/dist/web/rooms.d.ts +20 -0
- package/dist/web/rooms.d.ts.map +1 -0
- package/dist/web/rooms.js +51 -0
- package/dist/web/rooms.js.map +1 -0
- package/dist/web/rotation.d.ts +73 -0
- package/dist/web/rotation.d.ts.map +1 -0
- package/dist/web/rotation.js +130 -0
- package/dist/web/rotation.js.map +1 -0
- package/dist/web/secstate.d.ts +213 -0
- package/dist/web/secstate.d.ts.map +1 -0
- package/dist/web/secstate.js +789 -0
- package/dist/web/secstate.js.map +1 -0
- package/dist/web/store.d.ts +9 -0
- package/dist/web/store.d.ts.map +1 -0
- package/dist/web/store.js +32 -0
- package/dist/web/store.js.map +1 -0
- package/dist/web/transports/loopback.d.ts +11 -0
- package/dist/web/transports/loopback.d.ts.map +1 -0
- package/dist/web/transports/loopback.js +97 -0
- package/dist/web/transports/loopback.js.map +1 -0
- package/dist/web/transports/qr-sdp.d.ts +27 -0
- package/dist/web/transports/qr-sdp.d.ts.map +1 -0
- package/dist/web/transports/qr-sdp.js +131 -0
- package/dist/web/transports/qr-sdp.js.map +1 -0
- package/dist/web/transports/ws.d.ts +12 -0
- package/dist/web/transports/ws.d.ts.map +1 -0
- package/dist/web/transports/ws.js +139 -0
- package/dist/web/transports/ws.js.map +1 -0
- package/dist/web/trust/canonical.d.ts +8 -0
- package/dist/web/trust/canonical.d.ts.map +1 -0
- package/dist/web/trust/canonical.js +27 -0
- package/dist/web/trust/canonical.js.map +1 -0
- package/dist/web/trust/docs.d.ts +51 -0
- package/dist/web/trust/docs.d.ts.map +1 -0
- package/dist/web/trust/docs.js +170 -0
- package/dist/web/trust/docs.js.map +1 -0
- package/dist/web/trust/index.d.ts +10 -0
- package/dist/web/trust/index.d.ts.map +1 -0
- package/dist/web/trust/index.js +7 -0
- package/dist/web/trust/index.js.map +1 -0
- package/dist/web/trust/keys.d.ts +37 -0
- package/dist/web/trust/keys.d.ts.map +1 -0
- package/dist/web/trust/keys.js +75 -0
- package/dist/web/trust/keys.js.map +1 -0
- package/dist/web/trust/store.d.ts +150 -0
- package/dist/web/trust/store.d.ts.map +1 -0
- package/dist/web/trust/store.js +587 -0
- package/dist/web/trust/store.js.map +1 -0
- package/dist/web/trust/types.d.ts +134 -0
- package/dist/web/trust/types.d.ts.map +1 -0
- package/dist/web/trust/types.js +4 -0
- package/dist/web/trust/types.js.map +1 -0
- package/dist/web/types.d.ts +115 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/dist/web/util.d.ts +12 -0
- package/dist/web/util.d.ts.map +1 -0
- package/dist/web/util.js +54 -0
- package/dist/web/util.js.map +1 -0
- package/dist/web/webrtc.d.ts +35 -0
- package/dist/web/webrtc.d.ts.map +1 -0
- package/dist/web/webrtc.js +304 -0
- package/dist/web/webrtc.js.map +1 -0
- package/package.json +139 -4
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
// ─── MESH MANAGER ESCALABLE ─────────────────────────────────────────────
|
|
2
|
+
// Mesh manager optimizado para escalar a 50+ peers simultáneos.
|
|
3
|
+
// No flood broadcast: usa gossip protocol + fan-out limitado.
|
|
4
|
+
// Namespace-aware routing: solo envía updates a peers en el mismo salón.
|
|
5
|
+
//
|
|
6
|
+
// Estrategia: cada peer mantiene un subconjunto aleatorio de conexiones
|
|
7
|
+
// y propaga mensajes via gossip con fan-out controlado (default 3).
|
|
8
|
+
// Los heartbeats mantienen el mesh vivo. La detección de peers nuevos
|
|
9
|
+
// se hace via el broker PeerJS.
|
|
10
|
+
import { createEnvelope } from "../protocol/index.js";
|
|
11
|
+
import { generarNonce } from "../protocol/utils.js";
|
|
12
|
+
import { TokenBucketRateLimiter } from "../security/rate-limiter.js";
|
|
13
|
+
import { TIPO_MENSAJE } from "../types/index.js";
|
|
14
|
+
// ─── CONST OBJECT PATTERNS ────────────────────────────────────────────────
|
|
15
|
+
export const ESTRATEGIA_FAN_OUT = {
|
|
16
|
+
ALEATORIA: "aleatoria",
|
|
17
|
+
POR_SALUD: "por_salud",
|
|
18
|
+
POR_LATENCIA: "por_latencia",
|
|
19
|
+
};
|
|
20
|
+
// ─── CONSTANTS ─────────────────────────────────────────────────────────────
|
|
21
|
+
const FAN_OUT_POR_DEFECTO = 3;
|
|
22
|
+
const MAX_PEERS_POR_NODO = 12;
|
|
23
|
+
const HEARTBEAT_MESH_MS = 3_000;
|
|
24
|
+
const TIMEOUT_PEER_MS = 12_000;
|
|
25
|
+
const GOSSIP_TTL_POR_DEFECTO = 5;
|
|
26
|
+
const MAX_RECONEXIONES = 3;
|
|
27
|
+
const INTERVALO_LIMPIEZA_MS = 30_000;
|
|
28
|
+
/**
|
|
29
|
+
* Cuántos registros de divergencia retiene {@link MeshManager.obtenerDivergencias}.
|
|
30
|
+
*
|
|
31
|
+
* Acotado por la misma razón que `gossipsVistos`: esto es una cola de
|
|
32
|
+
* diagnóstico, no un libro mayor, y un mapa sin límite alimentado por pares
|
|
33
|
+
* remotos es un bug de crecimiento de memoria. La reconciliación, cuando llegue,
|
|
34
|
+
* será dueña del estado durable — no esta cola.
|
|
35
|
+
*/
|
|
36
|
+
export const MAX_DIVERGENCIAS_REGISTRADAS = 256;
|
|
37
|
+
/**
|
|
38
|
+
* Acepta el consumidor de karma directamente o una bolsa de opciones con nombre,
|
|
39
|
+
* así el emparejamiento común sigue siendo una llamada de dos argumentos y
|
|
40
|
+
* añadir una segunda opción más adelante no es un cambio incompatible.
|
|
41
|
+
*/
|
|
42
|
+
function resolverConsumidorKarma(arg) {
|
|
43
|
+
if (arg === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
if ("aplicarTransaccion" in arg)
|
|
46
|
+
return arg;
|
|
47
|
+
return arg.consumidorKarma;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Payload que MeshManager gossipea para llevar un aval. Versionado en el cable
|
|
51
|
+
* para que un cambio futuro en el formato de transacción sea distinguible.
|
|
52
|
+
*/
|
|
53
|
+
export const PAYLOAD_KARMA_TIPO = "karma:tx";
|
|
54
|
+
/**
|
|
55
|
+
* Rehidrat y valida estructuralmente un aval que llegó por el cable.
|
|
56
|
+
*
|
|
57
|
+
* Comprueba SOLO la forma necesaria para sobrevivir el transporte (las firmas
|
|
58
|
+
* Uint8Array vuelven como ArrayBuffer o como arrays de números en la práctica) y
|
|
59
|
+
* para handing algo bien tipado a KarmaManager.
|
|
60
|
+
*
|
|
61
|
+
* NO es una comprobación de seguridad. Nada de esto se confía: no inspecciona la
|
|
62
|
+
* firma ni la semántica de `emisor`, `sujeto` o `delta`. Esos veredictos
|
|
63
|
+
* pertenecen a KarmaManager.aplicarTransaccion, el único camino que puede tomar
|
|
64
|
+
* una transacción remota.
|
|
65
|
+
*/
|
|
66
|
+
function normalizarTransaccionKarma(valor) {
|
|
67
|
+
if (typeof valor !== "object" || valor === null)
|
|
68
|
+
return null;
|
|
69
|
+
const p = valor;
|
|
70
|
+
if (typeof p.id !== "string" || p.id.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof p.emisor !== "string" || p.emisor.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof p.sujeto !== "string" || p.sujeto.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
// Solo "es un numero". El rango y el signo son reglas de KarmaManager.
|
|
77
|
+
if (typeof p.delta !== "number" || !Number.isFinite(p.delta))
|
|
78
|
+
return null;
|
|
79
|
+
const firma = normalizarFirma(p.firma);
|
|
80
|
+
if (firma === null)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
id: p.id,
|
|
84
|
+
tipo: typeof p.tipo === "string" ? p.tipo : "",
|
|
85
|
+
proyecto: typeof p.proyecto === "string" ? p.proyecto : "",
|
|
86
|
+
sujeto: p.sujeto,
|
|
87
|
+
delta: p.delta,
|
|
88
|
+
razon: typeof p.razon === "string" ? p.razon : "",
|
|
89
|
+
emisor: p.emisor,
|
|
90
|
+
timestamp: typeof p.timestamp === "number" && Number.isFinite(p.timestamp)
|
|
91
|
+
? p.timestamp
|
|
92
|
+
: 0,
|
|
93
|
+
firma,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Rehidrata una firma que llegó como ArrayBuffer o como array de números
|
|
98
|
+
* (round-trip JSON) al Uint8Array que espera KarmaManager.verify. Un transporte
|
|
99
|
+
* que la estropeó más allá de lo reconocible da null y se rechaza.
|
|
100
|
+
*/
|
|
101
|
+
function normalizarFirma(firma) {
|
|
102
|
+
if (firma instanceof Uint8Array)
|
|
103
|
+
return firma.length > 0 ? firma : null;
|
|
104
|
+
if (firma instanceof ArrayBuffer)
|
|
105
|
+
return firma.byteLength > 0 ? new Uint8Array(firma) : null;
|
|
106
|
+
if (Array.isArray(firma)) {
|
|
107
|
+
if (firma.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
if (!firma.every((b) => typeof b === "number" && Number.isInteger(b) && b >= 0 && b <= 255))
|
|
110
|
+
return null;
|
|
111
|
+
return new Uint8Array(firma);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// ─── MESH MANAGER ─────────────────────────────────────────────────────────
|
|
116
|
+
export class MeshManager extends EventTarget {
|
|
117
|
+
config;
|
|
118
|
+
edgeMesh;
|
|
119
|
+
peers;
|
|
120
|
+
gossipsVistos;
|
|
121
|
+
gossipRateLimiter = new TokenBucketRateLimiter({
|
|
122
|
+
tokensPerInterval: 100,
|
|
123
|
+
intervalMs: 1000,
|
|
124
|
+
maxTokens: 200,
|
|
125
|
+
});
|
|
126
|
+
namespacePeers;
|
|
127
|
+
/**
|
|
128
|
+
* Motor de karma al que replicar. Ausente significa que este MeshManager es un
|
|
129
|
+
* portador puro (solo chat/sync) y que los avales remotos se ignoran en vez de
|
|
130
|
+
* validarse con un doble.
|
|
131
|
+
*/
|
|
132
|
+
consumidorKarma;
|
|
133
|
+
/** Aplicaciones de karma en vuelo, para poder esperar la propagacion sin dormir. */
|
|
134
|
+
karmaEnCurso = new Set();
|
|
135
|
+
/**
|
|
136
|
+
* Cola de diagnostico de divergencias de reputacion observadas, de la mas
|
|
137
|
+
* antigua a la mas reciente, acotada por {@link MAX_DIVERGENCIAS_REGISTRADAS}.
|
|
138
|
+
*
|
|
139
|
+
* Acotada y SOLO EN MEMORIA, a proposito. Esto no es un libro mayor y no debe
|
|
140
|
+
* convertirse en uno: escribir registros de divergencia en el OpLog seria el
|
|
141
|
+
* primer paso de la reconciliación, que es explicitamente una fase posterior.
|
|
142
|
+
* Perder esta cola al reiniciar es el coste aceptado de "hacerlo visible ahora,
|
|
143
|
+
* reconciliar despues".
|
|
144
|
+
*/
|
|
145
|
+
divergencias = [];
|
|
146
|
+
/**
|
|
147
|
+
* Serializa las aplicaciones de karma para que la lectura de score antes/despues
|
|
148
|
+
* de cada una abarque exactamente su propia aplicacion. Ver
|
|
149
|
+
* {@link MeshManager.encolarAplicacionKarma} para por qué importa.
|
|
150
|
+
*
|
|
151
|
+
* Cadena resuelta, nunca rechazada: cada eslabon captura su propio fallo, asi
|
|
152
|
+
* que un aval que lance no puede bloquear a todos los que tiene detrás.
|
|
153
|
+
*/
|
|
154
|
+
colaKarma = Promise.resolve();
|
|
155
|
+
activo = false;
|
|
156
|
+
intervalos = {};
|
|
157
|
+
constructor(config, edgeMesh, consumidorOrOpciones) {
|
|
158
|
+
super();
|
|
159
|
+
this.edgeMesh = edgeMesh;
|
|
160
|
+
this.peers = new Map();
|
|
161
|
+
this.gossipsVistos = new Set();
|
|
162
|
+
this.namespacePeers = new Map();
|
|
163
|
+
this.consumidorKarma = resolverConsumidorKarma(consumidorOrOpciones);
|
|
164
|
+
this.config = {
|
|
165
|
+
nodoId: config.nodoId,
|
|
166
|
+
fanOut: config.fanOut ?? FAN_OUT_POR_DEFECTO,
|
|
167
|
+
maxPeers: config.maxPeers ?? MAX_PEERS_POR_NODO,
|
|
168
|
+
heartbeatIntervalMs: config.heartbeatIntervalMs ?? HEARTBEAT_MESH_MS,
|
|
169
|
+
peerTimeoutMs: config.peerTimeoutMs ?? TIMEOUT_PEER_MS,
|
|
170
|
+
gossipTTL: config.gossipTTL ?? GOSSIP_TTL_POR_DEFECTO,
|
|
171
|
+
estrategia: config.estrategia ?? ESTRATEGIA_FAN_OUT.ALEATORIA,
|
|
172
|
+
namespacePorDefecto: config.namespacePorDefecto ?? "global",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// ─── CICLO DE VIDA ───────────────────────────────────────────────────
|
|
176
|
+
async iniciar() {
|
|
177
|
+
if (this.activo)
|
|
178
|
+
return;
|
|
179
|
+
this.activo = true;
|
|
180
|
+
// Heartbeat periódico
|
|
181
|
+
this.intervalos.heartbeat = setInterval(() => {
|
|
182
|
+
void this.transmitirHeartbeat();
|
|
183
|
+
}, this.config.heartbeatIntervalMs);
|
|
184
|
+
// Limpieza periódica de peers caídos y gossip cache
|
|
185
|
+
this.intervalos.limpieza = setInterval(() => {
|
|
186
|
+
this.limpiarPeersCaidos();
|
|
187
|
+
this.limpiarGossipCache();
|
|
188
|
+
}, INTERVALO_LIMPIEZA_MS);
|
|
189
|
+
// Escuchar eventos del edge mesh para actualizar peers
|
|
190
|
+
this.edgeMesh.on("nodoConectado", (ev) => {
|
|
191
|
+
void this.conectarPeer(ev.detail.nodoId);
|
|
192
|
+
});
|
|
193
|
+
this.edgeMesh.on("nodoDesconectado", (ev) => {
|
|
194
|
+
void this.desconectarPeer(ev.detail.nodoId);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async detener() {
|
|
198
|
+
this.activo = false;
|
|
199
|
+
if (this.intervalos.heartbeat !== undefined) {
|
|
200
|
+
clearInterval(this.intervalos.heartbeat);
|
|
201
|
+
}
|
|
202
|
+
if (this.intervalos.limpieza !== undefined) {
|
|
203
|
+
clearInterval(this.intervalos.limpieza);
|
|
204
|
+
}
|
|
205
|
+
this.peers.clear();
|
|
206
|
+
this.gossipsVistos.clear();
|
|
207
|
+
this.namespacePeers.clear();
|
|
208
|
+
// Las divergencias tambien se van. `detener` devuelve la malla a una hoja
|
|
209
|
+
// limpia, y la cola describe una sesion de trafico: conservar registros
|
|
210
|
+
// viejos junto a un nodo reiniciado haria que un operador los leyera como
|
|
211
|
+
// estado actual.
|
|
212
|
+
this.divergencias.length = 0;
|
|
213
|
+
}
|
|
214
|
+
// ─── GESTION DE PEERS ────────────────────────────────────────────────
|
|
215
|
+
async conectarPeer(peerId, namespace) {
|
|
216
|
+
if (peerId === this.config.nodoId)
|
|
217
|
+
return;
|
|
218
|
+
if (!this.activo)
|
|
219
|
+
return;
|
|
220
|
+
const existente = this.peers.get(peerId);
|
|
221
|
+
if (existente !== undefined) {
|
|
222
|
+
// Actualizar estado
|
|
223
|
+
this.peers.set(peerId, {
|
|
224
|
+
...existente,
|
|
225
|
+
estado: "activo",
|
|
226
|
+
ultimoHeartbeat: Date.now(),
|
|
227
|
+
namespace: namespace ?? existente.namespace,
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Verificar límite de peers
|
|
232
|
+
if (this.peers.size >= this.config.maxPeers) {
|
|
233
|
+
// Reemplazar el peer más inactivo
|
|
234
|
+
const peorPeer = this.encontrarPeorPeer();
|
|
235
|
+
if (peorPeer !== null) {
|
|
236
|
+
this.peers.delete(peorPeer);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
return; // No se puede conectar más peers
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const peerInfo = {
|
|
243
|
+
nodoId: peerId,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
ultimoHeartbeat: Date.now(),
|
|
246
|
+
latenciaMs: 0,
|
|
247
|
+
fanOutIndex: Math.floor(Math.random() * this.config.fanOut),
|
|
248
|
+
estado: "activo",
|
|
249
|
+
intentosReconexion: 0,
|
|
250
|
+
namespace,
|
|
251
|
+
};
|
|
252
|
+
this.peers.set(peerId, peerInfo);
|
|
253
|
+
// Registrar en el namespace correspondiente
|
|
254
|
+
const ns = namespace ?? this.config.namespacePorDefecto;
|
|
255
|
+
this.agregarPeerANamespace(peerId, ns);
|
|
256
|
+
this.dispatchEvent(new CustomEvent("peerConectado", {
|
|
257
|
+
detail: { peerId, namespace: ns },
|
|
258
|
+
}));
|
|
259
|
+
this.emitMeshSalud();
|
|
260
|
+
}
|
|
261
|
+
async desconectarPeer(peerId) {
|
|
262
|
+
this.peers.delete(peerId);
|
|
263
|
+
// Remover de todos los namespaces
|
|
264
|
+
for (const [, peers] of this.namespacePeers) {
|
|
265
|
+
peers.delete(peerId);
|
|
266
|
+
}
|
|
267
|
+
this.dispatchEvent(new CustomEvent("peerDesconectado", { detail: { peerId } }));
|
|
268
|
+
this.emitMeshSalud();
|
|
269
|
+
}
|
|
270
|
+
encontrarPeorPeer() {
|
|
271
|
+
let peorId = null;
|
|
272
|
+
let peorLatencia = -1;
|
|
273
|
+
for (const [id, info] of this.peers) {
|
|
274
|
+
if (info.estado === "caido" || info.latenciaMs > peorLatencia) {
|
|
275
|
+
peorId = id;
|
|
276
|
+
peorLatencia = info.latenciaMs;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return peorId;
|
|
280
|
+
}
|
|
281
|
+
// ─── NAMESPACE-AWARE ROUTING ─────────────────────────────────────────
|
|
282
|
+
agregarPeerANamespace(peerId, namespace) {
|
|
283
|
+
let peers = this.namespacePeers.get(namespace);
|
|
284
|
+
if (peers === undefined) {
|
|
285
|
+
peers = new Set();
|
|
286
|
+
this.namespacePeers.set(namespace, peers);
|
|
287
|
+
}
|
|
288
|
+
peers.add(peerId);
|
|
289
|
+
}
|
|
290
|
+
async unirANamespace(namespace, peerId) {
|
|
291
|
+
const targetPeer = peerId ?? this.config.nodoId;
|
|
292
|
+
this.agregarPeerANamespace(targetPeer, namespace);
|
|
293
|
+
// Notificar al mesh completo del namespace change via broadcast
|
|
294
|
+
// que será limitado por fan-out
|
|
295
|
+
}
|
|
296
|
+
async abandonarNamespace(namespace, peerId) {
|
|
297
|
+
const targetPeer = peerId ?? this.config.nodoId;
|
|
298
|
+
const peers = this.namespacePeers.get(namespace);
|
|
299
|
+
if (peers !== undefined) {
|
|
300
|
+
peers.delete(targetPeer);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
obtenerPeersEnNamespace(namespace) {
|
|
304
|
+
const peers = this.namespacePeers.get(namespace);
|
|
305
|
+
if (peers === undefined)
|
|
306
|
+
return [];
|
|
307
|
+
return Array.from(peers).filter((p) => this.peers.has(p));
|
|
308
|
+
}
|
|
309
|
+
// ─── GOSSIP PROTOCOL ─────────────────────────────────────────────────
|
|
310
|
+
async transmitirConGossip(namespace, payload, fanOut) {
|
|
311
|
+
if (!this.activo)
|
|
312
|
+
return;
|
|
313
|
+
const mensaje = {
|
|
314
|
+
id: generarNonce(),
|
|
315
|
+
namespace,
|
|
316
|
+
ttl: this.config.gossipTTL,
|
|
317
|
+
payload,
|
|
318
|
+
origen: this.config.nodoId,
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
ruta: [this.config.nodoId],
|
|
321
|
+
};
|
|
322
|
+
// Marcar como visto para no re-procesar
|
|
323
|
+
this.gossipsVistos.add(mensaje.id);
|
|
324
|
+
// Seleccionar peers según estrategia
|
|
325
|
+
const peersEnNamespace = this.obtenerPeersEnNamespace(namespace);
|
|
326
|
+
const peersObjetivo = this.seleccionarPeersParaFanOut(peersEnNamespace, fanOut ?? this.config.fanOut, []);
|
|
327
|
+
// Propagar a peers seleccionados
|
|
328
|
+
const promesas = peersObjetivo.map(async (peerId) => {
|
|
329
|
+
try {
|
|
330
|
+
const env = createEnvelope(TIPO_MENSAJE.GOVERNANCE, this.config.nodoId, peerId, { tipo: "gossip", mensaje });
|
|
331
|
+
await this.edgeMesh.enviar(peerId, env);
|
|
332
|
+
this.actualizarLatencia(peerId);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Peer puede estar caído, marcar
|
|
336
|
+
this.marcarPeerCaido(peerId);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
await Promise.allSettled(promesas);
|
|
340
|
+
}
|
|
341
|
+
seleccionarPeersParaFanOut(candidatos, fanOut, excluir) {
|
|
342
|
+
const disponibles = candidatos.filter((p) => p !== this.config.nodoId &&
|
|
343
|
+
!excluir.includes(p) &&
|
|
344
|
+
this.peers.get(p)?.estado === "activo");
|
|
345
|
+
if (disponibles.length <= fanOut)
|
|
346
|
+
return disponibles;
|
|
347
|
+
switch (this.config.estrategia) {
|
|
348
|
+
case ESTRATEGIA_FAN_OUT.ALEATORIA: {
|
|
349
|
+
return this.seleccionAleatoria(disponibles, fanOut);
|
|
350
|
+
}
|
|
351
|
+
case ESTRATEGIA_FAN_OUT.POR_SALUD: {
|
|
352
|
+
return this.seleccionPorSalud(disponibles, fanOut);
|
|
353
|
+
}
|
|
354
|
+
case ESTRATEGIA_FAN_OUT.POR_LATENCIA: {
|
|
355
|
+
return this.seleccionPorLatencia(disponibles, fanOut);
|
|
356
|
+
}
|
|
357
|
+
default: {
|
|
358
|
+
return this.seleccionAleatoria(disponibles, fanOut);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
seleccionAleatoria(peers, count) {
|
|
363
|
+
const shuffled = [...peers].sort(() => Math.random() - 0.5);
|
|
364
|
+
return shuffled.slice(0, count);
|
|
365
|
+
}
|
|
366
|
+
seleccionPorSalud(peers, count) {
|
|
367
|
+
const ordenados = [...peers].sort((a, b) => {
|
|
368
|
+
const pa = this.peers.get(a);
|
|
369
|
+
const pb = this.peers.get(b);
|
|
370
|
+
if (pa === undefined && pb === undefined)
|
|
371
|
+
return 0;
|
|
372
|
+
if (pa === undefined)
|
|
373
|
+
return -1;
|
|
374
|
+
if (pb === undefined)
|
|
375
|
+
return 1;
|
|
376
|
+
return (pa.latenciaMs ?? Infinity) - (pb.latenciaMs ?? Infinity);
|
|
377
|
+
});
|
|
378
|
+
return ordenados.slice(0, count);
|
|
379
|
+
}
|
|
380
|
+
seleccionPorLatencia(peers, count) {
|
|
381
|
+
return this.seleccionPorSalud(peers, count);
|
|
382
|
+
}
|
|
383
|
+
// ─── PROCESAR GOSSIP ─────────────────────────────────────────────────
|
|
384
|
+
/**
|
|
385
|
+
* EL CAMINO DE REPLICACIÓN DE LA REPUTACIÓN.
|
|
386
|
+
*
|
|
387
|
+
* ─── POR QUÉ EXISTE ESTE GANCHO ────────────────────────────────────────
|
|
388
|
+
*
|
|
389
|
+
* Hasta ahora `KarmaManager` escribía los avales en el OpLog LOCAL
|
|
390
|
+
* (`karma:emit`) y ahí terminaba, mientras que `procesarGossip` — lo único en
|
|
391
|
+
* el repo que podía haberlos llevado a un par — nunca lo invocaba nadie. Dos
|
|
392
|
+
* observadores honestos del mismo aval sostenían por tanto scores distintos.
|
|
393
|
+
*
|
|
394
|
+
* ─── MODELO DE CONFIANZA: EL GOSSIP ES UN PORTADOR, NO UNA AUTORIDAD ────
|
|
395
|
+
*
|
|
396
|
+
* Un payload de gossip es datos controlados por un atacante. Puede ser
|
|
397
|
+
* reenviado, falsificado, re-firmado por un par coludido, o enviado por un nodo
|
|
398
|
+
* que nunca ha sido avalado por nadie. Por eso este método NUNCA escribe en un
|
|
399
|
+
* OpLog y NUNCA toca un score. Hace exactamente una cosa con el payload:
|
|
400
|
+
* entregarlo a `ConsumidorKarma.aplicarTransaccion`, que es la entrada publica
|
|
401
|
+
* del propio KarmaManager. Ese método es el único lugar donde se verifica la
|
|
402
|
+
* firma ML-DSA-65 contra la clave REGISTRADA del emisor, se rechaza la
|
|
403
|
+
* auto-emisión, se aplica el techo de delta y el reenvío del mismo `tx.id` es
|
|
404
|
+
* idempotente. Reimplementar aquí cualquiera de esas reglas crearía un segundo
|
|
405
|
+
* validador más débil, que es exactamente el bug que este camino cierra.
|
|
406
|
+
*
|
|
407
|
+
* En consecuencia, un par NUNCA puede inyectar karma en un nodo que no ha
|
|
408
|
+
* firmado un aval: sin una firma válida de una clave en la que el nodo confía, la
|
|
409
|
+
* transacción se rechaza y ningún score se mueve.
|
|
410
|
+
*
|
|
411
|
+
* La confianza en claves públicas deliberadamente NO se toma del payload de
|
|
412
|
+
* gossip (eso dejaría que cualquier par respaldara a cualquier otro). Las únicas
|
|
413
|
+
* claves que se registran desde aquí son las que EdgeMesh ya autenticó en el
|
|
414
|
+
* handshake, leídas vía `obtenerClavePublica`.
|
|
415
|
+
*
|
|
416
|
+
* ─── LIMITACIÓN CONOCIDA — MEJOR ESFUERZO, NO CONSENSO (SIN RESOLVER) ───
|
|
417
|
+
*
|
|
418
|
+
* El gossip es mejor-esfuerzo. TTL, el rate limiter de tokens, pares offline y
|
|
419
|
+
* la seleccion de fan-out significan que un aval PUEDE PERDERSE: un médico que
|
|
420
|
+
* está offline cuando se gossipea su aval nunca se entera. Esta capa convierte
|
|
421
|
+
* por tanto la reputación en una SEÑAL que se propaga oportunísticamente, NO en
|
|
422
|
+
* un libro mayor autoritativo. Dos nodos pueden sostener legítimamente scores
|
|
423
|
+
* distintos para el mismo sujeto, y ningún reintento dentro de esta capa lo
|
|
424
|
+
* arregla — es un problema de convergencia, no de transporte.
|
|
425
|
+
*
|
|
426
|
+
* ─── QUÉ HACE ESTE MÉTODO AL RESPECTO: DETECTA, NO CORRIGE ─────────────
|
|
427
|
+
*
|
|
428
|
+
* Antes de este cambio la capa aplicaba el aval y seguía, así que el score se
|
|
429
|
+
* corregía solo y en silencio y ningún operador se enteraba de que dos nodos
|
|
430
|
+
* honestos habían mostrado reputaciones distintas del mismo médico. Ahora lee
|
|
431
|
+
* el score justo antes y justo después de la aplicación y, cuando el aval lo
|
|
432
|
+
* movió, emite `karmaDivergente` nombrando al sujeto, el score desfasado, el
|
|
433
|
+
* nuevo y el aval responsable.
|
|
434
|
+
*
|
|
435
|
+
* LA CORRECCIÓN SIGUE SIENDO LA DEL MOTOR. Esta capa lee scores y los reporta;
|
|
436
|
+
* solo KarmaManager decide qué es un score. Y nada se reconcilia: sin escritura
|
|
437
|
+
* en el OpLog, sin reordenación, sin retractación, sin intentar que los pares
|
|
438
|
+
* coincidan. Hacerlos coincidir es la fase siguiente y necesita diseño de
|
|
439
|
+
* quórum/anti-entropía que no pertenece a un portador.
|
|
440
|
+
*
|
|
441
|
+
* La solución real es un protocolo de reconciliación y está DELIBERADAMENTE NO
|
|
442
|
+
* IMPLEMENTADO AQUÍ. Direcciones candidatas, ninguna elegida aún:
|
|
443
|
+
* (a) Anti-entropía de OpLog: intercambio periódico de entradas `karma:emit`
|
|
444
|
+
* indexadas por `tx.id`, aplicadas vía `aplicarTransaccion` (la
|
|
445
|
+
* idempotencia hace gratis los reenvíos), para que un nodo que regresa se
|
|
446
|
+
* ponga al día.
|
|
447
|
+
* (b) Quórum/acuerdo Bizantino sobre deltas, para que un score se acredite
|
|
448
|
+
* solo cuando una supermayoría de pares lo ha visto de forma
|
|
449
|
+
* independiente.
|
|
450
|
+
* (c) Exposición explícita del conflicto: divergencia (el conjunto de `tx.id`s)
|
|
451
|
+
* para que un operador pueda ver que dos nodos honestos discrepan en vez
|
|
452
|
+
* de confiar calladamente en el gossip que llegó primero. ← esta fase: el
|
|
453
|
+
* evento `karmaDivergente` y {@link MeshManager.obtenerDivergencias} son
|
|
454
|
+
* (c), acotado a lo que la sola llegada de gossip puede probar.
|
|
455
|
+
* Sea cual se elija, los invariantes se mantienen: el karma nunca otorga acceso,
|
|
456
|
+
* y solo un aval firmado puede mover un score. Hasta entonces, quien consuma un
|
|
457
|
+
* número de karma debe tratarlo como una pista no verificada — nunca como una
|
|
458
|
+
* credencial.
|
|
459
|
+
*
|
|
460
|
+
* @returns el motivo del rechazo, o null cuando la transacción se aplicó (o fue
|
|
461
|
+
* un duplicado ya aplicado, que KarmaManager dobla en `null` por
|
|
462
|
+
* diseño).
|
|
463
|
+
*/
|
|
464
|
+
async aplicarKarmaRemoto(tx, desde) {
|
|
465
|
+
const consumidor = this.consumidorKarma;
|
|
466
|
+
if (consumidor === undefined) {
|
|
467
|
+
// Sin consumidor conectado: la malla sigue siendo un portador tonto.
|
|
468
|
+
// Nada se aplica, así que un MeshManager usado para chat/sync no puede
|
|
469
|
+
// crecer por accidente un motor de karma.
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
// Puente de las claves de par que la malla ya autenticó en el handshake al
|
|
473
|
+
// motor de karma. Nunca una clave tomada del payload.
|
|
474
|
+
if (consumidor.registrarClavePublica !== undefined) {
|
|
475
|
+
const clave = this.obtenerClavePublicaDePeer(tx.emisor);
|
|
476
|
+
if (clave !== undefined) {
|
|
477
|
+
// Llamado como método, nunca desligado: el registro de
|
|
478
|
+
// KarmaManager vive en su propia instancia y una referencia suelta
|
|
479
|
+
// perdería `this` y lanzaría.
|
|
480
|
+
consumidor.registrarClavePublica(tx.emisor, clave);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Lee el score que vamos a mover, ANTES de la aplicación. Un throw aquí no
|
|
484
|
+
// debe saltarse el aval: una capa de detección que puede vetar reputación es
|
|
485
|
+
// exactamente la autoridad que este archivo se niega a ser. Así que la
|
|
486
|
+
// lectura va aislada y su ausencia solo cuesta visibilidad, nunca el aval.
|
|
487
|
+
const scoreAntes = this.leerScoreSiPuede(consumidor, tx.sujeto);
|
|
488
|
+
const motivo = await consumidor.aplicarTransaccion(tx);
|
|
489
|
+
if (motivo !== null) {
|
|
490
|
+
this.dispatchEvent(new CustomEvent("karmaRechazado", {
|
|
491
|
+
detail: { txId: tx.id, motivo, desde },
|
|
492
|
+
}));
|
|
493
|
+
return motivo;
|
|
494
|
+
}
|
|
495
|
+
this.dispatchEvent(new CustomEvent("karmaRecibido", { detail: { tx, desde } }));
|
|
496
|
+
// ─── DETECCIÓN DE DIVERGENCIA ───────────────────────────────────
|
|
497
|
+
//
|
|
498
|
+
// El aval fue aceptado. Si el score del sujeto se MOVIÓ, este nodo nunca
|
|
499
|
+
// había visto ese `tx.id` — KarmaManager dobla un duplicado en `null` sin
|
|
500
|
+
// tocar el score — así que nuestra vista estaba desfasada exactamente en
|
|
501
|
+
// `delta` y la divergencia ya es un hecho, no una sospecha. Un duplicado
|
|
502
|
+
// deja el score intacto y no emite nada, que es lo que mantiene significativo
|
|
503
|
+
// el evento.
|
|
504
|
+
//
|
|
505
|
+
// Determinista por construcción, no heurístico: sin umbral, sin reloj, sin
|
|
506
|
+
// ventana. El mismo conjunto de avales en el mismo orden siempre produce los
|
|
507
|
+
// mismos eventos, y dos nodos que vieron el mismo conjunto en distinto orden
|
|
508
|
+
// son justo lo que esto reporta.
|
|
509
|
+
if (scoreAntes !== null) {
|
|
510
|
+
const scoreDespues = this.leerScoreSiPuede(consumidor, tx.sujeto);
|
|
511
|
+
if (scoreDespues !== null && scoreDespues !== scoreAntes) {
|
|
512
|
+
this.registrarDivergencia({
|
|
513
|
+
sujeto: tx.sujeto,
|
|
514
|
+
txId: tx.id,
|
|
515
|
+
emisor: tx.emisor,
|
|
516
|
+
desde,
|
|
517
|
+
delta: tx.delta,
|
|
518
|
+
scoreLocalAntes: scoreAntes,
|
|
519
|
+
scoreLocalDespues: scoreDespues,
|
|
520
|
+
motivo: "aval_tardio",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Lee un score, convirtiendo cualquier mal comportamiento del consumidor en
|
|
528
|
+
* "desconocido".
|
|
529
|
+
*
|
|
530
|
+
* Devuelve `null` — nunca un `0` fabricado — para que un lector roto sea
|
|
531
|
+
* indistinguible de uno ausente. Inventar un cero aquí haría que el informe de
|
|
532
|
+
* divergencia afirmara un movimiento que nunca ocurrió, que es peor que
|
|
533
|
+
* callarse: un operador no podría distinguir una discrepancia real de una
|
|
534
|
+
* inventada.
|
|
535
|
+
*/
|
|
536
|
+
leerScoreSiPuede(consumidor, nodoId) {
|
|
537
|
+
try {
|
|
538
|
+
const score = consumidor.getScore(nodoId);
|
|
539
|
+
return typeof score === "number" && Number.isFinite(score) ? score : null;
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Registra una divergencia y la anuncia.
|
|
547
|
+
*
|
|
548
|
+
* Ambas mitades importan y sirven a lectores distintos: el evento llega a quien
|
|
549
|
+
* esté mirando la malla en vivo, mientras que
|
|
550
|
+
* {@link MeshManager.obtenerDivergencias} permite a un operador descubrir A
|
|
551
|
+
* POSTERIORI EL HECHO que dos nodos discrepaban — incluidas divergencias
|
|
552
|
+
* ocurridas mientras nadie escuchaba. Solo con un evento, la divergencia
|
|
553
|
+
* seguiría desapareciendo en silencio justo en el momento en que importaba.
|
|
554
|
+
*
|
|
555
|
+
* `karmaRecibido` se emite primero, así que un listener que reacciona a un
|
|
556
|
+
* score nuevo ya tiene el registro de divergencia garantizado.
|
|
557
|
+
*/
|
|
558
|
+
registrarDivergencia(detalle) {
|
|
559
|
+
this.divergencias.push(detalle);
|
|
560
|
+
// Descarta los más antiguos en vez de crecer sin límite. Perder las
|
|
561
|
+
// divergencias más antiguas es estrictamente mejor que un crecimiento de
|
|
562
|
+
// memoria sin límite alimentado por entrada remota; el OpLog — no esta cola
|
|
563
|
+
// — es donde vive el historial durable.
|
|
564
|
+
if (this.divergencias.length > MAX_DIVERGENCIAS_REGISTRADAS) {
|
|
565
|
+
this.divergencias.splice(0, this.divergencias.length - MAX_DIVERGENCIAS_REGISTRADAS);
|
|
566
|
+
}
|
|
567
|
+
this.dispatchEvent(new CustomEvent("karmaDivergente", { detail: detalle }));
|
|
568
|
+
}
|
|
569
|
+
/** Lee la clave pública de un par del registro de EdgeMesh (verificada en handshake). */
|
|
570
|
+
obtenerClavePublicaDePeer(nodoId) {
|
|
571
|
+
const registros = this.edgeMesh;
|
|
572
|
+
if (typeof registros.obtenerClavePublica !== "function")
|
|
573
|
+
return undefined;
|
|
574
|
+
try {
|
|
575
|
+
return registros.obtenerClavePublica(nodoId);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Conecta (o desconecta) el motor de karma al que este MeshManager replica.
|
|
583
|
+
*
|
|
584
|
+
* Existe porque el orden de construccion lo impone: `EdgeMesh` crea el
|
|
585
|
+
* MeshManager en su constructor, y `MalocaKernel` crea el KarmaManager DESPUES
|
|
586
|
+
* de `super(config)`. Sin este gancho, la malla quedaria permanentemente sin
|
|
587
|
+
* consumidor y los avales seguirian sin viajar.
|
|
588
|
+
*
|
|
589
|
+
* Pasar `undefined` devuelve la malla a portador puro: deja de aplicar avales
|
|
590
|
+
* en vez de inventar un motor propio.
|
|
591
|
+
*/
|
|
592
|
+
setConsumidorKarma(consumidor) {
|
|
593
|
+
this.consumidorKarma = consumidor;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Gossipea un aval a la malla. Quien llame le pasa el objeto transacción que
|
|
597
|
+
* devolvió `KarmaManager.emit`, firma incluida — una transacción sin firma es
|
|
598
|
+
* inútil para todo par, y KarmaManager ya garantiza que no puede producirse.
|
|
599
|
+
*/
|
|
600
|
+
async transmitirKarma(tx, namespace) {
|
|
601
|
+
await this.transmitirConGossip(namespace ?? this.config.namespacePorDefecto, {
|
|
602
|
+
tipo: PAYLOAD_KARMA_TIPO,
|
|
603
|
+
tx,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Resuelve cuando todas las transacciones de karma en vuelo se han
|
|
608
|
+
* estabilizado. Los tests y gateways lo usan en vez de dormir; la malla nunca
|
|
609
|
+
* espera la aplicación de gossip en el camino caliente.
|
|
610
|
+
*/
|
|
611
|
+
async esperarPropagacionKarma() {
|
|
612
|
+
while (this.karmaEnCurso.size > 0) {
|
|
613
|
+
await Promise.allSettled(Array.from(this.karmaEnCurso));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Extrae un aval de un payload de gossip y lo aplica por el motor de karma.
|
|
618
|
+
* Cualquier cosa que no sea reconociblemente un aval se ignora: otros tipos de
|
|
619
|
+
* payload (eventos maloca, descubrimiento de plugins) no son asunto nuestro y
|
|
620
|
+
* se dejan a los listeners de `gossipRecibido`.
|
|
621
|
+
*/
|
|
622
|
+
enrutarKarmaDeGossip(mensaje) {
|
|
623
|
+
const consumidor = this.consumidorKarma;
|
|
624
|
+
if (consumidor === undefined)
|
|
625
|
+
return;
|
|
626
|
+
const payload = mensaje.payload;
|
|
627
|
+
if (payload === null || typeof payload !== "object")
|
|
628
|
+
return;
|
|
629
|
+
if (payload.tipo !== PAYLOAD_KARMA_TIPO)
|
|
630
|
+
return;
|
|
631
|
+
const tx = normalizarTransaccionKarma(payload.tx);
|
|
632
|
+
if (tx === null) {
|
|
633
|
+
const tarea = (async () => {
|
|
634
|
+
this.dispatchEvent(new CustomEvent("karmaRechazado", {
|
|
635
|
+
detail: {
|
|
636
|
+
txId: null,
|
|
637
|
+
motivo: "payload_malformado",
|
|
638
|
+
desde: mensaje.origen,
|
|
639
|
+
},
|
|
640
|
+
}));
|
|
641
|
+
})();
|
|
642
|
+
this.karmaEnCurso.add(tarea);
|
|
643
|
+
void tarea.finally(() => this.karmaEnCurso.delete(tarea));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const desde = mensaje.ruta.length > 0
|
|
647
|
+
? mensaje.ruta[mensaje.ruta.length - 1]
|
|
648
|
+
: mensaje.origen;
|
|
649
|
+
this.encolarAplicacionKarma(tx, desde);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Aplica avales de UNO EN UNO, en orden de llegada.
|
|
653
|
+
*
|
|
654
|
+
* Por qué la cola, dado que el gossip deliberadamente no se espera en el
|
|
655
|
+
* camino caliente: `aplicarKarmaRemoto` lee el score antes de la aplicación y
|
|
656
|
+
* otra vez después, con un `await` en medio (la comprobación de firma es una
|
|
657
|
+
* operación PQC real). Sin serialización, varios gossips procesados en el mismo
|
|
658
|
+
* tick leerían el MISMO score "anterior" y cada uno aplicaría encima de él — así
|
|
659
|
+
* que el `scoreLocalAntes` registrado sería el valor de antes de todo el lote y
|
|
660
|
+
* no el valor que el nodo realmente mostraba cuando llegó ese aval. El evento
|
|
661
|
+
* seguiría disparándose, pero describiría un score que nadie vio, que es
|
|
662
|
+
* exactamente el tipo de número silenciosamente incorrecto que esta fase existe
|
|
663
|
+
* para dejar de publicar.
|
|
664
|
+
*
|
|
665
|
+
* La cola conserva el orden de llegada (`.then` sobre la tarea anterior), así
|
|
666
|
+
* que la secuencia es determinista e idéntica en cada nodo que vio los mismos
|
|
667
|
+
* gossips en el mismo orden.Tampoco cambia el resultado independiente del
|
|
668
|
+
* orden: KarmaManager suma deltas, así que el score FINAL es el mismo
|
|
669
|
+
* cualquiera — la cola hace que el reporte intermedio sea fiel, no que el
|
|
670
|
+
* total sea distinto.
|
|
671
|
+
*
|
|
672
|
+
* Los fallos no atascan la cola: cada eslabon absorbe su propio rechazo, así
|
|
673
|
+
* que un aval envenenado no bloquea a todos los que tiene detrás.
|
|
674
|
+
*/
|
|
675
|
+
encolarAplicacionKarma(tx, desde) {
|
|
676
|
+
const tarea = this.colaKarma
|
|
677
|
+
.then(() => this.aplicarKarmaRemoto(tx, desde))
|
|
678
|
+
.catch(() => null);
|
|
679
|
+
this.colaKarma = tarea;
|
|
680
|
+
this.karmaEnCurso.add(tarea);
|
|
681
|
+
void tarea.finally(() => this.karmaEnCurso.delete(tarea));
|
|
682
|
+
}
|
|
683
|
+
procesarGossip(mensaje) {
|
|
684
|
+
// Verificar mensaje y TTL
|
|
685
|
+
if (!mensaje ||
|
|
686
|
+
typeof mensaje !== "object" ||
|
|
687
|
+
typeof mensaje.ttl !== "number" ||
|
|
688
|
+
mensaje.ttl <= 0)
|
|
689
|
+
return;
|
|
690
|
+
// Verificar duplicado
|
|
691
|
+
if (this.gossipsVistos.has(mensaje.id))
|
|
692
|
+
return;
|
|
693
|
+
// Rate Limiting
|
|
694
|
+
const peerId = mensaje.ruta.length > 0
|
|
695
|
+
? mensaje.ruta[mensaje.ruta.length - 1]
|
|
696
|
+
: mensaje.origen;
|
|
697
|
+
if (!this.gossipRateLimiter.consume(peerId)) {
|
|
698
|
+
console.warn(`Rate limit exceeded for peer: ${peerId} in gossip receive`);
|
|
699
|
+
this.dispatchEvent(new CustomEvent("rate_limited", {
|
|
700
|
+
detail: { peerId, resource: "gossip" },
|
|
701
|
+
}));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// Marcar como visto
|
|
705
|
+
this.gossipsVistos.add(mensaje.id);
|
|
706
|
+
// Verificar que estamos en el namespace
|
|
707
|
+
const peersEnNs = this.namespacePeers.get(mensaje.namespace);
|
|
708
|
+
if (peersEnNs === undefined || !peersEnNs.has(this.config.nodoId)) {
|
|
709
|
+
// Si no estamos en el namespace, no propagamos
|
|
710
|
+
// Pero procesamos si el payload es relevante
|
|
711
|
+
}
|
|
712
|
+
// Replicacion de reputacion: enruta el aval por KarmaManager. Se dispara sin
|
|
713
|
+
// await para que el reenvio de gossip siga siendo rapido; los errores quedan
|
|
714
|
+
// contenidos y se hace visible como `karmaRechazado` en aplicarKarmaRemoto.
|
|
715
|
+
this.enrutarKarmaDeGossip(mensaje);
|
|
716
|
+
this.dispatchEvent(new CustomEvent("gossipRecibido", { detail: { mensaje } }));
|
|
717
|
+
// Re-propagar con TTL reducido
|
|
718
|
+
if (mensaje.ttl > 1) {
|
|
719
|
+
const mensajeReenviado = {
|
|
720
|
+
...mensaje,
|
|
721
|
+
ttl: mensaje.ttl - 1,
|
|
722
|
+
ruta: [...mensaje.ruta, this.config.nodoId],
|
|
723
|
+
};
|
|
724
|
+
const peersParaReenvio = this.seleccionarPeersParaFanOut(this.obtenerPeersEnNamespace(mensaje.namespace), this.config.fanOut, mensaje.ruta);
|
|
725
|
+
for (const peerId of peersParaReenvio) {
|
|
726
|
+
if (peerId === this.config.nodoId)
|
|
727
|
+
continue;
|
|
728
|
+
void this.reenviarGossip(peerId, mensajeReenviado);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async reenviarGossip(peerId, mensaje) {
|
|
733
|
+
try {
|
|
734
|
+
const env = createEnvelope(TIPO_MENSAJE.GOVERNANCE, this.config.nodoId, peerId, { tipo: "gossip", mensaje });
|
|
735
|
+
await this.edgeMesh.enviar(peerId, env);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Ignorar errores individuales
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// ─── HEARTBEAT ───────────────────────────────────────────────────────
|
|
742
|
+
async transmitirHeartbeat() {
|
|
743
|
+
if (!this.activo || this.peers.size === 0)
|
|
744
|
+
return;
|
|
745
|
+
const peersActivos = this.obtenerPeersActivos();
|
|
746
|
+
const fanOut = Math.min(this.config.fanOut, peersActivos.length);
|
|
747
|
+
const objetivos = this.seleccionAleatoria(peersActivos, fanOut);
|
|
748
|
+
const heartbeatPayload = {
|
|
749
|
+
nodoId: this.config.nodoId,
|
|
750
|
+
timestamp: Date.now(),
|
|
751
|
+
peersConocidos: Array.from(this.peers.keys()),
|
|
752
|
+
namespaces: Array.from(this.namespacePeers.keys()),
|
|
753
|
+
};
|
|
754
|
+
for (const peerId of objetivos) {
|
|
755
|
+
try {
|
|
756
|
+
const env = createEnvelope(TIPO_MENSAJE.HEARTBEAT, this.config.nodoId, peerId, heartbeatPayload);
|
|
757
|
+
await this.edgeMesh.enviar(peerId, env);
|
|
758
|
+
this.actualizarLatencia(peerId);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
this.marcarPeerCaido(peerId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
procesarHeartbeatPeer(peerId, peersConocidos, namespaces) {
|
|
766
|
+
const existente = this.peers.get(peerId);
|
|
767
|
+
if (existente !== undefined) {
|
|
768
|
+
this.peers.set(peerId, {
|
|
769
|
+
...existente,
|
|
770
|
+
ultimoHeartbeat: Date.now(),
|
|
771
|
+
estado: "activo",
|
|
772
|
+
intentosReconexion: 0,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// Auto-descubrimiento: conectar si hay espacio
|
|
777
|
+
if (this.peers.size < this.config.maxPeers) {
|
|
778
|
+
void this.conectarPeer(peerId);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Descubrir nuevos peers via heartbeat de otros
|
|
782
|
+
for (const conocido of peersConocidos) {
|
|
783
|
+
if (!this.peers.has(conocido) && conocido !== this.config.nodoId) {
|
|
784
|
+
this.dispatchEvent(new CustomEvent("peerDescubierto", {
|
|
785
|
+
detail: { peerId: conocido, via: peerId },
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Registrar namespaces
|
|
790
|
+
for (const ns of namespaces) {
|
|
791
|
+
this.agregarPeerANamespace(peerId, ns);
|
|
792
|
+
}
|
|
793
|
+
this.emitMeshSalud();
|
|
794
|
+
}
|
|
795
|
+
// ─── PEER DISCOVERY ──────────────────────────────────────────────────
|
|
796
|
+
async descubrirSalon(salonId) {
|
|
797
|
+
// Buscar peers que estén en el namespace del salón
|
|
798
|
+
const peersEnSalon = this.namespacePeers.get(`salon:${salonId}`);
|
|
799
|
+
if (peersEnSalon !== undefined) {
|
|
800
|
+
return Array.from(peersEnSalon);
|
|
801
|
+
}
|
|
802
|
+
// Preguntar via gossip a peers conocidos
|
|
803
|
+
const preguntaId = `discover:${salonId}:${Date.now()}`;
|
|
804
|
+
await this.transmitirConGossip(this.config.namespacePorDefecto, { tipo: "discover", salonId, preguntaId }, 3);
|
|
805
|
+
// Retornar lo que tenemos (puede estar vacío si nadie responde aún)
|
|
806
|
+
return [];
|
|
807
|
+
}
|
|
808
|
+
// ─── CONSULTAS ───────────────────────────────────────────────────────
|
|
809
|
+
obtenerPeersConectados() {
|
|
810
|
+
return Array.from(this.peers.keys());
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Divergencias que este nodo ha observado, de la más antigua a la más reciente,
|
|
814
|
+
* acotadas por {@link MAX_DIVERGENCIAS_REGISTRADAS}.
|
|
815
|
+
*
|
|
816
|
+
* La respuesta a "¿mi red está de acuerdo conmigo?" — la pregunta que un
|
|
817
|
+
* presupuesto de divergencia v1 no puede responder por sí solo, ya que todo el
|
|
818
|
+
* coste del modelo es que el score PODRÍA estar mal.
|
|
819
|
+
*
|
|
820
|
+
* Devuelve una copia: la cola es contabilidad interna y handing fuera una
|
|
821
|
+
* referencia viva dejaría que un llamador mutara el estado de detección desde
|
|
822
|
+
* fuera.
|
|
823
|
+
*
|
|
824
|
+
* Vacío significa "no se observó divergencia DESDE QUE ARRANCÓ ESTE PROCESO", no
|
|
825
|
+
* "la red está de acuerdo". No es una prueba de consenso, y nada de esto
|
|
826
|
+
* debería presentarse como tal a un clínico.
|
|
827
|
+
*/
|
|
828
|
+
obtenerDivergencias() {
|
|
829
|
+
return [...this.divergencias];
|
|
830
|
+
}
|
|
831
|
+
obtenerPeersActivos() {
|
|
832
|
+
const ahora = Date.now();
|
|
833
|
+
return Array.from(this.peers.entries())
|
|
834
|
+
.filter(([_, info]) => ahora - info.ultimoHeartbeat < this.config.peerTimeoutMs)
|
|
835
|
+
.map(([id, _]) => id);
|
|
836
|
+
}
|
|
837
|
+
obtenerPeersLentos() {
|
|
838
|
+
return Array.from(this.peers.entries())
|
|
839
|
+
.filter(([_, info]) => info.estado === "lento")
|
|
840
|
+
.map(([id, _]) => id);
|
|
841
|
+
}
|
|
842
|
+
obtenerPeerInfo(peerId) {
|
|
843
|
+
return this.peers.get(peerId) ?? null;
|
|
844
|
+
}
|
|
845
|
+
obtenerTotalPeers() {
|
|
846
|
+
return this.peers.size;
|
|
847
|
+
}
|
|
848
|
+
obtenerNamespaces() {
|
|
849
|
+
return Array.from(this.namespacePeers.keys());
|
|
850
|
+
}
|
|
851
|
+
estaActivo() {
|
|
852
|
+
return this.activo;
|
|
853
|
+
}
|
|
854
|
+
// ─── UTILIDADES INTERNAS ─────────────────────────────────────────────
|
|
855
|
+
actualizarLatencia(peerId) {
|
|
856
|
+
const peer = this.peers.get(peerId);
|
|
857
|
+
if (peer === undefined)
|
|
858
|
+
return;
|
|
859
|
+
const latencia = Date.now() - peer.ultimoHeartbeat;
|
|
860
|
+
this.peers.set(peerId, {
|
|
861
|
+
...peer,
|
|
862
|
+
latenciaMs: latencia,
|
|
863
|
+
ultimoHeartbeat: Date.now(),
|
|
864
|
+
estado: latencia > 500 ? "lento" : "activo",
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
marcarPeerCaido(peerId) {
|
|
868
|
+
const peer = this.peers.get(peerId);
|
|
869
|
+
if (peer === undefined)
|
|
870
|
+
return;
|
|
871
|
+
const nuevosIntentos = peer.intentosReconexion + 1;
|
|
872
|
+
if (nuevosIntentos >= MAX_RECONEXIONES) {
|
|
873
|
+
// Peer definitivamente caído
|
|
874
|
+
void this.desconectarPeer(peerId);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
this.peers.set(peerId, {
|
|
878
|
+
...peer,
|
|
879
|
+
estado: "caido",
|
|
880
|
+
intentosReconexion: nuevosIntentos,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
limpiarPeersCaidos() {
|
|
885
|
+
const ahora = Date.now();
|
|
886
|
+
const aEliminar = [];
|
|
887
|
+
for (const [id, info] of this.peers) {
|
|
888
|
+
if (ahora - info.ultimoHeartbeat > this.config.peerTimeoutMs &&
|
|
889
|
+
info.intentosReconexion >= MAX_RECONEXIONES) {
|
|
890
|
+
aEliminar.push(id);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
for (const id of aEliminar) {
|
|
894
|
+
void this.desconectarPeer(id);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
limpiarGossipCache() {
|
|
898
|
+
const maxCache = 10_000;
|
|
899
|
+
if (this.gossipsVistos.size > maxCache) {
|
|
900
|
+
// Limpiar solo un batch para no bloquear
|
|
901
|
+
const entries = Array.from(this.gossipsVistos);
|
|
902
|
+
const aEliminar = entries.slice(0, entries.length - maxCache);
|
|
903
|
+
for (const id of aEliminar) {
|
|
904
|
+
this.gossipsVistos.delete(id);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
emitMeshSalud() {
|
|
909
|
+
const activos = this.obtenerPeersActivos();
|
|
910
|
+
this.dispatchEvent(new CustomEvent("meshSaludActualizada", {
|
|
911
|
+
detail: {
|
|
912
|
+
peersActivos: activos.length,
|
|
913
|
+
peersTotales: this.peers.size,
|
|
914
|
+
},
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
destruir() {
|
|
918
|
+
void this.detener();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
export class MeshGossip extends MeshManager {
|
|
922
|
+
recibirGossip(_origen, mensaje) {
|
|
923
|
+
this.procesarGossip(mensaje);
|
|
924
|
+
}
|
|
925
|
+
async propagarGossip(mensaje) {
|
|
926
|
+
await this.transmitirConGossip(mensaje.namespace, mensaje.payload, mensaje.ttl);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
//# sourceMappingURL=index.js.map
|