Skip to content

Commit

Permalink
Merge branch 'pr-1023' into pull-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
n1mmy committed May 23, 2013
2 parents 4e05cc6 + 65636b5 commit 1f78662
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 30 deletions.
2 changes: 2 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
packages can be used to perform an OAuth exchange without creating an
account and logging in. #1024

* Make `Meteor.defer` work in an inactive tab in iOS. #1023

* Allow new `Random` instances to be constructed with specified seed. This
can be used to create repeatable test cases for code that picks random
values. #1033
Expand Down
6 changes: 6 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ node-kexec: https://github.com/jprichardson/node-kexec
Copyright (c) 2011-2012 JP Richardson


----------
setImmediate: https://github.com/NobleJS/setImmediate
----------

Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola


==============
Apache License
Expand Down
1 change: 1 addition & 0 deletions examples/other/defer-in-inactive-tab/.meteor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
local
5 changes: 5 additions & 0 deletions examples/other/defer-in-inactive-tab/.meteor/packages
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

13 changes: 13 additions & 0 deletions examples/other/defer-in-inactive-tab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Defer in Inactive Tab

Tests that `Meteor.defer` works in an inactive tab in iOS Safari.

(`setTimeout` and `setInterval` events aren't delivered to inactive
tabs in iOS Safari until they become active again).

Sadly we have to run the test manually because scripts aren't allowed
to open windows themselves except in response to user events.

This test will not run on Chrome for iOS because the storage event is
not implemented in that browser. Also doesn't attempt to run on
versions of IE that don't support `window.addEventListener`.
52 changes: 52 additions & 0 deletions examples/other/defer-in-inactive-tab/test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<head>
<title>defer in inactive tab</title>
<meta name="viewport" content="width=device-width">
</head>

<body>
{{> route}}
</body>

<template name="route">
{{#if isParent}}
{{> parent}}
{{else}}
{{> child}}
{{/if}}
</template>

<template name="parent">
<h1>Test Defer in Inactive Tab</h1>

<p>
Step one: open second tab:
<button id="openTab">Open Tab</button>
</p>

<p>
Step two: run test:
<button id="runTest">Run Test</button>
</p>

<p>
In a successful test the test status will immediately change to
"test successful". (If you switch to the child tab yourself and
that makes the test claim to be successful, that's actually an
invalid test because you're letting the child tab become the
active tab).
</p>

<p style="padding: 1em; outline: 1px solid gray">
Test status: <b>{{testStatus}}</b>
</p>

<p>
After the test has run successfully you can close the child tab.
</p>

</template>

<template name="child">
<p>This is the child.</p>
<p>Switch back to the first tab and run the test.</p>
</template>
57 changes: 57 additions & 0 deletions examples/other/defer-in-inactive-tab/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
if (Meteor.isClient) {

var isParent = (window.location.pathname === '/');
var isChild = ! isParent;

Template.route.isParent = function () {
return isParent;
};

Template.parent.testStatus = function () {
return Session.get('testStatus');
};

Template.parent.events({
'click #openTab': function () {
window.open('/child');
},

'click #runTest': function () {
if (localStorage.getItem('ping') === '!' ||
localStorage.getItem('pong') === '!') {
Session.set('testStatus', 'Test already run. Close the second tab (if open), refresh this page, and run again.');
}
else {
localStorage.setItem('ping', '!');
}
}
});

if (isParent) {
Session.set('testStatus', '');

Meteor.startup(function () {
localStorage.setItem('ping', null);
localStorage.setItem('pong', null);
});
window.addEventListener('storage', function (event) {
if (event.key === 'pong' && event.newValue === '!') {
Session.set('testStatus', 'test successful');
}
});
}

if (isChild) {
window.addEventListener('storage', function (event) {
if (event.key === 'ping' && event.newValue === '!') {
// If we used setTimeout here in iOS Safari it wouldn't
// work (unless we switched tabs) because setTimeout and
// setInterval events don't fire in inactive tabs.
Meteor.defer(function () {
localStorage.setItem('pong', '!');
});
}
});
}

}
3 changes: 3 additions & 0 deletions packages/meteor/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Package.on_use(function (api, where) {
api.add_files('client_environment.js', 'client');
api.add_files('server_environment.js', 'server');
api.add_files('helpers.js', ['client', 'server']);
api.add_files('setimmediate.js', ['client', 'server']);
api.add_files('timers.js', ['client', 'server']);
api.add_files('errors.js', ['client', 'server']);
api.add_files('fiber_helpers.js', 'server');
Expand Down Expand Up @@ -63,4 +64,6 @@ Package.on_test(function (api) {
api.add_files('fiber_helpers_test.js', ['server']);

api.add_files('url_tests.js', ['client', 'server']);

api.add_files('timers_tests.js', ['client', 'server']);
});
141 changes: 141 additions & 0 deletions packages/meteor/setimmediate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Chooses one of three setImmediate implementations:
//
// * Native setImmediate (IE 10, Node 0.9+)
//
// * postMessage (many browsers)
//
// * setTimeout (fallback)
//
// The postMessage implementation is based on
// https://github.com/NobleJS/setImmediate/tree/1.0.1
//
// Don't use `nextTick` for Node since it runs its callbacks before
// I/O, which is stricter than we're looking for.
//
// Not installed as a polyfill, as our public API is `Meteor.defer`.
// Since we're not trying to be a polyfill, we have some
// simplifications:
//
// If one invocation of a setImmediate callback pauses itself by a
// call to alert/prompt/showModelDialog, the NobleJS polyfill
// implementation ensured that no setImmedate callback would run until
// the first invocation completed. While correct per the spec, what it
// would mean for us in practice is that any reactive updates relying
// on Meteor.defer would be hung in the main window until the modal
// dialog was dismissed. Thus we only ensure that a setImmediate
// function is called in a later event loop.
//
// We don't need to support using a string to be eval'ed for the
// callback, arguments to the function, or clearImmediate.

"use strict";

var global = this;


// IE 10, Node >= 9.1

function useSetImmediate() {
if (! global.setImmediate)
return null;
else {
var setImmediate = function (fn) {
global.setImmediate(fn);
};
setImmediate.implementation = 'setImmediate';
return setImmediate;
}
}


// Android 2.3.6, Chrome 26, Firefox 20, IE 8-9, iOS 5.1.1 Safari

function usePostMessage() {
// The test against `importScripts` prevents this implementation
// from being installed inside a web worker, where
// `global.postMessage` means something completely different and
// can't be used for this purpose.

if (!global.postMessage || global.importScripts) {
return null;
}

// Avoid synchronous post message implementations.

var postMessageIsAsynchronous = true;
var oldOnMessage = global.onmessage;
global.onmessage = function () {
postMessageIsAsynchronous = false;
};
global.postMessage("", "*");
global.onmessage = oldOnMessage;

if (! postMessageIsAsynchronous)
return null;

var funcIndex = 0;
var funcs = {};

// Installs an event handler on `global` for the `message` event: see
// * https://developer.mozilla.org/en/DOM/window.postMessage
// * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages

// XXX use Random.id() here?
var MESSAGE_PREFIX = "Meteor._setImmediate." + Math.random() + '.';

function isStringAndStartsWith(string, putativeStart) {
return (typeof string === "string" &&
string.substring(0, putativeStart.length) === putativeStart);
}

function onGlobalMessage(event) {
// This will catch all incoming messages (even from other
// windows!), so we need to try reasonably hard to avoid letting
// anyone else trick us into firing off. We test the origin is
// still this window, and that a (randomly generated)
// unpredictable identifying prefix is present.
if (event.source === global &&
isStringAndStartsWith(event.data, MESSAGE_PREFIX)) {
var index = event.data.substring(MESSAGE_PREFIX.length);
try {
if (funcs[index])
funcs[index]();
}
finally {
delete funcs[index];
}
}
}

if (global.addEventListener) {
global.addEventListener("message", onGlobalMessage, false);
} else {
global.attachEvent("onmessage", onGlobalMessage);
}

var setImmediate = function (fn) {
// Make `global` post a message to itself with the handle and
// identifying prefix, thus asynchronously invoking our
// onGlobalMessage listener above.
++funcIndex;
funcs[funcIndex] = fn;
global.postMessage(MESSAGE_PREFIX + funcIndex, "*");
};
setImmediate.implementation = 'postMessage';
return setImmediate;
}


function useTimeout() {
var setImmediate = function (fn) {
global.setTimeout(fn, 0);
};
setImmediate.implementation = 'setTimeout';
return setImmediate;
}


Meteor._setImmediate =
useSetImmediate() ||
usePostMessage() ||
useTimeout();
50 changes: 20 additions & 30 deletions packages/meteor/timers.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
var withCurrentInvocation = function (f) {
if (Meteor._CurrentInvocation) {
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
throw new Error("Can't set timers inside simulations");
return function () { Meteor._CurrentInvocation.withValue(null, f); };
}
else
return f;
};

var bindAndCatch = function (context, f) {
return Meteor.bindEnvironment(withCurrentInvocation(f), function (e) {
// XXX report nicely (or, should we catch it at all?)
Meteor._debug("Exception from " + context + ":", e);
});
};

_.extend(Meteor, {
// Meteor.setTimeout and Meteor.setInterval callbacks scheduled
// inside a server method are not part of the method invocation and
// should clear out the CurrentInvocation environment variable.

setTimeout: function (f, duration) {
if (Meteor._CurrentInvocation) {
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
throw new Error("Can't set timers inside simulations");

var f_with_ci = f;
f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); };
}

return setTimeout(Meteor.bindEnvironment(f, function (e) {
// XXX report nicely (or, should we catch it at all?)
Meteor._debug("Exception from setTimeout callback:", e.stack);
}), duration);
return setTimeout(bindAndCatch("setTimeout callback", f), duration);
},

setInterval: function (f, duration) {
if (Meteor._CurrentInvocation) {
if (Meteor._CurrentInvocation.get() && Meteor._CurrentInvocation.get().isSimulation)
throw new Error("Can't set timers inside simulations");

var f_with_ci = f;
f = function () { Meteor._CurrentInvocation.withValue(null, f_with_ci); };
}

return setInterval(Meteor.bindEnvironment(f, function (e) {
// XXX report nicely (or, should we catch it at all?)
Meteor._debug("Exception from setInterval callback:", e);
}), duration);
return setInterval(bindAndCatch("setInterval callback", f), duration);
},

clearInterval: function(x) {
Expand All @@ -41,16 +36,11 @@ _.extend(Meteor, {
return clearTimeout(x);
},

// won't be necessary once we clobber the global setTimeout
//
// XXX consider making this guarantee ordering of defer'd callbacks, like
// Deps.afterFlush or Node's nextTick (in practice). Then tests can do:
// callSomethingThatDefersSomeWork();
// Meteor.defer(expect(somethingThatValidatesThatTheWorkHappened));
defer: function (f) {
// Older Firefox will pass an argument to the setTimeout callback
// function, indicating the "actual lateness." It's non-standard,
// so for defer, standardize on not having it.
Meteor.setTimeout(function () {f();}, 0);
Meteor._setImmediate(bindAndCatch("defer callback", f));
}
});
Loading

0 comments on commit 1f78662

Please sign in to comment.