Skip to content

fix(channel): flip S1 S4 S5 S7 S8 and fix S9 drain - #3418

Merged
bpamiri merged 3 commits into
developfrom
cursor/channel-hardener-s1-s10-3345
Aug 25, 2026
Merged

fix(channel): flip S1 S4 S5 S7 S8 and fix S9 drain#3418
bpamiri merged 3 commits into
developfrom
cursor/channel-hardener-s1-s10-3345

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Why

Peter flipped all five Channel holds. Publish errors, unknown adapters, and empty names failed open. Memory lastEventId never replayed. channelSSETag was onmessage-only, so named events never reached the page. Memory drain clear() could drop events published mid-loop.

This PR flips S1, S4, S5, S7, and S8, proves S2/S3/S6 as they stand, fixes S9, and tightens S10. Desk IDs stay S1–S10.

LuCLI run 32846469178 failed on d06dc15d with HTTP 417 (5206/0/1). The one error was S5 $subscribeMemory sends replayed events and not the lastEventId itself reading sent[1].data on a boolean. MockBox $() of sendSSEEvent logs a boolean, not the event struct.

1880df1d already dropped that $callLog().sendSSEEvent[n].data read. The spec now asserts the SSE writer body (id: s5-new, data: new, no id: s5-old) plus engine.replay() unit pins for events after lastEventId, never the lastEventId itself.

The $getChannelEngine leftover in subscribeToChannel's else-branch stays closed. Not S11. subscribeToChannel(adapter="redis") still takes memory.

Scope

  • vendor/wheels/Channel.cfc — bounded event log, replay(), empty-name throw
  • vendor/wheels/channel/DatabaseAdapter.cfcPublishFailed on INSERT fail, empty-name throw
  • vendor/wheels/global/routing.cfmUnknownAdapter for names that are not memory or database
  • vendor/wheels/controller/channels.cfc — named-event addEventListener, lastEventId replay, $drainChannelBuffer via ConcurrentLinkedQueue.poll(), empty-name throw
  • vendor/wheels/tests/specs/channel/ChannelHardenerSpec.cfc
  • vendor/wheels/tests/specs/channel/DatabaseAdapterSpec.cfc
  • vendor/wheels/tests/specs/controller/channelSpec.cfc
  • vendor/wheels/tests/_assets/channel/BrokenDatasourceAdapter.cfc
  • vendor/wheels/tests/_assets/channel/MidLoopPublishBuffer.cfc
  • vendor/wheels/tests/_assets/channel/SseWriterFake.cfc
  • changelog.d/channel-hardener-s1-s8.changed.md
  • changelog.d/channel-hardener-s9.fixed.md

Out: CLI ReleaseChannel, docs/blog, SQL databaseAdapters, leftover $getChannelEngine silent-memory in subscribeToChannel.

Tradeoffs

Typed Wheels.Channel.* errors instead of raw engine exceptions. Duplicate-key and missing-table INSERT failures both become PublishFailed.

Memory replay is a bounded in-process log on Channel.cfc. It is not durable across restart. That is the memory adapter.

S5 subscribe no longer mocks sendSSEEvent. A real SseWriterFake records write() bytes so the spec can Find("id: s5-new") without touching MockBox call-log booleans.

Blast Radius

publish() with a failed INSERT now throws. Callers that read persisted and continued will see the throw.

$getChannelEngine("redis") throws. publish(adapter="redis") throws with it. subscribeToChannel(adapter="redis") still takes the memory branch. That leftover stays closed.

Empty and whitespace-only channel names throw on publish, subscribe, subscribeToChannel, and channelSSETag.

Desk S1–S10

ID Status Note
S1 PROVEN Duplicate event id throws Wheels.Channel.PublishFailed. Source pin. No persisted:false.
S2 PROVEN Broken datasource cleanup() returns 0 and does not throw. Catch-any fail-open kept.
S3 PROVEN SELECT catch falls through to CREATE. Broken datasource $ensureEventsTable() returns false and does not throw.
S4 PROVEN $getChannelEngine("redis") and "memeory" throw Wheels.Channel.UnknownAdapter. memory and database still resolve.
S5 PROVEN engine.replay() returns events after lastEventId. $subscribeMemory writes id: s5-new / data: new through SseWriterFake. Find("id: s5-old") is 0. 1880df1d dropped $callLog().sendSSEEvent[n].data.
S6 PROVEN Empty lastEventId still reaches the memory loop. GetHTTPRequestData catch-any kept.
S7 PROVEN channelSSETag(events="notification,alert") emits addEventListener for both names and keeps src.onmessage.
S8 PROVEN Empty and whitespace names throw Wheels.Channel.InvalidName on publish, subscribe, subscribeToChannel, and channelSSETag.
S9 PROVEN $drainChannelBuffer keeps a mid-loop publish. $subscribeMemory does not call clear().
S10 PROVEN Cleanup insert must succeed before the delete assert. Mixin methods are functions and channelSSETag emits EventSource. Poll counts are exact on unique channels.

Verification

Commands actually run:

wheels test --core --ci --filter=channel

Scope: wheels.tests.specs.channel

Red (LuCLI 32846469178 on d06dc15d): HTTP 417, Results 5206/0/1. The S5 subscribe spec read $callLog().sendSSEEvent[1].data on a boolean.

Green (LuCLI 32847005260 on 1880df1d): Results 5207 passed, 0 failed, 0 errors. The one S5 error is now a pass.

Local driver on this HEAD:

43 passed (0.64s)

JSON from the same filter (directory=wheels.tests.specs.channel):

totalPass=43
totalFail=0
totalError=0
totalSkipped=0
totalSpecs=43
bundlesDiscovered=2
directoryRejected=false
directoryResolved=wheels.tests.specs.channel

Wheels CLI 4.0.6. Lucee 7.0.0.395. sqlite.

Adobe and BoxLang were not run here.

HEAD 1880df1d179eb496a89409df6e374f133ff81032

Base b8065e3a8ee7b90c2374cd9f16d8bcfd010026e2 (develop after #3417).

No closer keywords. Do not merge.

Open in Web Open in Cursor 

Failing specs for the five Peter flips (S1 publish throw, S4 unknown
adapter, S5 memory lastEventId replay, S7 named-event listeners, S8
empty name). Prove-current specs for S2/S3/S6 swallows. S9 drain
regression and S10 assertion tightening.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
Publish INSERT failures throw Wheels.Channel.PublishFailed.
Unknown adapters throw Wheels.Channel.UnknownAdapter.
Memory Channel.replay() feeds lastEventId on subscribe.
channelSSETag registers addEventListener for named events.
Empty channel names throw Wheels.Channel.InvalidName.
$drainChannelBuffer removes items instead of clear().

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
@github-actions github-actions Bot added the docs label Aug 25, 2026
ArrayList.remove(0) bound to remove(Object) and returned false,
so replayed events never reached the SSE writer. poll() drains
the queue without dropping mid-loop publishes.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri marked this pull request as ready for review August 25, 2026 12:26
@bpamiri
bpamiri merged commit 8045731 into develop Aug 25, 2026
14 checks passed
@bpamiri
bpamiri deleted the cursor/channel-hardener-s1-s10-3345 branch August 25, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants