mongodb-livedata-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +63 -0
  2. package/dist/livedata_server.js +9 -0
  3. package/dist/meteor/binary-heap/max_heap.js +186 -0
  4. package/dist/meteor/binary-heap/min_heap.js +17 -0
  5. package/dist/meteor/binary-heap/min_max_heap.js +48 -0
  6. package/dist/meteor/callback-hook/hook.js +78 -0
  7. package/dist/meteor/ddp/crossbar.js +136 -0
  8. package/dist/meteor/ddp/heartbeat.js +77 -0
  9. package/dist/meteor/ddp/livedata_server.js +403 -0
  10. package/dist/meteor/ddp/method-invocation.js +72 -0
  11. package/dist/meteor/ddp/random-stream.js +100 -0
  12. package/dist/meteor/ddp/session-collection-view.js +106 -0
  13. package/dist/meteor/ddp/session-document-view.js +82 -0
  14. package/dist/meteor/ddp/session.js +570 -0
  15. package/dist/meteor/ddp/stream_server.js +181 -0
  16. package/dist/meteor/ddp/subscription.js +347 -0
  17. package/dist/meteor/ddp/utils.js +104 -0
  18. package/dist/meteor/ddp/writefence.js +111 -0
  19. package/dist/meteor/diff-sequence/diff.js +257 -0
  20. package/dist/meteor/ejson/ejson.js +569 -0
  21. package/dist/meteor/ejson/stringify.js +119 -0
  22. package/dist/meteor/ejson/utils.js +42 -0
  23. package/dist/meteor/id-map/id_map.js +92 -0
  24. package/dist/meteor/mongo/caching_change_observer.js +94 -0
  25. package/dist/meteor/mongo/doc_fetcher.js +53 -0
  26. package/dist/meteor/mongo/geojson_utils.js +41 -0
  27. package/dist/meteor/mongo/live_connection.js +264 -0
  28. package/dist/meteor/mongo/live_cursor.js +57 -0
  29. package/dist/meteor/mongo/minimongo_common.js +2002 -0
  30. package/dist/meteor/mongo/minimongo_matcher.js +217 -0
  31. package/dist/meteor/mongo/minimongo_sorter.js +268 -0
  32. package/dist/meteor/mongo/observe_driver_utils.js +73 -0
  33. package/dist/meteor/mongo/observe_multiplexer.js +228 -0
  34. package/dist/meteor/mongo/oplog-observe-driver.js +919 -0
  35. package/dist/meteor/mongo/oplog_tailing.js +352 -0
  36. package/dist/meteor/mongo/oplog_v2_converter.js +126 -0
  37. package/dist/meteor/mongo/polling_observe_driver.js +195 -0
  38. package/dist/meteor/mongo/synchronous-cursor.js +261 -0
  39. package/dist/meteor/mongo/synchronous-queue.js +110 -0
  40. package/dist/meteor/ordered-dict/ordered_dict.js +198 -0
  41. package/dist/meteor/random/AbstractRandomGenerator.js +92 -0
  42. package/dist/meteor/random/AleaRandomGenerator.js +90 -0
  43. package/dist/meteor/random/NodeRandomGenerator.js +42 -0
  44. package/dist/meteor/random/createAleaGenerator.js +32 -0
  45. package/dist/meteor/random/createRandom.js +22 -0
  46. package/dist/meteor/random/main.js +12 -0
  47. package/livedata_server.ts +3 -0
  48. package/meteor/LICENSE +28 -0
  49. package/meteor/binary-heap/max_heap.ts +225 -0
  50. package/meteor/binary-heap/min_heap.ts +15 -0
  51. package/meteor/binary-heap/min_max_heap.ts +53 -0
  52. package/meteor/callback-hook/hook.ts +85 -0
  53. package/meteor/ddp/crossbar.ts +148 -0
  54. package/meteor/ddp/heartbeat.ts +97 -0
  55. package/meteor/ddp/livedata_server.ts +473 -0
  56. package/meteor/ddp/method-invocation.ts +86 -0
  57. package/meteor/ddp/random-stream.ts +102 -0
  58. package/meteor/ddp/session-collection-view.ts +119 -0
  59. package/meteor/ddp/session-document-view.ts +92 -0
  60. package/meteor/ddp/session.ts +708 -0
  61. package/meteor/ddp/stream_server.ts +204 -0
  62. package/meteor/ddp/subscription.ts +392 -0
  63. package/meteor/ddp/utils.ts +119 -0
  64. package/meteor/ddp/writefence.ts +130 -0
  65. package/meteor/diff-sequence/diff.ts +295 -0
  66. package/meteor/ejson/ejson.ts +601 -0
  67. package/meteor/ejson/stringify.ts +122 -0
  68. package/meteor/ejson/utils.ts +38 -0
  69. package/meteor/id-map/id_map.ts +84 -0
  70. package/meteor/mongo/caching_change_observer.ts +120 -0
  71. package/meteor/mongo/doc_fetcher.ts +52 -0
  72. package/meteor/mongo/geojson_utils.ts +42 -0
  73. package/meteor/mongo/live_connection.ts +302 -0
  74. package/meteor/mongo/live_cursor.ts +79 -0
  75. package/meteor/mongo/minimongo_common.ts +2440 -0
  76. package/meteor/mongo/minimongo_matcher.ts +275 -0
  77. package/meteor/mongo/minimongo_sorter.ts +331 -0
  78. package/meteor/mongo/observe_driver_utils.ts +79 -0
  79. package/meteor/mongo/observe_multiplexer.ts +256 -0
  80. package/meteor/mongo/oplog-observe-driver.ts +1049 -0
  81. package/meteor/mongo/oplog_tailing.ts +414 -0
  82. package/meteor/mongo/oplog_v2_converter.ts +124 -0
  83. package/meteor/mongo/polling_observe_driver.ts +247 -0
  84. package/meteor/mongo/synchronous-cursor.ts +293 -0
  85. package/meteor/mongo/synchronous-queue.ts +119 -0
  86. package/meteor/ordered-dict/ordered_dict.ts +229 -0
  87. package/meteor/random/AbstractRandomGenerator.ts +99 -0
  88. package/meteor/random/AleaRandomGenerator.ts +96 -0
  89. package/meteor/random/NodeRandomGenerator.ts +37 -0
  90. package/meteor/random/createAleaGenerator.ts +31 -0
  91. package/meteor/random/createRandom.ts +19 -0
  92. package/meteor/random/main.ts +8 -0
  93. package/package.json +30 -0
  94. package/tsconfig.json +10 -0
@@ -0,0 +1,275 @@
1
+ import {
2
+ combineImportantPathsIntoProjection,
3
+ compileDocumentSelector,
4
+ hasOwn,
5
+ isNumericKey,
6
+ isOperatorObject,
7
+ nothingMatcher,
8
+ _modify,
9
+ _pathsElidingNumericKeys,
10
+ } from './minimongo_common';
11
+ import { Filter } from 'mongodb';
12
+ import { clone, isBinary } from '../ejson/ejson';
13
+
14
+ // The minimongo selector compiler!
15
+
16
+ // Terminology:
17
+ // - a 'selector' is the EJSON object representing a selector
18
+ // - a 'matcher' is its compiled form (whether a full Minimongo.Matcher
19
+ // object or one of the component lambdas that matches parts of it)
20
+ // - a 'result object' is an object with a 'result' field and maybe
21
+ // distance and arrayIndices.
22
+ // - a 'branched value' is an object with a 'value' field and maybe
23
+ // 'dontIterate' and 'arrayIndices'.
24
+ // - a 'document' is a top-level object that can be stored in a collection.
25
+ // - a 'lookup function' is a function that takes in a document and returns
26
+ // an array of 'branched values'.
27
+ // - a 'branched matcher' maps from an array of branched values to a result
28
+ // object.
29
+ // - an 'element matcher' maps from a single value to a bool.
30
+
31
+ // Main entry point.
32
+ // var matcher = new Minimongo.Matcher({a: {$gt: 5}});
33
+ // if (matcher.documentMatches({a: 7})) ...
34
+
35
+ export class MinimongoMatcher {
36
+ private _paths: Record<string, any>;
37
+ private _hasGeoQuery: boolean;
38
+ private _hasWhere: boolean;
39
+ private _isSimple: boolean;
40
+ private _matchingDocument: any;
41
+ private _selector: Filter<any>;
42
+ private _docMatcher: any;
43
+ private _isUpdate: boolean;
44
+
45
+ constructor(selector: Filter<any>, isUpdate?: boolean) {
46
+ // A set (object mapping string -> *) of all of the document paths looked
47
+ // at by the selector. Also includes the empty string if it may look at any
48
+ // path (eg, $where).
49
+ this._paths = {};
50
+ // Set to true if compilation finds a $near.
51
+ this._hasGeoQuery = false;
52
+ // Set to true if compilation finds a $where.
53
+ this._hasWhere = false;
54
+ // Set to false if compilation finds anything other than a simple equality
55
+ // or one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used
56
+ // with scalars as operands.
57
+ this._isSimple = true;
58
+ // Set to a dummy document which always matches this Matcher. Or set to null
59
+ // if such document is too hard to find.
60
+ this._matchingDocument = undefined;
61
+ // A clone of the original selector. It may just be a function if the user
62
+ // passed in a function; otherwise is definitely an object (eg, IDs are
63
+ // translated into {_id: ID} first. Used by canBecomeTrueByModifier and
64
+ // Sorter._useWithMatcher.
65
+ this._selector = null;
66
+ this._docMatcher = this._compileSelector(selector);
67
+ // Set to true if selection is done for an update operation
68
+ // Default is false
69
+ // Used for $near array update (issue #3599)
70
+ this._isUpdate = isUpdate;
71
+ }
72
+
73
+ documentMatches(doc) {
74
+ if (doc !== Object(doc)) {
75
+ throw Error('documentMatches needs a document');
76
+ }
77
+
78
+ return this._docMatcher(doc);
79
+ }
80
+
81
+ hasGeoQuery() {
82
+ return this._hasGeoQuery;
83
+ }
84
+
85
+ hasWhere() {
86
+ return this._hasWhere;
87
+ }
88
+
89
+ isSimple() {
90
+ return this._isSimple;
91
+ }
92
+
93
+ // Given a selector, return a function that takes one argument, a
94
+ // document. It returns a result object.
95
+ _compileSelector(selector: Filter<any>) {
96
+ // you can pass a literal function instead of a selector
97
+ if (selector instanceof Function) {
98
+ this._isSimple = false;
99
+ this._selector = selector;
100
+ this._recordPathUsed('');
101
+
102
+ return doc => ({ result: !!selector.call(doc) });
103
+ }
104
+
105
+ // protect against dangerous selectors. falsey and {_id: falsey} are both
106
+ // likely programmer error, and not what you want, particularly for
107
+ // destructive operations.
108
+ if (!selector || hasOwn.call(selector, '_id') && !selector._id) {
109
+ this._isSimple = false;
110
+ return nothingMatcher;
111
+ }
112
+
113
+ // Top level can't be an array or true or binary.
114
+ if (Array.isArray(selector) ||
115
+ isBinary(selector) ||
116
+ typeof selector === 'boolean') {
117
+ throw new Error(`Invalid selector: ${selector}`);
118
+ }
119
+
120
+ this._selector = clone(selector);
121
+
122
+ return compileDocumentSelector(selector, this, { isRoot: true });
123
+ }
124
+
125
+ affectedByModifier(modifier) {
126
+ // safe check for $set/$unset being objects
127
+ modifier = Object.assign({ $set: {}, $unset: {} }, modifier);
128
+
129
+ const meaningfulPaths = this._getPaths();
130
+ const modifiedPaths = [].concat(
131
+ Object.keys(modifier.$set),
132
+ Object.keys(modifier.$unset)
133
+ );
134
+
135
+ return modifiedPaths.some(path => {
136
+ const mod = path.split('.');
137
+
138
+ return meaningfulPaths.some(meaningfulPath => {
139
+ const sel = meaningfulPath.split('.');
140
+
141
+ let i = 0, j = 0;
142
+
143
+ while (i < sel.length && j < mod.length) {
144
+ if (isNumericKey(sel[i]) && isNumericKey(mod[j])) {
145
+ // foo.4.bar selector affected by foo.4 modifier
146
+ // foo.3.bar selector unaffected by foo.4 modifier
147
+ if (sel[i] === mod[j]) {
148
+ i++;
149
+ j++;
150
+ } else {
151
+ return false;
152
+ }
153
+ } else if (isNumericKey(sel[i])) {
154
+ // foo.4.bar selector unaffected by foo.bar modifier
155
+ return false;
156
+ } else if (isNumericKey(mod[j])) {
157
+ j++;
158
+ } else if (sel[i] === mod[j]) {
159
+ i++;
160
+ j++;
161
+ } else {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ // One is a prefix of another, taking numeric fields into account
167
+ return true;
168
+ });
169
+ });
170
+ }
171
+
172
+ canBecomeTrueByModifier = function (modifier) {
173
+ if (!this.affectedByModifier(modifier)) {
174
+ return false;
175
+ }
176
+
177
+ if (!this.isSimple()) {
178
+ return true;
179
+ }
180
+
181
+ modifier = Object.assign({ $set: {}, $unset: {} }, modifier);
182
+
183
+ const modifierPaths = [].concat(
184
+ Object.keys(modifier.$set),
185
+ Object.keys(modifier.$unset)
186
+ );
187
+
188
+ if (this._getPaths().some(pathHasNumericKeys) ||
189
+ modifierPaths.some(pathHasNumericKeys)) {
190
+ return true;
191
+ }
192
+
193
+ // check if there is a $set or $unset that indicates something is an
194
+ // object rather than a scalar in the actual object where we saw $-operator
195
+ // NOTE: it is correct since we allow only scalars in $-operators
196
+ // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would
197
+ // definitely set the result to false as 'a.b' appears to be an object.
198
+ const expectedScalarIsObject = Object.keys(this._selector).some(path => {
199
+ if (!isOperatorObject(this._selector[path])) {
200
+ return false;
201
+ }
202
+
203
+ return modifierPaths.some(modifierPath =>
204
+ modifierPath.startsWith(`${path}.`)
205
+ );
206
+ });
207
+
208
+ if (expectedScalarIsObject) {
209
+ return false;
210
+ }
211
+
212
+ // See if we can apply the modifier on the ideally matching object. If it
213
+ // still matches the selector, then the modifier could have turned the real
214
+ // object in the database into something matching.
215
+ const matchingDocument = clone(this.matchingDocument());
216
+
217
+ // The selector is too complex, anything can happen.
218
+ if (matchingDocument === null) {
219
+ return true;
220
+ }
221
+
222
+ try {
223
+ _modify(matchingDocument, modifier);
224
+ } catch (error) {
225
+ // Couldn't set a property on a field which is a scalar or null in the
226
+ // selector.
227
+ // Example:
228
+ // real document: { 'a.b': 3 }
229
+ // selector: { 'a': 12 }
230
+ // converted selector (ideal document): { 'a': 12 }
231
+ // modifier: { $set: { 'a.b': 4 } }
232
+ // We don't know what real document was like but from the error raised by
233
+ // $set on a scalar field we can reason that the structure of real document
234
+ // is completely different.
235
+ if (error.name === 'MinimongoError' && error.setPropertyError) {
236
+ return false;
237
+ }
238
+
239
+ throw error;
240
+ }
241
+
242
+ return this.documentMatches(matchingDocument).result;
243
+ }
244
+
245
+ // Knows how to combine a mongo selector and a fields projection to a new fields
246
+ // projection taking into account active fields from the passed selector.
247
+ // @returns Object - projection object (same as fields option of mongo cursor)
248
+ combineIntoProjection(projection) {
249
+ const selectorPaths = _pathsElidingNumericKeys(this._getPaths());
250
+
251
+ // Special case for $where operator in the selector - projection should depend
252
+ // on all fields of the document. getSelectorPaths returns a list of paths
253
+ // selector depends on. If one of the paths is '' (empty string) representing
254
+ // the root or the whole document, complete projection should be returned.
255
+ if (selectorPaths.includes('')) {
256
+ return {};
257
+ }
258
+
259
+ return combineImportantPathsIntoProjection(selectorPaths, projection);
260
+ }
261
+
262
+ // Returns a list of key paths the given selector is looking for. It includes
263
+ // the empty string if there is a $where.
264
+ _getPaths() {
265
+ return Object.keys(this._paths);
266
+ }
267
+
268
+ _recordPathUsed(path) {
269
+ this._paths[path] = true;
270
+ }
271
+ }
272
+
273
+ function pathHasNumericKeys(path: string) {
274
+ return path.split('.').some(isNumericKey);
275
+ }
@@ -0,0 +1,331 @@
1
+ import {
2
+ combineImportantPathsIntoProjection,
3
+ expandArraysInBranches,
4
+ hasOwn,
5
+ makeLookupFunction,
6
+ _f,
7
+ _pathsElidingNumericKeys,
8
+ } from './minimongo_common';
9
+ import { MinimongoMatcher } from './minimongo_matcher';
10
+
11
+ // Give a sort spec, which can be in any of these forms:
12
+ // {"key1": 1, "key2": -1}
13
+ // [["key1", "asc"], ["key2", "desc"]]
14
+ // ["key1", ["key2", "desc"]]
15
+ //
16
+ // (.. with the first form being dependent on the key enumeration
17
+ // behavior of your javascript VM, which usually does what you mean in
18
+ // this case if the key names don't look like integers ..)
19
+ //
20
+ // return a function that takes two objects, and returns -1 if the
21
+ // first object comes first in order, 1 if the second object comes
22
+ // first, or 0 if neither object comes before the other.
23
+
24
+ export default class MinimongoSorter {
25
+ private _sortSpecParts: {ascending: boolean, path: string, lookup: Function}[];
26
+ private _sortFunction: (a: any, b: any) => number | null;
27
+ private _keyComparator: (a: any, b: any) => number;
28
+ private _selectorForAffectedByModifier: MinimongoMatcher;
29
+
30
+ constructor(spec) {
31
+ this._sortSpecParts = [];
32
+ this._sortFunction = null;
33
+
34
+ const addSpecPart = (path: string, ascending: boolean) => {
35
+ if (!path) {
36
+ throw Error('sort keys must be non-empty');
37
+ }
38
+
39
+ if (path.charAt(0) === '$') {
40
+ throw Error(`unsupported sort key: ${path}`);
41
+ }
42
+
43
+ this._sortSpecParts.push({
44
+ ascending,
45
+ lookup: makeLookupFunction(path, { forSort: true }),
46
+ path
47
+ });
48
+ };
49
+
50
+ if (spec instanceof Array) {
51
+ spec.forEach(element => {
52
+ if (typeof element === 'string') {
53
+ addSpecPart(element, true);
54
+ } else {
55
+ addSpecPart(element[0], element[1] !== 'desc');
56
+ }
57
+ });
58
+ } else if (typeof spec === 'object') {
59
+ Object.keys(spec).forEach(key => {
60
+ addSpecPart(key, spec[key] >= 0);
61
+ });
62
+ } else if (typeof spec === 'function') {
63
+ this._sortFunction = spec;
64
+ } else {
65
+ throw Error(`Bad sort specification: ${JSON.stringify(spec)}`);
66
+ }
67
+
68
+ // If a function is specified for sorting, we skip the rest.
69
+ if (this._sortFunction) {
70
+ return;
71
+ }
72
+
73
+ // To implement affectedByModifier, we piggy-back on top of Matcher's
74
+ // affectedByModifier code; we create a selector that is affected by the
75
+ // same modifiers as this sort order. This is only implemented on the
76
+ // server.
77
+ if (this.affectedByModifier) {
78
+ const selector = {};
79
+
80
+ this._sortSpecParts.forEach(spec => {
81
+ selector[spec.path] = 1;
82
+ });
83
+
84
+ this._selectorForAffectedByModifier = new MinimongoMatcher(selector);
85
+ }
86
+
87
+ this._keyComparator = composeComparators(
88
+ this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i))
89
+ );
90
+ }
91
+
92
+ affectedByModifier(modifier) {
93
+ return this._selectorForAffectedByModifier.affectedByModifier(modifier);
94
+ }
95
+
96
+ getComparator(options?): (a: any, b: any) => number {
97
+ // If sort is specified or have no distances, just use the comparator from
98
+ // the source specification (which defaults to "everything is equal".
99
+ // issue #3599
100
+ // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation
101
+ // sort effectively overrides $near
102
+ if (this._sortSpecParts.length || !options || !options.distances) {
103
+ return this._getBaseComparator();
104
+ }
105
+
106
+ const distances = options.distances;
107
+
108
+ // Return a comparator which compares using $near distances.
109
+ return (a, b) => {
110
+ if (!distances.has(a._id)) {
111
+ throw Error(`Missing distance for ${a._id}`);
112
+ }
113
+
114
+ if (!distances.has(b._id)) {
115
+ throw Error(`Missing distance for ${b._id}`);
116
+ }
117
+
118
+ return distances.get(a._id) - distances.get(b._id);
119
+ };
120
+ }
121
+
122
+ // Takes in two keys: arrays whose lengths match the number of spec
123
+ // parts. Returns negative, 0, or positive based on using the sort spec to
124
+ // compare fields.
125
+ _compareKeys(key1, key2) {
126
+ if (key1.length !== this._sortSpecParts.length ||
127
+ key2.length !== this._sortSpecParts.length) {
128
+ throw Error('Key has wrong length');
129
+ }
130
+
131
+ return this._keyComparator(key1, key2);
132
+ }
133
+
134
+ // Iterates over each possible "key" from doc (ie, over each branch), calling
135
+ // 'cb' with the key.
136
+ _generateKeysFromDoc(doc, cb) {
137
+ if (this._sortSpecParts.length === 0) {
138
+ throw new Error('can\'t generate keys without a spec');
139
+ }
140
+
141
+ const pathFromIndices = indices => `${indices.join(',')},`;
142
+
143
+ let knownPaths = null;
144
+
145
+ // maps index -> ({'' -> value} or {path -> value})
146
+ const valuesByIndexAndPath = this._sortSpecParts.map(spec => {
147
+ // Expand any leaf arrays that we find, and ignore those arrays
148
+ // themselves. (We never sort based on an array itself.)
149
+ let branches = expandArraysInBranches(spec.lookup(doc), true);
150
+
151
+ // If there are no values for a key (eg, key goes to an empty array),
152
+ // pretend we found one undefined value.
153
+ if (!branches.length) {
154
+ branches = [{ value: void 0 }];
155
+ }
156
+
157
+ const element = Object.create(null);
158
+ let usedPaths = false;
159
+
160
+ branches.forEach(branch => {
161
+ if (!branch.arrayIndices) {
162
+ // If there are no array indices for a branch, then it must be the
163
+ // only branch, because the only thing that produces multiple branches
164
+ // is the use of arrays.
165
+ if (branches.length > 1) {
166
+ throw Error('multiple branches but no array used?');
167
+ }
168
+
169
+ element[''] = branch.value;
170
+ return;
171
+ }
172
+
173
+ usedPaths = true;
174
+
175
+ const path = pathFromIndices(branch.arrayIndices);
176
+
177
+ if (hasOwn.call(element, path)) {
178
+ throw Error(`duplicate path: ${path}`);
179
+ }
180
+
181
+ element[path] = branch.value;
182
+
183
+ // If two sort fields both go into arrays, they have to go into the
184
+ // exact same arrays and we have to find the same paths. This is
185
+ // roughly the same condition that makes MongoDB throw this strange
186
+ // error message. eg, the main thing is that if sort spec is {a: 1,
187
+ // b:1} then a and b cannot both be arrays.
188
+ //
189
+ // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a'
190
+ // and 'a.x.y' are both arrays, but we don't allow this for now.
191
+ // #NestedArraySort
192
+ // XXX achieve full compatibility here
193
+ if (knownPaths && !hasOwn.call(knownPaths, path)) {
194
+ throw Error('cannot index parallel arrays');
195
+ }
196
+ });
197
+
198
+ if (knownPaths) {
199
+ // Similarly to above, paths must match everywhere, unless this is a
200
+ // non-array field.
201
+ if (!hasOwn.call(element, '') &&
202
+ Object.keys(knownPaths).length !== Object.keys(element).length) {
203
+ throw Error('cannot index parallel arrays!');
204
+ }
205
+ } else if (usedPaths) {
206
+ knownPaths = {};
207
+
208
+ Object.keys(element).forEach(path => {
209
+ knownPaths[path] = true;
210
+ });
211
+ }
212
+
213
+ return element;
214
+ });
215
+
216
+ if (!knownPaths) {
217
+ // Easy case: no use of arrays.
218
+ const soleKey = valuesByIndexAndPath.map(values => {
219
+ if (!hasOwn.call(values, '')) {
220
+ throw Error('no value in sole key case?');
221
+ }
222
+
223
+ return values[''];
224
+ });
225
+
226
+ cb(soleKey);
227
+
228
+ return;
229
+ }
230
+
231
+ Object.keys(knownPaths).forEach(path => {
232
+ const key = valuesByIndexAndPath.map(values => {
233
+ if (hasOwn.call(values, '')) {
234
+ return values[''];
235
+ }
236
+
237
+ if (!hasOwn.call(values, path)) {
238
+ throw Error('missing path?');
239
+ }
240
+
241
+ return values[path];
242
+ });
243
+
244
+ cb(key);
245
+ });
246
+ }
247
+
248
+ // Returns a comparator that represents the sort specification (but not
249
+ // including a possible geoquery distance tie-breaker).
250
+ _getBaseComparator() {
251
+ if (this._sortFunction) {
252
+ return this._sortFunction;
253
+ }
254
+
255
+ // If we're only sorting on geoquery distance and no specs, just say
256
+ // everything is equal.
257
+ if (!this._sortSpecParts.length) {
258
+ return (doc1, doc2) => 0;
259
+ }
260
+
261
+ return (doc1, doc2) => {
262
+ const key1 = this._getMinKeyFromDoc(doc1);
263
+ const key2 = this._getMinKeyFromDoc(doc2);
264
+ return this._compareKeys(key1, key2);
265
+ };
266
+ }
267
+
268
+ // Finds the minimum key from the doc, according to the sort specs. (We say
269
+ // "minimum" here but this is with respect to the sort spec, so "descending"
270
+ // sort fields mean we're finding the max for that field.)
271
+ //
272
+ // Note that this is NOT "find the minimum value of the first field, the
273
+ // minimum value of the second field, etc"... it's "choose the
274
+ // lexicographically minimum value of the key vector, allowing only keys which
275
+ // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x:
276
+ // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and
277
+ // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key.
278
+ _getMinKeyFromDoc(doc) {
279
+ let minKey = null;
280
+
281
+ this._generateKeysFromDoc(doc, key => {
282
+ if (minKey === null) {
283
+ minKey = key;
284
+ return;
285
+ }
286
+
287
+ if (this._compareKeys(key, minKey) < 0) {
288
+ minKey = key;
289
+ }
290
+ });
291
+
292
+ return minKey;
293
+ }
294
+
295
+ _getPaths() {
296
+ return this._sortSpecParts.map(part => part.path);
297
+ }
298
+
299
+ // Given an index 'i', returns a comparator that compares two key arrays based
300
+ // on field 'i'.
301
+ _keyFieldComparator(i: number) {
302
+ const invert = !this._sortSpecParts[i].ascending;
303
+
304
+ return (key1, key2) => {
305
+ const compare = _f._cmp(key1[i], key2[i]);
306
+ return invert ? -compare : compare;
307
+ };
308
+ }
309
+
310
+ combineIntoProjection = function (projection) {
311
+ return combineImportantPathsIntoProjection(_pathsElidingNumericKeys(this._getPaths()), projection);
312
+ }
313
+
314
+ }
315
+
316
+ // Given an array of comparators
317
+ // (functions (a,b)->(negative or positive or zero)), returns a single
318
+ // comparator which uses each comparator in order and returns the first
319
+ // non-zero value.
320
+ function composeComparators(comparatorArray) {
321
+ return (a, b) => {
322
+ for (let i = 0; i < comparatorArray.length; ++i) {
323
+ const compare = comparatorArray[i](a, b);
324
+ if (compare !== 0) {
325
+ return compare;
326
+ }
327
+ }
328
+
329
+ return 0;
330
+ };
331
+ }
@@ -0,0 +1,79 @@
1
+ import MongoDB from "mongodb";
2
+ import { _InvalidationCrossbar } from "../ddp/crossbar";
3
+ import { CursorDescription } from "./live_cursor";
4
+
5
+ // Listen for the invalidation messages that will trigger us to poll the
6
+ // database for changes. If this selector specifies specific IDs, specify them
7
+ // here, so that updates to different specific IDs don't cause us to poll.
8
+ // listenCallback is the same kind of (notification, complete) callback passed
9
+ // to InvalidationCrossbar.listen.
10
+
11
+ export function listenAll(cursorDescription: CursorDescription<any>, listenCallback: Function) {
12
+ var listeners = [];
13
+ forEachTrigger(cursorDescription, function (trigger) {
14
+ listeners.push(_InvalidationCrossbar.listen(trigger, listenCallback));
15
+ });
16
+
17
+ return {
18
+ stop: function () {
19
+ for (const listener of listeners) {
20
+ listener.stop();
21
+ }
22
+ }
23
+ };
24
+ }
25
+
26
+ export function forEachTrigger(cursorDescription: CursorDescription<any>, triggerCallback: Function) {
27
+ var key = { collection: cursorDescription.collectionName };
28
+ var specificIds = _idsMatchedBySelector(cursorDescription.selector);
29
+ if (specificIds) {
30
+ for (const id of specificIds) {
31
+ triggerCallback(Object.assign({ id: id }, key));
32
+ }
33
+ triggerCallback(Object.assign({ dropCollection: true, id: null }, key));
34
+ } else {
35
+ triggerCallback(key);
36
+ }
37
+ // Everyone cares about the database being dropped.
38
+ triggerCallback({ dropDatabase: true });
39
+ }
40
+
41
+ export function _idsMatchedBySelector(selector: MongoDB.Filter<{ _id: string }>) {
42
+ if (!selector) {
43
+ return null;
44
+ }
45
+
46
+ // Do we have an _id clause?
47
+ if (selector.hasOwnProperty('_id')) {
48
+ // Is the _id clause just an ID?
49
+ if (typeof selector._id === "string") {
50
+ return [selector._id];
51
+ }
52
+
53
+ // Is the _id clause {_id: {$in: ["x", "y", "z"]}}?
54
+ if (selector._id
55
+ && "$in" in selector._id
56
+ && Array.isArray(selector._id.$in)
57
+ && selector._id.$in.length
58
+ && selector._id.$in.every(id => typeof id === "string")) {
59
+ return selector._id.$in;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ // If this is a top-level $and, and any of the clauses constrain their
66
+ // documents, then the whole selector is constrained by any one clause's
67
+ // constraint. (Well, by their intersection, but that seems unlikely.)
68
+ if ("$and" in selector && Array.isArray(selector.$and)) {
69
+ for (let i = 0; i < selector.$and.length; ++i) {
70
+ const subIds = _idsMatchedBySelector(selector.$and[i]);
71
+
72
+ if (subIds) {
73
+ return subIds;
74
+ }
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }