mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
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:
@@ -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",
|
||||
|
||||
2
npm-packages/meteor-babel/package-lock.json
generated
2
npm-packages/meteor-babel/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@meteorjs/babel",
|
||||
"version": "7.16.0-beta.1",
|
||||
"version": "7.16.0-beta.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
|
||||
1
packages/ddp-client-async/.npm/package/.gitignore
vendored
Normal file
1
packages/ddp-client-async/.npm/package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
packages/ddp-client-async/.npm/package/README
Normal file
7
packages/ddp-client-async/.npm/package/README
Normal 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.
|
||||
25
packages/ddp-client-async/.npm/package/npm-shrinkwrap.json
generated
Normal file
25
packages/ddp-client-async/.npm/package/npm-shrinkwrap.json
generated
Normal 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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
4
packages/ddp-client-async/README.md
Normal file
4
packages/ddp-client-async/README.md
Normal 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)
|
||||
***
|
||||
|
||||
6
packages/ddp-client-async/client/client.js
Normal file
6
packages/ddp-client-async/client/client.js
Normal 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';
|
||||
59
packages/ddp-client-async/client/client_convenience.js
Normal file
59
packages/ddp-client-async/client/client_convenience.js
Normal 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);
|
||||
});
|
||||
85
packages/ddp-client-async/common/MethodInvoker.js
Normal file
85
packages/ddp-client-async/common/MethodInvoker.js
Normal 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;
|
||||
}
|
||||
}
|
||||
1899
packages/ddp-client-async/common/livedata_connection.js
Normal file
1899
packages/ddp-client-async/common/livedata_connection.js
Normal file
File diff suppressed because it is too large
Load Diff
91
packages/ddp-client-async/common/namespace.js
Normal file
91
packages/ddp-client-async/common/namespace.js
Normal 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)
|
||||
);
|
||||
63
packages/ddp-client-async/package.js
Normal file
63
packages/ddp-client-async/package.js
Normal 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');
|
||||
});
|
||||
1
packages/ddp-client-async/server/server.js
Normal file
1
packages/ddp-client-async/server/server.js
Normal file
@@ -0,0 +1 @@
|
||||
export { DDP } from '../common/namespace.js';
|
||||
2491
packages/ddp-client-async/test/livedata_connection_tests.js
Normal file
2491
packages/ddp-client-async/test/livedata_connection_tests.js
Normal file
File diff suppressed because it is too large
Load Diff
376
packages/ddp-client-async/test/livedata_test_service.js
Normal file
376
packages/ddp-client-async/test/livedata_test_service.js
Normal 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);
|
||||
}
|
||||
});
|
||||
1103
packages/ddp-client-async/test/livedata_tests.js
Normal file
1103
packages/ddp-client-async/test/livedata_tests.js
Normal file
File diff suppressed because it is too large
Load Diff
44
packages/ddp-client-async/test/random_stream_tests.js
Normal file
44
packages/ddp-client-async/test/random_stream_tests.js
Normal 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();
|
||||
});
|
||||
57
packages/ddp-client-async/test/stub_stream.js
Normal file
57
packages/ddp-client-async/test/stub_stream.js
Normal 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
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
1
packages/ddp-server-async/.npm/package/.gitignore
vendored
Normal file
1
packages/ddp-server-async/.npm/package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
packages/ddp-server-async/.npm/package/README
Normal file
7
packages/ddp-server-async/.npm/package/README
Normal 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.
|
||||
45
packages/ddp-server-async/.npm/package/npm-shrinkwrap.json
generated
Normal file
45
packages/ddp-server-async/.npm/package/npm-shrinkwrap.json
generated
Normal 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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
4
packages/ddp-server-async/README.md
Normal file
4
packages/ddp-server-async/README.md
Normal 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)
|
||||
***
|
||||
|
||||
167
packages/ddp-server-async/crossbar.js
Normal file
167
packages/ddp-server-async/crossbar.js
Normal 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"
|
||||
});
|
||||
49
packages/ddp-server-async/crossbar_tests.js
Normal file
49
packages/ddp-server-async/crossbar_tests.js
Normal 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);
|
||||
});
|
||||
1917
packages/ddp-server-async/livedata_server.js
Normal file
1917
packages/ddp-server-async/livedata_server.js
Normal file
File diff suppressed because it is too large
Load Diff
192
packages/ddp-server-async/livedata_server_async_tests.js
Normal file
192
packages/ddp-server-async/livedata_server_async_tests.js
Normal 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();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
411
packages/ddp-server-async/livedata_server_tests.js
Normal file
411
packages/ddp-server-async/livedata_server_tests.js
Normal 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);
|
||||
})
|
||||
);
|
||||
61
packages/ddp-server-async/package.js
Normal file
61
packages/ddp-server-async/package.js
Normal 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']);
|
||||
});
|
||||
28
packages/ddp-server-async/server_convenience.js
Executable file
28
packages/ddp-server-async/server_convenience.js
Executable 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);
|
||||
}
|
||||
);
|
||||
393
packages/ddp-server-async/session_view_tests.js
Normal file
393
packages/ddp-server-async/session_view_tests.js
Normal 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();
|
||||
|
||||
});
|
||||
190
packages/ddp-server-async/stream_server.js
Normal file
190
packages/ddp-server-async/stream_server.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
125
packages/ddp-server-async/writefence.js
Normal file
125
packages/ddp-server-async/writefence.js
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
132
packages/meteor/async_helpers.js
Normal file
132
packages/meteor/async_helpers.js
Normal 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);
|
||||
};
|
||||
@@ -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']);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { normalizeProjection } from "./mongo_utils";
|
||||
* @namespace
|
||||
*/
|
||||
Mongo = {};
|
||||
|
||||
console.log('Using package: mongo-async');
|
||||
/**
|
||||
* @summary Constructor for a Collection
|
||||
* @locus Anywhere
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { normalizeProjection } from "./mongo_utils";
|
||||
*/
|
||||
Mongo = {};
|
||||
|
||||
console.log('Using package: mongo');
|
||||
/**
|
||||
* @summary Constructor for a Collection
|
||||
* @locus Anywhere
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
|
||||
MongoInternals = {};
|
||||
|
||||
// TODO remove after test
|
||||
MongoInternals.__packageName = 'mongo'
|
||||
|
||||
MongoInternals.NpmModules = {
|
||||
mongodb: {
|
||||
version: NpmModuleMongodbVersion,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user