@revealui/ai 0.1.0
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 +22 -0
- package/LICENSE.commercial +112 -0
- package/README.md +314 -0
- package/dist/a2a/card.d.ts +26 -0
- package/dist/a2a/card.d.ts.map +1 -0
- package/dist/a2a/card.js +173 -0
- package/dist/a2a/handler.d.ts +26 -0
- package/dist/a2a/handler.d.ts.map +1 -0
- package/dist/a2a/handler.js +170 -0
- package/dist/a2a/index.d.ts +10 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +9 -0
- package/dist/a2a/task-store.d.ts +42 -0
- package/dist/a2a/task-store.d.ts.map +1 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/audit/emitter.d.ts +34 -0
- package/dist/audit/emitter.d.ts.map +1 -0
- package/dist/audit/emitter.js +34 -0
- package/dist/audit/index.d.ts +44 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +48 -0
- package/dist/audit/observer.d.ts +108 -0
- package/dist/audit/observer.d.ts.map +1 -0
- package/dist/audit/observer.js +271 -0
- package/dist/audit/policy.d.ts +70 -0
- package/dist/audit/policy.d.ts.map +1 -0
- package/dist/audit/policy.js +209 -0
- package/dist/audit/store.d.ts +42 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +80 -0
- package/dist/audit/types.d.ts +169 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +80 -0
- package/dist/client/hooks/index.d.ts +22 -0
- package/dist/client/hooks/index.d.ts.map +1 -0
- package/dist/client/hooks/index.js +21 -0
- package/dist/client/hooks/useAgentContext.d.ts +30 -0
- package/dist/client/hooks/useAgentContext.d.ts.map +1 -0
- package/dist/client/hooks/useAgentContext.js +161 -0
- package/dist/client/hooks/useAgentEvents.d.ts +126 -0
- package/dist/client/hooks/useAgentEvents.d.ts.map +1 -0
- package/dist/client/hooks/useAgentEvents.js +232 -0
- package/dist/client/hooks/useAgentStream.d.ts +44 -0
- package/dist/client/hooks/useAgentStream.d.ts.map +1 -0
- package/dist/client/hooks/useAgentStream.js +101 -0
- package/dist/client/hooks/useEpisodicMemory.d.ts +25 -0
- package/dist/client/hooks/useEpisodicMemory.d.ts.map +1 -0
- package/dist/client/hooks/useEpisodicMemory.js +174 -0
- package/dist/client/hooks/useWorkingMemory.d.ts +57 -0
- package/dist/client/hooks/useWorkingMemory.d.ts.map +1 -0
- package/dist/client/hooks/useWorkingMemory.js +276 -0
- package/dist/client/index.d.ts +14 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +13 -0
- package/dist/embeddings/index.d.ts +51 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +73 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/inference/context-assembly.d.ts +27 -0
- package/dist/inference/context-assembly.d.ts.map +1 -0
- package/dist/inference/context-assembly.js +81 -0
- package/dist/inference/overflow-compressor.d.ts +17 -0
- package/dist/inference/overflow-compressor.d.ts.map +1 -0
- package/dist/inference/overflow-compressor.js +40 -0
- package/dist/inference/runRag.d.ts +35 -0
- package/dist/inference/runRag.d.ts.map +1 -0
- package/dist/inference/runRag.js +53 -0
- package/dist/ingestion/bm25.d.ts +29 -0
- package/dist/ingestion/bm25.d.ts.map +1 -0
- package/dist/ingestion/bm25.js +161 -0
- package/dist/ingestion/cms-indexer.d.ts +39 -0
- package/dist/ingestion/cms-indexer.d.ts.map +1 -0
- package/dist/ingestion/cms-indexer.js +74 -0
- package/dist/ingestion/file-parsers.d.ts +51 -0
- package/dist/ingestion/file-parsers.d.ts.map +1 -0
- package/dist/ingestion/file-parsers.js +247 -0
- package/dist/ingestion/hybrid-search.d.ts +22 -0
- package/dist/ingestion/hybrid-search.d.ts.map +1 -0
- package/dist/ingestion/hybrid-search.js +63 -0
- package/dist/ingestion/index.d.ts +9 -0
- package/dist/ingestion/index.d.ts.map +1 -0
- package/dist/ingestion/index.js +8 -0
- package/dist/ingestion/pipeline.d.ts +35 -0
- package/dist/ingestion/pipeline.d.ts.map +1 -0
- package/dist/ingestion/pipeline.js +114 -0
- package/dist/ingestion/rag-vector-service.d.ts +34 -0
- package/dist/ingestion/rag-vector-service.d.ts.map +1 -0
- package/dist/ingestion/rag-vector-service.js +98 -0
- package/dist/ingestion/reranker.d.ts +10 -0
- package/dist/ingestion/reranker.d.ts.map +1 -0
- package/dist/ingestion/reranker.js +41 -0
- package/dist/ingestion/text-splitter.d.ts +25 -0
- package/dist/ingestion/text-splitter.d.ts.map +1 -0
- package/dist/ingestion/text-splitter.js +119 -0
- package/dist/llm/cache-utils.d.ts +146 -0
- package/dist/llm/cache-utils.d.ts.map +1 -0
- package/dist/llm/cache-utils.js +204 -0
- package/dist/llm/client.d.ts +134 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +497 -0
- package/dist/llm/key-validator.d.ts +25 -0
- package/dist/llm/key-validator.d.ts.map +1 -0
- package/dist/llm/key-validator.js +101 -0
- package/dist/llm/provider-health.d.ts +40 -0
- package/dist/llm/provider-health.d.ts.map +1 -0
- package/dist/llm/provider-health.js +97 -0
- package/dist/llm/providers/anthropic.d.ts +31 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.js +248 -0
- package/dist/llm/providers/base.d.ts +111 -0
- package/dist/llm/providers/base.d.ts.map +1 -0
- package/dist/llm/providers/base.js +6 -0
- package/dist/llm/providers/groq.d.ts +23 -0
- package/dist/llm/providers/groq.d.ts.map +1 -0
- package/dist/llm/providers/groq.js +27 -0
- package/dist/llm/providers/ollama.d.ts +27 -0
- package/dist/llm/providers/ollama.d.ts.map +1 -0
- package/dist/llm/providers/ollama.js +48 -0
- package/dist/llm/providers/openai.d.ts +19 -0
- package/dist/llm/providers/openai.d.ts.map +1 -0
- package/dist/llm/providers/openai.js +245 -0
- package/dist/llm/providers/vultr.d.ts +18 -0
- package/dist/llm/providers/vultr.d.ts.map +1 -0
- package/dist/llm/providers/vultr.js +168 -0
- package/dist/llm/response-cache.d.ts +166 -0
- package/dist/llm/response-cache.d.ts.map +1 -0
- package/dist/llm/response-cache.js +233 -0
- package/dist/llm/semantic-cache.d.ts +179 -0
- package/dist/llm/semantic-cache.d.ts.map +1 -0
- package/dist/llm/semantic-cache.js +306 -0
- package/dist/llm/server.d.ts +14 -0
- package/dist/llm/server.d.ts.map +1 -0
- package/dist/llm/server.js +15 -0
- package/dist/llm/token-counter.d.ts +48 -0
- package/dist/llm/token-counter.d.ts.map +1 -0
- package/dist/llm/token-counter.js +77 -0
- package/dist/llm/workspace-provider-config.d.ts +38 -0
- package/dist/llm/workspace-provider-config.d.ts.map +1 -0
- package/dist/llm/workspace-provider-config.js +47 -0
- package/dist/memory/agent/context-manager.d.ts +148 -0
- package/dist/memory/agent/context-manager.d.ts.map +1 -0
- package/dist/memory/agent/context-manager.js +284 -0
- package/dist/memory/agent/index.d.ts +7 -0
- package/dist/memory/agent/index.d.ts.map +1 -0
- package/dist/memory/agent/index.js +6 -0
- package/dist/memory/crdt/index.d.ts +13 -0
- package/dist/memory/crdt/index.d.ts.map +1 -0
- package/dist/memory/crdt/index.js +12 -0
- package/dist/memory/crdt/lww-register.d.ts +108 -0
- package/dist/memory/crdt/lww-register.d.ts.map +1 -0
- package/dist/memory/crdt/lww-register.js +169 -0
- package/dist/memory/crdt/or-set.d.ts +141 -0
- package/dist/memory/crdt/or-set.d.ts.map +1 -0
- package/dist/memory/crdt/or-set.js +291 -0
- package/dist/memory/crdt/pn-counter.d.ts +116 -0
- package/dist/memory/crdt/pn-counter.d.ts.map +1 -0
- package/dist/memory/crdt/pn-counter.js +174 -0
- package/dist/memory/crdt/vector-clock.d.ts +115 -0
- package/dist/memory/crdt/vector-clock.d.ts.map +1 -0
- package/dist/memory/crdt/vector-clock.js +179 -0
- package/dist/memory/errors/index.d.ts +56 -0
- package/dist/memory/errors/index.d.ts.map +1 -0
- package/dist/memory/errors/index.js +85 -0
- package/dist/memory/index.d.ts +21 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +20 -0
- package/dist/memory/persistence/crdt-persistence.d.ts +85 -0
- package/dist/memory/persistence/crdt-persistence.d.ts.map +1 -0
- package/dist/memory/persistence/crdt-persistence.js +204 -0
- package/dist/memory/persistence/index.d.ts +7 -0
- package/dist/memory/persistence/index.d.ts.map +1 -0
- package/dist/memory/persistence/index.js +6 -0
- package/dist/memory/preferences/index.d.ts +7 -0
- package/dist/memory/preferences/index.d.ts.map +1 -0
- package/dist/memory/preferences/index.js +6 -0
- package/dist/memory/preferences/user-preferences-manager.d.ts +133 -0
- package/dist/memory/preferences/user-preferences-manager.d.ts.map +1 -0
- package/dist/memory/preferences/user-preferences-manager.js +342 -0
- package/dist/memory/services/index.d.ts +8 -0
- package/dist/memory/services/index.d.ts.map +1 -0
- package/dist/memory/services/index.js +6 -0
- package/dist/memory/services/node-id-service.d.ts +75 -0
- package/dist/memory/services/node-id-service.d.ts.map +1 -0
- package/dist/memory/services/node-id-service.js +190 -0
- package/dist/memory/stores/episodic-memory.d.ts +182 -0
- package/dist/memory/stores/episodic-memory.d.ts.map +1 -0
- package/dist/memory/stores/episodic-memory.js +378 -0
- package/dist/memory/stores/index.d.ts +16 -0
- package/dist/memory/stores/index.d.ts.map +1 -0
- package/dist/memory/stores/index.js +15 -0
- package/dist/memory/stores/procedural-memory.d.ts +89 -0
- package/dist/memory/stores/procedural-memory.d.ts.map +1 -0
- package/dist/memory/stores/procedural-memory.js +152 -0
- package/dist/memory/stores/semantic-memory.d.ts +92 -0
- package/dist/memory/stores/semantic-memory.d.ts.map +1 -0
- package/dist/memory/stores/semantic-memory.js +155 -0
- package/dist/memory/stores/working-memory.d.ts +225 -0
- package/dist/memory/stores/working-memory.d.ts.map +1 -0
- package/dist/memory/stores/working-memory.js +336 -0
- package/dist/memory/utils/deep-clone.d.ts +10 -0
- package/dist/memory/utils/deep-clone.d.ts.map +1 -0
- package/dist/memory/utils/deep-clone.js +9 -0
- package/dist/memory/utils/index.d.ts +8 -0
- package/dist/memory/utils/index.d.ts.map +1 -0
- package/dist/memory/utils/index.js +7 -0
- package/dist/memory/utils/logger.d.ts +21 -0
- package/dist/memory/utils/logger.d.ts.map +1 -0
- package/dist/memory/utils/logger.js +62 -0
- package/dist/memory/utils/sql-helpers.d.ts +97 -0
- package/dist/memory/utils/sql-helpers.d.ts.map +1 -0
- package/dist/memory/utils/sql-helpers.js +214 -0
- package/dist/memory/utils/validation.d.ts +62 -0
- package/dist/memory/utils/validation.d.ts.map +1 -0
- package/dist/memory/utils/validation.js +244 -0
- package/dist/memory/vector/index.d.ts +12 -0
- package/dist/memory/vector/index.d.ts.map +1 -0
- package/dist/memory/vector/index.js +14 -0
- package/dist/memory/vector/vector-memory-service.d.ts +88 -0
- package/dist/memory/vector/vector-memory-service.d.ts.map +1 -0
- package/dist/memory/vector/vector-memory-service.js +335 -0
- package/dist/observability/logger.d.ts +79 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +165 -0
- package/dist/observability/metrics.d.ts +43 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +197 -0
- package/dist/observability/query.d.ts +150 -0
- package/dist/observability/query.d.ts.map +1 -0
- package/dist/observability/query.js +339 -0
- package/dist/observability/types.d.ts +140 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +6 -0
- package/dist/orchestration/agent.d.ts +98 -0
- package/dist/orchestration/agent.d.ts.map +1 -0
- package/dist/orchestration/agent.js +6 -0
- package/dist/orchestration/defaults.d.ts +21 -0
- package/dist/orchestration/defaults.d.ts.map +1 -0
- package/dist/orchestration/defaults.js +22 -0
- package/dist/orchestration/memory-integration.d.ts +58 -0
- package/dist/orchestration/memory-integration.d.ts.map +1 -0
- package/dist/orchestration/memory-integration.js +130 -0
- package/dist/orchestration/orchestrator.d.ts +67 -0
- package/dist/orchestration/orchestrator.d.ts.map +1 -0
- package/dist/orchestration/orchestrator.js +174 -0
- package/dist/orchestration/runtime.d.ts +82 -0
- package/dist/orchestration/runtime.d.ts.map +1 -0
- package/dist/orchestration/runtime.js +251 -0
- package/dist/orchestration/streaming-runtime.d.ts +36 -0
- package/dist/orchestration/streaming-runtime.d.ts.map +1 -0
- package/dist/orchestration/streaming-runtime.js +175 -0
- package/dist/orchestration/ticket-agent.d.ts +70 -0
- package/dist/orchestration/ticket-agent.d.ts.map +1 -0
- package/dist/orchestration/ticket-agent.js +146 -0
- package/dist/skills/activation/index.d.ts +7 -0
- package/dist/skills/activation/index.d.ts.map +1 -0
- package/dist/skills/activation/index.js +6 -0
- package/dist/skills/activation/skill-activator.d.ts +68 -0
- package/dist/skills/activation/skill-activator.d.ts.map +1 -0
- package/dist/skills/activation/skill-activator.js +224 -0
- package/dist/skills/catalog/catalog-search.d.ts +55 -0
- package/dist/skills/catalog/catalog-search.d.ts.map +1 -0
- package/dist/skills/catalog/catalog-search.js +111 -0
- package/dist/skills/catalog/catalog-types.d.ts +81 -0
- package/dist/skills/catalog/catalog-types.d.ts.map +1 -0
- package/dist/skills/catalog/catalog-types.js +66 -0
- package/dist/skills/catalog/index.d.ts +9 -0
- package/dist/skills/catalog/index.d.ts.map +1 -0
- package/dist/skills/catalog/index.js +7 -0
- package/dist/skills/catalog/vercel-catalog.d.ts +42 -0
- package/dist/skills/catalog/vercel-catalog.d.ts.map +1 -0
- package/dist/skills/catalog/vercel-catalog.js +189 -0
- package/dist/skills/compat/index.d.ts +9 -0
- package/dist/skills/compat/index.d.ts.map +1 -0
- package/dist/skills/compat/index.js +8 -0
- package/dist/skills/compat/skill-enhancer.d.ts +37 -0
- package/dist/skills/compat/skill-enhancer.d.ts.map +1 -0
- package/dist/skills/compat/skill-enhancer.js +76 -0
- package/dist/skills/compat/tool-mapper.d.ts +61 -0
- package/dist/skills/compat/tool-mapper.d.ts.map +1 -0
- package/dist/skills/compat/tool-mapper.js +168 -0
- package/dist/skills/compat/vercel-compat.d.ts +33 -0
- package/dist/skills/compat/vercel-compat.d.ts.map +1 -0
- package/dist/skills/compat/vercel-compat.js +132 -0
- package/dist/skills/index.d.ts +40 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +47 -0
- package/dist/skills/integration/agent-skill-provider.d.ts +94 -0
- package/dist/skills/integration/agent-skill-provider.d.ts.map +1 -0
- package/dist/skills/integration/agent-skill-provider.js +161 -0
- package/dist/skills/integration/index.d.ts +7 -0
- package/dist/skills/integration/index.d.ts.map +1 -0
- package/dist/skills/integration/index.js +6 -0
- package/dist/skills/loader/github-loader.d.ts +61 -0
- package/dist/skills/loader/github-loader.d.ts.map +1 -0
- package/dist/skills/loader/github-loader.js +176 -0
- package/dist/skills/loader/index.d.ts +10 -0
- package/dist/skills/loader/index.d.ts.map +1 -0
- package/dist/skills/loader/index.js +9 -0
- package/dist/skills/loader/local-loader.d.ts +56 -0
- package/dist/skills/loader/local-loader.d.ts.map +1 -0
- package/dist/skills/loader/local-loader.js +186 -0
- package/dist/skills/loader/vercel-loader.d.ts +64 -0
- package/dist/skills/loader/vercel-loader.d.ts.map +1 -0
- package/dist/skills/loader/vercel-loader.js +313 -0
- package/dist/skills/loader/vercel-types.d.ts +64 -0
- package/dist/skills/loader/vercel-types.d.ts.map +1 -0
- package/dist/skills/loader/vercel-types.js +55 -0
- package/dist/skills/parser/index.d.ts +7 -0
- package/dist/skills/parser/index.d.ts.map +1 -0
- package/dist/skills/parser/index.js +6 -0
- package/dist/skills/parser/skill-md-parser.d.ts +64 -0
- package/dist/skills/parser/skill-md-parser.d.ts.map +1 -0
- package/dist/skills/parser/skill-md-parser.js +242 -0
- package/dist/skills/registry/index.d.ts +7 -0
- package/dist/skills/registry/index.d.ts.map +1 -0
- package/dist/skills/registry/index.js +6 -0
- package/dist/skills/registry/skill-registry.d.ts +133 -0
- package/dist/skills/registry/skill-registry.d.ts.map +1 -0
- package/dist/skills/registry/skill-registry.js +373 -0
- package/dist/skills/types.d.ts +216 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +176 -0
- package/dist/templates/agent-spec.d.ts +138 -0
- package/dist/templates/agent-spec.d.ts.map +1 -0
- package/dist/templates/agent-spec.js +138 -0
- package/dist/templates/index.d.ts +56 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +58 -0
- package/dist/templates/prompt-spec.d.ts +140 -0
- package/dist/templates/prompt-spec.d.ts.map +1 -0
- package/dist/templates/prompt-spec.js +210 -0
- package/dist/templates/skill-spec.d.ts +106 -0
- package/dist/templates/skill-spec.d.ts.map +1 -0
- package/dist/templates/skill-spec.js +119 -0
- package/dist/tools/base.d.ts +74 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +6 -0
- package/dist/tools/cms/collection-tools.d.ts +36 -0
- package/dist/tools/cms/collection-tools.d.ts.map +1 -0
- package/dist/tools/cms/collection-tools.js +178 -0
- package/dist/tools/cms/factory.d.ts +89 -0
- package/dist/tools/cms/factory.d.ts.map +1 -0
- package/dist/tools/cms/factory.js +462 -0
- package/dist/tools/cms/global-tools.d.ts +21 -0
- package/dist/tools/cms/global-tools.d.ts.map +1 -0
- package/dist/tools/cms/global-tools.js +92 -0
- package/dist/tools/cms/index.d.ts +11 -0
- package/dist/tools/cms/index.d.ts.map +1 -0
- package/dist/tools/cms/index.js +11 -0
- package/dist/tools/cms/media-tools.d.ts +31 -0
- package/dist/tools/cms/media-tools.d.ts.map +1 -0
- package/dist/tools/cms/media-tools.js +140 -0
- package/dist/tools/cms/user-tools.d.ts +31 -0
- package/dist/tools/cms/user-tools.d.ts.map +1 -0
- package/dist/tools/cms/user-tools.js +135 -0
- package/dist/tools/deduplicator.d.ts +19 -0
- package/dist/tools/deduplicator.d.ts.map +1 -0
- package/dist/tools/deduplicator.js +53 -0
- package/dist/tools/document-summarizer.d.ts +11 -0
- package/dist/tools/document-summarizer.d.ts.map +1 -0
- package/dist/tools/document-summarizer.js +82 -0
- package/dist/tools/mcp-adapter.d.ts +66 -0
- package/dist/tools/mcp-adapter.d.ts.map +1 -0
- package/dist/tools/mcp-adapter.js +152 -0
- package/dist/tools/memory/index.d.ts +3 -0
- package/dist/tools/memory/index.d.ts.map +1 -0
- package/dist/tools/memory/index.js +1 -0
- package/dist/tools/memory/store-memory.d.ts +39 -0
- package/dist/tools/memory/store-memory.d.ts.map +1 -0
- package/dist/tools/memory/store-memory.js +94 -0
- package/dist/tools/registry.d.ts +14 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +48 -0
- package/dist/tools/ticket-tools.d.ts +31 -0
- package/dist/tools/ticket-tools.d.ts.map +1 -0
- package/dist/tools/ticket-tools.js +74 -0
- package/dist/tools/web/duck-duck-go.d.ts +52 -0
- package/dist/tools/web/duck-duck-go.d.ts.map +1 -0
- package/dist/tools/web/duck-duck-go.js +202 -0
- package/dist/tools/web/exa.d.ts +34 -0
- package/dist/tools/web/exa.d.ts.map +1 -0
- package/dist/tools/web/exa.js +80 -0
- package/dist/tools/web/index.d.ts +6 -0
- package/dist/tools/web/index.d.ts.map +1 -0
- package/dist/tools/web/index.js +4 -0
- package/dist/tools/web/scraper.d.ts +9 -0
- package/dist/tools/web/scraper.d.ts.map +1 -0
- package/dist/tools/web/scraper.js +118 -0
- package/dist/tools/web/tavily.d.ts +32 -0
- package/dist/tools/web/tavily.d.ts.map +1 -0
- package/dist/tools/web/tavily.js +73 -0
- package/dist/tools/web/types.d.ts +31 -0
- package/dist/tools/web/types.d.ts.map +1 -0
- package/dist/tools/web/types.js +9 -0
- package/package.json +143 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Preferences Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages user preferences with CRDT support for conflict-free updates
|
|
5
|
+
* from multiple devices. Uses LWWRegister for last-writer-wins semantics.
|
|
6
|
+
*/
|
|
7
|
+
import { UserPreferencesSchema } from '@revealui/contracts/entities';
|
|
8
|
+
import { eq, users } from '@revealui/db/schema';
|
|
9
|
+
import { LWWRegister } from '../crdt/lww-register.js';
|
|
10
|
+
import { DatabaseConnectionError, DatabaseConstraintError, DatabaseOperationError, NotFoundError, ValidationError, } from '../errors/index.js';
|
|
11
|
+
import { deepClone } from '../utils/deep-clone.js';
|
|
12
|
+
import { createLogger } from '../utils/logger.js';
|
|
13
|
+
import { findUserById } from '../utils/sql-helpers.js';
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// User Preferences Manager
|
|
16
|
+
// =============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Manages user preferences with CRDT support.
|
|
19
|
+
*
|
|
20
|
+
* Note: This implementation stores CRDT state directly in users.preferences JSONB field.
|
|
21
|
+
* All preferences must be in CRDT format - legacy format is not supported.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const manager = new UserPreferencesManager('user-123', 'node-abc', db)
|
|
26
|
+
* await manager.load()
|
|
27
|
+
*
|
|
28
|
+
* await manager.updatePreferences({ theme: 'dark' })
|
|
29
|
+
* const prefs = manager.getPreferences()
|
|
30
|
+
*
|
|
31
|
+
* await manager.save()
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class UserPreferencesManager {
|
|
35
|
+
preferences;
|
|
36
|
+
userId;
|
|
37
|
+
nodeId;
|
|
38
|
+
db;
|
|
39
|
+
logger = createLogger('[UserPreferences]');
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new UserPreferencesManager.
|
|
42
|
+
*
|
|
43
|
+
* @param userId - User identifier
|
|
44
|
+
* @param nodeId - Node identifier (for CRDT operations)
|
|
45
|
+
* @param db - Database client
|
|
46
|
+
*/
|
|
47
|
+
constructor(userId, nodeId, db) {
|
|
48
|
+
this.userId = userId;
|
|
49
|
+
this.nodeId = nodeId;
|
|
50
|
+
this.db = db;
|
|
51
|
+
// Initialize with default preferences
|
|
52
|
+
const defaultPrefs = {
|
|
53
|
+
theme: 'system',
|
|
54
|
+
language: 'en',
|
|
55
|
+
timezone: 'UTC',
|
|
56
|
+
};
|
|
57
|
+
this.preferences = new LWWRegister(nodeId, defaultPrefs);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Gets the current preferences.
|
|
61
|
+
*
|
|
62
|
+
* @returns Current user preferences
|
|
63
|
+
*/
|
|
64
|
+
getPreferences() {
|
|
65
|
+
return this.preferences.get();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Updates preferences with partial data.
|
|
69
|
+
* Merges with existing preferences.
|
|
70
|
+
*
|
|
71
|
+
* @param updates - Partial preferences updates
|
|
72
|
+
*/
|
|
73
|
+
updatePreferences(updates) {
|
|
74
|
+
const current = this.preferences.get();
|
|
75
|
+
const merged = {
|
|
76
|
+
...current,
|
|
77
|
+
...updates,
|
|
78
|
+
// Deep merge for nested objects
|
|
79
|
+
notifications: updates.notifications
|
|
80
|
+
? { ...current.notifications, ...updates.notifications }
|
|
81
|
+
: current.notifications,
|
|
82
|
+
editor: updates.editor ? { ...current.editor, ...updates.editor } : current.editor,
|
|
83
|
+
ai: updates.ai ? { ...current.ai, ...updates.ai } : current.ai,
|
|
84
|
+
};
|
|
85
|
+
// Validate before setting
|
|
86
|
+
const validationResult = UserPreferencesSchema.safeParse(merged);
|
|
87
|
+
if (!validationResult.success) {
|
|
88
|
+
throw new ValidationError(`Invalid preferences: ${validationResult.error.message}`);
|
|
89
|
+
}
|
|
90
|
+
this.preferences.set(validationResult.data);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sets entire preferences object.
|
|
94
|
+
*
|
|
95
|
+
* @param preferences - Complete preferences object
|
|
96
|
+
*/
|
|
97
|
+
setPreferences(preferences) {
|
|
98
|
+
// Validate before setting
|
|
99
|
+
const validationResult = UserPreferencesSchema.safeParse(preferences);
|
|
100
|
+
if (!validationResult.success) {
|
|
101
|
+
throw new ValidationError(`Invalid preferences: ${validationResult.error.message}`);
|
|
102
|
+
}
|
|
103
|
+
this.preferences.set(validationResult.data);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Gets a specific preference value.
|
|
107
|
+
*
|
|
108
|
+
* @param key - Preference key (supports dot notation for nested keys)
|
|
109
|
+
* @returns Preference value or undefined
|
|
110
|
+
*/
|
|
111
|
+
getPreference(key) {
|
|
112
|
+
const prefs = this.preferences.get();
|
|
113
|
+
const keys = key.split('.');
|
|
114
|
+
let value = prefs;
|
|
115
|
+
for (const k of keys) {
|
|
116
|
+
if (value && typeof value === 'object' && k in value) {
|
|
117
|
+
value = value[k];
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Sets a specific preference value.
|
|
127
|
+
*
|
|
128
|
+
* @param key - Preference key (supports dot notation for nested keys)
|
|
129
|
+
* @param value - Preference value
|
|
130
|
+
*/
|
|
131
|
+
setPreference(key, value) {
|
|
132
|
+
// Deep clone to avoid mutations
|
|
133
|
+
const prefs = deepClone(this.preferences.get());
|
|
134
|
+
const keys = key.split('.');
|
|
135
|
+
const lastKey = keys[keys.length - 1];
|
|
136
|
+
if (!lastKey) {
|
|
137
|
+
throw new ValidationError(`Invalid preference key: "${key}"`);
|
|
138
|
+
}
|
|
139
|
+
const parentKeys = keys.slice(0, -1);
|
|
140
|
+
// Navigate to parent object in cloned structure
|
|
141
|
+
let parent = prefs;
|
|
142
|
+
for (const k of parentKeys) {
|
|
143
|
+
if (!(k in parent) || typeof parent[k] !== 'object' || parent[k] === null) {
|
|
144
|
+
parent[k] = {};
|
|
145
|
+
}
|
|
146
|
+
parent = parent[k];
|
|
147
|
+
}
|
|
148
|
+
// Set the value in cloned structure
|
|
149
|
+
parent[lastKey] = value;
|
|
150
|
+
// Validate and set
|
|
151
|
+
const validationResult = UserPreferencesSchema.safeParse(prefs);
|
|
152
|
+
if (!validationResult.success) {
|
|
153
|
+
throw new ValidationError(`Invalid preferences after update: ${validationResult.error.message}`);
|
|
154
|
+
}
|
|
155
|
+
this.preferences.set(validationResult.data);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Merges another UserPreferencesManager into this one.
|
|
159
|
+
* Uses CRDT merge for conflict resolution.
|
|
160
|
+
*
|
|
161
|
+
* @param other - UserPreferencesManager to merge
|
|
162
|
+
* @returns New merged UserPreferencesManager
|
|
163
|
+
*/
|
|
164
|
+
merge(other) {
|
|
165
|
+
const merged = new UserPreferencesManager(this.userId, this.nodeId, this.db);
|
|
166
|
+
merged.preferences = this.preferences.merge(other.preferences);
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Loads preferences from database.
|
|
171
|
+
* Stores CRDT state directly in users.preferences JSONB field.
|
|
172
|
+
* Requires preferences to be in CRDT format.
|
|
173
|
+
*
|
|
174
|
+
* @throws DatabaseConnectionError if database connection fails
|
|
175
|
+
* @throws DatabaseOperationError if database operation fails
|
|
176
|
+
* @throws ValidationError if preferences are not in CRDT format
|
|
177
|
+
*/
|
|
178
|
+
async load() {
|
|
179
|
+
try {
|
|
180
|
+
// Load user record (using raw SQL for Neon HTTP compatibility)
|
|
181
|
+
const user = await findUserById(this.db, this.userId);
|
|
182
|
+
if (!user) {
|
|
183
|
+
// User doesn't exist, use defaults
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Check if preferences exist
|
|
187
|
+
if (user.preferences && typeof user.preferences === 'object' && user.preferences !== null) {
|
|
188
|
+
const prefsRecord = user.preferences;
|
|
189
|
+
// Check for CRDT state in _crdt field
|
|
190
|
+
if (prefsRecord._crdt && typeof prefsRecord._crdt === 'object') {
|
|
191
|
+
const crdtData = prefsRecord._crdt;
|
|
192
|
+
const lwwData = crdtData.lww_register;
|
|
193
|
+
if (lwwData && 'value' in lwwData) {
|
|
194
|
+
const validationResult = UserPreferencesSchema.safeParse(lwwData.value);
|
|
195
|
+
if (validationResult.success) {
|
|
196
|
+
this.preferences = LWWRegister.fromData(lwwData);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Preferences exist but are not in CRDT format
|
|
202
|
+
// Per legacy code removal policy, we do not support legacy format
|
|
203
|
+
throw new ValidationError(`User preferences for ${this.userId} are not in valid CRDT format. ` +
|
|
204
|
+
`Expected CRDT structure with _crdt.lww_register.value, but got: ${JSON.stringify(user.preferences).substring(0, 100)}. ` +
|
|
205
|
+
`All preferences must be stored in CRDT format.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// Handle specific error types
|
|
210
|
+
if (error instanceof ValidationError) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
// Database errors
|
|
214
|
+
if (error instanceof Error) {
|
|
215
|
+
const errorMessage = error.message.toLowerCase();
|
|
216
|
+
if (errorMessage.includes('connection') ||
|
|
217
|
+
errorMessage.includes('timeout') ||
|
|
218
|
+
errorMessage.includes('econnrefused') ||
|
|
219
|
+
errorMessage.includes('connect econnrefused')) {
|
|
220
|
+
this.logger.error(`Database connection error loading preferences for user ${this.userId}:`, error);
|
|
221
|
+
throw new DatabaseConnectionError('Failed to load user preferences', error);
|
|
222
|
+
}
|
|
223
|
+
if (errorMessage.includes('violates') ||
|
|
224
|
+
errorMessage.includes('constraint') ||
|
|
225
|
+
errorMessage.includes('foreign key')) {
|
|
226
|
+
this.logger.error(`Database constraint error loading preferences for user ${this.userId}:`, error);
|
|
227
|
+
throw new DatabaseConstraintError('Failed to load user preferences', error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Generic database operation error
|
|
231
|
+
this.logger.error(`Failed to load preferences for user ${this.userId}:`, error);
|
|
232
|
+
throw new DatabaseOperationError('Failed to load user preferences', error instanceof Error ? error : new Error(String(error)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Saves preferences to database.
|
|
237
|
+
* Stores CRDT state directly in users.preferences JSONB field.
|
|
238
|
+
*
|
|
239
|
+
* @throws NotFoundError if user does not exist
|
|
240
|
+
* @throws DatabaseConnectionError if database connection fails
|
|
241
|
+
* @throws DatabaseConstraintError if database constraint violation occurs
|
|
242
|
+
* @throws DatabaseOperationError if database operation fails
|
|
243
|
+
*/
|
|
244
|
+
async save() {
|
|
245
|
+
try {
|
|
246
|
+
// Store CRDT state in users.preferences JSONB field
|
|
247
|
+
// Structure: { _crdt: { lww_register: {...} } }
|
|
248
|
+
const crdtData = this.preferences.toData();
|
|
249
|
+
const preferencesValue = {
|
|
250
|
+
// biome-ignore lint/style/useNamingConvention: _crdt is the CRDT state storage key — internal convention that must not be renamed
|
|
251
|
+
_crdt: {
|
|
252
|
+
lww_register: crdtData,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
// Check if user exists (using raw SQL for Neon HTTP compatibility)
|
|
256
|
+
const existingUser = await findUserById(this.db, this.userId);
|
|
257
|
+
if (!existingUser) {
|
|
258
|
+
throw new NotFoundError(`User ${this.userId}`);
|
|
259
|
+
}
|
|
260
|
+
// Update users table with CRDT state in preferences JSONB
|
|
261
|
+
await this.db
|
|
262
|
+
.update(users)
|
|
263
|
+
.set({
|
|
264
|
+
preferences: preferencesValue,
|
|
265
|
+
updatedAt: new Date(),
|
|
266
|
+
})
|
|
267
|
+
.where(eq(users.id, this.userId));
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
// Handle specific error types (re-throw as-is)
|
|
271
|
+
if (error instanceof NotFoundError ||
|
|
272
|
+
error instanceof DatabaseConnectionError ||
|
|
273
|
+
error instanceof DatabaseConstraintError ||
|
|
274
|
+
error instanceof DatabaseOperationError) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
// Handle database errors
|
|
278
|
+
if (error instanceof Error) {
|
|
279
|
+
const errorMessage = error.message.toLowerCase();
|
|
280
|
+
if (errorMessage.includes('connection') ||
|
|
281
|
+
errorMessage.includes('timeout') ||
|
|
282
|
+
errorMessage.includes('econnrefused') ||
|
|
283
|
+
errorMessage.includes('connect econnrefused')) {
|
|
284
|
+
this.logger.error(`Database connection error saving preferences for user ${this.userId}:`, error);
|
|
285
|
+
throw new DatabaseConnectionError('Failed to save user preferences', error);
|
|
286
|
+
}
|
|
287
|
+
if (errorMessage.includes('violates') ||
|
|
288
|
+
errorMessage.includes('constraint') ||
|
|
289
|
+
errorMessage.includes('foreign key') ||
|
|
290
|
+
errorMessage.includes('duplicate')) {
|
|
291
|
+
this.logger.error(`Database constraint error saving preferences for user ${this.userId}:`, error);
|
|
292
|
+
throw new DatabaseConstraintError('Failed to save user preferences', error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Generic database operation error
|
|
296
|
+
this.logger.error(`Failed to save preferences for user ${this.userId}:`, error);
|
|
297
|
+
throw new DatabaseOperationError('Failed to save user preferences', error instanceof Error ? error : new Error(String(error)));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Serializes preferences to plain object.
|
|
302
|
+
*
|
|
303
|
+
* @returns Serialized data
|
|
304
|
+
*/
|
|
305
|
+
toData() {
|
|
306
|
+
return this.preferences.toData();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Deserializes preferences from plain object.
|
|
310
|
+
*
|
|
311
|
+
* @param data - Serialized data
|
|
312
|
+
* @param userId - User identifier
|
|
313
|
+
* @param nodeId - Node identifier
|
|
314
|
+
* @param db - Database client
|
|
315
|
+
* @returns New UserPreferencesManager instance
|
|
316
|
+
*/
|
|
317
|
+
static fromData(data, userId, nodeId, db) {
|
|
318
|
+
const manager = new UserPreferencesManager(userId, nodeId, db);
|
|
319
|
+
manager.preferences = LWWRegister.fromData(data);
|
|
320
|
+
return manager;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Creates a copy of this UserPreferencesManager.
|
|
324
|
+
*
|
|
325
|
+
* @returns New UserPreferencesManager with same state
|
|
326
|
+
*/
|
|
327
|
+
clone() {
|
|
328
|
+
return UserPreferencesManager.fromData(this.toData(), this.userId, this.nodeId, this.db);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Gets the user ID.
|
|
332
|
+
*/
|
|
333
|
+
getUserId() {
|
|
334
|
+
return this.userId;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Gets the node ID.
|
|
338
|
+
*/
|
|
339
|
+
getNodeId() {
|
|
340
|
+
return this.nodeId;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/memory/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node ID Service
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic node IDs for CRDT operations using a hybrid approach:
|
|
5
|
+
* - Primary: SHA-256 hash of entity ID (fast, deterministic, no DB lookup)
|
|
6
|
+
* - Fallback: Database table for collision resolution and manual management
|
|
7
|
+
*
|
|
8
|
+
* This ensures:
|
|
9
|
+
* - Same entity always gets same node ID (deterministic)
|
|
10
|
+
* - Collision-resistant (SHA-256 + DB fallback)
|
|
11
|
+
* - Fast (hash is primary, DB only on collision)
|
|
12
|
+
*/
|
|
13
|
+
import type { Database } from '@revealui/db/client';
|
|
14
|
+
export type EntityType = 'session' | 'user';
|
|
15
|
+
/**
|
|
16
|
+
* Node ID Service
|
|
17
|
+
*/
|
|
18
|
+
export declare class NodeIdService {
|
|
19
|
+
private db;
|
|
20
|
+
constructor(db: Database);
|
|
21
|
+
/**
|
|
22
|
+
* Gets or creates a node ID for an entity.
|
|
23
|
+
*
|
|
24
|
+
* Strategy:
|
|
25
|
+
* 1. Generate SHA-256 hash of entityId
|
|
26
|
+
* 2. Check database for existing mapping
|
|
27
|
+
* 3. If exists, return stored nodeId
|
|
28
|
+
* 4. If not, create new mapping with UUID
|
|
29
|
+
* 5. Handle collisions (same hash, different entityId)
|
|
30
|
+
*
|
|
31
|
+
* @param entityType - Type of entity ('session' or 'user')
|
|
32
|
+
* @param entityId - Entity identifier (sessionId or userId)
|
|
33
|
+
* @returns Node ID (UUID string)
|
|
34
|
+
*/
|
|
35
|
+
getNodeId(entityType: EntityType, entityId: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves a hash collision (same hash, different entityId).
|
|
38
|
+
*
|
|
39
|
+
* Strategy: Generate a new hash using entityType + entityId to ensure uniqueness.
|
|
40
|
+
*
|
|
41
|
+
* @param hash - Original hash that collided
|
|
42
|
+
* @param entityType - Type of entity
|
|
43
|
+
* @param entityId - Entity identifier
|
|
44
|
+
* @param attempt - Current collision resolution attempt (default: 1)
|
|
45
|
+
* @returns Node ID (UUID string)
|
|
46
|
+
* @throws Error if max collision attempts exceeded
|
|
47
|
+
*/
|
|
48
|
+
private resolveCollision;
|
|
49
|
+
/**
|
|
50
|
+
* Generates SHA-256 hash of entity ID.
|
|
51
|
+
*
|
|
52
|
+
* @param entityId - Entity identifier
|
|
53
|
+
* @returns Hexadecimal hash string (64 characters)
|
|
54
|
+
*/
|
|
55
|
+
private hashEntityId;
|
|
56
|
+
/**
|
|
57
|
+
* Validates input parameters.
|
|
58
|
+
*
|
|
59
|
+
* @param entityType - Type of entity
|
|
60
|
+
* @param entityId - Entity identifier
|
|
61
|
+
* @throws Error if inputs are invalid
|
|
62
|
+
*/
|
|
63
|
+
private validateInputs;
|
|
64
|
+
/**
|
|
65
|
+
* Retry wrapper for database operations with exponential backoff.
|
|
66
|
+
*
|
|
67
|
+
* @param operation - Async operation to retry
|
|
68
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
69
|
+
* @param baseDelay - Base delay in milliseconds for exponential backoff (default: 100)
|
|
70
|
+
* @returns Result of the operation
|
|
71
|
+
* @throws Error if all retries fail
|
|
72
|
+
*/
|
|
73
|
+
private withRetry;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=node-id-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-id-service.d.ts","sourceRoot":"","sources":["../../../src/memory/services/node-id-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAInD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,CAAA;AAE3C;;GAEG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAEhC;;;;;;;;;;;;;OAaG;IACG,SAAS,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoC1E;;;;;;;;;;;OAWG;YACW,gBAAgB;IAsD9B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAIpB;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;;;;OAQG;YACW,SAAS;CAuCxB"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node ID Service
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic node IDs for CRDT operations using a hybrid approach:
|
|
5
|
+
* - Primary: SHA-256 hash of entity ID (fast, deterministic, no DB lookup)
|
|
6
|
+
* - Fallback: Database table for collision resolution and manual management
|
|
7
|
+
*
|
|
8
|
+
* This ensures:
|
|
9
|
+
* - Same entity always gets same node ID (deterministic)
|
|
10
|
+
* - Collision-resistant (SHA-256 + DB fallback)
|
|
11
|
+
* - Fast (hash is primary, DB only on collision)
|
|
12
|
+
*/
|
|
13
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
14
|
+
import { logger } from '@revealui/core/utils/logger';
|
|
15
|
+
import { nodeIdMappings } from '@revealui/db/schema';
|
|
16
|
+
import { findNodeIdMappingByHash } from '../utils/sql-helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Node ID Service
|
|
19
|
+
*/
|
|
20
|
+
export class NodeIdService {
|
|
21
|
+
db;
|
|
22
|
+
constructor(db) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Gets or creates a node ID for an entity.
|
|
27
|
+
*
|
|
28
|
+
* Strategy:
|
|
29
|
+
* 1. Generate SHA-256 hash of entityId
|
|
30
|
+
* 2. Check database for existing mapping
|
|
31
|
+
* 3. If exists, return stored nodeId
|
|
32
|
+
* 4. If not, create new mapping with UUID
|
|
33
|
+
* 5. Handle collisions (same hash, different entityId)
|
|
34
|
+
*
|
|
35
|
+
* @param entityType - Type of entity ('session' or 'user')
|
|
36
|
+
* @param entityId - Entity identifier (sessionId or userId)
|
|
37
|
+
* @returns Node ID (UUID string)
|
|
38
|
+
*/
|
|
39
|
+
async getNodeId(entityType, entityId) {
|
|
40
|
+
return this.withRetry(async () => {
|
|
41
|
+
// Validate inputs
|
|
42
|
+
this.validateInputs(entityType, entityId);
|
|
43
|
+
// Generate SHA-256 hash
|
|
44
|
+
const hash = this.hashEntityId(entityId);
|
|
45
|
+
// Check database for existing mapping (using raw SQL for Neon HTTP compatibility)
|
|
46
|
+
const existing = await findNodeIdMappingByHash(this.db, hash);
|
|
47
|
+
if (existing) {
|
|
48
|
+
// Verify entityId matches (collision detection)
|
|
49
|
+
if (existing.entityId !== entityId || existing.entityType !== entityType) {
|
|
50
|
+
// Collision detected - resolve it
|
|
51
|
+
return this.resolveCollision(hash, entityType, entityId, 1);
|
|
52
|
+
}
|
|
53
|
+
// Return existing node ID
|
|
54
|
+
return existing.nodeId;
|
|
55
|
+
}
|
|
56
|
+
// No existing mapping - create new one
|
|
57
|
+
const newNodeId = randomUUID();
|
|
58
|
+
await this.db.insert(nodeIdMappings).values({
|
|
59
|
+
id: hash,
|
|
60
|
+
entityType,
|
|
61
|
+
entityId,
|
|
62
|
+
nodeId: newNodeId,
|
|
63
|
+
createdAt: new Date(),
|
|
64
|
+
updatedAt: new Date(),
|
|
65
|
+
});
|
|
66
|
+
return newNodeId;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolves a hash collision (same hash, different entityId).
|
|
71
|
+
*
|
|
72
|
+
* Strategy: Generate a new hash using entityType + entityId to ensure uniqueness.
|
|
73
|
+
*
|
|
74
|
+
* @param hash - Original hash that collided
|
|
75
|
+
* @param entityType - Type of entity
|
|
76
|
+
* @param entityId - Entity identifier
|
|
77
|
+
* @param attempt - Current collision resolution attempt (default: 1)
|
|
78
|
+
* @returns Node ID (UUID string)
|
|
79
|
+
* @throws Error if max collision attempts exceeded
|
|
80
|
+
*/
|
|
81
|
+
async resolveCollision(hash, entityType, entityId, attempt = 1) {
|
|
82
|
+
const MaxCollisionAttempts = 10;
|
|
83
|
+
if (attempt > MaxCollisionAttempts) {
|
|
84
|
+
throw new Error(`Failed to resolve node ID collision after ${MaxCollisionAttempts} attempts. ` +
|
|
85
|
+
`This is extremely rare. Please contact support.`);
|
|
86
|
+
}
|
|
87
|
+
// Log collision for monitoring
|
|
88
|
+
logger.warn('Node ID collision detected', {
|
|
89
|
+
attempt,
|
|
90
|
+
maxAttempts: MaxCollisionAttempts,
|
|
91
|
+
hash,
|
|
92
|
+
entityType,
|
|
93
|
+
entityId,
|
|
94
|
+
});
|
|
95
|
+
// Generate collision-resistant hash using entityType + entityId
|
|
96
|
+
const collisionHash = this.hashEntityId(`${entityType}:${entityId}:${attempt}`);
|
|
97
|
+
// Check if collision hash already exists (using raw SQL for Neon HTTP compatibility)
|
|
98
|
+
const existing = await findNodeIdMappingByHash(this.db, collisionHash);
|
|
99
|
+
if (existing) {
|
|
100
|
+
// Verify this is actually our entity (not another collision)
|
|
101
|
+
if (existing.entityId === entityId && existing.entityType === entityType) {
|
|
102
|
+
// This is our mapping - use it
|
|
103
|
+
return existing.nodeId;
|
|
104
|
+
}
|
|
105
|
+
// Another collision - recurse with incremented attempt
|
|
106
|
+
return this.resolveCollision(collisionHash, entityType, entityId, attempt + 1);
|
|
107
|
+
}
|
|
108
|
+
// Create new mapping with collision hash
|
|
109
|
+
const newNodeId = randomUUID();
|
|
110
|
+
await this.db.insert(nodeIdMappings).values({
|
|
111
|
+
id: collisionHash,
|
|
112
|
+
entityType,
|
|
113
|
+
entityId,
|
|
114
|
+
nodeId: newNodeId,
|
|
115
|
+
createdAt: new Date(),
|
|
116
|
+
updatedAt: new Date(),
|
|
117
|
+
});
|
|
118
|
+
return newNodeId;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generates SHA-256 hash of entity ID.
|
|
122
|
+
*
|
|
123
|
+
* @param entityId - Entity identifier
|
|
124
|
+
* @returns Hexadecimal hash string (64 characters)
|
|
125
|
+
*/
|
|
126
|
+
hashEntityId(entityId) {
|
|
127
|
+
return createHash('sha256').update(entityId).digest('hex');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates input parameters.
|
|
131
|
+
*
|
|
132
|
+
* @param entityType - Type of entity
|
|
133
|
+
* @param entityId - Entity identifier
|
|
134
|
+
* @throws Error if inputs are invalid
|
|
135
|
+
*/
|
|
136
|
+
validateInputs(entityType, entityId) {
|
|
137
|
+
if (entityType !== 'session' && entityType !== 'user') {
|
|
138
|
+
throw new Error(`Invalid entityType: ${entityType}. Must be 'session' or 'user'`);
|
|
139
|
+
}
|
|
140
|
+
if (!entityId || typeof entityId !== 'string' || entityId.trim().length === 0) {
|
|
141
|
+
throw new Error(`Invalid entityId: must be a non-empty string`);
|
|
142
|
+
}
|
|
143
|
+
// Additional validation: entityId should be reasonable length
|
|
144
|
+
// Very long entityIds might indicate an attack or data corruption
|
|
145
|
+
const MaxEntityIdLength = 1000;
|
|
146
|
+
if (entityId.length > MaxEntityIdLength) {
|
|
147
|
+
throw new Error(`Invalid entityId: length ${entityId.length} exceeds maximum of ${MaxEntityIdLength} characters`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Retry wrapper for database operations with exponential backoff.
|
|
152
|
+
*
|
|
153
|
+
* @param operation - Async operation to retry
|
|
154
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
155
|
+
* @param baseDelay - Base delay in milliseconds for exponential backoff (default: 100)
|
|
156
|
+
* @returns Result of the operation
|
|
157
|
+
* @throws Error if all retries fail
|
|
158
|
+
*/
|
|
159
|
+
async withRetry(operation, maxRetries = 3, baseDelay = 100) {
|
|
160
|
+
let lastError = null;
|
|
161
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
162
|
+
try {
|
|
163
|
+
return await operation();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
167
|
+
// Don't retry on validation errors
|
|
168
|
+
if (lastError.message.includes('Invalid')) {
|
|
169
|
+
throw lastError;
|
|
170
|
+
}
|
|
171
|
+
// Don't retry on collision resolution limit exceeded
|
|
172
|
+
if (lastError.message.includes('Failed to resolve node ID collision')) {
|
|
173
|
+
throw lastError;
|
|
174
|
+
}
|
|
175
|
+
// Exponential backoff: 100ms, 200ms, 400ms
|
|
176
|
+
if (attempt < maxRetries - 1) {
|
|
177
|
+
const delay = baseDelay * 2 ** attempt;
|
|
178
|
+
logger.warn('Database operation failed, retrying', {
|
|
179
|
+
attempt: attempt + 1,
|
|
180
|
+
maxRetries,
|
|
181
|
+
delay,
|
|
182
|
+
error: lastError.message,
|
|
183
|
+
});
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Database operation failed after ${maxRetries} attempts: ${lastError?.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|