Skip to content

refactor(cli): make the Maple Local eventing stack Effect-first - #934

Draft
Makisuo wants to merge 1 commit into
mainfrom
feat/local-eventing-effect-first
Draft

Makisuo wants to merge 1 commit into
mainfrom
feat/local-eventing-effect-first

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #363 (merged), rebased onto main.

What changes

The Local eventing code from #363 was synchronous and threw its errors. They were already Schema.TaggedError classes, but serve.ts caught them with try/catch and instanceof. Now:

  • control-store.ts: LocalEventingControlStore is a Context.Service with static readonly layer = Layer.effect(this, this.make). It reads LocalEventingControlConfig, which the app root provides. The database opens and closes through the layer scope (acquireRelease, with a WAL checkpoint on close), replacing the manual acquireRelease in startServer. Every method returns an Effect and fails with EventingControlStoreError, the consumer errors, or OutboxAdministrationInvalid. It no longer throws or builds a plain Error. Each bun:sqlite transaction is still one Effect.try step. The SQL, ordering, lease, pruning and durability logic did not change. openControlStore, validateControlSnapshot, writeControlSnapshot and restoreControlSnapshot replace the static helpers.
  • runtime.ts: LocalEventingRuntime is a service too. Its layer provides the store, and a LocalEventingProjectors reference supplies the projectors. The four existing errors now travel in the error channel, and a non-JSON fingerprint fails with SourceOccurrenceInvalid. The Result.getOrThrow calls are gone. Registry commits go through SynchronizedRef.modifyEffect, so a stale activation always fails as a conflict, even when it runs outside the maintenance gate.
  • otlp.ts / encode.ts: OtlpFieldError is now a Schema.TaggedError, and the imperative encoder still throws it. Normalization returns Effect<…, OtlpFieldError>. A malformed record is still counted and skipped. The unused OTLP_LOG_ADAPTER is removed.
  • telemetry.ts: the EventingTelemetry.record() interface and its runFork bridge are replaced by observeEventing, which updates Effect metrics from the Effect code. The labels are the same bounded set. The ingest-path runtime operations and activation carry spans; claim and ack polling do not.
  • serve.ts: telemetry and the eventing services now share one ManagedRuntime, the one that already ran per-request spans. The layers are memoized, so the store opens once. ingest and every eventing handler are Effects run through runSpan, and they map failures with Effect.catchTags. The maintenance gate gains exclusiveEffect, built on acquireUseRelease so it always reopens. readBoundedJson is an Effect.
  • consumer-auth.ts / local-token.ts / checkpoints.ts: the consumer token load is an Effect. readRealFile throws a LocalFileNotReal TaggedError instead of a plain Error. Checkpoint creation validates the control snapshot inside its Effect flow instead of through a nested runPromise.

HTTP statuses are unchanged

Failure Status
in-batch source collision, OtlpFieldError 400 (never 503, which OTLP exporters retry forever)
other projection/stage failures, including defects 503
ProjectionActivationConflict / ProjectionActivationInvalid / unexpected commit failure 409 / 400 / 500
MaintenanceInProgress / RequestBodyTooLarge 409 / 413
DeliveryGap (JSON) / OutboxAdministrationInvalid, InputInvalid / NotFound / Conflict, LeaseConflict / unknown 409 / 400 / 404 / 409 / 500
/local/eventing/outbox bad query / store failure 400 / 500

A defect in a handler lands in the same fallback status the old catch-all produced.

One edge case now behaves differently: if closing the staging store fails during restoreControlSnapshot, the failure is logged instead of rethrown, because a scope finalizer cannot fail. The target snapshot has already been written and validated by then.

Tests

The existing suites now call the Effect APIs, and every behavioral assertion is kept. test/eventing-test-support.ts holds the helpers. The telemetry assertions read an isolated Metric.MetricRegistry instead of a recording stub. The collision test also covers a defect, which stays a 503.

  • bun run --cwd apps/cli typecheck: clean
  • apps/cli suite under bun 1.4.2: 588 pass, 0 fail
  • oxlint (.oxlintrc.effect.json --quiet) and oxfmt --check on the changed files: clean
  • knip --workspace apps/cli: clean

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

The control store and eventing runtime are now Context.Services with their
own layers. The store opens and checkpoints its database through the layer
scope, and every method returns an Effect with typed failures instead of
throwing. Each bun:sqlite transaction stays one synchronous Effect.try step,
so SQL, ordering, leases, pruning and durability are unchanged.

serve.ts runs the eventing handlers and ingest on the server's one
ManagedRuntime and maps failures with catchTags to the same HTTP statuses
as before. Eventing telemetry records Effect metrics directly, OTLP field
errors are a Schema.TaggedError in the error channel, and checkpoints use
the Effect snapshot helpers.
@Makisuo
Makisuo force-pushed the feat/local-eventing-effect-first branch from 98322a4 to 1ac39c9 Compare September 18, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant