Skip to content

feat(client+component): reducer $ops, submit op, length: predicate, reactive_on_complete (#226) - #227

Merged
mhenrixon merged 10 commits into
mainfrom
issue-226-reducer-ops-submit-on-complete
Jul 13, 2026
Merged

feat(client+component): reducer $ops, submit op, length: predicate, reactive_on_complete (#226)#227
mhenrixon merged 10 commits into
mainfrom
issue-226-reducer-ops-submit-on-complete

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the full plan from #226 (comment) — the "normalize on input, commit when complete" field is now declarable end to end, single- or multi-input, with no bespoke Stimulus controller. Three composable pieces:

1. submit client op (js.submit)

  • requestSubmit() the target's own form: the target itself when it is a form, input.form for a control (honors form=), else closest("form"). A real cancelable submit event fires, so it composes with native/Turbo forms AND on(:save, event: "submit") interception.
  • The general autosubmit story: select(**on_client(:change, js.submit("form"))) — one declared line, Turbo Drive handles the visit (dummy AutosubmitFilterComponent + system spec).
  • Actor-only like focus: BROADCAST_REFUSED_OPS gains submit (a broadcast would force-submit every subscriber's form); reply.js still allows it. on_client(:submit, js.submit) raises at render — it would re-fire itself forever.

2. Reserved $ops reducer output + client ops builder

  • A reactive_compute reducer may return $ops holding an op chain — the new immutable ops builder exported from phlex/reactive/compute (verbs mirror the wire op names), or a raw [[op, args], …] list. Run through the same frozen CLIENT_OPS whitelist as on_client, as phase 4 of the API simplification, part 2.3: reactive_compute — scoped names, root binding, order-independent writes #183 single-pass write set — after field writes, text sinks, and phase-3 input dispatches settle.
  • Rising edge, keyed on chain CONTENT, event-gated: an identical chain never re-fires (a capped 7th keystroke can't re-submit); a changed chain fires again (per-digit focus advance across split boxes); the connect/morph seed pass arms without firing — which is what breaks the submit → validation-error re-render → re-seed → submit loop; returning null re-arms.
  • Flagships with real-browser specs: VerificationCodeComponent (single input: dirty paste 123-456 → normalize → exactly ONE signed POST, no navigation) and SplitCodeComponent (six boxes: a paste into any box redistributes one digit per box, reducer-driven focus advance per digit, hidden joined-code output carries the submit payload).

3. length: predicate + declarative reactive_on_complete

  • The ONE ShowConditions language gains { length: 6 } / Integer-Range length predicates (len_eq/len_gte/len_gt/len_lte/len_lt), with exact De Morgan complements under unless:. Length counts codepoints on both sides (Ruby String#length, client [...value].length — NOT UTF-16 .length); the shared parity fixture gains 9 vectors including the multibyte emoji proof. reactive_show gets length: for free.
  • reactive_on_complete(name = :default, run:, **conditions) — the same if:/if_any:/unless: kwargs as reactive_show, run: a js chain (now buildable at class level) or an allowlist-re-checked raw list. Emitted by reactive_attrs as one data-reactive-on-complete JSON attr (per-class memo keyed on the registry generation; byte-stable wire when undeclared). The client evaluates each binding with the scope-aware show-sync field resolver and fires on the rising edge with the same arming semantics as $ops. Works on ClientBindings (tokenless) components.
  • CodeCompleteComponent proves the zero-JavaScript path in the browser: a Ruby-only declaration dispatches code:complete at exactly six characters, intercepted into one signed POST; a 7th character re-arms (condition false) and trimming back to six fires again.

Test coverage

  • Unit (Ruby): js_spec (submit wire), component_spec (on_client self-loop guard; reactive_on_complete registry/inheritance/validation/wire; byte-stable when undeclared), show_conditions_spec (+16 length examples), parity fixture 35 → 44 vectors, response_spec (reply.js allows submit), client_bindings_spec (tokenless on_complete). Fast suite: 1429 examples.
  • Request: js_broadcast_spec — broadcast refuses submit (chain + raw array).
  • JS (bun): reactive_submit_op.test.js (6), reactive_compute_ops.test.js (14), reactive_on_complete.test.js (9) — 571 tests across 49 files.
  • System (real browser): 4 new specs (autosubmit filter, single-input OTP, six-box split code, declarative completion), 113 examples green under BOTH Puma and Falcon (rake spec:system_servers).

Performance

reactive_attrs (render hot path) gains one respond_to? + memoized nil read. Same-machine before/after (same checkout, git switch --detach main, serial):

bench main branch
render_component 26.80k i/s ±1.7% 26.95k i/s ±3.1% (noise)
render_component allocations 136 obj / 18,521 B / 0 retained identical
to_stream_replace 12.49k ±9.7% 11.70k ±10.9% (noise)
state-backed token 195.7k ±3.0% 187.3k ±3.3% (noise)
token allocations 14 obj (state) / 50 (record) identical

Method-level framing: no measurable change, zero new allocations, zero retained. The client recompute addition is one reserved-key read + one JSON.stringify of a tiny chain per pass, only on compute roots.

Verification

  • bundle exec rspec spec/phlex spec/requests — 1429 examples, green
  • bun test spec/javascript — 571 tests, green
  • bundle exec rake spec:system_servers — 113 examples green under Puma AND Falcon
  • bundle exec rubocop — 298 files, clean; cd docs && bundle exec rubocop <edited pages> — clean
  • bundle exec rake build:js_check — committed min builds match fresh builds; vendored copies byte-identical (sync spec)
  • rake bench before/after captured (table above)
  • README + docs site (client-ops, payment-split, actions-events, security) + CHANGELOG updated

Deviations & judgment calls

  • The $ops latch is keyed on chain CONTENT, not presence (the plan comment said "presence"). Prompted by the mid-flight question about multi-box OTP: a presence latch swallows per-keystroke focus advance (present → present never re-fires even when the focus target changed). Content-keyed is strictly more expressive with identical safety — an unchanged chain is settled (capped 7th digit, post-replace seed), a changed chain is a new intent. All presence-semantics tests pass unchanged.
  • SplitCodeComponent (six boxes) added beyond the plan's single-input flagship — to prove the multi-input story end to end (paste redistribution, focus advance, hidden joined output).
  • rake bench was broken on main: benchmark/micro/broadcast.rb still called broadcast_replace_to, removed in API simplification, part 2.5: one broadcast_to — verbs as kwargs, components as payloads #185 — every bench run exited 1. Fixed as a drive-by (migrated to broadcast_to(replace:) / each:); the transport double is unchanged.
  • ShowConditions now rejects unknown Hash values loudly: previously if: { a: { weird: 1 } } silently compiled to an equals-on-stringified-hash term; adding the length: Hash form makes any other Hash key raise at render. Strictly tighter — a nonsense binding now fails loudly instead of never matching.
  • First bench baseline was contaminated (captured while exploration agents ran — a phantom 2× delta with identical allocations). Re-captured properly per the bench discipline: same checkout, detached main, serial, quiet; the table above is the clean pair.
  • The single-input flagship renders the form as the component root (form(**mix(reactive_root(compute:), on(:verify, event: "submit")))) so the default @root submit target resolves directly; the six-box and filter demos exercise the closest("form") and selector-target paths.

Closes #226

Summary by CodeRabbit

  • New Features
    • Added js.submit(...) for native/Turbo-compatible form submission, plus safeguards against recursive submit triggering.
    • Added reactive_on_complete completion bindings with edge-triggered, non-self-firing behavior.
    • Added reducer-emitted $ops chains for conditional auto-commit/dispatch ordering.
    • Extended reactive_show with codepoint-based length predicates (exact and ranges).
    • Added demo endpoints and end-to-end examples (autosubmit, verification, code complete, split code).
  • Bug Fixes
    • Fixed rake bench exit code and updated micro-benchmarks to the current broadcast API.
  • Documentation
    • Updated README and action/completion/length/security docs with the new vocab and examples.
  • Tests
    • Added Bun unit tests and Rails system/request coverage for submit, $ops, and reactive_on_complete, plus length predicate vectors.

…226)

## Summary
Adds `js.submit(to = :root)` to the op builder and `submit` to the client
CLIENT_OPS whitelist: resolve the target, then requestSubmit() its OWN form
(the target itself when it IS a form, its form owner via input.form, else
closest("form")). requestSubmit fires a real cancelable submit event, so it
composes with on(:action, event: "submit") interception AND native/Turbo
forms — select(**on_client(:change, js.submit("form"))) is the one-line
general autosubmit story.

Actor-only like focus: BROADCAST_REFUSED_OPS gains "submit" (a broadcast
would force-submit every subscriber's form); reply.js still allows it.
on_client(:submit, js.submit) raises at render — requestSubmit dispatches
the very event that trigger is bound to (infinite loop).

## Test Coverage
- js_spec: submit wire format, :root default, global:, target validation
- component_spec: submit-event self-loop guard (bare + buried in a chain),
  change-bound autosubmit allowed, non-submit ops on submit event allowed
- js_broadcast_spec: broadcast_to(js:) refuses submit (chain + raw array)
- response_spec: reply.js allows submit (actor-scoped, mirroring focus)
- reactive_submit_op.test.js: form-control/form/closest resolution, no-form
  no-op, known-op (no default-deny warn), chain composition
- autosubmit_filter_spec (system): select change submits the GET form via
  Turbo Drive, no full reload (window marker survives)

## Verification
- [x] bundle exec rubocop passes
- [x] bundle exec rspec spec/phlex spec/requests (1392 examples)
- [x] bun test spec/javascript (539 tests)
- [x] bundle exec rspec spec/system/autosubmit_filter_spec.rb
benchmark/micro/broadcast.rb still called broadcast_replace_to /
broadcast_replace_to_each, removed in issue #185 — every `rake bench` run
exited 1 via the guided NoMethodError. Same shape, #185 spellings:
broadcast_to(*key, replace: nil) and broadcast_to(each: keys, replace: nil).
The Turbo::StreamsChannel transport double is unchanged (the new path still
lands on the same channel entry points).
## Summary
A reactive_compute reducer may now return the reserved key `$ops` holding a
client-op chain — built with the new immutable `ops` builder exported from
phlex/reactive/compute (verbs mirror the Ruby js DSL + wire names), or a raw
[[name, args], ...] array. The controller consumes it as PHASE 4 of the #183
single-pass write set: ops run through the same frozen CLIENT_OPS whitelist
after the field writes, text sinks, and phase-3 input dispatches settle.

Rising-edge, event-gated: ops fire only on the transition from "$ops absent
last pass" to "present this pass", and only on event-driven passes — the
connect/morph seed pass (#199) arms the latch without firing, which is what
breaks the submit → error re-render → re-seed → submit loop. A pass with no
$ops re-arms. `$ops` is consumed, never written as a field/text sink/mirror.

The canonical use ships as the dummy flagship: a one-time-code field whose
reducer normalizes on input and returns `$ops: ops.submit()` at 6 digits —
requestSubmit fires the real submit event, on(:verify, event: "submit")
intercepts it into ONE signed action POST.

## Test Coverage
- reactive_compute_ops.test.js (13): builder immutability + wire shapes,
  rising edge, no re-fire while complete, re-arm, seed-arms-without-firing,
  raw array + missing to: defaults to @root, outputs:-declared $ops never
  written, unknown-op warn-skip with surviving siblings, non-chain value
  default-deny, ops run after phase-3 dispatches
- verification_code_spec (system, real browser): dirty paste → normalize →
  exactly ONE POST, no navigation; post-replace seed never self-fires;
  extra digit capped with no re-fire; incomplete → complete fires once more

## Verification
- [x] bun test spec/javascript (552 tests)
- [x] bundle exec rspec spec/phlex spec/requests (1392 examples)
- [x] bundle exec rspec spec/system/verification_code_spec.rb
- [x] bundle exec rubocop passes
…dvance (#226)

## Summary
The $ops rising-edge latch now compares the serialized chain against the
previous pass instead of mere presence. An identical chain is settled — no
re-fire (the capped-7th-digit and post-replace-seed safety all hold) — while
a CHANGED chain fires again. That unlocks the multi-box OTP shape: one
reducer joins six boxes (a paste into any box redistributes one digit per
box), mirrors the joined code into a hidden field, advances focus to the
first empty box with a per-digit focus op (a different target each pass),
and submits on completion.

Ships SplitCodeComponent + split_code_reducer.js as the multi-input dummy
flagship beside the single-input VerificationCodeComponent.

## Test Coverage
- reactive_compute_ops.test.js: a changed chain fires per pass while an
  unchanged pass stays settled (all presence-latch tests pass unchanged)
- split_code_spec (system): dirty paste into box 1 redistributes + commits
  exactly once; typing advances focus box-by-box (hidden-code barrier) and
  the sixth digit submits exactly once

## Verification
- [x] bun test spec/javascript (553 tests)
- [x] bundle exec rspec spec/system (the three #226 specs)
- [x] vendored copies re-synced (sync spec green)
- [x] bundle exec rubocop passes
…#226)

## Summary
Two halves of the declarative completion story:

1. The ONE ShowConditions language gains a length: value form —
   { length: 6 } (exact, len_eq) and Integer Ranges (len_gte/len_lte/len_lt),
   with exact De Morgan complements under unless:. Length is counted in
   CODEPOINTS on BOTH sides (Ruby String#length, client [...value].length —
   NOT UTF-16 .length); the shared parity fixture gains 9 vectors including
   the multibyte emoji proof. reactive_show gets length: for free.

2. reactive_on_complete — a class-level declarative completion binding:
   the same if:/if_any:/unless: kwargs reactive_show takes, plus run: (a js
   chain, now buildable at class level via a class-side `js` helper, or a
   raw op list re-checked through the attr allowlist). Emitted by
   reactive_attrs as ONE data-reactive-on-complete JSON attr (per-class memo
   keyed on the registry generation — the reactive_effect_attrs precedent;
   byte-stable wire when undeclared). The client gates on the attr, listens
   input/change/turbo:morph-element, evaluates each binding's DNF with the
   scope-aware show-sync field resolver, and runs its ops on the RISING EDGE
   — connect/morph passes arm without firing; going false re-arms.

The dummy CodeCompleteComponent proves the zero-JavaScript path end to end:
a Ruby-only declaration dispatches code:complete at exactly six characters,
intercepted by on(:verify, event: "code:complete") into one signed POST.

## Test Coverage
- show_conditions_spec: length compile forms, range legs, unless complements,
  validation raises, codepoint evaluation, blank-length-0
- show_predicate_vectors.json: 9 new vectors, both parity loops green
- component_spec: registry/inheritance/naming, class-level js, run:
  validation (type, empty, hostile raw list), conditions required, wire
  emission + byte-stable-when-undeclared
- reactive_on_complete.test.js (9): arm-without-firing on connect/morph,
  rising edge, re-arm, independent latches, scope-aware resolution,
  selector-target ops, malformed default-deny, teardown
- code_complete_spec (system): fires exactly once at six chars, seventh char
  re-arms (condition false), trim back to six fires again

## Verification
- [x] bundle exec rspec spec/phlex spec/requests (1428 examples)
- [x] bun test spec/javascript (571 tests)
- [x] bundle exec rspec spec/system/code_complete_spec.rb
- [x] bundle exec rubocop (298 files, clean)
README: submit bullet in the op vocabulary + the general autosubmit story;
length: in the reactive_show value language; the $ops contract (rising edge
keyed on content, event-gated seed, multi-box recipe) in the compute section;
a new "Declarative completion (reactive_on_complete)" section.

Docs site: example_client_ops (vocabulary + autosubmit + on_complete),
example_payment_split (a dedicated $ops section), actions_events (on_client
submit note), security (the actor-only op tier under Rule 2).

CHANGELOG: the #226 feature set under Added; the broadcast micro-bench
migration under Fixed.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b9c122e-1d2f-4c22-a348-6eab20f52413

📥 Commits

Reviewing files that changed from the base of the PR and between 3c263d0 and d5037f5.

📒 Files selected for processing (1)
  • spec/system/split_code_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • spec/system/split_code_spec.rb

📝 Walkthrough

Walkthrough

This PR adds reducer-emitted $ops, declarative reactive_on_complete bindings, a submit client operation, codepoint-based length predicates, related demos and tests, documentation, and migration of broadcast benchmarks to broadcast_to.

Changes

Reactive client orchestration

Layer / File(s) Summary
Length condition language
lib/phlex/reactive/show_conditions.rb, spec/fixtures/*, spec/phlex/reactive/show_conditions_spec.rb
Adds validated length: predicates and codepoint-based len_* evaluation, including negated ranges.
Server completion and submit contract
lib/phlex/reactive/component/*, lib/phlex/reactive/js.rb, lib/phlex/reactive/streamable.rb, spec/phlex/reactive/*
Adds completion-binding DSL and serialized payloads, supports js.submit, prevents recursive submit handlers, and refuses actor-only operations in broadcasts.
Browser operation execution
app/javascript/phlex/reactive/*.js, spec/javascript/*
Adds immutable ops chains, reducer $ops execution after writes, submit form resolution, rising-edge completion handling, listener teardown, and runtime tests.
Demo applications and end-to-end validation
spec/dummy/app/*, spec/dummy/config/routes.rb, spec/dummy/public/vendor/*, spec/system/*
Adds autosubmit, verification-code, split-code, and declarative completion demos with reducer wiring and system coverage.
Feature documentation
README.md, docs/app/views/docs/pages/*, CHANGELOG.md
Documents submit behavior, autosubmit flows, length predicates, $ops, completion bindings, and actor-only restrictions.

Broadcast benchmark update

Layer / File(s) Summary
Broadcast benchmark migration
benchmark/micro/broadcast.rb
Replaces removed broadcast APIs with broadcast_to(..., replace:) and broadcast_to(each:, replace:) benchmark cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReactiveController
  participant Reducer
  participant Form
  User->>ReactiveController: Enter or change value
  ReactiveController->>Reducer: Run reactive compute
  Reducer-->>ReactiveController: Normalized fields and $ops
  ReactiveController->>Form: Apply updates, then requestSubmit()
Loading

Possibly related PRs

Suggested labels: enhancement

Poem

A rabbit taps six digits bright,
The code grows neat in moonlit light.
$ops hops after fields are done,
A submit follows—one, not none.
Completion blooms on rising ground,
While benchmark carrots spin around.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely names the main feature areas and the linked issue number.
Linked Issues check ✅ Passed The PR implements the requested reducer-emitted effects, submit op, rising-edge behavior, and declarative completion bindings.
Out of Scope Changes check ✅ Passed The docs, tests, benchmark, and dummy app updates all support the stated feature work; no unrelated code change stands out.

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
spec/system/code_complete_spec.rb (1)

13-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated install_post_counter helper into a shared system-spec support module.

The exact same install_post_counter method (identical JS heredoc and body) is copy-pasted across three system specs. One shared helper (e.g. in spec/support/) would avoid drift if the fetch-counting logic ever needs to change.

  • spec/system/code_complete_spec.rb#L13-L23: remove this local definition and include the shared support helper instead.
  • spec/system/split_code_spec.rb#L11-L21: remove this local definition and include the shared support helper instead.
  • spec/system/verification_code_spec.rb#L15-L25: remove this local definition and include the shared support helper instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/system/code_complete_spec.rb` around lines 13 - 23, Extract the
duplicated install_post_counter helper into one shared system-spec support
module, preserving its existing JavaScript behavior. Remove the local
definitions and include the shared helper in spec/system/code_complete_spec.rb
lines 13-23, spec/system/split_code_spec.rb lines 11-21, and
spec/system/verification_code_spec.rb lines 15-25.
lib/phlex/reactive/component/dsl.rb (1)

548-568: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inserting normalize_on_complete_ops here detached the normalize_compute_inputs doc comment from its method.

The pre-existing block at Lines 535–547 ("Split the inputs: argument… THREE shapes…") documents normalize_compute_inputs (Line 568), but the new normalize_on_complete_ops (Lines 548–565) now sits between them — so that detailed doc reads as if it belongs to normalize_on_complete_ops, and normalize_compute_inputs is left with only the terse # Named after the shape it normalizes. line. Consider moving normalize_on_complete_ops (with its own comment) to sit alongside the other completion helpers (near reactive_on_complete_attr), or relocate the normalize_compute_inputs doc block back above its method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/phlex/reactive/component/dsl.rb` around lines 548 - 568, Restore the
documentation association for normalize_compute_inputs by moving its detailed
“inputs:” shape comment immediately above that method, or move
normalize_on_complete_ops alongside the other completion helpers near
reactive_on_complete_attr. Keep normalize_on_complete_ops’s own validation
comment with that method and retain the existing terse shape comment only where
appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/phlex/reactive/component/dsl.rb`:
- Around line 487-488: Define the missing OnCompleteDefinition class used by
reactive_on_complete, with accessible conditions and ops fields matching the
arguments it receives. Ensure reactive_on_complete_attr can read both fields and
the DSL loads without raising NameError.

In `@spec/dummy/public/vendor/compute.js`:
- Line 1: Update the source operation builder, especially transition creation in
K and class normalization in D, to freeze the nested transition and classes
arrays before storing them in operation payloads. Preserve the existing
validation and builder behavior, then regenerate the bundled compute.js fixture
so ops.ops and toJSON() cannot mutate those arrays after construction.

In `@spec/dummy/public/vendor/reactive_controller.js`:
- Line 1: Simple reactive_show bindings in nX omit the length predicates defined
by aX, so len_eq/len_gte/len_gt/len_lte/len_lt are ignored. Extend nX to iterate
aX, coerce each predicate attribute value to an integer, and evaluate it through
rX while preserving existing numeric predicate handling; then regenerate the
bundled reactive_controller.js artifact.

---

Nitpick comments:
In `@lib/phlex/reactive/component/dsl.rb`:
- Around line 548-568: Restore the documentation association for
normalize_compute_inputs by moving its detailed “inputs:” shape comment
immediately above that method, or move normalize_on_complete_ops alongside the
other completion helpers near reactive_on_complete_attr. Keep
normalize_on_complete_ops’s own validation comment with that method and retain
the existing terse shape comment only where appropriate.

In `@spec/system/code_complete_spec.rb`:
- Around line 13-23: Extract the duplicated install_post_counter helper into one
shared system-spec support module, preserving its existing JavaScript behavior.
Remove the local definitions and include the shared helper in
spec/system/code_complete_spec.rb lines 13-23, spec/system/split_code_spec.rb
lines 11-21, and spec/system/verification_code_spec.rb lines 15-25.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26e5ccab-1682-4975-840e-f5f69e848b5a

📥 Commits

Reviewing files that changed from the base of the PR and between 20eac36 and 8f1db45.

⛔ Files ignored due to path filters (4)
  • app/javascript/phlex/reactive/compute.min.js is excluded by !**/*.min.js
  • app/javascript/phlex/reactive/compute.min.js.map is excluded by !**/*.map, !**/*.min.js.map
  • app/javascript/phlex/reactive/reactive_controller.min.js is excluded by !**/*.min.js
  • app/javascript/phlex/reactive/reactive_controller.min.js.map is excluded by !**/*.map, !**/*.min.js.map
📒 Files selected for processing (41)
  • CHANGELOG.md
  • README.md
  • app/javascript/phlex/reactive/compute.js
  • app/javascript/phlex/reactive/reactive_controller.js
  • benchmark/micro/broadcast.rb
  • docs/app/views/docs/pages/actions_events.rb
  • docs/app/views/docs/pages/example_client_ops.rb
  • docs/app/views/docs/pages/example_payment_split.rb
  • docs/app/views/docs/pages/security.rb
  • lib/phlex/reactive/component.rb
  • lib/phlex/reactive/component/dsl.rb
  • lib/phlex/reactive/component/helpers.rb
  • lib/phlex/reactive/component/registry.rb
  • lib/phlex/reactive/js.rb
  • lib/phlex/reactive/show_conditions.rb
  • lib/phlex/reactive/streamable.rb
  • spec/dummy/app/components/autosubmit_filter_component.rb
  • spec/dummy/app/components/code_complete_component.rb
  • spec/dummy/app/components/split_code_component.rb
  • spec/dummy/app/components/verification_code_component.rb
  • spec/dummy/app/controllers/demos_controller.rb
  • spec/dummy/app/views/layouts/application.html.erb
  • spec/dummy/config/routes.rb
  • spec/dummy/public/vendor/compute.js
  • spec/dummy/public/vendor/otp_reducer.js
  • spec/dummy/public/vendor/reactive_controller.js
  • spec/dummy/public/vendor/split_code_reducer.js
  • spec/fixtures/show_predicate_vectors.json
  • spec/javascript/reactive_compute_ops.test.js
  • spec/javascript/reactive_on_complete.test.js
  • spec/javascript/reactive_submit_op.test.js
  • spec/phlex/reactive/client_bindings_spec.rb
  • spec/phlex/reactive/component_spec.rb
  • spec/phlex/reactive/js_spec.rb
  • spec/phlex/reactive/response_spec.rb
  • spec/phlex/reactive/show_conditions_spec.rb
  • spec/requests/js_broadcast_spec.rb
  • spec/system/autosubmit_filter_spec.rb
  • spec/system/code_complete_spec.rb
  • spec/system/split_code_spec.rb
  • spec/system/verification_code_spec.rb

Comment thread lib/phlex/reactive/component/dsl.rb
Comment thread spec/dummy/public/vendor/compute.js Outdated
Comment thread spec/dummy/public/vendor/reactive_controller.js
The compute-ops and on-complete tests replaced globalThis.CustomEvent with a
bare recorder class (no cancelable, no preventDefault) and never restored it.
bun runs the whole suite in ONE process, so any later file whose events need
real semantics inherited the stub — in CI's file order that was
reactive_lifecycle_events.test.js (four failures:
"event.preventDefault is not a function"); locally reactive_effects.test.js
happened to run in between and re-set a working happy-dom CustomEvent,
masking the leak. The tests only read event.type/detail, which bun's native
CustomEvent already carries — so the stubs are simply removed rather than
snapshot-restored.
PR #227 review: the chain froze its outer containers but left the classes
and transition arrays reachable-mutable through ops.ops/toJSON(), so a chain
held in a constant could drift after construction. Frozen now, mirroring the
Ruby builder's .freeze on the same payloads; a deep-immutability unit test
locks it. Vendored min build re-synced.
@mhenrixon mhenrixon self-assigned this Jul 13, 2026
The spec drove the paste with Capybara's .set, whose semantics are
driver-version-dependent (the gem root's lockfile is gitignored, so local
and CI resolve different capybara-playwright builds). On CI, set interleaved
its input events with the reducer's per-event redistribution writes and the
six digits landed scrambled (deterministic 498765 on all four matrix jobs) —
the per-character flow, which is the TYPING contract the adjacent spec
already covers with explicit per-box keystrokes. A paste is ONE input event
carrying the full dirty string; the spec now dispatches exactly that, which
is what the test was always asserting.
@mhenrixon
mhenrixon merged commit aa158c4 into main Jul 13, 2026
11 checks passed
@mhenrixon
mhenrixon deleted the issue-226-reducer-ops-submit-on-complete branch July 13, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reactive_compute: let a reducer emit an effect (dispatch/submit) when a computed condition is met

1 participant