Merge remote-tracking branch 'origin/fibers-optional-no-tla-pkgs' into fibers-optional-no-tla-pkgs

# Conflicts:
#	packages/meteor/fiber_helpers.js
This commit is contained in:
Matheus Castro
2022-11-11 18:55:47 -03:00
64 changed files with 10253 additions and 268 deletions

View File

@@ -15,7 +15,7 @@ var packageJson = {
"node-gyp": "8.0.0",
"node-pre-gyp": "0.15.0",
typescript: "4.5.4",
"@meteorjs/babel": "7.16.0-beta.1",
"@meteorjs/babel": "7.16.0-beta.7",
// Keep the versions of these packages consistent with the versions
// found in dev-bundle-server-package.js.
"meteor-promise": "0.9.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@meteorjs/babel",
"version": "7.16.0-beta.1",
"version": "7.16.0-beta.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@meteorjs/babel",
"author": "Meteor <dev@meteor.com>",
"version": "7.16.0-beta.1",
"version": "7.16.0-beta.7",
"license": "MIT",
"description": "Babel wrapper package for use with Meteor",
"keywords": [
@@ -37,7 +37,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.16.8",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-react": "^7.16.7",
"@babel/runtime": "^7.17.2",
"@babel/runtime": "7.17.2",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",

View File

@@ -11,7 +11,7 @@ Module.prototype.resolve = function (id) {
require("@meteorjs/reify/lib/runtime").enable(Module.prototype);
if (!!!process.env.DISABLE_FIBERS) {
if (!process.env.DISABLE_FIBERS) {
require("meteor-promise").makeCompatible(
global.Promise = global.Promise ||
require("promise/lib/es6-extensions"),

View File

@@ -24,11 +24,8 @@ Package.onUse(api => {
// need this because of the Meteor.users collection but in the future
// we'd probably want to abstract this away
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', ['client', 'server']);
} else {
api.use('mongo-async', ['client', 'server']);
}
api.use('mongo', ['client', 'server']);
// If the 'blaze' package is loaded, we'll define some helpers like
// {{currentUser}}. If not, no biggie.
api.use('blaze@2.5.0', 'client', { weak: true });

View File

@@ -1025,7 +1025,8 @@ Accounts.createUserAsync = async (options, callback) => {
// method calling Accounts.createUser could set?
//
Accounts.createUser = (options, callback) => {
Accounts.createUser = !Meteor._isFibersEnabled ? Accounts.createUserAsync
: (options, callback) => {
return Promise.await(Accounts.createUserAsync(options, callback));
};

View File

@@ -5,7 +5,7 @@ Package.describe({
});
Npm.depends({
'@meteorjs/babel': '7.16.0-beta.1',
'@meteorjs/babel': '7.16.0-beta.7',
'json5': '2.1.1'
});

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

View File

@@ -0,0 +1,25 @@
{
"lockfileVersion": 1,
"dependencies": {
"@sinonjs/commons": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
"integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ=="
},
"@sinonjs/fake-timers": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.0.5.tgz",
"integrity": "sha512-fUt6b15bjV/VW93UP5opNXJxdwZSbK1EdiwnhN7XrQrcpaOhMJpZ/CjwFpM3THpxwA+YviBUJKSuEqKlCK5alw=="
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
}
}
}

View File

@@ -0,0 +1,4 @@
# ddp-client
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/ddp-client) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/ddp-client)
***

View File

@@ -0,0 +1,6 @@
export { DDP } from '../common/namespace.js';
import '../common/livedata_connection';
// Initialize the default server connection and put it on Meteor.connection
import './client_convenience';

View File

@@ -0,0 +1,59 @@
import { DDP } from '../common/namespace.js';
import { Meteor } from 'meteor/meteor';
// Meteor.refresh can be called on the client (if you're in common code) but it
// only has an effect on the server.
Meteor.refresh = () => {};
// By default, try to connect back to the same endpoint as the page
// was served from.
//
// XXX We should be doing this a different way. Right now we don't
// include ROOT_URL_PATH_PREFIX when computing ddpUrl. (We don't
// include it on the server when computing
// DDP_DEFAULT_CONNECTION_URL, and we don't include it in our
// default, '/'.) We get by with this because DDP.connect then
// forces the URL passed to it to be interpreted relative to the
// app's deploy path, even if it is absolute. Instead, we should
// make DDP_DEFAULT_CONNECTION_URL, if set, include the path prefix;
// make the default ddpUrl be '' rather that '/'; and make
// _translateUrl in stream_client_common.js not force absolute paths
// to be treated like relative paths. See also
// stream_client_common.js #RationalizingRelativeDDPURLs
const runtimeConfig = typeof __meteor_runtime_config__ !== 'undefined' ? __meteor_runtime_config__ : Object.create(null);
const ddpUrl = runtimeConfig.DDP_DEFAULT_CONNECTION_URL || '/';
const retry = new Retry();
function onDDPVersionNegotiationFailure(description) {
Meteor._debug(description);
if (Package.reload) {
const migrationData = Package.reload.Reload._migrationData('livedata') || Object.create(null);
let failures = migrationData.DDPVersionNegotiationFailures || 0;
++failures;
Package.reload.Reload._onMigrate('livedata', () => [true, { DDPVersionNegotiationFailures: failures }]);
retry.retryLater(failures, () => {
Package.reload.Reload._reload({ immediateMigration: true });
});
}
}
Meteor.connection = DDP.connect(ddpUrl, {
onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure
});
// Proxy the public methods of Meteor.connection so they can
// be called directly on Meteor.
[
'subscribe',
'methods',
'call',
'callAsync',
'apply',
'applyAsync',
'status',
'reconnect',
'disconnect'
].forEach(name => {
Meteor[name] = Meteor.connection[name].bind(Meteor.connection);
});

View File

@@ -0,0 +1,85 @@
// A MethodInvoker manages sending a method to the server and calling the user's
// callbacks. On construction, it registers itself in the connection's
// _methodInvokers map; it removes itself once the method is fully finished and
// the callback is invoked. This occurs when it has both received a result,
// and the data written by it is fully visible.
export default class MethodInvoker {
constructor(options) {
// Public (within this file) fields.
this.methodId = options.methodId;
this.sentMessage = false;
this._callback = options.callback;
this._connection = options.connection;
this._message = options.message;
this._onResultReceived = options.onResultReceived || (() => {});
this._wait = options.wait;
this.noRetry = options.noRetry;
this._methodResult = null;
this._dataVisible = false;
// Register with the connection.
this._connection._methodInvokers[this.methodId] = this;
}
// Sends the method message to the server. May be called additional times if
// we lose the connection and reconnect before receiving a result.
sendMessage() {
// This function is called before sending a method (including resending on
// reconnect). We should only (re)send methods where we don't already have a
// result!
if (this.gotResult())
throw new Error('sendingMethod is called on method with result');
// If we're re-sending it, it doesn't matter if data was written the first
// time.
this._dataVisible = false;
this.sentMessage = true;
// If this is a wait method, make all data messages be buffered until it is
// done.
if (this._wait)
this._connection._methodsBlockingQuiescence[this.methodId] = true;
// Actually send the message.
this._connection._send(this._message);
}
// Invoke the callback, if we have both a result and know that all data has
// been written to the local cache.
_maybeInvokeCallback() {
if (this._methodResult && this._dataVisible) {
// Call the callback. (This won't throw: the callback was wrapped with
// bindEnvironment.)
this._callback(this._methodResult[0], this._methodResult[1]);
// Forget about this method.
delete this._connection._methodInvokers[this.methodId];
// Let the connection know that this method is finished, so it can try to
// move on to the next block of methods.
this._connection._outstandingMethodFinished();
}
}
// Call with the result of the method from the server. Only may be called
// once; once it is called, you should not call sendMessage again.
// If the user provided an onResultReceived callback, call it immediately.
// Then invoke the main callback if data is also visible.
receiveResult(err, result) {
if (this.gotResult())
throw new Error('Methods should only receive results once');
this._methodResult = [err, result];
this._onResultReceived(err, result);
this._maybeInvokeCallback();
}
// Call this when all data written by the method is visible. This means that
// the method has returns its "data is done" message *AND* all server
// documents that are buffered at that time have been written to the local
// cache. Invokes the main callback if the result has been received.
dataVisible() {
this._dataVisible = true;
this._maybeInvokeCallback();
}
// True if receiveResult has been called.
gotResult() {
return !!this._methodResult;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
import { DDPCommon } from 'meteor/ddp-common';
import { Meteor } from 'meteor/meteor';
import { Connection } from './livedata_connection.js';
// This array allows the `_allSubscriptionsReady` method below, which
// is used by the `spiderable` package, to keep track of whether all
// data is ready.
const allConnections = [];
/**
* @namespace DDP
* @summary Namespace for DDP-related methods/classes.
*/
export const DDP = {};
// This is private but it's used in a few places. accounts-base uses
// it to get the current user. Meteor.setTimeout and friends clear
// it. We can probably find a better way to factor this.
DDP._CurrentMethodInvocation = new Meteor.EnvironmentVariable();
DDP._CurrentPublicationInvocation = new Meteor.EnvironmentVariable();
// XXX: Keep DDP._CurrentInvocation for backwards-compatibility.
DDP._CurrentInvocation = DDP._CurrentMethodInvocation;
// This is passed into a weird `makeErrorType` function that expects its thing
// to be a constructor
function connectionErrorConstructor(message) {
this.message = message;
}
DDP.ConnectionError = Meteor.makeErrorType(
'DDP.ConnectionError',
connectionErrorConstructor
);
DDP.ForcedReconnectError = Meteor.makeErrorType(
'DDP.ForcedReconnectError',
() => {}
);
// Returns the named sequence of pseudo-random values.
// The scope will be DDP._CurrentMethodInvocation.get(), so the stream will produce
// consistent values for method calls on the client and server.
DDP.randomStream = name => {
const scope = DDP._CurrentMethodInvocation.get();
return DDPCommon.RandomStream.get(scope, name);
};
// @param url {String} URL to Meteor app,
// e.g.:
// "subdomain.meteor.com",
// "http://subdomain.meteor.com",
// "/",
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs"
/**
* @summary Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods.
* @locus Anywhere
* @param {String} url The URL of another Meteor application.
* @param {Object} [options]
* @param {Boolean} options.reloadWithOutstanding is it OK to reload if there are outstanding methods?
* @param {Object} options.headers extra headers to send on the websockets connection, for server-to-server DDP only
* @param {Object} options._sockjsOptions Specifies options to pass through to the sockjs client
* @param {Function} options.onDDPNegotiationVersionFailure callback when version negotiation fails.
*/
DDP.connect = (url, options) => {
const ret = new Connection(url, options);
allConnections.push(ret); // hack. see below.
return ret;
};
DDP._reconnectHook = new Hook({ bindEnvironment: false });
/**
* @summary Register a function to call as the first step of
* reconnecting. This function can call methods which will be executed before
* any other outstanding methods. For example, this can be used to re-establish
* the appropriate authentication context on the connection.
* @locus Anywhere
* @param {Function} callback The function to call. It will be called with a
* single argument, the [connection object](#ddp_connect) that is reconnecting.
*/
DDP.onReconnect = callback => DDP._reconnectHook.register(callback);
// Hack for `spiderable` package: a way to see if the page is done
// loading all the data it needs.
//
DDP._allSubscriptionsReady = () => allConnections.every(
conn => Object.values(conn._subscriptions).every(sub => sub.ready)
);

View File

@@ -0,0 +1,63 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data client",
version: '2.6.0',
documentation: null
});
Npm.depends({
'@sinonjs/fake-timers': '7.0.5'
});
Package.onUse((api) => {
api.use([
'check',
'random',
'ejson',
'tracker',
'retry',
'id-map',
'ecmascript',
'callback-hook',
'ddp-common',
'reload',
'socket-stream-client',
// we depend on _diffObjects, _applyChanges,
'diff-sequence',
// _idParse, _idStringify.
'mongo-id'
], ['client', 'server']);
api.use('reload', 'client', { weak: true });
// For backcompat where things use Package.ddp.DDP, etc
api.export('DDP');
api.mainModule('client/client.js', 'client');
api.mainModule('server/server.js', 'server');
});
Package.onTest((api) => {
api.use([
'livedata',
'mongo',
'test-helpers',
'ecmascript',
'underscore',
'tinytest',
'random',
'tracker',
'reactive-var',
'mongo-id',
'diff-sequence',
'ejson',
'ddp-common',
'check'
]);
api.addFiles('test/stub_stream.js');
api.addFiles('test/livedata_connection_tests.js');
api.addFiles('test/livedata_tests.js');
api.addFiles('test/livedata_test_service.js');
api.addFiles('test/random_stream_tests.js');
});

View File

@@ -0,0 +1 @@
export { DDP } from '../common/namespace.js';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,376 @@
Meteor.methods({
nothing: function() {
// No need to check if there are no arguments.
},
echo: function(/* arguments */) {
check(arguments, [Match.Any]);
return _.toArray(arguments);
},
echoOne: function(/*arguments*/) {
check(arguments, [Match.Any]);
return arguments[0];
},
exception: function(where, options) {
check(where, String);
check(
options,
Match.Optional({
intended: Match.Optional(Boolean),
throwThroughFuture: Match.Optional(Boolean)
})
);
options = options || Object.create(null);
const shouldThrow =
(Meteor.isServer && where === 'server') ||
(Meteor.isClient && where === 'client') ||
where === 'both';
if (shouldThrow) {
let e;
if (options.intended)
e = new Meteor.Error(999, 'Client-visible test exception');
else e = new Error('Test method throwing an exception');
e._expectedByTest = true;
// We used to improperly serialize errors that were thrown through a
// future first.
if (Meteor.isServer && options.throwThroughFuture) {
const Future = Npm.require('fibers/future');
const f = new Future();
f['throw'](e);
e = f.wait();
}
throw e;
}
},
setUserId: function(userId) {
check(userId, Match.OneOf(String, null));
this.setUserId(userId);
}
});
// Methods to help test applying methods with `wait: true`: delayedTrue returns
// true 1s after being run unless makeDelayedTrueImmediatelyReturnFalse was run
// in the meanwhile. Increasing the timeout makes the "wait: true" test slower;
// decreasing the timeout makes the "wait: false" test flakier (ie, the timeout
// could fire before processing the second method).
if (Meteor.isServer) {
// Keys are random tokens, used to isolate multiple test invocations from each
// other.
const waiters = Object.create(null);
const Future = Npm.require('fibers/future');
const returnThroughFuture = function(token, returnValue) {
// Make sure that when we call return, the fields are already cleared.
const record = waiters[token];
if (!record) return;
delete waiters[token];
record.future['return'](returnValue);
};
Meteor.methods({
delayedTrue: function(token) {
check(token, String);
const record = (waiters[token] = {
future: new Future(),
timer: Meteor.setTimeout(function() {
returnThroughFuture(token, true);
}, 1000)
});
this.unblock();
return record.future.wait();
},
makeDelayedTrueImmediatelyReturnFalse: function(token) {
check(token, String);
const record = waiters[token];
if (!record) return; // since delayedTrue's timeout had already run
clearTimeout(record.timer);
returnThroughFuture(token, false);
}
});
}
/*****/
Ledger = new Mongo.Collection('ledger');
Ledger.allow({
insert: function() {
return true;
},
update: function() {
return true;
},
remove: function() {
return true;
},
fetch: []
});
Meteor.startup(function() {
if (Meteor.isServer) Ledger.remove({}); // XXX can this please be Ledger.remove()?
});
if (Meteor.isServer)
Meteor.publish('ledger', function(world) {
check(world, String);
return Ledger.find({ world: world });
});
Meteor.methods({
'ledger/transfer': function(world, from_name, to_name, amount, cheat) {
check(world, String);
check(from_name, String);
check(to_name, String);
check(amount, Number);
check(cheat, Match.Optional(Boolean));
const from = Ledger.findOne({ name: from_name, world: world });
const to = Ledger.findOne({ name: to_name, world: world });
if (Meteor.isServer) cheat = false;
if (!from)
throw new Meteor.Error(
404,
'No such account ' + from_name + ' in ' + world
);
if (!to)
throw new Meteor.Error(
404,
'No such account ' + to_name + ' in ' + world
);
if (from.balance < amount && !cheat)
throw new Meteor.Error(409, 'Insufficient funds');
Ledger.update(from._id, { $inc: { balance: -amount } });
Ledger.update(to._id, { $inc: { balance: amount } });
}
});
/*****/
/// Helpers for "livedata - changing userid reruns subscriptions..."
objectsWithUsers = new Mongo.Collection('objectsWithUsers');
if (Meteor.isServer) {
objectsWithUsers.remove({});
objectsWithUsers.insert({ name: 'owned by none', ownerUserIds: [null] });
objectsWithUsers.insert({ name: 'owned by one - a', ownerUserIds: ['1'] });
objectsWithUsers.insert({
name: 'owned by one/two - a',
ownerUserIds: ['1', '2']
});
objectsWithUsers.insert({
name: 'owned by one/two - b',
ownerUserIds: ['1', '2']
});
objectsWithUsers.insert({ name: 'owned by two - a', ownerUserIds: ['2'] });
objectsWithUsers.insert({ name: 'owned by two - b', ownerUserIds: ['2'] });
Meteor.publish('objectsWithUsers', function() {
return objectsWithUsers.find(
{ ownerUserIds: this.userId },
{ fields: { ownerUserIds: 0 } }
);
});
(function() {
const userIdWhenStopped = Object.create(null);
Meteor.publish('recordUserIdOnStop', function(key) {
check(key, String);
const self = this;
self.onStop(function() {
userIdWhenStopped[key] = self.userId;
});
});
Meteor.methods({
userIdWhenStopped: function(key) {
check(key, String);
return userIdWhenStopped[key];
}
});
})();
}
/*****/
/// Helper for "livedata - setUserId fails when called on server"
if (Meteor.isServer) {
Meteor.startup(function() {
errorThrownWhenCallingSetUserIdDirectlyOnServer = null;
try {
Meteor.call('setUserId', '1000');
} catch (e) {
errorThrownWhenCallingSetUserIdDirectlyOnServer = e;
}
});
}
/// Helper for "livedata - no setUserId after unblock"
if (Meteor.isServer) {
Meteor.methods({
setUserIdAfterUnblock: function() {
this.unblock();
let threw = false;
const originalUserId = this.userId;
try {
// Calling setUserId after unblock should throw an error (and not mutate
// userId).
this.setUserId(originalUserId + 'bla');
} catch (e) {
threw = true;
}
return threw && this.userId === originalUserId;
}
});
}
/*****/
/// Helper for "livedata - overlapping universal subs"
if (Meteor.isServer) {
(function() {
const collName = 'overlappingUniversalSubs';
const universalSubscribers = [[], []];
_.each([0, 1], function(index) {
Meteor.publish(null, function() {
const sub = this;
universalSubscribers[index].push(sub);
sub.onStop(function() {
universalSubscribers[index] = _.without(
universalSubscribers[index],
sub
);
});
});
});
Meteor.methods({
testOverlappingSubs: function(token) {
check(token, String);
_.each(universalSubscribers[0], function(sub) {
sub.added(collName, token, {});
});
_.each(universalSubscribers[1], function(sub) {
sub.added(collName, token, {});
});
_.each(universalSubscribers[0], function(sub) {
sub.removed(collName, token);
});
}
});
})();
}
/// Helper for "livedata - runtime universal sub creation"
if (Meteor.isServer) {
Meteor.methods({
runtimeUniversalSubCreation: function(token) {
check(token, String);
Meteor.publish(null, function() {
this.added('runtimeSubCreation', token, {});
});
}
});
}
/// Helper for "livedata - publisher errors"
if (Meteor.isServer) {
Meteor.publish('publisherErrors', function(collName, options) {
check(collName, String);
// See below to see what options are accepted.
check(options, Object);
const sub = this;
// First add a random item, which should be cleaned up. We use ready/onReady
// to make sure that the second test block is only called after the added is
// processed, so that there's any chance of the coll.find().count() failing.
sub.added(collName, Random.id(), { foo: 42 });
sub.ready();
if (options.stopInHandler) {
sub.stop();
return;
}
let error;
if (options.internalError) {
error = new Error('Egads!');
error._expectedByTest = true; // don't log
} else {
error = new Meteor.Error(412, 'Explicit error');
}
if (options.throwInHandler) {
throw error;
} else if (options.errorInHandler) {
sub.error(error);
} else if (options.throwWhenUserIdSet) {
if (sub.userId) throw error;
} else if (options.errorLater) {
Meteor.defer(function() {
sub.error(error);
});
}
});
}
/*****/
/// Helpers for "livedata - publish multiple cursors"
One = new Mongo.Collection('collectionOne');
Two = new Mongo.Collection('collectionTwo');
if (Meteor.isServer) {
One.remove({});
One.insert({ name: 'value1' });
One.insert({ name: 'value2' });
Two.remove({});
Two.insert({ name: 'value3' });
Two.insert({ name: 'value4' });
Two.insert({ name: 'value5' });
Meteor.publish('multiPublish', function(options) {
// See below to see what options are accepted.
check(options, Object);
if (options.normal) {
return [One.find(), Two.find()];
} else if (options.dup) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [
One.find(),
One.find({ name: 'value2' }), // multiple cursors for one collection - error
Two.find()
];
} else if (options.notCursor) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [One.find(), 'not a cursor', Two.find()];
} else throw 'unexpected options';
});
}
/// Helper for "livedata - result by value"
const resultByValueArrays = Object.create(null);
Meteor.methods({
getArray: function(testId) {
if (!_.has(resultByValueArrays, testId)) resultByValueArrays[testId] = [];
return resultByValueArrays[testId];
},
pushToArray: function(testId, value) {
if (!_.has(resultByValueArrays, testId)) resultByValueArrays[testId] = [];
resultByValueArrays[testId].push(value);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
Tinytest.add('livedata - DDP.randomStream', function(test) {
const randomSeed = Random.id();
const context = { randomSeed: randomSeed };
let sequence = DDP._CurrentMethodInvocation.withValue(context, function() {
return DDP.randomStream('1');
});
let seeds = sequence.alea.args;
test.equal(seeds.length, 2);
test.equal(seeds[0], randomSeed);
test.equal(seeds[1], '1');
const id1 = sequence.id();
// Clone the sequence by building it the same way RandomStream.get does
const sequenceClone = Random.createWithSeeds.apply(null, seeds);
const id1Cloned = sequenceClone.id();
const id2Cloned = sequenceClone.id();
test.equal(id1, id1Cloned);
// We should get the same sequence when we use the same key
sequence = DDP._CurrentMethodInvocation.withValue(context, function() {
return DDP.randomStream('1');
});
seeds = sequence.alea.args;
test.equal(seeds.length, 2);
test.equal(seeds[0], randomSeed);
test.equal(seeds[1], '1');
// But we should be at the 'next' position in the stream
const id2 = sequence.id();
// Technically these could be equal, but likely to be a bug if hit
// http://search.dilbert.com/comic/Random%20Number%20Generator
test.notEqual(id1, id2);
test.equal(id2, id2Cloned);
});
Tinytest.add('livedata - DDP.randomStream with no-args', function(test) {
DDP.randomStream().id();
});

View File

@@ -0,0 +1,57 @@
StubStream = function() {
const self = this;
self.sent = [];
self.callbacks = Object.create(null);
};
_.extend(StubStream.prototype, {
// Methods from Stream
on: function(name, callback) {
const self = this;
if (!self.callbacks[name]) self.callbacks[name] = [callback];
else self.callbacks[name].push(callback);
},
send: function(data) {
const self = this;
self.sent.push(data);
},
status: function() {
return { status: 'connected', fake: true };
},
reconnect: function() {
// no-op
},
_lostConnection: function() {
// no-op
},
// Methods for tests
receive: function(data) {
const self = this;
if (typeof data === 'object') {
data = EJSON.stringify(data);
}
_.each(self.callbacks['message'], function(cb) {
cb(data);
});
},
reset: function() {
const self = this;
_.each(self.callbacks['reset'], function(cb) {
cb();
});
},
// Provide a tag to detect stub streams.
// We don't log heartbeat failures on stub streams, for example.
_isStub: true
});

View File

@@ -9,6 +9,11 @@ Npm.depends({
});
Package.onUse((api) => {
if (process.env.DISABLE_FIBERS) {
api.use('ddp-client-async');
api.export('DDP', 'server');
return;
}
api.use([
'check',
'random',
@@ -40,6 +45,7 @@ Package.onUse((api) => {
Package.onTest((api) => {
api.use([
'livedata',
'mongo',
'test-helpers',
'ecmascript',
'underscore',
@@ -53,11 +59,6 @@ Package.onTest((api) => {
'ddp-common',
'check'
]);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', ['client', 'server']);
} else {
api.use('mongo-async', ['client', 'server']);
}
api.addFiles('test/stub_stream.js');
api.addFiles('test/livedata_connection_tests.js');

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

View File

@@ -0,0 +1,45 @@
{
"lockfileVersion": 1,
"dependencies": {
"faye-websocket": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz",
"integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA=="
},
"http-parser-js": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz",
"integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg=="
},
"permessage-deflate": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/permessage-deflate/-/permessage-deflate-0.1.7.tgz",
"integrity": "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"sockjs": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
"integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw=="
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="
},
"websocket-extensions": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="
}
}
}

View File

@@ -0,0 +1,4 @@
# ddp-server
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/ddp-server) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/ddp-server)
***

View File

@@ -0,0 +1,167 @@
// A "crossbar" is a class that provides structured notification registration.
// See _match for the definition of how a notification matches a trigger.
// All notifications and triggers must have a string key named 'collection'.
DDPServer._Crossbar = function (options) {
var self = this;
options = options || {};
self.nextId = 1;
// map from collection name (string) -> listener id -> object. each object has
// keys 'trigger', 'callback'. As a hack, the empty string means "no
// collection".
self.listenersByCollection = {};
self.listenersByCollectionCount = {};
self.factPackage = options.factPackage || "livedata";
self.factName = options.factName || null;
};
_.extend(DDPServer._Crossbar.prototype, {
// msg is a trigger or a notification
_collectionForMessage: function (msg) {
var self = this;
if (! _.has(msg, 'collection')) {
return '';
} else if (typeof(msg.collection) === 'string') {
if (msg.collection === '')
throw Error("Message has empty collection!");
return msg.collection;
} else {
throw Error("Message has non-string collection!");
}
},
// Listen for notification that match 'trigger'. A notification
// matches if it has the key-value pairs in trigger as a
// subset. When a notification matches, call 'callback', passing
// the actual notification.
//
// Returns a listen handle, which is an object with a method
// stop(). Call stop() to stop listening.
//
// XXX It should be legal to call fire() from inside a listen()
// callback?
listen: function (trigger, callback) {
var self = this;
var id = self.nextId++;
var collection = self._collectionForMessage(trigger);
var record = {trigger: EJSON.clone(trigger), callback: callback};
if (! _.has(self.listenersByCollection, collection)) {
self.listenersByCollection[collection] = {};
self.listenersByCollectionCount[collection] = 0;
}
self.listenersByCollection[collection][id] = record;
self.listenersByCollectionCount[collection]++;
if (self.factName && Package['facts-base']) {
Package['facts-base'].Facts.incrementServerFact(
self.factPackage, self.factName, 1);
}
return {
stop: function () {
if (self.factName && Package['facts-base']) {
Package['facts-base'].Facts.incrementServerFact(
self.factPackage, self.factName, -1);
}
delete self.listenersByCollection[collection][id];
self.listenersByCollectionCount[collection]--;
if (self.listenersByCollectionCount[collection] === 0) {
delete self.listenersByCollection[collection];
delete self.listenersByCollectionCount[collection];
}
}
};
},
// Fire the provided 'notification' (an object whose attribute
// values are all JSON-compatibile) -- inform all matching listeners
// (registered with listen()).
//
// If fire() is called inside a write fence, then each of the
// listener callbacks will be called inside the write fence as well.
//
// The listeners may be invoked in parallel, rather than serially.
fire: function (notification) {
var self = this;
var collection = self._collectionForMessage(notification);
if (! _.has(self.listenersByCollection, collection)) {
return;
}
var listenersForCollection = self.listenersByCollection[collection];
var callbackIds = [];
_.each(listenersForCollection, function (l, id) {
if (self._matches(notification, l.trigger)) {
callbackIds.push(id);
}
});
// Listener callbacks can yield, so we need to first find all the ones that
// match in a single iteration over self.listenersByCollection (which can't
// be mutated during this iteration), and then invoke the matching
// callbacks, checking before each call to ensure they haven't stopped.
// Note that we don't have to check that
// self.listenersByCollection[collection] still === listenersForCollection,
// because the only way that stops being true is if listenersForCollection
// first gets reduced down to the empty object (and then never gets
// increased again).
_.each(callbackIds, function (id) {
if (_.has(listenersForCollection, id)) {
listenersForCollection[id].callback(notification);
}
});
},
// A notification matches a trigger if all keys that exist in both are equal.
//
// Examples:
// N:{collection: "C"} matches T:{collection: "C"}
// (a non-targeted write to a collection matches a
// non-targeted query)
// N:{collection: "C", id: "X"} matches T:{collection: "C"}
// (a targeted write to a collection matches a non-targeted query)
// N:{collection: "C"} matches T:{collection: "C", id: "X"}
// (a non-targeted write to a collection matches a
// targeted query)
// N:{collection: "C", id: "X"} matches T:{collection: "C", id: "X"}
// (a targeted write to a collection matches a targeted query targeted
// at the same document)
// N:{collection: "C", id: "X"} does not match T:{collection: "C", id: "Y"}
// (a targeted write to a collection does not match a targeted query
// targeted at a different document)
_matches: function (notification, trigger) {
// Most notifications that use the crossbar have a string `collection` and
// maybe an `id` that is a string or ObjectID. We're already dividing up
// triggers by collection, but let's fast-track "nope, different ID" (and
// avoid the overly generic EJSON.equals). This makes a noticeable
// performance difference; see https://github.com/meteor/meteor/pull/3697
if (typeof(notification.id) === 'string' &&
typeof(trigger.id) === 'string' &&
notification.id !== trigger.id) {
return false;
}
if (notification.id instanceof MongoID.ObjectID &&
trigger.id instanceof MongoID.ObjectID &&
! notification.id.equals(trigger.id)) {
return false;
}
return _.all(trigger, function (triggerValue, key) {
return !_.has(notification, key) ||
EJSON.equals(triggerValue, notification[key]);
});
}
});
// The "invalidation crossbar" is a specific instance used by the DDP server to
// implement write fence notifications. Listener callbacks on this crossbar
// should call beginWrite on the current write fence before they return, if they
// want to delay the write fence from firing (ie, the DDP method-data-updated
// message from being sent).
DDPServer._InvalidationCrossbar = new DDPServer._Crossbar({
factName: "invalidation-crossbar-listeners"
});

View File

@@ -0,0 +1,49 @@
// White box tests of invalidation crossbar matching function.
// Note: the current crossbar match function is designed specifically
// to ensure that a modification that targets a specific ID does not
// notify a query that is watching a different specific ID. (And to
// keep separate collections separate.) Other than that, there's no
// deep meaning to the matching function, and it could be changed later
// as long as it preserves that property.
Tinytest.add('livedata - crossbar', function (test) {
var crossbar = new DDPServer._Crossbar;
test.isTrue(crossbar._matches({collection: "C"},
{collection: "C"}));
test.isTrue(crossbar._matches({collection: "C", id: "X"},
{collection: "C"}));
test.isTrue(crossbar._matches({collection: "C"},
{collection: "C", id: "X"}));
test.isTrue(crossbar._matches({collection: "C", id: "X"},
{collection: "C"}));
test.isFalse(crossbar._matches({collection: "C", id: "X"},
{collection: "C", id: "Y"}));
// Test that stopped listens definitely don't fire.
var calledFirst = false;
var calledSecond = false;
var trigger = {collection: "C"};
var secondHandle;
crossbar.listen(trigger, function (notification) {
// This test assumes that listeners will be called in the order
// registered. It's not wrong for the crossbar to do something different,
// but the test won't be valid in that case, so make it fail so we notice.
calledFirst = true;
if (calledSecond) {
test.fail({
type: "test_assumption_failed",
message: "test assumed that listeners would be called in the order registered"
});
} else {
secondHandle.stop();
}
});
secondHandle = crossbar.listen(trigger, function (notification) {
// This should not get invoked, because it should be stopped by the other
// listener!
calledSecond = true;
});
crossbar.fire(trigger);
test.isTrue(calledFirst);
test.isFalse(calledSecond);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
var Fiber = Npm.require('fibers');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// connectionId -> callback
var onSubscription = {};
Meteor.publish('livedata_server_test_sub_async', async function(connectionId) {
await sleep(50);
var callback = onSubscription[connectionId];
if (callback) callback(this);
this.stop();
});
Meteor.publish('livedata_server_test_sub_context_async', async function(
connectionId,
userId
) {
await sleep(50);
var callback = onSubscription[connectionId];
var methodInvocation = DDP._CurrentMethodInvocation.get();
var publicationInvocation = DDP._CurrentPublicationInvocation.get();
// Check the publish function's environment variables and context.
if (callback) {
callback.call(this, methodInvocation, publicationInvocation);
}
// Check that onStop callback is have the same context as the publish function
// and that it runs with the same environment variables as this publish function.
this.onStop(function() {
var onStopMethodInvocation = DDP._CurrentMethodInvocation.get();
var onStopPublicationInvocation = DDP._CurrentPublicationInvocation.get();
callback.call(
this,
onStopMethodInvocation,
onStopPublicationInvocation,
true
);
});
if (this.userId) {
this.stop();
} else {
this.ready();
Meteor.call('livedata_server_test_setuserid', userId);
}
});
Tinytest.addAsync(
'livedata server - connection in async publish function',
function(test, onComplete) {
makeTestConnection(test, function(clientConn, serverConn) {
onSubscription[serverConn.id] = function(subscription) {
delete onSubscription[serverConn.id];
test.equal(subscription.connection.id, serverConn.id);
clientConn.disconnect();
onComplete();
};
clientConn.subscribe('livedata_server_test_sub_async', serverConn.id);
});
}
);
Tinytest.addAsync(
'livedata server - verify context in async publish function',
function(test, onComplete) {
makeTestConnection(test, function(clientConn, serverConn) {
var userId = 'someUserId';
onSubscription[serverConn.id] = function(
methodInvocation,
publicationInvocation,
fromOnStop
) {
// DDP._CurrentMethodInvocation should be undefined in a publish function
test.isUndefined(methodInvocation, 'Should have been undefined');
// DDP._CurrentPublicationInvocation should be set in a publish function
test.isNotUndefined(publicationInvocation, 'Should have been defined');
if (this.userId === userId && fromOnStop) {
delete onSubscription[serverConn.id];
clientConn.disconnect();
onComplete();
}
};
clientConn.subscribe(
'livedata_server_test_sub_context_async',
serverConn.id,
userId
);
});
}
);
let onSubscriptions = {};
Meteor.publish({
async publicationObjectAsync() {
await sleep(50);
let callback = onSubscriptions;
if (callback) callback();
this.stop();
},
});
Meteor.publish({
publication_object_async: async function() {
await sleep(50);
let callback = onSubscriptions;
if (callback) callback();
this.stop();
},
});
Meteor.publish('publication_compatibility_async', async function() {
await sleep(50);
let callback = onSubscriptions;
if (callback) callback();
this.stop();
});
Tinytest.addAsync('livedata server - async publish object', function(
test,
onComplete
) {
makeTestConnection(test, function(clientConn, serverConn) {
let testsLength = 0;
onSubscriptions = function(subscription) {
delete onSubscriptions;
clientConn.disconnect();
testsLength++;
if (testsLength == 3) {
onComplete();
}
};
clientConn.subscribe('publicationObjectAsync');
clientConn.subscribe('publication_object_async');
clientConn.subscribe('publication_compatibility_async');
});
});
const collection = new Mongo.Collection('names');
async function getAllNames(shouldThrow = false) {
const count = await collection.rawCollection().count();
if (shouldThrow) {
throw new Meteor.Error('Expected error');
}
if (count <= 0) {
collection.insert({ name: 'async' });
}
}
Meteor.publish('asyncPublishCursor', async function() {
await getAllNames();
return collection.find();
});
Tinytest.addAsync('livedata server - async publish cursor', function(
test,
onComplete
) {
makeTestConnection(test, (clientConn, serverConn) => {
const remoteCollection = new Mongo.Collection('names', {
connection: clientConn,
});
clientConn.subscribe('asyncPublishCursor', () => {
const actual = remoteCollection.find().fetch();
test.equal(actual[0].name, 'async');
onComplete();
});
});
});
Meteor.publish('asyncPublishErrorCursor', async function() {
await getAllNames(true);
return collection.find();
});
Tinytest.addAsync('livedata server - async publish test error thrown', function(
test,
onComplete
) {
makeTestConnection(test, (clientConn, serverConn) => {
clientConn.subscribe('asyncPublishErrorCursor', {
onStop: e => {
test.equal(e.error, 'Expected error');
onComplete();
},
});
});
});

View File

@@ -0,0 +1,411 @@
var Fiber = Npm.require('fibers');
Tinytest.addAsync(
"livedata server - connectionHandle.onClose()",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
// On the server side, wait for the connection to be closed.
serverConn.onClose(function () {
test.isTrue(true);
// Add a new onClose after the connection is already
// closed. See that it fires.
serverConn.onClose(function () {
onComplete();
});
});
// Close the connection from the client.
clientConn.disconnect();
},
onComplete
);
}
);
Tinytest.addAsync(
"livedata server - connectionHandle.close()",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
// Wait for the connection to be closed from the server side.
simplePoll(
function () {
return ! clientConn.status().connected;
},
onComplete,
function () {
test.fail("timeout waiting for the connection to be closed on the server side");
onComplete();
}
);
// Close the connection from the server.
serverConn.close();
},
onComplete
);
}
);
testAsyncMulti(
"livedata server - onConnection doesn't get callback after stop.",
[function (test, expect) {
var afterStop = false;
var expectStop1 = expect();
var stopHandle1 = Meteor.onConnection(function (conn) {
stopHandle2.stop();
stopHandle1.stop();
afterStop = true;
// yield to the event loop for a moment to see that no other calls
// to listener2 are called.
Meteor.setTimeout(expectStop1, 10);
});
var stopHandle2 = Meteor.onConnection(function (conn) {
test.isFalse(afterStop);
});
// trigger a connection
var expectConnection = expect();
makeTestConnection(
test,
function (clientConn, serverConn) {
// Close the connection from the client.
clientConn.disconnect();
expectConnection();
},
expectConnection
);
}]
);
Meteor.methods({
livedata_server_test_inner: function () {
return this.connection && this.connection.id;
},
livedata_server_test_outer: function () {
return Meteor.call('livedata_server_test_inner');
},
livedata_server_test_setuserid: function (userId) {
this.setUserId(userId);
}
});
Tinytest.addAsync(
"livedata server - onMessage hook",
function (test, onComplete) {
var cb = Meteor.onMessage(function (msg, session) {
test.equal(msg.method, 'livedata_server_test_inner');
cb.stop();
onComplete();
});
makeTestConnection(
test,
function (clientConn, serverConn) {
clientConn.call('livedata_server_test_inner');
clientConn.disconnect();
},
onComplete
);
}
);
Tinytest.addAsync(
"livedata server - connection in method invocation",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
var res = clientConn.call('livedata_server_test_inner');
test.equal(res, serverConn.id);
clientConn.disconnect();
onComplete();
},
onComplete
);
}
);
Tinytest.addAsync(
"livedata server - connection in nested method invocation",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
var res = clientConn.call('livedata_server_test_outer');
test.equal(res, serverConn.id);
clientConn.disconnect();
onComplete();
},
onComplete
);
}
);
// connectionId -> callback
var onSubscription = {};
Meteor.publish("livedata_server_test_sub", function (connectionId) {
var callback = onSubscription[connectionId];
if (callback)
callback(this);
this.stop();
});
Meteor.publish("livedata_server_test_sub_method", function (connectionId) {
var callback = onSubscription[connectionId];
if (callback) {
var id = Meteor.call('livedata_server_test_inner');
callback(id);
}
this.stop();
});
Meteor.publish("livedata_server_test_sub_context", function (connectionId, userId) {
var callback = onSubscription[connectionId];
var methodInvocation = DDP._CurrentMethodInvocation.get();
var publicationInvocation = DDP._CurrentPublicationInvocation.get();
// Check the publish function's environment variables and context.
if (callback) {
callback.call(this, methodInvocation, publicationInvocation);
}
// Check that onStop callback is have the same context as the publish function
// and that it runs with the same environment variables as this publish function.
this.onStop(function () {
var onStopMethodInvocation = DDP._CurrentMethodInvocation.get();
var onStopPublicationInvocation = DDP._CurrentPublicationInvocation.get();
callback.call(this, onStopMethodInvocation, onStopPublicationInvocation, true);
});
if (this.userId) {
this.stop();
} else {
this.ready();
Meteor.call('livedata_server_test_setuserid', userId);
}
});
Tinytest.addAsync(
"livedata server - connection in publish function",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
onSubscription[serverConn.id] = function (subscription) {
delete onSubscription[serverConn.id];
test.equal(subscription.connection.id, serverConn.id);
clientConn.disconnect();
onComplete();
};
clientConn.subscribe("livedata_server_test_sub", serverConn.id);
}
);
}
);
Tinytest.addAsync(
"livedata server - connection in method called from publish function",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
onSubscription[serverConn.id] = function (id) {
delete onSubscription[serverConn.id];
test.equal(id, serverConn.id);
clientConn.disconnect();
onComplete();
};
clientConn.subscribe("livedata_server_test_sub_method", serverConn.id);
}
);
}
);
Tinytest.addAsync(
"livedata server - verify context in publish function",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
var userId = 'someUserId';
onSubscription[serverConn.id] = function (methodInvocation, publicationInvocation, fromOnStop) {
// DDP._CurrentMethodInvocation should be undefined in a publish function
test.isUndefined(methodInvocation, 'Should have been undefined');
// DDP._CurrentPublicationInvocation should be set in a publish function
test.isNotUndefined(publicationInvocation, 'Should have been defined');
if (this.userId === userId && fromOnStop) {
delete onSubscription[serverConn.id];
clientConn.disconnect();
onComplete();
}
}
clientConn.subscribe("livedata_server_test_sub_context", serverConn.id, userId);
}
);
}
);
let onSubscriptions = {};
Meteor.publish({
publicationObject () {
let callback = onSubscriptions;
if (callback)
callback();
this.stop();
}
});
Meteor.publish({
"publication_object": function () {
let callback = onSubscriptions;
if (callback)
callback();
this.stop();
}
});
Meteor.publish("publication_compatibility", function () {
let callback = onSubscriptions;
if (callback)
callback();
this.stop();
});
Tinytest.addAsync(
"livedata server - publish object",
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
let testsLength = 0;
onSubscriptions = function (subscription) {
delete onSubscriptions;
clientConn.disconnect();
testsLength++;
if(testsLength == 3){
onComplete();
}
};
clientConn.subscribe("publicationObject");
clientConn.subscribe("publication_object");
clientConn.subscribe("publication_compatibility");
}
);
}
);
Meteor.methods({
testResolvedPromise(arg) {
const invocation1 = DDP._CurrentMethodInvocation.get();
return Promise.resolve(arg).then(result => {
const invocation2 = DDP._CurrentMethodInvocation.get();
// This equality holds because Promise callbacks are bound to the
// dynamic environment where .then was called.
if (invocation1 !== invocation2) {
throw new Meteor.Error("invocation mismatch");
}
return result + " after waiting";
});
},
testRejectedPromise(arg) {
return Promise.resolve(arg).then(result => {
throw new Meteor.Error(result + " raised Meteor.Error");
});
},
testRejectedPromiseWithGenericError(arg) {
return Promise.resolve(arg).then(result => {
const error = new Error('MESSAGE');
error.error = 'ERROR';
error.reason = 'REASON';
error.details = { foo: 'bar' };
error.isClientSafe = true;
throw error;
});
}
});
Tinytest.addAsync(
"livedata server - waiting for Promise",
(test, onComplete) => makeTestConnection(test, (clientConn, serverConn) => {
test.equal(
clientConn.call("testResolvedPromise", "clientConn.call"),
"clientConn.call after waiting"
);
const clientCallPromise = new Promise(
(resolve, reject) => clientConn.call(
"testResolvedPromise",
"clientConn.call with callback",
(error, result) => error ? reject(error) : resolve(result)
)
);
const serverCallAsyncPromise = Meteor.server.callAsync(
"testResolvedPromise",
"Meteor.server.callAsync"
);
const serverApplyAsyncPromise = Meteor.server.applyAsync(
"testResolvedPromise",
["Meteor.server.applyAsync"]
);
const clientCallRejectedPromise = new Promise(resolve => {
clientConn.call(
"testRejectedPromise",
"with callback",
(error, result) => resolve(error.message)
);
});
const clientCallRejectedPromiseWithGenericError = new Promise(resolve => {
clientConn.call(
"testRejectedPromiseWithGenericError",
(error, result) => resolve({
message: error.message,
error: error.error,
reason: error.reason,
details: error.details,
})
);
});
Promise.all([
clientCallPromise,
clientCallRejectedPromise,
clientCallRejectedPromiseWithGenericError,
serverCallAsyncPromise,
serverApplyAsyncPromise
]).then(results => test.equal(results, [
"clientConn.call with callback after waiting",
"[with callback raised Meteor.Error]",
{
message: 'REASON [ERROR]',
error: 'ERROR',
reason: 'REASON',
details: { foo: 'bar' },
},
"Meteor.server.callAsync after waiting",
"Meteor.server.applyAsync after waiting"
]), error => test.fail(error))
.then(onComplete);
})
);

View File

@@ -0,0 +1,61 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data server",
version: '2.6.0',
documentation: null
});
Npm.depends({
"permessage-deflate": "0.1.7",
sockjs: "0.3.21"
});
Package.onUse(function (api) {
api.use(['check', 'random', 'ejson', 'underscore',
'retry', 'mongo-id', 'diff-sequence', 'ecmascript'],
'server');
// common functionality
api.use('ddp-common', 'server'); // heartbeat
api.use('ddp-rate-limiter', 'server', {weak: true});
// Transport
api.use('ddp-client', 'server');
api.imply('ddp-client');
api.use(['webapp', 'routepolicy'], 'server');
// Detect whether or not the user wants us to audit argument checks.
api.use(['audit-argument-checks'], 'server', {weak: true});
// Allow us to detect 'autopublish', so we can print a warning if the user
// runs Meteor.publish while it's loaded.
api.use('autopublish', 'server', {weak: true});
// If the facts package is loaded, publish some statistics.
api.use('facts-base', 'server', {weak: true});
api.use('callback-hook', 'server');
api.export('DDPServer', 'server');
api.addFiles('stream_server.js', 'server');
api.addFiles('livedata_server.js', 'server');
api.addFiles('writefence.js', 'server');
api.addFiles('crossbar.js', 'server');
api.addFiles('server_convenience.js', 'server');
});
Package.onTest(function (api) {
api.use('ecmascript', ['client', 'server']);
api.use('livedata', ['client', 'server']);
api.use('mongo', ['client', 'server']);
api.use('test-helpers', ['client', 'server']);
api.use(['underscore', 'tinytest', 'random', 'tracker', 'minimongo', 'reactive-var']);
api.addFiles('livedata_server_tests.js', 'server');
api.addFiles('livedata_server_async_tests.js', 'server');
api.addFiles('session_view_tests.js', ['server']);
api.addFiles('crossbar_tests.js', ['server']);
});

View File

@@ -0,0 +1,28 @@
if (process.env.DDP_DEFAULT_CONNECTION_URL) {
__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL =
process.env.DDP_DEFAULT_CONNECTION_URL;
}
Meteor.server = new Server;
Meteor.refresh = function (notification) {
DDPServer._InvalidationCrossbar.fire(notification);
};
// Proxy the public methods of Meteor.server so they can
// be called directly on Meteor.
_.each(
[
'publish',
'methods',
'call',
'callAsync',
'apply',
'applyAsync',
'onConnection',
'onMessage',
],
function(name) {
Meteor[name] = _.bind(Meteor.server[name], Meteor.server);
}
);

View File

@@ -0,0 +1,393 @@
var newView = function(test) {
var results = [];
var view = new DDPServer._SessionCollectionView('test', {
added: function (collection, id, fields) {
results.push({fun: 'added', id: id, fields: fields});
},
changed: function (collection, id, changed) {
if (_.isEmpty(changed))
return;
results.push({fun: 'changed', id: id, changed: changed});
},
removed: function (collection, id) {
results.push({fun: 'removed', id: id});
}
});
var v = {
view: view,
results: results
};
_.each(["added", "changed", "removed"], function (it) {
v[it] = _.bind(view[it], view);
});
v.expectResult = function (result) {
test.equal(results.shift(), result);
};
v.expectNoResult = function () {
test.equal(results, []);
};
v.drain = function() {
var ret = results;
results = [];
return ret;
};
return v;
};
Tinytest.add('livedata - sessionview - exists reveal', function (test) {
var v = newView(test);
v.added("A", "A1", {});
v.expectResult({fun: 'added', id: "A1", fields: {}});
v.expectNoResult();
v.added("B", "A1", {});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - added a second field in another sub', function (test) {
var v = newView(test);
v.added("A", "A1", {a: "foo"});
v.expectResult({fun: 'added', id: "A1", fields: {a: "foo"}});
v.expectNoResult();
v.added("B", "A1", {a: "foo", b: "bar"});
v.expectResult({fun: 'changed', 'id': "A1", changed: {b: "bar"}});
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - field reveal', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {foo: "baz"});
v.removed("A", "A1");
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz"}});
v.expectNoResult();
// Somewhere in here we must have changed foo to baz. Legal either on the
// added or on the removed, but only once.
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - field change', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.changed("A", "A1", {foo: "baz"}, []);
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz"}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - field clear', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.changed("A", "A1", {foo: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: undefined}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - change makes a new field', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.changed("A", "A1", {baz:"quux"});
v.expectResult({fun: 'changed', id: "A1", changed: {baz: "quux"}});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - add, remove, add', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - field clear reveal', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {foo: "baz"});
v.changed("A", "A1", {foo: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz"}});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - change to canonical value produces no change', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {foo: "baz"});
var canon = "bar";
var maybeResults = v.drain();
if (!_.isEmpty(maybeResults)) {
// if something happened, it was a change message to baz.
// if nothing did, canon is still bar.
test.length(maybeResults, 1);
test.equal(maybeResults[0], {fun: 'changed', id: "A1", changed: {foo: "baz"}});
canon = "baz";
}
v.changed("B", "A1", {foo: canon});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - new field of canonical value produces no change', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {});
v.changed("B", "A1", {foo: "bar"});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - clear all clears only once', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {foo: "bar"});
v.added("C", "A1", {foo: "bar"});
v.changed("A", "A1", {foo: undefined});
v.changed("B", "A1", {foo: undefined});
v.changed("C", "A1", {foo: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: undefined}});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - change all changes only once', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar"}});
v.expectNoResult();
v.added("B", "A1", {foo: "bar"});
v.added("C", "A1", {foo: "bar"});
v.changed("B", "A1", {foo: "baz"});
v.changed("A", "A1", {foo: "baz"});
v.changed("C", "A1", {foo: "baz"});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz"}});
v.expectNoResult();
v.removed("A", "A1");
v.expectNoResult();
v.removed("B", "A1");
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - multiple operations at once in a change', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar", baz: "quux"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar", baz: "quux"}});
v.expectNoResult();
v.added("B", "A1", {foo: "baz"});
v.changed("A", "A1", {thing: "stuff", foo: undefined, baz: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz", thing: "stuff", baz: undefined}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'changed', id: "A1", changed: {thing: undefined}});
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - more than one document', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar", baz: "quux"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar", baz: "quux"}});
v.expectNoResult();
v.added("A", "A2", {foo: "baz"});
v.expectResult({fun: 'added', id: "A2", fields: {foo: "baz"}});
v.changed("A", "A1", {thing: "stuff", foo: undefined, baz: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {thing: "stuff", foo: undefined, baz: undefined}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
v.removed("A", "A2");
v.expectResult({fun: 'removed', id: "A2"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - multiple docs removed', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar", baz: "quux"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar", baz: "quux"}});
v.expectNoResult();
v.added("A", "A2", {foo: "baz"});
v.expectResult({fun: 'added', id: "A2", fields: {foo: "baz"}});
v.expectNoResult();
v.removed("A", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.removed("A", "A2");
v.expectResult({fun: 'removed', id: "A2"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - complicated sequence', function (test) {
var v = newView(test);
v.added("A", "A1", {foo: "bar", baz: "quux"});
v.expectResult({fun: 'added', id: "A1", fields: {foo: "bar", baz: "quux"}});
v.expectNoResult();
v.added("A", "A2", {foo: "eats"});
v.expectResult({fun: 'added', id: "A2", fields: {foo: "eats"}});
v.added("B", "A1", {foo: "baz"});
v.changed("A", "A1", {thing: "stuff", foo: undefined, baz: undefined});
v.expectResult({fun: 'changed', id: "A1", changed: {foo: "baz", thing: "stuff", baz: undefined}});
v.expectNoResult();
v.removed("A", "A1");
v.removed("A", "A2");
v.expectResult({fun: 'changed', id: "A1", changed: {thing: undefined}});
v.expectResult({fun: 'removed', id: "A2"});
v.expectNoResult();
v.removed("B", "A1");
v.expectResult({fun: 'removed', id: "A1"});
v.expectNoResult();
});
Tinytest.add('livedata - sessionview - added becomes changed', function (test) {
var v = newView(test);
v.added('A', "A1", {foo: 'bar'});
v.expectResult({fun: 'added', id: "A1", fields: {foo: 'bar'}});
v.added('B', "A1", {hi: 'there'});
v.expectResult({fun: 'changed', id: 'A1', changed: {hi: 'there'}});
v.removed('A', 'A1');
v.expectResult({fun: 'changed', id: 'A1', changed: {foo: undefined}});
v.removed('B', 'A1');
v.expectResult({fun: 'removed', id: 'A1'});
});
Tinytest.add('livedata - sessionview - weird key names', function (test) {
var v = newView(test);
v.added('A', "A1", {});
v.expectResult({fun: 'added', id: "A1", fields: {}});
v.changed('A', "A1", {constructor: 'bla'});
v.expectResult({fun: 'changed', id: 'A1', changed: {constructor: 'bla'}});
});
Tinytest.add('livedata - sessionview - clear undefined value', function (test) {
var v = newView(test);
v.added("A", "A1", {field: "value"});
v.expectResult({fun: 'added', id: "A1", fields: {field: "value"}});
v.expectNoResult();
v.changed("A", "A1", {field: undefined});
v.expectResult({fun: 'changed', id: 'A1', changed: {field: undefined}});
v.expectNoResult();
v.changed("A", "A1", {field: undefined});
v.expectNoResult();
});

View File

@@ -0,0 +1,190 @@
// By default, we use the permessage-deflate extension with default
// configuration. If $SERVER_WEBSOCKET_COMPRESSION is set, then it must be valid
// JSON. If it represents a falsey value, then we do not use permessage-deflate
// at all; otherwise, the JSON value is used as an argument to deflate's
// configure method; see
// https://github.com/faye/permessage-deflate-node/blob/master/README.md
//
// (We do this in an _.once instead of at startup, because we don't want to
// crash the tool during isopacket load if your JSON doesn't parse. This is only
// a problem because the tool has to load the DDP server code just in order to
// be a DDP client; see https://github.com/meteor/meteor/issues/3452 .)
var websocketExtensions = _.once(function () {
var extensions = [];
var websocketCompressionConfig = process.env.SERVER_WEBSOCKET_COMPRESSION
? JSON.parse(process.env.SERVER_WEBSOCKET_COMPRESSION) : {};
if (websocketCompressionConfig) {
extensions.push(Npm.require('permessage-deflate').configure(
websocketCompressionConfig
));
}
return extensions;
});
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "";
StreamServer = function () {
var self = this;
self.registration_callbacks = [];
self.open_sockets = [];
// Because we are installing directly onto WebApp.httpServer instead of using
// WebApp.app, we have to process the path prefix ourselves.
self.prefix = pathPrefix + '/sockjs';
RoutePolicy.declare(self.prefix + '/', 'network');
// set up sockjs
var sockjs = Npm.require('sockjs');
var serverOptions = {
prefix: self.prefix,
log: function() {},
// this is the default, but we code it explicitly because we depend
// on it in stream_client:HEARTBEAT_TIMEOUT
heartbeat_delay: 45000,
// The default disconnect_delay is 5 seconds, but if the server ends up CPU
// bound for that much time, SockJS might not notice that the user has
// reconnected because the timer (of disconnect_delay ms) can fire before
// SockJS processes the new connection. Eventually we'll fix this by not
// combining CPU-heavy processing with SockJS termination (eg a proxy which
// converts to Unix sockets) but for now, raise the delay.
disconnect_delay: 60 * 1000,
// Set the USE_JSESSIONID environment variable to enable setting the
// JSESSIONID cookie. This is useful for setting up proxies with
// session affinity.
jsessionid: !!process.env.USE_JSESSIONID
};
// If you know your server environment (eg, proxies) will prevent websockets
// from ever working, set $DISABLE_WEBSOCKETS and SockJS clients (ie,
// browsers) will not waste time attempting to use them.
// (Your server will still have a /websocket endpoint.)
if (process.env.DISABLE_WEBSOCKETS) {
serverOptions.websocket = false;
} else {
serverOptions.faye_server_options = {
extensions: websocketExtensions()
};
}
self.server = sockjs.createServer(serverOptions);
// Install the sockjs handlers, but we want to keep around our own particular
// request handler that adjusts idle timeouts while we have an outstanding
// request. This compensates for the fact that sockjs removes all listeners
// for "request" to add its own.
WebApp.httpServer.removeListener(
'request', WebApp._timeoutAdjustmentRequestCallback);
self.server.installHandlers(WebApp.httpServer);
WebApp.httpServer.addListener(
'request', WebApp._timeoutAdjustmentRequestCallback);
// Support the /websocket endpoint
self._redirectWebsocketEndpoint();
self.server.on('connection', function (socket) {
// sockjs sometimes passes us null instead of a socket object
// so we need to guard against that. see:
// https://github.com/sockjs/sockjs-node/issues/121
// https://github.com/meteor/meteor/issues/10468
if (!socket) return;
// We want to make sure that if a client connects to us and does the initial
// Websocket handshake but never gets to the DDP handshake, that we
// eventually kill the socket. Once the DDP handshake happens, DDP
// heartbeating will work. And before the Websocket handshake, the timeouts
// we set at the server level in webapp_server.js will work. But
// faye-websocket calls setTimeout(0) on any socket it takes over, so there
// is an "in between" state where this doesn't happen. We work around this
// by explicitly setting the socket timeout to a relatively large time here,
// and setting it back to zero when we set up the heartbeat in
// livedata_server.js.
socket.setWebsocketTimeout = function (timeout) {
if ((socket.protocol === 'websocket' ||
socket.protocol === 'websocket-raw')
&& socket._session.recv) {
socket._session.recv.connection.setTimeout(timeout);
}
};
socket.setWebsocketTimeout(45 * 1000);
socket.send = function (data) {
socket.write(data);
};
socket.on('close', function () {
self.open_sockets = _.without(self.open_sockets, socket);
});
self.open_sockets.push(socket);
// only to send a message after connection on tests, useful for
// socket-stream-client/server-tests.js
if (process.env.TEST_METADATA && process.env.TEST_METADATA !== "{}") {
socket.send(JSON.stringify({ testMessageOnConnect: true }));
}
// call all our callbacks when we get a new socket. they will do the
// work of setting up handlers and such for specific messages.
_.each(self.registration_callbacks, function (callback) {
callback(socket);
});
});
};
Object.assign(StreamServer.prototype, {
// call my callback when a new socket connects.
// also call it for all current connections.
register: function (callback) {
var self = this;
self.registration_callbacks.push(callback);
_.each(self.all_sockets(), function (socket) {
callback(socket);
});
},
// get a list of all sockets
all_sockets: function () {
var self = this;
return _.values(self.open_sockets);
},
// Redirect /websocket to /sockjs/websocket in order to not expose
// sockjs to clients that want to use raw websockets
_redirectWebsocketEndpoint: function() {
var self = this;
// Unfortunately we can't use a connect middleware here since
// sockjs installs itself prior to all existing listeners
// (meaning prior to any connect middlewares) so we need to take
// an approach similar to overshadowListeners in
// https://github.com/sockjs/sockjs-node/blob/cf820c55af6a9953e16558555a31decea554f70e/src/utils.coffee
['request', 'upgrade'].forEach((event) => {
var httpServer = WebApp.httpServer;
var oldHttpServerListeners = httpServer.listeners(event).slice(0);
httpServer.removeAllListeners(event);
// request and upgrade have different arguments passed but
// we only care about the first one which is always request
var newListener = function(request /*, moreArguments */) {
// Store arguments for use within the closure below
var args = arguments;
// TODO replace with url package
var url = Npm.require('url');
// Rewrite /websocket and /websocket/ urls to /sockjs/websocket while
// preserving query string.
var parsedUrl = url.parse(request.url);
if (parsedUrl.pathname === pathPrefix + '/websocket' ||
parsedUrl.pathname === pathPrefix + '/websocket/') {
parsedUrl.pathname = self.prefix + '/websocket';
request.url = url.format(parsedUrl);
}
_.each(oldHttpServerListeners, function(oldListener) {
oldListener.apply(httpServer, args);
});
};
httpServer.addListener(event, newListener);
});
}
});

View File

@@ -0,0 +1,125 @@
// A write fence collects a group of writes, and provides a callback
// when all of the writes are fully committed and propagated (all
// observers have been notified of the write and acknowledged it.)
//
DDPServer._WriteFence = class {
constructor() {
this.armed = false;
this.fired = false;
this.retired = false;
this.outstanding_writes = 0;
this.before_fire_callbacks = [];
this.completion_callbacks = [];
}
// Start tracking a write, and return an object to represent it. The
// object has a single method, committed(). This method should be
// called when the write is fully committed and propagated. You can
// continue to add writes to the WriteFence up until it is triggered
// (calls its callbacks because all writes have committed.)
beginWrite() {
if (this.retired)
return { committed: function () {} };
if (this.fired)
throw new Error("fence has already activated -- too late to add writes");
this.outstanding_writes++;
let committed = false;
const _committedFn = async () => {
if (committed)
throw new Error("committed called twice on the same write");
committed = true;
this.outstanding_writes--;
await this._maybeFire();
};
const self = this;
return {
committed: Meteor._isFibersEnabled ? () => Promise.await(_committedFn.apply(self)) : _committedFn,
};
}
// Arm the fence. Once the fence is armed, and there are no more
// uncommitted writes, it will activate.
arm() {
if (this === DDPServer._CurrentWriteFence.get())
throw Error("Can't arm the current fence");
this.armed = true;
return Meteor._isFibersEnabled ? Promise.await(this._maybeFire()) : this._maybeFire();
}
// Register a function to be called once before firing the fence.
// Callback function can add new writes to the fence, in which case
// it won't fire until those writes are done as well.
onBeforeFire(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
this.before_fire_callbacks.push(func);
}
// Register a function to be called when the fence fires.
onAllCommitted(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
this.completion_callbacks.push(func);
}
_armAndWait() {
let resolver;
const returnValue = new Promise(r => resolver = r);
this.onAllCommitted(resolver);
this.arm();
return returnValue;
}
// Convenience function. Arms the fence, then blocks until it fires.
armAndWait() {
return Meteor._isFibersEnabled ? Promise.await(this._armAndWait()) : this._armAndWait();
}
async _maybeFire() {
if (this.fired)
throw new Error("write fence already activated?");
if (this.armed && !this.outstanding_writes) {
const invokeCallback = async (func) => {
try {
await func(this);
} catch (err) {
Meteor._debug("exception in write fence callback:", err);
}
};
this.outstanding_writes++;
while (this.before_fire_callbacks.length > 0) {
const cb = this.before_fire_callbacks.shift();
await invokeCallback(cb);
}
this.outstanding_writes--;
if (!this.outstanding_writes) {
this.fired = true;
while (this.completion_callbacks.length > 0) {
const cb = this.completion_callbacks.shift();
await invokeCallback(cb);
}
}
}
}
// Deactivate this fence so that adding more writes has no effect.
// The fence must have already fired.
retire() {
if (!this.fired)
throw new Error("Can't retire a fence that hasn't fired.");
this.retired = true;
}
};
// The current write fence. When there is a current write fence, code
// that writes to databases should register their writes with it using
// beginWrite().
//
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable;

View File

@@ -1,5 +1,7 @@
DDPServer = {};
var Fiber = Npm.require('fibers');
// Publication strategies define how we handle data from published cursors at the collection level
// This allows someone to:
// - Choose a trade-off between client-server bandwidth and server memory usage
@@ -328,9 +330,9 @@ var Session = function (server, version, socket, options) {
self.send({ msg: 'connected', session: self.id });
// On initial connect, spin up all the universal publishers.
Meteor._runAsync(function() {
Fiber(function () {
self.startUniversalSubs();
});
}).run();
if (version !== 'pre1' && options.heartbeatInterval !== 0) {
// We no longer need the low level timeout because we have heartbeats.
@@ -555,10 +557,10 @@ Object.assign(Session.prototype, {
// Any message counts as receiving a pong, as it demonstrates that
// the client is still alive.
if (self.heartbeat) {
Meteor._runAsync(function() {
Fiber(function () {
self.heartbeat.messageReceived();
});
};
}).run();
}
if (self.version !== 'pre1' && msg_in.msg === 'ping') {
if (self._respondToPings)
@@ -582,7 +584,7 @@ Object.assign(Session.prototype, {
return;
}
function runHandlers() {
Fiber(function () {
var blocked = true;
var unblock = function () {
@@ -602,9 +604,7 @@ Object.assign(Session.prototype, {
else
self.sendError('Bad request', msg);
unblock(); // in case the handler didn't already do it
}
Meteor._runAsync(runHandlers);
}).run();
};
processNext();
@@ -1177,21 +1177,15 @@ Object.assign(Subscription.prototype, {
return c && c._publishCursor;
};
if (isCursor(res)) {
if (Meteor._isFibersEnabled) {
try {
res._publishCursor(self);
} catch (e) {
self.error(e);
return;
}
// _publishCursor only returns after the initial added callbacks have run.
// mark subscription as ready.
self.ready();
} else {
res._publishCursor(self).then(() => {
self.ready();
}).catch((e) => self.error(e));
try {
res._publishCursor(self);
} catch (e) {
self.error(e);
return;
}
// _publishCursor only returns after the initial added callbacks have run.
// mark subscription as ready.
self.ready();
} else if (_.isArray(res)) {
// Check all the elements are cursors
if (! _.all(res, isCursor)) {
@@ -1213,21 +1207,15 @@ Object.assign(Subscription.prototype, {
collectionNames[collectionName] = true;
};
if (Meteor._isFibersEnabled) {
try {
_.each(res, function (cur) {
cur._publishCursor(self);
});
} catch (e) {
self.error(e);
return;
}
self.ready();
} else {
Promise.all(res.map((c) => c._publishCursor(self))).then(() => {
self.ready();
}).catch((e) => self.error(e));
try {
_.each(res, function (cur) {
cur._publishCursor(self);
});
} catch (e) {
self.error(e);
return;
}
self.ready();
} else if (res) {
// Truthy values other than cursors or arrays are probably a
// user mistake (possible returning a Mongo document via, say,
@@ -1504,11 +1492,9 @@ Server = function (options = {}) {
sendError("Already connected", msg);
return;
}
Meteor._runAsync(function() {
Fiber(function () {
self._handleConnect(socket, msg);
})
}).run();
return;
}
@@ -1525,9 +1511,9 @@ Server = function (options = {}) {
socket.on('close', function () {
if (socket._meteorSession) {
Meteor._runAsync(function() {
Fiber(function () {
socket._meteorSession.close();
});
}).run();
}
});
});
@@ -1705,9 +1691,9 @@ Object.assign(Server.prototype, {
// self.sessions to change while we're running this loop.
self.sessions.forEach(function (session) {
if (!session._dontStartNewUniversalSubs) {
Meteor._runAsync(function() {
Fiber(function() {
session._startSubscription(handler);
});
}).run();
}
});
}

View File

@@ -10,6 +10,11 @@ Npm.depends({
});
Package.onUse(function (api) {
if (process.env.DISABLE_FIBERS) {
api.use('ddp-server-async', 'server');
api.export('DDPServer', 'server');
return;
}
api.use(['check', 'random', 'ejson', 'underscore',
'retry', 'mongo-id', 'diff-sequence', 'ecmascript'],
'server');
@@ -50,11 +55,7 @@ Package.onUse(function (api) {
Package.onTest(function (api) {
api.use('ecmascript', ['client', 'server']);
api.use('livedata', ['client', 'server']);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', ['client', 'server']);
} else {
api.use('mongo-async', ['client', 'server']);
}
api.use('mongo', ['client', 'server']);
api.use('test-helpers', ['client', 'server']);
api.use(['underscore', 'tinytest', 'random', 'tracker', 'minimongo', 'reactive-var']);

View File

@@ -1,121 +1,18 @@
var Future = Npm.require('fibers/future');
// A write fence collects a group of writes, and provides a callback
// when all of the writes are fully committed and propagated (all
// observers have been notified of the write and acknowledged it.)
//
DDPServer._WriteFence = class {
constructor() {
this.armed = false;
this.fired = false;
this.retired = false;
this.outstanding_writes = 0;
this.before_fire_callbacks = [];
this.completion_callbacks = [];
}
DDPServer._WriteFence = function () {
var self = this;
// Start tracking a write, and return an object to represent it. The
// object has a single method, committed(). This method should be
// called when the write is fully committed and propagated. You can
// continue to add writes to the WriteFence up until it is triggered
// (calls its callbacks because all writes have committed.)
beginWrite() {
if (this.retired)
return { committed: function () {} };
if (this.fired)
throw new Error("fence has already activated -- too late to add writes");
this.outstanding_writes++;
let committed = false;
const _committedFn = async () => {
if (committed)
throw new Error("committed called twice on the same write");
committed = true;
this.outstanding_writes--;
await this._maybeFire();
};
const self = this;
return {
committed: Meteor._isFibersEnabled ? () => Promise.await(_committedFn.apply(self)) : _committedFn,
};
}
// Arm the fence. Once the fence is armed, and there are no more
// uncommitted writes, it will activate.
arm() {
if (this === DDPServer._CurrentWriteFence.get())
throw Error("Can't arm the current fence");
this.armed = true;
return Meteor._isFibersEnabled ? Promise.await(this._maybeFire()) : this._maybeFire();
}
// Register a function to be called once before firing the fence.
// Callback function can add new writes to the fence, in which case
// it won't fire until those writes are done as well.
onBeforeFire(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
this.before_fire_callbacks.push(func);
}
// Register a function to be called when the fence fires.
onAllCommitted(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
this.completion_callbacks.push(func);
}
_armAndWait() {
let resolver;
const returnValue = new Promise(r => resolver = r);
this.onAllCommitted(resolver);
this.arm();
return returnValue;
}
// Convenience function. Arms the fence, then blocks until it fires.
armAndWait() {
return Meteor._isFibersEnabled ? Promise.await(this._armAndWait()) : this._armAndWait();
}
async _maybeFire() {
if (this.fired)
throw new Error("write fence already activated?");
if (this.armed && !this.outstanding_writes) {
const invokeCallback = async (func) => {
try {
await func(this);
} catch (err) {
Meteor._debug("exception in write fence callback:", err);
}
};
this.outstanding_writes++;
while (this.before_fire_callbacks.length > 0) {
const cb = this.before_fire_callbacks.shift();
await invokeCallback(cb);
}
this.outstanding_writes--;
if (!this.outstanding_writes) {
this.fired = true;
while (this.completion_callbacks.length > 0) {
const cb = this.completion_callbacks.shift();
await invokeCallback(cb);
}
}
}
}
// Deactivate this fence so that adding more writes has no effect.
// The fence must have already fired.
retire() {
if (!this.fired)
throw new Error("Can't retire a fence that hasn't fired.");
this.retired = true;
}
self.armed = false;
self.fired = false;
self.retired = false;
self.outstanding_writes = 0;
self.before_fire_callbacks = [];
self.completion_callbacks = [];
};
// The current write fence. When there is a current write fence, code
@@ -123,3 +20,112 @@ DDPServer._WriteFence = class {
// beginWrite().
//
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable;
_.extend(DDPServer._WriteFence.prototype, {
// Start tracking a write, and return an object to represent it. The
// object has a single method, committed(). This method should be
// called when the write is fully committed and propagated. You can
// continue to add writes to the WriteFence up until it is triggered
// (calls its callbacks because all writes have committed.)
beginWrite: function () {
var self = this;
if (self.retired)
return { committed: function () {} };
if (self.fired)
throw new Error("fence has already activated -- too late to add writes");
self.outstanding_writes++;
var committed = false;
return {
committed: function () {
if (committed)
throw new Error("committed called twice on the same write");
committed = true;
self.outstanding_writes--;
self._maybeFire();
}
};
},
// Arm the fence. Once the fence is armed, and there are no more
// uncommitted writes, it will activate.
arm: function () {
var self = this;
if (self === DDPServer._CurrentWriteFence.get())
throw Error("Can't arm the current fence");
self.armed = true;
self._maybeFire();
},
// Register a function to be called once before firing the fence.
// Callback function can add new writes to the fence, in which case
// it won't fire until those writes are done as well.
onBeforeFire: function (func) {
var self = this;
if (self.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
self.before_fire_callbacks.push(func);
},
// Register a function to be called when the fence fires.
onAllCommitted: function (func) {
var self = this;
if (self.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
self.completion_callbacks.push(func);
},
// Convenience function. Arms the fence, then blocks until it fires.
armAndWait: function () {
var self = this;
var future = new Future;
self.onAllCommitted(function () {
future['return']();
});
self.arm();
future.wait();
},
_maybeFire: function () {
var self = this;
if (self.fired)
throw new Error("write fence already activated?");
if (self.armed && !self.outstanding_writes) {
function invokeCallback (func) {
try {
func(self);
} catch (err) {
Meteor._debug("exception in write fence callback", err);
}
}
self.outstanding_writes++;
while (self.before_fire_callbacks.length > 0) {
var callbacks = self.before_fire_callbacks;
self.before_fire_callbacks = [];
_.each(callbacks, invokeCallback);
}
self.outstanding_writes--;
if (!self.outstanding_writes) {
self.fired = true;
var callbacks = self.completion_callbacks;
self.completion_callbacks = [];
_.each(callbacks, invokeCallback);
}
}
},
// Deactivate this fence so that adding more writes has no effect.
// The fence must have already fired.
retire: function () {
var self = this;
if (! self.fired)
throw new Error("Can't retire a fence that hasn't fired.");
self.retired = true;
}
});

View File

@@ -7,12 +7,8 @@ Package.describe({
Package.onUse(function (api) {
api.use(['underscore'], ['client', 'server']);
api.use(['templating@1.2.13', 'ddp'], ['client']);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', ['client', 'server']);
} else {
api.use('mongo-async', ['client', 'server']);
}
api.use(['templating@1.2.13', 'mongo', 'ddp'], ['client']);
// Detect whether autopublish is used.
api.use('autopublish', 'server', {weak: true});

View File

@@ -30,6 +30,8 @@ Package.onUse(function(api) {
// DDP: Meteor's client/server protocol.
'ddp',
'livedata', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0.
// You want to keep your data somewhere? How about MongoDB?
'mongo',
// Blaze: Reactive DOM!
'blaze',
'ui', // XXX COMPAT WITH PACKAGES BUILT FOR 0.9.0.
@@ -48,12 +50,6 @@ Package.onUse(function(api) {
// People like being able to clone objects.
'ejson'
]);
// You want to keep your data somewhere? How about MongoDB?
if (!process.env.DISABLE_FIBERS) {
api.imply(['mongo']);
} else {
api.imply(['mongo-async']);
}
// These are useful too! But you don't have to see their exports
// unless you want to.

View File

@@ -11,12 +11,7 @@ Package.onUse(function onUse(api) {
});
Package.onTest(function onTest(api) {
api.use(['ecmascript', 'tinytest']);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo');
} else {
api.use('mongo-async');
}
api.use(['ecmascript', 'tinytest', 'mongo']);
api.use('ejson');
api.mainModule('ejson_tests.js');
});

View File

@@ -7,13 +7,10 @@ Package.onUse(function (api) {
api.use([
'ecmascript',
'facts-base',
'mongo',
'templating@1.2.13'
], 'client');
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', 'client');
} else {
api.use('mongo-async', 'client');
}
api.imply('facts-base');
api.addFiles('facts_ui.html', 'client');

View File

@@ -0,0 +1,132 @@
var Fiber = Npm.require('fibers');
var Future = Npm.require('fibers/future');
Meteor._noYieldsAllowed = function (f) {
return f();
};
Meteor._DoubleEndedQueue = Npm.require('double-ended-queue');
// Meteor._SynchronousQueue is a queue which runs task functions serially.
// Tasks are assumed to be synchronous: ie, it's assumed that they are
// done when they return.
//
// It has two methods:
// - queueTask queues a task to be run, and returns immediately.
// - runTask queues a task to be run, and then yields. It returns
// when the task finishes running.
//
// It's safe to call queueTask from within a task, but not runTask (unless
// you're calling runTask from a nested Fiber).
//
// Somewhat inspired by async.queue, but specific to blocking tasks.
// XXX break this out into an NPM module?
// XXX could maybe use the npm 'schlock' module instead, which would
// also support multiple concurrent "read" tasks
//
class AsynchronousQueue {
constructor() {
this._taskHandles = new Meteor._DoubleEndedQueue();
this._runningOrRunScheduled = false;
// This is true if we're currently draining. While we're draining, a further
// drain is a noop, to prevent infinite loops. "drain" is a heuristic type
// operation, that has a meaning like unto "what a naive person would expect
// when modifying a table from an observe"
this._draining = false;
}
queueTask(task) {
this._taskHandles.push({
task: task,
name: task.name
});
return this._scheduleRun();
}
_scheduleRun() {
// Already running or scheduled? Do nothing.
if (this._runningOrRunScheduled)
return;
this._runningOrRunScheduled = true;
let resolver;
const returnValue = new Promise(r => resolver = r);
setImmediate(() => {
Meteor._runAsync(async () => {
await this._run();
if (!resolver) {
throw new Error("Resolver not found for task");
}
resolver();
});
});
return returnValue;
}
async _run() {
if (!this._runningOrRunScheduled)
throw new Error("expected to be _runningOrRunScheduled");
if (this._taskHandles.isEmpty()) {
// Done running tasks! Don't immediately schedule another run, but
// allow future tasks to do so.
this._runningOrRunScheduled = false;
return;
}
const taskHandle = this._taskHandles.shift();
// Run the task.
try {
await taskHandle.task();
} catch (err) {
Meteor._debug("Exception in queued task", err);
}
// Soon, run the next task, if there is any.
this._runningOrRunScheduled = false;
await this._scheduleRun();
}
runTask(task) {
const handle = {
task: Meteor.bindEnvironment(task, function(e) {
Meteor._debug('Exception from task', e);
throw e;
}),
name: task.name
};
this._taskHandles.push(handle);
return this._scheduleRun();
}
flush() {
return this.runTask(() => {});
}
async drain() {
if (this._draining)
return;
this._draining = true;
while (!this._taskHandles.isEmpty()) {
await this.flush();
}
this._draining = false;
}
}
Meteor._AsynchronousQueue = AsynchronousQueue;
Meteor._SynchronousQueue = AsynchronousQueue;
// Sleep. Mostly used for debugging (eg, inserting latency into server
// methods).
//
const _sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
Meteor._sleepForMs = function (ms) {
return _sleep(ms);
};

View File

@@ -34,7 +34,11 @@ Package.onUse(function (api) {
api.addFiles('timers.js', ['client', 'server']);
api.addFiles('errors.js', ['client', 'server']);
api.addFiles('asl-helpers.js', 'server');
api.addFiles('fiber_helpers.js', 'server');
if (process.env.DISABLE_FIBERS) {
api.addFiles('async_helpers.js', 'server');
} else {
api.addFiles('fiber_helpers.js', 'server');
}
api.addFiles('fiber_stubs_client.js', 'client');
api.addFiles('asl-helpers-client.js', 'client');
api.addFiles('startup_client.js', ['client']);

View File

@@ -12,7 +12,7 @@ import { normalizeProjection } from "./mongo_utils";
* @namespace
*/
Mongo = {};
console.log('Using package: mongo-async');
/**
* @summary Constructor for a Collection
* @locus Anywhere

View File

@@ -14,7 +14,6 @@ const util = require("util");
/** @type {import('mongodb')} */
var MongoDB = NpmModuleMongodb;
var Future = Npm.require('fibers/future');
import { DocFetcher } from "./doc_fetcher.js";
import {
ASYNC_CURSOR_METHODS,
@@ -23,6 +22,9 @@ import {
MongoInternals = {};
// TODO remove after test
MongoInternals.__packageName = 'mongo-async';
MongoInternals.NpmModules = {
mongodb: {
version: NpmModuleMongodbVersion,
@@ -244,19 +246,15 @@ MongoConnection.prototype.rawCollection = function (collectionName) {
return self.db.collection(collectionName);
};
MongoConnection.prototype._createCappedCollection = function (
MongoConnection.prototype._createCappedCollection = async function (
collectionName, byteSize, maxDocuments) {
var self = this;
if (! self.db)
throw Error("_createCappedCollection called before Connection created?");
var future = new Future();
self.db.createCollection(
collectionName,
{ capped: true, size: byteSize, max: maxDocuments },
future.resolver());
future.wait();
await self.db.createCollection(collectionName,
{ capped: true, size: byteSize, max: maxDocuments });
};
// This should be called synchronously with a write, to create a
@@ -834,29 +832,25 @@ MongoConnection.prototype.findOne = async function (collection_name, selector, o
// We'll actually design an index API later. For now, we just pass through to
// Mongo's, but make it synchronous.
MongoConnection.prototype.createIndex = function (collectionName, index,
MongoConnection.prototype.createIndex = async function (collectionName, index,
options) {
var self = this;
// We expect this function to be called at startup, not from within a method,
// so we don't interact with the write fence.
var collection = self.rawCollection(collectionName);
var future = new Future;
var indexName = collection.createIndex(index, options, future.resolver());
future.wait();
var indexName = await collection.createIndex(index, options);
};
MongoConnection.prototype._ensureIndex = MongoConnection.prototype.createIndex;
MongoConnection.prototype._dropIndex = function (collectionName, index) {
MongoConnection.prototype._dropIndex = async function (collectionName, index) {
var self = this;
// This function is only used by test code, not within a method, so we don't
// interact with the write fence.
var collection = self.rawCollection(collectionName);
var future = new Future;
var indexName = collection.dropIndex(index, future.resolver());
future.wait();
var indexName = await collection.dropIndex(index);
};
// CURSORS

View File

@@ -85,7 +85,7 @@ Package.onUse(function (api) {
});
Package.onTest(function (api) {
api.use('mongo-async');
api.use('mongo');
api.use('check');
api.use('ecmascript');
api.use('npm-mongo', 'server');

View File

@@ -4,9 +4,5 @@ Package.describe({
});
Package.onUse(function (api) {
if (!process.env.DISABLE_FIBERS) {
api.imply('mongo');
} else {
api.imply('mongo-async');
}
api.imply("mongo");
});

View File

@@ -13,6 +13,7 @@ import { normalizeProjection } from "./mongo_utils";
*/
Mongo = {};
console.log('Using package: mongo');
/**
* @summary Constructor for a Collection
* @locus Anywhere

View File

@@ -23,6 +23,9 @@ import {
MongoInternals = {};
// TODO remove after test
MongoInternals.__packageName = 'mongo'
MongoInternals.NpmModules = {
mongodb: {
version: NpmModuleMongodbVersion,

View File

@@ -21,6 +21,13 @@ Npm.strip({
});
Package.onUse(function (api) {
if (process.env.DISABLE_FIBERS) {
api.use('mongo-async', ['server', 'client']);
api.export("Mongo");
api.export('MongoInternals', 'server');
api.export('ObserveMultiplexer', 'server', {testOnly: true});
return;
}
api.use('npm-mongo', 'server');
api.use('allow-deny');

View File

@@ -15,11 +15,7 @@ Package.onUse(function (api) {
});
Package.onTest(function (api) {
if (!process.env.DISABLE_FIBERS) {
api.use('mongo');
} else {
api.use('mongo-async');
}
api.use('mongo');
api.use('mongo-decimal');
api.use('insecure');
api.use(['tinytest']);

View File

@@ -5,12 +5,8 @@ Package.describe({
Package.onUse(api => {
api.use(['check', 'ecmascript', 'localstorage', 'url']);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', 'server');
} else {
api.use('mongo-async', 'server');
}
api.use(['routepolicy', 'webapp', 'service-configuration', 'logging'], 'server');
api.use(['routepolicy', 'webapp', 'mongo', 'service-configuration', 'logging'], 'server');
api.use(['reload', 'base64'], 'client');

View File

@@ -10,11 +10,7 @@ Package.onUse(api => {
api.use('oauth', ['client', 'server']);
api.use('check', 'server');
if (!process.env.DISABLE_FIBERS) {
api.use('mongo');
} else {
api.use('mongo-async');
}
api.use('mongo');
api.export('OAuth1Binding', 'server');
api.export('OAuth1Test', 'server', {testOnly: true});

View File

@@ -1,6 +1,6 @@
Package.describe({
name: "promise",
version: "0.12.0",
version: "0.12.1",
summary: "ECMAScript 2015 Promise polyfill with Fiber support",
git: "https://github.com/meteor/promise",
documentation: "README.md"

View File

@@ -1,6 +1,6 @@
require("./extensions.js");
if (!!!process.env.DISABLE_FIBERS) {
if (!process.env.DISABLE_FIBERS) {
require("meteor-promise").makeCompatible(
Promise,
// Allow every Promise callback to run in a Fiber drawn from a pool of
@@ -9,7 +9,6 @@ if (!!!process.env.DISABLE_FIBERS) {
);
}
// Reference: https://caniuse.com/#feat=promises
require("meteor/modern-browsers").setMinimumBrowserVersions({
chrome: 32,

View File

@@ -6,12 +6,7 @@ Package.describe({
Package.onUse(function (api) {
api.use(['tracker', 'ejson', 'ecmascript']);
// If we are loading mongo-livedata, let you store ObjectIDs in it.
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', { weak: true });
} else {
api.use('mongo-async', { weak: true });
}
api.use(['reload'], { weak: true });
api.use(['mongo', 'reload'], { weak: true });
api.mainModule('migration.js');
api.export('ReactiveDict');
});

View File

@@ -5,11 +5,7 @@ Package.describe({
Package.onUse(function(api) {
api.use('accounts-base', ['client', 'server']);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo', ['client', 'server']);
} else {
api.use('mongo-async', ['client', 'server']);
}
api.use('mongo', ['client', 'server']);
api.use('ecmascript', ['client', 'server']);
api.export('ServiceConfiguration');
api.addFiles('service_configuration_common.js', ['client', 'server']);

View File

@@ -20,10 +20,6 @@ Package.onTest(function (api) {
api.use('tinytest');
api.use('session', 'client');
api.use('tracker');
if (!process.env.DISABLE_FIBERS) {
api.use('mongo');
} else {
api.use('mongo-async');
}
api.use('mongo');
api.addFiles('session_tests.js', 'client');
});

View File

@@ -10,13 +10,9 @@ Package.onUse(function (api) {
'underscore',
'random',
'ddp',
'mongo',
'check'
]);
if (!process.env.DISABLE_FIBERS) {
api.use('mongo');
} else {
api.use('mongo-async');
}
api.mainModule('tinytest_client.js', 'client');
api.mainModule('tinytest_server.js', 'server');

View File

@@ -15,7 +15,7 @@ var packageJson = {
"node-gyp": "8.0.0",
"node-pre-gyp": "0.15.0",
typescript: "4.5.4",
"@meteorjs/babel": "7.16.0-beta.1",
"@meteorjs/babel": "7.16.0-beta.7",
// Keep the versions of these packages consistent with the versions
// found in dev-bundle-server-package.js.
"meteor-promise": "0.9.0",