@resolveio/server-lib 22.3.219 → 22.3.221
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/.nodemon.json +5 -0
- package/.vscode/settings.json +21 -0
- package/AGENTS.md +195 -0
- package/README.md +22 -0
- package/build_package.sh +5 -0
- package/compileDTS.pl +64 -0
- package/docs/ai-assistant-nightly-eval.md +65 -0
- package/docs/ai-assistant-preflight-checklist.md +23 -0
- package/docs/ai-assistant-report-builder-bridge-playbook.md +115 -0
- package/eslint-plugin-custom/index.js +7 -0
- package/eslint-plugin-custom/rules/no-filter-zero-index.js +44 -0
- package/eslint.config.js +103 -0
- package/gulpfile.js +216 -0
- package/methodAndPublicationListGenerator.py +375 -0
- package/mongodbensurers.js +2 -0
- package/mongostop.js +3 -0
- package/package.json +1 -1
- package/scripts/cleanup-bypassed-callmethod-logs.js +616 -0
- package/settings.development.json +25 -0
- package/settings.development.redacted.json +25 -0
- package/src/.env +12 -0
- package/src/ai/assistant-core-heuristics.ts +379 -0
- package/src/ai/resolveio-platform-intelligence-memory-corpus.ts +185 -0
- package/src/ai/resolveio-platform-intelligence-memory.ts +325 -0
- package/{ai/resolveio-platform-intelligence-types.d.ts → src/ai/resolveio-platform-intelligence-types.ts} +20 -15
- package/src/ai/resolveio-platform-intelligence.ts +462 -0
- package/src/client-server-app.ts +12 -0
- package/src/collections/ai-run.collection.ts +117 -0
- package/src/collections/ai-terminal-conversation.collection.ts +91 -0
- package/src/collections/ai-terminal-issue-report.collection.ts +99 -0
- package/src/collections/ai-terminal-message.collection.ts +77 -0
- package/src/collections/app-setting.collection.ts +104 -0
- package/src/collections/app-status.collection.ts +58 -0
- package/src/collections/communication-metric.collection.ts +84 -0
- package/src/collections/counter.collection.ts +56 -0
- package/src/collections/cron-job-history.collection.ts +94 -0
- package/src/collections/cron-job.collection.ts +92 -0
- package/src/collections/customer-notification.collection.ts +131 -0
- package/src/collections/customer-portal-password.collection.ts +76 -0
- package/src/collections/email-history.collection.ts +134 -0
- package/src/collections/email-verified.collection.ts +62 -0
- package/src/collections/file.collection.ts +74 -0
- package/src/collections/flag-update.collection.ts +57 -0
- package/src/collections/flag.collection.ts +57 -0
- package/src/collections/log-method-latency.collection.ts +77 -0
- package/src/collections/log-subscription.collection.ts +80 -0
- package/src/collections/log.collection.ts +93 -0
- package/src/collections/logged-in-users.collection.ts +67 -0
- package/src/collections/monitor-cpu.collection.ts +65 -0
- package/src/collections/monitor-function.collection.ts +74 -0
- package/src/collections/monitor-memory.collection.ts +77 -0
- package/src/collections/monitor-mongo.collection.ts +71 -0
- package/src/collections/notification.collection.ts +57 -0
- package/src/collections/openai-usage-ledger.collection.ts +131 -0
- package/src/collections/report-builder-dashboard-builder.collection.ts +109 -0
- package/src/collections/report-builder-library.collection.ts +89 -0
- package/src/collections/report-builder-report.collection.ts +184 -0
- package/src/collections/user-group.collection.ts +89 -0
- package/src/collections/user-guide.collection.ts +57 -0
- package/src/collections/user.collection.ts +181 -0
- package/src/cron/cron.ts +117 -0
- package/src/fixtures/cron-jobs.ts +95 -0
- package/src/fixtures/init.ts +35 -0
- package/src/http/auth.ts +818 -0
- package/src/http/health.ts +7 -0
- package/src/http/home.ts +90 -0
- package/src/http/slow-query-publication.ts +49 -0
- package/src/index.ts +1 -0
- package/src/managers/ai-assistant-codex-manager.manager.ts +1131 -0
- package/src/managers/ai-run-evidence.manager.ts +264 -0
- package/src/managers/communication-metric.manager.ts +82 -0
- package/src/managers/cron.manager.ts +333 -0
- package/src/managers/customer-notification-content.manager.ts +236 -0
- package/src/managers/diagnostic-manager-bootstrap.ts +165 -0
- package/src/managers/error-auto-fix.manager.ts +2767 -0
- package/src/managers/local-log.manager.ts +113 -0
- package/src/managers/method.manager.ts +1857 -0
- package/src/managers/mongo.manager.ts +4575 -0
- package/src/managers/monitor.manager.ts +507 -0
- package/src/managers/openai-usage-ledger.manager.ts +112 -0
- package/src/managers/slow-query-verifier.manager.ts +3590 -0
- package/src/managers/slow-query.manager.ts +519 -0
- package/src/managers/subscription.manager.ts +3128 -0
- package/src/managers/websocket.manager.ts +746 -0
- package/src/managers/worker-dispatcher.manager.ts +1360 -0
- package/src/managers/worker-server.manager.ts +536 -0
- package/src/methods/accounts.ts +532 -0
- package/src/methods/ai-terminal.ts +29070 -0
- package/src/methods/app-settings.ts +114 -0
- package/src/methods/aws.ts +649 -0
- package/src/methods/collections.ts +641 -0
- package/src/methods/counters.ts +69 -0
- package/src/methods/cron-jobs.ts +2614 -0
- package/src/methods/customer-notifications.ts +458 -0
- package/src/methods/diagnostics.ts +616 -0
- package/src/methods/flag-updates.ts +7 -0
- package/src/methods/flags.ts +7 -0
- package/src/methods/logs.ts +657 -0
- package/src/methods/mongo-explorer.ts +1880 -0
- package/src/methods/monitor.ts +540 -0
- package/src/methods/pdf.ts +1236 -0
- package/src/methods/publications.ts +129 -0
- package/src/methods/report-builder.ts +3300 -0
- package/src/methods/support.ts +335 -0
- package/src/models/ai-run.model.ts +27 -0
- package/src/models/ai-terminal-conversation.model.ts +19 -0
- package/src/models/ai-terminal-issue-report.model.ts +21 -0
- package/src/models/ai-terminal-message.model.ts +24 -0
- package/src/models/app-setting.model.ts +17 -0
- package/{models/app-status.model.d.ts → src/models/app-status.model.ts} +3 -2
- package/{models/billing-logged-in-users.model.d.ts → src/models/billing-logged-in-users.model.ts} +5 -4
- package/src/models/collection-document.model.ts +24 -0
- package/src/models/communication-metric.model.ts +23 -0
- package/{models/counter.model.d.ts → src/models/counter.model.ts} +4 -3
- package/src/models/cron-job-history.model.ts +16 -0
- package/src/models/cron-job.model.ts +15 -0
- package/src/models/customer-notification.model.ts +28 -0
- package/src/models/customer-portal-password.model.ts +12 -0
- package/src/models/dialog.model.ts +25 -0
- package/{models/email-history.model.js → src/models/email-history.model.ts} +36 -4
- package/{models/email-verified.model.d.ts → src/models/email-verified.model.ts} +6 -5
- package/{models/file.model.d.ts → src/models/file.model.ts} +8 -7
- package/{models/flag-update.model.d.ts → src/models/flag-update.model.ts} +4 -3
- package/{models/flag.model.d.ts → src/models/flag.model.ts} +4 -3
- package/src/models/log-method-latency.model.ts +11 -0
- package/{models/log-subscription.model.d.ts → src/models/log-subscription.model.ts} +11 -9
- package/src/models/log.model.ts +19 -0
- package/{models/logged-in-users.model.d.ts → src/models/logged-in-users.model.ts} +6 -5
- package/{models/method-response.model.d.ts → src/models/method-response.model.ts} +7 -6
- package/src/models/method.model.ts +25 -0
- package/{models/monitor-cpu.model.d.ts → src/models/monitor-cpu.model.ts} +9 -7
- package/src/models/monitor-function.model.ts +16 -0
- package/src/models/monitor-memory.model.ts +17 -0
- package/src/models/monitor-mongo.model.ts +15 -0
- package/{models/notification.model.d.ts → src/models/notification.model.ts} +6 -4
- package/src/models/openai-usage-ledger.model.ts +56 -0
- package/src/models/pagination.model.ts +35 -0
- package/src/models/permission.model.ts +14 -0
- package/src/models/report-builder-dashboard-builder.model.ts +29 -0
- package/src/models/report-builder-library.model.ts +20 -0
- package/src/models/report-builder-report.model.ts +136 -0
- package/src/models/report-builder.model.ts +68 -0
- package/src/models/select-data-label.model.ts +9 -0
- package/src/models/server-message.model.ts +31 -0
- package/src/models/slow-query-report.model.ts +23 -0
- package/src/models/subscription.model.ts +73 -0
- package/src/models/support-ticket.model.ts +104 -0
- package/src/models/user-group.model.ts +24 -0
- package/{models/user-guide.model.d.ts → src/models/user-guide.model.ts} +5 -4
- package/src/models/user.model.ts +96 -0
- package/src/private/images/ResolveIO.png +0 -0
- package/src/publications/ai-terminal.ts +73 -0
- package/src/publications/app-settings.ts +25 -0
- package/src/publications/app-status.ts +13 -0
- package/src/publications/cron-jobs.ts +40 -0
- package/src/publications/customer-notifications.ts +101 -0
- package/src/publications/files.ts +33 -0
- package/src/publications/flags-update.ts +19 -0
- package/src/publications/flags.ts +19 -0
- package/src/publications/logs.ts +163 -0
- package/src/publications/notifications.ts +13 -0
- package/src/publications/report-builder-dashboard-builders.ts +39 -0
- package/src/publications/report-builder-libraries.ts +41 -0
- package/src/publications/report-builder-reports.ts +47 -0
- package/src/publications/super-admin.ts +13 -0
- package/src/publications/user-groups.ts +12 -0
- package/src/publications/user-guides.ts +12 -0
- package/src/resolveio-server-app.ts +617 -0
- package/src/server-app.ts +3354 -0
- package/src/services/codex-client.ts +1231 -0
- package/src/services/openai-client.ts +265 -0
- package/src/types/error-report.ts +26 -0
- package/src/types/js-tiktoken.d.ts +11 -0
- package/src/types/slow-query-report.ts +28 -0
- package/src/util/ai-qa-policy.ts +925 -0
- package/src/util/ai-run-evidence-adapters.ts +8347 -0
- package/src/util/ai-run-evidence-dashboard.ts +323 -0
- package/src/util/ai-run-evidence-eval.ts +1057 -0
- package/src/util/ai-run-evidence.ts +1430 -0
- package/src/util/ai-runner-artifacts.ts +586 -0
- package/src/util/ai-runner-manager-autopilot.ts +961 -0
- package/src/util/ai-runner-manager-policy.ts +5011 -0
- package/src/util/ai-runner-qa-auth.ts +838 -0
- package/src/util/ai-runner-qa-tools.ts +3536 -0
- package/src/util/aicoder-runner-v6.ts +3121 -0
- package/src/util/common.ts +649 -0
- package/src/util/customer-portal-password.ts +183 -0
- package/src/util/error-reporter.ts +332 -0
- package/src/util/error-tracking.ts +79 -0
- package/src/util/openai-usage-cost.ts +114 -0
- package/src/util/report-builder-unwinds.ts +180 -0
- package/src/util/runner-process-janitor.ts +219 -0
- package/src/util/schema-report-builder.ts +448 -0
- package/src/util/slow-query-reporter.ts +216 -0
- package/src/util/subscription-dependency-context.ts +1096 -0
- package/src/util/support-runner-v5.ts +10040 -0
- package/src/util/tokenizer.ts +38 -0
- package/src/workers/codex-runner.worker.ts +142 -0
- package/start_server.sh +5 -0
- package/tests/ai-assistant-corpus-build.ts +484 -0
- package/tests/ai-assistant-corpus-replay-e2e.ts +774 -0
- package/tests/ai-assistant-data-parity-e2e.ts +1989 -0
- package/tests/ai-assistant-eval-triage.ts +831 -0
- package/tests/ai-assistant-openai-e2e.ts +1061 -0
- package/tests/ai-assistant-openai-git-e2e.ts +155 -0
- package/tests/ai-assistant-preflight-matrix.ts +215 -0
- package/tests/ai-assistant-routing-eval.test.ts +585 -0
- package/tests/ai-assistant-snf-live-eval.ts +975 -0
- package/tests/ai-assistant-utils.test.ts +4834 -0
- package/tests/ai-manager-autopilot-snapshot.test.ts +193 -0
- package/tests/ai-manager-recovery-checkpoint.test.ts +1383 -0
- package/tests/ai-run-eval.test.ts +132 -0
- package/tests/ai-run-evidence.test.ts +3773 -0
- package/tests/ai-runner-contract.test.ts +515 -0
- package/tests/aicoder-runner-v6.test.ts +822 -0
- package/tests/error-reporter.test.ts +145 -0
- package/tests/method-publication-generator.test.ts +46 -0
- package/tests/report-builder-linking.test.ts +79 -0
- package/tests/resolveio-platform-intelligence.test.ts +352 -0
- package/tests/server-app-cron-owner.test.ts +127 -0
- package/tests/subscription-connect-race.test.ts +158 -0
- package/tests/subscription-dependency-context.test.ts +324 -0
- package/tests/subscription-manager-collection-tracking.test.ts +86 -0
- package/tests/subscription-manager-invalidation.test.ts +86 -0
- package/tests/support-runner-v5.test.ts +3201 -0
- package/tsconfig.json +34 -0
- package/ai/assistant-core-heuristics.d.ts +0 -11
- package/ai/assistant-core-heuristics.js +0 -356
- package/ai/assistant-core-heuristics.js.map +0 -1
- package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +0 -3
- package/ai/resolveio-platform-intelligence-memory-corpus.js +0 -214
- package/ai/resolveio-platform-intelligence-memory-corpus.js.map +0 -1
- package/ai/resolveio-platform-intelligence-memory.d.ts +0 -20
- package/ai/resolveio-platform-intelligence-memory.js +0 -341
- package/ai/resolveio-platform-intelligence-memory.js.map +0 -1
- package/ai/resolveio-platform-intelligence-types.js +0 -4
- package/ai/resolveio-platform-intelligence-types.js.map +0 -1
- package/ai/resolveio-platform-intelligence.d.ts +0 -6
- package/ai/resolveio-platform-intelligence.js +0 -463
- package/ai/resolveio-platform-intelligence.js.map +0 -1
- package/client-server-app.d.ts +0 -1
- package/client-server-app.js +0 -68
- package/client-server-app.js.map +0 -1
- package/collections/ai-run.collection.d.ts +0 -3
- package/collections/ai-run.collection.js +0 -170
- package/collections/ai-run.collection.js.map +0 -1
- package/collections/ai-terminal-conversation.collection.d.ts +0 -2
- package/collections/ai-terminal-conversation.collection.js +0 -140
- package/collections/ai-terminal-conversation.collection.js.map +0 -1
- package/collections/ai-terminal-issue-report.collection.d.ts +0 -2
- package/collections/ai-terminal-issue-report.collection.js +0 -148
- package/collections/ai-terminal-issue-report.collection.js.map +0 -1
- package/collections/ai-terminal-message.collection.d.ts +0 -2
- package/collections/ai-terminal-message.collection.js +0 -121
- package/collections/ai-terminal-message.collection.js.map +0 -1
- package/collections/app-setting.collection.d.ts +0 -3
- package/collections/app-setting.collection.js +0 -103
- package/collections/app-setting.collection.js.map +0 -1
- package/collections/app-status.collection.d.ts +0 -3
- package/collections/app-status.collection.js +0 -57
- package/collections/app-status.collection.js.map +0 -1
- package/collections/communication-metric.collection.d.ts +0 -2
- package/collections/communication-metric.collection.js +0 -133
- package/collections/communication-metric.collection.js.map +0 -1
- package/collections/counter.collection.d.ts +0 -3
- package/collections/counter.collection.js +0 -56
- package/collections/counter.collection.js.map +0 -1
- package/collections/cron-job-history.collection.d.ts +0 -3
- package/collections/cron-job-history.collection.js +0 -137
- package/collections/cron-job-history.collection.js.map +0 -1
- package/collections/cron-job.collection.d.ts +0 -3
- package/collections/cron-job.collection.js +0 -92
- package/collections/cron-job.collection.js.map +0 -1
- package/collections/customer-notification.collection.d.ts +0 -3
- package/collections/customer-notification.collection.js +0 -130
- package/collections/customer-notification.collection.js.map +0 -1
- package/collections/customer-portal-password.collection.d.ts +0 -3
- package/collections/customer-portal-password.collection.js +0 -75
- package/collections/customer-portal-password.collection.js.map +0 -1
- package/collections/email-history.collection.d.ts +0 -3
- package/collections/email-history.collection.js +0 -134
- package/collections/email-history.collection.js.map +0 -1
- package/collections/email-verified.collection.d.ts +0 -3
- package/collections/email-verified.collection.js +0 -62
- package/collections/email-verified.collection.js.map +0 -1
- package/collections/file.collection.d.ts +0 -3
- package/collections/file.collection.js +0 -74
- package/collections/file.collection.js.map +0 -1
- package/collections/flag-update.collection.d.ts +0 -3
- package/collections/flag-update.collection.js +0 -57
- package/collections/flag-update.collection.js.map +0 -1
- package/collections/flag.collection.d.ts +0 -3
- package/collections/flag.collection.js +0 -57
- package/collections/flag.collection.js.map +0 -1
- package/collections/log-method-latency.collection.d.ts +0 -3
- package/collections/log-method-latency.collection.js +0 -77
- package/collections/log-method-latency.collection.js.map +0 -1
- package/collections/log-subscription.collection.d.ts +0 -3
- package/collections/log-subscription.collection.js +0 -80
- package/collections/log-subscription.collection.js.map +0 -1
- package/collections/log.collection.d.ts +0 -3
- package/collections/log.collection.js +0 -93
- package/collections/log.collection.js.map +0 -1
- package/collections/logged-in-users.collection.d.ts +0 -3
- package/collections/logged-in-users.collection.js +0 -67
- package/collections/logged-in-users.collection.js.map +0 -1
- package/collections/monitor-cpu.collection.d.ts +0 -3
- package/collections/monitor-cpu.collection.js +0 -65
- package/collections/monitor-cpu.collection.js.map +0 -1
- package/collections/monitor-function.collection.d.ts +0 -3
- package/collections/monitor-function.collection.js +0 -74
- package/collections/monitor-function.collection.js.map +0 -1
- package/collections/monitor-memory.collection.d.ts +0 -3
- package/collections/monitor-memory.collection.js +0 -77
- package/collections/monitor-memory.collection.js.map +0 -1
- package/collections/monitor-mongo.collection.d.ts +0 -3
- package/collections/monitor-mongo.collection.js +0 -71
- package/collections/monitor-mongo.collection.js.map +0 -1
- package/collections/notification.collection.d.ts +0 -3
- package/collections/notification.collection.js +0 -57
- package/collections/notification.collection.js.map +0 -1
- package/collections/openai-usage-ledger.collection.d.ts +0 -2
- package/collections/openai-usage-ledger.collection.js +0 -188
- package/collections/openai-usage-ledger.collection.js.map +0 -1
- package/collections/report-builder-dashboard-builder.collection.d.ts +0 -3
- package/collections/report-builder-dashboard-builder.collection.js +0 -109
- package/collections/report-builder-dashboard-builder.collection.js.map +0 -1
- package/collections/report-builder-library.collection.d.ts +0 -3
- package/collections/report-builder-library.collection.js +0 -87
- package/collections/report-builder-library.collection.js.map +0 -1
- package/collections/report-builder-report.collection.d.ts +0 -4
- package/collections/report-builder-report.collection.js +0 -184
- package/collections/report-builder-report.collection.js.map +0 -1
- package/collections/user-group.collection.d.ts +0 -4
- package/collections/user-group.collection.js +0 -89
- package/collections/user-group.collection.js.map +0 -1
- package/collections/user-guide.collection.d.ts +0 -3
- package/collections/user-guide.collection.js +0 -57
- package/collections/user-guide.collection.js.map +0 -1
- package/collections/user.collection.d.ts +0 -4
- package/collections/user.collection.js +0 -180
- package/collections/user.collection.js.map +0 -1
- package/cron/cron.d.ts +0 -14
- package/cron/cron.js +0 -216
- package/cron/cron.js.map +0 -1
- package/fixtures/cron-jobs.d.ts +0 -1
- package/fixtures/cron-jobs.js +0 -150
- package/fixtures/cron-jobs.js.map +0 -1
- package/fixtures/init.d.ts +0 -1
- package/fixtures/init.js +0 -91
- package/fixtures/init.js.map +0 -1
- package/http/auth.d.ts +0 -2
- package/http/auth.js +0 -951
- package/http/auth.js.map +0 -1
- package/http/health.d.ts +0 -1
- package/http/health.js +0 -11
- package/http/health.js.map +0 -1
- package/http/home.d.ts +0 -1
- package/http/home.js +0 -134
- package/http/home.js.map +0 -1
- package/http/slow-query-publication.d.ts +0 -2
- package/http/slow-query-publication.js +0 -99
- package/http/slow-query-publication.js.map +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -19
- package/index.js.map +0 -1
- package/managers/ai-assistant-codex-manager.manager.d.ts +0 -67
- package/managers/ai-assistant-codex-manager.manager.js +0 -1113
- package/managers/ai-assistant-codex-manager.manager.js.map +0 -1
- package/managers/ai-run-evidence.manager.d.ts +0 -36
- package/managers/ai-run-evidence.manager.js +0 -377
- package/managers/ai-run-evidence.manager.js.map +0 -1
- package/managers/communication-metric.manager.d.ts +0 -16
- package/managers/communication-metric.manager.js +0 -134
- package/managers/communication-metric.manager.js.map +0 -1
- package/managers/cron.manager.d.ts +0 -20
- package/managers/cron.manager.js +0 -534
- package/managers/cron.manager.js.map +0 -1
- package/managers/customer-notification-content.manager.d.ts +0 -55
- package/managers/customer-notification-content.manager.js +0 -158
- package/managers/customer-notification-content.manager.js.map +0 -1
- package/managers/diagnostic-manager-bootstrap.d.ts +0 -9
- package/managers/diagnostic-manager-bootstrap.js +0 -260
- package/managers/diagnostic-manager-bootstrap.js.map +0 -1
- package/managers/error-auto-fix.manager.d.ts +0 -149
- package/managers/error-auto-fix.manager.js +0 -3064
- package/managers/error-auto-fix.manager.js.map +0 -1
- package/managers/local-log.manager.d.ts +0 -18
- package/managers/local-log.manager.js +0 -88
- package/managers/local-log.manager.js.map +0 -1
- package/managers/method.manager.d.ts +0 -84
- package/managers/method.manager.js +0 -1964
- package/managers/method.manager.js.map +0 -1
- package/managers/mongo.manager.d.ts +0 -224
- package/managers/mongo.manager.js +0 -5000
- package/managers/mongo.manager.js.map +0 -1
- package/managers/monitor.manager.d.ts +0 -70
- package/managers/monitor.manager.js +0 -550
- package/managers/monitor.manager.js.map +0 -1
- package/managers/openai-usage-ledger.manager.d.ts +0 -30
- package/managers/openai-usage-ledger.manager.js +0 -142
- package/managers/openai-usage-ledger.manager.js.map +0 -1
- package/managers/slow-query-verifier.manager.d.ts +0 -144
- package/managers/slow-query-verifier.manager.js +0 -3857
- package/managers/slow-query-verifier.manager.js.map +0 -1
- package/managers/slow-query.manager.d.ts +0 -28
- package/managers/slow-query.manager.js +0 -468
- package/managers/slow-query.manager.js.map +0 -1
- package/managers/subscription.manager.d.ts +0 -169
- package/managers/subscription.manager.js +0 -3434
- package/managers/subscription.manager.js.map +0 -1
- package/managers/websocket.manager.d.ts +0 -73
- package/managers/websocket.manager.js +0 -673
- package/managers/websocket.manager.js.map +0 -1
- package/managers/worker-dispatcher.manager.d.ts +0 -120
- package/managers/worker-dispatcher.manager.js +0 -1266
- package/managers/worker-dispatcher.manager.js.map +0 -1
- package/managers/worker-server.manager.d.ts +0 -35
- package/managers/worker-server.manager.js +0 -582
- package/managers/worker-server.manager.js.map +0 -1
- package/methods/accounts.d.ts +0 -2
- package/methods/accounts.js +0 -624
- package/methods/accounts.js.map +0 -1
- package/methods/ai-terminal.d.ts +0 -458
- package/methods/ai-terminal.js +0 -27991
- package/methods/ai-terminal.js.map +0 -1
- package/methods/app-settings.d.ts +0 -2
- package/methods/app-settings.js +0 -169
- package/methods/app-settings.js.map +0 -1
- package/methods/aws.d.ts +0 -2
- package/methods/aws.js +0 -877
- package/methods/aws.js.map +0 -1
- package/methods/collections.d.ts +0 -2
- package/methods/collections.js +0 -719
- package/methods/collections.js.map +0 -1
- package/methods/counters.d.ts +0 -2
- package/methods/counters.js +0 -113
- package/methods/counters.js.map +0 -1
- package/methods/cron-jobs.d.ts +0 -2
- package/methods/cron-jobs.js +0 -2475
- package/methods/cron-jobs.js.map +0 -1
- package/methods/customer-notifications.d.ts +0 -2
- package/methods/customer-notifications.js +0 -528
- package/methods/customer-notifications.js.map +0 -1
- package/methods/diagnostics.d.ts +0 -2
- package/methods/diagnostics.js +0 -703
- package/methods/diagnostics.js.map +0 -1
- package/methods/flag-updates.d.ts +0 -2
- package/methods/flag-updates.js +0 -8
- package/methods/flag-updates.js.map +0 -1
- package/methods/flags.d.ts +0 -2
- package/methods/flags.js +0 -8
- package/methods/flags.js.map +0 -1
- package/methods/logs.d.ts +0 -2
- package/methods/logs.js +0 -751
- package/methods/logs.js.map +0 -1
- package/methods/mongo-explorer.d.ts +0 -2
- package/methods/mongo-explorer.js +0 -1808
- package/methods/mongo-explorer.js.map +0 -1
- package/methods/monitor.d.ts +0 -2
- package/methods/monitor.js +0 -543
- package/methods/monitor.js.map +0 -1
- package/methods/pdf.d.ts +0 -2
- package/methods/pdf.js +0 -1216
- package/methods/pdf.js.map +0 -1
- package/methods/publications.d.ts +0 -1
- package/methods/publications.js +0 -183
- package/methods/publications.js.map +0 -1
- package/methods/report-builder.d.ts +0 -2
- package/methods/report-builder.js +0 -3094
- package/methods/report-builder.js.map +0 -1
- package/methods/support.d.ts +0 -2
- package/methods/support.js +0 -430
- package/methods/support.js.map +0 -1
- package/models/ai-run.model.d.ts +0 -19
- package/models/ai-run.model.js +0 -4
- package/models/ai-run.model.js.map +0 -1
- package/models/ai-terminal-conversation.model.d.ts +0 -17
- package/models/ai-terminal-conversation.model.js +0 -4
- package/models/ai-terminal-conversation.model.js.map +0 -1
- package/models/ai-terminal-issue-report.model.d.ts +0 -19
- package/models/ai-terminal-issue-report.model.js +0 -4
- package/models/ai-terminal-issue-report.model.js.map +0 -1
- package/models/ai-terminal-message.model.d.ts +0 -22
- package/models/ai-terminal-message.model.js +0 -4
- package/models/ai-terminal-message.model.js.map +0 -1
- package/models/app-setting.model.d.ts +0 -16
- package/models/app-setting.model.js +0 -4
- package/models/app-setting.model.js.map +0 -1
- package/models/app-status.model.js +0 -4
- package/models/app-status.model.js.map +0 -1
- package/models/billing-logged-in-users.model.js +0 -4
- package/models/billing-logged-in-users.model.js.map +0 -1
- package/models/collection-document.model.d.ts +0 -21
- package/models/collection-document.model.js +0 -4
- package/models/collection-document.model.js.map +0 -1
- package/models/communication-metric.model.d.ts +0 -20
- package/models/communication-metric.model.js +0 -4
- package/models/communication-metric.model.js.map +0 -1
- package/models/counter.model.js +0 -4
- package/models/counter.model.js.map +0 -1
- package/models/cron-job-history.model.d.ts +0 -15
- package/models/cron-job-history.model.js +0 -4
- package/models/cron-job-history.model.js.map +0 -1
- package/models/cron-job.model.d.ts +0 -14
- package/models/cron-job.model.js +0 -4
- package/models/cron-job.model.js.map +0 -1
- package/models/customer-notification.model.d.ts +0 -26
- package/models/customer-notification.model.js +0 -4
- package/models/customer-notification.model.js.map +0 -1
- package/models/customer-portal-password.model.d.ts +0 -11
- package/models/customer-portal-password.model.js +0 -4
- package/models/customer-portal-password.model.js.map +0 -1
- package/models/dialog.model.d.ts +0 -23
- package/models/dialog.model.js +0 -4
- package/models/dialog.model.js.map +0 -1
- package/models/email-history.model.d.ts +0 -32
- package/models/email-history.model.js.map +0 -1
- package/models/email-verified.model.js +0 -4
- package/models/email-verified.model.js.map +0 -1
- package/models/file.model.js +0 -4
- package/models/file.model.js.map +0 -1
- package/models/flag-update.model.js +0 -4
- package/models/flag-update.model.js.map +0 -1
- package/models/flag.model.js +0 -4
- package/models/flag.model.js.map +0 -1
- package/models/log-method-latency.model.d.ts +0 -10
- package/models/log-method-latency.model.js +0 -4
- package/models/log-method-latency.model.js.map +0 -1
- package/models/log-subscription.model.js +0 -4
- package/models/log-subscription.model.js.map +0 -1
- package/models/log.model.d.ts +0 -17
- package/models/log.model.js +0 -4
- package/models/log.model.js.map +0 -1
- package/models/logged-in-users.model.js +0 -4
- package/models/logged-in-users.model.js.map +0 -1
- package/models/method-response.model.js +0 -4
- package/models/method-response.model.js.map +0 -1
- package/models/method.model.d.ts +0 -26
- package/models/method.model.js +0 -4
- package/models/method.model.js.map +0 -1
- package/models/monitor-cpu.model.js +0 -4
- package/models/monitor-cpu.model.js.map +0 -1
- package/models/monitor-function.model.d.ts +0 -14
- package/models/monitor-function.model.js +0 -4
- package/models/monitor-function.model.js.map +0 -1
- package/models/monitor-memory.model.d.ts +0 -15
- package/models/monitor-memory.model.js +0 -4
- package/models/monitor-memory.model.js.map +0 -1
- package/models/monitor-mongo.model.d.ts +0 -13
- package/models/monitor-mongo.model.js +0 -4
- package/models/monitor-mongo.model.js.map +0 -1
- package/models/notification.model.js +0 -4
- package/models/notification.model.js.map +0 -1
- package/models/openai-usage-ledger.model.d.ts +0 -30
- package/models/openai-usage-ledger.model.js +0 -4
- package/models/openai-usage-ledger.model.js.map +0 -1
- package/models/pagination.model.d.ts +0 -11
- package/models/pagination.model.js +0 -28
- package/models/pagination.model.js.map +0 -1
- package/models/permission.model.d.ts +0 -12
- package/models/permission.model.js +0 -4
- package/models/permission.model.js.map +0 -1
- package/models/report-builder-dashboard-builder.model.d.ts +0 -25
- package/models/report-builder-dashboard-builder.model.js +0 -4
- package/models/report-builder-dashboard-builder.model.js.map +0 -1
- package/models/report-builder-library.model.d.ts +0 -17
- package/models/report-builder-library.model.js +0 -4
- package/models/report-builder-library.model.js.map +0 -1
- package/models/report-builder-report.model.d.ts +0 -121
- package/models/report-builder-report.model.js +0 -4
- package/models/report-builder-report.model.js.map +0 -1
- package/models/report-builder.model.d.ts +0 -61
- package/models/report-builder.model.js +0 -4
- package/models/report-builder.model.js.map +0 -1
- package/models/select-data-label.model.d.ts +0 -9
- package/models/select-data-label.model.js +0 -4
- package/models/select-data-label.model.js.map +0 -1
- package/models/server-message.model.d.ts +0 -32
- package/models/server-message.model.js +0 -4
- package/models/server-message.model.js.map +0 -1
- package/models/slow-query-report.model.d.ts +0 -23
- package/models/slow-query-report.model.js +0 -4
- package/models/slow-query-report.model.js.map +0 -1
- package/models/subscription.model.d.ts +0 -31
- package/models/subscription.model.js +0 -4
- package/models/subscription.model.js.map +0 -1
- package/models/support-ticket.model.d.ts +0 -87
- package/models/support-ticket.model.js +0 -4
- package/models/support-ticket.model.js.map +0 -1
- package/models/user-group.model.d.ts +0 -20
- package/models/user-group.model.js +0 -4
- package/models/user-group.model.js.map +0 -1
- package/models/user-guide.model.js +0 -4
- package/models/user-guide.model.js.map +0 -1
- package/models/user.model.d.ts +0 -84
- package/models/user.model.js +0 -4
- package/models/user.model.js.map +0 -1
- package/private/images/ResolveIO.png +0 -0
- package/public_api.js +0 -127
- package/public_api.js.map +0 -1
- package/publications/ai-terminal.d.ts +0 -1
- package/publications/ai-terminal.js +0 -122
- package/publications/ai-terminal.js.map +0 -1
- package/publications/app-settings.d.ts +0 -2
- package/publications/app-settings.js +0 -28
- package/publications/app-settings.js.map +0 -1
- package/publications/app-status.d.ts +0 -2
- package/publications/app-status.js +0 -16
- package/publications/app-status.js.map +0 -1
- package/publications/cron-jobs.d.ts +0 -2
- package/publications/cron-jobs.js +0 -88
- package/publications/cron-jobs.js.map +0 -1
- package/publications/customer-notifications.d.ts +0 -2
- package/publications/customer-notifications.js +0 -161
- package/publications/customer-notifications.js.map +0 -1
- package/publications/files.d.ts +0 -2
- package/publications/files.js +0 -36
- package/publications/files.js.map +0 -1
- package/publications/flags-update.d.ts +0 -2
- package/publications/flags-update.js +0 -22
- package/publications/flags-update.js.map +0 -1
- package/publications/flags.d.ts +0 -2
- package/publications/flags.js +0 -22
- package/publications/flags.js.map +0 -1
- package/publications/logs.d.ts +0 -2
- package/publications/logs.js +0 -164
- package/publications/logs.js.map +0 -1
- package/publications/notifications.d.ts +0 -2
- package/publications/notifications.js +0 -16
- package/publications/notifications.js.map +0 -1
- package/publications/report-builder-dashboard-builders.d.ts +0 -2
- package/publications/report-builder-dashboard-builders.js +0 -42
- package/publications/report-builder-dashboard-builders.js.map +0 -1
- package/publications/report-builder-libraries.d.ts +0 -2
- package/publications/report-builder-libraries.js +0 -90
- package/publications/report-builder-libraries.js.map +0 -1
- package/publications/report-builder-reports.d.ts +0 -2
- package/publications/report-builder-reports.js +0 -50
- package/publications/report-builder-reports.js.map +0 -1
- package/publications/super-admin.d.ts +0 -2
- package/publications/super-admin.js +0 -16
- package/publications/super-admin.js.map +0 -1
- package/publications/user-groups.d.ts +0 -1
- package/publications/user-groups.js +0 -16
- package/publications/user-groups.js.map +0 -1
- package/publications/user-guides.d.ts +0 -1
- package/publications/user-guides.js +0 -16
- package/publications/user-guides.js.map +0 -1
- package/resolveio-server-app.d.ts +0 -70
- package/resolveio-server-app.js +0 -801
- package/resolveio-server-app.js.map +0 -1
- package/server-app.d.ts +0 -228
- package/server-app.js +0 -3566
- package/server-app.js.map +0 -1
- package/services/codex-client.d.ts +0 -128
- package/services/codex-client.js +0 -1629
- package/services/codex-client.js.map +0 -1
- package/services/openai-client.d.ts +0 -46
- package/services/openai-client.js +0 -318
- package/services/openai-client.js.map +0 -1
- package/types/error-report.d.ts +0 -25
- package/types/error-report.js +0 -4
- package/types/error-report.js.map +0 -1
- package/types/slow-query-report.d.ts +0 -27
- package/types/slow-query-report.js +0 -6
- package/types/slow-query-report.js.map +0 -1
- package/util/ai-qa-policy.d.ts +0 -124
- package/util/ai-qa-policy.js +0 -736
- package/util/ai-qa-policy.js.map +0 -1
- package/util/ai-run-evidence-adapters.d.ts +0 -109
- package/util/ai-run-evidence-adapters.js +0 -7234
- package/util/ai-run-evidence-adapters.js.map +0 -1
- package/util/ai-run-evidence-dashboard.d.ts +0 -88
- package/util/ai-run-evidence-dashboard.js +0 -343
- package/util/ai-run-evidence-dashboard.js.map +0 -1
- package/util/ai-run-evidence-eval.d.ts +0 -86
- package/util/ai-run-evidence-eval.js +0 -1018
- package/util/ai-run-evidence-eval.js.map +0 -1
- package/util/ai-run-evidence.d.ts +0 -244
- package/util/ai-run-evidence.js +0 -1096
- package/util/ai-run-evidence.js.map +0 -1
- package/util/ai-runner-artifacts.d.ts +0 -82
- package/util/ai-runner-artifacts.js +0 -713
- package/util/ai-runner-artifacts.js.map +0 -1
- package/util/ai-runner-manager-autopilot.d.ts +0 -210
- package/util/ai-runner-manager-autopilot.js +0 -642
- package/util/ai-runner-manager-autopilot.js.map +0 -1
- package/util/ai-runner-manager-policy.d.ts +0 -807
- package/util/ai-runner-manager-policy.js +0 -3501
- package/util/ai-runner-manager-policy.js.map +0 -1
- package/util/ai-runner-qa-auth.d.ts +0 -5
- package/util/ai-runner-qa-auth.js +0 -839
- package/util/ai-runner-qa-auth.js.map +0 -1
- package/util/ai-runner-qa-tools.d.ts +0 -26
- package/util/ai-runner-qa-tools.js +0 -3520
- package/util/ai-runner-qa-tools.js.map +0 -1
- package/util/aicoder-runner-v6.d.ts +0 -426
- package/util/aicoder-runner-v6.js +0 -2464
- package/util/aicoder-runner-v6.js.map +0 -1
- package/util/common.d.ts +0 -31
- package/util/common.js +0 -683
- package/util/common.js.map +0 -1
- package/util/customer-portal-password.d.ts +0 -13
- package/util/customer-portal-password.js +0 -209
- package/util/customer-portal-password.js.map +0 -1
- package/util/error-reporter.d.ts +0 -52
- package/util/error-reporter.js +0 -326
- package/util/error-reporter.js.map +0 -1
- package/util/error-tracking.d.ts +0 -13
- package/util/error-tracking.js +0 -120
- package/util/error-tracking.js.map +0 -1
- package/util/openai-usage-cost.d.ts +0 -6
- package/util/openai-usage-cost.js +0 -103
- package/util/openai-usage-cost.js.map +0 -1
- package/util/report-builder-unwinds.d.ts +0 -15
- package/util/report-builder-unwinds.js +0 -156
- package/util/report-builder-unwinds.js.map +0 -1
- package/util/runner-process-janitor.d.ts +0 -27
- package/util/runner-process-janitor.js +0 -208
- package/util/runner-process-janitor.js.map +0 -1
- package/util/schema-report-builder.d.ts +0 -6
- package/util/schema-report-builder.js +0 -481
- package/util/schema-report-builder.js.map +0 -1
- package/util/slow-query-reporter.d.ts +0 -28
- package/util/slow-query-reporter.js +0 -226
- package/util/slow-query-reporter.js.map +0 -1
- package/util/subscription-dependency-context.d.ts +0 -34
- package/util/subscription-dependency-context.js +0 -1283
- package/util/subscription-dependency-context.js.map +0 -1
- package/util/support-runner-v5.d.ts +0 -1426
- package/util/support-runner-v5.js +0 -7624
- package/util/support-runner-v5.js.map +0 -1
- package/util/tokenizer.d.ts +0 -5
- package/util/tokenizer.js +0 -41
- package/util/tokenizer.js.map +0 -1
- package/workers/codex-runner.worker.d.ts +0 -1
- package/workers/codex-runner.worker.js +0 -192
- package/workers/codex-runner.worker.js.map +0 -1
- /package/{private → src/private}/email-templates/enrollment.html +0 -0
- /package/{private → src/private}/email-templates/forgot-password.html +0 -0
- /package/{private → src/private}/email-templates/support-ticket-deleted.html +0 -0
- /package/{private → src/private}/email-templates/support-ticket-modified.html +0 -0
- /package/{private → src/private}/email-templates/support-ticket.html +0 -0
- /package/{public_api.d.ts → src/public_api.ts} +0 -0
|
@@ -0,0 +1,3128 @@
|
|
|
1
|
+
import { ChangeStream, ChangeStreamDeleteDocument, ChangeStreamInsertDocument, ChangeStreamReplaceDocument, ChangeStreamUpdateDocument, ResumeToken } from 'mongodb';
|
|
2
|
+
import * as NodeCache from 'node-cache';
|
|
3
|
+
import { pack, unpack } from 'msgpackr';
|
|
4
|
+
import * as WebSocket from 'ws';
|
|
5
|
+
import { Flags } from '../collections/flag.collection';
|
|
6
|
+
import { LoggedInUsers } from '../collections/logged-in-users.collection';
|
|
7
|
+
import { LoggedInUserModel } from '../models/logged-in-users.model';
|
|
8
|
+
import { ServerResponseModel } from '../models/server-message.model';
|
|
9
|
+
import { ActiveSubscriptionClientModel, ActiveSubscriptionModel, SubscriptionModel, SubscriptionPubModel } from '../models/subscription.model';
|
|
10
|
+
import { loadAppStatusPublications } from '../publications/app-status';
|
|
11
|
+
import { loadAppSettingsPublications } from '../publications/app-settings';
|
|
12
|
+
import { loadAiTerminalPublications } from '../publications/ai-terminal';
|
|
13
|
+
import { loadCronJobPublications } from '../publications/cron-jobs';
|
|
14
|
+
import { loadCustomerNotificationPublications } from '../publications/customer-notifications';
|
|
15
|
+
import { loadFilePublications } from '../publications/files';
|
|
16
|
+
import { loadFlagsPublications } from '../publications/flags';
|
|
17
|
+
import { loadFlagsUpdatePublications } from '../publications/flags-update';
|
|
18
|
+
import { loadLogPublications } from '../publications/logs';
|
|
19
|
+
import { loadNotificationPublications } from '../publications/notifications';
|
|
20
|
+
import { loadReportBuilderDashboardBuilderPublications } from '../publications/report-builder-dashboard-builders';
|
|
21
|
+
import { loadReportBuilderLibraryPublications } from '../publications/report-builder-libraries';
|
|
22
|
+
import { loadReportBuilderReportPublications } from '../publications/report-builder-reports';
|
|
23
|
+
import { loadSuperAdminPublications } from '../publications/super-admin';
|
|
24
|
+
import { loadUserGroupPublications } from '../publications/user-groups';
|
|
25
|
+
import { loadUserGuidePublications } from '../publications/user-guides';
|
|
26
|
+
import { ResolveIOServer } from '../resolveio-server-app';
|
|
27
|
+
import { deepCopy, objectIdHexString, round } from '../util/common';
|
|
28
|
+
import { ErrorReporter } from '../util/error-reporter';
|
|
29
|
+
import { ensureErrorWithCorrelation } from '../util/error-tracking';
|
|
30
|
+
import { DependencyContextSnapshot, PublicationContext, QueryMeta, deserializeDependencySnapshot, withDependencyTracking } from '../util/subscription-dependency-context';
|
|
31
|
+
import { MonitorManagerFunction } from './monitor.manager';
|
|
32
|
+
import { WebSocketManager } from './websocket.manager';
|
|
33
|
+
const v8 = require('v8');
|
|
34
|
+
const sift = require('sift');
|
|
35
|
+
|
|
36
|
+
function resolveHeapLimitBytes(): number {
|
|
37
|
+
const stats = v8.getHeapStatistics();
|
|
38
|
+
if (stats && typeof stats.heap_size_limit === 'number') {
|
|
39
|
+
return stats.heap_size_limit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (stats && typeof stats.total_available_size === 'number') {
|
|
43
|
+
return stats.total_available_size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Performance Dependencies
|
|
50
|
+
// import * as path from 'path';
|
|
51
|
+
// import { Worker } from 'worker_threads';
|
|
52
|
+
|
|
53
|
+
interface MongoQueueModel {
|
|
54
|
+
_id: number,
|
|
55
|
+
type: string;
|
|
56
|
+
collection: string;
|
|
57
|
+
subscription: ActiveSubscriptionModel;
|
|
58
|
+
running: boolean;
|
|
59
|
+
run_again: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface OplogResumeTokenDocument {
|
|
63
|
+
_id: string;
|
|
64
|
+
token?: ResumeToken;
|
|
65
|
+
updatedAt?: Date;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SubscriptionInvalidationEvent {
|
|
69
|
+
type: string;
|
|
70
|
+
documentId?: any;
|
|
71
|
+
document?: any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PublicationExecution {
|
|
75
|
+
result?: any;
|
|
76
|
+
packedResult?: Uint8Array | Buffer;
|
|
77
|
+
snapshot?: DependencyContextSnapshot;
|
|
78
|
+
encoding?: 'msgpack' | 'json';
|
|
79
|
+
workerUsed?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// interface CurrentPerformanceMonitor {
|
|
83
|
+
// _id: number;
|
|
84
|
+
// function: string;
|
|
85
|
+
// publication: string;
|
|
86
|
+
// subscriptionData: any[];
|
|
87
|
+
// date_start: Date;
|
|
88
|
+
// date_end: Date;
|
|
89
|
+
// duration: number;
|
|
90
|
+
// result: string;
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
export class SubscriptionManager {
|
|
94
|
+
private _websocketManager: WebSocketManager;
|
|
95
|
+
private _publications: SubscriptionModel = {};
|
|
96
|
+
private _subscriptions: ActiveSubscriptionModel[] = [];
|
|
97
|
+
private _wss: WebSocket.Server;
|
|
98
|
+
private _loggedInUsers: LoggedInUserModel[] = [];
|
|
99
|
+
private _lastRouteBySocket = new Map<string, { route: string, dateMs: number }>();
|
|
100
|
+
|
|
101
|
+
private _mongoQueue: MongoQueueModel[] = [];
|
|
102
|
+
private _mongoQueueId = 0;
|
|
103
|
+
|
|
104
|
+
private _oplog$: ChangeStream;
|
|
105
|
+
private _oplogMode: 'auto' | 'change-stream' | 'local' = 'auto';
|
|
106
|
+
private _useLocalOplog = false;
|
|
107
|
+
private _localOplogResyncIntervalMs = 0;
|
|
108
|
+
private _localOplogResyncTimer: NodeJS.Timeout = null;
|
|
109
|
+
|
|
110
|
+
private _nodeCache;
|
|
111
|
+
private _cacheId = 1;
|
|
112
|
+
|
|
113
|
+
private _heapSize = resolveHeapLimitBytes();
|
|
114
|
+
private _heapLimit: number;
|
|
115
|
+
|
|
116
|
+
private serverConfig;
|
|
117
|
+
|
|
118
|
+
private _monitorManagerFunction: MonitorManagerFunction;
|
|
119
|
+
|
|
120
|
+
private _enableDebug = false;
|
|
121
|
+
private _enableDependencyDebug = false;
|
|
122
|
+
private _connectDebug = false;
|
|
123
|
+
private _debugOplogCollections = [];
|
|
124
|
+
private _debugOplogHits = 0;
|
|
125
|
+
private _debugSubCollections = [];
|
|
126
|
+
private _debugSubHits = 0;
|
|
127
|
+
private _debugUnSubHits = 0;
|
|
128
|
+
private _debugUnSubAllHits = 0;
|
|
129
|
+
private _debugMongoQueueHits = 0;
|
|
130
|
+
private _debugMongoQueueCollections = [];
|
|
131
|
+
private _debugSendQueueHits = 0;
|
|
132
|
+
private _debugRemoveCacheHits = 0;
|
|
133
|
+
private _subSendDebug = false;
|
|
134
|
+
private _subSendLogEnabled = false;
|
|
135
|
+
private _subSendLogThresholdMs = 0;
|
|
136
|
+
private _subSendLogBytes = 0;
|
|
137
|
+
private _subSendLogSampleRate = 1;
|
|
138
|
+
private _aiWorkerDebug = false;
|
|
139
|
+
private _aiSubscriptionDebug = false;
|
|
140
|
+
private _aiSubLastAt = new Map<string, number>();
|
|
141
|
+
|
|
142
|
+
private _oplogRetryCount = 0;
|
|
143
|
+
private _lastResumeToken: ResumeToken = null;
|
|
144
|
+
private _lastResumeTokenSaveMs = 0;
|
|
145
|
+
private _resumeTokenSavePromise: Promise<void> = null;
|
|
146
|
+
private _resumeTokenSaveForcePending = false;
|
|
147
|
+
private _fullResyncPromise: Promise<void> = null;
|
|
148
|
+
private readonly RESUME_TOKEN_COLLECTION = 'subscription-manager-resume-tokens';
|
|
149
|
+
private readonly RESUME_TOKEN_SAVE_INTERVAL_MS = 5000;
|
|
150
|
+
private readonly RESUME_TOKEN_AUTO_HEAL_RETRY_THRESHOLD = 2;
|
|
151
|
+
|
|
152
|
+
// Buffer to store throttled latency updates with timestamps
|
|
153
|
+
private latencyBuffer = new Map<string, { latency: number, lastUpdate: Date }>();
|
|
154
|
+
private _latencyFlushInProgress = false;
|
|
155
|
+
private _latencyFlushPending = false;
|
|
156
|
+
|
|
157
|
+
// Interval to flush latency updates in MongoDB
|
|
158
|
+
private readonly LATENCY_UPDATE_INTERVAL = 60000;
|
|
159
|
+
|
|
160
|
+
// Minimum time difference between two latency updates for the same user
|
|
161
|
+
private readonly LATENCY_UPDATE_THRESHOLD_MS = 30000;
|
|
162
|
+
private readonly LATENCY_DEBUG_THRESHOLD_MS = 1000;
|
|
163
|
+
private readonly PUBLICATION_DEBUG_THRESHOLD_MS = 300;
|
|
164
|
+
|
|
165
|
+
// private currentPerfomanceMonitor: CurrentPerformanceMonitor[] = [];
|
|
166
|
+
// private idPerformance: number = 0;
|
|
167
|
+
// private performanceThread;
|
|
168
|
+
|
|
169
|
+
private _invalidationDebounceTimers = new Map<string, NodeJS.Timeout>();
|
|
170
|
+
private _invalidationPendingTimestamps = new Map<string, number>();
|
|
171
|
+
private _pendingInvalidations = new Map<string, { events: SubscriptionInvalidationEvent[] }>();
|
|
172
|
+
private readonly DEBOUNCE_DELAY = 100; // 100ms debounce window
|
|
173
|
+
private readonly MAX_WAIT_TIME = 500; // 500ms maximum delay
|
|
174
|
+
|
|
175
|
+
private _publicationWorkerQueueLimit = 0;
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
constructor() {}
|
|
179
|
+
|
|
180
|
+
static createPublicationRegistry(serverConfig?) {
|
|
181
|
+
const subscriptionManager = new SubscriptionManager();
|
|
182
|
+
subscriptionManager.initializePublicationRegistry(serverConfig);
|
|
183
|
+
return subscriptionManager;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static create(wss: WebSocket.Server, serverConfig, monitorManagerFunction: MonitorManagerFunction) {
|
|
187
|
+
const subscriptionManager = new SubscriptionManager();
|
|
188
|
+
setImmediate(async () => {
|
|
189
|
+
await subscriptionManager.initialize(wss, serverConfig, monitorManagerFunction);
|
|
190
|
+
});
|
|
191
|
+
return subscriptionManager;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private initializePublicationRegistry(serverConfig) {
|
|
195
|
+
this.serverConfig = serverConfig || ResolveIOServer.getServerConfig();
|
|
196
|
+
this.registerCorePublications();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async initialize(wss: WebSocket.Server, serverConfig, monitorManagerFunction: MonitorManagerFunction) {
|
|
200
|
+
this._websocketManager = ResolveIOServer.getMainServer().getWebSocketManager();
|
|
201
|
+
this._monitorManagerFunction = monitorManagerFunction;
|
|
202
|
+
|
|
203
|
+
this._nodeCache = new NodeCache( { stdTTL: 0, checkperiod: 0 } );
|
|
204
|
+
|
|
205
|
+
setInterval(() => this.flushThrottledLatencyUpdates(), this.LATENCY_UPDATE_INTERVAL);
|
|
206
|
+
|
|
207
|
+
// setTimeout(() => {
|
|
208
|
+
// console.log('Setting up performance thread');
|
|
209
|
+
|
|
210
|
+
// this.performanceThread = new Worker(path.join(__dirname, './subscription.performance.js'));
|
|
211
|
+
|
|
212
|
+
// this.performanceThread.on('exit', code => {
|
|
213
|
+
// console.error(new Date(), 'THREAD EXITED!!!!!!!!!!!!!!!!!!', code);
|
|
214
|
+
// });
|
|
215
|
+
|
|
216
|
+
// this.performanceThread.on('error', code => {
|
|
217
|
+
// console.error(new Date(), 'THREAD RECV ERROR !!!!!!!!!!!!!!!!!!', code);
|
|
218
|
+
// });
|
|
219
|
+
// }, 5000);
|
|
220
|
+
|
|
221
|
+
// setInterval(() => {
|
|
222
|
+
// console.log('Post thread msg');
|
|
223
|
+
// this.performanceThread.postMessage(this.currentPerfomanceMonitor);
|
|
224
|
+
// this.currentPerfomanceMonitor = [];
|
|
225
|
+
// }, 10000);
|
|
226
|
+
|
|
227
|
+
this.serverConfig = serverConfig;
|
|
228
|
+
this._wss = wss;
|
|
229
|
+
this._connectDebug = this.resolveConnectDebug();
|
|
230
|
+
this._subSendDebug = this.resolveSubSendDebug();
|
|
231
|
+
this._subSendLogEnabled = this.resolveSubSendLogEnabled();
|
|
232
|
+
this._subSendLogThresholdMs = this.resolveSubSendLogThresholdMs();
|
|
233
|
+
this._subSendLogBytes = this.resolveSubSendLogBytes();
|
|
234
|
+
this._subSendLogSampleRate = this.resolveSubSendLogSampleRate();
|
|
235
|
+
this._aiWorkerDebug = this.parseDebugFlag(process.env.AI_ASSISTANT_WORKER_DEBUG);
|
|
236
|
+
this._aiSubscriptionDebug = this.resolveAiSubscriptionDebug();
|
|
237
|
+
|
|
238
|
+
this._oplogMode = this.resolveOplogMode();
|
|
239
|
+
this._localOplogResyncIntervalMs = this.resolveLocalOplogResyncIntervalMs();
|
|
240
|
+
|
|
241
|
+
this.registerCorePublications();
|
|
242
|
+
|
|
243
|
+
if (this._oplogMode === 'local') {
|
|
244
|
+
await this.enableLocalOplogFallback('config');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const resumeToken = await this.loadResumeToken();
|
|
248
|
+
await this.tailOpLog(resumeToken || undefined);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setInterval(() => {
|
|
252
|
+
this._oplogRetryCount = 0;
|
|
253
|
+
}, 15000);
|
|
254
|
+
|
|
255
|
+
setInterval(() => {
|
|
256
|
+
if (this.getEnableDebug()) {
|
|
257
|
+
console.log(new Date(), 'Sub Manager', 'Subs', this._subscriptions.length);
|
|
258
|
+
console.log(new Date(), 'Sub Manager', 'Logged In Users', this._loggedInUsers.length);
|
|
259
|
+
console.log(new Date(), 'Sub Manager', 'Mongo Queue', this._mongoQueue.length);
|
|
260
|
+
console.log(new Date(), 'Sub Manager', 'Mongo Queue Hits', this._debugMongoQueueHits);
|
|
261
|
+
console.log(new Date(), 'Sub Manager', 'Mongo Queue Collections', JSON.stringify(this._debugMongoQueueCollections.sort((a, b) => a.collection.localeCompare(b.collection) || a.publication.localeCompare(b.publication)), null, 2));
|
|
262
|
+
console.log(new Date(), 'Sub Manager', 'Oplog Hits', this._debugOplogHits);
|
|
263
|
+
console.log(new Date(), 'Sub Manager', 'Oplog Collections', JSON.stringify(this._debugOplogCollections.sort((a, b) => a.collection.localeCompare(b.collection) || a.type.localeCompare(b.type)), null, 2));
|
|
264
|
+
console.log(new Date(), 'Sub Manager', 'Send Queue Hits', this._debugSendQueueHits);
|
|
265
|
+
console.log(new Date(), 'Sub Manager', 'Sub Hits', this._debugSubHits);
|
|
266
|
+
console.log(new Date(), 'Sub Manager', 'Sub Collections', JSON.stringify(this._debugSubCollections.sort((a, b) => a.publication.localeCompare(b.publication)), null, 2));
|
|
267
|
+
console.log(new Date(), 'Sub Manager', 'Unsub Hits', this._debugUnSubHits);
|
|
268
|
+
console.log(new Date(), 'Sub Manager', 'Unsub All Hits', this._debugUnSubAllHits);
|
|
269
|
+
console.log(new Date(), 'Sub Manager', 'Cache Cleanup Hits', this._debugRemoveCacheHits);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this._debugOplogHits = 0;
|
|
273
|
+
this._debugOplogCollections = [];
|
|
274
|
+
this._debugSubCollections = [];
|
|
275
|
+
this._debugMongoQueueHits = 0;
|
|
276
|
+
this._debugMongoQueueCollections = [];
|
|
277
|
+
this._debugSendQueueHits = 0;
|
|
278
|
+
this._debugSubHits = 0;
|
|
279
|
+
this._debugUnSubHits = 0;
|
|
280
|
+
this._debugUnSubAllHits = 0;
|
|
281
|
+
this._debugRemoveCacheHits = 0;
|
|
282
|
+
}, 60000);
|
|
283
|
+
|
|
284
|
+
setInterval(async () => {
|
|
285
|
+
this._loggedInUsers = await LoggedInUsers.find();
|
|
286
|
+
|
|
287
|
+
let userCopy = deepCopy(this._loggedInUsers);
|
|
288
|
+
for (let i = this._loggedInUsers.length - 1; i >= 0; i--) {
|
|
289
|
+
let loggedInUser = userCopy[i];
|
|
290
|
+
|
|
291
|
+
if (!loggedInUser.date || Date.now() - loggedInUser.date.getTime() > 120000) {
|
|
292
|
+
if (this._websocketManager.getWebSocket(loggedInUser.id_ws)) {
|
|
293
|
+
if (this.getEnableDebug()) {
|
|
294
|
+
console.log(new Date(), 'Sub Manager', 'Unsub WS', this._websocketManager.getWebSocket(loggedInUser.id_ws)['user'], this._websocketManager.getWebSocket(loggedInUser.id_ws)['id_socket'], 2);
|
|
295
|
+
}
|
|
296
|
+
await this.unsubscribeAll(this._websocketManager.getWebSocket(loggedInUser.id_ws));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
this._subscriptions.forEach(sub => {
|
|
300
|
+
for (let j = sub.clients.length - 1; j >= 0; j--) {
|
|
301
|
+
let client = sub.clients[j];
|
|
302
|
+
|
|
303
|
+
if (client.id_socket === loggedInUser.id_ws) {
|
|
304
|
+
sub.clients.splice(j, 1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await LoggedInUsers.deleteOne({_id: loggedInUser._id});
|
|
310
|
+
|
|
311
|
+
if (this._loggedInUsers.findIndex(a => a._id === loggedInUser._id) >= 0) {
|
|
312
|
+
this._loggedInUsers.splice(this._loggedInUsers.findIndex(a => a._id === loggedInUser._id), 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < this._subscriptions.length; i++) {
|
|
319
|
+
let sub = this._subscriptions[i];
|
|
320
|
+
|
|
321
|
+
for (let j = sub.clients.length - 1; j >= 0; j--) {
|
|
322
|
+
let client = sub.clients[j];
|
|
323
|
+
|
|
324
|
+
if (!this._loggedInUsers.some(a => a.id_ws === client.id_socket)) {
|
|
325
|
+
sub.clients.splice(j, 1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}, 30000);
|
|
330
|
+
|
|
331
|
+
let flag = await Flags.findOne({type: 'Enable Debug'});
|
|
332
|
+
let dependencyFlag = await Flags.findOne({type: 'Enable Dependency Debug'});
|
|
333
|
+
|
|
334
|
+
if (flag && flag.value) {
|
|
335
|
+
this._enableDebug = true;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this._enableDebug = false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (dependencyFlag && dependencyFlag.value) {
|
|
342
|
+
this._enableDependencyDebug = true;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
this._enableDependencyDebug = process.env.ENABLE_DEPENDENCY_DEBUG === 'true';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.setCacheLimit();
|
|
349
|
+
this.configurePublicationWorkers();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private registerCorePublications() {
|
|
353
|
+
loadSuperAdminPublications(this);
|
|
354
|
+
loadAiTerminalPublications(this);
|
|
355
|
+
loadAppStatusPublications(this);
|
|
356
|
+
loadAppSettingsPublications(this);
|
|
357
|
+
loadLogPublications(this);
|
|
358
|
+
loadFilePublications(this);
|
|
359
|
+
loadCronJobPublications(this);
|
|
360
|
+
loadFlagsUpdatePublications(this);
|
|
361
|
+
loadFlagsPublications(this);
|
|
362
|
+
loadNotificationPublications(this);
|
|
363
|
+
loadCustomerNotificationPublications(this);
|
|
364
|
+
loadReportBuilderReportPublications(this);
|
|
365
|
+
loadReportBuilderLibraryPublications(this);
|
|
366
|
+
loadUserGroupPublications(this);
|
|
367
|
+
loadUserGuidePublications(this);
|
|
368
|
+
loadReportBuilderDashboardBuilderPublications(this);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private setCacheLimit() {
|
|
372
|
+
if (process.env.IS_WORKERS_ENABLED === 'true') {
|
|
373
|
+
this._heapLimit = this._heapSize * 0.4;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
this._heapLimit = this._heapSize * 0.3; // Use 50% of total heap size
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private configurePublicationWorkers() {
|
|
381
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
382
|
+
this._publicationWorkerQueueLimit = this.parsePositiveNumber(process.env.SUBSCRIPTION_WORKER_QUEUE_MAX ?? config['SUBSCRIPTION_WORKER_QUEUE_MAX']);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private parseBoolean(value: any): boolean {
|
|
386
|
+
if (value === true) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (value === false || value === null || value === undefined) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (typeof value === 'number') {
|
|
395
|
+
return value === 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (typeof value === 'string') {
|
|
399
|
+
const normalized = value.trim().toLowerCase();
|
|
400
|
+
return ['1', 'true', 'yes', 'y', 'on'].includes(normalized);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private clampNumber(value: number, min: number, max: number): number {
|
|
407
|
+
if (value < min) {
|
|
408
|
+
return min;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (value > max) {
|
|
412
|
+
return max;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public async invalidatePubsCache(collection: string, type: string, documentId?: any, document?: any) {
|
|
419
|
+
let queue = this._pendingInvalidations.get(collection);
|
|
420
|
+
if (!queue) {
|
|
421
|
+
queue = { events: [] };
|
|
422
|
+
this._pendingInvalidations.set(collection, queue);
|
|
423
|
+
}
|
|
424
|
+
queue.events.push({ type, documentId, document });
|
|
425
|
+
|
|
426
|
+
const debounceKey = collection;
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
|
|
429
|
+
// Initialize or get existing timestamp
|
|
430
|
+
const firstInvalidationTime = this._invalidationPendingTimestamps.get(debounceKey) || now;
|
|
431
|
+
this._invalidationPendingTimestamps.set(debounceKey, firstInvalidationTime);
|
|
432
|
+
|
|
433
|
+
// Clear any existing timer
|
|
434
|
+
if (this._invalidationDebounceTimers.has(debounceKey)) {
|
|
435
|
+
clearTimeout(this._invalidationDebounceTimers.get(debounceKey));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check if we've waited too long
|
|
439
|
+
const waitedTooLong = (now - firstInvalidationTime) >= this.MAX_WAIT_TIME;
|
|
440
|
+
|
|
441
|
+
if (waitedTooLong) {
|
|
442
|
+
// Immediate execution path
|
|
443
|
+
this._invalidationPendingTimestamps.delete(debounceKey);
|
|
444
|
+
await this._executeInvalidation(collection);
|
|
445
|
+
} else {
|
|
446
|
+
// Normal debounce path
|
|
447
|
+
this._invalidationDebounceTimers.set(
|
|
448
|
+
debounceKey,
|
|
449
|
+
setTimeout(async () => {
|
|
450
|
+
this._invalidationPendingTimestamps.delete(debounceKey);
|
|
451
|
+
await this._executeInvalidation(collection);
|
|
452
|
+
}, this.DEBOUNCE_DELAY)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async _executeInvalidation(collection: string) {
|
|
458
|
+
// Clean up any existing timer (defensive)
|
|
459
|
+
if (this._invalidationDebounceTimers.has(collection)) {
|
|
460
|
+
clearTimeout(this._invalidationDebounceTimers.get(collection));
|
|
461
|
+
this._invalidationDebounceTimers.delete(collection);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const queued = this._pendingInvalidations.get(collection);
|
|
465
|
+
this._pendingInvalidations.delete(collection);
|
|
466
|
+
|
|
467
|
+
if (!queued || !queued.events.length) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const events = queued.events;
|
|
472
|
+
|
|
473
|
+
// Original invalidation logic
|
|
474
|
+
ResolveIOServer.getMongoManager().invalidateQueryCache(collection, true);
|
|
475
|
+
|
|
476
|
+
const collSubs = this._subscriptions.filter(sub => this.subscriptionReferencesCollection(sub, collection));
|
|
477
|
+
|
|
478
|
+
for (const sub of collSubs) {
|
|
479
|
+
if (this._enableDebug) {
|
|
480
|
+
console.log(new Date(), 'Invalidate Sub', sub.publication, sub.running, sub.runAgain);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!(await this.shouldInvalidateSubscriptionForEvents(sub, collection, events))) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (sub.running) {
|
|
488
|
+
sub.runAgain = true;
|
|
489
|
+
this.dependencyDebug('Subscription busy, scheduling rerun', { publication: sub.publication, collection, events: this.summarizeEvents(events) });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const batchType = events.length === 1 ? events[0].type : events.map(event => event.type).join(',');
|
|
494
|
+
|
|
495
|
+
if (this._publications[sub.publication].user_specific) {
|
|
496
|
+
this.dependencyDebug('Triggering user-specific invalidation', { publication: sub.publication, collection, events: this.summarizeEvents(events), clientCount: sub.clients.length });
|
|
497
|
+
for (let client of sub.clients) {
|
|
498
|
+
const ws = this._websocketManager.getWebSocket(client.id_socket);
|
|
499
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
500
|
+
try {
|
|
501
|
+
await this.sendDataToOneWithRetry(ws, client.messageId, sub, collection, batchType);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Error handling
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
this.dependencyDebug('Triggering broadcast invalidation', { publication: sub.publication, collection, events: this.summarizeEvents(events), clientCount: sub.clients.length });
|
|
511
|
+
await this.sendDataToAllWithRetry(sub, collection, batchType);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private subscriptionReferencesCollection(sub: ActiveSubscriptionModel, collection: string): boolean {
|
|
517
|
+
if (sub.collections?.includes(collection)) {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.ensureDependencyContainers(sub);
|
|
522
|
+
|
|
523
|
+
const dependencyIds = sub.collectionDependencies?.get(collection);
|
|
524
|
+
if (dependencyIds && dependencyIds.size > 0) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const filters = sub.collectionFilters?.get(collection);
|
|
529
|
+
if (filters && filters.length > 0) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (sub.watchAllCollections?.has(collection)) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private async delay(ms: number) {
|
|
541
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
542
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private getMessageDateMs(messageDate: any): number {
|
|
546
|
+
if (messageDate instanceof Date && !isNaN(messageDate.getTime())) {
|
|
547
|
+
return messageDate.getTime();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (typeof messageDate === 'string' || typeof messageDate === 'number') {
|
|
551
|
+
const parsed = new Date(messageDate);
|
|
552
|
+
if (!isNaN(parsed.getTime())) {
|
|
553
|
+
return parsed.getTime();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return Date.now();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private isStaleRouteMessage(ws: WebSocket, messageRoute: string, messageDate: any): boolean {
|
|
561
|
+
const socketId = ws ? ws['id_socket'] : '';
|
|
562
|
+
if (!socketId) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!messageRoute || messageRoute === 'Bypass') {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const messageDateMs = this.getMessageDateMs(messageDate);
|
|
571
|
+
const lastRoute = this._lastRouteBySocket.get(socketId);
|
|
572
|
+
|
|
573
|
+
if (lastRoute && messageDateMs < lastRoute.dateMs) {
|
|
574
|
+
this.dependencyDebug('Skip route cleanup due to stale route message', {
|
|
575
|
+
socketId,
|
|
576
|
+
messageRoute,
|
|
577
|
+
messageDateMs,
|
|
578
|
+
lastRoute: lastRoute.route,
|
|
579
|
+
lastRouteDateMs: lastRoute.dateMs
|
|
580
|
+
});
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
this._lastRouteBySocket.set(socketId, { route: messageRoute, dateMs: messageDateMs });
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private getPublicationWorkerDispatcher() {
|
|
589
|
+
const mainServer = ResolveIOServer.getMainServer();
|
|
590
|
+
return mainServer && typeof mainServer.getWorkerDispatcherManager === 'function'
|
|
591
|
+
? mainServer.getWorkerDispatcherManager()
|
|
592
|
+
: null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private shouldUsePublicationWorkers(): boolean {
|
|
596
|
+
const dispatcher = this.getPublicationWorkerDispatcher();
|
|
597
|
+
if (!dispatcher || !dispatcher.hasWorkers()) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
if (typeof dispatcher.hasWorkersForMethod === 'function' && !dispatcher.hasWorkersForMethod('runPublication')) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (this._publicationWorkerQueueLimit > 0) {
|
|
605
|
+
const snapshot = dispatcher.getQueueSnapshot();
|
|
606
|
+
if (snapshot.queueDepth >= this._publicationWorkerQueueLimit) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private async runPublicationViaWorker(sub: ActiveSubscriptionModel, userId?: string): Promise<PublicationExecution | null> {
|
|
615
|
+
if (!this.shouldUsePublicationWorkers()) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const dispatcher = this.getPublicationWorkerDispatcher();
|
|
620
|
+
if (!dispatcher) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
const response = await dispatcher.sendInternalPromiseRaw('runPublication', [
|
|
626
|
+
sub.publication,
|
|
627
|
+
sub.subscriptionData,
|
|
628
|
+
userId
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
if (!response || response.error) {
|
|
632
|
+
throw response?.result || new Error('Publication worker error');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!response.packedResult) {
|
|
636
|
+
throw new Error('Publication worker missing packedResult');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const meta = response.meta || {};
|
|
640
|
+
const snapshot = meta.snapshot ? deserializeDependencySnapshot(meta.snapshot) : undefined;
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
packedResult: response.packedResult,
|
|
644
|
+
snapshot,
|
|
645
|
+
encoding: response.encoding || meta.encoding || 'msgpack',
|
|
646
|
+
workerUsed: true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
if (this._enableDebug) {
|
|
651
|
+
console.log(new Date(), 'Sub Manager', 'Worker publication failed, falling back', sub.publication, error?.message || error);
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private async runPublicationLocally(sub: ActiveSubscriptionModel, userId?: string): Promise<PublicationExecution> {
|
|
658
|
+
const pub = this._publications[sub.publication];
|
|
659
|
+
if (!pub) {
|
|
660
|
+
throw new Error(`Publication not found: ${sub.publication}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const context = Object.assign({}, this, SubscriptionManager.prototype);
|
|
664
|
+
const metadata: PublicationContext = {
|
|
665
|
+
publication: sub.publication,
|
|
666
|
+
subscriptionData: sub.subscriptionData
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const execution = pub.user_specific
|
|
670
|
+
? await withDependencyTracking(() => pub.function.call(context, userId || '', ...sub.subscriptionData), Object.assign({}, metadata, { userId }))
|
|
671
|
+
: await withDependencyTracking(() => pub.function.call(context, ...sub.subscriptionData), metadata);
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
result: execution.result,
|
|
675
|
+
packedResult: this.packCachePayload(execution.result),
|
|
676
|
+
snapshot: execution.snapshot,
|
|
677
|
+
encoding: 'msgpack',
|
|
678
|
+
workerUsed: false
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private async runPublicationExecution(sub: ActiveSubscriptionModel, userId?: string): Promise<PublicationExecution> {
|
|
683
|
+
const pub = this._publications[sub.publication];
|
|
684
|
+
if (!pub) {
|
|
685
|
+
throw new Error(`Publication not found: ${sub.publication}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const effectiveUserId = pub.user_specific ? userId : undefined;
|
|
689
|
+
const workerExecution = await this.runPublicationViaWorker(sub, effectiveUserId);
|
|
690
|
+
if (workerExecution) {
|
|
691
|
+
return workerExecution;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return this.runPublicationLocally(sub, effectiveUserId);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private async sendDataToAllWithRetry(sub: ActiveSubscriptionModel, collection: string, type: string) {
|
|
698
|
+
sub.running = true;
|
|
699
|
+
|
|
700
|
+
do {
|
|
701
|
+
if (this._enableDebug) {
|
|
702
|
+
console.log(new Date(), 'Running sendDataToAll Sub', sub.publication);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
sub.runAgain = false;
|
|
706
|
+
await this.sendDataToAll(sub, collection, type);
|
|
707
|
+
|
|
708
|
+
if (this._enableDebug) {
|
|
709
|
+
console.log(new Date(), 'Done sendDataToAll Sub', sub.publication, sub.runAgain);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (sub.runAgain) {
|
|
713
|
+
await this.delay(500); // delay, adjust as needed
|
|
714
|
+
}
|
|
715
|
+
} while (sub.runAgain);
|
|
716
|
+
|
|
717
|
+
sub.running = false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private async sendDataToOneWithRetry(ws: WebSocket, messageId: number, sub: ActiveSubscriptionModel, collection: string, type: string) {
|
|
721
|
+
sub.running = true;
|
|
722
|
+
|
|
723
|
+
do {
|
|
724
|
+
if (this._enableDebug) {
|
|
725
|
+
console.log(new Date(), 'Running sendDataToOne Sub', sub.publication);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
sub.runAgain = false;
|
|
729
|
+
await this.sendDataToOne(ws, messageId, sub, collection, type);
|
|
730
|
+
|
|
731
|
+
if (this._enableDebug) {
|
|
732
|
+
console.log(new Date(), 'Done sendDataToOne Sub', sub.publication, sub.runAgain);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (sub.runAgain) {
|
|
736
|
+
await this.delay(500); // delay, adjust as needed
|
|
737
|
+
}
|
|
738
|
+
} while (sub.runAgain);
|
|
739
|
+
sub.running = false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Add all files to publications private object
|
|
743
|
+
public publications(method: SubscriptionModel) {
|
|
744
|
+
this._publications = Object.assign(this._publications, method);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
public getPublication(publication: string): SubscriptionPubModel | undefined {
|
|
748
|
+
return this._publications[publication];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
public getPublications(): SubscriptionModel {
|
|
752
|
+
return this._publications;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Throttled `loggedInLatency` method
|
|
756
|
+
public loggedInLatency(ws: WebSocket) {
|
|
757
|
+
const now = new Date();
|
|
758
|
+
const newLatency = ws['latency'];
|
|
759
|
+
const socketId = ws['id_socket'];
|
|
760
|
+
let loggedInUser = this._loggedInUsers.find(a => a.id_ws === socketId);
|
|
761
|
+
|
|
762
|
+
if (!loggedInUser) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const existingEntry = this.latencyBuffer.get(socketId);
|
|
767
|
+
|
|
768
|
+
// Throttle updates: only update if time threshold has passed or latency has significantly changed
|
|
769
|
+
if (
|
|
770
|
+
!existingEntry ||
|
|
771
|
+
(now.getTime() - existingEntry.lastUpdate.getTime() >= this.LATENCY_UPDATE_THRESHOLD_MS) ||
|
|
772
|
+
(Math.abs(newLatency - existingEntry.latency) > 100) // Optional: log only significant changes
|
|
773
|
+
) {
|
|
774
|
+
// Update in-memory and buffer
|
|
775
|
+
loggedInUser.date = now;
|
|
776
|
+
this.latencyBuffer.set(socketId, { latency: newLatency, lastUpdate: now });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Method to flush buffered latency updates in bulk
|
|
781
|
+
private async flushThrottledLatencyUpdates() {
|
|
782
|
+
if (this.latencyBuffer.size === 0) return; // No updates to flush
|
|
783
|
+
if (this._latencyFlushInProgress) {
|
|
784
|
+
this._latencyFlushPending = true;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this._latencyFlushInProgress = true;
|
|
789
|
+
|
|
790
|
+
const pendingEntries = Array.from(this.latencyBuffer.entries());
|
|
791
|
+
this.latencyBuffer.clear();
|
|
792
|
+
const updates = pendingEntries.map(([id_ws, { latency, lastUpdate }]) => ({
|
|
793
|
+
updateOne: {
|
|
794
|
+
filter: { id_ws },
|
|
795
|
+
update: { $set: { latency, date: lastUpdate } }
|
|
796
|
+
}
|
|
797
|
+
}));
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
await LoggedInUsers.bulkWrite(updates, { ordered: false });
|
|
801
|
+
|
|
802
|
+
if (this.getEnableDebug()) {
|
|
803
|
+
console.log(new Date(), 'Sub Manager', 'Throttled latency batch update successful', updates.length);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
for (const [id_ws, payload] of pendingEntries) {
|
|
808
|
+
const current = this.latencyBuffer.get(id_ws);
|
|
809
|
+
if (!current || current.lastUpdate < payload.lastUpdate) {
|
|
810
|
+
this.latencyBuffer.set(id_ws, payload);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
console.error(new Date(), 'Sub Manager', 'Throttled latency batch update failed', error);
|
|
814
|
+
// Optional: implement retry logic or logging for failed updates
|
|
815
|
+
}
|
|
816
|
+
finally {
|
|
817
|
+
this._latencyFlushInProgress = false;
|
|
818
|
+
if (this._latencyFlushPending) {
|
|
819
|
+
this._latencyFlushPending = false;
|
|
820
|
+
setImmediate(() => this.flushThrottledLatencyUpdates());
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Subscribe to publication
|
|
826
|
+
public async subscribe(messageRoute: string, messageDate: Date, ws: WebSocket, messageId: number, publication: string, subscriptionData: any[]) {
|
|
827
|
+
this._debugSubHits += 1;
|
|
828
|
+
this.connectDebug('Subscribe request', {
|
|
829
|
+
publication,
|
|
830
|
+
messageRoute,
|
|
831
|
+
messageId,
|
|
832
|
+
id_socket: ws ? ws['id_socket'] : null,
|
|
833
|
+
user: ws ? ws['user'] : null,
|
|
834
|
+
args: Array.isArray(subscriptionData) ? subscriptionData.length : 0
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
if (!this._debugSubCollections.some(a => a.publication === publication)) {
|
|
838
|
+
this._debugSubCollections.push({
|
|
839
|
+
publication: publication,
|
|
840
|
+
hits: 1
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
this._debugSubCollections.find(a => a.publication === publication).hits += 1;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
let pub = this._publications[publication];
|
|
848
|
+
|
|
849
|
+
if (!pub) {
|
|
850
|
+
console.error(new Date(), 'No Publication: ' + publication);
|
|
851
|
+
this.connectDebug('Missing publication', { publication, messageRoute, messageId });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
if (subscriptionData.length > 1 || subscriptionData[0]) {
|
|
856
|
+
if (!pub.check) {
|
|
857
|
+
console.error(new Date(), 'No Check Function For Pub ' + publication);
|
|
858
|
+
this.connectDebug('Missing check for publication', { publication, messageRoute, messageId });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
else if (!pub.check._schema) {
|
|
862
|
+
console.error(new Date(), 'No Check Schema For Pub ' + publication);
|
|
863
|
+
this.connectDebug('Missing check schema for publication', { publication, messageRoute, messageId });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
let valObj = {};
|
|
868
|
+
let valKeys = Object.keys(pub.check._schema);
|
|
869
|
+
|
|
870
|
+
let rootKeys = valKeys.filter(a => !a.includes('.'));
|
|
871
|
+
|
|
872
|
+
for (let i = 0; i < subscriptionData.length; i++) {
|
|
873
|
+
valObj[rootKeys[i]] = subscriptionData[i];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
pub.check.validate(valObj);
|
|
878
|
+
}
|
|
879
|
+
catch (errors) {
|
|
880
|
+
if (errors) {
|
|
881
|
+
console.error(new Date(), 'Error in Pub Check (' + publication + ')', errors);
|
|
882
|
+
this.connectDebug('Publication check failed', {
|
|
883
|
+
publication,
|
|
884
|
+
messageRoute,
|
|
885
|
+
messageId,
|
|
886
|
+
args: subscriptionData.length,
|
|
887
|
+
valObj,
|
|
888
|
+
error: errors?.message || errors
|
|
889
|
+
});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const staleRouteMessage = this.isStaleRouteMessage(ws, messageRoute, messageDate);
|
|
897
|
+
if (messageRoute !== 'Bypass' && messageRoute !== '/' && !staleRouteMessage) {
|
|
898
|
+
let urlData = messageRoute.split('/');
|
|
899
|
+
let urlModule = '';
|
|
900
|
+
let urlNext = urlData[0];
|
|
901
|
+
|
|
902
|
+
if (urlData[0] === '') {
|
|
903
|
+
urlModule = '/';
|
|
904
|
+
urlNext = urlData[1];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
urlModule += urlNext;
|
|
908
|
+
|
|
909
|
+
if (urlData.length > 1) {
|
|
910
|
+
urlModule += '/';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let otherRouteSubs = this._subscriptions.filter(a => a.clients.some(b => b.id_socket === ws['id_socket'] && b.messageRoute !== 'Bypass' && b.messageRoute !== '/' && b.messageRoute !== messageRoute && !b.messageRoute.startsWith(urlModule)));
|
|
914
|
+
|
|
915
|
+
if (otherRouteSubs.length) {
|
|
916
|
+
// ResolveIOServer.getMainServer().getMethodManager().sendEmail('dev@resolveio.com', 'SERVER - Detected Undestroyed Subscription - ' + this.serverConfig['CLIENT_NAME'], 'USER: ' + ws['user'] + ' (Socket: ' + ws['id_socket'] + ') ' + ' is on route: ' + messageRoute + ' but has the following subscriptions on other routes:' + JSON.stringify(otherRouteSubs, null, 2));
|
|
917
|
+
|
|
918
|
+
otherRouteSubs.forEach(otherSub => {
|
|
919
|
+
otherSub.clients.filter(a => a.id_socket === ws['id_socket']).forEach(client => {
|
|
920
|
+
this.unsubscribe(client.messageRoute, new Date(), ws, client.messageId, otherSub.publication, otherSub.subscriptionData);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const normalizedSubscriptionData = Array.isArray(subscriptionData) ? subscriptionData : [];
|
|
927
|
+
const subscriptionKey = JSON.stringify(normalizedSubscriptionData);
|
|
928
|
+
let sub = this._subscriptions.find(a => a.publication === publication && a.subscriptionKey === subscriptionKey);
|
|
929
|
+
const isAiPublication = this.isAiPublication(publication);
|
|
930
|
+
const wsId = ws ? ws['id_socket'] : null;
|
|
931
|
+
const wsUserId = ws ? ws['id_user'] : null;
|
|
932
|
+
|
|
933
|
+
// If sub found (another user watching same data), add client to same sub
|
|
934
|
+
if (sub) {
|
|
935
|
+
if (isAiPublication && wsId) {
|
|
936
|
+
const existingIndex = sub.clients.findIndex(a => a.id_socket === wsId);
|
|
937
|
+
if (existingIndex >= 0) {
|
|
938
|
+
const existingClient = sub.clients[existingIndex];
|
|
939
|
+
for (let i = sub.clients.length - 1; i >= 0; i--) {
|
|
940
|
+
if (i !== existingIndex && sub.clients[i].id_socket === wsId) {
|
|
941
|
+
sub.clients.splice(i, 1);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
existingClient.id_user = wsUserId;
|
|
945
|
+
existingClient.messageId = messageId;
|
|
946
|
+
existingClient.messageRoute = messageRoute;
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
sub.clients.push({
|
|
950
|
+
id_user: wsUserId,
|
|
951
|
+
messageId: messageId,
|
|
952
|
+
id_socket: wsId,
|
|
953
|
+
messageRoute: messageRoute
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
else if (!sub.clients.some(a => a.id_socket === ws['id_socket'] && a.messageId === messageId)) {
|
|
958
|
+
sub.clients.push({
|
|
959
|
+
id_user: ws['id_user'],
|
|
960
|
+
messageId: messageId,
|
|
961
|
+
id_socket: ws['id_socket'],
|
|
962
|
+
messageRoute: messageRoute
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// If sub not found, create new sub
|
|
967
|
+
else {
|
|
968
|
+
this._subscriptions.push({
|
|
969
|
+
publication: publication,
|
|
970
|
+
subscriptionKey: subscriptionKey,
|
|
971
|
+
subscriptionData: normalizedSubscriptionData,
|
|
972
|
+
collections: this.getPublicationCollections(publication),
|
|
973
|
+
clients: [{
|
|
974
|
+
id_user: ws['id_user'],
|
|
975
|
+
messageId: messageId,
|
|
976
|
+
id_socket: ws['id_socket'],
|
|
977
|
+
messageRoute: messageRoute,
|
|
978
|
+
}],
|
|
979
|
+
cacheId: 0,
|
|
980
|
+
running: false,
|
|
981
|
+
runAgain: false,
|
|
982
|
+
collectionDependencies: new Map(),
|
|
983
|
+
collectionFilters: new Map(),
|
|
984
|
+
watchAllCollections: new Set(),
|
|
985
|
+
collectionQueryMeta: new Map()
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (!sub) {
|
|
990
|
+
sub = this._subscriptions.find(a => a.publication === publication && a.subscriptionKey === subscriptionKey);
|
|
991
|
+
}
|
|
992
|
+
this.ensureDependencyContainers(sub);
|
|
993
|
+
this.logAiSubscriptionEvent('sub', {
|
|
994
|
+
publication,
|
|
995
|
+
subscriptionKey,
|
|
996
|
+
messageId,
|
|
997
|
+
id_socket: ws ? ws['id_socket'] : null,
|
|
998
|
+
messageRoute,
|
|
999
|
+
clients: sub?.clients?.length || 0,
|
|
1000
|
+
subscriptionData: normalizedSubscriptionData
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
if (this._enableDebug) {
|
|
1004
|
+
console.log(new Date(), 'New Sub', sub.publication, sub.running, sub.runAgain, sub.clients.length);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Immediately send data to the new client
|
|
1008
|
+
await this.processSubscription(sub, ws, messageId);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
public async createLoggedInUser(id_ws: string): Promise<LoggedInUserModel> {
|
|
1013
|
+
let ws = this._websocketManager.getWebSocket(id_ws);
|
|
1014
|
+
|
|
1015
|
+
if (ws) {
|
|
1016
|
+
let user = {
|
|
1017
|
+
_id: objectIdHexString(),
|
|
1018
|
+
__v: 0,
|
|
1019
|
+
date: new Date(),
|
|
1020
|
+
id_user: ws['id_user'],
|
|
1021
|
+
user: ws['user'],
|
|
1022
|
+
id_ws: ws['id_socket']
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
this._loggedInUsers.push(user);
|
|
1026
|
+
await LoggedInUsers.insertOne(user);
|
|
1027
|
+
|
|
1028
|
+
return user;
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
throw new Error('Error in Create Logged In User: No WS');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Unsubscribe from publication
|
|
1036
|
+
public unsubscribe(messageRoute: string, messageDate: Date, ws: WebSocket, messageId: number, publication: string, subscriptionData: any[]) {
|
|
1037
|
+
this._debugUnSubHits += 1;
|
|
1038
|
+
|
|
1039
|
+
if (!this._publications[publication]) {
|
|
1040
|
+
console.log('No Publication: ' + publication);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
const normalizedSubscriptionData = Array.isArray(subscriptionData) ? subscriptionData : [];
|
|
1045
|
+
const subscriptionKey = JSON.stringify(normalizedSubscriptionData);
|
|
1046
|
+
let sub = this._subscriptions.find(a => a.publication === publication && a.subscriptionKey === subscriptionKey);
|
|
1047
|
+
|
|
1048
|
+
if (sub) {
|
|
1049
|
+
const isAiPublication = this.isAiPublication(publication);
|
|
1050
|
+
for (let i = sub.clients.length - 1; i >= 0; i--) {
|
|
1051
|
+
if (isAiPublication && sub.clients[i].id_socket === ws['id_socket']) {
|
|
1052
|
+
sub.clients.splice(i, 1);
|
|
1053
|
+
}
|
|
1054
|
+
else if (sub.clients[i].id_user === ws['id_user'] && sub.clients[i].messageId === messageId && sub.clients[i].id_socket === ws['id_socket']) {
|
|
1055
|
+
sub.clients.splice(i, 1);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
this.logAiSubscriptionEvent('unsub', {
|
|
1060
|
+
publication,
|
|
1061
|
+
subscriptionKey,
|
|
1062
|
+
messageId,
|
|
1063
|
+
id_socket: ws ? ws['id_socket'] : null,
|
|
1064
|
+
messageRoute,
|
|
1065
|
+
clients: sub.clients.length,
|
|
1066
|
+
subscriptionData: normalizedSubscriptionData
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
if (this._enableDebug) {
|
|
1070
|
+
console.log(new Date(), 'Unsub', sub.publication, sub.running, sub.runAgain, sub.clients.length);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
// Unsubscribe from publication
|
|
1078
|
+
public async unsubscribeAll(ws: WebSocket) {
|
|
1079
|
+
this._debugUnSubAllHits += 1;
|
|
1080
|
+
|
|
1081
|
+
if (ws) {
|
|
1082
|
+
if (ws['id_socket']) {
|
|
1083
|
+
this._lastRouteBySocket.delete(ws['id_socket']);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Check if WebSocket has already been unsubscribed
|
|
1087
|
+
if (ws['isUnsubscribed']) {
|
|
1088
|
+
return; // Skip if already unsubscribed
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Mark the WebSocket as unsubscribed to prevent further calls
|
|
1092
|
+
ws['isUnsubscribed'] = true;
|
|
1093
|
+
|
|
1094
|
+
if (this._loggedInUsers.map(a => a.id_ws).indexOf(ws['id_socket']) >= 0) {
|
|
1095
|
+
this._loggedInUsers.splice(this._loggedInUsers.map(a => a.id_ws).indexOf(ws['id_socket']), 1);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
await LoggedInUsers.deleteOne({ id_ws: ws['id_socket'] });
|
|
1099
|
+
|
|
1100
|
+
let userSubs = this._subscriptions.filter(a => a.clients.some(b => b.id_user === ws['id_user'] && b.id_socket === ws['id_socket']));
|
|
1101
|
+
|
|
1102
|
+
for (let i = userSubs.length - 1; i >= 0; i--) {
|
|
1103
|
+
let sub = userSubs[i];
|
|
1104
|
+
|
|
1105
|
+
for (let j = sub.clients.length - 1; j >= 0; j--) {
|
|
1106
|
+
if (sub.clients[j].id_socket === ws['id_socket']) {
|
|
1107
|
+
sub.clients.splice(j, 1);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
this._websocketManager.removeWebSocket(ws);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
public getActiveSubscriptions() {
|
|
1117
|
+
return this._subscriptions;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Get publication collection
|
|
1121
|
+
private getPublicationCollections(publication: string) {
|
|
1122
|
+
return this._publications[publication].collections;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private getWatchedDatabases(): string[] {
|
|
1126
|
+
const mongoManager = ResolveIOServer.getMongoManager();
|
|
1127
|
+
const managerDatabases = mongoManager?.getWatchedDatabases() || [];
|
|
1128
|
+
|
|
1129
|
+
if (managerDatabases.length) {
|
|
1130
|
+
return managerDatabases;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig();
|
|
1134
|
+
const mainDb = config && typeof config['DATABASE'] === 'string' ? config['DATABASE'] : '';
|
|
1135
|
+
|
|
1136
|
+
return mainDb ? [mainDb] : [];
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private resolveOplogMode(): 'auto' | 'change-stream' | 'local' {
|
|
1140
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1141
|
+
const raw = process.env.SUBSCRIPTION_OPLOG_MODE || process.env.OPLOG_MODE || config['SUBSCRIPTION_OPLOG_MODE'] || config['OPLOG_MODE'];
|
|
1142
|
+
|
|
1143
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
1144
|
+
return 'auto';
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const normalized = raw.trim().toLowerCase();
|
|
1148
|
+
|
|
1149
|
+
if (['local', 'single', 'standalone', 'fallback', 'poll'].includes(normalized)) {
|
|
1150
|
+
return 'local';
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (['change-stream', 'changestream', 'oplog', 'replica', 'replicaset', 'rs'].includes(normalized)) {
|
|
1154
|
+
return 'change-stream';
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return 'auto';
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private resolveLocalOplogResyncIntervalMs(): number {
|
|
1161
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1162
|
+
const raw = process.env.OPLOG_FALLBACK_RESYNC_MS
|
|
1163
|
+
|| process.env.SUBSCRIPTION_OPLOG_RESYNC_MS
|
|
1164
|
+
|| config['OPLOG_FALLBACK_RESYNC_MS']
|
|
1165
|
+
|| config['SUBSCRIPTION_OPLOG_RESYNC_MS'];
|
|
1166
|
+
|
|
1167
|
+
return this.parsePositiveNumber(raw);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private resolveResumeTokenMaxAgeMs(): number {
|
|
1171
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1172
|
+
const raw = process.env.OPLOG_RESUME_TOKEN_MAX_AGE_MS
|
|
1173
|
+
|| process.env.SUBSCRIPTION_OPLOG_RESUME_TOKEN_MAX_AGE_MS
|
|
1174
|
+
|| config['OPLOG_RESUME_TOKEN_MAX_AGE_MS']
|
|
1175
|
+
|| config['SUBSCRIPTION_OPLOG_RESUME_TOKEN_MAX_AGE_MS'];
|
|
1176
|
+
|
|
1177
|
+
return this.parsePositiveNumber(raw);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private resolveConnectDebug(): boolean {
|
|
1181
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1182
|
+
const raw = process.env.WS_CONNECT_DEBUG
|
|
1183
|
+
|| process.env.CONNECT_DEBUG
|
|
1184
|
+
|| config['WS_CONNECT_DEBUG']
|
|
1185
|
+
|| config['CONNECT_DEBUG'];
|
|
1186
|
+
|
|
1187
|
+
return this.parseDebugFlag(raw);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private resolveSubSendDebug(): boolean {
|
|
1191
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1192
|
+
const raw = process.env.SUB_SEND_DEBUG
|
|
1193
|
+
|| process.env.SUBSCRIPTION_SEND_DEBUG
|
|
1194
|
+
|| config['SUB_SEND_DEBUG']
|
|
1195
|
+
|| config['SUBSCRIPTION_SEND_DEBUG'];
|
|
1196
|
+
|
|
1197
|
+
return this.parseDebugFlag(raw);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private resolveSubSendLogEnabled(): boolean {
|
|
1201
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1202
|
+
const raw = process.env.SUB_SEND_LOG_ENABLED
|
|
1203
|
+
|| process.env.SUBSCRIPTION_SEND_LOG_ENABLED
|
|
1204
|
+
|| config['SUB_SEND_LOG_ENABLED']
|
|
1205
|
+
|| config['SUBSCRIPTION_SEND_LOG_ENABLED'];
|
|
1206
|
+
|
|
1207
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
1208
|
+
return this._subSendDebug;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return this.parseDebugFlag(raw);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private resolveSubSendLogThresholdMs(): number {
|
|
1215
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1216
|
+
const raw = process.env.SUB_SEND_LOG_THRESHOLD_MS
|
|
1217
|
+
|| process.env.SUBSCRIPTION_SEND_LOG_THRESHOLD_MS
|
|
1218
|
+
|| config['SUB_SEND_LOG_THRESHOLD_MS']
|
|
1219
|
+
|| config['SUBSCRIPTION_SEND_LOG_THRESHOLD_MS'];
|
|
1220
|
+
|
|
1221
|
+
return this.parsePositiveNumber(raw);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
private resolveSubSendLogBytes(): number {
|
|
1225
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1226
|
+
const raw = process.env.SUB_SEND_LOG_BYTES
|
|
1227
|
+
|| process.env.SUBSCRIPTION_SEND_LOG_BYTES
|
|
1228
|
+
|| config['SUB_SEND_LOG_BYTES']
|
|
1229
|
+
|| config['SUBSCRIPTION_SEND_LOG_BYTES'];
|
|
1230
|
+
|
|
1231
|
+
return this.parsePositiveNumber(raw);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private resolveSubSendLogSampleRate(): number {
|
|
1235
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1236
|
+
const raw = process.env.SUB_SEND_LOG_SAMPLE_RATE
|
|
1237
|
+
|| process.env.SUBSCRIPTION_SEND_LOG_SAMPLE_RATE
|
|
1238
|
+
|| config['SUB_SEND_LOG_SAMPLE_RATE']
|
|
1239
|
+
|| config['SUBSCRIPTION_SEND_LOG_SAMPLE_RATE'];
|
|
1240
|
+
|
|
1241
|
+
return this.parseSampleRate(raw, this._subSendLogSampleRate);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private resolveAiSubscriptionDebug(): boolean {
|
|
1245
|
+
const config = this.serverConfig || ResolveIOServer.getServerConfig() || {};
|
|
1246
|
+
const raw = process.env.AI_ASSISTANT_SUB_DEBUG
|
|
1247
|
+
|| process.env.AI_TERMINAL_SUB_DEBUG
|
|
1248
|
+
|| config['AI_ASSISTANT_SUB_DEBUG']
|
|
1249
|
+
|| config['AI_TERMINAL_SUB_DEBUG'];
|
|
1250
|
+
|
|
1251
|
+
return this.parseDebugFlag(raw);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
private parseDebugFlag(value: any): boolean {
|
|
1256
|
+
if (value === true) {
|
|
1257
|
+
return true;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (value === false || value === null || value === undefined) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (typeof value === 'number') {
|
|
1265
|
+
return value === 1;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (typeof value === 'string') {
|
|
1269
|
+
const normalized = value.trim().toLowerCase();
|
|
1270
|
+
return ['1', 'true', 'yes', 'y', 'on'].includes(normalized);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private parsePositiveNumber(value: any): number {
|
|
1277
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
1278
|
+
return value;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
1282
|
+
const parsed = parseInt(value, 10);
|
|
1283
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1284
|
+
return parsed;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return 0;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
private parseSampleRate(value: any, fallback: number): number {
|
|
1292
|
+
const parsed = parseFloat(value ?? '');
|
|
1293
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed <= 1) {
|
|
1294
|
+
return parsed;
|
|
1295
|
+
}
|
|
1296
|
+
return fallback;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private shouldLogSubSend(durationMs: number, payloadBytes: number | null): boolean {
|
|
1300
|
+
if (!this._subSendLogEnabled) {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (this._subSendLogSampleRate < 1 && Math.random() > this._subSendLogSampleRate) {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (this._subSendDebug) {
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (this._subSendLogThresholdMs > 0 && durationMs >= this._subSendLogThresholdMs) {
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (payloadBytes !== null && this._subSendLogBytes > 0 && payloadBytes >= this._subSendLogBytes) {
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private logSubSend(sub: ActiveSubscriptionModel, details: { collection: string; type: string; clients: number; durationMs: number; payloadBytes: number | null }) {
|
|
1324
|
+
if (!details.clients) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (!this.shouldLogSubSend(details.durationMs, details.payloadBytes)) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
console.log(new Date(), '[Sub Send]', JSON.stringify({
|
|
1333
|
+
publication: sub.publication,
|
|
1334
|
+
subscriptionKey: sub.subscriptionKey,
|
|
1335
|
+
clients: details.clients,
|
|
1336
|
+
durationMs: details.durationMs,
|
|
1337
|
+
payloadBytes: details.payloadBytes,
|
|
1338
|
+
collection: details.collection,
|
|
1339
|
+
type: details.type
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private isAiPublication(publication: string): boolean {
|
|
1344
|
+
return publication === 'aiTerminalMessages' || publication === 'aiTerminalConversations';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private logAiSubSend(details: {
|
|
1348
|
+
publication: string;
|
|
1349
|
+
subscriptionKey: string;
|
|
1350
|
+
messageId: number;
|
|
1351
|
+
id_socket: string;
|
|
1352
|
+
clients: number;
|
|
1353
|
+
payloadBytes: number | null;
|
|
1354
|
+
packed: boolean;
|
|
1355
|
+
cacheHit?: boolean;
|
|
1356
|
+
collection: string;
|
|
1357
|
+
type: string;
|
|
1358
|
+
subscriptionData?: any[];
|
|
1359
|
+
}): void {
|
|
1360
|
+
if (!this._aiWorkerDebug) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (!this.isAiPublication(details.publication)) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
console.log(new Date(), '[AI Worker Debug] subSend', JSON.stringify({
|
|
1367
|
+
publication: details.publication,
|
|
1368
|
+
subscriptionKey: details.subscriptionKey,
|
|
1369
|
+
messageId: details.messageId,
|
|
1370
|
+
id_socket: details.id_socket,
|
|
1371
|
+
clients: details.clients,
|
|
1372
|
+
payloadBytes: details.payloadBytes,
|
|
1373
|
+
packed: details.packed,
|
|
1374
|
+
cacheHit: details.cacheHit ?? false,
|
|
1375
|
+
collection: details.collection,
|
|
1376
|
+
type: details.type,
|
|
1377
|
+
subscriptionData: details.subscriptionData
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private logAiSubscriptionEvent(event: 'sub' | 'unsub', details: {
|
|
1382
|
+
publication: string;
|
|
1383
|
+
subscriptionKey: string;
|
|
1384
|
+
messageId: number;
|
|
1385
|
+
id_socket: string;
|
|
1386
|
+
messageRoute: string;
|
|
1387
|
+
clients: number;
|
|
1388
|
+
subscriptionData?: any[];
|
|
1389
|
+
}): void {
|
|
1390
|
+
if (!this._aiSubscriptionDebug) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (!this.isAiPublication(details.publication)) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const wsId = details.id_socket || 'unknown';
|
|
1397
|
+
const key = `${event}:${wsId}:${details.publication}:${details.subscriptionKey}`;
|
|
1398
|
+
const now = Date.now();
|
|
1399
|
+
const lastAt = this._aiSubLastAt.get(key);
|
|
1400
|
+
const deltaMs = lastAt ? now - lastAt : null;
|
|
1401
|
+
this._aiSubLastAt.set(key, now);
|
|
1402
|
+
console.log(new Date(), `[AI Sub Debug] ${event}`, JSON.stringify({
|
|
1403
|
+
publication: details.publication,
|
|
1404
|
+
subscriptionKey: details.subscriptionKey,
|
|
1405
|
+
messageId: details.messageId,
|
|
1406
|
+
messageRoute: details.messageRoute,
|
|
1407
|
+
id_socket: wsId,
|
|
1408
|
+
clients: details.clients,
|
|
1409
|
+
deltaMs,
|
|
1410
|
+
subscriptionData: details.subscriptionData
|
|
1411
|
+
}));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private connectDebug(message: string, details?: Record<string, unknown>) {
|
|
1415
|
+
if (!this._connectDebug) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (details) {
|
|
1420
|
+
console.log(new Date(), '[Connect Debug]', message, JSON.stringify(details));
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
console.log(new Date(), '[Connect Debug]', message);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private parsePositiveFloat(value: any): number {
|
|
1428
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
1429
|
+
return value;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
1433
|
+
const parsed = parseFloat(value);
|
|
1434
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1435
|
+
return parsed;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return 0;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private isChangeStreamUnsupported(error: any): boolean {
|
|
1443
|
+
const code = typeof error?.code === 'number' ? error.code : null;
|
|
1444
|
+
const codeName = typeof error?.codeName === 'string' ? error.codeName.toLowerCase() : '';
|
|
1445
|
+
const message = typeof error?.message === 'string' ? error.message.toLowerCase() : '';
|
|
1446
|
+
|
|
1447
|
+
if (code === 40573) {
|
|
1448
|
+
return true;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (codeName && codeName.includes('changestream')) {
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (message.includes('change stream') && message.includes('replica')) {
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (message.includes('change stream') && message.includes('not supported')) {
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (code === 13 && message.includes('allchangesforcluster')) {
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (code === 13 && message.includes('not authorized') && message.includes('$db: "admin"')) {
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private isResumeTokenInvalid(error: any): boolean {
|
|
1475
|
+
const code = typeof error?.code === 'number' ? error.code : null;
|
|
1476
|
+
const codeName = typeof error?.codeName === 'string' ? error.codeName.toLowerCase() : '';
|
|
1477
|
+
const message = typeof error?.message === 'string' ? error.message.toLowerCase() : '';
|
|
1478
|
+
|
|
1479
|
+
if (code === 286) {
|
|
1480
|
+
// ChangeStreamHistoryLost
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (codeName.includes('changestreamhistorylost')) {
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (message.includes('resume token') && (
|
|
1489
|
+
message.includes('not found') ||
|
|
1490
|
+
message.includes('not possible') ||
|
|
1491
|
+
message.includes('no longer') ||
|
|
1492
|
+
message.includes('not in the oplog') ||
|
|
1493
|
+
message.includes('too old')
|
|
1494
|
+
)) {
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (message.includes('resume of change stream') && message.includes('not possible')) {
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
private isChangeStreamDocumentTooLarge(error: any): boolean {
|
|
1506
|
+
const code = typeof error?.code === 'number' ? error.code : null;
|
|
1507
|
+
const codeName = typeof error?.codeName === 'string' ? error.codeName.toLowerCase() : '';
|
|
1508
|
+
const message = typeof error?.message === 'string' ? error.message.toLowerCase() : '';
|
|
1509
|
+
|
|
1510
|
+
if (code === 10334) {
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (codeName.includes('bsonobjecttoolarge')) {
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (message.includes('bsonobj size') && message.includes('16mb')) {
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private async enableLocalOplogFallback(reason: string, error?: any) {
|
|
1526
|
+
if (this._useLocalOplog) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
this._useLocalOplog = true;
|
|
1531
|
+
this._oplogMode = 'local';
|
|
1532
|
+
this._oplogRetryCount = 0;
|
|
1533
|
+
|
|
1534
|
+
if (this._oplog$ && !this._oplog$.closed) {
|
|
1535
|
+
try {
|
|
1536
|
+
this._oplog$.removeAllListeners();
|
|
1537
|
+
await this._oplog$.close();
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
// ignore close errors
|
|
1541
|
+
}
|
|
1542
|
+
this._oplog$ = null;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const message = typeof error?.message === 'string' ? error.message : '';
|
|
1546
|
+
console.log(new Date(), 'oplog fallback enabled', reason, message ? `(${message})` : '');
|
|
1547
|
+
|
|
1548
|
+
this.queueFullResync('oplog-fallback-enabled');
|
|
1549
|
+
|
|
1550
|
+
if (this._localOplogResyncIntervalMs > 0 && !this._localOplogResyncTimer) {
|
|
1551
|
+
this._localOplogResyncTimer = setInterval(() => {
|
|
1552
|
+
this.queueFullResync('oplog-fallback-poll');
|
|
1553
|
+
}, this._localOplogResyncIntervalMs);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
public getUseLocalOplog() {
|
|
1558
|
+
return this._useLocalOplog;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
public async handleMongoReconnect(reason: string): Promise<void> {
|
|
1562
|
+
if (this._useLocalOplog) {
|
|
1563
|
+
this.queueFullResync(`mongo-reconnect:${reason}`);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
const resumeToken = this._lastResumeToken || await this.loadResumeToken();
|
|
1569
|
+
await this.tailOpLog(resumeToken || undefined);
|
|
1570
|
+
}
|
|
1571
|
+
catch (error) {
|
|
1572
|
+
console.log(new Date(), 'Sub Manager', 'Mongo reconnect: tailOpLog restart failed', reason, error);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
this.queueFullResync(`mongo-reconnect:${reason}`);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
public notifyLocalOplog(collection: string, type = 'update') {
|
|
1579
|
+
if (!this._useLocalOplog) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
setImmediate(async () => {
|
|
1584
|
+
try {
|
|
1585
|
+
await this.invalidatePubsCache(collection, type);
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
// ignore local oplog cache invalidation errors
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private getResumeTokenDocId(): string {
|
|
1594
|
+
if (process.env.NODE_APP_INSTANCE) {
|
|
1595
|
+
return `oplog:${process.env.NODE_APP_INSTANCE}`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Fallback: avoid cross-process contention when NODE_APP_INSTANCE is not set.
|
|
1599
|
+
return `oplog:${process.pid}`;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
private async loadResumeToken(): Promise<ResumeToken | null> {
|
|
1603
|
+
try {
|
|
1604
|
+
const db = ResolveIOServer.getMainDB();
|
|
1605
|
+
if (!db) {
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const loadToken = async () => {
|
|
1610
|
+
const collection = db.collection<OplogResumeTokenDocument>(this.RESUME_TOKEN_COLLECTION);
|
|
1611
|
+
const doc = await collection.findOne({ _id: this.getResumeTokenDocId() });
|
|
1612
|
+
const token = doc?.token || null;
|
|
1613
|
+
|
|
1614
|
+
if (!token) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const maxAgeMs = this.resolveResumeTokenMaxAgeMs();
|
|
1619
|
+
if (maxAgeMs > 0 && doc?.updatedAt instanceof Date) {
|
|
1620
|
+
const ageMs = Date.now() - doc.updatedAt.getTime();
|
|
1621
|
+
if (ageMs > maxAgeMs) {
|
|
1622
|
+
await collection.deleteOne({ _id: this.getResumeTokenDocId() });
|
|
1623
|
+
console.log(new Date(), 'Sub Manager', 'Resume token expired, clearing', round(ageMs / 1000), 's');
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return token;
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
const mongoManager = ResolveIOServer.getMongoManager();
|
|
1632
|
+
return mongoManager ? await mongoManager.runWithoutSession(loadToken) : await loadToken();
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
console.log(new Date(), 'Sub Manager', 'Failed to load oplog resume token', error);
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
private async saveResumeToken(token: ResumeToken): Promise<void> {
|
|
1641
|
+
if (!token) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
try {
|
|
1646
|
+
const db = ResolveIOServer.getMainDB();
|
|
1647
|
+
if (!db) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const saveToken = async () => {
|
|
1652
|
+
await db.collection<OplogResumeTokenDocument>(this.RESUME_TOKEN_COLLECTION).updateOne(
|
|
1653
|
+
{ _id: this.getResumeTokenDocId() },
|
|
1654
|
+
{ $set: { token, updatedAt: new Date() } },
|
|
1655
|
+
{ upsert: true }
|
|
1656
|
+
);
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const mongoManager = ResolveIOServer.getMongoManager();
|
|
1660
|
+
if (mongoManager) {
|
|
1661
|
+
await mongoManager.runWithoutSession(saveToken);
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
await saveToken();
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
console.log(new Date(), 'Sub Manager', 'Failed to persist oplog resume token', error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
private async clearResumeToken(): Promise<void> {
|
|
1673
|
+
try {
|
|
1674
|
+
const db = ResolveIOServer.getMainDB();
|
|
1675
|
+
if (!db) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const clearToken = async () => {
|
|
1680
|
+
await db.collection<OplogResumeTokenDocument>(this.RESUME_TOKEN_COLLECTION)
|
|
1681
|
+
.deleteOne({ _id: this.getResumeTokenDocId() });
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
const mongoManager = ResolveIOServer.getMongoManager();
|
|
1685
|
+
if (mongoManager) {
|
|
1686
|
+
await mongoManager.runWithoutSession(clearToken);
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
await clearToken();
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
catch (error) {
|
|
1693
|
+
console.log(new Date(), 'Sub Manager', 'Failed to clear oplog resume token', error);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private queueResumeTokenSave(token: ResumeToken, options?: { force?: boolean }) {
|
|
1698
|
+
if (!token) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
this._lastResumeToken = token;
|
|
1703
|
+
|
|
1704
|
+
const now = Date.now();
|
|
1705
|
+
const shouldSave = !!options?.force || (now - this._lastResumeTokenSaveMs) >= this.RESUME_TOKEN_SAVE_INTERVAL_MS;
|
|
1706
|
+
|
|
1707
|
+
if (!shouldSave) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (this._resumeTokenSavePromise !== null) {
|
|
1712
|
+
if (options?.force) {
|
|
1713
|
+
this._resumeTokenSaveForcePending = true;
|
|
1714
|
+
}
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
this._lastResumeTokenSaveMs = now;
|
|
1719
|
+
const tokenToSave = this._lastResumeToken;
|
|
1720
|
+
|
|
1721
|
+
this._resumeTokenSavePromise = (async () => {
|
|
1722
|
+
try {
|
|
1723
|
+
await this.saveResumeToken(tokenToSave);
|
|
1724
|
+
}
|
|
1725
|
+
finally {
|
|
1726
|
+
this._resumeTokenSavePromise = null;
|
|
1727
|
+
if (this._resumeTokenSaveForcePending) {
|
|
1728
|
+
this._resumeTokenSaveForcePending = false;
|
|
1729
|
+
this.queueResumeTokenSave(this._lastResumeToken, { force: true });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
})();
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
private queueFullResync(reason: string) {
|
|
1736
|
+
if (this._fullResyncPromise !== null) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
this._fullResyncPromise = (async () => {
|
|
1741
|
+
try {
|
|
1742
|
+
await this.fullResyncSubscriptions(reason);
|
|
1743
|
+
}
|
|
1744
|
+
finally {
|
|
1745
|
+
this._fullResyncPromise = null;
|
|
1746
|
+
}
|
|
1747
|
+
})();
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
private async fullResyncSubscriptions(reason: string): Promise<void> {
|
|
1751
|
+
try {
|
|
1752
|
+
if (this._enableDebug) {
|
|
1753
|
+
console.log(new Date(), 'Sub Manager', 'Full subscription resync', reason, this._subscriptions.length);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
try {
|
|
1757
|
+
ResolveIOServer.getMongoManager()?.clearQueryCache?.(reason);
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
// ignore cache-clear errors
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
try {
|
|
1764
|
+
this._nodeCache?.flushAll?.();
|
|
1765
|
+
}
|
|
1766
|
+
catch {
|
|
1767
|
+
// ignore cache-clear errors
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const subs = this._subscriptions.slice();
|
|
1771
|
+
|
|
1772
|
+
for (const sub of subs) {
|
|
1773
|
+
if (!sub?.clients?.length) {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const pub = this._publications[sub.publication];
|
|
1778
|
+
if (!pub) {
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
if (sub.running) {
|
|
1783
|
+
sub.runAgain = true;
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (pub.user_specific) {
|
|
1788
|
+
for (const client of sub.clients) {
|
|
1789
|
+
const ws = this._websocketManager.getWebSocket(client.id_socket);
|
|
1790
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
1791
|
+
try {
|
|
1792
|
+
await this.sendDataToOneWithRetry(ws, client.messageId, sub, '', reason);
|
|
1793
|
+
}
|
|
1794
|
+
catch {
|
|
1795
|
+
// ignore individual failures
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
await this.sendDataToAllWithRetry(sub, '', reason);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
catch (error) {
|
|
1806
|
+
console.log(new Date(), 'Sub Manager', 'Full resync failed', reason, error);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Watch (tail) Mongo's operation log on the entire database (all insert/modify/delete will trigger this function)
|
|
1811
|
+
private async tailOpLog(resumeToken?: ResumeToken) {
|
|
1812
|
+
if (this._useLocalOplog) {
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
if (this._oplog$ && !this._oplog$.closed) {
|
|
1817
|
+
this._oplog$.removeAllListeners();
|
|
1818
|
+
await this._oplog$.close();
|
|
1819
|
+
this._oplog$ = null;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1823
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1824
|
+
|
|
1825
|
+
if (!this._oplog$ || this._oplog$.closed) {
|
|
1826
|
+
this._oplogRetryCount += 1;
|
|
1827
|
+
|
|
1828
|
+
if (this._oplogRetryCount > 5) {
|
|
1829
|
+
console.error('****************** TAIL OPLOG ERROR, RETRYING A BUNCH OF TIMES, KILLING PROCESS **************************');
|
|
1830
|
+
process.exit(1);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const watchDatabases = this.getWatchedDatabases();
|
|
1834
|
+
const pipeline = [
|
|
1835
|
+
{
|
|
1836
|
+
$match: {
|
|
1837
|
+
$and: [
|
|
1838
|
+
...(watchDatabases.length ? [{ 'ns.db': { $in: watchDatabases } }] : []),
|
|
1839
|
+
{'ns.coll': { $nin: [
|
|
1840
|
+
'log-method-latencies',
|
|
1841
|
+
'log-subscriptions',
|
|
1842
|
+
'logs',
|
|
1843
|
+
'counters',
|
|
1844
|
+
'cron-job-histories',
|
|
1845
|
+
'email-histories',
|
|
1846
|
+
'qb-soap-request-histories',
|
|
1847
|
+
'qb-soap-request-responses',
|
|
1848
|
+
'qb-soap-requests',
|
|
1849
|
+
'qb-soap-retries',
|
|
1850
|
+
this.RESUME_TOKEN_COLLECTION
|
|
1851
|
+
] }},
|
|
1852
|
+
{'ns.coll': { $not: /.*\.versions$/ }},
|
|
1853
|
+
{'ns.coll': { $not: /^monitor-/ }},
|
|
1854
|
+
]
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
];
|
|
1858
|
+
|
|
1859
|
+
let lastResumeToken: ResumeToken;
|
|
1860
|
+
let startedWithResumeToken = false;
|
|
1861
|
+
let sawChangeEvent = false;
|
|
1862
|
+
let resumeTokenInvalidated = false;
|
|
1863
|
+
const streamStartedAtMs = Date.now();
|
|
1864
|
+
|
|
1865
|
+
if (resumeToken) {
|
|
1866
|
+
lastResumeToken = resumeToken;
|
|
1867
|
+
try {
|
|
1868
|
+
this._oplog$ = ResolveIOServer.getMongoConnection().watch(pipeline, { resumeAfter: resumeToken, fullDocument: 'updateLookup' });
|
|
1869
|
+
startedWithResumeToken = true;
|
|
1870
|
+
}
|
|
1871
|
+
catch (error) {
|
|
1872
|
+
if (this.isChangeStreamUnsupported(error)) {
|
|
1873
|
+
await this.enableLocalOplogFallback('change-stream-unsupported', error);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (this.isChangeStreamDocumentTooLarge(error)) {
|
|
1877
|
+
await this.clearResumeToken();
|
|
1878
|
+
lastResumeToken = null;
|
|
1879
|
+
this.queueFullResync('oplog-change-stream-document-too-large');
|
|
1880
|
+
await this.enableLocalOplogFallback('change-stream-document-too-large', error);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (this._oplog$) {
|
|
1885
|
+
this._oplog$.removeAllListeners();
|
|
1886
|
+
await this._oplog$.close();
|
|
1887
|
+
this._oplog$ = null;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
await this.clearResumeToken();
|
|
1891
|
+
lastResumeToken = null;
|
|
1892
|
+
|
|
1893
|
+
console.log(new Date(), 'oplog resumeAfter failed, starting fresh', error);
|
|
1894
|
+
|
|
1895
|
+
try {
|
|
1896
|
+
this._oplog$ = ResolveIOServer.getMongoConnection().watch(pipeline, { fullDocument: 'updateLookup' });
|
|
1897
|
+
startedWithResumeToken = false;
|
|
1898
|
+
}
|
|
1899
|
+
catch (innerError) {
|
|
1900
|
+
if (this.isChangeStreamUnsupported(innerError)) {
|
|
1901
|
+
await this.enableLocalOplogFallback('change-stream-unsupported', innerError);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
throw innerError;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
this.queueFullResync('oplog-resumeAfter-failed');
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
try {
|
|
1913
|
+
this._oplog$ = ResolveIOServer.getMongoConnection().watch(pipeline, { fullDocument: 'updateLookup' });
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
if (this.isChangeStreamUnsupported(error)) {
|
|
1917
|
+
await this.enableLocalOplogFallback('change-stream-unsupported', error);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (this.isChangeStreamDocumentTooLarge(error)) {
|
|
1921
|
+
await this.enableLocalOplogFallback('change-stream-document-too-large', error);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
throw error;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
console.log(new Date(), 'oplog started', startedWithResumeToken ? '(resumeAfter)' : '');
|
|
1930
|
+
|
|
1931
|
+
this._oplog$.on('change', async (doc: ChangeStreamInsertDocument | ChangeStreamUpdateDocument | ChangeStreamReplaceDocument | ChangeStreamDeleteDocument) => {
|
|
1932
|
+
sawChangeEvent = true;
|
|
1933
|
+
this._oplogRetryCount = 0;
|
|
1934
|
+
|
|
1935
|
+
if (doc.ns) {
|
|
1936
|
+
if (this._enableDebug) {
|
|
1937
|
+
console.log(new Date(), 'Oplog Hit', doc.ns);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
let collection = doc.ns.coll;
|
|
1941
|
+
|
|
1942
|
+
if (!this._debugOplogCollections.some(a => a.collection === doc.ns.coll && a.type === doc.operationType)) {
|
|
1943
|
+
this._debugOplogCollections.push({
|
|
1944
|
+
collection: doc.ns.coll,
|
|
1945
|
+
type: doc.operationType,
|
|
1946
|
+
hits: 1
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
this._debugOplogCollections.find(a => a.collection === doc.ns.coll && a.type === doc.operationType).hits += 1;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
if (collection) {
|
|
1954
|
+
this._debugOplogHits += 1;
|
|
1955
|
+
const fullDocument = doc['fullDocument'];
|
|
1956
|
+
const docId = doc.documentKey && doc.documentKey['_id'] !== undefined ? doc.documentKey['_id'] : (fullDocument && fullDocument['_id'] !== undefined ? fullDocument['_id'] : null);
|
|
1957
|
+
|
|
1958
|
+
if (doc.operationType === 'insert') {
|
|
1959
|
+
if (collection === 'support-tickets') {
|
|
1960
|
+
if (this.serverConfig['ROOT_URL'] === 'https://resolveio.com') {
|
|
1961
|
+
await ResolveIOServer.getMainServer().getMethodManager().callMethod.call(ResolveIOServer.getMainServer().getMethodManager(), 'sendSupportTicketEmail', doc.documentKey['_id']);
|
|
1962
|
+
await this.invalidatePubsCache(collection, 'insert', docId, fullDocument);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
if (collection !== 'method-responses') {
|
|
1967
|
+
await this.invalidatePubsCache(collection, 'insert', docId, fullDocument);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
else if (doc.operationType === 'update' || doc.operationType === 'replace') {
|
|
1971
|
+
if (collection !== 'method-responses') {
|
|
1972
|
+
await this.invalidatePubsCache(collection, 'update', docId, fullDocument);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
else if (doc.operationType === 'delete') {
|
|
1976
|
+
if (collection !== 'method-responses') {
|
|
1977
|
+
await this.invalidatePubsCache(collection, 'delete', docId);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
if (collection === 'flags') {
|
|
1983
|
+
let flag = await Flags.findOne({ type: 'Enable Debug' });
|
|
1984
|
+
let dependencyFlag = await Flags.findOne({ type: 'Enable Dependency Debug' });
|
|
1985
|
+
|
|
1986
|
+
if (flag && flag.value) {
|
|
1987
|
+
this._enableDebug = true;
|
|
1988
|
+
}
|
|
1989
|
+
else {
|
|
1990
|
+
this._enableDebug = false;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (dependencyFlag && dependencyFlag.value) {
|
|
1994
|
+
this._enableDependencyDebug = true;
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
this._enableDependencyDebug = process.env.ENABLE_DEPENDENCY_DEBUG === 'true';
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
lastResumeToken = doc._id;
|
|
2002
|
+
this.queueResumeTokenSave(lastResumeToken);
|
|
2003
|
+
|
|
2004
|
+
if ((!process.env.NODE_APP_INSTANCE || process.env.NODE_APP_INSTANCE === '0') && (process.env.IS_WORKERS_ENABLED === 'false' || (process.env.IS_WORKER_INSTANCE === 'true' && process.env.WORKER_INDEX === '0'))) {
|
|
2005
|
+
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
this._oplog$.on('error', async error => {
|
|
2011
|
+
console.log(new Date(), 'oplog error', error);
|
|
2012
|
+
if (this.isChangeStreamUnsupported(error)) {
|
|
2013
|
+
await this.enableLocalOplogFallback('change-stream-unsupported', error);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (this.isChangeStreamDocumentTooLarge(error)) {
|
|
2017
|
+
await this.clearResumeToken();
|
|
2018
|
+
lastResumeToken = null;
|
|
2019
|
+
this.queueFullResync('oplog-change-stream-document-too-large');
|
|
2020
|
+
await this.enableLocalOplogFallback('change-stream-document-too-large', error);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
if (this.isResumeTokenInvalid(error)) {
|
|
2025
|
+
resumeTokenInvalidated = true;
|
|
2026
|
+
await this.clearResumeToken();
|
|
2027
|
+
lastResumeToken = null;
|
|
2028
|
+
this.queueFullResync('oplog-resume-token-invalid');
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
await this._oplog$.close();
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
this._oplog$.on('end', async () => {
|
|
2035
|
+
console.log(new Date(), 'oplog end');
|
|
2036
|
+
await this._oplog$.close();
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
this._oplog$.on('close', async () => {
|
|
2040
|
+
console.log(new Date(), 'oplog close');
|
|
2041
|
+
const retryThresholdReached = this._oplogRetryCount >= this.RESUME_TOKEN_AUTO_HEAL_RETRY_THRESHOLD;
|
|
2042
|
+
const shouldAutoHealResumeToken = startedWithResumeToken
|
|
2043
|
+
&& !!lastResumeToken
|
|
2044
|
+
&& !sawChangeEvent
|
|
2045
|
+
&& (resumeTokenInvalidated || retryThresholdReached);
|
|
2046
|
+
|
|
2047
|
+
if (shouldAutoHealResumeToken) {
|
|
2048
|
+
await this.clearResumeToken();
|
|
2049
|
+
lastResumeToken = null;
|
|
2050
|
+
if (!resumeTokenInvalidated) {
|
|
2051
|
+
this.queueFullResync('oplog-resume-token-auto-heal');
|
|
2052
|
+
}
|
|
2053
|
+
console.log(
|
|
2054
|
+
new Date(),
|
|
2055
|
+
'Sub Manager',
|
|
2056
|
+
'Auto-healed stale oplog resume token',
|
|
2057
|
+
{ retryCount: this._oplogRetryCount, streamLifetimeMs: Date.now() - streamStartedAtMs }
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
if (lastResumeToken) {
|
|
2062
|
+
this.queueResumeTokenSave(lastResumeToken, { force: true });
|
|
2063
|
+
}
|
|
2064
|
+
this._oplog$.removeAllListeners();
|
|
2065
|
+
this._oplog$ = null;
|
|
2066
|
+
await this.tailOpLog(lastResumeToken);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
private packCachePayload(data: any): Buffer | null {
|
|
2072
|
+
if (data === undefined) {
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
try {
|
|
2077
|
+
const packed = pack(data);
|
|
2078
|
+
return Buffer.isBuffer(packed) ? packed : Buffer.from(packed);
|
|
2079
|
+
}
|
|
2080
|
+
catch (err) {
|
|
2081
|
+
if (this._enableDebug) {
|
|
2082
|
+
console.log(new Date(), 'Sub Cache', 'Pack Failed', err?.message || err);
|
|
2083
|
+
}
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
private decodeCachePayload(raw: any): any | null {
|
|
2089
|
+
if (!raw) {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
if (Buffer.isBuffer(raw)) {
|
|
2094
|
+
try {
|
|
2095
|
+
return unpack(raw);
|
|
2096
|
+
}
|
|
2097
|
+
catch (err) {
|
|
2098
|
+
if (this._enableDebug) {
|
|
2099
|
+
console.log(new Date(), 'Sub Cache', 'Unpack Failed', err?.message || err);
|
|
2100
|
+
}
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (raw instanceof Uint8Array) {
|
|
2106
|
+
try {
|
|
2107
|
+
return unpack(raw);
|
|
2108
|
+
}
|
|
2109
|
+
catch (err) {
|
|
2110
|
+
if (this._enableDebug) {
|
|
2111
|
+
console.log(new Date(), 'Sub Cache', 'Unpack Failed', err?.message || err);
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
if (raw instanceof ArrayBuffer) {
|
|
2118
|
+
try {
|
|
2119
|
+
return unpack(new Uint8Array(raw));
|
|
2120
|
+
}
|
|
2121
|
+
catch (err) {
|
|
2122
|
+
if (this._enableDebug) {
|
|
2123
|
+
console.log(new Date(), 'Sub Cache', 'Unpack Failed', err?.message || err);
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Treat legacy string caches as invalid to avoid JSON parsing in the hot path.
|
|
2130
|
+
if (typeof raw === 'string') {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
return raw;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
private getCacheBuffer(raw: any): Buffer | null {
|
|
2138
|
+
if (!raw) {
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (Buffer.isBuffer(raw)) {
|
|
2143
|
+
return raw;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (raw instanceof Uint8Array) {
|
|
2147
|
+
return Buffer.from(raw);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (raw instanceof ArrayBuffer) {
|
|
2151
|
+
return Buffer.from(new Uint8Array(raw));
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
private shouldCachePayload(sub: ActiveSubscriptionModel, payload: Buffer | Uint8Array | null): boolean {
|
|
2158
|
+
if (!payload || payload.byteLength >= 1000000) {
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (sub.collections.includes('logs')) {
|
|
2163
|
+
return false;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if (sub.collections.find(a => a.endsWith('.versions'))) {
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (sub.collections.find(a => a.startsWith('monitor-'))) {
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
private buffersEqual(a: Uint8Array | Buffer | null, b: Uint8Array | Buffer | null): boolean {
|
|
2178
|
+
if (!a || !b) {
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (a.byteLength !== b.byteLength) {
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const toBuffer = (value: Uint8Array | Buffer): Buffer => {
|
|
2187
|
+
if (Buffer.isBuffer(value)) {
|
|
2188
|
+
return value;
|
|
2189
|
+
}
|
|
2190
|
+
const buffer = value.buffer as ArrayBuffer;
|
|
2191
|
+
return Buffer.from(buffer, value.byteOffset, value.byteLength);
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
const aBuf = toBuffer(a);
|
|
2195
|
+
const bBuf = toBuffer(b);
|
|
2196
|
+
return aBuf.equals(bBuf as Uint8Array);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
private async processSubscription(sub: ActiveSubscriptionModel, ws: WebSocket, messageId: number) {
|
|
2200
|
+
if (!this._publications[sub.publication].user_specific) {
|
|
2201
|
+
if (sub.cacheId) {
|
|
2202
|
+
try {
|
|
2203
|
+
const cachedRaw = this._nodeCache.get(sub.cacheId);
|
|
2204
|
+
const cachedBuffer = this.getCacheBuffer(cachedRaw);
|
|
2205
|
+
|
|
2206
|
+
if (cachedBuffer && cachedBuffer.byteLength) {
|
|
2207
|
+
if (this._enableDebug) {
|
|
2208
|
+
console.log(new Date(), 'Process Sub, Cache (packed)', sub.publication);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
this._websocketManager.sendPackedBuffer(ws, messageId, false, cachedBuffer, 'msgpack');
|
|
2212
|
+
this.logAiSubSend({
|
|
2213
|
+
publication: sub.publication,
|
|
2214
|
+
subscriptionKey: sub.subscriptionKey,
|
|
2215
|
+
messageId,
|
|
2216
|
+
id_socket: ws['id_socket'],
|
|
2217
|
+
clients: 1,
|
|
2218
|
+
payloadBytes: cachedBuffer.byteLength,
|
|
2219
|
+
packed: true,
|
|
2220
|
+
cacheHit: true,
|
|
2221
|
+
collection: '',
|
|
2222
|
+
type: 'cache',
|
|
2223
|
+
subscriptionData: sub.subscriptionData
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
const cacheData = this.decodeCachePayload(cachedRaw);
|
|
2228
|
+
if (cacheData === null || cacheData === undefined) {
|
|
2229
|
+
throw new Error('cache-miss');
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
let serverRes: ServerResponseModel = {
|
|
2233
|
+
messageId: messageId,
|
|
2234
|
+
hasError: false,
|
|
2235
|
+
data: cacheData
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
if (this._enableDebug) {
|
|
2239
|
+
console.log(new Date(), 'Process Sub, Cache', sub.publication);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
this.sendWS(ws, serverRes);
|
|
2243
|
+
this.logAiSubSend({
|
|
2244
|
+
publication: sub.publication,
|
|
2245
|
+
subscriptionKey: sub.subscriptionKey,
|
|
2246
|
+
messageId,
|
|
2247
|
+
id_socket: ws['id_socket'],
|
|
2248
|
+
clients: 1,
|
|
2249
|
+
payloadBytes: cacheData ? Buffer.byteLength(JSON.stringify(cacheData)) : null,
|
|
2250
|
+
packed: false,
|
|
2251
|
+
cacheHit: true,
|
|
2252
|
+
collection: '',
|
|
2253
|
+
type: 'cache',
|
|
2254
|
+
subscriptionData: sub.subscriptionData
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
this._nodeCache.del(sub.cacheId);
|
|
2260
|
+
sub.cacheId = 0;
|
|
2261
|
+
|
|
2262
|
+
await this.sendDataToAllWithRetry(sub, '', 'newSub');
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
else {
|
|
2266
|
+
if (this._enableDebug) {
|
|
2267
|
+
console.log(new Date(), 'Process Sub, Non - Cache', sub.publication, sub.running);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
if (sub.running) {
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
await this.sendDataToAllWithRetry(sub, '', 'newSub');
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
if (this._enableDebug) {
|
|
2279
|
+
console.log(new Date(), 'Process Sub Specific, Non - Cache', sub.publication, sub.running);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
if (sub.running) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
await this.sendDataToOneWithRetry(ws, messageId, sub, '', 'newSub');
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
private async sendDataToOne(ws: WebSocket, messageId: number, sub: ActiveSubscriptionModel, collection: string, type: string) {
|
|
2291
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
let monitor = this._monitorManagerFunction.startMonitorFunction('User Specific Publication', sub.publication, '', '', sub.subscriptionData);
|
|
2296
|
+
let dependencySnapshot: DependencyContextSnapshot;
|
|
2297
|
+
let res;
|
|
2298
|
+
try {
|
|
2299
|
+
await ResolveIOServer.getMainServer().getMethodManager().callMethod.call(ResolveIOServer.getMainServer().getMethodManager(), 'insertSubscriptionLog', type, sub.publication, collection, JSON.stringify(sub.subscriptionData));
|
|
2300
|
+
|
|
2301
|
+
const execution = await this.runPublicationExecution(sub, ws['id_user']);
|
|
2302
|
+
res = execution.result;
|
|
2303
|
+
dependencySnapshot = execution.snapshot;
|
|
2304
|
+
this.updateSubscriptionDependencies(sub, dependencySnapshot);
|
|
2305
|
+
const packedRes = execution.packedResult ?? this.packCachePayload(res);
|
|
2306
|
+
const payloadBytes = packedRes ? packedRes.byteLength : null;
|
|
2307
|
+
|
|
2308
|
+
if (execution.packedResult) {
|
|
2309
|
+
this._websocketManager.sendPackedBuffer(
|
|
2310
|
+
ws,
|
|
2311
|
+
messageId,
|
|
2312
|
+
false,
|
|
2313
|
+
execution.packedResult,
|
|
2314
|
+
execution.encoding || 'msgpack',
|
|
2315
|
+
{ passThrough: !!execution.workerUsed }
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
let serverRes: ServerResponseModel = {
|
|
2320
|
+
messageId: messageId,
|
|
2321
|
+
hasError: false,
|
|
2322
|
+
data: res
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
this.sendWS(ws, serverRes);
|
|
2326
|
+
}
|
|
2327
|
+
this.logAiSubSend({
|
|
2328
|
+
publication: sub.publication,
|
|
2329
|
+
subscriptionKey: sub.subscriptionKey,
|
|
2330
|
+
messageId,
|
|
2331
|
+
id_socket: ws['id_socket'],
|
|
2332
|
+
clients: 1,
|
|
2333
|
+
payloadBytes,
|
|
2334
|
+
packed: !!execution.packedResult,
|
|
2335
|
+
cacheHit: false,
|
|
2336
|
+
collection,
|
|
2337
|
+
type,
|
|
2338
|
+
subscriptionData: sub.subscriptionData
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
catch (err) {
|
|
2342
|
+
const { error: normalizedError, correlationId } = ensureErrorWithCorrelation(err);
|
|
2343
|
+
|
|
2344
|
+
let serverRes: ServerResponseModel = {
|
|
2345
|
+
messageId: messageId,
|
|
2346
|
+
hasError: true,
|
|
2347
|
+
data: Object.assign({}, normalizedError, { correlationId })
|
|
2348
|
+
};
|
|
2349
|
+
|
|
2350
|
+
this.sendWS(ws, serverRes);
|
|
2351
|
+
|
|
2352
|
+
const errorPayload = {
|
|
2353
|
+
publication: sub.publication,
|
|
2354
|
+
subscriptionData: sub.subscriptionData,
|
|
2355
|
+
type,
|
|
2356
|
+
collection,
|
|
2357
|
+
messageId,
|
|
2358
|
+
correlationId,
|
|
2359
|
+
error: {
|
|
2360
|
+
name: normalizedError?.name,
|
|
2361
|
+
message: normalizedError?.message,
|
|
2362
|
+
stack: normalizedError?.stack,
|
|
2363
|
+
code: normalizedError?.code,
|
|
2364
|
+
codeName: normalizedError?.codeName
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
await ErrorReporter.report({
|
|
2369
|
+
sourceApp: 'subscription-manager',
|
|
2370
|
+
message: 'SERVER - Error Detected - ' + this.serverConfig['CLIENT_NAME'],
|
|
2371
|
+
environment: this.serverConfig?.ROOT_URL,
|
|
2372
|
+
clientSlug: ResolveIOServer.getClientName(),
|
|
2373
|
+
clientName: this.serverConfig['CLIENT_NAME'],
|
|
2374
|
+
stack: normalizedError?.stack,
|
|
2375
|
+
context: errorPayload,
|
|
2376
|
+
metadata: {
|
|
2377
|
+
context: 'subscription-sendDataToOne',
|
|
2378
|
+
publication: sub.publication,
|
|
2379
|
+
correlationId
|
|
2380
|
+
},
|
|
2381
|
+
correlationId
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
finally {
|
|
2385
|
+
await this._monitorManagerFunction.finishMonitorFunction(monitor);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// Fetch pub once, send to all clients linked to this pub
|
|
2390
|
+
private async sendDataToAll(sub: ActiveSubscriptionModel, collection: string, type: string) {
|
|
2391
|
+
const isAiPublication = this.isAiPublication(sub.publication);
|
|
2392
|
+
if (isAiPublication && sub.clients.length > 1) {
|
|
2393
|
+
const seenSockets = new Set<string>();
|
|
2394
|
+
sub.clients = sub.clients.filter(client => {
|
|
2395
|
+
if (seenSockets.has(client.id_socket)) {
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
seenSockets.add(client.id_socket);
|
|
2399
|
+
return true;
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
const activeClients: { client: ActiveSubscriptionClientModel; ws: WebSocket }[] = [];
|
|
2403
|
+
if (sub.clients.length) {
|
|
2404
|
+
for (const client of sub.clients) {
|
|
2405
|
+
const ws = this._websocketManager.getWebSocket(client.id_socket);
|
|
2406
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2407
|
+
activeClients.push({ client, ws });
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
if (!activeClients.length) {
|
|
2413
|
+
if (sub.cacheId) {
|
|
2414
|
+
this._nodeCache.del(sub.cacheId);
|
|
2415
|
+
sub.cacheId = 0;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
let subIndex = this._subscriptions.findIndex(a => a.publication === sub.publication && a.subscriptionKey === sub.subscriptionKey);
|
|
2419
|
+
if (subIndex >= 0) {
|
|
2420
|
+
this._subscriptions.splice(subIndex, 1);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
else {
|
|
2426
|
+
if (activeClients.length !== sub.clients.length) {
|
|
2427
|
+
sub.clients = activeClients.map(entry => entry.client);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
let monitor = this._monitorManagerFunction.startMonitorFunction('Publication', sub.publication, '', '', sub.subscriptionData);
|
|
2431
|
+
let res;
|
|
2432
|
+
let dependencySnapshot: DependencyContextSnapshot;
|
|
2433
|
+
|
|
2434
|
+
try {
|
|
2435
|
+
if (sub.publication !== 'superadminAPM' && sub.publication !== 'loggedInUsers') {
|
|
2436
|
+
await ResolveIOServer.getMainServer().getMethodManager().callMethod.call(ResolveIOServer.getMainServer().getMethodManager(), 'insertSubscriptionLog', type, sub.publication, collection, JSON.stringify(sub.subscriptionData));
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const execution = await this.runPublicationExecution(sub);
|
|
2440
|
+
res = execution.result;
|
|
2441
|
+
dependencySnapshot = execution.snapshot;
|
|
2442
|
+
this.updateSubscriptionDependencies(sub, dependencySnapshot);
|
|
2443
|
+
const packedRes = execution.packedResult ?? this.packCachePayload(res);
|
|
2444
|
+
const passThrough = !!execution.workerUsed;
|
|
2445
|
+
const payloadBytes = packedRes ? packedRes.byteLength : null;
|
|
2446
|
+
const shouldCache = this.shouldCachePayload(sub, packedRes);
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
if (sub.cacheId) {
|
|
2450
|
+
const cachedBuffer = this.getCacheBuffer(this._nodeCache.get(sub.cacheId));
|
|
2451
|
+
const isSame = this.buffersEqual(cachedBuffer, packedRes);
|
|
2452
|
+
|
|
2453
|
+
if (!isSame) {
|
|
2454
|
+
const sendStartMs = Date.now();
|
|
2455
|
+
let sentClients = 0;
|
|
2456
|
+
for (const entry of activeClients) {
|
|
2457
|
+
const { client, ws } = entry;
|
|
2458
|
+
if (packedRes) {
|
|
2459
|
+
this._websocketManager.sendPackedBuffer(
|
|
2460
|
+
ws,
|
|
2461
|
+
client.messageId,
|
|
2462
|
+
false,
|
|
2463
|
+
packedRes,
|
|
2464
|
+
execution.encoding || 'msgpack',
|
|
2465
|
+
{ passThrough }
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
else {
|
|
2469
|
+
let serverRes: ServerResponseModel = {
|
|
2470
|
+
messageId: client.messageId,
|
|
2471
|
+
hasError: false,
|
|
2472
|
+
data: res
|
|
2473
|
+
};
|
|
2474
|
+
|
|
2475
|
+
this.sendWS(ws, serverRes);
|
|
2476
|
+
}
|
|
2477
|
+
if (isAiPublication) {
|
|
2478
|
+
this.logAiSubSend({
|
|
2479
|
+
publication: sub.publication,
|
|
2480
|
+
subscriptionKey: sub.subscriptionKey,
|
|
2481
|
+
messageId: client.messageId,
|
|
2482
|
+
id_socket: ws['id_socket'],
|
|
2483
|
+
clients: activeClients.length,
|
|
2484
|
+
payloadBytes,
|
|
2485
|
+
packed: !!packedRes,
|
|
2486
|
+
cacheHit: false,
|
|
2487
|
+
collection,
|
|
2488
|
+
type,
|
|
2489
|
+
subscriptionData: sub.subscriptionData
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
sentClients += 1;
|
|
2493
|
+
}
|
|
2494
|
+
this.logSubSend(sub, {
|
|
2495
|
+
collection,
|
|
2496
|
+
type,
|
|
2497
|
+
clients: sentClients,
|
|
2498
|
+
durationMs: Date.now() - sendStartMs,
|
|
2499
|
+
payloadBytes
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
this._nodeCache.del(sub.cacheId);
|
|
2503
|
+
|
|
2504
|
+
if (shouldCache) {
|
|
2505
|
+
this._nodeCache.set(sub.cacheId, packedRes);
|
|
2506
|
+
}
|
|
2507
|
+
else {
|
|
2508
|
+
sub.cacheId = 0;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
else if (!cachedBuffer && shouldCache) {
|
|
2512
|
+
this._nodeCache.set(sub.cacheId, packedRes);
|
|
2513
|
+
}
|
|
2514
|
+
else if (!shouldCache) {
|
|
2515
|
+
this._nodeCache.del(sub.cacheId);
|
|
2516
|
+
sub.cacheId = 0;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
else {
|
|
2520
|
+
const sendStartMs = Date.now();
|
|
2521
|
+
let sentClients = 0;
|
|
2522
|
+
for (const entry of activeClients) {
|
|
2523
|
+
const { client, ws } = entry;
|
|
2524
|
+
if (packedRes) {
|
|
2525
|
+
this._websocketManager.sendPackedBuffer(
|
|
2526
|
+
ws,
|
|
2527
|
+
client.messageId,
|
|
2528
|
+
false,
|
|
2529
|
+
packedRes,
|
|
2530
|
+
execution.encoding || 'msgpack',
|
|
2531
|
+
{ passThrough }
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
else {
|
|
2535
|
+
let serverRes: ServerResponseModel = {
|
|
2536
|
+
messageId: client.messageId,
|
|
2537
|
+
hasError: false,
|
|
2538
|
+
data: res
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
this.sendWS(ws, serverRes);
|
|
2542
|
+
}
|
|
2543
|
+
if (isAiPublication) {
|
|
2544
|
+
this.logAiSubSend({
|
|
2545
|
+
publication: sub.publication,
|
|
2546
|
+
subscriptionKey: sub.subscriptionKey,
|
|
2547
|
+
messageId: client.messageId,
|
|
2548
|
+
id_socket: ws['id_socket'],
|
|
2549
|
+
clients: activeClients.length,
|
|
2550
|
+
payloadBytes,
|
|
2551
|
+
packed: !!packedRes,
|
|
2552
|
+
cacheHit: false,
|
|
2553
|
+
collection,
|
|
2554
|
+
type,
|
|
2555
|
+
subscriptionData: sub.subscriptionData
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
sentClients += 1;
|
|
2559
|
+
}
|
|
2560
|
+
this.logSubSend(sub, {
|
|
2561
|
+
collection,
|
|
2562
|
+
type,
|
|
2563
|
+
clients: sentClients,
|
|
2564
|
+
durationMs: Date.now() - sendStartMs,
|
|
2565
|
+
payloadBytes
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
if (shouldCache) {
|
|
2569
|
+
sub.cacheId = this._cacheId++;
|
|
2570
|
+
this._nodeCache.set(sub.cacheId, packedRes);
|
|
2571
|
+
|
|
2572
|
+
const nodeCacheSize = this._nodeCache.getStats().vsize;
|
|
2573
|
+
|
|
2574
|
+
if (nodeCacheSize > this._heapLimit) {
|
|
2575
|
+
// Evict cache entries as needed
|
|
2576
|
+
let deleteCount = 0;
|
|
2577
|
+
const subArr = this._subscriptions.filter(a => a.cacheId && !a.clients.length);
|
|
2578
|
+
|
|
2579
|
+
for (let zz = 0; zz < subArr.length; zz++) {
|
|
2580
|
+
this._debugRemoveCacheHits += 1;
|
|
2581
|
+
this._nodeCache.del(subArr[zz].cacheId);
|
|
2582
|
+
subArr[zz].cacheId = 0;
|
|
2583
|
+
deleteCount += 1;
|
|
2584
|
+
if (this._nodeCache.getStats().vsize < this._heapLimit * 0.75) {
|
|
2585
|
+
break;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (this._enableDebug) {
|
|
2590
|
+
console.log(
|
|
2591
|
+
'Sub Cache: ' +
|
|
2592
|
+
'Too Big - ' +
|
|
2593
|
+
sub.publication +
|
|
2594
|
+
' - Deleted: ' +
|
|
2595
|
+
deleteCount +
|
|
2596
|
+
' - ' +
|
|
2597
|
+
nodeCacheSize
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
else {
|
|
2603
|
+
sub.cacheId = 0;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
catch (err) {
|
|
2608
|
+
const { error: normalizedError, correlationId } = ensureErrorWithCorrelation(err);
|
|
2609
|
+
|
|
2610
|
+
for (let client of sub.clients) {
|
|
2611
|
+
let ws = this._websocketManager.getWebSocket(client.id_socket);
|
|
2612
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2613
|
+
let serverRes: ServerResponseModel = {
|
|
2614
|
+
messageId: client.messageId,
|
|
2615
|
+
hasError: true,
|
|
2616
|
+
data: Object.assign({}, normalizedError, { correlationId })
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2619
|
+
this.sendWS(ws, serverRes);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
const errorPayload = {
|
|
2624
|
+
publication: sub.publication,
|
|
2625
|
+
subscriptionData: sub.subscriptionData,
|
|
2626
|
+
type,
|
|
2627
|
+
collection,
|
|
2628
|
+
correlationId,
|
|
2629
|
+
error: {
|
|
2630
|
+
name: normalizedError?.name,
|
|
2631
|
+
message: normalizedError?.message,
|
|
2632
|
+
stack: normalizedError?.stack,
|
|
2633
|
+
code: normalizedError?.code,
|
|
2634
|
+
codeName: normalizedError?.codeName
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
await ErrorReporter.report({
|
|
2639
|
+
sourceApp: 'subscription-manager',
|
|
2640
|
+
message: 'SERVER - Error Detected - ' + this.serverConfig['CLIENT_NAME'],
|
|
2641
|
+
environment: this.serverConfig?.ROOT_URL,
|
|
2642
|
+
clientSlug: ResolveIOServer.getClientName(),
|
|
2643
|
+
clientName: this.serverConfig['CLIENT_NAME'],
|
|
2644
|
+
stack: normalizedError?.stack,
|
|
2645
|
+
context: errorPayload,
|
|
2646
|
+
metadata: {
|
|
2647
|
+
context: 'subscription-sendDataToAll',
|
|
2648
|
+
publication: sub.publication,
|
|
2649
|
+
correlationId
|
|
2650
|
+
},
|
|
2651
|
+
correlationId
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
finally {
|
|
2655
|
+
await this._monitorManagerFunction.finishMonitorFunction(monitor);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
private ensureDependencyContainers(sub: ActiveSubscriptionModel) {
|
|
2661
|
+
if (!sub.collectionDependencies) {
|
|
2662
|
+
sub.collectionDependencies = new Map();
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
if (!sub.collectionFilters) {
|
|
2666
|
+
sub.collectionFilters = new Map();
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
if (!sub.watchAllCollections) {
|
|
2670
|
+
sub.watchAllCollections = new Set();
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
if (!sub.collectionQueryMeta) {
|
|
2674
|
+
sub.collectionQueryMeta = new Map();
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
private updateSubscriptionDependencies(sub: ActiveSubscriptionModel, snapshot?: DependencyContextSnapshot) {
|
|
2679
|
+
this.ensureDependencyContainers(sub);
|
|
2680
|
+
|
|
2681
|
+
if (!snapshot) {
|
|
2682
|
+
sub.collectionDependencies.clear();
|
|
2683
|
+
sub.collectionFilters.clear();
|
|
2684
|
+
sub.watchAllCollections.clear();
|
|
2685
|
+
sub.collectionQueryMeta.clear();
|
|
2686
|
+
this.dependencyDebug('Cleared dependency snapshot', { publication: sub.publication });
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
sub.collectionDependencies = snapshot.dependencies;
|
|
2691
|
+
sub.collectionFilters = snapshot.filters;
|
|
2692
|
+
sub.watchAllCollections = snapshot.watchAllCollections;
|
|
2693
|
+
sub.collectionQueryMeta = snapshot.queryMetadata;
|
|
2694
|
+
this.logDependencySnapshot(sub, 'Snapshot updated');
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
private normalizeDocumentId(rawId: any): string {
|
|
2698
|
+
if (rawId === null || rawId === undefined) {
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
if (typeof rawId === 'string') {
|
|
2703
|
+
return rawId;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
if (typeof rawId === 'object' && typeof rawId.toHexString === 'function') {
|
|
2707
|
+
return rawId.toHexString();
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
if (typeof rawId?.toString === 'function') {
|
|
2711
|
+
const str = rawId.toString();
|
|
2712
|
+
if (str && str !== '[object Object]') {
|
|
2713
|
+
return str;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
return null;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
private getDocumentIdQueryCandidates(documentId: any): any[] {
|
|
2721
|
+
const candidates: any[] = [];
|
|
2722
|
+
|
|
2723
|
+
if (documentId === undefined || documentId === null) {
|
|
2724
|
+
return [documentId];
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
const addCandidate = value => {
|
|
2728
|
+
if (value === undefined || value === null) {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
if (!candidates.includes(value)) {
|
|
2733
|
+
candidates.push(value);
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
addCandidate(documentId);
|
|
2738
|
+
|
|
2739
|
+
if (typeof documentId === 'object') {
|
|
2740
|
+
if (typeof documentId.toHexString === 'function') {
|
|
2741
|
+
addCandidate(documentId.toHexString());
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
if (typeof documentId.toString === 'function') {
|
|
2745
|
+
const str = documentId.toString();
|
|
2746
|
+
if (str && str !== '[object Object]') {
|
|
2747
|
+
addCandidate(str);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
else if (typeof documentId !== 'string') {
|
|
2752
|
+
addCandidate(String(documentId));
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
return candidates;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
private isFilterSafeForInMemory(filter: any): boolean {
|
|
2759
|
+
if (!filter || typeof filter !== 'object') {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const allowedOperators = new Set([
|
|
2764
|
+
'$and',
|
|
2765
|
+
'$or',
|
|
2766
|
+
'$nor',
|
|
2767
|
+
'$in',
|
|
2768
|
+
'$nin',
|
|
2769
|
+
'$eq',
|
|
2770
|
+
'$ne',
|
|
2771
|
+
'$gt',
|
|
2772
|
+
'$gte',
|
|
2773
|
+
'$lt',
|
|
2774
|
+
'$lte',
|
|
2775
|
+
'$exists',
|
|
2776
|
+
'$size',
|
|
2777
|
+
'$all',
|
|
2778
|
+
'$elemMatch',
|
|
2779
|
+
'$regex',
|
|
2780
|
+
'$options'
|
|
2781
|
+
]);
|
|
2782
|
+
|
|
2783
|
+
const disallowedOperators = new Set([
|
|
2784
|
+
'$where',
|
|
2785
|
+
'$expr',
|
|
2786
|
+
'$text',
|
|
2787
|
+
'$geoWithin',
|
|
2788
|
+
'$geoIntersects',
|
|
2789
|
+
'$near',
|
|
2790
|
+
'$nearSphere',
|
|
2791
|
+
'$jsonSchema',
|
|
2792
|
+
'$mod',
|
|
2793
|
+
'$type',
|
|
2794
|
+
'$bitsAllSet',
|
|
2795
|
+
'$bitsAllClear',
|
|
2796
|
+
'$bitsAnySet',
|
|
2797
|
+
'$bitsAnyClear'
|
|
2798
|
+
]);
|
|
2799
|
+
|
|
2800
|
+
const walk = (value: any): boolean => {
|
|
2801
|
+
if (Array.isArray(value)) {
|
|
2802
|
+
return value.every(item => walk(item));
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
if (!value || typeof value !== 'object') {
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2810
|
+
if (key.startsWith('$')) {
|
|
2811
|
+
if (disallowedOperators.has(key)) {
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
if (!allowedOperators.has(key)) {
|
|
2815
|
+
return false;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
if (!walk(entry)) {
|
|
2820
|
+
return false;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
return true;
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2827
|
+
return walk(filter);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
private documentIdMatchesEvent(documentId: any, document?: any): boolean {
|
|
2831
|
+
if (!document || documentId === undefined || documentId === null) {
|
|
2832
|
+
return true;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
const eventCandidates = this.getDocumentIdQueryCandidates(documentId)
|
|
2836
|
+
.map(id => this.normalizeDocumentId(id))
|
|
2837
|
+
.filter(Boolean);
|
|
2838
|
+
const docCandidates = this.getDocumentIdQueryCandidates(document?._id)
|
|
2839
|
+
.map(id => this.normalizeDocumentId(id))
|
|
2840
|
+
.filter(Boolean);
|
|
2841
|
+
|
|
2842
|
+
if (!eventCandidates.length || !docCandidates.length) {
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
const docSet = new Set(docCandidates);
|
|
2847
|
+
|
|
2848
|
+
for (const candidate of eventCandidates) {
|
|
2849
|
+
if (docSet.has(candidate)) {
|
|
2850
|
+
return true;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
private resolveFilterMatch(filter: any, document?: any, documentId?: any): boolean | null {
|
|
2858
|
+
if (!document || !filter || typeof filter !== 'object') {
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
if (!this.isFilterSafeForInMemory(filter)) {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
if (!this.documentIdMatchesEvent(documentId, document)) {
|
|
2867
|
+
return false;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
try {
|
|
2871
|
+
const matcher = sift(filter);
|
|
2872
|
+
return matcher(document);
|
|
2873
|
+
}
|
|
2874
|
+
catch {
|
|
2875
|
+
return null;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
private async documentMatchesFilter(collection: string, documentId: any, filter: any, document?: any): Promise<boolean> {
|
|
2880
|
+
try {
|
|
2881
|
+
const inMemoryMatch = this.resolveFilterMatch(filter, document, documentId);
|
|
2882
|
+
if (inMemoryMatch !== null) {
|
|
2883
|
+
return inMemoryMatch;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
const db = ResolveIOServer.getMainDB();
|
|
2887
|
+
const filterCopy = deepCopy(filter) || {};
|
|
2888
|
+
const idCandidates = this.getDocumentIdQueryCandidates(documentId);
|
|
2889
|
+
|
|
2890
|
+
for (const idValue of idCandidates) {
|
|
2891
|
+
const combinedFilter = {
|
|
2892
|
+
$and: [
|
|
2893
|
+
{ _id: idValue },
|
|
2894
|
+
filterCopy
|
|
2895
|
+
]
|
|
2896
|
+
};
|
|
2897
|
+
|
|
2898
|
+
const doc = await db.collection(collection).findOne(combinedFilter);
|
|
2899
|
+
if (doc) {
|
|
2900
|
+
return true;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
return false;
|
|
2905
|
+
}
|
|
2906
|
+
catch {
|
|
2907
|
+
return true;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
private async shouldInvalidateSubscription(sub: ActiveSubscriptionModel, collection: string, type: string, documentId?: any, document?: any): Promise<boolean> {
|
|
2912
|
+
this.ensureDependencyContainers(sub);
|
|
2913
|
+
|
|
2914
|
+
const normalizedDocumentId = this.normalizeDocumentId(documentId);
|
|
2915
|
+
const trackedIds = sub.collectionDependencies.get(collection);
|
|
2916
|
+
const filters = sub.collectionFilters.get(collection);
|
|
2917
|
+
const hasTrackedIds = !!(trackedIds && trackedIds.size);
|
|
2918
|
+
const hasFilters = !!(filters && filters.length);
|
|
2919
|
+
const hasDependencyData =
|
|
2920
|
+
(sub.collectionDependencies.get(collection)?.size || 0) > 0 ||
|
|
2921
|
+
(sub.collectionFilters.get(collection)?.length || 0) > 0 ||
|
|
2922
|
+
sub.watchAllCollections.has(collection);
|
|
2923
|
+
|
|
2924
|
+
if (!normalizedDocumentId) {
|
|
2925
|
+
this.dependencyDebug('Invalidate due to missing documentId', { publication: sub.publication, collection, type });
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (!hasDependencyData) {
|
|
2930
|
+
this.dependencyDebug('Invalidate due to missing dependency metadata', {
|
|
2931
|
+
publication: sub.publication,
|
|
2932
|
+
collection,
|
|
2933
|
+
type
|
|
2934
|
+
});
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
if (sub.watchAllCollections.has(collection)) {
|
|
2939
|
+
this.dependencyDebug('Invalidate due to watch-all collection', { publication: sub.publication, collection, type });
|
|
2940
|
+
return true;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
if (trackedIds && trackedIds.has(normalizedDocumentId)) {
|
|
2944
|
+
this.dependencyDebug('Invalidate due to tracked id', { publication: sub.publication, collection, type, documentId: normalizedDocumentId });
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
if (type === 'delete') {
|
|
2949
|
+
if (hasFilters && !hasTrackedIds) {
|
|
2950
|
+
this.dependencyDebug('Invalidate delete due to filter-only dependency without tracked ids', {
|
|
2951
|
+
publication: sub.publication,
|
|
2952
|
+
collection,
|
|
2953
|
+
type,
|
|
2954
|
+
documentId: normalizedDocumentId
|
|
2955
|
+
});
|
|
2956
|
+
return true;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
this.dependencyDebug('Skip invalidation on delete for unknown id', { publication: sub.publication, collection, type, documentId: normalizedDocumentId });
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
if (type === 'update' && hasFilters && !hasTrackedIds) {
|
|
2964
|
+
this.dependencyDebug('Invalidate update due to filter-only dependency without tracked ids', {
|
|
2965
|
+
publication: sub.publication,
|
|
2966
|
+
collection,
|
|
2967
|
+
type,
|
|
2968
|
+
documentId: normalizedDocumentId
|
|
2969
|
+
});
|
|
2970
|
+
return true;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
if (filters && filters.length) {
|
|
2974
|
+
for (const filter of filters) {
|
|
2975
|
+
if (await this.documentMatchesFilter(collection, documentId, filter, document)) {
|
|
2976
|
+
this.dependencyDebug('Invalidate due to filter match', { publication: sub.publication, collection, type, documentId: normalizedDocumentId, filter });
|
|
2977
|
+
return true;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
this.dependencyDebug('Skip invalidation after dependency checks', { publication: sub.publication, collection, type, documentId: normalizedDocumentId });
|
|
2983
|
+
return false;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
private async shouldInvalidateSubscriptionForEvents(sub: ActiveSubscriptionModel, collection: string, events: SubscriptionInvalidationEvent[]): Promise<boolean> {
|
|
2987
|
+
let sawInsert = false;
|
|
2988
|
+
let sawDelete = false;
|
|
2989
|
+
|
|
2990
|
+
for (const event of events) {
|
|
2991
|
+
if (event.type === 'insert') {
|
|
2992
|
+
sawInsert = true;
|
|
2993
|
+
}
|
|
2994
|
+
else if (event.type === 'delete') {
|
|
2995
|
+
sawDelete = true;
|
|
2996
|
+
}
|
|
2997
|
+
if (await this.shouldInvalidateSubscription(sub, collection, event.type, event.documentId, event.document)) {
|
|
2998
|
+
return true;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
const paginationMeta = this.getPaginationMeta(sub, collection);
|
|
3003
|
+
const paginationReasons = [];
|
|
3004
|
+
if (sawInsert) {
|
|
3005
|
+
paginationReasons.push('insert');
|
|
3006
|
+
}
|
|
3007
|
+
if (sawDelete) {
|
|
3008
|
+
paginationReasons.push('delete');
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
if (paginationMeta?.length && paginationReasons.length) {
|
|
3012
|
+
this.dependencyDebug('Invalidate due to pagination metadata', {
|
|
3013
|
+
publication: sub.publication,
|
|
3014
|
+
collection,
|
|
3015
|
+
events: this.summarizeEvents(events),
|
|
3016
|
+
reasons: paginationReasons,
|
|
3017
|
+
queryMeta: paginationMeta.slice(0, 5)
|
|
3018
|
+
});
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
return false;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
private getPaginationMeta(sub: ActiveSubscriptionModel, collection: string): QueryMeta[] {
|
|
3026
|
+
this.ensureDependencyContainers(sub);
|
|
3027
|
+
const metaList = sub.collectionQueryMeta?.get(collection);
|
|
3028
|
+
if (!metaList || !metaList.length) {
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const relevantMeta = metaList.filter(meta => {
|
|
3033
|
+
if (!meta) {
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
const limitUsed = typeof meta.limit === 'number' && meta.limit > 0;
|
|
3038
|
+
const skipUsed = typeof meta.skip === 'number' && meta.skip > 0;
|
|
3039
|
+
return limitUsed || skipUsed;
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
return relevantMeta.length ? relevantMeta : null;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
private sendWS(ws: WebSocket, data: ServerResponseModel) {
|
|
3046
|
+
this._websocketManager.send(ws, data);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
public getEnableDebug() {
|
|
3050
|
+
return this._enableDebug;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
private dependencyDebug(message: string, details?: Record<string, unknown>) {
|
|
3054
|
+
if (!this._enableDependencyDebug) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
if (details) {
|
|
3059
|
+
console.log(new Date(), '[Dependency Debug]', message, JSON.stringify(details, null, 2));
|
|
3060
|
+
}
|
|
3061
|
+
else {
|
|
3062
|
+
console.log(new Date(), '[Dependency Debug]', message);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
private summarizeEvents(events: SubscriptionInvalidationEvent[]) {
|
|
3067
|
+
return events.map(event => ({
|
|
3068
|
+
type: event.type,
|
|
3069
|
+
documentId: this.normalizeDocumentId(event.documentId)
|
|
3070
|
+
}));
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
private logDependencySnapshot(sub: ActiveSubscriptionModel, context: string) {
|
|
3074
|
+
if (!this._enableDependencyDebug) {
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
const dependencySummary = Array.from(sub.collectionDependencies.entries()).map(([collectionName, ids]) => {
|
|
3079
|
+
const idList = Array.from(ids || []);
|
|
3080
|
+
return {
|
|
3081
|
+
collection: collectionName,
|
|
3082
|
+
count: idList.length,
|
|
3083
|
+
sample: idList.slice(0, 10)
|
|
3084
|
+
};
|
|
3085
|
+
});
|
|
3086
|
+
|
|
3087
|
+
const filterSummary = Array.from(sub.collectionFilters.entries()).map(([collectionName, filters]) => ({
|
|
3088
|
+
collection: collectionName,
|
|
3089
|
+
count: filters.length
|
|
3090
|
+
}));
|
|
3091
|
+
|
|
3092
|
+
const queryMetaSummary = Array.from(sub.collectionQueryMeta.entries()).map(([collectionName, metaList]) => ({
|
|
3093
|
+
collection: collectionName,
|
|
3094
|
+
count: metaList.length,
|
|
3095
|
+
meta: metaList.slice(0, 5)
|
|
3096
|
+
}));
|
|
3097
|
+
|
|
3098
|
+
const watchAll = Array.from(sub.watchAllCollections || []);
|
|
3099
|
+
|
|
3100
|
+
this.dependencyDebug('Dependency snapshot updated', {
|
|
3101
|
+
context,
|
|
3102
|
+
publication: sub.publication,
|
|
3103
|
+
dependencies: dependencySummary,
|
|
3104
|
+
collectionsWithFilters: filterSummary,
|
|
3105
|
+
collectionsWithMeta: queryMetaSummary,
|
|
3106
|
+
watchAll
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
public async runPublicationOnce(publication: string, subscriptionData: any[] = [], options?: { userId?: string }) {
|
|
3111
|
+
const pub = this._publications[publication];
|
|
3112
|
+
if (!pub) {
|
|
3113
|
+
throw new Error(`Publication not found: ${publication}`);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const context = Object.assign({}, this, SubscriptionManager.prototype);
|
|
3117
|
+
const metadata: PublicationContext = {
|
|
3118
|
+
publication,
|
|
3119
|
+
subscriptionData
|
|
3120
|
+
};
|
|
3121
|
+
|
|
3122
|
+
if (pub.user_specific) {
|
|
3123
|
+
return withDependencyTracking(() => pub.function.call(context, options?.userId || '', ...subscriptionData), metadata);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
return withDependencyTracking(() => pub.function.call(context, ...subscriptionData), metadata);
|
|
3127
|
+
}
|
|
3128
|
+
}
|