Skip to content

fix(cordova): force DDP transport for time sync on Cordova/Capacitor#61

Merged
harryadel merged 4 commits into
Meteor-Community-Packages:feature/meteor3from
BastienRodz:fix/cordova-meteor3
Apr 16, 2026
Merged

fix(cordova): force DDP transport for time sync on Cordova/Capacitor#61
harryadel merged 4 commits into
Meteor-Community-Packages:feature/meteor3from
BastienRodz:fix/cordova-meteor3

Conversation

@BastienRodz
Copy link
Copy Markdown
Contributor

Problem

On Cordova and Capacitor apps, the HTTP fetch to /_timesync can fail with CORS or URL resolution errors. This is a known class of issues (see #44, #52) that affects any Meteor 3 app running inside a mobile webview.

The web.cordova architecture is used by both legacy Cordova apps and modern Capacitor setups (via METEOR_FORCE_INCLUDE_ARCHS="web.cordova"), so Meteor.isCordova is true in both cases.

Fix

Force DDP transport when Meteor.isCordova is true:

- if (TimeSync.forceDDP || SyncInternals.useDDP) {
+ if (Meteor.isCordova || TimeSync.forceDDP || SyncInternals.useDDP) {

This is safe because:

  • The DDP connection is already established before timesync runs
  • DDP was already the fallback mechanism (via forceDDP / useDDP)
  • The existing setSyncUrl() already special-cases Cordova (L51), acknowledging that HTTP routing behaves differently on mobile — this change aligns updateOffset() with that same assumption
  • No behavior change for web.browser clients

Context

@harryadel
Copy link
Copy Markdown
Member

Thank you for your contribution @BastienRodz
The change is small and simple but can you add a test case to showcase the problem solved and prevent regression?

@BastienRodz
Copy link
Copy Markdown
Contributor Author

Hello @harryadel !
I'll do that. Thanks for feedback !

@harryadel
Copy link
Copy Markdown
Member

On a side note, you mentioned Capacitor. I was interested how you managed to do it in your app. Any links/resources on that would be appreciated. Thanks!

Add two Mocha tests under a 'transport selection' describe block:

- forces DDP on Meteor.isCordova=true — asserts Meteor.callAsync('_timeSync')
  is used and globalThis.fetch to the sync URL is not. Reproduces the
  scenario that motivated the fix (Cordova client with useDDP still false).

- uses HTTP on a plain browser — guards the else fetch(...) branch of
  updateOffset() against accidental deletion, proving the fix does not
  regress the web.browser path.

Both tests install spies on Meteor.callAsync and globalThis.fetch, drive
a TimeSync.resync(), and poll with simplePoll rather than a fixed timeout
to stay reliable under CI load.
@BastienRodz
Copy link
Copy Markdown
Contributor Author

Glad you asked - there is unfortunately no plug-and-play way to do this today, it's mostly glue around Meteor's existing web.cordova architecture. My colleague @tmeyer24 at ALLOHOUSTON did the actual integration work and documented our approach on the Meteor forum, so I'll point you at his posts rather than reinvent the wheel here:

The short version of what we do in our app:

  1. Reuse the web.cordova arch as the build target - no new Meteor arch for Capacitor, we just repurpose the one that already produces a client bundle meant to run inside a webview. In dev we force its inclusion with METEOR_FORCE_INCLUDE_ARCHS="web.cordova" alongside DDP_DEFAULT_CONNECTION and --mobile-server.
  2. Post-process the build for Capacitor: copy programs/web.cordova/{js,css,packages,app} into capacitor/www-dist/, strip .map files, pull index.html from a running Meteor server with curl http://<host>:<port>/__cordova/index.html, then rewrite /__cordova// with sed.
  3. Inject __meteor_runtime_config__ (the one carrying DDP_DEFAULT_CONNECTION_URL) from the real backend into the local index.html, otherwise the app boots but can't reach the server.
  4. Shim WebAppLocalServer with a tiny JS file, since cordova-plugin-meteor-webapp doesn't run under Capacitor. Hot code push would normally live there - we do a server-side version check and fall back to store updates.

That's essentially it. It's ugly, but it lets us ship web, iOS and Android from a single Meteor codebase. Meteor.isCordova stays true because from the client bundle's perspective it is the Cordova arch, which is exactly what motivated this PR.

Also worth knowing: on iOS the WebView reports mobileSafari 0.0.0, so Meteor classifies it as a legacy client and skips the modern bundle (breaks modern packages like@tanstack/react-query@5). Fix is an AppDelegate UA override - covered in meteor/meteor#10794 and in @tmeyer24's first post. The umbrella discussion meteor/meteor#13562 tracks upstream progress on first-class Capacitor support.

@harryadel
Copy link
Copy Markdown
Member

@BastienRodz Universe works in weird way! Today I was working on migrating from Cordova to Capacitor. And now you're opening up a PR and I'm asking you about it lol. 🤣

Yes, I relied on https://forums.meteor.com/t/migrating-from-cordova-to-capacitor/63874 like you mentioned and it worked for the most part. Except for tiny error. I guess your setup is a lot more nuanced. It's important for @nachocodoner to take note of this.

@harryadel
Copy link
Copy Markdown
Member

uses HTTP transport on a plain browser by default test is failing @BastienRodz

The previous fetch spy installed itself on globalThis.fetch, but
timesync-client.js imports fetch from meteor/fetch as a module binding
at load time. Replacing globalThis.fetch had no effect, so the browser
regression test could never observe an HTTP sync and timed out.

Switch to PerformanceObserver on 'resource' entries, which captures
any fetch() to /_timesync regardless of how the caller references it.
@BastienRodz
Copy link
Copy Markdown
Contributor Author

Sorry, I was not paying enough attention. All fixed 👍

@harryadel harryadel merged commit 2e2cd43 into Meteor-Community-Packages:feature/meteor3 Apr 16, 2026
2 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