Skip to content

docs(web/guides): add Channels pub/sub guide with honest WebSocket status - #3004

Merged
bpamiri merged 3 commits into
developfrom
peter/issue-2962-channels-docs
Jun 11, 2026
Merged

bpamiri merged 3 commits into
developfrom
peter/issue-2962-channels-docs

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

What

Wave-4 slice of the #2962 roadmap: the channels documentation item only. Channels (the SSE pub/sub layer: publish(), subscribeToChannel(), channelSSETag(), wheels.Channel, wheels.channel.DatabaseAdapter, wheels-sse.js) have shipped since 4.0 with zero user-facing docs — only the internal .ai/wheels/channels/channels.md existed, and it contained two stale claims.

  • New guide: web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx — quick start, configuration, memory vs database adapters (incl. wheels_events schema and the post-fix(sse): blank-line framing, DB cursor advance, channel pruning, bounded retention cleanup #2940 bounded/throttled cleanup numbers: 60-min retention, sweep at most every 5 min / 15 s while a backlog drains, max 1,000 rows per pass, scheduled cleanup() recommendation), full API tables verified against vendor/wheels source, WheelsSSE JS client, usage patterns, auth + deployment cross-links into the SSE guide.
  • Honesty points the internal doc got wrong or omitted, now documented correctly:
  • SSE guide (server-sent-events.mdx): WebSockets paragraph now states Wheels ships no WebSocket layer and points at Channels; Channels LinkCard added to Related guides.
  • Sidebar: registered the page in web/sites/guides/src/sidebars/v4-0-0.json directly after Server-Sent Events — the sidebar is explicitly configured there, not autogenerated from frontmatter (the verify-scout plan assumed frontmatter sidebar.order would slot it in; it would not).
  • Internal doc (.ai/wheels/channels/channels.md): fixed the phantom JS-client URL and the pre-fix(sse): blank-line framing, DB cursor advance, channel pruning, bounded retention cleanup #2940 "cleanup runs every 5 minutes" wording so internal and external docs agree.

Out of scope (per the roadmap split): auth scaffold, policy layer, file attachments, any WebSocket code. No runtime code touched. Docs-only — no changelog fragment.

Test evidence

  • Guides build: pnpm build green (433 pages); Channels renders, sidebar order verified (SSE → Channels → i18n), all cross-page anchor targets verified present in built HTML.
  • verify:docs: 58 passed / 310 failed — identical counts on clean origin/develop (stash-verified); the failures are pre-existing native-mode wheels cfml execution failures in other pages. The new page adds no {test:*} blocks (controller/publish() examples need app context, per harness rules).
  • Core suite, Lucee 7 + SQLite (docker): baseline on origin/develop 345b30d = 4293 pass / 12 fail / 0 error (4323 specs); final after changes = 4293 pass / 12 fail / 0 error — byte-identical. All 12 pre-existing failures are in wheels.tests.specs.internal.testClientSpec (environment-specific, present on clean develop, untouched by this docs-only diff).

Suggested issue comment (roadmap sequencing for the remaining features)

Channels docs shipped (this PR). Proposed sequencing for the remaining three features, each split into its own child issue when scheduled:

  1. wheels generate auth scaffold — next; medium effort, highest leverage. The primitives (Authenticator, Session/Token/JWT strategies, AuthMiddleware, JwtService) already ship; the work is a CLI generator emitting user-owned app code (User model + migrations + Sessions/Passwords/Registrations controllers + views + routes), Phoenix phx.gen.auth-style. Generated controllers must call super.config() (CSRF footgun). Child-issue design calls: default strategy, registration on/off flag.
  2. Authorization / policy layer — after the auth scaffold, since it presumes the current-user convention the scaffold establishes. Laravel/Pundit hybrid: wheels.Policy base + authorize()/can()/policyScope() mixins (public $-prefixed internals per anti-pattern 7), app/policies/<Model>Policy.cfc discovery, wheels generate policy. Child-issue design call: default-deny when no policy file exists.
  3. File attachments — last; highest effort. Phase it: storage-disk abstraction first (vendor/wheels/storage/, local + S3 via signed cfhttp, service("storage"), signed URLs), defer the hasOneAttached model macro + variants to a follow-up. Adapters alone cover most demand at a fraction of the effort.

WebSocket parity stays unscheduled until user evidence shows POST + publish() (now documented as the chat-room pattern) does not cover real use cases — a portable WS layer across Lucee/Adobe/BoxLang is non-trivial.

Refs #2962

🤖 Generated with Claude Code

…atus

Channels (SSE pub/sub via publish() / subscribeToChannel() / channelSSETag())
have shipped since 4.0 with zero user-facing documentation. Add a guide page
at digging-deeper/channels documenting the real surface, verified against
vendor/wheels source:

- publish() return shapes per adapter (memory vs database)
- subscribeToChannel() args incl. Last-Event-ID auto-detection
- channelSSETag() incl. the Wheels.Channel.MissingEndpoint throw and an
  honest caution that its generated script only relays default 'message'
  events (it wires onmessage only — named event types are not dispatched)
- memory vs database adapters, wheels_events schema, post-#2940 bounded/
  throttled publish-path cleanup numbers, scheduled-sweep recommendation
- WheelsSSE JS client incl. the fact it is NOT served at any URL — it must
  be copied from vendor/wheels/public/assets/js/wheels-sse.js into the app
- an explicit WebSocket status section: Wheels ships no WebSocket layer;
  channels are SSE-only by design (AdonisJS Transmit prior art); the
  client-to-server path is a plain POST + publish() (chat-room pattern);
  roadmap link to ##2962

Cross-link the SSE guide both ways (WebSockets paragraph + Related guides
card), register the page in the v4-0-0 sidebar JSON (the sidebar is
explicitly configured, not autogenerated from frontmatter), and fix two
stale claims in the internal .ai channels doc (wheels-sse.js URL that never
existed; pre-#2940 cleanup cadence).

Docs-only; no changelog fragment.

Refs #2962

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@github-actions github-actions Bot added the docs label Jun 11, 2026

@wheels-bot wheels-bot 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.

Wheels Bot — Reviewer A

This is a docs-only wave-4 slice from the #2962 roadmap: a new user-facing Channels guide, two corrections to the internal .ai/wheels/channels/channels.md, a cross-link from the SSE guide, and a sidebar entry. No runtime code is touched. The research is careful — the PR author verified retention/cleanup numbers against the #2940 source, caught the phantom JS-client URL, and correctly identified the onmessage-only limitation of channelSSETag(). Overall verdict: comment (two documentation accuracy findings and one dead-frontmatter nit, nothing blocking).

Correctness

Quick start example is self-contradictory — the wheels:sse listener shown would never fire

File: web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx, lines 72–100

The quick start shows three things together:

  1. A subscribe action with events = "notification,alert"
  2. A model callback that publishes event = "notification"
  3. A view calling channelSSETag() followed by JS that listens for wheels:sse CustomEvents

The Aside immediately after correctly explains the limitation: channelSSETag() generates a script that wires only src.onmessage, which the browser fires only for the default message SSE event type (frames with no event: field, or event: message). Events framed as event: notification are named SSE events — the browser silently ignores them from onmessage and would only reach a notification-type addEventListener you never attached. Confirmed by vendor/wheels/controller/channels.cfc line 131:

src.onmessage = function(e) {
    document.dispatchEvent(new CustomEvent('wheels:sse', ...));
};

So the document.addEventListener('wheels:sse', ...) shown in the quick start would never be called for the notification events being published. A reader following the example sees nothing happen, then has to find the Aside buried after the code.

Suggested fix: make the quick start internally consistent. Either change the publish call to the default event type so channelSSETag() works —

publish(
    channel = "user.#this.userId#",
    event   = "message",   // works with channelSSETag()
    data    = SerializeJSON({title: "Order received", orderId: this.id})
);

— or swap channelSSETag() for a plain WheelsSSE constructor (which wires addEventListener) so the named-event path works end to end. Move the Aside to a later section explaining the channelSSETag() + named-event gotcha in context.

Docs

$getChannelEngine() is a $-prefixed internal function; exposing it in the user-facing guide without a caveat sets a breakage trap

File: web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx, line 153

var adapter = $getChannelEngine("database");
adapter.cleanup(olderThanMinutes = 60, maxRows = 10000);

By Wheels convention (CLAUDE.md section on $-prefixed mixins), functions prefixed with $ are internal implementation details not intended for direct caller use — even though $getChannelEngine happens to be declared public in Global.cfc (required by the mixin system). The internal doc (.ai/wheels/channels/channels.md) uses this pattern too, but that doc targets framework contributors, not end users.

A user following this guide and scheduling a cleanup job against $getChannelEngine would be calling an unstable API. Suggested approaches (pick one):

  • Add an inline note: $getChannelEngine is an internal helper — its signature may change across minor versions.
  • Or replace with a direct instantiation that does not depend on the app-scope singleton and is stable across versions: CreateObject("component", "wheels.channel.DatabaseAdapter").init() — this has the same cleanup effect in a standalone scheduled task.

sidebar.order: 7.5 in frontmatter is dead configuration (minor nit)

File: web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx, lines 5–7

sidebar:
  order: 7.5

The PR body correctly notes that the v4-0-0 sidebar is explicitly configured in web/sites/guides/src/sidebars/v4-0-0.json — Starlight's autogenerated sidebar.order frontmatter is ignored when the sidebar is explicit. The v4-0-0.json entry controls the actual position; this frontmatter field is silently ignored and could mislead a future editor into thinking it does something. Safe to remove.

@wheels-bot

wheels-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's review is accurate and well-evidenced. All three findings are technically correct, the verdict of comment is consistent with the findings, and I have no sycophancy or false positives to flag. One nuance on Finding 2's remediation options; one missed nit. Aligned on changes needed.

Sycophancy

None detected. A cites specific lines, explains reasoning, and issues comment rather than approving.

False positives

None detected.

On Finding 1 (quick start / named events): A's characterization of the example as "self-contradictory" is slightly strong — the Aside is immediately adjacent (not buried) and the example appears to be a deliberate "here's the gotcha" teaching device. But A's technical claim is correct: channelSSETag() wires only onmessage, verified at vendor/wheels/controller/channels.cfc:131, so the wheels:sse listener shown would never fire for the notification event. The suggestion to make the quick start internally consistent is valid regardless of the framing.

On Finding 2 ($getChannelEngine): vendor/wheels/Global.cfc:1332 confirms public any function $getChannelEngine — A is correct that the $ prefix marks it internal per CLAUDE.md convention. A offers two remediation options; the first (add a stability caveat) is safer. The second (replace with CreateObject("component", "wheels.channel.DatabaseAdapter").init()) would create a fresh stateless instance and bypass the app-scope singleton (application.wheels.channelEngine), which may differ from what the framework configures. For a standalone scheduled-task cleanup the distinction may not matter, but it is a subtle difference worth naming. The caveat option should be the recommended path.

On Finding 3 (dead frontmatter): Confirmed by the sidebar JSON diff — the v4-0-0.json entry controls position; sidebar.order: 7.5 is silently ignored. A is correct.

Missed issues

None of consequence. PR is docs-only; no runtime code, no migration, no cross-engine surface. Commit type docs, scope web/guides, header docs(web/guides): add Channels pub/sub guide with honest WebSocket status is ~73 chars — within the 100-char limit. No changelog fragment is correct per CLAUDE.md (docs-only PRs are exempt). The .ai/wheels/channels/channels.md corrections (phantom URL, cleanup throttle wording) are accurate relative to the source in vendor/wheels.

Verdict alignment

A's comment verdict is consistent with the findings — concrete and actionable, but not blocking for a docs-only PR.

Convergence

Aligned, changes needed. A's three findings hold up. The concrete ask: (1) make the quick start example internally consistent so it works end-to-end without requiring an Aside to understand (event="message" or switch to WheelsSSE/addEventListener), (2) add a stability caveat to the $getChannelEngine code block (preferred over direct instantiation), (3) remove the dead sidebar.order: 7.5 frontmatter. These are all well-scoped.

- Make quick start internally consistent: drop named-event filter from
  the subscribe action and publish with `event = "message"` so the
  default `onmessage` handler that channelSSETag() wires actually
  catches the event. Replace the trailing Aside with a forward pointer
  to the channelSSETag() reference for named-event handling.
- Relocate the named-event caution to the channelSSETag() API reference
  where the gotcha is most relevant. Remove the now-broken cross-link
  from the helper description.
- Add a stability caveat to the $getChannelEngine() cleanup example
  noting it's a `$`-prefixed internal helper, with a direct-instantiate
  alternative for callers that want a non-internal API.
- Remove the dead `sidebar.order: 7.5` frontmatter — the v4-0-0 sidebar
  is configured explicitly in `sidebars/v4-0-0.json`, so the field is
  silently ignored.

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@wheels-bot

wheels-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Address Review (round 1)

Applied consensus findings from Reviewer A and Reviewer B's convergence (round 1):

  • Finding 1 (quick start internally consistent). Dropped the events = "notification,alert" filter from subscribeToChannel() (channels.mdx:57), changed the publish call to event = "message" (channels.mdx:78) so channelSSETag()'s onmessage handler actually catches the event end-to-end. Replaced the trailing Aside with a brief forward pointer to the channelSSETag() API reference for the named-event case (channels.mdx:100). Per B's preference: example now works without the reader needing to consult an Aside to understand it.
  • Finding 2 ($getChannelEngine stability caveat). Added an Aside type="note" directly below the cleanup() example (channels.mdx:154-156) noting that $getChannelEngine() is a $-prefixed internal helper whose signature may change across minor versions. Included the direct-instantiate alternative (CreateObject("component", "wheels.channel.DatabaseAdapter").init()) as a stable-API option for callers who prefer one. Per B's preferred option (caveat over forced replacement, since direct instantiation bypasses the app-scope singleton).
  • Finding 3 (dead frontmatter). Removed the sidebar:\n order: 7.5 block from the page's frontmatter (channels.mdx:5-7). The v4-0-0 sidebar is configured explicitly in web/sites/guides/src/sidebars/v4-0-0.json, so the field was silently ignored.
  • Side effect of Finding 1. Relocated the named-event caution to the channelSSETag() API reference section (where the gotcha is most relevant) and removed the now-broken (see [the caution above](#quick-start)...) cross-link from the helper description.

No skipped findings — all three consensus items addressed.

The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on approve or the outer-loop cap (5 rounds) is reached.

@wheels-bot wheels-bot 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.

Wheels Bot — Reviewer A

Round 2 of the channels documentation PR. The first-round findings were all addressed cleanly: dead sidebar.order: 7.5 frontmatter removed, the $getChannelEngine Aside correctly flags the internal nature and offers a stable CreateObject alternative, and the quick start's named-event mismatch is fixed — publish now uses event = "message" so channelSSETag()'s onmessage handler actually fires. One correctness issue surfaced that the round-1 edits introduced: the subscribe action in the quick start reads params.userId to reconstruct the channel name, but channelSSETag() sends the full channel string as params.channel — params.userId is never present in the SSE endpoint's URL params, so the subscription listens on "user." instead of "user.42" and no events arrive. Verdict: request changes.

Correctness

Quick start subscribe action reads params.userId but channelSSETag() sends params.channel

File: web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx, line 58

function stream() {
    subscribeToChannel(channel = "user.#params.userId#");
}

The view (line 88) calls channelSSETag(channel="user.#params.userId#", route="notificationStream"). channelSSETag() appends ?channel=<encoded-string> to the SSE endpoint URL (vendor/wheels/controller/channels.cfc line 123):

local.url &= (Find("?", local.url) ? "&" : "?") & "channel=" & EncodeForURL(arguments.channel);

So the browser connects to /notifications/stream?channel=user.42. That is a new HTTP request; params in the subscribe action contains only channel=user.42. There is no params.userId — the action evaluates "user.#params.userId#" as "user." and subscribes to the wrong channel. Events published to "user.42" are never delivered.

The guide's own API reference (lines ~201–205) calls this out explicitly: "the channel and events arrive at your action as URL parameters; your action decides what to do with them (typically subscribeToChannel(channel=params.channel, events=params.events ?: "") after validating the client may subscribe)". The quick start contradicts that guidance.

Two correct options:

Option A — validate then pass through (matches the API reference guidance):

function stream() {
    // Caller may only subscribe to their own channel
    if (params.channel == "user.#session.userId#") {
        subscribeToChannel(channel = params.channel);
    } else {
        renderNothing(status = 403);
    }
}

Option B — derive the channel from the session, ignore the param entirely (the pattern already shown under "Per-user notifications" later in the guide):

function stream() {
    subscribeToChannel(channel = "user.#session.userId#");
}

Option B is simpler and more secure; it makes the channel argument passed to channelSSETag() superfluous for the action's logic (though channelSSETag() still needs it to build the URL). Option A is what the API reference describes. Either is correct; the current code is neither.

The quick start's stream() action subscribed with channel="user.##params.userId##",
but the SSE request generated by channelSSETag() carries only the channel (and
optional events) URL parameters (vendor/wheels/controller/channels.cfc lines
122-126) — params.userId was undefined on every EventSource connect, so the
endpoint threw and EventSource retried forever.

Derive the channel server-side from session.userId in both the action and the
view tag, matching the guide's own Per-user notifications pattern, and explain
why the action deliberately ignores the client-supplied channel parameter (with
a pointer to the validated params.channel wiring under channelSSETag()).

The other half of the review finding — publishing event="notification" while
channelSSETag relays only default message-type events — was already fixed in
the round-1 commit (publish now uses event="message").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@wheels-bot

wheels-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's review is clean. The single correctness finding is genuine and well-evidenced: I verified vendor/wheels/controller/channels.cfc:123 appends ?channel=<encoded> to the URL exactly as A claims, so the SSE endpoint's params scope contains channel but not userId. The guide's own channelSSETag() API reference section (which recommends params.channel) already contradicts the quick start example, confirming the bug. No sycophancy, no false positives, no missed issues on a docs-only diff.

Sycophancy

None detected. A requests changes and cites the specific source line responsible for the behaviour.

False positives

None detected. A's claim that channelSSETag() appends ?channel=<encoded-string> to the URL is verified against the actual source at vendor/wheels/controller/channels.cfc:123. The relevant line appends "channel=" & EncodeForURL(arguments.channel) to the endpoint URL, so the SSE request carries params.channel, not params.userId. The current quick start code evaluates to "user." and subscribes to the wrong channel.

Missed issues

None detected. The diff is docs-only. A's two proposed options (validate + pass through params.channel, or derive from session.userId) are both correct; the second already appears in the guide's own "Per-user notifications" pattern section, making it the natural choice for consistency.

Verdict alignment

request-changes is consistent with the finding. An example in a new user-facing guide that silently subscribes to the wrong channel is a concrete correctness defect, not a nitpick.

Convergence

Aligned: the finding is real and the fix is unambiguous. The quick start stream() action should use session.userId (matching the pattern shown later in the same guide) or params.channel with an authorization check. Either of A's proposed options resolves it; Option B (session.userId) is simpler and more consistent with the guide's own later example.

@wheels-bot wheels-bot 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.

Wheels Bot — Reviewer A

This PR delivers the user-facing Channels guide (Wave 4 of the #2962 roadmap), fixes two stale claims in the internal reference doc, and threads a Channels cross-link into the existing SSE page. Docs-only, no runtime code touched. Overall verdict: comment — one actionable nit, everything else checks out cleanly.

Docs

$getChannelEngine() in the scheduled-task example will not work as written

In web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx, the fenced code block titled illustrative — scheduled task body:

var adapter = $getChannelEngine("database");
adapter.cleanup(olderThanMinutes = 60, maxRows = 10000);

$getChannelEngine() is a bare function call that resolves only through the Wheels mixin system — it is available inside a CFC that has been mixed in (a Wheels Job, a controller, etc.) but not in a standalone .cfm scheduled task page. A reader copying this into a task page would get Function $getChannelEngine is undefined at runtime.

The Aside immediately below the snippet already recommends the correct alternative:

Instantiate the adapter directly: CreateObject("component", "wheels.channel.DatabaseAdapter").init() — cleanup() works the same on both.

The fix is to promote that form to the primary example:

var adapter = CreateObject("component", "wheels.channel.DatabaseAdapter").init();
adapter.cleanup(olderThanMinutes = 60, maxRows = 10000);

If you want to keep the $getChannelEngine form, scope the title to its actual context: illustrative — inside a Wheels Job.


Everything else checked against source:

  • Cleanup numbers (60-min retention, 5-min/15-s throttle, 1 000-row cap) verified against vendor/wheels/channel/DatabaseAdapter.cfc lines 27-31 and 217-222. Pass.
  • channelSSETag() only wires onmessage (named SSE event types not relayed) verified against vendor/wheels/controller/channels.cfc lines 131-132; the Aside caution is accurate. Pass.
  • JS client not served at any URL — the old phantom /wheels/assets/js/... claim in .ai/wheels/channels/channels.md is correctly replaced; vendor/wheels/public/assets/js/wheels-sse.js confirmed present. Pass.
  • API tables for publish, subscribeToChannel, channelSSETag, and the WheelsSSE constructor match the CFC source signatures. Pass.
  • Quick start uses session.userId server-side rather than a client-supplied params.channel, and calls out the security reason explicitly. Pass.
  • Sidebar JSON slots the entry between Server-Sent Events and Internationalization as described. Pass.
  • No changelog fragment — correct; the fragment requirement covers fix/feat PRs, not docs. Pass.

Commits

All three headers use the valid docs(web/guides): prefix and are well under the 100-char limit.

SHA Header Chars
f046f3f docs(web/guides): add Channels pub/sub guide with honest WebSocket status 73
58151f5 docs(web/guides): address Reviewer A/B consensus findings (round 1) 67
02f24a7 docs(web/guides): make channels quick start work verbatim 57

@wheels-bot

wheels-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Address Review (round 2)

The consensus findings from Reviewer B's convergence on 58151f53e were already addressed in the author's manual commit 02f24a7e (the current HEAD) — no further bot changes needed this round.

Verified against HEAD:

  • Convergence on 58151f53e (channel/userId mismatch). The B comment aligned on Option B (session.userId, server-derived). Already implemented in 02f24a7e:
    • web/sites/guides/src/content/docs/v4-0-0/digging-deeper/channels.mdx:59 — subscribeToChannel(channel = "user.#session.userId#")
    • channels.mdx:89 — channelSSETag(channel="user.#session.userId#", route="notificationStream")
    • channels.mdx:92 — explanatory paragraph naming why the action ignores the client-supplied channel parameter, with a pointer to the validated params.channel wiring under channelSSETag()
    • The latest Reviewer A run on 02f24a7e independently re-verified this: "Quick start uses session.userId server-side rather than a client-supplied params.channel, and calls out the security reason explicitly. Pass."
  • Convergence on f046f3fec (round-1 items). All three still hold at HEAD: dead sidebar.order: 7.5 frontmatter is removed; the $getChannelEngine() stability caveat Aside with the CreateObject(...) alternative is present at channels.mdx:155-157; quick start uses event = "message" so channelSSETag()'s onmessage handler fires.

Skipped (intentionally not part of the consensus this round):

  • Reviewer A's new finding on 02f24a7e — that $getChannelEngine() is a bare function call only resolvable inside a mixin-composed CFC, so the illustrative — scheduled task body code block wouldn't work as written in a standalone .cfm task page — is a fresh finding on the current HEAD. Reviewer B has not yet responded, so no convergence exists for it. The next loop iteration (Reviewer B on 02f24a7e) will decide whether to converge on it.

No new commit this round. The convergence loop continues — the next Reviewer B run on 02f24a7e will determine whether the new finding gets address-reviewed.

@wheels-bot

wheels-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's review is substantive and the $getChannelEngine finding is accurate and unaddressed in the current head. One additional missed issue worth fixing at the same time. Converging on changes needed.

Sycophancy: None detected. A verified cleanup numbers, the onmessage-only behaviour, the JS client URL claim, the API tables, the sidebar slot, and the changelog-fragment rule against specific source locations. No unearned LGTM.

False positives: None detected. The $getChannelEngine finding is correct — that function is mixed in via $integrateComponents and is only available inside mixed-in CFCs (controllers, Jobs, etc.). A standalone .cfm scheduled task page is not a mixed-in CFC, so calling $getChannelEngine() there fails with Function $getChannelEngine is undefined. The Aside added in round 1 provides the correct alternative but the primary example is still titled 'illustrative — scheduled task body' and still leads with the broken form. A's call is solid.

Missed issues: The Per-user notifications usage pattern still publishes with event='notification' after the quick start was corrected in round 1 to use event='message' (because channelSSETag() only wires onmessage, which fires only for message-typed events). The per-user pattern shows no JavaScript listener side, so a reader adapting from the quick start's channelSSETag() setup would copy the publish call, get no events, and not know why. Either change the pattern publish to event='message' or add a note that named event types require WheelsSSE / addEventListener.

Verdict alignment: A used COMMENTED instead of CHANGES_REQUESTED for a finding where the recommendation is to change the primary code example. CHANGES_REQUESTED would be more precise, but COMMENTED with an explicit fix suggestion maps to the same convergence outcome.

Convergence: Aligned with A on two actionable fixes — (1) change the $getChannelEngine scheduled-task example to use CreateObject('component','wheels.channel.DatabaseAdapter').init() as primary (or retitle it to 'inside a Wheels Job'), and (2) fix the per-user notifications pattern event type. Both are small docs edits.

@bpamiri
bpamiri merged commit 4a881c2 into develop Jun 11, 2026
14 checks passed
@bpamiri
bpamiri deleted the peter/issue-2962-channels-docs branch June 11, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant