Fix dev proxy websocket cleanup - #14386
Conversation
The test demonstrates that abandoned websocket upgrades can consume the dev proxy agent capacity and block unrelated HTTP requests. Made-with: Cursor
Avoid sharing the HTTP keep-alive agent with websocket proxy requests, and tear down pending websocket upgrade requests when the client closes before the upstream upgrade completes. Made-with: Cursor
✅ Deploy Preview for v3-meteor-api-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for v3-migration-docs canceled.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughBumps the Meteor CLI package version; changes proxy runner to use a shared upstream HTTP agent with explicit destruction and improved websocket upgrade lifecycle handling; adds a selftest that exercises abandoned websocket-style upgrade connections. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tools/runners/run-proxy.js (1)
195-201:⚠️ Potential issue | 🟠 MajorDefer
proxyAgent.destroy()until the server has drained active connections.The comment at line 197 states that shutdown should allow existing connections to complete gracefully. However, calling
self.proxyAgent.destroy()immediately (lines 200-201) contradicts this:http.Agent.destroy()terminates all sockets managed by the agent, including active ones. This will abruptly close in-flight proxied HTTP responses during a proxy stop.Move the
proxyAgentdestruction to theserver.close()callback (or defer it until after proxy work has actually drained) to let active requests complete.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/runners/run-proxy.js` around lines 195 - 201, The shutdown currently calls self.server.close() then immediately calls self.proxyAgent?.destroy(), which force-closes all agent sockets and aborts in-flight proxied responses; move the proxyAgent teardown into the server.close() callback (or equivalent drain-complete handler) so that you call self.proxyAgent?.destroy() only after the server has finished closing connections and active proxy work has drained; update the code around self.server.close() to accept a callback/promise and in that completion handler call self.proxyAgent?.destroy() and set self.proxyAgent = null.
🧹 Nitpick comments (1)
tools/tests/run-proxy.js (1)
34-41: Avoid hardcoding the agent cap in this regression.This reproduces the old starvation bug only while
100stays in sync withtools/runners/run-proxy.js, Line 43. IfmaxSocketschanges, the pre-fix behavior can stop failing here and the regression silently goes stale. Derive the count fromproxy.proxyAgent.maxSocketsor share a constant instead.Possible change
await proxy.start(); proxy.setMode("proxy"); var proxyPort = proxy.server.address().port; - for (var i = 0; i < 100; i++) { + var upgradeCount = proxy.proxyAgent.maxSockets; + for (var i = 0; i < upgradeCount; i++) { clientSockets.push(await openWebsocketUpgrade(proxyPort)); } await waitUntil(function () { - return targetSockets.length === 100; + return targetSockets.length === upgradeCount; });As per coding guidelines,
tools/**: "Be thorough about correctness, edge cases, and performance in the CLI/build pipeline."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/tests/run-proxy.js` around lines 34 - 41, The test hardcodes 100 for the number of client sockets which can get out of sync with the agent cap; replace the literal 100 used in the loop that calls openWebsocketUpgrade(proxy.server.address().port) and in the waitUntil check against targetSockets.length with a derived value (e.g. let expected = proxy.proxyAgent.maxSockets or a shared constant) and use expected for both pushing to clientSockets and the waitUntil comparison so the test always matches the actual proxy.proxyAgent.maxSockets cap (ensure a sensible fallback if maxSockets can be undefined).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@tools/runners/run-proxy.js`:
- Around line 195-201: The shutdown currently calls self.server.close() then
immediately calls self.proxyAgent?.destroy(), which force-closes all agent
sockets and aborts in-flight proxied responses; move the proxyAgent teardown
into the server.close() callback (or equivalent drain-complete handler) so that
you call self.proxyAgent?.destroy() only after the server has finished closing
connections and active proxy work has drained; update the code around
self.server.close() to accept a callback/promise and in that completion handler
call self.proxyAgent?.destroy() and set self.proxyAgent = null.
---
Nitpick comments:
In `@tools/tests/run-proxy.js`:
- Around line 34-41: The test hardcodes 100 for the number of client sockets
which can get out of sync with the agent cap; replace the literal 100 used in
the loop that calls openWebsocketUpgrade(proxy.server.address().port) and in the
waitUntil check against targetSockets.length with a derived value (e.g. let
expected = proxy.proxyAgent.maxSockets or a shared constant) and use expected
for both pushing to clientSockets and the waitUntil comparison so the test
always matches the actual proxy.proxyAgent.maxSockets cap (ensure a sensible
fallback if maxSockets can be undefined).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b0b8a3e8-bf90-4afc-8ab0-4e4ada8695f1
📒 Files selected for processing (3)
packages/meteor-tool/package.jstools/runners/run-proxy.jstools/tests/run-proxy.js
Defer shared proxy agent destruction until the HTTP server has drained active connections, and derive the websocket regression count from the proxy agent cap. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tools/tests/run-proxy.js (1)
51-60: Wait for the server shutdown to finish.
target.close()is asynchronous here, but thefinallyblock does not wait for it. That can leave the listener alive long enough to leak handles into later selftests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/tests/run-proxy.js` around lines 51 - 60, The finally block destroys sockets but calls target.close() (and proxy.stop() if it is async) without awaiting their completion, which can leave the listener alive; change the cleanup to wait for shutdown by converting target.close() (and proxy.stop() if it returns/accepts a callback) into a Promise and await it (e.g., wrap target.close in a Promise that resolves/rejects in its callback) before finishing the finally block, while still destroying clientSockets and targetSockets first to ensure they’re closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tools/tests/run-proxy.js`:
- Around line 44-60: Replace the fixed sleep(100) with a deterministic wait for
socket teardown: after calling clientSockets.forEach(socket.destroy) await a
Promise.all that listens for each socket's 'close' (or 'end'/'error') event (use
a helper that resolves per socket) and only then call await
expectHttpResponse(proxyPort, 'ok'); likewise ensure the finally block waits for
Promise.all over clientSockets and targetSockets close events before invoking
proxy.stop() and target.close() so teardown is not timing-dependent (reference
clientSockets, targetSockets, sleep, expectHttpResponse, proxy.stop,
target.close).
---
Nitpick comments:
In `@tools/tests/run-proxy.js`:
- Around line 51-60: The finally block destroys sockets but calls target.close()
(and proxy.stop() if it is async) without awaiting their completion, which can
leave the listener alive; change the cleanup to wait for shutdown by converting
target.close() (and proxy.stop() if it returns/accepts a callback) into a
Promise and await it (e.g., wrap target.close in a Promise that resolves/rejects
in its callback) before finishing the finally block, while still destroying
clientSockets and targetSockets first to ensure they’re closed.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b9b6a3bc-172d-401b-a9f1-a06a7e2f60e6
📒 Files selected for processing (2)
tools/runners/run-proxy.jstools/tests/run-proxy.js
🚧 Files skipped from review as they are similar to previous changes (1)
- tools/runners/run-proxy.js
Wait for socket and server shutdown in the regression test, and document the test helpers used to exercise abandoned websocket upgrades. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tools/tests/run-proxy.js`:
- Around line 116-123: The waitForSocketToClose function can hang when called on
sockets already destroyed; update it to check socket.destroyed in addition to
socket.closed and immediately resolve if either is true. Locate
waitForSocketToClose and change the initial guard from checking socket.closed to
checking (socket.closed || socket.destroyed) so the function does not attach a
'close' listener to an already-destroyed socket.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2fa69752-608b-42cf-8800-c5643bf54cec
📒 Files selected for processing (1)
tools/tests/run-proxy.js
| Package.describe({ | ||
| summary: "The Meteor command-line tool", | ||
| version: '3.5.0', | ||
| version: '3.4.2', |
There was a problem hiding this comment.
please, do not update this pkg version
| @@ -130,6 +132,32 @@ Object.assign(Proxy.prototype, { | |||
| } | |||
| }); | |||
|
|
|||
| self.proxy.on('proxyReqWs', function (proxyReq, _req, socket) { | |||
| var proxySocket = null; | |||
|
@jfurneaux can you please fix the conflicts? this PR is prettry good, i'm just waitting the comments be addressed so we can merge it into 3.5.1 |
# Conflicts: # packages/meteor-tool/package.js
* update optimism and @wry/context tool deps
* update glob and escope tool deps
* update deps in proper file
* add a verbose config to get insights of transpilation process
* re-run checks
* chore: upgrade uuidfrom 3.4.0 to 8.3.2 in dev bundle
* style: remove unnecessary quotes around uuid key
* chore(inter-process-messaging): upgrade uuid to 8.3.2
* test(inter-process-messaging): add explicit uuid v4 format validation
* bump BUNDLE_VERSION to 22.22.1.1
* re-run checks
* bump BUNDLE_VERSION to 22.22.1.2
* re-run checks
* update tests and dependencies for `uuid` upgrade to 8.3.2
* replace `glob.sync` with `globSync` for consistency and improved readability
* Bump SWC server target to es2022
* replaces the binary legacy/empty segment with the actual target value ('legacy' / 'es2022' / 'es2015')
* add regression test for Rspack devserver port cleanup after SIGTERM
* Improve process cleanup and signal handling for Rspack devserver to ensure proper resource release on termination.
* improve E2E port cleanup test with timeout-based port state validation
* fix(ddp-server): pass method name to server-side MethodInvocation
The DDP method path passes `name: msg.method` when building the
MethodInvocation, but Server.applyAsync (server-side Meteor.callAsync)
omitted it even though the name is available locally. This made
DDP._CurrentMethodInvocation.get().name present for DDP calls but
missing for server-side calls. Pass `name` for parity.
Internal-only: nothing reads invocation.name for control flow, and
random seeds / stubs are unaffected (makeRpcSeed takes the method name
as an explicit argument).
* Fix lazy compilation errors being silently swallowed (#10366)
When a lazily-compiled file (under imports/) fails to compile and the
compiler reports the error without emitting any output (e.g. the Blaze
templating-compiler or coagmano:stylus aborting on a syntax error), the
build succeeded anyway and the only symptom was a runtime
"Cannot find module".
InputFile#_reportError defers errors for lazy files, storing them on
ResourceSlot.errors instead of failing the build, so that a broken lazy
file that is never imported does not break the build. The only thing
that later surfaces a deferred error is OutputResource#reportPendingErrors,
invoked by the ImportScanner -- but that requires an output resource to
exist. When the compiler errors out before calling addJavaScript, no
resource is produced, so the deferred error is never handed to the
scanner, the import resolves to "missing", and the error is lost.
Emit a lazy stub JS resource for any source slot that recorded errors but
produced no JS output, carrying the deferred errors into the ImportScanner.
When the module is actually imported, reportPendingErrors surfaces the real
compile error and fails the build, exactly like the eager path. Unimported
broken lazy files stay harmless. The fix lives entirely in the build tool,
so it covers every compiler plugin without changes to external compilers.
* fix(#10923): clearer oplog error for an uninitialized replica set
OplogHandle._startTailing threw "MONGO_OPLOG_URL must be set to the 'local'
database of a Mongo replica set" whenever the ismaster response had no setName.
An uninitialized replica set reports isreplicaset:true with no setName, so a
correctly-pointed oplog URL produced a misleading message. Classify the ismaster
response (extracted to a pure replicaSetOplogError helper) and report the
uninitialized-replica-set case specifically. Adds a unit test.
Fixes #10923
* fix(mongo): validate projection in Change Streams driver check so unsupported projections fall back
* Fix formatUrl dropping IPv6 bind host, yielding http://placeholder
parseUrl strips the brackets from IPv6 literals (e.g. "[::]" becomes
"::"), and the WHATWG URL parser only accepts IPv6 hosts bracketed. The
old sentinel-based formatUrl assigned the bare address via the hostname
setter, which silently ignored it, so binding to an IPv6 "any" host
(e.g. `meteor test --port "[::]:3005"`) left the internal "placeholder"
sentinel in the URL: ROOT_URL became http://placeholder and any
Meteor.absoluteUrl() call failed with getaddrinfo ENOTFOUND placeholder.
Bracket bare IPv6 literals and build the URL straight from its parts, so
the WHATWG URL constructor accepts the host and throws on an empty/invalid
one instead of silently producing a broken URL. Keeps formatUrl on the
WHATWG URL builtin (no dependency on the legacy Node url module) and drops
the placeholder sentinel entirely.
Fixes #14552
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Note absence of a builtin IPv6 host escaper in formatUrl comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Drop issue/PR references from formatUrl code comments
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Drop regression wording from formatUrl test comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(dynamic-import): align uuid to ^8.3.2 with other test apps
* fix(types): use 'declare namespace' in mongo and webapp .d.ts
The non-quoted `declare module Identifier` form is legacy syntax for
what is now spelled `declare namespace`. TypeScript treated the two as
equivalent for years, but TS 6.0 turned the old spelling into a hard
error (TS1540), and tsgo / TS 7.0 does the same. This blocks downstream
projects from type checking against TS 6 or newer, since `mongo.d.ts`
and `webapp.d.ts` are pulled in via `zodern:types`.
The change is purely syntactic. TS 5.9 accepts both forms, and a
consumer using `MongoInternals` in type and value position resolves to
an identical type surface either way, so nothing downstream shifts for
people still on TS 5. `logging.d.ts` and `roles/definitions.d.ts`
already use `declare namespace`, so this also brings `mongo` and
`webapp` in line with the rest of the packages.
Fixes #14253.
* chore: keep BUNDLE_VERSION at 24.15.0.2
Revert accidental bump to 24.15.0.3 (came in via merge). The 24.15.0.3
dev bundle was never published to the CDN, so CI fails at ./meteor
--get-ready with 'gzip: stdin: not in gzip format' on a cache miss.
24.15.0.2 matches devel/release-3.5.1 and is published.
* fix(tools,rspack): remove Node url.parse/util._extend deprecation warnings (#13491)
The util._extend deprecation from the CLI dev proxy was fixed in 3.5 by
replacing http-proxy with http-proxy-3 (#13916). Two sources still print
Node deprecation warnings on 'meteor run':
- The tool itself calls the legacy url.parse()/url.resolve() (DEP0169) in
config.js (package-stats/build-farm/package-server domains, hit on every
run via stats.recordPackages), bundler.js (source map URLs) and
cli/main.js (ROOT_URL validation). Replaced with the WHATWG URL API.
Output verified identical for all relevant inputs.
- The rspack integration pulled the old http-proxy back in transitively via
http-proxy-middleware, so rspack users still hit the original util._extend
(and url.parse) on every proxied HMR/asset request. Replaced
http-proxy-middleware with http-proxy-3 used directly; the proxy behavior
(mount-path stripping, changeOrigin, ws upgrades) is preserved, and WS
upgrades are now scoped to rspack's own paths so DDP is untouched.
Verified: fresh 'meteor run' no longer prints any deprecation warning,
whereas the same checkout without this change does.
* fix(tools): replace remaining legacy url.parse/url.resolve with WHATWG URL API
Node 24 (Meteor 3.5's runtime) emits the DEP0169 deprecation warning by
default for url.parse() called outside node_modules, so every 'meteor run'
on 3.5 prints a '`url.parse()` behavior is not standardized' warning.
PR #14248 migrated most of the codebase to the WHATWG URL API but missed
five call sites in tools/, one of which (url.resolve() in the bundler,
which calls url.parse() internally) fires on every run. Migrate them and
pass the raw Mongo URL to mongosh instead of round-tripping it through
url.parse(), which mangles comma-separated multi-host URLs.
Reported on the forum after the 3.5 release:
https://forums.meteor.com/t/meteor-3-5-is-out-change-streams-performance-improvements/64461/139
(cherry picked from commit e951f3b009d9728cb0d3d70310ee5e90e7f161a2)
* fix(tools): re-scope to deploy.js and run-mongo.js; rest covered by #14558
config.js, cli/main.js and isobuild/bundler.js are migrated by #14558;
keep only the two remaining legacy url.parse call sites here.
Empty on this branch: those three files are already migrated here by
#13491, so this re-scope revert is a no-op. Kept for parity with the
original PR #14559 history.
(cherry picked from commit 90f72c37de3f...)
* fix(cli): use let instead of var for parsedUrl in ROOT_URL processing
* Fire the subscription record's current stopCallback on local stop
An autorun rerun reuses an inactive subscription record and replaces
its readyCallback/errorCallback/stopCallback — but the record's stop()
closed over the callbacks object captured when the record was first
created, so a local stop fired the original onStop while the
server-initiated stop path (nosub) fired the replacement. Use the
record's current stopCallback on both paths.
* fix(isobuild): don't re-discover addAssets files as compilable sources
A file declared via api.addAssets (e.g. spacebars-tests' server asset
assets/markdown_basic.html) was being re-discovered by PackageSource#_findSources
as a lazy source on any arch whose compiler claims its extension. The web
templating compiler claims `.html`, so the raw-markdown asset was handed to the
HTML/spacebars compiler and failed with "Expected one of: <body>, <head>,
<template>". getFiles only deduped auto-discovered paths against explicitly-added
sources, never against declared assets, and assets are tracked per-arch while the
collision happens on a different arch.
The error was silently swallowed before #10366; since #10366 correctly surfaces
lazy compile errors, this latent bug became a deterministic build abort
(`_get "hash" called for file with pending errors`) that blocks release-3.5.1's
Test Packages (runs 29094729320, 29095492420, 29102750424).
Fix: in getFiles, gather asset relPaths across ALL arches and skip any
auto-discovered path already declared as an asset — an explicit asset declaration
wins over source auto-discovery.
Verified locally: test-packages of spacebars-tests aborts on markdown_basic.html
before this change and builds cleanly (reaches Started proxy / Started MongoDB)
after it.
* fix(tools-core): make getGlobalState read the store setGlobalState writes to
getGlobalState guarded on Package.meteor.global[key] but returned
Package.meteor.global.persistentState[key]. Since setGlobalState only
ever writes keys into persistentState, the guard was always false and
getGlobalState unconditionally returned the default value — stored
global state was invisible to every consumer. In practice this broke
all cross-rebuild persistence built on this module: the rspack build
plugin's process-reuse guards (CLIENT_PROCESS/SERVER_PROCESS) could
never fire, dependency checks (REACT_CHECKED, TYPESCRIPT_CHECKED,
ANGULAR_CHECKED) re-ran on every rebuild, and first-compilation
tracking always reset.
Fix the read to use the same persistentState store, and harden the
sibling helpers against the same class of bug:
- setGlobalState: the optional chain was misplaced
(Package?.meteor.global... still throws when Package.meteor is
undefined); guard the whole Package.meteor.global path.
- removeGlobalState: threw TypeError ("Cannot convert undefined or
null to object") when called before anything was ever stored.
- clearGlobalState: same unguarded dereference.
Add tinytest coverage to tools-core (set/get roundtrip, falsy values,
missing-key default, no-op remove, clear). The roundtrip and remove
tests fail against the previous implementation.
Run with:
meteor test-packages ./packages/tools-core --once \ --driver-package test-server-tests-in-console-once
* refactor(isobuild): concise cross-arch asset gather (review feedback)
Apply @Grubba27's review suggestions on #14566: gather asset relPaths with
Object.values(api.files).flatMap(...) into the Set, and drop the space in the
!assetRelPaths.has(...) negation. Behaviourally identical.
Re-verified locally: test-packages of spacebars-tests still builds cleanly
(reaches 'App running') with no markdown_basic.html compile abort.
* Deregister closed connections from DDP._allConnections
Every DDP.connect pushed into the allConnections registry and nothing
ever removed entries, so the array grew for the lifetime of the process
and _allSubscriptionsReady (the spiderable hook) consulted dead
connections forever. Connection#close() now removes itself via
DDP._removeConnection — a permanently closed connection can never
become ready.
* Fire the browser stream's disconnect event only when a socket was closed
_cleanup runs both when tearing down a live socket and at the top of
every (re)connection attempt. The browser implementation fired the
'disconnect' callbacks unconditionally, sending consumers a phantom
disconnect event (undefined payload) once per retry cycle; the node
implementation only fires when a client existed. Match node's behavior
by firing inside the socket guard.
* Fix store beginUpdate batch size: read updates by store name, not store._name
The store registry wrapper created in createStoreMethods carries only the
whitelisted store methods — it has no _name property — so
updates[store._name]?.length || 0 always passed batchSize 0 to
beginUpdate. Minimongo only pauses observers when batchSize > 1 (or on
reset), so multi-document batches were applied unpaused: observers fired
per document and the flicker prevention that buffered writes exist for
was silently disabled (regressed in 4e8c7cf181).
Use the _stores registry key, which is the same name the updates
accumulator is keyed by.
* Harden session resumption edge cases
Four related fixes in the disconnect grace-period machinery:
- sessionRemoveFunction now nulls messageQueue: send() on a removed
session previously kept buffering until overflow invoked the nulled
_pendingRemoveFunction and threw a TypeError into the sender (e.g. a
write-fence callback).
- connectionHandle.close() during the grace period previously hit the
_isClosing latch and did nothing, leaving the session resumable
against explicit server intent; it now cancels the grace period and
removes the session immediately.
- The resume check refuses sessions flagged _expectingDisconnect, and a
successful resume clears the flag; previously a stale flag silently
skipped the next disconnect's grace period.
- The grace-period message queue is flushed synchronously on resume;
the previous Meteor.defer let messages sent between 'connected' and
the flush (e.g. live observe callbacks) jump ahead of older buffered
ones, breaking DDP ordering.
* Add regression tests for session resumption edge cases
* Discard malformed DDP frames without crashing the message handler
parseDDP returns null for frames that are invalid JSON or valid JSON
that is not an object. onMessage's invalid-message branch then called
Object.keys(msg) on that null and threw a TypeError, which escaped the
un-awaited async handler as an unhandled rejection — a single malformed
frame silently killed message processing. Guard the server_id check and
fall through to the normal discard path.
* Make reconnect() a no-op on permanently disconnected streams
reconnect() decrements retryCount before calling _retryNow ('don't
count manual retries'), but _retryNow refuses to relaunch a stream
whose _forcedToDisconnect flag is set — so the decrement was never
compensated. The browser 'online' handler calls reconnect() on any
non-offline stream, including permanently failed ones, drifting
retryCount further negative on every online event. Bail out of
reconnect() up front: a permanent disconnect has no revive path.
* Make crossbar listen-handle stop() idempotent
A second stop() on the same handle decremented the collection's
listener count (and the server fact) again: once the count hit zero
the collection's whole listener map was deleted — silently unhooking
every remaining listener, e.g. live observe drivers — and any later
stop() on the collection threw a TypeError. Latch the handle so
repeated stops are no-ops.
* Hold _sendQueued messages sent while disconnected until reconnect
_sendQueued routes messages through the client async-stub queue (the
shouldQueue argument consumed by queue_stub_helpers) so they stay
ordered behind queued sub/method sends — but that queue does not
survive disconnection: the stream drops data sent while not connected.
A subscription stopped while disconnected therefore lost its 'unsub',
and since stop() removes the registry record, nothing re-sent it on
reconnect. With session resumption the resumed server session keeps
the ghost subscription streaming forever.
Hold messages passed to _sendQueued while the stream is not connected
and flush them on reset, after subscriptions are re-sent, so a queued
'unsub' can never precede its subscription's 'sub'.
* fix: opt-in to keep Content-Length on built assets instead of compressing (meteor/meteor#12772) (#14588)
Built JS/CSS assets are served without a Content-Length header because webapp
compresses them and the compression middleware removes Content-Length and
switches to chunked transfer. Some CDN/proxy setups want the origin to serve
uncompressed-with-Content-Length. Add an opt-in setting
Meteor.settings.packages.webapp.skipCompressionWithContentLength (default off):
when enabled, shouldCompress leaves any response that already has a
Content-Length uncompressed so the header survives. Default behavior unchanged.
Adds a tinytest.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add package-types.json to npm-mongo and configure environment permissions (#14422)
* fix(cordova): default modern config to enabled when no user override (#14411)
* fix(cordova): default modern config to enabled when no user override
* test(cordova): add E2E regression for modern bundle on web.cordova
* ci: retrigger failing jobs
* style(cordova): apply METEOR_MODERN string guard to project-context.js
---------
Co-authored-by: Italo José <italo.i@live.com>
* chore(deps): bump bcrypt to 6.0.0 and argon2 to 0.44.0 in accounts-password (#14407)
Bumps bcrypt 5.0.1->6.0.0 (drops the node-pre-gyp/tar chain, fixes Node 24 / linux-arm64 native build) and argon2 0.41.1->0.44.0. accounts-password 3.3.0->3.3.1. Hash formats and APIs unchanged; no stored-credential migration needed.
* fix(rspack): streamline process cleanup with `sendSignal` utility
* Fix watcher fallback on `EINTR` signals (#14450)
* fix watcher fallback and config handling for Docker buildsk
* bump @parcel/watcher version from 2.5.1 to 2.5.6
* update e2e-tests workflow paths to include new watched files
* refactor: extract isENOSPCorEINTR helper in safe-watcher
Address review feedback by consolidating the duplicated ENOSPC/EINTR
error checks into a single top-level helper and reuse the constants
module instead of repeated inline require("constants") calls.
---------
Co-authored-by: Italo José <italo.i@live.com>
* fix: prevent server crash on malformed request URL in sockjs transport (#14594) (#14598)
* docs: add v3-docs for disable-oplog package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to disable-oplog (install-only, no config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: document per-query disableOplog alternative in disable-oplog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: disable-oplog round 2 (oplog vs polling tradeoff, options compose, oplog activation)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(disable-oplog): document polling env vars, trim provenance
Address review: note the app-wide METEOR_POLLING_INTERVAL_MS and
METEOR_POLLING_THROTTLE_MS env vars (verified in
packages/mongo/polling_observe_driver.ts) as an alternative to per-query
options; soften 'contains no code' and drop the package.js/README
provenance notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(disable-oplog): reflect Change Streams as the 3.5 default
Address review feedback on #14511 now that Meteor 3.5 (with Change
Streams) has landed on devel:
- Clarify that Change Streams — not the oplog — are the default
reactivity mechanism in 3.5, and that disable-oplog only removes the
oplog driver, turning the effective order into changeStreams → polling
(it no longer forces polling app-wide on a Change Streams-capable
deployment).
- Document forcing polling / reordering drivers via
METEOR_REACTIVITY_ORDER and the packages.mongo.reactivity setting
(string or array), with links to the change-streams and env-var docs.
- Note that per-query disableOplog likewise only removes the oplog
driver.
- Link the disable-oplog package from community-packages/pub-sub.md.
* docs: add v3-docs for insecure package (#14495)
* docs: add v3-docs for insecure package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to insecure (install-only, no config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Method migration example to insecure
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: insecure round 2 (allow-deny attribution, per-collection override, remove = deny-all)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(insecure): precise method list, soften no-code claim, wording
Address CodeRabbit review: enumerate client-side insert/update/remove
instead of 'almost all'; 'exposes no JavaScript API' instead of
'contains no code'; 'precisely' instead of overused 'exactly'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(insecure): link autopublish in See also
Address review feedback: turn the `autopublish` "See also" entry into a
link to its documentation page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for autopublish package (#14494)
* docs: add v3-docs for autopublish package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to autopublish (install-only, no config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add publication migration example to autopublish
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: autopublish round 2 (migration caveat: publications inert until removed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(autopublish): credit publishing to mongo, ddp-server only warns
Address review: only the mongo package publishes collections when
autopublish is present; ddp-server checks for the package solely to
warn about manual Meteor.publish calls. Also soften the 'no code'
claim to 'exposes no JavaScript API'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(autopublish): mention meteor create --prototype and link insecure
Address review feedback: note that a prototyping project including
autopublish (and insecure) can be scaffolded with `meteor create
--prototype`, and turn the `insecure` "See also" entry into a link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for static-html package (#14496)
* docs: add v3-docs for static-html package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to static-html (install-only, no config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add framework integration and body-attributes examples to static-html
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: static-html round 2 (prerequisites: install framework, client entry point)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(static-html): clarify eager loading, build-time/runtime, client arch
Address review + CodeRabbit: rephrase eager-loading note (any client
file outside imports/ is loaded); drop devOnly jargon and state no
runtime API; scope compiler to client .html files (archMatching web);
align 'one or the other — not both'; remove provenance blockquote.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for mobile-experience package (#14500)
* docs: add v3-docs for mobile-experience package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to mobile-experience (install-only, no config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: mobile-experience round 2 (surface status-bar/launch-screen config pointers)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(mobile-experience): implies wording, links, config precision
Address review + CodeRabbit: 'implies' instead of 're-exports'; status-bar
prefs set in mobile-config.js via App.setPreference(...); link the config
APIs to ../api/app.md and the sibling pages; drop provenance note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(mobile-experience): drop links to pages from unmerged PRs
The See also section linked to ./mobile-status-bar.md and
./launch-screen.md, which live in separate PRs (#14501, #14502) and do
not yet exist on devel. VitePress dead-link checking (ignoreDeadLinks
only allows localhost) failed the Netlify build. Keep them as plain
references so this page builds standalone.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for mobile-status-bar package (#14501)
* docs: add v3-docs for mobile-status-bar package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to mobile-status-bar (install gives defaults, config optional)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: list common preferences in mobile-status-bar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: mobile-status-bar round 2 (platform arg, iOS-only prefs, string values, fix overstatement)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(mobile-status-bar): fix dead mobile-config link, clarify API, links
Address review + CodeRabbit: replace the 404 docs.meteor.com
mobile-config.html link with ../api/app.md (both occurrences); clarify
that the Meteor package has no module API but the Cordova plugin exposes
a runtime StatusBar global; link the mobile-experience sibling; trim
source provenance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify native module rebuild on custom deployment (#14489)
Document that the (cd programs/server && npm install) step is mandatory
on the target machine because it recompiles native addons for the
target OS/arch, and that copying node_modules across platforms causes
the 'invalid ELF header' error.
Closes #11682
* docs: add v3-docs for accounts-facebook package (#14503)
* docs: add v3-docs for accounts-facebook package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add complete login example to accounts-facebook (config, button, logout, errors)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: accounts-facebook round 2 (app setup, callback URL, real fields, notice fix)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(accounts-facebook): note default email scope, tag fence
Address review + CodeRabbit: state that facebook-oauth requests the
email scope by default when requestPermissions is omitted; add the text
language specifier to the redirect-URI fence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for launch-screen package (#14502)
* docs: add v3-docs for launch-screen package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: document splash image config (App.launchScreens) in launch-screen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: launch-screen round 2 (dark mode, Android model, key/path notes, hold/release edge cases)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(launch-screen): fix keys link, Cordova-only export, android key rule
Address review + CodeRabbit: point the keys/sizes reference at the
App.launchScreens API (../api/app.md) instead of the Cordova guide;
note LaunchScreen is a Cordova-client global; correct the unknown-key
note (android values must be strings or the build throws, verified in
tools/cordova/builder.js); drop provenance and api.export jargon; link
the mobile-experience sibling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for facts-ui package (#14498)
* docs: add v3-docs for facts-ui package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Blaze + non-Blaze usage example to facts-ui
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: facts-ui round 2 (correct import source, runnable server snippet, reactive-context note)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(facts-ui): drop provenance note, link facts-base, trim build jargon
Address review: remove the 'README labels it internal' Notes blockquote
(its user-relevant points are already in the body), link the facts-base
sibling in See also, and drop the api.imply(...) build-API syntax.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v3-docs for force-ssl package (#14499)
* docs: add v3-docs for force-ssl package
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add Usage section to force-ssl (install-only, deploy-layer requirements)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add proxy config examples and HSTS note to force-ssl
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: force-ssl round 2 (absoluteUrl secure, proxy trust/redirect loop, port stripping, verify)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(force-ssl): explicit websocket caveat, fence lang, RFC links, provenance
Address review + CodeRabbit: make the WebSocket non-redirect consequence
explicit (raw DDP over ws:// is not forced to TLS) and soften 'always
encrypted'; add text language to the HAProxy fence; update stale
tools.ietf.org RFC 7239 links to datatracker; drop provenance blockquote.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add TypeScript package page to v3 docs (#14475)
* docs: add TypeScript package page to v3 docs
Document the typescript core package (compiler plugin for .ts/.tsx),
which ships by default in new apps but had no page in the v3 docs.
- New page at packages/typescript covering installation, the fact that
the plugin compiles but does not type-check, running tsc --noEmit,
tsconfig.json being ignored by the build, per-module transpilation
limits (e.g. export const enum), .d.ts handling, and React Fast Refresh.
- Register the page under the Packages > Framework compatibility sidebar.
* docs(typescript): cross-link the Using core types guide
Addresses review feedback to also update the using-core-types page:
link the typescript package page and the core-types guide to each other
so readers can move between compiling .ts/.tsx and getting core-package
types.
* docs(typescript): address CodeRabbit review feedback
- Clarify typescript package is included by default in new apps (core types guide)
- Distinguish classic isobuild stack from modern Rspack/SWC build stack
- Give const enum examples valid identifiers
- Point React Fast Refresh link to the HMR docs page
- Hyphenate 'full-page reload'
* test(e2e): fail fast when the test app directory is missing (#14577)
When an app-creation phase fails or times out, the suite's shared
tempDir stays undefined and later phases called linkLocalRspack with
it: meteor update --npm then ran from the jest working directory and
died with a cryptic 'missing projectDir!', burying the initiating
error. Validate the app directory in linkLocalRspack and stop
optional-chaining away a failed setupMeteorApp, so CI logs point at
the phase that actually failed.
Co-authored-by: Vlad Lasky <vlasky@users.noreply.github.com>
* fix: prevent server crash on malformed request URL in sockjs transport (meteor/meteor#14594)
The SockJS transport overshadows the HTTP server's 'request'/'upgrade'
listeners with a wrapper that calls new URL(request.url, 'http://localhost')
to rewrite /websocket to /sockjs/websocket. For malformed request targets
such as `//` or `//%5Cexample.com`, new URL() throws TypeError: Invalid URL.
Because the wrapper is a plain EventEmitter listener rather than connect
middleware, the throw is uncaught and crashes the whole server process,
letting any client take the app down with a single request.
Wrap the parse/rewrite in try/catch: when the URL can't be parsed, log it with
Meteor._debug, skip the rewrite, and let the request fall through to the normal
downstream listeners, which respond as usual. The legitimate /websocket rewrite
is unchanged.
Export redirectWebsocketEndpoint and add tinytest coverage: a malformed URL no
longer throws and is left untouched, while /websocket (with query string) is
still rewritten to /sockjs/websocket.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vlad Lasky <12727610+vlasky@users.noreply.github.com>
Co-authored-by: Vlad Lasky <vlasky@users.noreply.github.com>
* fix: scope change-stream fence timestamps to their connection (#14600) (#14602)
* fix(mongo): scope change-stream fence timestamps to their connection (#14600)
A write records its clusterTime on the DDP write fence so a
ChangeStreamObserveDriver can wait for that exact timestamp before the fence
fires. That annotation was keyed by collection name alone.
An app can hold more than one MongoConnection — a second
MongoInternals.RemoteCollectionDriver pointed at another cluster is the common
case — and those connections routinely use the same collection names. The
crossbar notifies every driver listening on a collection *name*, so a write on
connection B enlisted the driver watching connection A on the same fence. That
driver then read the annotation for its collection name and waited for a
clusterTime produced by a different cluster, which its own change stream can
never emit. The wait parked forever: "change stream catching up took too long"
every 10s, and the method that issued the write never returned.
Key the annotation by (connection, collection) and look it up the same way, so
a write is only ever awaited by drivers watching the connection it went to.
clusterTimes are only comparable within a single cluster, so they must not be
matched across connections.
Waits within one connection are unchanged and still block until the stream
reaches the target, so this does not re-open #14452.
Verified against the reporter's two-app reproduction: before, the sync hangs at
removeAsync with the warning repeating indefinitely; after, it runs to
completion with no warnings.
* docs(mongo): update stale fence-key comment to the composite key
The comment above _waitUntilCaughtUp still described the pre-fix
name-only annotation key, which is now the primary in-code explanation
of a mechanism it no longer matches.
* Ignore admin.$cmd drop/create oplog entries instead of throwing (#14519)
* fix(#12727): ignore admin.$cmd drop/create oplog entries instead of throwing
handleDoc only handled admin.$cmd entries for applyOps (transactions); any other
admin.$cmd command (e.g. a drop, which Percona Server's hot-backup can replay)
fell through to throw "Unknown command". Such an entry has no db-qualified
namespace to map to a collection observer, so it is safe to ignore rather than
spam errors and stall the last-processed timestamp. Ignore drop/dropDatabase/
create admin commands (the fix radekmie approved), extracted to a testable
isIgnorableAdminCommand helper; genuinely unknown admin commands still throw.
Adds a unit test.
Fixes #12727
* test(mongo): guard admin-command predicate and cover handleDoc path
Address CodeRabbit review:
- isIgnorableAdminCommand now asserts `doc.ns === "admin.$cmd"` itself,
so the exported predicate can't misclassify a db-qualified `<db>.$cmd`
entry as ignorable if called outside the tailer's guard.
- Export handleDoc and add an integration-level regression test that
drives the admin.$cmd path end-to-end: ignorable drop/dropDatabase/
create entries resolve without throwing (the #12727 crash), while an
unknown admin command still throws "Unknown command".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: serve runtime updates to Meteor.settings.public to new clients (meteor/meteor#13489) (#14515)
* fix: serve runtime updates to Meteor.settings.public to new clients (meteor/meteor#13489)
The encoded __meteor_runtime_config__ (which embeds PUBLIC_SETTINGS) is
generated once at startup, inside generateBoilerplateInstance, and cached per
arch in boilerplateByArch / clientPrograms. getBoilerplateAsync then serves the
cached config to every request, so mutations to Meteor.settings.public made
after server startup never reached newly-connecting clients — contradicting the
documented behavior (it regressed after Fibers were dropped; worked in 2.0).
Refresh the cached runtime config from the live Meteor.settings.public before
serving, but only re-encode when the public settings actually changed (tracked
per arch) so the common no-change path stays cheap. Both the inline boilerplate
path and the external /meteor_runtime_config.js path are covered.
Adds a regression test asserting that public settings set after startup appear
in the boilerplate served to a new client.
* fix(webapp): retry public-settings refresh after a decode/parse failure
Address CodeRabbit review: only advance the per-arch snapshot
(lastPublicSettingsByArch) when the runtime-config refresh actually
succeeds. Previously the snapshot was updated unconditionally, so a
swallowed decode/parse error left the cached config stale while the
early-return guard short-circuited every later request, permanently
dropping the update. Track a `failed` flag and log the swallowed error
at debug level so the failure is no longer silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(webapp): prevent duplicate signal listeners in registerSocketFileCleanup (#14259)
* fix(webapp): prevent duplicate signal listeners in registerSocketFileCleanup
* code rabbit concerns addressed
* Made removeTestSocketFile accept an optional path (default preserves existing call sites), then wrapped the test body in try/finally so both testSocketFile and testSocketFile2 are cleared even if an assertion throws.
* Arm the legacy heartbeat watchdog only on the sockjs transport (#14546)
The 100-second watchdog exists to detect missing SockJS heartbeat
frames (sent by the server every 45s). It was armed on every inbound
message regardless of transport, and native WebSocket has no such
frames — so a healthy connection that goes quiet (e.g. with DDP
heartbeats disabled via heartbeatInterval: 0) was killed after 100s
of silence.
* fix: force-kill app process if it ignores SIGTERM on dev restart (meteor/meteor#13490) (#14516)
* fix: force-kill app process if it ignores SIGTERM on dev restart (meteor/meteor#13490)
AppProcess.stop() only sent the default SIGTERM via proc.kill(). An app that
registers its own process.on('SIGTERM') handler without calling process.exit()
overrides Node's default "terminate on SIGTERM" behavior, so the old instance
survived dev-server restarts — it leaked across restarts and kept holding the
port, which could turn into a crash loop for the newly started instance.
Escalate to SIGKILL after a short grace period if the process is still alive,
mirroring what run-mongo.js already does. The timer is unref'd so it never keeps
the tool's event loop alive, and it is cleared if the process exits on its own.
* refactor: use child handle for SIGKILL escalation to avoid pid-reuse race
Force-kill via proc.kill('SIGKILL') on the ChildProcess handle instead of
process.kill(pid, 'SIGKILL'). Once the child has exited and been reaped, the
handle-based kill is a no-op, so there is no window where a reused pid could be
signalled. This also removes the now-unnecessary liveness probe and try/catch.
* style: use const and drop historical references in stop() comments
* chore(run-app): log when SIGKILL escalation fires; document unref tradeoff
The force-kill escalation was silent even though it triggers on exactly the
#13490 symptom (an app ignoring SIGTERM and holding the port). Log a runLog
line when SIGKILL is sent; because proc.once('exit') cancels the timer on
normal exit, this only prints when an app truly ignored SIGTERM for the full
grace period, so it stays low-noise.
Also expand the unref() comment to document that the escalation only fires
while the tool keeps running (e.g. across a dev restart); a full tool
shutdown may exit before the grace period, unchanged from before this fix.
* E2E symlink-monorepo test suite and fixes (#14442)
* fix(rspack): preserve symlinks by dropping default SWC baseUrl
* add E2E symlink-monorepo test suite and app fixtures
* add Symlink support to E2E test workflows
* remove Symlink app from E2E workflows; rename test suite to symlink-monorepo
* docs: add symlinks and monorepos section to rspack integration
* docs: update symlink approaches section with diagram link
* docs: add pnpm monorepo example and note on future Meteor scaffolding support
* docs: fix spacing issues in symlinks and monorepos section
* ci: enable Git symlink support in Windows self-test workflow
* exclude `tools/e2e-tests` from `meteor-tool` isopack to prevent bloating downloads and fix Windows build issues with Babel
---------
Co-authored-by: shanky <sankalpt92@gmail.com>
* Null-guard the accounting set in Subscription.removed() (#14540)
* Null-guard the accounting set in Subscription.removed()
added() lazily creates the per-collection accounting Set and
null-guards it; removed() read the same structure unguarded. The Set
only exists if doAccountingForCollection was true at add time, and
publication strategies can change per collection at runtime through
the public setPublicationStrategy API — so an add under a
no-accounting strategy followed by a remove under an accounting one
threw a TypeError out of the publish handler.
* Disconnect the test connection even when the subscription errors
* Restore publication-strategy state fully in the strategy-flip test
Review feedback: the test left an explicit per-collection strategy
entry behind (default-equivalent, but still a global mutation). Delete
the override in the finally block instead.
* Fix change-stream fence/multiplexer queue deadlock that hangs login-style methods (#14564)
* Fix change-stream fence/multiplexer queue deadlock on login-style methods
When a method both writes to a collection and causes new observers on
that collection to be created mid-method (the canonical case is a login
method: it writes the token to `users`, then setUserId reruns the
userData publications), the new driver's _sendInitialAdds parks a
fence.beginWrite() in _writesToCommitWhenReady. _flushWritesToCommit
then commits those writes inside a multiplexer queue task and awaits
committed().
If that commit is the last outstanding write on an already-armed fence,
committed() awaits _maybeFire(), which awaits the fence's onBeforeFire
callbacks — and the change-stream fence-sync handler registered by
_startListening re-enters the SAME multiplexer queue via onFlush().
That task is queued behind the currently-running commit task, which is
itself awaiting the fire: a circular wait. The queue wedges silently
(no "catching up took too long" warning fires), the method's `updated`
message is never sent, and the client hangs in loggingIn forever. Every
subsequent method that writes the collection fences on the dead
multiplexer and hangs too.
Fix: inside the onFlush callback, start each committed() in a microtask
instead of awaiting it. The ordering guarantee is preserved — commits
still begin only after the flush point — but the queue task completes
without holding the queue across the fence fire, so the fence's own
onFlush task can run and the fire completes.
Observed in production-shaped app upgrading 3.4 -> 3.5: 2FA login
(loginWithPasswordAnd2faCode) hung deterministically; DDP inspection
showed the login method with gotResult=true but dataVisible=false, and
queue diagnostics showed the multiplexer queue with a running-but-never-
settling onFlush task holding queueLen=1 behind it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ry6SkCcLCsYwnhTQkERJZ
* Seed a joining driver's caught-up floor to avoid unbounded fence stalls
_waitUntilCaughtUp only advances on change events delivered to THIS
driver. A driver created after a write — the login-method shape again:
write the collection, then setUserId reruns publications and creates
the observer — can never receive that write's event: the shared stream
(possibly opened long ago by another observer on the same collection)
dispatched it before the driver joined, and the driver's own snapshot
already contains the write. The fence wait therefore stalls until the
NEXT unrelated write to the collection, which on an idle collection is
unbounded. Symptom: repeated "Meteor: change stream catching up took
too long" with lastProcessedOperationTime: null, and login latencies of
10s+ on quiet databases.
Fix: after addDriver resolves (stream subscription active) and before
the snapshot read, ping the server and seed _lastProcessedOperationTime
with the returned operationTime. Every write at or before that moment
is either already dispatched to _onChange or reflected in the snapshot,
so fences targeting older writes release immediately; genuine catch-up
waits (write while stream open) are unaffected.
Verified against a 4-shard Playwright E2E suite on a single-node
replica set (change streams active on all collections): stall warnings
went from 14-50 per shard to zero and all login-dependent specs passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ry6SkCcLCsYwnhTQkERJZ
* Log change-stream caught-up floor failures
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Refactor Rspack Node polyfills, replacing `node-polyfill-webpack-plugin` to solve `eliptic` audit issues (#14584)
* refactor Rspack Node polyfills, replacing `node-polyfill-webpack-plugin` with custom configuration.
* add `isomorphic-timers-promises` to Rspack Node polyfills and update client test compatibility
* simplify e2e test dependency path resolution for `@meteorjs/rspack`
* feat: make meteor-node-stubs an optional peer dependency
* test(e2e): assign rspack audit test to the 'Regressions' CI matrix category
* fix: improve module resolution fallback for SWC and meteor-node-stubs
- Add enhanced handling for local resolution of `@swc/core` and `meteor-node-stubs`.
- Implement fallback logic to avoid crashes when modules are not found.
* [Rspack] Fix spaced paths on Windows (#14474)
* add conditional shell logic and binary candidate resolution to perserve args containing spaces on windows
* add Windows-specific Meteor command path resolution and fallback support for npm/npx execution
* refactor Rspack CLI invocation to bypass `npx` on Windows and improve argument handling
* improve Rspack CLI path resolution with dynamic package.json bin lookup and fallback support
* improve Windows Meteor command resolution by adding PATH precheck for `meteor.bat` fallback
* Meteor-Rspack memory monitor & fixes (#14464)
* add build-stack-memory-bench.js for memory benchmarking during rebuild cycles
* enhance build-stack-memory-bench.js: add process monitoring, port handling, and timeout management
* enhance build-stack-memory-bench.js: add leak detection mode, heapsnapshot integration, and detailed metrics reporting
* enhance build-stack-memory-bench.js: add local meteor-rspack linking mechanism for local validation
* enhance build-stack-memory-bench.js: add TypeScript-based legacy variant skipping, detailed process RSS tracking, and other memory metric improvements
* enhance build-stack-memory-bench.js: add 'rspack-node' variant support and second top process RSS metric
* Avoid app-wide extension scans
* Deduplicate METEOR_IGNORE patterns
* scope dist watch ignore to app output
* bump runtime build ID on server recompile; ignore rspack output in app directory
* refactor server bundle loading: improve path resolution, module creation, and cache handling
* refactor RequireExternalsPlugin: centralize regex definitions and streamline stale require/import handling
* refactor server bundle loading: enhance module require handling to support Meteor package resolution and restore original behavior safely
* dedupe and append METEOR_IGNORE patterns; add unit tests to validate handling
* refactor e2e tests: centralize Playwright page handling, add fallback recovery logic, and ensure page state isolation across tests
* add e2e tests for rspack server runtime regressions and refine server bundle loading logic
* refactor server bundle loading: clarify Node require usage with Npm['require'] for distinction from Rspack imports
* rename `server-runtime.test.js` for consistency and clarity within e2e tests structure
* refactor Rspack file extensions handling: centralize discovery logic, simplify ignore patterns, and add unit tests
* update TypeScript configurations and dependencies: support TypeScript 7, enable `tsgo` checker, refine Rspack plugins, and enhance E2E test coverage
* refactor RequireExternalsPlugin: add standalone import regex handling and improve stale dependency cleanup
* expand ignore patterns: include `*.cjs` and `*.cjs.map` for Rspack build contexts
* validate heap snapshot signal configuration for leak mode in build-stack-memory-bench script
* refactor build-stack-memory-bench: improve FD count handling and enhance heap snapshot discovery logic
* refactor build-stack-memory-bench: replace execSync with execFileSync for safer and more consistent process execution
* refactor build-stack-memory-bench: update Rspack server bundle extension to `.cjs` for consistency
* refactor build-stack-memory-bench: replace `cycle` with `sample` and introduce `rebuilds` for clearer benchmarking logic
* refactor build-stack-memory-bench: add metadata handling, improve touched file restoration, and enhance rebuild readiness tracking logic
* refactor e2e-tests assertions: add fallback browser handling and ensure proper cleanup in teardown
* refactor build-stack-memory-bench: improve error propagation, enhance touched file restoration, and ensure readiness handling robustness
* refactor RequireExternalsPlugin: add regex for managed import blocks and improve stale import cleanup logic
* docs: add usage guide and benchmarking details for build-stack-memory-bench script
* docs: update MEMORY_BENCHMARK.md with generic paths and release validation instructions
* refactor(config): simplify globSync parameter in discoverRspackFileExtensions
* Add documentation for modern tools: Rspack, SWC, Profiler, and contributors' guide for integration maintenance (#14603)
* add documentation for modern tools: Rspack, SWC, Profiler, and contributors' guide for integration maintenance
* Update and reorganize Modern Tools maintainer documentation
* expand "Common maintenance tasks" sections in profiler, rspack, and swc READMEs; add debugging tips and clarifications for E2E workflows.
* remove outdated Memory Benchmarking instructions and clarify SWC dependency bumping steps
* Bridge Npm and Assets into the dev-mode Rspack server bundle
Meteor's server boot runs every linked file inside a
`(function (Npm, Assets) { ... })` wrapper, so those names are function
parameters, not globals. Since the development server started loading
`server-rspack.cjs` with a raw Node `Module.load()` instead of a linked
import (#14464), code inside the bundle can no longer see either name,
and any modern app touching `Assets` (the documented API for `private/`
files) or `Npm.require` crashes at boot with a ReferenceError.
Production builds and test mode still link the bundle and are
unaffected.
Bridge both objects through `globalThis` before loading the bundle. The
generated shim file is itself linked by Meteor, so the wrapper
parameters are in scope there, and every linked file shadows the new
globals with its own wrapper parameters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mongo): break change-stream restart loop after ChangeStreamHistoryLost (#14604) (#14607)
* fix(mongo): break change-stream restart loop after ChangeStreamHistoryLost (meteor/meteor#14604)
SharedChangeStream stored the resume token from each change event and, on
any stream error/close, restarted with `startAfter: <token>`. When that
token ages out of the oplog, `collection.watch()` reopens but the first
getMore fails with error 286 (ChangeStreamHistoryLost /
NonResumableChangeStreamError). The token was never cleared, so every
restart re-sent the same dead token — an error → restart → error loop that
ran ~10x/sec forever (log flood + wasted CPU) until the process restarted.
It armed silently on quiet-but-observed collections whose token aged past
the oplog window, then triggered on the next transient stream hiccup.
Fix:
- Detect non-resumable errors (code 286 / codeName ChangeStreamHistoryLost /
label NonResumableChangeStreamError) in the error handler, drop the resume
token, and flag the stream. The restart then falls back to
startAtOperationTime (now) instead of the dead token, so the loop ends.
- After the reopen, reconcile each attached driver against the collection:
events during the lost window were never delivered, so the driver re-runs
its query and diffs it against its cache, reusing the existing cache-guarded
_handleInsert/_handleUpdate/_handleDelete so a doc concurrently redelivered
by the reopened cursor is reconciled once, not double-emitted. Best-effort
and isolated per driver so it can never re-loop the recovered stream.
Adds tinytests covering token-clear recovery (no loop) and gap reconciliation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mongo): address review of the ChangeStreamHistoryLost recovery
Follow-up to the history-lost restart-loop fix, resolving issues found in
code review of the reconciliation path.
Correctness:
- Resync now fetches FULL documents (strips projection/fields/sort/skip/limit
before the raw find). _handleInsert re-runs the matcher, which needs every
selector field, so a server-side projection that omitted one silently dropped
genuinely-matching docs — unlike the live path, which matches the full
fullDocument. Field filtering still happens locally via _projectionFn.
- The reopened cursor is live before the resync runs, so live events raced the
reconciliation: a doc inserted live could be spuriously (and permanently)
removed, and a doc deleted live could be re-added as a phantom. Live events
now win — ids touched by _flushPendingWrites during a resync are recorded in
_resyncLiveTouched and left untouched by the resync.
Robustness:
- _restart serializes: a second non-resumable error on the freshly reopened
cursor coalesces into one follow-up run instead of racing a second resync.
- _isNonResumableError no longer checks the nonexistent
NonResumableChangeStreamError label; it uses the driver's real
ResumableChangeStreamError label (plus the code 286 fast path), so other
non-resumable errors recover too instead of looping.
- Per-document try/catch in the resync loop, and bounded exponential backoff on
repeated failed reopens so a stream that cannot reopen backs off.
Docs/cleanup: updated the SharedChangeStream docstring for the non-resumable
path, added _deriveMeteorId to de-duplicate id derivation, consistent cache
optional-chaining.
Tests: harden test 1 (restore patched method in finally, widen the tight wait),
add a projected-cursor resync test and a resumable-error test. 87/0/0/87 pass
(TINYTEST_FILTER=changestream METEOR_REACTIVITY_ORDER=changeStreams).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mongo): address second-round review of history-lost recovery
Follow-up to the review fixes, resolving issues found re-reviewing them.
- _isNonResumableError: the previous round over-broadened this to "any error
lacking the ResumableChangeStreamError label", which misclassified resumable
MongoNetworkError / CursorNotFound / timeout errors (the driver tags those
resumable by TYPE, not by label, and re-emits them only after its own resume
gave up). That discarded a still-valid resume token and forced a full-
collection resync on every sustained connectivity blip. Narrow it back to the
genuinely non-resumable change-stream errors: ChangeStreamHistoryLost (286)
and ChangeStreamFatalError (280). Everything else keeps the token and resumes
via startAfter.
- Restart backoff now applies on the error/close handler paths too (previously
only the rarely-hit _restart catch), so a stream that re-errors on every
reopen backs off instead of spinning ~10x/sec. The failure counter resets when
the reopened stream actually delivers an event (_onChange), not merely on a
successful reopen.
- _flushPendingWrites records an id into _resyncLiveTouched AFTER the live
handler applies it, not before — so a handler that throws no longer suppresses
the resync's corrective pass for that id.
- _deriveMeteorId uses the toHexString guard (matching the live path) so a
non-string/non-ObjectID _id passes through instead of throwing, and the whole
resync per-doc body is wrapped so one malformed doc can't abort reconciliation.
Tests: replace the resumable-error test with one modeling a real MongoNetworkError
(the regression scenario), add a 280-fatal test and a deterministic live-touched
recording test, wrap test 2 cleanup in try/finally. 89/0/0/89 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mongo): third-round review nits — backoff double-count + test gaps
- Backoff counter: the mongo driver emits BOTH 'error' and 'close' for a single
stream failure, so incrementing _restartFailures in each handler advanced it by
2 per failure (steeper backoff than the 2^(failures-1) intent). Count a cursor's
failure at most once via a per-cursor _failureCounted flag reset in _open;
_noteFailure() de-dupes the paired events. The failed-reopen path still counts
directly in _restart's catch (no cursor is created there).
- Tests: add a NEGATIVE live-touched test (an apply that throws must NOT record,
the assertion that actually distinguishes record-after-apply from before), and
a deterministic backoff test (delay growth + 5000ms cap, error+close counted
once, reset on a delivered event). 91/0/0/91 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(mongo): cover the per-cursor failure-flag reset and restart coalescing
Close the two coverage gaps the review flagged (both guard mechanisms added by
this PR, so a silent revert would otherwise ship undetected):
- assert reopening a fresh cursor clears _failureCounted (so later failures keep
backing off);
- assert a restart requested while one is in flight coalesces into a follow-up
request instead of running a second reopen/resync.
92/0/0/92 changestream tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix dev proxy websocket cleanup (#14386)
* Add regression test for abandoned dev proxy websocket upgrades
The test demonstrates that abandoned websocket upgrades can consume the dev proxy agent capacity and block unrelated HTTP requests.
Made-with: Cursor
* Fix dev proxy websocket cleanup
Avoid sharing the HTTP keep-alive agent with websocket proxy requests, and tear down pending websocket upgrade requests when the client closes before the upstream upgrade completes.
Made-with: Cursor
* Refine dev proxy cleanup behavior
Defer shared proxy agent destruction until the HTTP server has drained active connections, and derive the websocket regression count from the proxy agent cap.
Made-with: Cursor
* Tighten proxy websocket selftest cleanup
Wait for socket and server shutdown in the regression test, and document the test helpers used to exercise abandoned websocket upgrades.
Made-with: Cursor
* Guard repeat socket teardown in proxy selftest
Made-with: Cursor
---------
Co-authored-by: italo jose <italo.i@live.com>
* fix(mongo): normalize both operands in compareOperationTimes (#14600) (#14609)
MongoDB.Timestamp#compare never reads .t/.i off a plain {t,i} object, so
compareOperationTimes silently returned a wrong result whenever the second
operand was passed in object form (the documented contract accepts it).
Wrap both operands in MongoDB.Timestamp before comparing.
This unblocks the changeStreams test-packages job on release-3.5.1: the
change-stream driver's caught-up-floor seed makes _lastProcessedOperationTime
non-null, so _waitUntilCaughtUp now runs this comparison against a fence
target ts and the mis-compare released a wait that must still block.
* chore: update @meteorjs/rspack version to 2.1.0-beta.0 across all packages
* Bump dev bundle version to 24.15.0.3 :comet:
* Meteor version to 3.5.1-beta.0 :comet:
* Changelog, docs and release config for 3.5.1-beta.0 :comet:
* Meteor version to 3.5.1 :comet:
* Bump Meteor version to 3.5.1 in package.json
---------
Co-authored-by: Nacho Codoñer <igcogi@gmail.com>
Co-authored-by: AviraL0013 <aviralsapra13@gmail.com>
Co-authored-by: Michael Vogt <mtpfeif@gmail.com>
Co-authored-by: dupontbertrand <dupontbertrand59@gmail.com>
Co-authored-by: BastienRdz <barodrig@student.42.fr>
Co-authored-by: Per Bergland <per@refapp.se>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Evan Broder <evan@ebroder.net>
Co-authored-by: Mark Russell <mark.russell@tulip.co>
Co-authored-by: Jordan Baker <jbb@scryent.com>
Co-authored-by: Sankalp Tripathi <sankalpt92@gmail.com>
Co-authored-by: Julio Araujo <julio.araujo@rocket.chat>
Co-authored-by: Vlad Lasky <12727610+vlasky@users.noreply.github.com>
Co-authored-by: Vlad Lasky <vlasky@users.noreply.github.com>
Co-authored-by: Tim Heckel <tim.heckel@springmathaccelerate.com>
Co-authored-by: John Furneaux <john@hive.com>
Summary
Fixes a Meteor dev proxy failure mode where abandoned websocket upgrades can block unrelated HTTP requests through
meteor --port.The dev proxy keeps a shared
http.Agent({ maxSockets: 100 })for proxied HTTP traffic. Websocket upgrades were using that same pooled agent, so enough client sockets closing during unresolved upstream upgrades could consume all agent slots. After that, normal HTTP requests through the proxy waited behind abandoned websocket proxy requests and the dev app appeared wedged.This PR:
agent: falsemeteor-toolfrom3.4.1to3.4.2Broken Before / Fixed After
Broken before, with the regression test applied to current
develbehavior:Fixed after this PR:
Validation
Summary by CodeRabbit
Chores
Bug Fixes
Tests