Skip to content

fix(playground): subscribe the Pull lab to client-published messages - #557

Merged
IgorShevchik merged 2 commits into
mainfrom
claude/pull-lab-encode-verdict
Sep 23, 2026
Merged

IgorShevchik merged 2 commits into
mainfrom
claude/pull-lab-encode-verdict

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

The bug

/pull-lab check 9 timed out on every portal it was ever run against, and the page reported that the push server had not redelivered the frame. It had not been asked to.

  • subscribe() defaults type to SubscriptionType.Server (client.ts:432), and the lab took that default.
  • broadcastMessage routes on the frame's own extra.sender.type: back-end-published messages (pull.application.event.add — checks 4–8) arrive as Server; client-published ones (sendMessage() — check 9, the only route to the encoder) arrive as Client and are emitted to a different subscriber set (client.ts:1510-1533 vs :1571-1580).

So check 9's own echo could never reach the page. The lab now subscribes to both types.

What this corrects in the first commit

4cbdefc read "no exception, no echo" as evidence that the server had declined to redeliver an accepted frame. It was not evidence of anything — the lab could not have observed the echo either way, so that commit's warn text and its documentation additions attributed fault to the push server on a measurement that cannot support it.

What the run does establish, and this survives:

  • the channel lookup was skipped, because the recipient was already cached — the startup config call prefills that cache from publicChannels, which carries the current user's own channel, and check 9 sends to the current user;
  • therefore the batch was encoded and the connector accepted the frame.

What it does not establish: that the bytes were correct. A frame the push server cannot parse or address is dropped in silence, so a genuine encode bug produces the identical observation. The warn text now says exactly that and nothing more.

Also in the lab

  • Check 9 skips on JSON-RPC portals. Its only gate was isPublishingEnabled(), which is version > 3 and so also true on push-server 5+, where sendMessage() branches into the JSON-RPC adapter and neither codec runs. The check would have reported an encode that did not happen.
  • encodePathExercised resets per run — it was a sticky ref reporting a previous run's fact — and is now also set on a JSSDK_PULL_SEND_REFUSED failure, which is thrown after the encoder has run.

In the SDK

Both JSON-RPC publish paths went through this._jsonRpcAdapter?.…, which resolves undefined before init() assigns the adapter: nothing encoded, no frame, no error — the outcome 3.0.0 exists to remove, surviving on the branch only push-server 5+ takes. sendMessageBatch was worse: the optional chain fed JSON.stringify(undefined) to the connector, putting the string "undefined" on the wire and resolving true. Both now reject with JSSDK_PULL_SEND_REFUSED.

Documentation

The overstatement this branch opened to fix ("in an application sendMessage() always rejects") was in five more places than the first commit corrected: sendMessage()'s JSDoc, 60.pull.md, the 96.error-codes.md row, skills/b24jssdk-helpers/SKILL.md, and the playground README — which still described one outcome for a check that now has three (skip / fail / warn). All corrected, with the guidance ("publish from the back end") left intact, since only the stated reason was wrong.

.github/contributing/pull-protobuf.md claimed the encode half has no route to a portal from an application. It does. Its exit criterion is now split:

  1. decode — a real ResponseBatch decoded by both codecs: met;
  2. encode — a frame the lite codec produced, accepted by a portal and read back decoded: not met. The encoder has run and the socket took its output; no encoded frame has ever been read back, so transport acceptance says nothing about the bytes.

The fixture's knownGaps is corrected to match.

Known gaps, stated rather than hidden

  • A report cannot identify the SDK build it ran against. A pre-fix(pull)!: report every way a send can fail, instead of hiding four of them #555 SDK reproduces "resolved, nothing thrown, no echo" exactly. A version field would not close this either, since a workspace build keeps the same version string across rebuilds.
  • The JSON-RPC branch of sendMessage() bypasses the isPublishingEnabled() guard, so JSSDK_PULL_PUBLISHING_DISABLED is unreachable on push-server 5+. Left alone deliberately: making it consistent changes behaviour on a portal type not available to test here.
  • createMessageReceivers throws a bare Error with no code (client.ts:1802), reachable when getPublicIds resolves a partial answer. Pre-existing and out of scope for this PR.

Verification

typecheck (jssdk + nuxt playground), eslint on all changed sources, pnpm lint:md, docs-lint --strict, and the Pull suites (12 files, 91 tests) all pass locally. The routing claim was verified directly against client.ts:432, :1510-1533 and :1571-1580.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr

…sent

Check 9 reported `fail` with "accepted but nothing came back" whenever no
echo arrived. That sentence described the pre-3.0.0 client, where
`sendMessage()` resolved before the channel lookup had answered and an
unsent message was indistinguishable from an undelivered one. Since #555 it
cannot be: the call returns only after the batch has been encoded by the
codec under test and the connector has accepted the frame, and every earlier
failure throws with a code. So the page was reporting the one outcome that
can no longer happen, and `encodePathExercised` said `false` on runs where
the encoder had demonstrably run.

Measured on a live push-server v4 portal, which is what surfaced this: the
run produced no rejection at all. `PullClient` seeds the channel cache from
`config.publicChannels`, returned there by `pull.config.get` — an ordinary
application call — so `pull.channel.public.list` was never requested and had
nothing to refuse. The batch was encoded, the socket took it, and the server
did not redeliver it.

A missing echo is therefore a push-server delivery fact, not a codec one:
it is now a `warn` whose text says what did run, and `encodePathExercised`
is tracked from the send itself rather than inferred from the verdict. The
channel-manager JSDoc and the migration note said the rejection was the
permanent answer for an application; both now cover the cached-channel case
as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
Check 9's echo was unroutable by construction, so the check timed out on
every portal it was ever run against and the page blamed the push server.

`subscribe()` defaults `type` to `SubscriptionType.Server`, and the lab took
that default. `broadcastMessage` routes on the frame's own
`extra.sender.type`: a message the BACK END published — which is how checks
4-8 send, through `pull.application.event.add` — arrives as `Server`, while
one a CLIENT published through `sendMessage()` arrives as `Client` and is
emitted to a different subscriber set entirely. `sendMessage()` is the only
way to reach the encoder, so check 9 was the one check whose reply could
never arrive. The lab now subscribes to both.

That also corrects the diagnosis this branch was opened with. The previous
commit read "no exception, no echo" as proof that the server had declined to
redeliver an accepted frame. It was not proof of anything: the lab could not
have observed the echo either way. What the run does establish is narrower
and still worth having — the channel lookup was skipped because the
recipient was already cached, so the batch was encoded and the socket took
the frame. A missing echo does not acquit the encoder: the push server drops
a frame it cannot parse or address in silence, which looks identical.

Also in the lab:

- check 9 now skips on JSON-RPC portals. Its only gate was
  `isPublishingEnabled()`, which is `version > 3` and therefore also true on
  push-server 5+, where `sendMessage()` never reaches either codec. The
  check would have reported an encode that did not happen.
- `encodePathExercised` resets per run, and is set on a
  `JSSDK_PULL_SEND_REFUSED` failure too, since that is thrown after the
  encoder has already run.

In the SDK, one hole behind the same claim: both JSON-RPC publish paths went
through `this._jsonRpcAdapter?.…`, which resolves `undefined` before `init()`
has assigned the adapter — nothing encoded, no frame, no error, which is the
outcome 3.0.0 exists to remove. `sendMessageBatch` was worse: the optional
chain fed `JSON.stringify(undefined)` to the connector, putting the string
"undefined" on the wire and resolving `true`. Both now reject with
`JSSDK_PULL_SEND_REFUSED`.

Documentation carried the overstatement this branch set out to fix in five
more places than it corrected — `sendMessage()`'s own JSDoc, the public Pull
page, the error-codes row, the skill, and the playground README, which still
described one outcome for a check that now has three. The contributing guide
claimed the encode half had no route to a portal from an application; it
does, and the guide's exit criterion is now split into a decode half that is
met and an encode half that is not, because no encoded frame has been read
back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
@IgorShevchik IgorShevchik changed the title fix(playground): tell an encoded-and-sent frame apart from one never sent fix(playground): subscribe the Pull lab to client-published messages Sep 23, 2026
@IgorShevchik
IgorShevchik merged commit 2f69d47 into main Sep 23, 2026
10 checks passed
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.

2 participants