roostorg / roostorg/coop

Scylla disable control lives at the storage layer, not the feature layer

Open
#1,237 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
135
Forks
46
Avg merge
2d 6h
Merged PRs (30d)
113

Description

ITEM_INVESTIGATION_AND_STRIKES_ENABLED=false turns off two features — Item Investigation and User Strikes — but the control is implemented in two places, at the wrong layer, and the two disagree.

Current implementation

  • scylla/noOpScylla.ts — a null-object Scylla returning empty reads and dropping writes, selected by the Scylla DI factory in iocContainer.
  • services/userStrikeService/userStrikeService.ts:36 — an enabled: boolean constructor parameter that early-returns at line 101.

Why this is wrong

One condition, two encodings. Whether a given code path is disabled by the null object or by the early return is a per-method accident. A method added to UserStrikeService tomorrow inherits the null object's silent-empty behaviour; whether that is correct depends on whether the method has side effects that don't touch Scylla. publishActions is exactly such a side effect, which is why the constructor flag had to exist at all — the no-op driver cannot stop it.

The storage layer can't know the right empty value. NoOpScylla.select() returns no rows because that is the only answer a driver can give. The domain answers differ: a strike count should be 0, a strike history should be empty, "is this user over the threshold" should be a definite no. Today those fall out of empty-rows by coincidence rather than by decision.

A health check becomes a silent success.
workers_jobs/ItemProcessingWorker.ts:47 calls scylla.connect() to fail fast when Scylla is unreachable. Against the null object that always resolves, so the fail-fast is inert — and that consumer is outside both feature services, so it isn't covered by either control.

The failure mode is silent. A deployment that sets SCYLLA_ENABLED=false unintentionally looks healthy: reads return empty, writes are dropped, strike counts read as 0. It is indistinguishable from a working-but-empty cluster. The only signal is one scylla.disabled log line at boot.

Proposed

Move the no-op to the feature boundary:

  • Extract IUserStrikeService / IItemInvestigationService (6 public methods each) and register a no-op implementation of each when the feature is off.
  • Delete NoOpScylla and the enabled constructor parameter; "disabled" becomes "you were handed the no-op implementation", stated once per method at the layer that knows the domain semantics.
  • Give ItemProcessingWorker an explicit gate rather than relying on a driver whose connect() silently resolves.

Roughly cost-neutral: ~30 lines per no-op service against the 83 deleted, with the interface extraction as the one real prerequisite.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading scylla/noOpScylla.ts, services/userStrikeService/userStrikeService.ts, and workers_jobs/ItemProcessingWorker.ts to trace the current disable paths and their consumers. Then map the six public methods in each feature service and the Scylla DI registration. Done means feature-level no-op implementations handle disabled behavior, the storage no-op and constructor flag are removed, and the worker has an explicit gate.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, databases
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.