Skip to content

Commit 2e2cd43

Browse files
authored
Merge pull request #61 from BastienRodz/fix/cordova-meteor3
2 parents d4a230f + d46742f commit 2e2cd43

2 files changed

Lines changed: 132 additions & 1 deletion

File tree

client/timesync-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ TimeSync.setSyncUrl();
6868

6969
const updateOffset = function () {
7070
const t0 = Date.now();
71-
if (TimeSync.forceDDP || SyncInternals.useDDP) {
71+
if (Meteor.isCordova || TimeSync.forceDDP || SyncInternals.useDDP) {
7272
Meteor.callAsync('_timeSync')
7373
.then((res) => handleResponse(t0, null, res))
7474
.catch((err) => handleResponse(t0, err, null));

tests/client.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,135 @@ describe('Timesync', () => {
170170
}
171171
});
172172
});
173+
174+
// Regression tests for the Cordova/Capacitor fix: on mobile webviews the HTTP
175+
// request to /_timesync can fail (CORS, URL resolution). updateOffset()
176+
// should force DDP transport when Meteor.isCordova is true.
177+
describe('transport selection', () => {
178+
// Spy on the DDP side via Meteor.callAsync monkey-patch, and on the HTTP
179+
// side via PerformanceObserver: `fetch` is imported as a module binding in
180+
// timesync-client.js and cannot be swapped from the test, but every fetch
181+
// still surfaces as a `resource` entry in the Performance API.
182+
function installTransportSpies(syncUrl, counters) {
183+
const originalCallAsync = Meteor.callAsync;
184+
Meteor.callAsync = function (methodName) {
185+
if (methodName === '_timeSync') counters.ddp++;
186+
return originalCallAsync.apply(this, arguments);
187+
};
188+
189+
const po = new PerformanceObserver((list) => {
190+
for (const entry of list.getEntries()) {
191+
if (entry.name && entry.name.indexOf(syncUrl) !== -1) counters.http++;
192+
}
193+
});
194+
po.observe({ type: 'resource', buffered: false });
195+
196+
return function restore() {
197+
Meteor.callAsync = originalCallAsync;
198+
po.disconnect();
199+
};
200+
}
201+
202+
it('forces DDP transport when Meteor.isCordova is true', function (done) {
203+
this.timeout(10000);
204+
205+
const originalIsCordova = Meteor.isCordova;
206+
const originalForceDDP = TimeSync.forceDDP;
207+
const originalUseDDP = SyncInternals.useDDP;
208+
const syncUrl = TimeSync.getSyncUrl();
209+
210+
const counters = { ddp: 0, http: 0 };
211+
const restoreSpies = installTransportSpies(syncUrl, counters);
212+
213+
// Simulate a Cordova client whose DDP connection flag has not yet flipped.
214+
// Without the fix, this combination would route through HTTP fetch.
215+
Meteor.isCordova = true;
216+
TimeSync.forceDDP = false;
217+
SyncInternals.useDDP = false;
218+
219+
function cleanup() {
220+
restoreSpies();
221+
Meteor.isCordova = originalIsCordova;
222+
TimeSync.forceDDP = originalForceDDP;
223+
SyncInternals.useDDP = originalUseDDP;
224+
// A second resync rearms the internal setInterval with the restored
225+
// state, avoiding a transport-mismatched timer leaking into later tests.
226+
TimeSync.resync();
227+
}
228+
229+
TimeSync.resync();
230+
231+
simplePoll(
232+
() => counters.ddp >= 1,
233+
() => {
234+
// Give PerformanceObserver a tick to flush any pending resource
235+
// entries before asserting that HTTP was not used.
236+
Meteor.setTimeout(() => {
237+
cleanup();
238+
try {
239+
assert.isAtLeast(counters.ddp, 1,
240+
"Meteor.callAsync('_timeSync') must be used on Cordova");
241+
assert.equal(counters.http, 0,
242+
"HTTP fetch to the sync URL must not be used on Cordova");
243+
done();
244+
} catch (err) {
245+
done(err);
246+
}
247+
}, 100);
248+
},
249+
() => {
250+
cleanup();
251+
done(new Error('Cordova client did not route through DDP within 5s'));
252+
},
253+
5000, 50
254+
);
255+
});
256+
257+
it('uses HTTP transport on a plain browser by default', function (done) {
258+
this.timeout(10000);
259+
260+
const originalIsCordova = Meteor.isCordova;
261+
const originalForceDDP = TimeSync.forceDDP;
262+
const originalUseDDP = SyncInternals.useDDP;
263+
const syncUrl = TimeSync.getSyncUrl();
264+
265+
const counters = { ddp: 0, http: 0 };
266+
const restoreSpies = installTransportSpies(syncUrl, counters);
267+
268+
Meteor.isCordova = false;
269+
TimeSync.forceDDP = false;
270+
SyncInternals.useDDP = false;
271+
272+
function cleanup() {
273+
restoreSpies();
274+
Meteor.isCordova = originalIsCordova;
275+
TimeSync.forceDDP = originalForceDDP;
276+
SyncInternals.useDDP = originalUseDDP;
277+
TimeSync.resync();
278+
}
279+
280+
TimeSync.resync();
281+
282+
simplePoll(
283+
() => counters.http >= 1,
284+
() => {
285+
cleanup();
286+
try {
287+
assert.isAtLeast(counters.http, 1,
288+
'HTTP fetch to the sync URL must be used on a plain browser');
289+
assert.equal(counters.ddp, 0,
290+
"Meteor.callAsync('_timeSync') must not be used on a plain browser");
291+
done();
292+
} catch (err) {
293+
done(err);
294+
}
295+
},
296+
() => {
297+
cleanup();
298+
done(new Error('Browser client did not route through HTTP within 5s'));
299+
},
300+
5000, 50
301+
);
302+
});
303+
});
173304
});

0 commit comments

Comments
 (0)