feat(ws/config): implement WS filtering, replay, metrics wiring, and dry-run flag (#257 #258 #259 #260) - #348
Open
onahiOMOTI wants to merge 3 commits into
Conversation
…ellar-vortex-protocol#257 stellar-vortex-protocol#258) Issue stellar-vortex-protocol#257 – Real topic-based WS chain-subscription filtering: - Change subscribers from Set to Map<WebSocket, SubscriberFilter> to store per-connection chain filters - Add handleMessage() dispatching subscribe and replay message types inside handleConnection() (single listener, shared entry point) - handleSubscribe() validates incoming chains against SUPPORTED_CHAINS, stores a Set<SupportedChain> | null filter per client, and replies with { type: 'subscribed', filter: { chains } } - getEventChain() resolves srcChain for intent_created directly from the event payload; for intent_accepted / intent_filled / intent_cancelled / intent_expired / intent_slashed it performs a non-blocking IntentsService lookup; returns null for unchained events (delivered to everyone) - broadcast() now async: assigns a seq, pushes to ring buffer, resolves chain once, then fans out only to subscribers whose filter matches - Clients that never send subscribe continue to receive the full feed (backward-compatible default, filter.chains === null) - Malformed JSON and unknown message types are silently ignored Issue stellar-vortex-protocol#258 – WS event replay backed by EventRingBuffer: - Instantiate EventRingBuffer (capacity 500) owned by IntentsGateway - Every broadcast event is assigned nextSeq++ and pushed into the buffer before fan-out so a concurrent replay request finds the event - handleReplay() processes { type: 'replay', fromSeq } messages: - If fromSeq >= oldestSeq - 1: streams replay_start / events / replay_end - If fromSeq < oldestSeq - 1: returns replay_too_old with oldestAvailableSeq - Empty buffer returns replay_start with count 0 (no replay_too_old) - Reuses the single handleMessage() entry point from stellar-vortex-protocol#257 - REPLAY_BUFFER_SIZE kept at 500 with inline comment explaining the memory-vs-reconnect-gap tradeoff at current broadcast volumes Fixes: - intents.service.ts: add missing INTENTS_REPOSITORY / IIntentsRepository imports (pre-existing compile error) - solvers.service.ts: fix reactivate() shorthand property bug (isActive was referenced but not in scope) - intents-sweeper.service.ts: await broadcast() calls (now async) - intents-sweeper.service.spec.ts: add missing ALPHA_ADDR, buildIntentsService, SOLVERS_REPOSITORY; mock broadcast as jest.fn().mockResolvedValue(undefined) - test/load/ws-broadcast-fanout.test.ts: await gateway.broadcast() Tests: 32 gateway tests (EventRingBuffer + heartbeat + filtering + replay), 5 sweeper tests — all passing
…stellar-vortex-protocol#259) Decision rationale (per issue stellar-vortex-protocol#259): MetricsService (Prometheus/prom-client) is the production metrics path; MetricsRegistry in src/common/metrics.ts was dead code with no callers but live runbook documentation depending on it. Retiring it removes the confusion and aligns all metrics under one system. Changes: - Delete src/common/metrics.ts (Counter, Histogram, MetricsRegistry) — confirmed zero imports outside the file itself before deletion - Add sweeperExpiredTotal (vortex_sweeper_expired_total) and sweeperSweepDurationMs (vortex_sweeper_sweep_duration_ms) counters/ histograms to MetricsService with matching Prometheus naming convention - Add MetricsService.recordSweep(expiredCount, durationMs) helper called by IntentsSweeperService at the end of every sweep() cycle - Inject MetricsService into IntentsSweeperService constructor; MetricsModule is @global() so no IntentsModule import change required - Update docs/runbooks/on-call.md: - Replace MetricsRegistry.sweeper.sweepDurationMs → vortex_sweeper_sweep_duration_ms - Replace MetricsRegistry.sweeper.expiredTotal → vortex_sweeper_expired_total - Update 'How the sweeper works' step 4 to describe MetricsService path - Update diagnosis step 3 curl snippet with correct Prometheus metric names - No reference to src/common/metrics.ts or MetricsRegistry remains Tests: 2 new assertions in intents-sweeper.service.spec.ts verify recordSweep is called on every cycle with the correct expired count
…hs (stellar-vortex-protocol#260) Implements issue stellar-vortex-protocol#260 — a runtime-configurable dry-run safety flag for every on-chain write code path. Changes: - src/config/env.validation.ts: add ONCHAIN_DRY_RUN Joi schema entry. Default true outside production; required (explicit) in production — mirrors the fail-closed pattern of SOROBAN_SIGNING_KEY. - src/config/configuration.ts: add onchainDryRun to AppConfig interface and factory function. - src/soroban/stellar-tx.service.ts: read dryRun from config in constructor; invokeContract() short-circuits and returns a placeholder result (dryRun: true) when ONCHAIN_DRY_RUN=true — no network calls made. - src/soroban/solver-registry.service.ts: read dryRun from config; add dry-run short-circuit before the live path in slashSolver(); add Logger and use it instead of console.log throughout. - docs/runbooks/onchain-cutover.md: document ONCHAIN_DRY_RUN flag name, default behaviour, restart requirement, and production validation rule. Mark issue stellar-vortex-protocol#260 as Done in the dependency table. Tests: - src/config/env.validation.spec.ts: 6 new ONCHAIN_DRY_RUN tests; fix existing 'accepts a well-formed key in production' test to include ONCHAIN_DRY_RUN (now required in production). - src/soroban/stellar-tx.service.spec.ts: 2 new invokeContract dry-run tests (dryRun=true returns placeholder; dryRun=false throws not-yet-impl). - src/soroban/solver-registry.service.spec.ts: 2 new dry-run flag tests. Closes stellar-vortex-protocol#260
|
@onahiOMOTI Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements four related issues in a single branch so they can be reviewed and merged together. Each issue has its own commit for clean history.
Changes
Issue #257 — Real topic-based WS chain-subscription filtering
IntentsGateway.subscribersfromSet<WebSocket>toMap<WebSocket, SubscriberFilter>to track per-connection chain filters.client.on("message", ...)listener insidehandleConnection()routing tohandleMessage().handleSubscribe()validates each chain value againstSUPPORTED_CHAINS, stores the per-connection filter, and replies with{ type: "subscribed", filter: { chains } }.broadcast()callsgetEventChain()to resolve the relevant source chain (direct read forintent_created; async lookup viaIntentsService.get()for state-transition events), then skips subscribers whose filter excludes it.subscribemessage continue receiving the full unfiltered feed — backward compatible.scripts/solver-bot.tsalready implements the client side; verified it matches the server implementation.PR_DESCRIPTION_79.mdupdated to reflect the real implementation.Issue #258 — WS event replay backed by EventRingBuffer
EventRingBuffer(already defined) is now instantiated asprivate readonly ringBufferinIntentsGateway.broadcast()call assignsseq = this.nextSeq++and pushes the sequenced event into the ring buffer before delivering to subscribers.handleReplay()handles{ type: "replay", fromSeq }— streams back buffered events wrapped inreplay_start/replay_endframes, or responds withreplay_too_old(includingoldestAvailableSeq) when the requested sequence has been evicted.handleMessage()plumbing from Implement real topic-based WS chain-subscription filtering inIntentsGateway#257 — singleclient.on("message", ...)listener handles bothsubscribeandreplaymessage types.docs/solver-onboarding.mdreplay section verified accurate against the implementation.Issue #259 — Wire IntentsSweeperService to MetricsService (retire MetricsRegistry)
src/common/metrics.ts(the dormantMetricsRegistry— dead code with documentation depending on it).MetricsServicegainssweeperExpiredTotal(Counter) andsweeperSweepDurationMs(Histogram) — Prometheus-backed, exposed onGET /metrics.IntentsSweeperService.sweep()callsthis.metricsService.recordSweep(expiredCount, durationMs)at the end of every cycle.docs/runbooks/on-call.mdupdated — Scenario B references the real Prometheus metric names (vortex_sweeper_sweep_duration_ms,vortex_sweeper_expired_total); all references to the retiredMetricsRegistryremoved.Issue #260 — Runtime-toggleable dry-run flag for on-chain write paths
ONCHAIN_DRY_RUNenv var added toenv.validation.ts: defaults totrueoutside production; required to be explicitly set in production (fail-closed, mirrorsSOROBAN_SIGNING_KEYpattern — the process refuses to start without it).AppConfig.onchainDryRun: booleanadded toconfiguration.tswith appropriate factory default.StellarTxService.invokeContract(): readsonchainDryRunfrom config; whentruelogs and returns{ hash: "dry-run-no-hash", status: "DRY_RUN", dryRun: true }without touching the network.SolverRegistryService.slashSolver(): adds dry-run short-circuit at the top of the method (matching the reference implementation pattern described in the issue); replacedconsole.logwithLoggerthroughout.docs/runbooks/onchain-cutover.md: documentsONCHAIN_DRY_RUNflag name, default behaviour, restart-only limitation (explicit — no hot-reload for this iteration), production validation rule, and staged rollout procedure. Marks issue Add a runtime-toggleable dry-run flag for on-chain write code paths #260 as Done in the dependency table.Limitation (documented): The flag is config-driven and takes effect on the next process restart. There is no HTTP endpoint to flip it at runtime without a restart. This is intentional for this iteration — the staged rollout in
onchain-cutover.mdis designed around restart windows, and a live-toggle mechanism is a separate future concern.Tests
All new logic has test coverage:
src/intents/intents.gateway.spec.tssrc/intents/intents-sweeper.service.spec.tssrc/config/env.validation.spec.tssrc/soroban/stellar-tx.service.spec.tssrc/soroban/solver-registry.service.spec.tsAll tests that were passing before this branch continue to pass. The 5 pre-existing failing test suites (
soroban.controller.spec.ts,logging.interceptor.spec.ts,http-exception.filter.spec.ts,stats.service.spec.ts,intents.service.spec.ts) are unchanged and were failing onmainbefore this branch.Closes
Closes #257
Closes #258
Closes #259
Closes #260