From 897cc78d8454996cb743efb1829263159cbdcb05 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 23 Jun 2014 16:35:55 -0700 Subject: [PATCH 01/69] Update docs for bcrypt dependency --- docs/client/concepts.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 0ed41a18b3..dcf141beb3 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -758,8 +758,9 @@ the native packages included in the bundle. To do that, make sure you have `npm` available, and run the following: $ cd bundle/programs/server/node_modules - $ rm -r fibers + $ rm -r fibers bcrypt $ npm install fibers@1.0.1 + $ npm install bcrypt@0.7.7 {{/warning}} {{/markdown}} From 72c3646cfe9213d20daa533581527ab0c92db77c Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 23 Jun 2014 16:38:35 -0700 Subject: [PATCH 02/69] Make docs `npm install` one line --- docs/client/concepts.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index dcf141beb3..a41d1694f3 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -759,8 +759,7 @@ have `npm` available, and run the following: $ cd bundle/programs/server/node_modules $ rm -r fibers bcrypt - $ npm install fibers@1.0.1 - $ npm install bcrypt@0.7.7 + $ npm install fibers@1.0.1 bcrypt@0.7.7 {{/warning}} {{/markdown}} From c5292257533fd899dde0d4f3849864d23090c26e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 24 Jun 2014 08:33:02 -0700 Subject: [PATCH 03/69] Remove sentence about SRP from docs --- docs/client/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client/api.js b/docs/client/api.js index e1d5d24746..b389f4fad9 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1087,7 +1087,7 @@ Template.api.loginWithPassword = { { name: "password", type: "String", - descr: "The user's password. This is __not__ sent in plain text over the wire — it is secured with [SRP](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)." + descr: "The user's password." }, { name: "callback", From 7106caef4910749e048594f6a447013bff5314bc Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 7 Jul 2014 11:44:10 -0700 Subject: [PATCH 04/69] Remove more SRP from docs --- docs/client/api.html | 10 ++++------ docs/client/concepts.html | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index adc97f191e..83580b0103 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1886,12 +1886,10 @@ authentication. In addition to the basic username and password-based sign-in process, it also supports email-based sign-in including address verification and password recovery emails. -Unlike most web applications, the Meteor client does not send the user's -password directly to the server. It uses the [Secure Remote Password -protocol](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) -to ensure the server never sees the user's plain-text password. This -helps protect against embarrassing password leaks if the server's -database is compromised. +The Meteor server stores passwords using the +[bcrypt](http://en.wikipedia.org/wiki/Bcrypt) algorithm. This helps +protect against embarrassing password leaks if the server's database is +compromised. To add password support to your application, run `$ meteor add accounts-password`. You can construct your own user interface using the diff --git a/docs/client/concepts.html b/docs/client/concepts.html index a41d1694f3..0dcd570c19 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -311,8 +311,7 @@ releases will include support for other databases. Meteor includes [Meteor Accounts](#accounts_api), a state-of-the-art authentication system. It features secure password login using the -[Secure Remote Password -protocol](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol), +[bcrypt](http://en.wikipedia.org/wiki/Bcrypt) algorithm, and integration with external services including Facebook, GitHub, Google, Meetup, Twitter, and Weibo. Meteor Accounts defines a [`Meteor.users`](#meteor_users) collection where developers can store From 5b80ea33b94acf1265dac057cf55f168249f94c5 Mon Sep 17 00:00:00 2001 From: Maria Pacana Date: Tue, 8 Jul 2014 17:59:31 -0700 Subject: [PATCH 05/69] Added links to Meteor Manual on docs page. --- docs/client/api.html | 15 ++++++++++++++- docs/client/introduction.html | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/client/api.html b/docs/client/api.html index 83580b0103..f9f10bdf74 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1994,7 +1994,7 @@ Override fields of the object by assigning to them: a `String` for the subject line of a reset password email. - `resetPassword.text`: A `Function` that takes a user object and a url, and returns the body text for a reset password email. - - `resetPassword.html`: An optional `Function` that takes a user object and a + - `resetPassword.html`: An optional `Function` that takes a user object and a url, and returns the body html for a reset password email. - `enrollAccount`: Same as `resetPassword`, but for initial password setup for new accounts. @@ -2558,6 +2558,11 @@ advanced facilities such as `Deps.Dependency` and `onInvalidate` callbacks are intended primarily for package authors implementing new reactive data sources. +To learn more about how Deps works and to explore advanced features of Deps, +visit the Deps chapter in the +Meteor Manual, which describes it in +complete detail. + {{> api_box deps_autorun }} `Deps.autorun` allows you to run a function that depends on reactive data @@ -2633,6 +2638,10 @@ after processing outstanding invalidations. It is illegal to call `flush` from inside a `flush` or from a running computation. +The Meteor Manual +describes the motivation for the flush cycle and the guarantees made by +`Deps.flush` and `Deps.afterFlush`. + {{> api_box deps_nonreactive }} Calls `func` with `Deps.currentComputation` temporarily set to `null` @@ -2820,6 +2829,10 @@ A Dependency's dependent computations are always valid (they have either by the Dependency itself or some other way, it is immediately removed. +See the +Meteor Manual to learn how to create a reactive data source using + Deps.Dependency. + {{> api_box dependency_changed }} {{> api_box dependency_depend }} diff --git a/docs/client/introduction.html b/docs/client/introduction.html index 7783fe4024..6a5220c5a4 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -130,6 +130,11 @@ with the project!
GitHub
The core code is on GitHub. If you're able to write code or file issues, we'd love to have your help. Please read Contributing to Meteor for how to get started. +
+ +
The Meteor Manual
+
In-depth articles about the core components of Meteor can be found on theMeteor Manual. We've published the first article, which is about Deps, our transparent reactivity framework. More articles (covering topics like Blaze, Unibuild, and DDP) coming soon! +
{{/markdown}} From 121a4adff2b9f315b6f6dae9501d69f27aa09934 Mon Sep 17 00:00:00 2001 From: Maria Pacana Date: Tue, 8 Jul 2014 18:55:21 -0700 Subject: [PATCH 06/69] Revised wording about Manual on docs page. --- docs/client/api.html | 2 +- docs/client/introduction.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index f9f10bdf74..76da6c15f1 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2558,7 +2558,7 @@ advanced facilities such as `Deps.Dependency` and `onInvalidate` callbacks are intended primarily for package authors implementing new reactive data sources. -To learn more about how Deps works and to explore advanced features of Deps, +To learn more about how Deps works and to explore advanced ways to use it, visit the Deps chapter in the Meteor Manual, which describes it in complete detail. diff --git a/docs/client/introduction.html b/docs/client/introduction.html index 6a5220c5a4..6c4d9e2e30 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -133,7 +133,7 @@ with the project!
The Meteor Manual
-
In-depth articles about the core components of Meteor can be found on theMeteor Manual. We've published the first article, which is about Deps, our transparent reactivity framework. More articles (covering topics like Blaze, Unibuild, and DDP) coming soon! +
In-depth articles about the core components of Meteor can be found on the Meteor Manual. The first article is about Deps, our transparent reactivity framework. More articles (covering topics like Blaze, Unibuild, and DDP) are coming soon!
From 0ba651a79bd35574151b4ff1e4e4bf9b8f20fe02 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Thu, 17 Jul 2014 19:10:54 -0700 Subject: [PATCH 07/69] =?UTF-8?q?Don=E2=80=99t=20create=20Connection=20unt?= =?UTF-8?q?il=20livedata=20test=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit for “livedata - publisher errors” --- packages/livedata/livedata_tests.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index 07bd42e779..6cf075a703 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -526,16 +526,18 @@ if (Meteor.isClient) { ]); testAsyncMulti("livedata - publisher errors", (function () { - // Use a separate connection so that we can safely check to see if - // conn._subscriptions is empty. - var conn = new LivedataTest.Connection('/', - {reloadWithOutstanding: true}); - var collName = Random.id(); - var coll = new Meteor.Collection(collName, {connection: conn}); + var conn, collName, coll; var errorFromRerun; var gotErrorFromStopper = false; return [ function (test, expect) { + // Use a separate connection so that we can safely check to see if + // conn._subscriptions is empty. + conn = new LivedataTest.Connection('/', + {reloadWithOutstanding: true}); + collName = Random.id(); + coll = new Meteor.Collection(collName, {connection: conn}); + var testSubError = function (options) { conn.subscribe("publisherErrors", collName, options, { onReady: expect(), From d3d3e129ed0b68278fabb26e19eaee0afba540c8 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 18 Jul 2014 10:09:50 -0700 Subject: [PATCH 08/69] History pass --- History.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/History.md b/History.md index fc4ae4180f..b9a12f2564 100644 --- a/History.md +++ b/History.md @@ -1,8 +1,70 @@ ## v.NEXT + +## v0.8.3 + +#### XXX blaze-refactor + +* Create `` tags as SVG elements when they have `xlink:href` + attributes. (Previously, `` tags inside SVGs were never created as + SVG elements.) #2178 + +* Throw an error in `{{foo bar}}` if `foo` is missing or not a function. + +* Check that arguments to `UI.insert` have the right types. + +* XXX 3c6c8e5 + + +#### Meteor Accounts + +* Fix OAuth popup flow in mobile apps that don't support `window.opener` + (such as iOS Chrome). #2302 + +* Fix regression in 0.8.2 where an exception would be thrown if + `Meteor.loginWithPassword` didn't have a callback. Callbacks to + `Meteor.loginWithPassword` are now optional again. #2255 + +* Fix "Email already exists" error with MongoDB 2.6. #2238 + + +#### mongo-livedata and minimongo + * Fix performance issue where a large batch of oplog updates could block the node event loop for long periods. #2299. +* Fix oplog bug resulting in error message "Buffer inexplicably empty". #2274 + +* Fix regression from 0.8.2 that caused collections to appear empty in + reactive `findOne()` or `fetch` queries that run before a mutator + returns. #2275 + + +#### Miscellaneous + +* Stop including code by default that automatically refreshes the page + if JavaScript and CSS don't load correctly. While this code is useful + in some multi-server deployments, it can cause infinite refresh loops + if there are errors on the page. Add the `reload-safetybelt` package + to your app if you want to include this code. + +* Add `WebAppInternals.addStaticJs()` for adding static JavaScript code + to be served in the app, inline if allowed by `browser-policy`. + +* On the server, `Meteor.startup(c)` now calls `c` immediately if the + server has already started up, matching the client behavior. #2239 + +* Fix `Meteor._inherits` to copy static properties of the parent + function to the child function. + +* Make the `tinytest/run` method return immediately, so that `wait` + method calls from client tests don't block on server tests completing. + +* Add support for source maps for server-side code. + +* Log errors from method invocations on the client if there is no + callback provided. + * Upgraded dependencies: - less: 1.7.1 (from 1.6.1) From c287a6b3ab0907fb690be63bb1ab0002a2157870 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 18 Jul 2014 10:22:49 -0700 Subject: [PATCH 09/69] Add contributors list --- .mailmap | 2 ++ History.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.mailmap b/.mailmap index efd15a1605..705934494b 100644 --- a/.mailmap +++ b/.mailmap @@ -27,6 +27,7 @@ GITHUB: codeinthehole GITHUB: dandv GITHUB: davegonzalez GITHUB: ducdigital +GITHUB: duckspeaker GITHUB: emgee3 GITHUB: felixrabe GITHUB: FredericoC @@ -75,3 +76,4 @@ METEOR: sixolet METEOR: Slava METEOR: stubailo METEOR: ekatek +METEOR: mariapacana diff --git a/History.md b/History.md index b9a12f2564..0861c189ec 100644 --- a/History.md +++ b/History.md @@ -68,6 +68,8 @@ * Upgraded dependencies: - less: 1.7.1 (from 1.6.1) +Patches contributed by GitHub users Cangit, cmather, duckspeaker, zol. + ## v0.8.2 From 2c76a9e8868e03930a2abfebf073b70a644beb42 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 18 Jul 2014 16:25:33 -0700 Subject: [PATCH 10/69] Add a sentence about blaze-refactor to history --- History.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 0861c189ec..00cbe9facb 100644 --- a/History.md +++ b/History.md @@ -3,7 +3,10 @@ ## v0.8.3 -#### XXX blaze-refactor +#### Blaze + +* Refactor Blaze to simplify internals while preserving the public + API. `UI.Component` has been replaced with `Blaze.View.` * Create `` tags as SVG elements when they have `xlink:href` attributes. (Previously, `` tags inside SVGs were never created as From 7ba1d231938e4e981ef5030c08d1cfdbd1a14cb6 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 18 Jul 2014 16:46:16 -0700 Subject: [PATCH 11/69] more History.md --- History.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/History.md b/History.md index 00cbe9facb..170d4843b1 100644 --- a/History.md +++ b/History.md @@ -8,14 +8,16 @@ * Refactor Blaze to simplify internals while preserving the public API. `UI.Component` has been replaced with `Blaze.View.` +* Fix performance issues and memory leaks concerning event handlers. + +* Add `UI.remove` to complement `UI.render`. + * Create `` tags as SVG elements when they have `xlink:href` attributes. (Previously, `` tags inside SVGs were never created as SVG elements.) #2178 * Throw an error in `{{foo bar}}` if `foo` is missing or not a function. -* Check that arguments to `UI.insert` have the right types. - * XXX 3c6c8e5 @@ -57,9 +59,6 @@ * On the server, `Meteor.startup(c)` now calls `c` immediately if the server has already started up, matching the client behavior. #2239 -* Fix `Meteor._inherits` to copy static properties of the parent - function to the child function. - * Make the `tinytest/run` method return immediately, so that `wait` method calls from client tests don't block on server tests completing. From 105c7686280deea8264852fc9bec07d1b9b15e96 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Fri, 18 Jul 2014 17:10:29 -0700 Subject: [PATCH 12/69] Add a note about Blaze supporting different cursors for #each --- History.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/History.md b/History.md index 170d4843b1..45f1101052 100644 --- a/History.md +++ b/History.md @@ -18,6 +18,10 @@ * Throw an error in `{{foo bar}}` if `foo` is missing or not a function. +* Cursors returned from template helpers for #each should implement + `observeChanges` method and don't have to be Minimongo cursors + (allows new custom data stores for Blaze like Miniredis) + * XXX 3c6c8e5 From c52ca68ea2e73af261034a36ee91322132a542c1 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Sat, 19 Jul 2014 17:04:29 -0700 Subject: [PATCH 13/69] Add note about #2315 and c05ae2 to History --- History.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 45f1101052..4cb96ecb86 100644 --- a/History.md +++ b/History.md @@ -9,9 +9,9 @@ API. `UI.Component` has been replaced with `Blaze.View.` * Fix performance issues and memory leaks concerning event handlers. - + * Add `UI.remove` to complement `UI.render`. - + * Create `` tags as SVG elements when they have `xlink:href` attributes. (Previously, `` tags inside SVGs were never created as SVG elements.) #2178 @@ -48,6 +48,10 @@ reactive `findOne()` or `fetch` queries that run before a mutator returns. #2275 +* Throw an exception when `observeChanges` is called from within an + observe callback on the same collection. This is a temporary measure + to address #2315. + #### Miscellaneous From d3eae5a2e8166c0108e536595c20aa7d70ad35bc Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Sun, 20 Jul 2014 21:16:33 -0700 Subject: [PATCH 14/69] Make a trivial change to oauth to force new package version. Linux build of rc0's oauth package seems to have disappeared from s3, so forcing a new oauth version to be published. --- packages/oauth/oauth_server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/oauth/oauth_server.js b/packages/oauth/oauth_server.js index b4f0c3e5c1..d49444e08a 100644 --- a/packages/oauth/oauth_server.js +++ b/packages/oauth/oauth_server.js @@ -11,7 +11,6 @@ var registeredServices = {}; // Internal: Maps from service version to handler function. The // 'oauth1' and 'oauth2' packages manipulate this directly to register // for callbacks. -// OAuth._requestHandlers = {}; From 78d08b5537235f63cb40e538952568dd3e892ab6 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 21 Jul 2014 10:50:32 -0700 Subject: [PATCH 15/69] Revert part of "Test that reverting df2820 fixed #2275." This reverts commit c05ae240af3870de0b743289d174ebcba3ed82ed EXCEPT for the "fetch in observe" test, which I'm leaving in because we still want a test for #2275. This commit made it an error to start an observeChanges from within an observeChanges callback for the same collection, but it turns out that this is actually a somewhat common thing to do (for example, nested 'each'). Instead, we'll leave things the way they were pre-0.8.2: you can start an observeChanges from within an observeChanges callback, but it'll be subtly buggy in that you won't get synchronous 'added' callbacks. This issue is described in #2315, along with the fact that insert/update/remove/resumeObservers won't run their affected observe callbacks if they are called from within a task on the collection's queue. Eventually we'll implement the full fix (which relaxes the requirement that insert/update/remove run all their callbacks before returning) described in #2315. --- packages/minimongo/minimongo.js | 42 ++++++++++++--------------- packages/minimongo/minimongo_tests.js | 25 ---------------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index d9c13dff08..25fc10f8b6 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -339,6 +339,21 @@ _.extend(LocalCollection.Cursor.prototype, { query.movedBefore = wrapCallback(options.movedBefore); } + if (!options._suppress_initial && !self.collection.paused) { + // XXX unify ordered and unordered interface + var each = ordered + ? _.bind(_.each, null, query.results) + : _.bind(query.results.forEach, query.results); + each(function (doc) { + var fields = EJSON.clone(doc); + + delete fields._id; + if (ordered) + query.addedBefore(doc._id, fields, null); + query.added(doc._id, fields); + }); + } + var handle = new LocalCollection.ObserveHandle; _.extend(handle, { collection: self.collection, @@ -358,30 +373,9 @@ _.extend(LocalCollection.Cursor.prototype, { handle.stop(); }); } - - if (!options._suppress_initial && !self.collection.paused) { - // XXX unify ordered and unordered interface - var each = ordered - ? _.bind(_.each, null, query.results) - : _.bind(query.results.forEach, query.results); - each(function (doc) { - var fields = EJSON.clone(doc); - - delete fields._id; - if (ordered) - query.addedBefore(doc._id, fields, null); - query.added(doc._id, fields); - }); - - // run the observe callbacks resulting from the initial contents - // before we leave the observe. - if (self.collection._observeQueue.safeToRunTask()) { - self.collection._observeQueue.drain(); - } else if (options.added || options.addedBefore) { - // See #2315. - throw Error("observeChanges called from an observe callback on the same collection cannot differentiate between initial and later adds"); - } - } + // run the observe callbacks resulting from the initial contents + // before we leave the observe. + self.collection._observeQueue.drain(); return handle; } diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 90b43cb094..c7eaba4acb 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -3109,28 +3109,3 @@ Tinytest.add("minimongo - fetch in observe", function (test) { observe.stop(); computation.stop(); }); - -Tinytest.add("minimongo - observe in observe", function (test) { - var coll = new LocalCollection; - coll.insert({foo: 2}); - - var observe1AddedCalled = false; - var observe1 = coll.find({foo: 1}).observeChanges({ - added: function (id, fields) { - observe1AddedCalled = true; - test.equal(fields, {foo: 1}); - - // It would be even better if this didn't throw; see #2315. - test.throws(function () { - coll.find({foo: 2}).observeChanges({ - added: function () { - } - }); - }); - } - }); - test.isFalse(observe1AddedCalled); - coll.insert({foo: 1}); - test.isTrue(observe1AddedCalled); - observe1.stop(); -}); From 9c37a006539c29d757794b3305c20aedd03103a2 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 21 Jul 2014 11:07:13 -0700 Subject: [PATCH 16/69] Remove history note for c05ae240a, which has been reverted --- History.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/History.md b/History.md index 4cb96ecb86..cb80c76339 100644 --- a/History.md +++ b/History.md @@ -48,10 +48,6 @@ reactive `findOne()` or `fetch` queries that run before a mutator returns. #2275 -* Throw an exception when `observeChanges` is called from within an - observe callback on the same collection. This is a temporary measure - to address #2315. - #### Miscellaneous From be73e042a1bad00cd5218a1910223b772013148f Mon Sep 17 00:00:00 2001 From: Ryan Yeske Date: Thu, 15 May 2014 12:08:31 -0700 Subject: [PATCH 17/69] recognize forceApprovalPrompt option in Accounts.ui.config this option was originally added via #1226 --- docs/client/api.js | 5 +++++ packages/accounts-ui-unstyled/accounts_ui.js | 20 +++++++++++++++++-- .../accounts-ui-unstyled/accounts_ui_tests.js | 6 +++++- .../login_buttons_single.js | 2 ++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/client/api.js b/docs/client/api.js index a4c9a3a828..d18cdf2f76 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1186,6 +1186,11 @@ Template.api.accounts_ui_config = { type: "Object", descr: "To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details." }, + { + name: "forceApprovalPrompt", + type: "Boolean", + descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google." + }, { name: "passwordSignupFields", type: "String", diff --git a/packages/accounts-ui-unstyled/accounts_ui.js b/packages/accounts-ui-unstyled/accounts_ui.js index 5f241ab997..6a0f0851c2 100644 --- a/packages/accounts-ui-unstyled/accounts_ui.js +++ b/packages/accounts-ui-unstyled/accounts_ui.js @@ -2,12 +2,14 @@ Accounts.ui = {}; Accounts.ui._options = { requestPermissions: {}, - requestOfflineToken: {} + requestOfflineToken: {}, + forceApprovalPrompt: {} }; +// XXX refactor duplicated code in this function Accounts.ui.config = function(options) { // validate options keys - var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken']; + var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) throw new Error("Accounts.ui.config: Invalid key: " + key); @@ -56,6 +58,20 @@ Accounts.ui.config = function(options) { } }); } + + // deal with `forceApprovalPrompt` + if (options.forceApprovalPrompt) { + _.each(options.forceApprovalPrompt, function (value, service) { + if (service !== 'google') + throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); + + if (Accounts.ui._options.forceApprovalPrompt[service]) { + throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); + } else { + Accounts.ui._options.forceApprovalPrompt[service] = value; + } + }); + } }; passwordSignupFields = function () { diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js index a8f4fc40f5..5e4bef9ea0 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -5,7 +5,7 @@ // XXX it'd be cool to also test that the right thing happens if options -// *are* validated, but Accouns.ui._options is global state which makes this hard +// *are* validated, but Accounts.ui._options is global state which makes this hard // (impossible?) Tinytest.add('accounts-ui - config validates keys', function (test) { test.throws(function () { @@ -19,4 +19,8 @@ Tinytest.add('accounts-ui - config validates keys', function (test) { test.throws(function () { Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); }); + + test.throws(function () { + Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); + }); }); diff --git a/packages/accounts-ui-unstyled/login_buttons_single.js b/packages/accounts-ui-unstyled/login_buttons_single.js index 79d35a4190..96e1e65624 100644 --- a/packages/accounts-ui-unstyled/login_buttons_single.js +++ b/packages/accounts-ui-unstyled/login_buttons_single.js @@ -29,6 +29,8 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({ options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; if (Accounts.ui._options.requestOfflineToken[serviceName]) options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; + if (Accounts.ui._options.forceApprovalPrompt[serviceName]) + options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; loginWithService(options, callback); } From 265e4e6814daa4e4747a26842edf550024139d94 Mon Sep 17 00:00:00 2001 From: Mitar Date: Thu, 12 Jun 2014 00:21:51 -0700 Subject: [PATCH 18/69] Assure that transform is not changing cached object. --- packages/minimongo/observe.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js index 38bcb4514c..cc97ca73d0 100644 --- a/packages/minimongo/observe.js +++ b/packages/minimongo/observe.js @@ -157,6 +157,7 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) var self = this; if (observeCallbacks.changed) { var oldDoc = self.docs.get(id); + oldDoc = EJSON.clone(oldDoc); var doc = EJSON.clone(oldDoc); LocalCollection._applyChanges(doc, fields); observeCallbacks.changed(transform(doc), transform(oldDoc)); From 6b26cb1e8ea2220a67a33bbe9761a5bd355e4a68 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 21 Jul 2014 18:41:27 -0700 Subject: [PATCH 19/69] Make it more clear that the clone is for transform --- packages/minimongo/observe.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js index cc97ca73d0..95b92d3d51 100644 --- a/packages/minimongo/observe.js +++ b/packages/minimongo/observe.js @@ -157,10 +157,10 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) var self = this; if (observeCallbacks.changed) { var oldDoc = self.docs.get(id); - oldDoc = EJSON.clone(oldDoc); var doc = EJSON.clone(oldDoc); LocalCollection._applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), transform(oldDoc)); + observeCallbacks.changed(transform(doc), + transform(EJSON.clone(oldDoc))); } }, removed: function (id) { From 3c0f3bac1472cc2048f3bf13fd97b2a1ff432f28 Mon Sep 17 00:00:00 2001 From: Tom Wang Date: Tue, 22 Jul 2014 18:37:01 +0800 Subject: [PATCH 20/69] fix typo --- docs/client/concepts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 2884d102fc..4466066fa4 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -33,7 +33,7 @@ packages that most any app will use (for example `webapp`, which handles incoming HTTP connections, and `templating`, which lets you make HTML templates that automatically update live as data changes). Then there are optional packages like `email`, which lets your app -send emails, or the Meteor Accounts series (`account-password`, +send emails, or the Meteor Accounts series (`accounts-password`, `accounts-facebook`, `accounts-ui`, and others) which provide a full-featured user account system that you can drop right into your app. And beyond these "official" packages, there are hundreds of From f0fd348afeb771deb5d8801282884bab47499941 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Thu, 24 Jul 2014 18:30:43 -0700 Subject: [PATCH 21/69] =?UTF-8?q?Don=E2=80=99t=20use=20empty=20text=20node?= =?UTF-8?q?=20placeholders=20in=20IE=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s a long story. See comment. --- packages/blaze/domrange.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/blaze/domrange.js b/packages/blaze/domrange.js index 055e71ec52..ba71fc20d3 100644 --- a/packages/blaze/domrange.js +++ b/packages/blaze/domrange.js @@ -29,6 +29,28 @@ Blaze.DOMRange = function (nodeAndRangeArray) { }; var DOMRange = Blaze.DOMRange; +// In IE 8, don't use empty text nodes as placeholders +// in empty DOMRanges, use comment nodes instead. Using +// empty text nodes in modern browsers is great because +// it doesn't clutter the web inspector. In IE 8, however, +// it seems to lead in some roundabout way to the OAuth +// pop-up crashing the browser completely. In the past, +// we didn't use empty text nodes on IE 8 because they +// don't accept JS properties, so just use the same logic +// even though we don't need to set properties on the +// placeholder anymore. +DOMRange._USE_COMMENT_PLACEHOLDERS = (function () { + var result = false; + var textNode = document.createTextNode(""); + try { + textNode.someProp = true; + } catch (e) { + // IE 8 + result = true; + } + return result; +})(); + // static methods DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) { var m = rangeOrNode; @@ -118,7 +140,10 @@ DOMRange.prototype.attach = function (parentElement, nextNode, _isMove) { DOMRange._insert(members[i], parentElement, nextNode, _isMove); } } else { - var placeholder = document.createTextNode(""); + var placeholder = ( + DOMRange._USE_COMMENT_PLACEHOLDERS ? + document.createComment("") : + document.createTextNode("")); this.emptyRangePlaceholder = placeholder; parentElement.insertBefore(placeholder, nextNode || null); } From fa28ac2cbc57a77b0fdd65a5e1f7db207974f406 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Thu, 24 Jul 2014 18:34:34 -0700 Subject: [PATCH 22/69] =?UTF-8?q?Add=20template=20instance=20=E2=80=9Cthis?= =?UTF-8?q?.autorun=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Needs docs --- packages/templating/templating.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/templating/templating.js b/packages/templating/templating.js index b871b9c180..e74c1506ac 100644 --- a/packages/templating/templating.js +++ b/packages/templating/templating.js @@ -37,6 +37,9 @@ Template.__updateTemplateInstance = function (view) { data: null, firstNode: null, lastNode: null, + autorun: function (f) { + return view.autorun(f); + }, __view__: view }; } From 631e9aab73554eb57bc8f84c91d50f05ae41bd1c Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Thu, 24 Jul 2014 22:21:46 -0700 Subject: [PATCH 23/69] Update canonicalizeHtml for 0fd348a. Makes tests pass in IE8. Maybe 0fd348a should have been used as the placeholder? --- packages/test-helpers/canonicalize_html.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 28dd2520f0..1d159b85ae 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -2,6 +2,7 @@ canonicalizeHtml = function(html) { var h = html; // kill IE-specific comments inserted by DomRange h = h.replace(//g, ''); + h = h.replace(//g, ''); // ignore exact text of comments h = h.replace(//g, ''); // make all tags lowercase From abbf3c78fac5b188789eb07b55540f57e3a3ab4c Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 08:20:33 -0700 Subject: [PATCH 24/69] shrinkwrap update --- packages/mongo-livedata/.npm/package/npm-shrinkwrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json index ea17322d4e..622dfc20e9 100644 --- a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json @@ -4,7 +4,7 @@ "version": "1.4.1", "dependencies": { "bson": { - "version": "0.2.7", + "version": "https://github.com/meteor/js-bson/tarball/574c0ee", "dependencies": { "nan": { "version": "0.8.0" From adeb649bf684d5166b4444883d8b08785c0ffd49 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 25 Jul 2014 13:57:47 -0700 Subject: [PATCH 25/69] Fix Blaze.currentView in event handlers with test --- packages/blaze/view.js | 6 ++++- packages/spacebars-tests/template_tests.html | 4 ++++ packages/spacebars-tests/template_tests.js | 23 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/blaze/view.js b/packages/blaze/view.js index 9b4e703e91..c3412d34e3 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -529,7 +529,11 @@ Blaze._addEventMap = function (view, eventMap, thisInHandler) { function (evt) { if (! range.containsElement(evt.currentTarget)) return null; - return handler.apply(thisInHandler || this, arguments); + var handlerThis = thisInHandler || this; + var handlerArgs = arguments; + return Blaze.withCurrentView(view, function () { + return handler.apply(handlerThis, handlerArgs); + }); }, range, function (r) { return r.parentRange; diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index bfd11eef8f..d7d715c09d 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -922,3 +922,7 @@ Hi there! + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index dffe1f750d..655f476f44 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2604,3 +2604,26 @@ _.each([1, 2, 3], function (n) { } ); }); + +Tinytest.add('spacebars-tests - template_tests - current view in event handler', function (test) { + var tmpl = Template.spacebars_test_current_view_in_event; + + var currentView; + var currentData; + + tmpl.events({ + 'click span': function () { + currentView = Blaze.getCurrentView(); + currentData = Blaze.getCurrentData(); + } + }); + + var div = renderToDiv(tmpl, 'blah'); + test.equal(canonicalizeHtml(div.innerHTML), 'blah'); + document.body.appendChild(div); + clickElement(div.querySelector('span')); + $(div).remove(); + + test.isTrue(currentView); + test.equal(currentData, 'blah'); +}); From c715613e483302421982112f3404b94036d81685 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 14:04:11 -0700 Subject: [PATCH 26/69] Add sketchy fallback for flaky `window.close()` in OAuth popup. Using an onerror event handler looks like the only semi-reliable way to be able to close the popup in iOS Chrome, even though it's almost certainly a bug that this works. We'll replace it soon with redirect-based OAuth. --- packages/oauth/end_of_login_response.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/oauth/end_of_login_response.html b/packages/oauth/end_of_login_response.html index 81f24b22f0..53ac2fa80d 100644 --- a/packages/oauth/end_of_login_response.html +++ b/packages/oauth/end_of_login_response.html @@ -18,5 +18,12 @@ window.close(); - + +

+ Login completed. + Click here to close this window. +

+ + From 0cc2624c5936bedbb5d6dc055cdd87956ae58512 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 25 Jul 2014 14:20:33 -0700 Subject: [PATCH 27/69] Fix #2339 (dynamic attributes on textareas) --- packages/blaze/materializer.js | 20 ++++-- packages/spacebars-tests/template_tests.html | 12 ++++ packages/spacebars-tests/template_tests.js | 69 ++++++++++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/packages/blaze/materializer.js b/packages/blaze/materializer.js index 84b95003dd..fc71f11f66 100644 --- a/packages/blaze/materializer.js +++ b/packages/blaze/materializer.js @@ -49,13 +49,19 @@ Blaze.DOMMaterializer.def({ var rawAttrs = tag.attrs; var children = tag.children; - if (tagName === 'textarea' && ! (rawAttrs && ('value' in rawAttrs))) { - // turn TEXTAREA contents into a value attribute. - // Reactivity in the form of nested Views won't work here - // because the Views have already been instantiated. To - // get Views in a textarea they need to be wrapped in a - // function and provided as the "value" attribute by the - // compiler. + if (tagName === 'textarea' && tag.children.length && + ! (rawAttrs && ('value' in rawAttrs))) { + // Provide very limited support for TEXTAREA tags with children + // rather than a "value" attribute. + // Reactivity in the form of Views nested in the tag's children + // won't work. Compilers should compile textarea contents into + // the "value" attribute of the tag, wrapped in a function if there + // is reactivity. + if (typeof rawAttrs === 'function' || + HTML.isArray(rawAttrs)) { + throw new Error("Can't have reactive children of TEXTAREA node; " + + "use the 'value' attribute instead."); + } rawAttrs = _.extend({}, rawAttrs || null); rawAttrs.value = Blaze._expand(children, self.parentView); children = []; diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index d7d715c09d..4cd7ba58bf 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -926,3 +926,15 @@ Hi there! + + + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 655f476f44..c24c325152 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2627,3 +2627,72 @@ Tinytest.add('spacebars-tests - template_tests - current view in event handler', test.isTrue(currentView); test.equal(currentData, 'blah'); }); + + +Tinytest.add( + "spacebars-tests - template_tests - textarea attrs", function (test) { + var tmplNoContents = { + tmpl: Template.spacebars_test_textarea_attrs, + hasTextAreaContents: false + }; + var tmplWithContents = { + tmpl: Template.spacebars_test_textarea_attrs_contents, + hasTextAreaContents: true + }; + var tmplWithContentsAndMoreAttrs = { + tmpl: Template.spacebars_test_textarea_attrs_array_contents, + hasTextAreaContents: true + }; + + _.each( + [tmplNoContents, tmplWithContents, + tmplWithContentsAndMoreAttrs], + function (tmplInfo) { + + var id = new ReactiveVar("textarea-" + Random.id()); + var name = new ReactiveVar("one"); + var attrs = new ReactiveVar({ + id: "textarea-" + Random.id() + }); + + var div = renderToDiv(tmplInfo.tmpl, { + attrs: function () { + return attrs.get(); + }, + name: function () { + return name.get(); + } + }); + + // Check that the id and value attribute are as we expect. + // We can't check div.innerHTML because Chrome at least doesn't + // appear to put textarea value attributes in innerHTML. + var textarea = div.querySelector("textarea"); + test.equal(textarea.id, attrs.get().id); + test.equal( + textarea.value, tmplInfo.hasTextAreaContents ? "Hello one" : ""); + // One of the templates has a separate attribute in addition to + // an attributes dictionary. + if (tmplInfo === tmplWithContentsAndMoreAttrs) { + test.equal($(textarea).attr("class"), "bar"); + } + + // Change the id, check that the attribute updates reactively. + attrs.set({ id: "textarea-" + Random.id() }); + Deps.flush(); + test.equal(textarea.id, attrs.get().id); + + // Change the name variable, check that the textarea value + // updates reactively. + name.set("two"); + Deps.flush(); + test.equal( + textarea.value, tmplInfo.hasTextAreaContents ? "Hello two" : ""); + + if (tmplInfo === tmplWithContentsAndMoreAttrs) { + test.equal($(textarea).attr("class"), "bar"); + } + + }); + + }); From 416b0170d7ca27814d7f02d3851c4ee40922465a Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 25 Jul 2014 15:01:00 -0700 Subject: [PATCH 28/69] Simplify UI.dynamic --- packages/spacebars/dynamic.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/spacebars/dynamic.html b/packages/spacebars/dynamic.html index 3e93b50538..054e208f6a 100644 --- a/packages/spacebars/dynamic.html +++ b/packages/spacebars/dynamic.html @@ -17,10 +17,6 @@ render the template. --> the template to render) and a `data` property, which can be falsey. --> From 1ac501784a4435079df8723260a152871f65e2c2 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 15:04:42 -0700 Subject: [PATCH 29/69] Add test for `this.autorun` --- packages/spacebars-tests/template_tests.html | 10 ++++ packages/spacebars-tests/template_tests.js | 50 ++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 4cd7ba58bf..5268fa64cc 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -938,3 +938,13 @@ Hi there! + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index c24c325152..6bcc2aaf86 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2696,3 +2696,53 @@ Tinytest.add( }); }); + +Tinytest.add( + "spacebars-tests - template_tests - this.autorun", + function (test) { + var tmpl = Template.spacebars_test_autorun; + var tmplInner = Template.spacebars_test_autorun_inner; + + // Keep track of the value of `UI._templateInstance()` inside the + // autorun each time it runs. + var autorunTemplateInstances = []; + var actualTemplateInstance; + + var show = new ReactiveVar(true); + var rv = new ReactiveVar("foo"); + + tmplInner.created = function () { + actualTemplateInstance = this; + this.autorun(function () { + rv.get(); + autorunTemplateInstances.push(UI._templateInstance()); + }); + }; + + tmpl.helpers({ + show: function () { + return show.get(); + } + }); + + var div = renderToDiv(tmpl); + test.equal(autorunTemplateInstances.length, 1); + test.equal(autorunTemplateInstances[0], actualTemplateInstance); + + // Make sure the autorun re-runs when `rv` changes, and that it has + // the correct current view. + rv.set("bar"); + Deps.flush(); + test.equal(autorunTemplateInstances.length, 2); + test.equal(autorunTemplateInstances[1], actualTemplateInstance); + + // If the inner template is destroyed, the autorun should be stopped. + show.set(false); + Deps.flush(); + rv.set("baz"); + Deps.flush(); + + test.equal(autorunTemplateInstances.length, 2); + test.equal(rv.numListeners(), 0); + } +); From 6ec8f05e4c7abc45ee37417a3d74ca45cc9dc0fc Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 25 Jul 2014 16:06:45 -0700 Subject: [PATCH 30/69] Docs changes for 0.8.3 --- docs/client/api.html | 23 +++++++++++++++++++++++ docs/client/api.js | 27 +++++++++++++++++++++++++++ docs/client/docs.js | 6 ++++-- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 5e8a7b30f6..876e0b877f 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2189,6 +2189,12 @@ This property provides access to the data context at the top level of the template. It is updated each time the template is re-rendered. Access is read-only and non-reactive. +{{> api_box template_autorun}} + +You can use `this.autorun` from a [`created`](#template_created) or +[`rendered`](#template_rendered) callback to reactively update the DOM +or the template instance. The Computation is automatically stopped +when the template is destroyed.

Template utilities

@@ -2205,6 +2211,15 @@ any part of the DOM for finer control than just using template inclusions. You can define helpers and event maps on `UI.body` just like on any `Template.myTemplate` object. +Helpers on `UI.body` are only available in the `` tags of your +app. To register a global helper, use +[UI.registerHelper](#ui_registerhelper). + +Event maps on `UI.body` don't apply to elements added to the body via +`UI.insert`, jQuery, or the DOM API, or to the body element itself. +To handle events on the body, window, or document, use jQuery or the +DOM API. + {{> api_box ui_render}} This returns an "rendered template" object, which can be passed to @@ -2238,7 +2253,15 @@ changes. {{> api_box ui_getelementdata}} +{{> api_box ui_dynamic}} +`UI.dynamic` allows you to include a template by name, where the name +may be calculated by a helper and may change reactively. The `data` +argument is optional, and if it is omitted, the current data context +is used. + +For example, if there is a template named "foo", `{{dstache}}> UI.dynamic +template="foo"}}` is equivalent to `{{dstache}}> foo}}`. {{#api_box eventmaps}} diff --git a/docs/client/api.js b/docs/client/api.js index a4c9a3a828..3d469a7c88 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1845,6 +1845,17 @@ Template.api.template_data = { descr: ["The data context of this instance's latest invocation."] }; +Template.api.template_autorun = { + id: "template_autorun", + name: "this.autorun(runFunc)", + locus: "Client", + descr: ["A version of [Deps.autorun](#deps_autorun) that is stopped when the template is destroyed."], + args: [ + {name: "runFunc", + type: "Function", + descr: "The function to run. It receives one argument: a Deps.Computation object."} + ] +}; Template.api.ui_registerhelper = { id: "ui_registerhelper", @@ -1862,6 +1873,22 @@ Template.api.ui_registerhelper = { }] }; +Template.api.ui_dynamic = { + id: "ui_dynamic", + name: "{{> UI.dynamic template=templateName [data=dataContext]}}", + locus: "Client", + descr: ["Choose a template to include dynamically, by name."], + args: [ + {name: "templateName", + type: "String", + descr: "The name of the template to include." + }, + {name: "dataContext", + type: "Object", + descr: "Optional. The data context in which to include the template." + }] +}; + Template.api.ui_body = { id: "ui_body", name: "UI.body", diff --git a/docs/client/docs.js b/docs/client/docs.js index bb3fc8eec8..f76df144bf 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -253,7 +253,8 @@ var toc = [ {instance: "this", name: "find", id: "template_find"}, {instance: "this", name: "firstNode", id: "template_firstNode"}, {instance: "this", name: "lastNode", id: "template_lastNode"}, - {instance: "this", name: "data", id: "template_data"} + {instance: "this", name: "data", id: "template_data"}, + {instance: "this", name: "autorun", id: "template_autorun"} ], "UI", [ "UI.registerHelper", @@ -262,7 +263,8 @@ var toc = [ "UI.renderWithData", "UI.insert", "UI.remove", - "UI.getElementData" + "UI.getElementData", + {name: "{{> UI.dynamic}}", id: "ui_dynamic"} ], {type: "spacer"}, {name: "Event maps", style: "noncode"} From df6e70320a658db1591b871bc433ca0de6f60964 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 25 Jul 2014 16:14:25 -0700 Subject: [PATCH 31/69] Update History.md (template autorun) --- History.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index cb80c76339..eac8d9e9f9 100644 --- a/History.md +++ b/History.md @@ -10,7 +10,9 @@ * Fix performance issues and memory leaks concerning event handlers. -* Add `UI.remove` to complement `UI.render`. +* Add `UI.remove`. + +* Add `this.autorun` to the template instance. * Create `` tags as SVG elements when they have `xlink:href` attributes. (Previously, `` tags inside SVGs were never created as From 0f1e310a10f63c7422c13c42260cc3c35370dfad Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 15:38:07 -0700 Subject: [PATCH 32/69] Test return value and argument of `this.autorun` --- packages/spacebars-tests/package.js | 1 + packages/spacebars-tests/template_tests.js | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/spacebars-tests/package.js b/packages/spacebars-tests/package.js index a0d3be4e4b..442d356067 100644 --- a/packages/spacebars-tests/package.js +++ b/packages/spacebars-tests/package.js @@ -13,6 +13,7 @@ Package.on_test(function (api) { api.use('test-helpers'); api.use('showdown'); api.use('minimongo'); + api.use('deps'); api.use('templating', 'client'); api.add_files([ diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 6bcc2aaf86..5c549601c2 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2707,13 +2707,16 @@ Tinytest.add( // autorun each time it runs. var autorunTemplateInstances = []; var actualTemplateInstance; + var returnedComputation; + var computationArg; var show = new ReactiveVar(true); var rv = new ReactiveVar("foo"); tmplInner.created = function () { actualTemplateInstance = this; - this.autorun(function () { + returnedComputation = this.autorun(function (c) { + computationArg = c; rv.get(); autorunTemplateInstances.push(UI._templateInstance()); }); @@ -2729,6 +2732,11 @@ Tinytest.add( test.equal(autorunTemplateInstances.length, 1); test.equal(autorunTemplateInstances[0], actualTemplateInstance); + // Test that the autorun returned a computation and received a + // computation as an argument. + test.isTrue(returnedComputation instanceof Deps.Computation); + test.equal(returnedComputation, computationArg); + // Make sure the autorun re-runs when `rv` changes, and that it has // the correct current view. rv.set("bar"); From 189116a65b9d15f11e9073ba414a32055b5ebdb9 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 16:14:34 -0700 Subject: [PATCH 33/69] History tweaks --- History.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index eac8d9e9f9..d552d4f6dd 100644 --- a/History.md +++ b/History.md @@ -24,8 +24,8 @@ `observeChanges` method and don't have to be Minimongo cursors (allows new custom data stores for Blaze like Miniredis) -* XXX 3c6c8e5 - +* Remove warnings when {{#each}} iterates over a list of strings, + numbers, or other items that contains duplicates. #1980 #### Meteor Accounts @@ -68,7 +68,8 @@ * Make the `tinytest/run` method return immediately, so that `wait` method calls from client tests don't block on server tests completing. -* Add support for source maps for server-side code. +* Add support for server-side source maps when debugging with + `node-inspector`. * Log errors from method invocations on the client if there is no callback provided. From 1b866b729d969025b71909d39579fd3d7b4a48ec Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Fri, 25 Jul 2014 17:24:32 -0700 Subject: [PATCH 34/69] Try window.opener first in OAuth popup, then localStorage. We've occasionally seen weird configurations of IE where localStorage isn't shared between same-origin windows, so trying window.opener first is safer. --- packages/oauth/end_of_login_response.html | 20 +++++++++++--------- packages/oauth/oauth_client.js | 15 +++++++-------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/oauth/end_of_login_response.html b/packages/oauth/end_of_login_response.html index 53ac2fa80d..f4480867d3 100644 --- a/packages/oauth/end_of_login_response.html +++ b/packages/oauth/end_of_login_response.html @@ -4,15 +4,17 @@ if (##SET_CREDENTIAL_TOKEN##) { var credentialToken = ##TOKEN##; var credentialSecret = ##SECRET##; - try { - localStorage[##LOCAL_STORAGE_PREFIX## + credentialToken] = credentialSecret; - } catch (err) { - // localStorage didn't work; try window.opener. - window.opener && - window.opener.Package.oauth.OAuth._handleCredentialSecret( - credentialToken, credentialSecret); - // If window.opener isn't set, we can't do much else, but at least - // close the popup instead of having it hang around on a blank page. + if (window.opener && window.opener.Package && + window.opener.Package.oauth) { + window.opener.Package.oauth.OAuth._handleCredentialSecret( + credentialToken, credentialSecret); + } else { + try { + localStorage[##LOCAL_STORAGE_PREFIX## + credentialToken] = credentialSecret; + } catch (err) { + // We can't do much else, but at least close the popup instead + // of having it hang around on a blank page. + } } } window.close(); diff --git a/packages/oauth/oauth_client.js b/packages/oauth/oauth_client.js index 8ba5e48aa0..dd05c2e85d 100644 --- a/packages/oauth/oauth_client.js +++ b/packages/oauth/oauth_client.js @@ -92,17 +92,16 @@ OAuth._handleCredentialSecret = function (credentialToken, secret) { // Used by accounts-oauth, which needs both a credentialToken and the // corresponding to credential secret to call the `login` method over DDP. OAuth._retrieveCredentialSecret = function (credentialToken) { - // Check localStorage first, then check the secrets collected by - // OAuth._handleCredentialSecret. This matches what we do in + // First check the secrets collected by OAuth._handleCredentialSecret, + // then check localStorage. This matches what we do in // end_of_login_response.html. - var localStorageKey = OAuth._localStorageTokenPrefix + - credentialToken; - var secret = Meteor._localStorage.getItem(localStorageKey); - - if (secret) { + var secret = credentialSecrets[credentialToken]; + if (! secret) { + var localStorageKey = OAuth._localStorageTokenPrefix + + credentialToken; + secret = Meteor._localStorage.getItem(localStorageKey); Meteor._localStorage.removeItem(localStorageKey); } else { - secret = credentialSecrets[credentialToken]; delete credentialSecrets[credentialToken]; } return secret; From 75c30ef073b864779762aa804b65a29cd52f0070 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 28 Jul 2014 12:23:54 -0700 Subject: [PATCH 35/69] Fix scope of UI.contentBlock argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eric Dobbertin’s bug --- packages/blaze/builtins.js | 1 + packages/spacebars-tests/template_tests.html | 12 +++++++++ packages/spacebars-tests/template_tests.js | 10 ++++++++ packages/spacebars/spacebars-runtime.js | 27 +++++++++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index 524171af48..d65b33b2a2 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -169,6 +169,7 @@ Blaze.InOuterTemplateScope = function (templateView, contentFunc) { parentView = parentView.parentView; view.onCreated(function () { + this.originalParentView = this.parentView; this.parentView = parentView; }); return view; diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index 5268fa64cc..4d32086cb1 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -948,3 +948,15 @@ Hi there! + + + + diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 5c549601c2..c748505f84 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2754,3 +2754,13 @@ Tinytest.add( test.equal(rv.numListeners(), 0); } ); + +// Test that argument in {{> UI.contentBlock arg}} is evaluated in +// the proper data context. +Tinytest.add( + "spacebars-tests - template_tests - contentBlock argument", + function (test) { + var tmpl = Template.spacebars_test_contentBlock_arg; + var div = renderToDiv(tmpl); + test.equal(canonicalizeHtml(div.innerHTML), 'AAA BBB'); + }); diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js index 7d02d7f9b9..33ffd83549 100644 --- a/packages/spacebars/spacebars-runtime.js +++ b/packages/spacebars/spacebars-runtime.js @@ -205,7 +205,32 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) { }; Spacebars.TemplateWith = function (argFunc, contentBlock) { - var w = Blaze.With(argFunc, contentBlock); + var w; + + // This is a little messy. When we compile `{{> UI.contentBlock}}`, we + // wrap it in Blaze.InOuterTemplateScope in order to skip the intermediate + // parent Views in the current template. However, when there's an argument + // (`{{> UI.contentBlock arg}}`), the argument needs to be evaluated + // in the original scope. There's no good order to nest + // Blaze.InOuterTemplateScope and Spacebars.TemplateWith to achieve this, + // so we wrap argFunc to run it in the "original parentView" of the + // Blaze.InOuterTemplateScope. + // + // To make this better, reconsider InOuterTemplateScope as a primitive. + // Longer term, evaluate expressions in the proper lexical scope. + var wrappedArgFunc = function () { + var viewToEvaluateArg = null; + if (w.parentView && w.parentView.kind === 'InOuterTemplateScope') { + viewToEvaluateArg = w.parentView.originalParentView; + } + if (viewToEvaluateArg) { + return Blaze.withCurrentView(viewToEvaluateArg, argFunc); + } else { + return argFunc(); + } + }; + + w = Blaze.With(wrappedArgFunc, contentBlock); w.__isTemplateWith = true; return w; }; From 34bfcffa8296be366911c31aad35ad404e4051fb Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 28 Jul 2014 13:28:02 -0700 Subject: [PATCH 36/69] History tweaks --- History.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/History.md b/History.md index d552d4f6dd..ab2a3105e6 100644 --- a/History.md +++ b/History.md @@ -21,21 +21,21 @@ * Throw an error in `{{foo bar}}` if `foo` is missing or not a function. * Cursors returned from template helpers for #each should implement - `observeChanges` method and don't have to be Minimongo cursors - (allows new custom data stores for Blaze like Miniredis) + the `observeChanges` method and don't have to be Minimongo cursors + (allowing new custom data stores for Blaze like Miniredis). * Remove warnings when {{#each}} iterates over a list of strings, numbers, or other items that contains duplicates. #1980 #### Meteor Accounts -* Fix OAuth popup flow in mobile apps that don't support `window.opener` - (such as iOS Chrome). #2302 - * Fix regression in 0.8.2 where an exception would be thrown if `Meteor.loginWithPassword` didn't have a callback. Callbacks to `Meteor.loginWithPassword` are now optional again. #2255 +* Fix OAuth popup flow in mobile apps that don't support + `window.opener`. #2302 + * Fix "Email already exists" error with MongoDB 2.6. #2238 @@ -59,18 +59,18 @@ if there are errors on the page. Add the `reload-safetybelt` package to your app if you want to include this code. -* Add `WebAppInternals.addStaticJs()` for adding static JavaScript code - to be served in the app, inline if allowed by `browser-policy`. - * On the server, `Meteor.startup(c)` now calls `c` immediately if the server has already started up, matching the client behavior. #2239 -* Make the `tinytest/run` method return immediately, so that `wait` - method calls from client tests don't block on server tests completing. - * Add support for server-side source maps when debugging with `node-inspector`. +* Add `WebAppInternals.addStaticJs()` for adding static JavaScript code + to be served in the app, inline if allowed by `browser-policy`. + +* Make the `tinytest/run` method return immediately, so that `wait` + method calls from client tests don't block on server tests completing. + * Log errors from method invocations on the client if there is no callback provided. From 9bb83c4d4418286e700c17793336d490eadd9e41 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 28 Jul 2014 14:20:23 -0700 Subject: [PATCH 37/69] Moar History words --- History.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index ab2a3105e6..0ce44b0e8c 100644 --- a/History.md +++ b/History.md @@ -10,9 +10,10 @@ * Fix performance issues and memory leaks concerning event handlers. -* Add `UI.remove`. +* Add `UI.remove`, which removes a template after `UI.render`/`UI.insert`. -* Add `this.autorun` to the template instance. +* Add `this.autorun` to the template instance, which is like `Deps.autorun` + but is automatically stopped when the template is destroyed. * Create `` tags as SVG elements when they have `xlink:href` attributes. (Previously, `` tags inside SVGs were never created as From 8dbda26c32f83a96dcb912f1d96a9a34bd9f827a Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Mon, 28 Jul 2014 15:13:10 -0700 Subject: [PATCH 38/69] Banner and notices --- scripts/admin/banner.txt | 8 +++----- scripts/admin/notices.json | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index c72d6b8ddb..6573d75782 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -1,7 +1,5 @@ -=> Meteor 0.8.2: Switch `accounts-password` to use bcrypt on the - server. User accounts will seamlessly transition to bcrypt on the - next login, but this transition is one-way, so you cannot downgrade a - production app once you upgrade to 0.8.2. +=> Meteor 0.8.3: Performance improvements and a big refactoring of the + Blaze internals. This release is being downloaded in the background. Update your - project to Meteor 0.8.2 by running 'meteor update'. + project to Meteor 0.8.3 by running 'meteor update'. diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index 3b002c98b5..4e93ea80c9 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -149,6 +149,9 @@ ] } }, + { + "release": "0.8.3" + }, { "release": "NEXT" } From 452ae36c683f17e6029a7e9b7aca539de3a0751b Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 28 Jul 2014 16:27:49 -0700 Subject: [PATCH 39/69] Change how Blaze.render,toHTML infer parentView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you call UI.renderWithData, say, from an event handler, you may be unpleasantly surprised if it gets Blaze.currentView as its parentView (where Blaze.currentView is based on the template where the event handler is defined). On the other hand, showdown/template-integration.js takes advantage of the fact that Blaze.toHTML in render() is infers the parentView from Blaze.currentView. So only infer currentView as parent while in the view’s render(). This change should only affect apps and packages that use Blaze.render, Blaze.toHTML, UI.render, or UI.renderWithData. --- packages/blaze/view.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/blaze/view.js b/packages/blaze/view.js index c3412d34e3..7597f97a63 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -312,14 +312,21 @@ Blaze.HTMLJSExpander.def({ } }); +// Return Blaze.currentView, but only if it is being rendered +// (i.e. we are in its render() method). +var currentViewIfRendering = function () { + var view = Blaze.currentView; + return (view && view.isInRender) ? view : null; +}; + Blaze._expand = function (htmljs, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return (new Blaze.HTMLJSExpander( {parentView: parentView})).visit(htmljs); }; Blaze._expandAttributes = function (attrs, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return (new Blaze.HTMLJSExpander( {parentView: parentView})).visitAttributes(attrs); }; @@ -383,7 +390,7 @@ Blaze.runTemplate = function (t/*, args*/) { }; Blaze.render = function (content, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); var view; if (typeof content === 'function') { @@ -401,7 +408,7 @@ Blaze.render = function (content, parentView) { Blaze.toHTML = function (htmljs, parentView) { if (typeof htmljs === 'function') throw new Error("Blaze.toHTML doesn't take a function, just HTMLjs"); - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return HTML.toHTML(Blaze._expand(htmljs, parentView)); }; @@ -414,7 +421,7 @@ Blaze.toText = function (htmljs, parentView, textMode) { textMode = parentView; parentView = null; } - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); if (! textMode) throw new Error("textMode required"); From cd8bd67ff86f42ba2188c719311c1912a71490ae Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 29 Jul 2014 10:18:40 -0700 Subject: [PATCH 40/69] Fix a few exceptions in the oplog observe driver First exception: _runQuery didn't check to see if it was stopped after running the query, which could lead to this harmless error: Exception in defer callback: TypeError: Cannot call method 'clear' of null at _.extend._publishNewResults (packages/mongo-livedata/oplog_observe_driver.js:749) at _.extend._runQuery (packages/mongo-livedata/oplog_observe_driver.js:657) at packages/mongo-livedata/oplog_observe_driver.js:615 at _.extend.withValue (packages/meteor/dynamics_nodejs.js:56) at packages/meteor/timers.js:6 at runWithEnvironment (packages/meteor/dynamics_nodejs.js:108) Second exception: _fetchModifiedDocuments thought that it should only be in FETCHING in a certain case, but QUERYING is also OK. This is also harmless since the correct behavior is to end the deferred routine. Exception in defer callback: Error: phase in fetchModifiedDocuments: QUERYING at packages/mongo-livedata/oplog_observe_driver.js:435 at packages/mongo-livedata/oplog_observe_driver.js:16 at _.extend.withValue (packages/meteor/dynamics_nodejs.js:56) at packages/meteor/timers.js:6 at runWithEnvironment (packages/meteor/dynamics_nodejs.js:108) --- packages/mongo-livedata/oplog_observe_driver.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/mongo-livedata/oplog_observe_driver.js b/packages/mongo-livedata/oplog_observe_driver.js index ac03d24cb0..d9348a62b1 100644 --- a/packages/mongo-livedata/oplog_observe_driver.js +++ b/packages/mongo-livedata/oplog_observe_driver.js @@ -431,6 +431,14 @@ _.extend(OplogObserveDriver.prototype, { // fetch() yields. Meteor.defer(finishIfNeedToPollQuery(function () { while (!self._stopped && !self._needToFetch.empty()) { + if (self._phase === PHASE.QUERYING) { + // While fetching, we decided to go into QUERYING mode, and then we + // saw another oplog entry, so _needToFetch is not empty. But we + // shouldn't fetch these documents until AFTER the query is done. + break; + } + + // Being in steady phase here would be surprising. if (self._phase !== PHASE.FETCHING) throw new Error("phase in fetchModifiedDocuments: " + self._phase); @@ -654,6 +662,9 @@ _.extend(OplogObserveDriver.prototype, { } } + if (self._stopped) + return; + self._publishNewResults(newResults, newBuffer); }, @@ -789,6 +800,9 @@ _.extend(OplogObserveDriver.prototype, { // This stop function is invoked from the onStop of the ObserveMultiplexer, so // it shouldn't actually be possible to call it until the multiplexer is // ready. + // + // It's important to check self._stopped after every call in this file that + // can yield! stop: function () { var self = this; if (self._stopped) From b4e02f345a40db826ceae9da318f5532e8d6714e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 29 Jul 2014 10:18:40 -0700 Subject: [PATCH 41/69] Fix a few exceptions in the oplog observe driver First exception: _runQuery didn't check to see if it was stopped after running the query, which could lead to this harmless error: Exception in defer callback: TypeError: Cannot call method 'clear' of null at _.extend._publishNewResults (packages/mongo-livedata/oplog_observe_driver.js:749) at _.extend._runQuery (packages/mongo-livedata/oplog_observe_driver.js:657) at packages/mongo-livedata/oplog_observe_driver.js:615 at _.extend.withValue (packages/meteor/dynamics_nodejs.js:56) at packages/meteor/timers.js:6 at runWithEnvironment (packages/meteor/dynamics_nodejs.js:108) Second exception: _fetchModifiedDocuments thought that it should only be in FETCHING in a certain case, but QUERYING is also OK. This is also harmless since the correct behavior is to end the deferred routine. Exception in defer callback: Error: phase in fetchModifiedDocuments: QUERYING at packages/mongo-livedata/oplog_observe_driver.js:435 at packages/mongo-livedata/oplog_observe_driver.js:16 at _.extend.withValue (packages/meteor/dynamics_nodejs.js:56) at packages/meteor/timers.js:6 at runWithEnvironment (packages/meteor/dynamics_nodejs.js:108) --- packages/mongo-livedata/oplog_observe_driver.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/mongo-livedata/oplog_observe_driver.js b/packages/mongo-livedata/oplog_observe_driver.js index ac03d24cb0..d9348a62b1 100644 --- a/packages/mongo-livedata/oplog_observe_driver.js +++ b/packages/mongo-livedata/oplog_observe_driver.js @@ -431,6 +431,14 @@ _.extend(OplogObserveDriver.prototype, { // fetch() yields. Meteor.defer(finishIfNeedToPollQuery(function () { while (!self._stopped && !self._needToFetch.empty()) { + if (self._phase === PHASE.QUERYING) { + // While fetching, we decided to go into QUERYING mode, and then we + // saw another oplog entry, so _needToFetch is not empty. But we + // shouldn't fetch these documents until AFTER the query is done. + break; + } + + // Being in steady phase here would be surprising. if (self._phase !== PHASE.FETCHING) throw new Error("phase in fetchModifiedDocuments: " + self._phase); @@ -654,6 +662,9 @@ _.extend(OplogObserveDriver.prototype, { } } + if (self._stopped) + return; + self._publishNewResults(newResults, newBuffer); }, @@ -789,6 +800,9 @@ _.extend(OplogObserveDriver.prototype, { // This stop function is invoked from the onStop of the ObserveMultiplexer, so // it shouldn't actually be possible to call it until the multiplexer is // ready. + // + // It's important to check self._stopped after every call in this file that + // can yield! stop: function () { var self = this; if (self._stopped) From 1b9bb206cd229fe1639d9c1f94455865000ba5c3 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 29 Jul 2014 10:54:21 -0700 Subject: [PATCH 42/69] Revert "shrinkwrap update" This reverts commit abbf3c78fac5b188789eb07b55540f57e3a3ab4c. Something probably got weird switching between the 'packaging' branch and 'devel'; mongodb and bson are forked on packaging, but we didn't intend to be using a normal mongodb release with a forked bson. --- packages/mongo-livedata/.npm/package/npm-shrinkwrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json index 622dfc20e9..ea17322d4e 100644 --- a/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json +++ b/packages/mongo-livedata/.npm/package/npm-shrinkwrap.json @@ -4,7 +4,7 @@ "version": "1.4.1", "dependencies": { "bson": { - "version": "https://github.com/meteor/js-bson/tarball/574c0ee", + "version": "0.2.7", "dependencies": { "nan": { "version": "0.8.0" From 0f3b16f9425f3ca3ff136a451dd2f32bdfa28416 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 29 Jul 2014 11:01:42 -0700 Subject: [PATCH 43/69] Add a lot of _noYieldsAllowed to oplog driver Makes it easier to reason about where yields are (and ensure that _stopped is checked after all yielding calls) --- .../mongo-livedata/oplog_observe_driver.js | 905 +++++++++--------- 1 file changed, 475 insertions(+), 430 deletions(-) diff --git a/packages/mongo-livedata/oplog_observe_driver.js b/packages/mongo-livedata/oplog_observe_driver.js index d9348a62b1..1ba2e56d49 100644 --- a/packages/mongo-livedata/oplog_observe_driver.js +++ b/packages/mongo-livedata/oplog_observe_driver.js @@ -168,419 +168,453 @@ OplogObserveDriver = function (options) { _.extend(OplogObserveDriver.prototype, { _addPublished: function (id, doc) { var self = this; - var fields = _.clone(doc); - delete fields._id; - self._published.set(id, self._sharedProjectionFn(doc)); - self._multiplexer.added(id, self._projectionFn(fields)); + Meteor._noYieldsAllowed(function () { + var fields = _.clone(doc); + delete fields._id; + self._published.set(id, self._sharedProjectionFn(doc)); + self._multiplexer.added(id, self._projectionFn(fields)); - // After adding this document, the published set might be overflowed - // (exceeding capacity specified by limit). If so, push the maximum element - // to the buffer, we might want to save it in memory to reduce the amount of - // Mongo lookups in the future. - if (self._limit && self._published.size() > self._limit) { - // XXX in theory the size of published is no more than limit+1 - if (self._published.size() !== self._limit + 1) { - throw new Error("After adding to published, " + - (self._published.size() - self._limit) + - " documents are overflowing the set"); + // After adding this document, the published set might be overflowed + // (exceeding capacity specified by limit). If so, push the maximum + // element to the buffer, we might want to save it in memory to reduce the + // amount of Mongo lookups in the future. + if (self._limit && self._published.size() > self._limit) { + // XXX in theory the size of published is no more than limit+1 + if (self._published.size() !== self._limit + 1) { + throw new Error("After adding to published, " + + (self._published.size() - self._limit) + + " documents are overflowing the set"); + } + + var overflowingDocId = self._published.maxElementId(); + var overflowingDoc = self._published.get(overflowingDocId); + + if (EJSON.equals(overflowingDocId, id)) { + throw new Error("The document just added is overflowing the published set"); + } + + self._published.remove(overflowingDocId); + self._multiplexer.removed(overflowingDocId); + self._addBuffered(overflowingDocId, overflowingDoc); } - - var overflowingDocId = self._published.maxElementId(); - var overflowingDoc = self._published.get(overflowingDocId); - - if (EJSON.equals(overflowingDocId, id)) { - throw new Error("The document just added is overflowing the published set"); - } - - self._published.remove(overflowingDocId); - self._multiplexer.removed(overflowingDocId); - self._addBuffered(overflowingDocId, overflowingDoc); - } + }); }, _removePublished: function (id) { var self = this; - self._published.remove(id); - self._multiplexer.removed(id); - if (! self._limit || self._published.size() === self._limit) - return; + Meteor._noYieldsAllowed(function () { + self._published.remove(id); + self._multiplexer.removed(id); + if (! self._limit || self._published.size() === self._limit) + return; - if (self._published.size() > self._limit) - throw Error("self._published got too big"); + if (self._published.size() > self._limit) + throw Error("self._published got too big"); - // OK, we are publishing less than the limit. Maybe we should look in the - // buffer to find the next element past what we were publishing before. + // OK, we are publishing less than the limit. Maybe we should look in the + // buffer to find the next element past what we were publishing before. - if (!self._unpublishedBuffer.empty()) { - // There's something in the buffer; move the first thing in it to - // _published. - var newDocId = self._unpublishedBuffer.minElementId(); - var newDoc = self._unpublishedBuffer.get(newDocId); - self._removeBuffered(newDocId); - self._addPublished(newDocId, newDoc); - return; - } + if (!self._unpublishedBuffer.empty()) { + // There's something in the buffer; move the first thing in it to + // _published. + var newDocId = self._unpublishedBuffer.minElementId(); + var newDoc = self._unpublishedBuffer.get(newDocId); + self._removeBuffered(newDocId); + self._addPublished(newDocId, newDoc); + return; + } - // There's nothing in the buffer. This could mean one of a few things. + // There's nothing in the buffer. This could mean one of a few things. - // (a) We could be in the middle of re-running the query (specifically, we - // could be in _publishNewResults). In that case, _unpublishedBuffer is - // empty because we clear it at the beginning of _publishNewResults. In this - // case, our caller already knows the entire answer to the query and we - // don't need to do anything fancy here. Just return. - if (self._phase === PHASE.QUERYING) - return; + // (a) We could be in the middle of re-running the query (specifically, we + // could be in _publishNewResults). In that case, _unpublishedBuffer is + // empty because we clear it at the beginning of _publishNewResults. In + // this case, our caller already knows the entire answer to the query and + // we don't need to do anything fancy here. Just return. + if (self._phase === PHASE.QUERYING) + return; - // (b) We're pretty confident that the union of _published and - // _unpublishedBuffer contain all documents that match selector. Because - // _unpublishedBuffer is empty, that means we're confident that _published - // contains all documents that match selector. So we have nothing to do. - if (self._safeAppendToBuffer) - return; + // (b) We're pretty confident that the union of _published and + // _unpublishedBuffer contain all documents that match selector. Because + // _unpublishedBuffer is empty, that means we're confident that _published + // contains all documents that match selector. So we have nothing to do. + if (self._safeAppendToBuffer) + return; - // (c) Maybe there are other documents out there that should be in our - // buffer. But in that case, when we emptied _unpublishedBuffer in - // _removeBuffered, we should have called _needToPollQuery, which will - // either put something in _unpublishedBuffer or set _safeAppendToBuffer (or - // both), and it will put us in QUERYING for that whole time. So in fact, we - // shouldn't be able to get here. + // (c) Maybe there are other documents out there that should be in our + // buffer. But in that case, when we emptied _unpublishedBuffer in + // _removeBuffered, we should have called _needToPollQuery, which will + // either put something in _unpublishedBuffer or set _safeAppendToBuffer + // (or both), and it will put us in QUERYING for that whole time. So in + // fact, we shouldn't be able to get here. - throw new Error("Buffer inexplicably empty"); + throw new Error("Buffer inexplicably empty"); + }); }, _changePublished: function (id, oldDoc, newDoc) { var self = this; - self._published.set(id, self._sharedProjectionFn(newDoc)); - var changed = LocalCollection._makeChangedFields(_.clone(newDoc), oldDoc); - changed = self._projectionFn(changed); - if (!_.isEmpty(changed)) - self._multiplexer.changed(id, changed); + Meteor._noYieldsAllowed(function () { + self._published.set(id, self._sharedProjectionFn(newDoc)); + var changed = LocalCollection._makeChangedFields(_.clone(newDoc), oldDoc); + changed = self._projectionFn(changed); + if (!_.isEmpty(changed)) + self._multiplexer.changed(id, changed); + }); }, _addBuffered: function (id, doc) { var self = this; - self._unpublishedBuffer.set(id, self._sharedProjectionFn(doc)); + Meteor._noYieldsAllowed(function () { + self._unpublishedBuffer.set(id, self._sharedProjectionFn(doc)); - // If something is overflowing the buffer, we just remove it from cache - if (self._unpublishedBuffer.size() > self._limit) { - var maxBufferedId = self._unpublishedBuffer.maxElementId(); + // If something is overflowing the buffer, we just remove it from cache + if (self._unpublishedBuffer.size() > self._limit) { + var maxBufferedId = self._unpublishedBuffer.maxElementId(); - self._unpublishedBuffer.remove(maxBufferedId); + self._unpublishedBuffer.remove(maxBufferedId); - // Since something matching is removed from cache (both published set and - // buffer), set flag to false - self._safeAppendToBuffer = false; - } + // Since something matching is removed from cache (both published set and + // buffer), set flag to false + self._safeAppendToBuffer = false; + } + }); }, // Is called either to remove the doc completely from matching set or to move // it to the published set later. _removeBuffered: function (id) { var self = this; - self._unpublishedBuffer.remove(id); - // To keep the contract "buffer is never empty in STEADY phase unless the - // everything matching fits into published" true, we poll everything as soon - // as we see the buffer becoming empty. - if (! self._unpublishedBuffer.size() && ! self._safeAppendToBuffer) - self._needToPollQuery(); + Meteor._noYieldsAllowed(function () { + self._unpublishedBuffer.remove(id); + // To keep the contract "buffer is never empty in STEADY phase unless the + // everything matching fits into published" true, we poll everything as + // soon as we see the buffer becoming empty. + if (! self._unpublishedBuffer.size() && ! self._safeAppendToBuffer) + self._needToPollQuery(); + }); }, // Called when a document has joined the "Matching" results set. // Takes responsibility of keeping _unpublishedBuffer in sync with _published // and the effect of limit enforced. _addMatching: function (doc) { var self = this; - var id = doc._id; - if (self._published.has(id)) - throw Error("tried to add something already published " + id); - if (self._limit && self._unpublishedBuffer.has(id)) - throw Error("tried to add something already existed in buffer " + id); + Meteor._noYieldsAllowed(function () { + var id = doc._id; + if (self._published.has(id)) + throw Error("tried to add something already published " + id); + if (self._limit && self._unpublishedBuffer.has(id)) + throw Error("tried to add something already existed in buffer " + id); - var limit = self._limit; - var comparator = self._comparator; - var maxPublished = (limit && self._published.size() > 0) ? - self._published.get(self._published.maxElementId()) : null; - var maxBuffered = (limit && self._unpublishedBuffer.size() > 0) ? - self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()) : null; - // The query is unlimited or didn't publish enough documents yet or the new - // document would fit into published set pushing the maximum element out, - // then we need to publish the doc. - var toPublish = ! limit || self._published.size() < limit || - comparator(doc, maxPublished) < 0; + var limit = self._limit; + var comparator = self._comparator; + var maxPublished = (limit && self._published.size() > 0) ? + self._published.get(self._published.maxElementId()) : null; + var maxBuffered = (limit && self._unpublishedBuffer.size() > 0) + ? self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()) + : null; + // The query is unlimited or didn't publish enough documents yet or the + // new document would fit into published set pushing the maximum element + // out, then we need to publish the doc. + var toPublish = ! limit || self._published.size() < limit || + comparator(doc, maxPublished) < 0; - // Otherwise we might need to buffer it (only in case of limited query). - // Buffering is allowed if the buffer is not filled up yet and all matching - // docs are either in the published set or in the buffer. - var canAppendToBuffer = !toPublish && self._safeAppendToBuffer && - self._unpublishedBuffer.size() < limit; + // Otherwise we might need to buffer it (only in case of limited query). + // Buffering is allowed if the buffer is not filled up yet and all + // matching docs are either in the published set or in the buffer. + var canAppendToBuffer = !toPublish && self._safeAppendToBuffer && + self._unpublishedBuffer.size() < limit; - // Or if it is small enough to be safely inserted to the middle or the - // beginning of the buffer. - var canInsertIntoBuffer = !toPublish && maxBuffered && - comparator(doc, maxBuffered) <= 0; + // Or if it is small enough to be safely inserted to the middle or the + // beginning of the buffer. + var canInsertIntoBuffer = !toPublish && maxBuffered && + comparator(doc, maxBuffered) <= 0; - var toBuffer = canAppendToBuffer || canInsertIntoBuffer; + var toBuffer = canAppendToBuffer || canInsertIntoBuffer; - if (toPublish) { - self._addPublished(id, doc); - } else if (toBuffer) { - self._addBuffered(id, doc); - } else { - // dropping it and not saving to the cache - self._safeAppendToBuffer = false; - } + if (toPublish) { + self._addPublished(id, doc); + } else if (toBuffer) { + self._addBuffered(id, doc); + } else { + // dropping it and not saving to the cache + self._safeAppendToBuffer = false; + } + }); }, // Called when a document leaves the "Matching" results set. // Takes responsibility of keeping _unpublishedBuffer in sync with _published // and the effect of limit enforced. _removeMatching: function (id) { var self = this; - if (! self._published.has(id) && ! self._limit) - throw Error("tried to remove something matching but not cached " + id); + Meteor._noYieldsAllowed(function () { + if (! self._published.has(id) && ! self._limit) + throw Error("tried to remove something matching but not cached " + id); - if (self._published.has(id)) { - self._removePublished(id); - } else if (self._unpublishedBuffer.has(id)) { - self._removeBuffered(id); - } + if (self._published.has(id)) { + self._removePublished(id); + } else if (self._unpublishedBuffer.has(id)) { + self._removeBuffered(id); + } + }); }, _handleDoc: function (id, newDoc) { var self = this; - var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result; + Meteor._noYieldsAllowed(function () { + var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result; - var publishedBefore = self._published.has(id); - var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); - var cachedBefore = publishedBefore || bufferedBefore; + var publishedBefore = self._published.has(id); + var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); + var cachedBefore = publishedBefore || bufferedBefore; - if (matchesNow && !cachedBefore) { - self._addMatching(newDoc); - } else if (cachedBefore && !matchesNow) { - self._removeMatching(id); - } else if (cachedBefore && matchesNow) { - var oldDoc = self._published.get(id); - var comparator = self._comparator; - var minBuffered = self._limit && self._unpublishedBuffer.size() && - self._unpublishedBuffer.get(self._unpublishedBuffer.minElementId()); + if (matchesNow && !cachedBefore) { + self._addMatching(newDoc); + } else if (cachedBefore && !matchesNow) { + self._removeMatching(id); + } else if (cachedBefore && matchesNow) { + var oldDoc = self._published.get(id); + var comparator = self._comparator; + var minBuffered = self._limit && self._unpublishedBuffer.size() && + self._unpublishedBuffer.get(self._unpublishedBuffer.minElementId()); - if (publishedBefore) { - // Unlimited case where the document stays in published once it matches - // or the case when we don't have enough matching docs to publish or the - // changed but matching doc will stay in published anyways. - // XXX: We rely on the emptiness of buffer. Be sure to maintain the fact - // that buffer can't be empty if there are matching documents not - // published. Notably, we don't want to schedule repoll and continue - // relying on this property. - var staysInPublished = ! self._limit || - self._unpublishedBuffer.size() === 0 || - comparator(newDoc, minBuffered) <= 0; + if (publishedBefore) { + // Unlimited case where the document stays in published once it + // matches or the case when we don't have enough matching docs to + // publish or the changed but matching doc will stay in published + // anyways. + // + // XXX: We rely on the emptiness of buffer. Be sure to maintain the + // fact that buffer can't be empty if there are matching documents not + // published. Notably, we don't want to schedule repoll and continue + // relying on this property. + var staysInPublished = ! self._limit || + self._unpublishedBuffer.size() === 0 || + comparator(newDoc, minBuffered) <= 0; - if (staysInPublished) { - self._changePublished(id, oldDoc, newDoc); - } else { - // after the change doc doesn't stay in the published, remove it - self._removePublished(id); - // but it can move into buffered now, check it - var maxBuffered = self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()); + if (staysInPublished) { + self._changePublished(id, oldDoc, newDoc); + } else { + // after the change doc doesn't stay in the published, remove it + self._removePublished(id); + // but it can move into buffered now, check it + var maxBuffered = self._unpublishedBuffer.get( + self._unpublishedBuffer.maxElementId()); - var toBuffer = self._safeAppendToBuffer || - (maxBuffered && comparator(newDoc, maxBuffered) <= 0); + var toBuffer = self._safeAppendToBuffer || + (maxBuffered && comparator(newDoc, maxBuffered) <= 0); - if (toBuffer) { - self._addBuffered(id, newDoc); + if (toBuffer) { + self._addBuffered(id, newDoc); + } else { + // Throw away from both published set and buffer + self._safeAppendToBuffer = false; + } + } + } else if (bufferedBefore) { + oldDoc = self._unpublishedBuffer.get(id); + // remove the old version manually instead of using _removeBuffered so + // we don't trigger the querying immediately. if we end this block + // with the buffer empty, we will need to trigger the query poll + // manually too. + self._unpublishedBuffer.remove(id); + + var maxPublished = self._published.get( + self._published.maxElementId()); + var maxBuffered = self._unpublishedBuffer.size() && + self._unpublishedBuffer.get( + self._unpublishedBuffer.maxElementId()); + + // the buffered doc was updated, it could move to published + var toPublish = comparator(newDoc, maxPublished) < 0; + + // or stays in buffer even after the change + var staysInBuffer = (! toPublish && self._safeAppendToBuffer) || + (!toPublish && maxBuffered && + comparator(newDoc, maxBuffered) <= 0); + + if (toPublish) { + self._addPublished(id, newDoc); + } else if (staysInBuffer) { + // stays in buffer but changes + self._unpublishedBuffer.set(id, newDoc); } else { // Throw away from both published set and buffer self._safeAppendToBuffer = false; + // Normally this check would have been done in _removeBuffered but + // we didn't use it, so we need to do it ourself now. + if (! self._unpublishedBuffer.size()) { + self._needToPollQuery(); + } } - } - } else if (bufferedBefore) { - oldDoc = self._unpublishedBuffer.get(id); - // remove the old version manually instead of using _removeBuffered so - // we don't trigger the querying immediately. if we end this block with - // the buffer empty, we will need to trigger the query poll manually - // too. - self._unpublishedBuffer.remove(id); - - var maxPublished = self._published.get(self._published.maxElementId()); - var maxBuffered = self._unpublishedBuffer.size() && self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()); - - // the buffered doc was updated, it could move to published - var toPublish = comparator(newDoc, maxPublished) < 0; - - // or stays in buffer even after the change - var staysInBuffer = (! toPublish && self._safeAppendToBuffer) || - (!toPublish && maxBuffered && comparator(newDoc, maxBuffered) <= 0); - - if (toPublish) { - self._addPublished(id, newDoc); - } else if (staysInBuffer) { - // stays in buffer but changes - self._unpublishedBuffer.set(id, newDoc); } else { - // Throw away from both published set and buffer - self._safeAppendToBuffer = false; - // Normally this check would have been done in _removeBuffered but we - // didn't use it, so we need to do it ourself now. - if (! self._unpublishedBuffer.size()) { - self._needToPollQuery(); - } + throw new Error("cachedBefore implies either of publishedBefore or bufferedBefore is true."); } - } else { - throw new Error("cachedBefore implies either of publishedBefore or bufferedBefore is true."); } - } + }); }, _fetchModifiedDocuments: function () { var self = this; - self._registerPhaseChange(PHASE.FETCHING); - // Defer, because nothing called from the oplog entry handler may yield, but - // fetch() yields. - Meteor.defer(finishIfNeedToPollQuery(function () { - while (!self._stopped && !self._needToFetch.empty()) { - if (self._phase === PHASE.QUERYING) { - // While fetching, we decided to go into QUERYING mode, and then we - // saw another oplog entry, so _needToFetch is not empty. But we - // shouldn't fetch these documents until AFTER the query is done. - break; - } + Meteor._noYieldsAllowed(function () { + self._registerPhaseChange(PHASE.FETCHING); + // Defer, because nothing called from the oplog entry handler may yield, + // but fetch() yields. + Meteor.defer(finishIfNeedToPollQuery(function () { + while (!self._stopped && !self._needToFetch.empty()) { + if (self._phase === PHASE.QUERYING) { + // While fetching, we decided to go into QUERYING mode, and then we + // saw another oplog entry, so _needToFetch is not empty. But we + // shouldn't fetch these documents until AFTER the query is done. + break; + } - // Being in steady phase here would be surprising. - if (self._phase !== PHASE.FETCHING) - throw new Error("phase in fetchModifiedDocuments: " + self._phase); + // Being in steady phase here would be surprising. + if (self._phase !== PHASE.FETCHING) + throw new Error("phase in fetchModifiedDocuments: " + self._phase); - self._currentlyFetching = self._needToFetch; - var thisGeneration = ++self._fetchGeneration; - self._needToFetch = new LocalCollection._IdMap; - var waiting = 0; - var fut = new Future; - // This loop is safe, because _currentlyFetching will not be updated - // during this loop (in fact, it is never mutated). - self._currentlyFetching.forEach(function (cacheKey, id) { - waiting++; - self._mongoHandle._docFetcher.fetch( - self._cursorDescription.collectionName, id, cacheKey, - finishIfNeedToPollQuery(function (err, doc) { - try { - if (err) { - Meteor._debug("Got exception while fetching documents: " + - err); - // If we get an error from the fetcher (eg, trouble connecting - // to Mongo), let's just abandon the fetch phase altogether - // and fall back to polling. It's not like we're getting live - // updates anyway. - if (self._phase !== PHASE.QUERYING) { - self._needToPollQuery(); + self._currentlyFetching = self._needToFetch; + var thisGeneration = ++self._fetchGeneration; + self._needToFetch = new LocalCollection._IdMap; + var waiting = 0; + var fut = new Future; + // This loop is safe, because _currentlyFetching will not be updated + // during this loop (in fact, it is never mutated). + self._currentlyFetching.forEach(function (cacheKey, id) { + waiting++; + self._mongoHandle._docFetcher.fetch( + self._cursorDescription.collectionName, id, cacheKey, + finishIfNeedToPollQuery(function (err, doc) { + try { + if (err) { + Meteor._debug("Got exception while fetching documents: " + + err); + // If we get an error from the fetcher (eg, trouble + // connecting to Mongo), let's just abandon the fetch phase + // altogether and fall back to polling. It's not like we're + // getting live updates anyway. + if (self._phase !== PHASE.QUERYING) { + self._needToPollQuery(); + } + } else if (!self._stopped && self._phase === PHASE.FETCHING + && self._fetchGeneration === thisGeneration) { + // We re-check the generation in case we've had an explicit + // _pollQuery call (eg, in another fiber) which should + // effectively cancel this round of fetches. (_pollQuery + // increments the generation.) + self._handleDoc(id, doc); } - } else if (!self._stopped && self._phase === PHASE.FETCHING - && self._fetchGeneration === thisGeneration) { - // We re-check the generation in case we've had an explicit - // _pollQuery call (eg, in another fiber) which should - // effectively cancel this round of fetches. (_pollQuery - // increments the generation.) - self._handleDoc(id, doc); + } finally { + waiting--; + // Because fetch() never calls its callback synchronously, + // this is safe (ie, we won't call fut.return() before the + // forEach is done). + if (waiting === 0) + fut.return(); } - } finally { - waiting--; - // Because fetch() never calls its callback synchronously, this - // is safe (ie, we won't call fut.return() before the forEach is - // done). - if (waiting === 0) - fut.return(); - } - })); - }); - fut.wait(); - // Exit now if we've had a _pollQuery call (here or in another fiber). - if (self._phase === PHASE.QUERYING) - return; - self._currentlyFetching = null; - } - // We're done fetching, so we can be steady, unless we've had a _pollQuery - // call (here or in another fiber). - if (self._phase !== PHASE.QUERYING) - self._beSteady(); - })); + })); + }); + fut.wait(); + // Exit now if we've had a _pollQuery call (here or in another fiber). + if (self._phase === PHASE.QUERYING) + return; + self._currentlyFetching = null; + } + // We're done fetching, so we can be steady, unless we've had a + // _pollQuery call (here or in another fiber). + if (self._phase !== PHASE.QUERYING) + self._beSteady(); + })); + }); }, _beSteady: function () { var self = this; - self._registerPhaseChange(PHASE.STEADY); - var writes = self._writesToCommitWhenWeReachSteady; - self._writesToCommitWhenWeReachSteady = []; - self._multiplexer.onFlush(function () { - _.each(writes, function (w) { - w.committed(); + Meteor._noYieldsAllowed(function () { + self._registerPhaseChange(PHASE.STEADY); + var writes = self._writesToCommitWhenWeReachSteady; + self._writesToCommitWhenWeReachSteady = []; + self._multiplexer.onFlush(function () { + _.each(writes, function (w) { + w.committed(); + }); }); }); }, _handleOplogEntryQuerying: function (op) { var self = this; - self._needToFetch.set(idForOp(op), op.ts.toString()); + Meteor._noYieldsAllowed(function () { + self._needToFetch.set(idForOp(op), op.ts.toString()); + }); }, _handleOplogEntrySteadyOrFetching: function (op) { var self = this; - var id = idForOp(op); - // If we're already fetching this one, or about to, we can't optimize; make - // sure that we fetch it again if necessary. - if (self._phase === PHASE.FETCHING && - ((self._currentlyFetching && self._currentlyFetching.has(id)) || - self._needToFetch.has(id))) { - self._needToFetch.set(id, op.ts.toString()); - return; - } - - if (op.op === 'd') { - if (self._published.has(id) || (self._limit && self._unpublishedBuffer.has(id))) - self._removeMatching(id); - } else if (op.op === 'i') { - if (self._published.has(id)) - throw new Error("insert found for already-existing ID in published"); - if (self._unpublishedBuffer && self._unpublishedBuffer.has(id)) - throw new Error("insert found for already-existing ID in buffer"); - - // XXX what if selector yields? for now it can't but later it could have - // $where - if (self._matcher.documentMatches(op.o).result) - self._addMatching(op.o); - } else if (op.op === 'u') { - // Is this a modifier ($set/$unset, which may require us to poll the - // database to figure out if the whole document matches the selector) or a - // replacement (in which case we can just directly re-evaluate the - // selector)? - var isReplace = !_.has(op.o, '$set') && !_.has(op.o, '$unset'); - // If this modifier modifies something inside an EJSON custom type (ie, - // anything with EJSON$), then we can't try to use - // LocalCollection._modify, since that just mutates the EJSON encoding, - // not the actual object. - var canDirectlyModifyDoc = - !isReplace && modifierCanBeDirectlyApplied(op.o); - - var publishedBefore = self._published.has(id); - var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); - - if (isReplace) { - self._handleDoc(id, _.extend({_id: id}, op.o)); - } else if ((publishedBefore || bufferedBefore) && canDirectlyModifyDoc) { - // Oh great, we actually know what the document is, so we can apply - // this directly. - var newDoc = self._published.has(id) ? - self._published.get(id) : - self._unpublishedBuffer.get(id); - newDoc = EJSON.clone(newDoc); - - newDoc._id = id; - LocalCollection._modify(newDoc, op.o); - self._handleDoc(id, self._sharedProjectionFn(newDoc)); - } else if (!canDirectlyModifyDoc || - self._matcher.canBecomeTrueByModifier(op.o) || - (self._sorter && self._sorter.affectedByModifier(op.o))) { + Meteor._noYieldsAllowed(function () { + var id = idForOp(op); + // If we're already fetching this one, or about to, we can't optimize; + // make sure that we fetch it again if necessary. + if (self._phase === PHASE.FETCHING && + ((self._currentlyFetching && self._currentlyFetching.has(id)) || + self._needToFetch.has(id))) { self._needToFetch.set(id, op.ts.toString()); - if (self._phase === PHASE.STEADY) - self._fetchModifiedDocuments(); + return; } - } else { - throw Error("XXX SURPRISING OPERATION: " + op); - } + + if (op.op === 'd') { + if (self._published.has(id) || + (self._limit && self._unpublishedBuffer.has(id))) + self._removeMatching(id); + } else if (op.op === 'i') { + if (self._published.has(id)) + throw new Error("insert found for already-existing ID in published"); + if (self._unpublishedBuffer && self._unpublishedBuffer.has(id)) + throw new Error("insert found for already-existing ID in buffer"); + + // XXX what if selector yields? for now it can't but later it could + // have $where + if (self._matcher.documentMatches(op.o).result) + self._addMatching(op.o); + } else if (op.op === 'u') { + // Is this a modifier ($set/$unset, which may require us to poll the + // database to figure out if the whole document matches the selector) or + // a replacement (in which case we can just directly re-evaluate the + // selector)? + var isReplace = !_.has(op.o, '$set') && !_.has(op.o, '$unset'); + // If this modifier modifies something inside an EJSON custom type (ie, + // anything with EJSON$), then we can't try to use + // LocalCollection._modify, since that just mutates the EJSON encoding, + // not the actual object. + var canDirectlyModifyDoc = + !isReplace && modifierCanBeDirectlyApplied(op.o); + + var publishedBefore = self._published.has(id); + var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); + + if (isReplace) { + self._handleDoc(id, _.extend({_id: id}, op.o)); + } else if ((publishedBefore || bufferedBefore) && + canDirectlyModifyDoc) { + // Oh great, we actually know what the document is, so we can apply + // this directly. + var newDoc = self._published.has(id) + ? self._published.get(id) : self._unpublishedBuffer.get(id); + newDoc = EJSON.clone(newDoc); + + newDoc._id = id; + LocalCollection._modify(newDoc, op.o); + self._handleDoc(id, self._sharedProjectionFn(newDoc)); + } else if (!canDirectlyModifyDoc || + self._matcher.canBecomeTrueByModifier(op.o) || + (self._sorter && self._sorter.affectedByModifier(op.o))) { + self._needToFetch.set(id, op.ts.toString()); + if (self._phase === PHASE.STEADY) + self._fetchModifiedDocuments(); + } + } else { + throw Error("XXX SURPRISING OPERATION: " + op); + } + }); }, + // Yields! _runInitialQuery: function () { var self = this; if (self._stopped) throw new Error("oplog stopped surprisingly early"); - self._runQuery(); + self._runQuery(); // yields if (self._stopped) throw new Error("oplog stopped quite early"); @@ -588,7 +622,7 @@ _.extend(OplogObserveDriver.prototype, { // stop() to be called.) self._multiplexer.ready(); - self._doneQuerying(); + self._doneQuerying(); // yields }, // In various circumstances, we may just want to stop processing the oplog and @@ -607,24 +641,26 @@ _.extend(OplogObserveDriver.prototype, { // changes. Will put off implementing this until driver 1.4 is out. _pollQuery: function () { var self = this; + Meteor._noYieldsAllowed(function () { + if (self._stopped) + return; - if (self._stopped) - return; + // Yay, we get to forget about all the things we thought we had to fetch. + self._needToFetch = new LocalCollection._IdMap; + self._currentlyFetching = null; + ++self._fetchGeneration; // ignore any in-flight fetches + self._registerPhaseChange(PHASE.QUERYING); - // Yay, we get to forget about all the things we thought we had to fetch. - self._needToFetch = new LocalCollection._IdMap; - self._currentlyFetching = null; - ++self._fetchGeneration; // ignore any in-flight fetches - self._registerPhaseChange(PHASE.QUERYING); - - // Defer so that we don't block. We don't need finishIfNeedToPollQuery here - // because SwitchedToQuery is not called in QUERYING mode. - Meteor.defer(function () { - self._runQuery(); - self._doneQuerying(); + // Defer so that we don't yield. We don't need finishIfNeedToPollQuery + // here because SwitchedToQuery is not thrown in QUERYING mode. + Meteor.defer(function () { + self._runQuery(); + self._doneQuerying(); + }); }); }, + // Yields! _runQuery: function () { var self = this; var newResults, newBuffer; @@ -647,7 +683,7 @@ _.extend(OplogObserveDriver.prototype, { // buffer if such is needed. var cursor = self._cursorForQuery({ limit: self._limit * 2 }); try { - cursor.forEach(function (doc, i) { + cursor.forEach(function (doc, i) { // yields if (!self._limit || i < self._limit) newResults.set(doc._id, doc); else @@ -682,65 +718,70 @@ _.extend(OplogObserveDriver.prototype, { // _fetchModifiedDocuments does this.) _needToPollQuery: function () { var self = this; - if (self._stopped) - return; + Meteor._noYieldsAllowed(function () { + if (self._stopped) + return; - // If we're not already in the middle of a query, we can query now (possibly - // pausing FETCHING). - if (self._phase !== PHASE.QUERYING) { - self._pollQuery(); - throw new SwitchedToQuery; - } + // If we're not already in the middle of a query, we can query now + // (possibly pausing FETCHING). + if (self._phase !== PHASE.QUERYING) { + self._pollQuery(); + throw new SwitchedToQuery; + } - // We're currently in QUERYING. Set a flag to ensure that we run another - // query when we're done. - self._requeryWhenDoneThisQuery = true; + // We're currently in QUERYING. Set a flag to ensure that we run another + // query when we're done. + self._requeryWhenDoneThisQuery = true; + }); }, + // Yields! _doneQuerying: function () { var self = this; if (self._stopped) return; - self._mongoHandle._oplogHandle.waitUntilCaughtUp(); - + self._mongoHandle._oplogHandle.waitUntilCaughtUp(); // yields if (self._stopped) return; if (self._phase !== PHASE.QUERYING) throw Error("Phase unexpectedly " + self._phase); - if (self._requeryWhenDoneThisQuery) { - self._requeryWhenDoneThisQuery = false; - self._pollQuery(); - } else if (self._needToFetch.empty()) { - self._beSteady(); - } else { - self._fetchModifiedDocuments(); - } + Meteor._noYieldsAllowed(function () { + if (self._requeryWhenDoneThisQuery) { + self._requeryWhenDoneThisQuery = false; + self._pollQuery(); + } else if (self._needToFetch.empty()) { + self._beSteady(); + } else { + self._fetchModifiedDocuments(); + } + }); }, _cursorForQuery: function (optionsOverwrite) { var self = this; + return Meteor._noYieldsAllowed(function () { + // The query we run is almost the same as the cursor we are observing, + // with a few changes. We need to read all the fields that are relevant to + // the selector, not just the fields we are going to publish (that's the + // "shared" projection). And we don't want to apply any transform in the + // cursor, because observeChanges shouldn't use the transform. + var options = _.clone(self._cursorDescription.options); - // The query we run is almost the same as the cursor we are observing, with - // a few changes. We need to read all the fields that are relevant to the - // selector, not just the fields we are going to publish (that's the - // "shared" projection). And we don't want to apply any transform in the - // cursor, because observeChanges shouldn't use the transform. - var options = _.clone(self._cursorDescription.options); + // Allow the caller to modify the options. Useful to specify different + // skip and limit values. + _.extend(options, optionsOverwrite); - // Allow the caller to modify the options. Useful to specify different skip - // and limit values. - _.extend(options, optionsOverwrite); - - options.fields = self._sharedProjection; - delete options.transform; - // We are NOT deep cloning fields or selector here, which should be OK. - var description = new CursorDescription( - self._cursorDescription.collectionName, - self._cursorDescription.selector, - options); - return new Cursor(self._mongoHandle, description); + options.fields = self._sharedProjection; + delete options.transform; + // We are NOT deep cloning fields or selector here, which should be OK. + var description = new CursorDescription( + self._cursorDescription.collectionName, + self._cursorDescription.selector, + options); + return new Cursor(self._mongoHandle, description); + }); }, @@ -749,52 +790,54 @@ _.extend(OplogObserveDriver.prototype, { // Replace self._unpublishedBuffer with newBuffer. // // XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We - // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict (b) - // Rewrite diff.js to use these classes instead of arrays and objects. + // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict + // (b) Rewrite diff.js to use these classes instead of arrays and objects. _publishNewResults: function (newResults, newBuffer) { var self = this; + Meteor._noYieldsAllowed(function () { - // If the query is limited and there is a buffer, shut down so it doesn't - // stay in a way. - if (self._limit) { - self._unpublishedBuffer.clear(); - } + // If the query is limited and there is a buffer, shut down so it doesn't + // stay in a way. + if (self._limit) { + self._unpublishedBuffer.clear(); + } - // First remove anything that's gone. Be careful not to modify - // self._published while iterating over it. - var idsToRemove = []; - self._published.forEach(function (doc, id) { - if (!newResults.has(id)) - idsToRemove.push(id); + // First remove anything that's gone. Be careful not to modify + // self._published while iterating over it. + var idsToRemove = []; + self._published.forEach(function (doc, id) { + if (!newResults.has(id)) + idsToRemove.push(id); + }); + _.each(idsToRemove, function (id) { + self._removePublished(id); + }); + + // Now do adds and changes. + // If self has a buffer and limit, the new fetched result will be + // limited correctly as the query has sort specifier. + newResults.forEach(function (doc, id) { + self._handleDoc(id, doc); + }); + + // Sanity-check that everything we tried to put into _published ended up + // there. + // XXX if this is slow, remove it later + if (self._published.size() !== newResults.size()) { + throw Error("failed to copy newResults into _published!"); + } + self._published.forEach(function (doc, id) { + if (!newResults.has(id)) + throw Error("_published has a doc that newResults doesn't; " + id); + }); + + // Finally, replace the buffer + newBuffer.forEach(function (doc, id) { + self._addBuffered(id, doc); + }); + + self._safeAppendToBuffer = newBuffer.size() < self._limit; }); - _.each(idsToRemove, function (id) { - self._removePublished(id); - }); - - // Now do adds and changes. - // If self has a buffer and limit, the new fetched result will be - // limited correctly as the query has sort specifier. - newResults.forEach(function (doc, id) { - self._handleDoc(id, doc); - }); - - // Sanity-check that everything we tried to put into _published ended up - // there. - // XXX if this is slow, remove it later - if (self._published.size() !== newResults.size()) { - throw Error("failed to copy newResults into _published!"); - } - self._published.forEach(function (doc, id) { - if (!newResults.has(id)) - throw Error("_published has a doc that newResults doesn't; " + id); - }); - - // Finally, replace the buffer - newBuffer.forEach(function (doc, id) { - self._addBuffered(id, doc); - }); - - self._safeAppendToBuffer = newBuffer.size() < self._limit; }, // This stop function is invoked from the onStop of the ObserveMultiplexer, so @@ -818,7 +861,7 @@ _.extend(OplogObserveDriver.prototype, { // to get flushed (and it's probably not valid to call methods on the // dying multiplexer). _.each(self._writesToCommitWhenWeReachSteady, function (w) { - w.committed(); + w.committed(); // maybe yields? }); self._writesToCommitWhenWeReachSteady = null; @@ -836,16 +879,18 @@ _.extend(OplogObserveDriver.prototype, { _registerPhaseChange: function (phase) { var self = this; - var now = new Date; + Meteor._noYieldsAllowed(function () { + var now = new Date; - if (self._phase) { - var timeDiff = now - self._phaseStartTime; - Package.facts && Package.facts.Facts.incrementServerFact( - "mongo-livedata", "time-spent-in-" + self._phase + "-phase", timeDiff); - } + if (self._phase) { + var timeDiff = now - self._phaseStartTime; + Package.facts && Package.facts.Facts.incrementServerFact( + "mongo-livedata", "time-spent-in-" + self._phase + "-phase", timeDiff); + } - self._phase = phase; - self._phaseStartTime = now; + self._phase = phase; + self._phaseStartTime = now; + }); } }); From 6efb7c85ccff82ddd54a9b67eacef74540906937 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 29 Jul 2014 14:34:19 -0700 Subject: [PATCH 44/69] Update docs and examples to 0.8.3 --- docs/.meteor/release | 2 +- docs/lib/release-override.js | 2 +- examples/clock/.meteor/release | 2 +- examples/leaderboard/.meteor/release | 2 +- examples/parties/.meteor/release | 2 +- examples/todos/.meteor/release | 2 +- examples/wordplay/.meteor/release | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/.meteor/release b/docs/.meteor/release index 100435be13..ee94dd834b 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/docs/lib/release-override.js b/docs/lib/release-override.js index d0fc420710..6bfbd0e614 100644 --- a/docs/lib/release-override.js +++ b/docs/lib/release-override.js @@ -1,5 +1,5 @@ // While galaxy apps are on their own special meteor releases, override // Meteor.release here. if (Meteor.isClient) { - Meteor.release = Meteor.release ? "0.8.2" : undefined; + Meteor.release = Meteor.release ? "0.8.3" : undefined; } diff --git a/examples/clock/.meteor/release b/examples/clock/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/clock/.meteor/release +++ b/examples/clock/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 From 33eee2c25acc353d5ca6922e56c782d4cee2a9cc Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 30 Jul 2014 09:37:48 -0700 Subject: [PATCH 45/69] history tweak --- History.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/History.md b/History.md index 6b4ee782f4..7769472360 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,4 @@ -## v.NEXT.NEXT +## v.NEXT * The `appcache` package now defaults to functioning on all browsers that support the AppCache API, rather than a whitelist of browsers. You can still @@ -7,10 +7,6 @@ Firefox no longer makes a confusing popup. #2241 - -## v.NEXT - - ## v0.8.3 #### Blaze From dd82a8bcd55af2f1fda8b6b9c67f757ff180ad4f Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 30 Jul 2014 09:38:30 -0700 Subject: [PATCH 46/69] Add node upgrade to History --- History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/History.md b/History.md index 7769472360..6811c731ed 100644 --- a/History.md +++ b/History.md @@ -82,6 +82,7 @@ callback provided. * Upgraded dependencies: + - node: 0.10.29 (from 0.10.28) - less: 1.7.1 (from 1.6.1) Patches contributed by GitHub users Cangit, cmather, duckspeaker, zol. From 902de60d486934820a7e18e77017fc7e888b7ea6 Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Wed, 30 Jul 2014 09:38:30 -0700 Subject: [PATCH 47/69] Add node upgrade to History --- History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/History.md b/History.md index 0ce44b0e8c..d017cc21f6 100644 --- a/History.md +++ b/History.md @@ -76,6 +76,7 @@ callback provided. * Upgraded dependencies: + - node: 0.10.29 (from 0.10.28) - less: 1.7.1 (from 1.6.1) Patches contributed by GitHub users Cangit, cmather, duckspeaker, zol. From 47831e8744d2a5cc9d3def613d72bc5b9ed42822 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 30 Jul 2014 12:00:51 -0700 Subject: [PATCH 48/69] Expose match failures in server logs --- packages/livedata/livedata_server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 0ff5dd81f3..11d1114bba 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1456,6 +1456,11 @@ var wrapInternalException = function (exception, context) { if (!exception || exception instanceof Meteor.Error) return exception; + // tests can set the 'expected' flag on an exception so it won't go to the + // server log + if (!exception.expected) + Meteor._debug("Exception " + context, exception.stack); + // Did the error contain more details that could have been useful if caught in // server code (or if thrown from non-client-originated code), but also // provided a "sanitized" version with more context than 500 Internal server @@ -1467,11 +1472,6 @@ var wrapInternalException = function (exception, context) { "is not a Meteor.Error; ignoring"); } - // tests can set the 'expected' flag on an exception so it won't go to the - // server log - if (!exception.expected) - Meteor._debug("Exception " + context, exception.stack); - return new Meteor.Error(500, "Internal server error"); }; From c3508acc07a60c3fb07770cb608792d0766c53d0 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Tue, 29 Jul 2014 09:44:12 -0700 Subject: [PATCH 49/69] Tweak benchmark to log each operation --- examples/unfinished/benchmark/benchmark.js | 25 +++++++++++++++---- .../benchmark/scenarios/scale10.json | 11 ++++++++ .../benchmark/scenarios/scale100.json | 11 ++++++++ .../benchmark/scenarios/scale50.json | 11 ++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 examples/unfinished/benchmark/scenarios/scale10.json create mode 100644 examples/unfinished/benchmark/scenarios/scale100.json create mode 100644 examples/unfinished/benchmark/scenarios/scale50.json diff --git a/examples/unfinished/benchmark/benchmark.js b/examples/unfinished/benchmark/benchmark.js index abbbde6ad8..1a206ea094 100644 --- a/examples/unfinished/benchmark/benchmark.js +++ b/examples/unfinished/benchmark/benchmark.js @@ -35,6 +35,16 @@ var randomString = function (length) { return ret; }; +var preCall = function (name) { + console.log('> ' + name); +}; + +var postCall = function (name) { + return function (err, callback) { + console.log('< ' + name + ' ' + (err ? 'ERR' : 'OK')); + }; +}; + var pickCollection = function () { return Random.choice(Collections); }; @@ -96,7 +106,8 @@ if (Meteor.isServer) { Meteor.setInterval(function () { var when = +(new Date) - PARAMS.maxAgeSeconds*1000; _.each(Collections, function (C) { - C.remove({when: {$lt: when}}); + preCall('removeMaxAge'); + C.remove({when: {$lt: when}}, postCall('removeMaxAge')); }); // Clear out 5% of the DB each time, steady state. XXX parameterize? }, 1000*PARAMS.maxAgeSeconds / 20); @@ -121,7 +132,8 @@ if (Meteor.isServer) { doc.when = +(new Date); var C = pickCollection(); - C.insert(doc); + preCall('insert'); + C.insert(doc, postCall('insert')); }, update: function (processId, field, value) { check([processId, field, value], [String]); @@ -130,15 +142,18 @@ if (Meteor.isServer) { var C = pickCollection(); // update one message. - C.update({fromProcess: processId}, {$set: modifer}, {multi: false}); + preCall('update'); + C.update({fromProcess: processId}, {$set: modifer}, {multi: false}, postCall('update')); }, remove: function (processId) { check(processId, String); var C = pickCollection(); // remove one message. var obj = C.findOne({fromProcess: processId}); - if (obj) - C.remove(obj._id); + if (obj) { + preCall('remove'); + C.remove(obj._id, postCall('remove')); + } } }); diff --git a/examples/unfinished/benchmark/scenarios/scale10.json b/examples/unfinished/benchmark/scenarios/scale10.json new file mode 100644 index 0000000000..5962540e04 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale10.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 10, + "updatesPerSecond": 10, + "removesPerSecond": 1, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale100.json b/examples/unfinished/benchmark/scenarios/scale100.json new file mode 100644 index 0000000000..ec0f4853e9 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale100.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 100, + "updatesPerSecond": 100, + "removesPerSecond": 10, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale50.json b/examples/unfinished/benchmark/scenarios/scale50.json new file mode 100644 index 0000000000..0e4df53c12 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale50.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 50, + "updatesPerSecond": 50, + "removesPerSecond": 5, + "documentSize": 128, + "documentNumFields": 2 + } +} From 4e6efe91a5fa390da56708e11af34b54f7ce784d Mon Sep 17 00:00:00 2001 From: Justin SB Date: Tue, 29 Jul 2014 09:44:50 -0700 Subject: [PATCH 50/69] Output test results in xunit format when the path is 'xunit' We output via console.log; to differentiate from normal output, we prefix xunit output with 'XUNIT '. --- packages/test-in-console/driver.js | 72 +++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index 50e03156f3..35208a0b74 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -8,18 +8,43 @@ TEST_STATUS = { FAILURES: null }; +// xUnit format uses XML output +var XML_CHAR_MAP = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' +}; +// Escapes a string for insertion into XML +var escapeXml = function (s) { + return s.replace(/[<>&"']/g, function (c) { + return XML_CHAR_MAP[c]; + }); +} + +// Returns a human name for a test var getName = function (result) { return (result.server ? "S: " : "C: ") + result.groupPath.join(" - ") + " - " + result.test; }; +// Calls console.log, but returns silently if console.log is not available var log = function (/*arguments*/) { if (typeof console !== 'undefined') { console.log.apply(console, arguments); } }; +// Logs xUnit output, if xunit output is enabled +// Output is sent to console.log, prefixed with a magic string 'XUNIT ' +// By grepping for that prefix, the xUnit output can be extracted +var xunit = function (s) { + if (xunitEnabled) { + log('XUNIT ' + s); + } +}; var passed = 0; var failed = 0; @@ -31,6 +56,10 @@ var hrefPath = document.location.href.split("/"); var platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]); if (!platform) platform = "local"; + +// We enable xUnit output when platform is xunit +var xunitEnabled = (platform == 'xunit'); + var doReport = Meteor && Meteor.settings && Meteor.settings.public && @@ -82,10 +111,13 @@ Meteor.startup(function () { status: "PENDING", events: [], server: !!results.server, - testPath: testPath + testPath: testPath, + test: results.test }; report(name, false); } + // Loop through events, and record status for each test + // Also log result if test has finished _.each(results.events, function (event) { resultSet[name].events.push(event); switch (event.type) { @@ -136,6 +168,7 @@ Meteor.startup(function () { }); }, + // After test completion, log a quick summary function () { if (failed > 0) { log("~~~~~~~ THERE ARE FAILURES ~~~~~~~"); @@ -153,6 +186,43 @@ Meteor.startup(function () { TEST_STATUS.DONE = DONE = true; } }); + + // Also log xUnit output + xunit(''); + _.each(resultSet, function (result, name) { + var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client"); + var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client"); + var time = ""; + var error = ""; + _.each(result.events, function (event) { + switch (event.type) { + case "finish": + var timeMs = event.timeMs; + if (timeMs !== undefined) { + time = (timeMs / 1000) + ""; + } + break; + case "fail": + var details = event.details || {}; + error = (details.message || '?') + " filename=" + (details.filename || '?') + " line=" + (details.line || '?'); + } + }); + switch (event.status) { + case "FAIL": + error = error || '?'; + break; + case "EXPECTED": + error = "Expected failure"; + break; + } + + xunit(''); + if (error) { + xunit(' ' + escapeXml(error) + ''); + } + xunit(''); + }); + xunit(''); }, ["tinytest"]); }); From 3163112235b3d294bca151b2477ceab71a41f402 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Tue, 29 Jul 2014 09:47:11 -0700 Subject: [PATCH 51/69] Get S3 credentials from env variables, if set, in publish-release.js --- .../admin/publish-release/server/publish-release.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/admin/publish-release/server/publish-release.js b/scripts/admin/publish-release/server/publish-release.js index 75caa7c2cf..d33885113d 100644 --- a/scripts/admin/publish-release/server/publish-release.js +++ b/scripts/admin/publish-release/server/publish-release.js @@ -59,7 +59,15 @@ var configureS3 = function () { return {accessKey: accessKey, secretKey: secretKey}; }; - var s3Credentials = getS3Credentials(); + var s3Credentials; + if (process.env.AWS_ACCESS_KEY_ID) { + s3Credentials = {}; + s3Credentials.accessKey = process.env.AWS_ACCESS_KEY_ID; + s3Credentials.secretKey = process.env.AWS_SECRET_ACCESS_KEY; + } else { + s3Credentials = getS3Credentials(); + } + var s3 = new S3({ accessKeyId: s3Credentials.accessKey, secretAccessKey: s3Credentials.secretKey, From 706fc7ebe77afc60a5b5967f753c4f16761b1a39 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Tue, 29 Jul 2014 09:49:03 -0700 Subject: [PATCH 52/69] Allow TIMEOUT_SCALE_FACTOR env variable to scale up timeouts for self-test Particularly for automated tests, where we may run on a slow machine, we need an escape valve to let us boost the timeouts. This also allows for a 'tolerant' test (scale factor 2+?) and a 'strict' test (scale factor 0.5?) etc --- tools/selftest.js | 4 ++++ tools/test-utils.js | 2 +- tools/utils.js | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/selftest.js b/tools/selftest.js index 0d84ac73e8..52099a8633 100644 --- a/tools/selftest.js +++ b/tools/selftest.js @@ -698,6 +698,7 @@ _.extend(Run.prototype, { self._ensureStarted(); var timeout = self.baseTimeout + self.extraTime; + timeout *= utils.timeoutScaleFactor; self.extraTime = 0; return self.stdoutMatcher.match(pattern, timeout, _strict); }), @@ -708,6 +709,7 @@ _.extend(Run.prototype, { self._ensureStarted(); var timeout = self.baseTimeout + self.extraTime; + timeout *= utils.timeoutScaleFactor; self.extraTime = 0; return self.stderrMatcher.match(pattern, timeout, _strict); }), @@ -753,6 +755,7 @@ _.extend(Run.prototype, { self._ensureStarted(); var timeout = self.baseTimeout + self.extraTime; + timeout *= utils.timeoutScaleFactor; self.extraTime = 0; self.expectExit(); @@ -770,6 +773,7 @@ _.extend(Run.prototype, { if (self.exitStatus === undefined) { var timeout = self.baseTimeout + self.extraTime; + timeout *= utils.timeoutScaleFactor; self.extraTime = 0; var fut = new Future; diff --git a/tools/test-utils.js b/tools/test-utils.js index c9840ff65b..80858e2520 100644 --- a/tools/test-utils.js +++ b/tools/test-utils.js @@ -12,7 +12,7 @@ var randomString = function (charsCount) { return str; }; -exports.accountsCommandTimeoutSecs = 15; +exports.accountsCommandTimeoutSecs = 15 * exports.timeoutScaleFactor; exports.randomString = randomString; diff --git a/tools/utils.js b/tools/utils.js index 80ae7ab6e2..1fe6f54569 100644 --- a/tools/utils.js +++ b/tools/utils.js @@ -151,3 +151,10 @@ exports.validEmail = function (address) { exports.quotemeta = function (str) { return String(str).replace(/(\W)/g, '\\$1'); }; + +// Allow a simple way to scale up all timeouts from the command line +var timeoutScaleFactor = 1.0; +if (process.env.TIMEOUT_SCALE_FACTOR) { + timeoutScaleFactor = parseFloat(process.env.TIMEOUT_SCALE_FACTOR); +} +exports.timeoutScaleFactor = timeoutScaleFactor; From 812a3af2cd0b44cb5f4e9192ead1b7dd0c252208 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Tue, 29 Jul 2014 09:50:15 -0700 Subject: [PATCH 53/69] By default, don't run server & client tests in parallel --- packages/tinytest/tinytest_client.js | 31 +++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/tinytest/tinytest_client.js b/packages/tinytest/tinytest_client.js index f1c1f677d2..6749c172e2 100644 --- a/packages/tinytest/tinytest_client.js +++ b/packages/tinytest/tinytest_client.js @@ -2,23 +2,40 @@ // the server. Sets a 'server' flag on test results that came from the // server. // -Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) { +// Options: +// serial if true, will not run tests in parallel. Currently this means +// running the server tests before running the client tests. +// Default is currently true (serial operation), but we will likely +// change this to false in future. +Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix, options) { var runId = Random.id(); var localComplete = false; + var localStarted = false; var remoteComplete = false; var done = false; + options = _.extend({ + serial: true + }, options); + var serial = !!options.serial; + var maybeDone = function () { if (!done && localComplete && remoteComplete) { done = true; onComplete && onComplete(); } + if (serial && remoteComplete && !localStarted) { + startLocalTests(); + } }; - Tinytest._runTests(onReport, function () { - localComplete = true; - maybeDone(); - }, pathPrefix); + var startLocalTests = function() { + localStarted = true; + Tinytest._runTests(onReport, function () { + localComplete = true; + maybeDone(); + }, pathPrefix); + }; var handle; @@ -59,4 +76,8 @@ Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) { // XXX better report error throw new Error("Test server returned an error"); }); + + if (!serial) { + startLocalTests(); + } }; From f2184d3dbe47a3be0e7edc09df73ddaeef05da81 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Wed, 30 Jul 2014 11:55:10 -0700 Subject: [PATCH 54/69] Tweak examples/unfinished/benchmark script --- examples/unfinished/benchmark/run-local.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/unfinished/benchmark/run-local.sh b/examples/unfinished/benchmark/run-local.sh index 3c533ad54a..9fecfcaba9 100755 --- a/examples/unfinished/benchmark/run-local.sh +++ b/examples/unfinished/benchmark/run-local.sh @@ -1,8 +1,12 @@ #!/bin/bash PORT=9000 -NUM_CLIENTS=10 -DURATION=120 +if [ -z "$NUM_CLIENTS" ]; then + NUM_CLIENTS=10 +fi +if [ -z "$DURATION" ]; then + DURATION=120 +fi REPORT_INTERVAL=10 set -e @@ -20,7 +24,7 @@ pkill -f "$PROJDIR/.meteor/local/db" || true ../../../meteor reset || true # start the benchmark app -../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port 9000 & +../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port ${PORT} & OUTER_PID=$! echo "Waiting for server to come up" @@ -30,12 +34,13 @@ function wait_for_port { sleep 1 N=$(($N+1)) if [ $N -ge $2 ] ; then + curl -v "$1" || true echo "Timed out waiting for port $1" exit 2 fi done } -wait_for_port "http://localhost:9001" 60 +wait_for_port "http://localhost:${PORT}" 60 echo "Starting phantoms" From 693b78001c83d48ef31ea0fe432327f68ee16023 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 30 Jul 2014 16:21:13 -0700 Subject: [PATCH 55/69] Also log sanitized error in server logs (which gets sent to client) on match errors --- packages/livedata/livedata_server.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 11d1114bba..d845232113 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1458,8 +1458,11 @@ var wrapInternalException = function (exception, context) { // tests can set the 'expected' flag on an exception so it won't go to the // server log - if (!exception.expected) + if (!exception.expected) { Meteor._debug("Exception " + context, exception.stack); + Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message); + Meteor._debug(); + } // Did the error contain more details that could have been useful if caught in // server code (or if thrown from non-client-originated code), but also From b85e3e1c5efeff31048ae3e3bc0fc9bf7fd5ecbf Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 30 Jul 2014 16:22:58 -0700 Subject: [PATCH 56/69] Update History.md: log match errors on server --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 6811c731ed..0c02250709 100644 --- a/History.md +++ b/History.md @@ -6,6 +6,9 @@ change is that `appcache` is now enabled by default on Firefox, because Firefox no longer makes a confusing popup. #2241 +* When a call to `match` fails in a method or subscription, log the + failure on the server. (This matches the behavior described in our docs) + ## v0.8.3 From 7c4af8d12fd627995f3a4aeb789b0d0247a5031d Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 30 Jul 2014 16:51:12 -0700 Subject: [PATCH 57/69] Add guard I forgot to add in 693b78001c83d48ef31ea0fe432327f68ee16023 (Thanks @justinsb for noticing this!) --- packages/livedata/livedata_server.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index d845232113..e7b00b6fde 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1460,8 +1460,10 @@ var wrapInternalException = function (exception, context) { // server log if (!exception.expected) { Meteor._debug("Exception " + context, exception.stack); - Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message); - Meteor._debug(); + if (exception.sanitizedError) { + Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message); + Meteor._debug(); + } } // Did the error contain more details that could have been useful if caught in From bf0e0b61df4b5fd07f054cb51ca02aa092dbc88e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Thu, 31 Jul 2014 15:05:56 -0700 Subject: [PATCH 58/69] Fix usage of timeoutScaleFactor in test-utils --- tools/test-utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/test-utils.js b/tools/test-utils.js index 80858e2520..f959254262 100644 --- a/tools/test-utils.js +++ b/tools/test-utils.js @@ -2,6 +2,7 @@ var _ = require('underscore'); var release = require('./release.js'); var unipackage = require('./unipackage.js'); var config = require('./config.js'); +var utils = require('./utils.js'); var randomString = function (charsCount) { var chars = 'abcdefghijklmnopqrstuvwxyz'; @@ -12,7 +13,7 @@ var randomString = function (charsCount) { return str; }; -exports.accountsCommandTimeoutSecs = 15 * exports.timeoutScaleFactor; +exports.accountsCommandTimeoutSecs = 15 * utils.timeoutScaleFactor; exports.randomString = randomString; From 93b7a6fb1098ea5bf7147bb9b9cbbc170becf456 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Thu, 31 Jul 2014 19:58:16 -0700 Subject: [PATCH 59/69] Add some more scenarios for benchmark --- examples/unfinished/benchmark/scenarios/scale20.json | 11 +++++++++++ examples/unfinished/benchmark/scenarios/scale40.json | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 examples/unfinished/benchmark/scenarios/scale20.json create mode 100644 examples/unfinished/benchmark/scenarios/scale40.json diff --git a/examples/unfinished/benchmark/scenarios/scale20.json b/examples/unfinished/benchmark/scenarios/scale20.json new file mode 100644 index 0000000000..db357f2994 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale20.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 20, + "updatesPerSecond": 20, + "removesPerSecond": 2, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale40.json b/examples/unfinished/benchmark/scenarios/scale40.json new file mode 100644 index 0000000000..53cab32bd1 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale40.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 40, + "updatesPerSecond": 40, + "removesPerSecond": 4, + "documentSize": 128, + "documentNumFields": 2 + } +} From 80b09735071e0b522be0fd63d84b9e523b70f490 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 1 Aug 2014 14:58:11 -0700 Subject: [PATCH 60/69] rename `bindToCurrentDataIfIsfunction` to `bindDataContext` (for clarity) --- packages/blaze/lookup.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/blaze/lookup.js b/packages/blaze/lookup.js index 31f5701fde..3bc97a5f6b 100644 --- a/packages/blaze/lookup.js +++ b/packages/blaze/lookup.js @@ -6,7 +6,9 @@ var bindIfIsFunction = function (x, target) { }; }; -var bindToCurrentDataIfIsFunction = function (x) { +// If `x` is a function, binds the value of `this` for that function +// to the current data context. +var bindDataContext = function (x) { if (typeof x === 'function') { return function () { var data = Blaze.getCurrentData(); @@ -22,6 +24,8 @@ var wrapHelper = function (f) { return Blaze.wrapCatchingExceptions(f, 'template helper'); }; +// !!! FIX THIS COMMENT !!! +// // Implements {{foo}} where `name` is "foo" // and `component` is the component the tag is found in // (the lexical "self," on which to look for methods). @@ -45,11 +49,11 @@ Blaze.View.prototype.lookup = function (name, _options) { return Blaze._parentData(name.length - 1, true /*_functionWrapped*/); } else if (template && (name in template)) { - return wrapHelper(bindToCurrentDataIfIsFunction(template[name])); + return wrapHelper(bindDataContext(template[name])); } else if (lookupTemplate && Template.__lookup__(name)) { return Template.__lookup__(name); } else if (UI._globalHelpers[name]) { - return wrapHelper(bindToCurrentDataIfIsFunction(UI._globalHelpers[name])); + return wrapHelper(bindDataContext(UI._globalHelpers[name])); } else { return function () { var isCalledAsFunction = (arguments.length > 0); From 6166abde14b3c49cfb22fdbdca38b933f7275805 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 1 Aug 2014 14:59:48 -0700 Subject: [PATCH 61/69] Reorg observe-sequence (in preparation for {{#each}} over objects) --- packages/observe-sequence/observe_sequence.js | 143 ++++++++++-------- 1 file changed, 77 insertions(+), 66 deletions(-) diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index b4a7dc9b71..29cbc04a5c 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -94,78 +94,19 @@ ObserveSequence = { } if (!seq) { - seqArray = []; - diffArray(lastSeqArray, seqArray, callbacks); + seqArray = seqChangedToEmpty(lastSeqArray, callbacks); } else if (seq instanceof Array) { - var idsUsed = {}; - seqArray = _.map(seq, function (item, index) { - var id; - if (typeof item === 'string') { - // ensure not empty, since other layers (eg DomRange) assume this as well - id = "-" + item; - } else if (typeof item === 'number' || - typeof item === 'boolean' || - item === undefined) { - id = item; - } else if (typeof item === 'object') { - id = (item && item._id) || index; - } else { - throw new Error("{{#each}} doesn't support arrays with " + - "elements of type " + typeof item); - } - - var idString = idStringify(id); - if (idsUsed[idString]) { - if (typeof item === 'object' && '_id' in item) - warn("duplicate id " + id + " in", seq); - id = Random.id(); - } else { - idsUsed[idString] = true; - } - - return { _id: id, item: item }; - }); - - diffArray(lastSeqArray, seqArray, callbacks); + seqArray = seqChangedToArray(lastSeqArray, seq, callbacks); } else if (isStoreCursor(seq)) { - var cursor = seq; - seqArray = []; - - var initial = true; // are we observing initial data from cursor? - activeObserveHandle = cursor.observe({ - addedAt: function (document, atIndex, before) { - if (initial) { - // keep track of initial data so that we can diff once - // we exit `observe`. - if (before !== null) - throw new Error("Expected initial data from observe in order"); - seqArray.push({ _id: document._id, item: document }); - } else { - callbacks.addedAt(document._id, document, atIndex, before); - } - }, - changedAt: function (newDocument, oldDocument, atIndex) { - callbacks.changedAt(newDocument._id, newDocument, oldDocument, - atIndex); - }, - removedAt: function (oldDocument, atIndex) { - callbacks.removedAt(oldDocument._id, oldDocument, atIndex); - }, - movedTo: function (document, fromIndex, toIndex, before) { - callbacks.movedTo( - document._id, document, fromIndex, toIndex, before); - } - }); - initial = false; - - // diff the old sequnce with initial data in the new cursor. this will - // fire `addedAt` callbacks on the initial data. - diffArray(lastSeqArray, seqArray, callbacks); - + var result /* [seqArray, activeObserveHandle] */ = + seqChangedToCursor(lastSeqArray, seq, callbacks); + seqArray = result[0]; + activeObserveHandle = result[1]; } else { throw badSequenceError(); } + diffArray(lastSeqArray, seqArray, callbacks); lastSeq = seq; lastSeqArray = seqArray; }); @@ -306,3 +247,73 @@ var diffArray = function (lastSeqArray, seqArray, callbacks) { } }); }; + +seqChangedToEmpty = function (lastSeqArray, callbacks) { + return []; +}; + +seqChangedToArray = function (lastSeqArray, array, callbacks) { + var idsUsed = {}; + var seqArray = _.map(array, function (item, index) { + var id; + if (typeof item === 'string') { + // ensure not empty, since other layers (eg DomRange) assume this as well + id = "-" + item; + } else if (typeof item === 'number' || + typeof item === 'boolean' || + item === undefined) { + id = item; + } else if (typeof item === 'object') { + id = (item && item._id) || index; + } else { + throw new Error("{{#each}} doesn't support arrays with " + + "elements of type " + typeof item); + } + + var idString = idStringify(id); + if (idsUsed[idString]) { + if (typeof item === 'object' && '_id' in item) + warn("duplicate id " + id + " in", array); + id = Random.id(); + } else { + idsUsed[idString] = true; + } + + return { _id: id, item: item }; + }); + + return seqArray; +}; + +seqChangedToCursor = function (lastSeqArray, cursor, callbacks) { + var initial = true; // are we observing initial data from cursor? + var seqArray = []; + + var observeHandle = cursor.observe({ + addedAt: function (document, atIndex, before) { + if (initial) { + // keep track of initial data so that we can diff once + // we exit `observe`. + if (before !== null) + throw new Error("Expected initial data from observe in order"); + seqArray.push({ _id: document._id, item: document }); + } else { + callbacks.addedAt(document._id, document, atIndex, before); + } + }, + changedAt: function (newDocument, oldDocument, atIndex) { + callbacks.changedAt(newDocument._id, newDocument, oldDocument, + atIndex); + }, + removedAt: function (oldDocument, atIndex) { + callbacks.removedAt(oldDocument._id, oldDocument, atIndex); + }, + movedTo: function (document, fromIndex, toIndex, before) { + callbacks.movedTo( + document._id, document, fromIndex, toIndex, before); + } + }); + initial = false; + + return [seqArray, observeHandle]; +}; From e7250a045fd3c6b817eb7562eb97032511a52d88 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 1 Aug 2014 15:01:26 -0700 Subject: [PATCH 62/69] Eliminate `_fetch` on handles returned from `cursor.observe()` This was originally introduced with f1b77fec966c8c97d53628bfd79196afb0c99498, but it looks like all of our tests now pass. (Maybe eliminating `rewind` in b5a0613f85002ee35732c01a27c0d9b1901ed0de made this no longer necessary?) If we find that this commit did break something, let's make sure to add a failing test before reverting. --- packages/minimongo/observe.js | 27 ------------------- packages/observe-sequence/observe_sequence.js | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js index 95b92d3d51..191b78333f 100644 --- a/packages/minimongo/observe.js +++ b/packages/minimongo/observe.js @@ -177,32 +177,5 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) var handle = cursor.observeChanges(changeObserver.applyChange); suppressed = false; - if (changeObserver.ordered) { - // Fetches the current list of documents, in order, as an array. Can be - // called at any time. Internal API assumed by the `observe-sequence` - // package (used by Meteor UI for `#each` blocks). Only defined on ordered - // observes (those that listen on `addedAt` or similar). Continues to work - // after `stop()` is called on the handle. - // - // Because we already materialize the full OrderedDict of all documents, it - // seems nice to provide access to the view rather than making the data - // consumer reconstitute it. This gives the consumer a shot at doing - // something smart with the feed like proxying it, since firing callbacks - // like `changed` and `movedTo` basically requires omniscience (knowing old - // and new documents, old and new indices, and the correct value for - // `before`). - // - // NOTE: If called from an observe callback for a certain change, the result - // is *not* guaranteed to be a snapshot of the cursor up to that - // change. This is because the callbacks are invoked before updating docs. - handle._fetch = function () { - var docsArray = []; - changeObserver.docs.forEach(function (doc) { - docsArray.push(transform(EJSON.clone(doc))); - }); - return docsArray; - }; - } - return handle; }; diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index 29cbc04a5c..b736ff88ef 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -86,7 +86,7 @@ ObserveSequence = { // more up-to-date information (specifically, the state of the observe // before it was stopped, which may be older than the DB). if (activeObserveHandle) { - lastSeqArray = _.map(activeObserveHandle._fetch(), function (doc) { + lastSeqArray = _.map(lastSeq.fetch(), function (doc) { return {_id: doc._id, item: doc}; }); activeObserveHandle.stop(); From 112198f3e8c7e1a4915065bdb2959c2553c40c92 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 1 Aug 2014 16:39:43 -0700 Subject: [PATCH 63/69] Don't tell users to add ~/.meteor to PATH Tilde expansion doesn't work in $PATH --- scripts/admin/install-engine.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/admin/install-engine.sh b/scripts/admin/install-engine.sh index 52b7692a62..463ea985ce 100644 --- a/scripts/admin/install-engine.sh +++ b/scripts/admin/install-engine.sh @@ -141,7 +141,7 @@ Couldn't write the launcher script. Please either: (1) Run the following as root: cp ~/.meteor/tools/latest/launch-meteor /usr/bin/meteor - (2) Add ~/.meteor to your path, or + (2) Add "$HOME/.meteor" to your path, or (3) Rerun this command to try again. Then to get started, take a look at 'meteor --help' or see the docs at @@ -153,7 +153,7 @@ else Now you need to do one of the following: - (1) Add ~/.meteor to your path, or + (1) Add "$HOME/.meteor" to your path, or (2) Run this command as root: cp ~/.meteor/tools/latest/launch-meteor /usr/bin/meteor From 6345d92f0757043814d121e3a0431214d2cfe3ef Mon Sep 17 00:00:00 2001 From: MaximDubrovin Date: Sat, 19 Jul 2014 07:13:27 +0600 Subject: [PATCH 64/69] move phantom_script.js setInterval into page.open callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until I made this I always got only `````` with it's content, `````` was empty. It seems setInterval script was finished earlier then url content was loaded to the page. Maybe because I have subscriptions with response time lower then 100ms so they were ready very quickly — database server in the same data center. http://phantomjs.org/api/webpage/method/open.html --- packages/spiderable/phantom_script.js | 40 +++++++++++++++------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/spiderable/phantom_script.js b/packages/spiderable/phantom_script.js index 00835824fb..a4208d1547 100644 --- a/packages/spiderable/phantom_script.js +++ b/packages/spiderable/phantom_script.js @@ -1,22 +1,26 @@ // 'url' is assigned to in a statement before this. var page = require('webpage').create(); -page.open(url); -setInterval(function() { - var ready = page.evaluate(function () { - if (typeof Meteor !== 'undefined' - && typeof(Meteor.status) !== 'undefined' - && Meteor.status().connected) { - Deps.flush(); - return DDP._allSubscriptionsReady(); - } - return false; - }); - if (ready) { - var out = page.content; - out = out.replace(/]+>(.|\n|\r)*?<\/script\s*>/ig, ''); - out = out.replace('', ''); - console.log(out); +page.open(url, function(status) { + if (status === 'fail') phantom.exit(); - } -}, 100); + setInterval(function() { + var ready = page.evaluate(function () { + if (typeof Meteor !== 'undefined' + && typeof(Meteor.status) !== 'undefined' + && Meteor.status().connected) { + Deps.flush(); + return DDP._allSubscriptionsReady(); + } + return false; + }); + if (ready) { + var out = page.content; + out = out.replace(/]+>(.|\n|\r)*?<\/script\s*>/ig, ''); + out = out.replace('', ''); + console.log(out); + phantom.exit(); + } + }, 100); +}); + From 4b7adf83973c221eaefdaff76c735fb199263154 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 1 Aug 2014 17:04:24 -0700 Subject: [PATCH 65/69] Make events documentation more clear Fixes #2335. See also Issue #2202. --- docs/client/api.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 876e0b877f..f1ac161bb2 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2304,8 +2304,8 @@ Example: // Fires when any element with the 'accept' class is clicked 'click .accept': function (event) { ... }, - // Fires when 'accept' is clicked, or a key is pressed - 'keydown, click .accept': function (event) { ... } + // Fires when 'accept' is clicked or focused, or a key is pressed + 'click .accept, focus .accept, keypress': function (event) { ... } } Most events bubble up the document tree from their originating From 5ff811731050f7718a08b7199baa120e75c42e5b Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 4 Aug 2014 19:06:15 -0700 Subject: [PATCH 66/69] Update comments after looking into e7250a0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It seems that when ObserveSequence observed a Cursor, it used to stop() the observe when the main autorun was invalidated, creating a “gap” during which no callbacks would be received (or fired). This was before we used Deps.nonreactive in the main autorun (shielding the cursor.observe from being stopped). Now the observe is only stopped upon re-run or when the ObserveSequence is stopped, and there is no gap. Removed references to this gap from comments. I believe the current code is correct, and in addition, we could now optimize the cursor-to-same-cursor case (and basically not do anything if seq===lastSeq and is a Cursor, i.e. not stop the old handle, create a new handle, or diff). --- packages/observe-sequence/observe_sequence.js | 5 ++--- packages/observe-sequence/observe_sequence_tests.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index b736ff88ef..b9273f2ab4 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -82,10 +82,9 @@ ObserveSequence = { Deps.nonreactive(function () { var seqArray; // same structure as `lastSeqArray` above. - // If we were previously observing a cursor, replace lastSeqArray with - // more up-to-date information (specifically, the state of the observe - // before it was stopped, which may be older than the DB). if (activeObserveHandle) { + // If we were previously observing a cursor, replace lastSeqArray with + // more up-to-date information. Then stop the old observe. lastSeqArray = _.map(lastSeq.fetch(), function (doc) { return {_id: doc._id, item: doc}; }); diff --git a/packages/observe-sequence/observe_sequence_tests.js b/packages/observe-sequence/observe_sequence_tests.js index 340de1179c..5fdb311989 100644 --- a/packages/observe-sequence/observe_sequence_tests.js +++ b/packages/observe-sequence/observe_sequence_tests.js @@ -439,9 +439,8 @@ Tinytest.add('observe-sequence - cursor to same cursor', function (test) { }, [ {addedAt: ["13", {_id: "13", rank: 1}, 0, null]}, {addedAt: ["24", {_id: "24", rank: 2}, 1, null]}, - // even if the cursor changes to the same cursor, we diff to see if we - // missed anything during the invalidation, which leads to these - // 'changedAt' events. + // even if the cursor changes to the same cursor, we do a diff, + // which leads to these 'changedAt' events. {changedAt: ["13", {_id: "13", rank: 1}, {_id: "13", rank: 1}, 0]}, {changedAt: ["24", {_id: "24", rank: 2}, {_id: "24", rank: 2}, 1]}, {addedAt: ["78", {_id: "78", rank: 3}, 2, null]} From 835eb66377c01f5dfbe247e56800e5f79978376f Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 5 Aug 2014 12:56:07 -0700 Subject: [PATCH 67/69] a tiny bit more info to help debug an error --- packages/livedata/stream_client_nodejs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/livedata/stream_client_nodejs.js b/packages/livedata/stream_client_nodejs.js index 718ee3fe41..a473fc5059 100644 --- a/packages/livedata/stream_client_nodejs.js +++ b/packages/livedata/stream_client_nodejs.js @@ -54,7 +54,7 @@ _.extend(LivedataTest.ClientStream.prototype, { // But _launchConnection calls _cleanup which closes previous connections. // It's our belief that this stifles future 'open' events, but maybe // we are wrong? - throw new Error("Got open from inactive client"); + throw new Error("Got open from inactive client " + !!self.client); } if (self._forcedToDisconnect) { From af2387808a32cacb9f137d62796944807e5bc066 Mon Sep 17 00:00:00 2001 From: Sashko Stubailo Date: Tue, 5 Aug 2014 13:29:38 -0700 Subject: [PATCH 68/69] Updated skeleton app with a reactive example and changed code style --- tools/skel/~name~.html | 7 ++++--- tools/skel/~name~.js | 18 +++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tools/skel/~name~.html b/tools/skel/~name~.html index 8a6749bfed..ce4a85e82d 100644 --- a/tools/skel/~name~.html +++ b/tools/skel/~name~.html @@ -3,11 +3,12 @@ +

Welcome to Meteor!

+ {{> hello}} diff --git a/tools/skel/~name~.js b/tools/skel/~name~.js index 50433270a2..f8bee6b4ce 100644 --- a/tools/skel/~name~.js +++ b/tools/skel/~name~.js @@ -1,13 +1,17 @@ if (Meteor.isClient) { - Template.hello.greeting = function () { - return "Welcome to ~name~."; - }; + // counter starts at 0 + Session.setDefault("counter", 0); + + Template.hello.helpers({ + counter: function () { + return Session.get("counter"); + } + }); Template.hello.events({ - 'click input': function () { - // template data, if any, is available in 'this' - if (typeof console !== 'undefined') - console.log("You pressed the button"); + 'click button': function () { + // increment the counter when button is clicked + Session.set("counter", Session.get("counter") + 1); } }); } From 3d75f53149183d0ec60ccdf33faf75fcb8d6729c Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 5 Aug 2014 14:50:21 -0700 Subject: [PATCH 69/69] Client sync queue: better logic around setTimeout In the old code, if you queued a task, did something which flushes the queue (runTask, flush, or drain), and then queued another task, you'd end up with two pending timeouts, rather than zero. With this change, we're careful to never have two pending timeouts, and also to clear the timeout if we're about to run all current tasks. --- packages/meteor/fiber_stubs_client.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js index 3babd0e08f..c11118d0a6 100644 --- a/packages/meteor/fiber_stubs_client.js +++ b/packages/meteor/fiber_stubs_client.js @@ -14,6 +14,7 @@ Meteor._SynchronousQueue = function () { var self = this; self._tasks = []; self._running = false; + self._runTimeout = null; }; _.extend(Meteor._SynchronousQueue.prototype, { @@ -25,6 +26,15 @@ _.extend(Meteor._SynchronousQueue.prototype, { var tasks = self._tasks; self._tasks = []; self._running = true; + + if (self._runTimeout) { + // Since we're going to drain the queue, we can forget about the timeout + // which tries to run it. (But if one of our tasks queues something else, + // the timeout will be correctly re-created.) + clearTimeout(self._runTimeout); + self._runTimeout = null; + } + try { while (!_.isEmpty(tasks)) { var t = tasks.shift(); @@ -47,12 +57,12 @@ _.extend(Meteor._SynchronousQueue.prototype, { queueTask: function (task) { var self = this; - var wasEmpty = _.isEmpty(self._tasks); self._tasks.push(task); // Intentionally not using Meteor.setTimeout, because it doesn't like runing // in stubs for now. - if (wasEmpty) - setTimeout(_.bind(self.flush, self), 0); + if (!self._runTimeout) { + self._runTimeout = setTimeout(_.bind(self.flush, self), 0); + } }, flush: function () {