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.
- package/README.md +63 -0
- package/dist/livedata_server.js +9 -0
- package/dist/meteor/binary-heap/max_heap.js +186 -0
- package/dist/meteor/binary-heap/min_heap.js +17 -0
- package/dist/meteor/binary-heap/min_max_heap.js +48 -0
- package/dist/meteor/callback-hook/hook.js +78 -0
- package/dist/meteor/ddp/crossbar.js +136 -0
- package/dist/meteor/ddp/heartbeat.js +77 -0
- package/dist/meteor/ddp/livedata_server.js +403 -0
- package/dist/meteor/ddp/method-invocation.js +72 -0
- package/dist/meteor/ddp/random-stream.js +100 -0
- package/dist/meteor/ddp/session-collection-view.js +106 -0
- package/dist/meteor/ddp/session-document-view.js +82 -0
- package/dist/meteor/ddp/session.js +570 -0
- package/dist/meteor/ddp/stream_server.js +181 -0
- package/dist/meteor/ddp/subscription.js +347 -0
- package/dist/meteor/ddp/utils.js +104 -0
- package/dist/meteor/ddp/writefence.js +111 -0
- package/dist/meteor/diff-sequence/diff.js +257 -0
- package/dist/meteor/ejson/ejson.js +569 -0
- package/dist/meteor/ejson/stringify.js +119 -0
- package/dist/meteor/ejson/utils.js +42 -0
- package/dist/meteor/id-map/id_map.js +92 -0
- package/dist/meteor/mongo/caching_change_observer.js +94 -0
- package/dist/meteor/mongo/doc_fetcher.js +53 -0
- package/dist/meteor/mongo/geojson_utils.js +41 -0
- package/dist/meteor/mongo/live_connection.js +264 -0
- package/dist/meteor/mongo/live_cursor.js +57 -0
- package/dist/meteor/mongo/minimongo_common.js +2002 -0
- package/dist/meteor/mongo/minimongo_matcher.js +217 -0
- package/dist/meteor/mongo/minimongo_sorter.js +268 -0
- package/dist/meteor/mongo/observe_driver_utils.js +73 -0
- package/dist/meteor/mongo/observe_multiplexer.js +228 -0
- package/dist/meteor/mongo/oplog-observe-driver.js +919 -0
- package/dist/meteor/mongo/oplog_tailing.js +352 -0
- package/dist/meteor/mongo/oplog_v2_converter.js +126 -0
- package/dist/meteor/mongo/polling_observe_driver.js +195 -0
- package/dist/meteor/mongo/synchronous-cursor.js +261 -0
- package/dist/meteor/mongo/synchronous-queue.js +110 -0
- package/dist/meteor/ordered-dict/ordered_dict.js +198 -0
- package/dist/meteor/random/AbstractRandomGenerator.js +92 -0
- package/dist/meteor/random/AleaRandomGenerator.js +90 -0
- package/dist/meteor/random/NodeRandomGenerator.js +42 -0
- package/dist/meteor/random/createAleaGenerator.js +32 -0
- package/dist/meteor/random/createRandom.js +22 -0
- package/dist/meteor/random/main.js +12 -0
- package/livedata_server.ts +3 -0
- package/meteor/LICENSE +28 -0
- package/meteor/binary-heap/max_heap.ts +225 -0
- package/meteor/binary-heap/min_heap.ts +15 -0
- package/meteor/binary-heap/min_max_heap.ts +53 -0
- package/meteor/callback-hook/hook.ts +85 -0
- package/meteor/ddp/crossbar.ts +148 -0
- package/meteor/ddp/heartbeat.ts +97 -0
- package/meteor/ddp/livedata_server.ts +473 -0
- package/meteor/ddp/method-invocation.ts +86 -0
- package/meteor/ddp/random-stream.ts +102 -0
- package/meteor/ddp/session-collection-view.ts +119 -0
- package/meteor/ddp/session-document-view.ts +92 -0
- package/meteor/ddp/session.ts +708 -0
- package/meteor/ddp/stream_server.ts +204 -0
- package/meteor/ddp/subscription.ts +392 -0
- package/meteor/ddp/utils.ts +119 -0
- package/meteor/ddp/writefence.ts +130 -0
- package/meteor/diff-sequence/diff.ts +295 -0
- package/meteor/ejson/ejson.ts +601 -0
- package/meteor/ejson/stringify.ts +122 -0
- package/meteor/ejson/utils.ts +38 -0
- package/meteor/id-map/id_map.ts +84 -0
- package/meteor/mongo/caching_change_observer.ts +120 -0
- package/meteor/mongo/doc_fetcher.ts +52 -0
- package/meteor/mongo/geojson_utils.ts +42 -0
- package/meteor/mongo/live_connection.ts +302 -0
- package/meteor/mongo/live_cursor.ts +79 -0
- package/meteor/mongo/minimongo_common.ts +2440 -0
- package/meteor/mongo/minimongo_matcher.ts +275 -0
- package/meteor/mongo/minimongo_sorter.ts +331 -0
- package/meteor/mongo/observe_driver_utils.ts +79 -0
- package/meteor/mongo/observe_multiplexer.ts +256 -0
- package/meteor/mongo/oplog-observe-driver.ts +1049 -0
- package/meteor/mongo/oplog_tailing.ts +414 -0
- package/meteor/mongo/oplog_v2_converter.ts +124 -0
- package/meteor/mongo/polling_observe_driver.ts +247 -0
- package/meteor/mongo/synchronous-cursor.ts +293 -0
- package/meteor/mongo/synchronous-queue.ts +119 -0
- package/meteor/ordered-dict/ordered_dict.ts +229 -0
- package/meteor/random/AbstractRandomGenerator.ts +99 -0
- package/meteor/random/AleaRandomGenerator.ts +96 -0
- package/meteor/random/NodeRandomGenerator.ts +37 -0
- package/meteor/random/createAleaGenerator.ts +31 -0
- package/meteor/random/createRandom.ts +19 -0
- package/meteor/random/main.ts +8 -0
- package/package.json +30 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
MongoDB Live Data Server
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
This project is essentially a MongoDB live data driver (based either on polling or on Oplog tailing) combined with a DDP server, extracted
|
|
5
|
+
out of [Meteor](https://github.com/meteor/meteor), with **Fibers** and **underscore** dependencies removed and code converted to Typescript.
|
|
6
|
+
|
|
7
|
+
Live data is one of the root concepts of Meteor. Data is served via WebSockets via the DDP protocol and updated automatically whenever something changes in the database. Also, calling server methods via WebSocket is supported.
|
|
8
|
+
|
|
9
|
+
Using Meteor locks you into the Meteor ecosystem, which has some problems (mostly for historical reasons). Using live data as a separate npm package might be preferable in many scenarios. Also, people who are trying to migrate from Meteor, might find this package useful as an intermediate step.
|
|
10
|
+
|
|
11
|
+
### Usage
|
|
12
|
+
|
|
13
|
+
As a most common example, this is how you can use livedata with Express.js:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { DDPServer, LiveCursor, LiveMongoConnection } = require('livedata-server')
|
|
17
|
+
const express = require('express')
|
|
18
|
+
const app = express()
|
|
19
|
+
const port = 3000
|
|
20
|
+
|
|
21
|
+
app.get('/', (req, res) => {
|
|
22
|
+
res.send('Hello World!')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const httpServer = app.listen(port, () => {
|
|
26
|
+
console.log(`Example app listening on port ${port}`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const liveMongoConnection = new LiveMongoConnection(process.env.MONGO_URL, {
|
|
30
|
+
oplogUrl: process.env.MONGO_OPLOG_URL
|
|
31
|
+
});
|
|
32
|
+
const liveDataServer = new DDPServer({}, httpServer);
|
|
33
|
+
|
|
34
|
+
liveDataServer.methods({
|
|
35
|
+
"test-method": async (msg) => {
|
|
36
|
+
console.log("Test msg: ", msg);
|
|
37
|
+
return "hello! Current timestamp is: " + Date.now()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
liveDataServer.publish({
|
|
42
|
+
"test-subscription": async () => {
|
|
43
|
+
return new LiveCursor(liveMongoConnection, "test-collection", { category: "apples" });
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`liveDataServer.methods` and `liveDataServer.publish` have exactly same interface as [Meteor.methods](https://docs.meteor.com/api/methods.html#Meteor-methods) and [Meteor.publish](https://docs.meteor.com/api/pubsub.html#Meteor-publish) respectively, notice however that when publishing subscriptions, you must use `LiveCursor` rather than a normal MongoDB cursor.
|
|
50
|
+
|
|
51
|
+
### Important notes
|
|
52
|
+
|
|
53
|
+
- The project is in alpha. Use at your own risk.
|
|
54
|
+
- Neither method context nor subscription context have the `unblock` method anymore (because this package doesn't use Fibers)
|
|
55
|
+
- Meteor syntax for MongoDB queries is not supported. Please always use MongoDB Node.js driver syntax. For example, instead of
|
|
56
|
+
```ts
|
|
57
|
+
const doc = myCollection.findOne(id);
|
|
58
|
+
```
|
|
59
|
+
use
|
|
60
|
+
```ts
|
|
61
|
+
const doc = await myCollection.findOne({ _id: id });
|
|
62
|
+
```
|
|
63
|
+
- Neither MongoDB.ObjectId nor it's Meteor.js alternative is supported at the moment. String ids only.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LiveCursor = exports.LiveMongoConnection = exports.DDPServer = void 0;
|
|
4
|
+
var livedata_server_1 = require("./meteor/ddp/livedata_server");
|
|
5
|
+
Object.defineProperty(exports, "DDPServer", { enumerable: true, get: function () { return livedata_server_1.DDPServer; } });
|
|
6
|
+
var live_connection_1 = require("./meteor/mongo/live_connection");
|
|
7
|
+
Object.defineProperty(exports, "LiveMongoConnection", { enumerable: true, get: function () { return live_connection_1.LiveMongoConnection; } });
|
|
8
|
+
var live_cursor_1 = require("./meteor/mongo/live_cursor");
|
|
9
|
+
Object.defineProperty(exports, "LiveCursor", { enumerable: true, get: function () { return live_cursor_1.LiveCursor; } });
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaxHeap = void 0;
|
|
4
|
+
// Constructor of Heap
|
|
5
|
+
// - comparator - Function - given two items returns a number
|
|
6
|
+
// - options:
|
|
7
|
+
// - initData - Array - Optional - the initial data in a format:
|
|
8
|
+
// Object:
|
|
9
|
+
// - id - String - unique id of the item
|
|
10
|
+
// - value - Any - the data value
|
|
11
|
+
// each value is retained
|
|
12
|
+
// - IdMap - Constructor - Optional - custom IdMap class to store id->index
|
|
13
|
+
// mappings internally. Standard IdMap is used by default.
|
|
14
|
+
class MaxHeap {
|
|
15
|
+
constructor(comparator, options = {}) {
|
|
16
|
+
if (typeof comparator !== 'function') {
|
|
17
|
+
throw new Error('Passed comparator is invalid, should be a comparison function');
|
|
18
|
+
}
|
|
19
|
+
// a C-style comparator that is given two values and returns a number,
|
|
20
|
+
// negative if the first value is less than the second, positive if the second
|
|
21
|
+
// value is greater than the first and zero if they are equal.
|
|
22
|
+
this._comparator = comparator;
|
|
23
|
+
if (!options.IdMap) {
|
|
24
|
+
options.IdMap = Map;
|
|
25
|
+
}
|
|
26
|
+
// _heapIdx maps an id to an index in the Heap array the corresponding value
|
|
27
|
+
// is located on.
|
|
28
|
+
this._heapIdx = new options.IdMap;
|
|
29
|
+
// The Heap data-structure implemented as a 0-based contiguous array where
|
|
30
|
+
// every item on index idx is a node in a complete binary tree. Every node can
|
|
31
|
+
// have children on indexes idx*2+1 and idx*2+2, except for the leaves. Every
|
|
32
|
+
// node has a parent on index (idx-1)/2;
|
|
33
|
+
this._heap = [];
|
|
34
|
+
// If the initial array is passed, we can build the heap in linear time
|
|
35
|
+
// complexity (O(N)) compared to linearithmic time complexity (O(nlogn)) if
|
|
36
|
+
// we push elements one by one.
|
|
37
|
+
if (Array.isArray(options.initData)) {
|
|
38
|
+
this._initFromData(options.initData);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Builds a new heap in-place in linear time based on passed data
|
|
42
|
+
_initFromData(data) {
|
|
43
|
+
this._heap = data.map(({ id, value }) => ({ id, value }));
|
|
44
|
+
data.forEach(({ id }, i) => this._heapIdx.set(id, i));
|
|
45
|
+
if (!data.length) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// start from the first non-leaf - the parent of the last leaf
|
|
49
|
+
for (let i = parentIdx(data.length - 1); i >= 0; i--) {
|
|
50
|
+
this._downHeap(i);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
_downHeap(idx) {
|
|
54
|
+
while (leftChildIdx(idx) < this.size()) {
|
|
55
|
+
const left = leftChildIdx(idx);
|
|
56
|
+
const right = rightChildIdx(idx);
|
|
57
|
+
let largest = idx;
|
|
58
|
+
if (left < this.size()) {
|
|
59
|
+
largest = this._maxIndex(largest, left);
|
|
60
|
+
}
|
|
61
|
+
if (right < this.size()) {
|
|
62
|
+
largest = this._maxIndex(largest, right);
|
|
63
|
+
}
|
|
64
|
+
if (largest === idx) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
this._swap(largest, idx);
|
|
68
|
+
idx = largest;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_upHeap(idx) {
|
|
72
|
+
while (idx > 0) {
|
|
73
|
+
const parent = parentIdx(idx);
|
|
74
|
+
if (this._maxIndex(parent, idx) === idx) {
|
|
75
|
+
this._swap(parent, idx);
|
|
76
|
+
idx = parent;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
_maxIndex(idxA, idxB) {
|
|
84
|
+
const valueA = this._get(idxA);
|
|
85
|
+
const valueB = this._get(idxB);
|
|
86
|
+
return this._comparator(valueA, valueB) >= 0 ? idxA : idxB;
|
|
87
|
+
}
|
|
88
|
+
// Internal: gets raw data object placed on idxth place in heap
|
|
89
|
+
_get(idx) {
|
|
90
|
+
return this._heap[idx].value;
|
|
91
|
+
}
|
|
92
|
+
_swap(idxA, idxB) {
|
|
93
|
+
const recA = this._heap[idxA];
|
|
94
|
+
const recB = this._heap[idxB];
|
|
95
|
+
this._heapIdx.set(recA.id, idxB);
|
|
96
|
+
this._heapIdx.set(recB.id, idxA);
|
|
97
|
+
this._heap[idxA] = recB;
|
|
98
|
+
this._heap[idxB] = recA;
|
|
99
|
+
}
|
|
100
|
+
get(id) {
|
|
101
|
+
return this.has(id) ?
|
|
102
|
+
this._get(this._heapIdx.get(id)) :
|
|
103
|
+
null;
|
|
104
|
+
}
|
|
105
|
+
set(id, value) {
|
|
106
|
+
if (this.has(id)) {
|
|
107
|
+
if (this.get(id) === value) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const idx = this._heapIdx.get(id);
|
|
111
|
+
this._heap[idx].value = value;
|
|
112
|
+
// Fix the new value's position
|
|
113
|
+
// Either bubble new value up if it is greater than its parent
|
|
114
|
+
this._upHeap(idx);
|
|
115
|
+
// or bubble it down if it is smaller than one of its children
|
|
116
|
+
this._downHeap(idx);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this._heapIdx.set(id, this._heap.length);
|
|
120
|
+
this._heap.push({ id, value });
|
|
121
|
+
this._upHeap(this._heap.length - 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
remove(id) {
|
|
125
|
+
if (this.has(id)) {
|
|
126
|
+
const last = this._heap.length - 1;
|
|
127
|
+
const idx = this._heapIdx.get(id);
|
|
128
|
+
if (idx !== last) {
|
|
129
|
+
this._swap(idx, last);
|
|
130
|
+
this._heap.pop();
|
|
131
|
+
this._heapIdx.remove(id);
|
|
132
|
+
// Fix the swapped value's position
|
|
133
|
+
this._upHeap(idx);
|
|
134
|
+
this._downHeap(idx);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this._heap.pop();
|
|
138
|
+
this._heapIdx.remove(id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
has(id) {
|
|
143
|
+
return this._heapIdx.has(id);
|
|
144
|
+
}
|
|
145
|
+
empty() {
|
|
146
|
+
return !this.size();
|
|
147
|
+
}
|
|
148
|
+
clear() {
|
|
149
|
+
this._heap = [];
|
|
150
|
+
this._heapIdx.clear();
|
|
151
|
+
}
|
|
152
|
+
// iterate over values in no particular order
|
|
153
|
+
forEach(iterator) {
|
|
154
|
+
this._heap.forEach(obj => iterator(obj.value, obj.id));
|
|
155
|
+
}
|
|
156
|
+
size() {
|
|
157
|
+
return this._heap.length;
|
|
158
|
+
}
|
|
159
|
+
setDefault(id, def) {
|
|
160
|
+
if (this.has(id)) {
|
|
161
|
+
return this.get(id);
|
|
162
|
+
}
|
|
163
|
+
this.set(id, def);
|
|
164
|
+
return def;
|
|
165
|
+
}
|
|
166
|
+
clone() {
|
|
167
|
+
const clone = new MaxHeap(this._comparator, { initData: this._heap });
|
|
168
|
+
return clone;
|
|
169
|
+
}
|
|
170
|
+
maxElementId() {
|
|
171
|
+
return this.size() ? this._heap[0].id : null;
|
|
172
|
+
}
|
|
173
|
+
_selfCheck() {
|
|
174
|
+
for (let i = 1; i < this._heap.length; i++) {
|
|
175
|
+
if (this._maxIndex(parentIdx(i), i) !== parentIdx(i)) {
|
|
176
|
+
throw new Error(`An item with id ${this._heap[i].id}` +
|
|
177
|
+
" has a parent younger than it: " +
|
|
178
|
+
this._heap[parentIdx(i)].id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.MaxHeap = MaxHeap;
|
|
184
|
+
const leftChildIdx = i => i * 2 + 1;
|
|
185
|
+
const rightChildIdx = i => i * 2 + 2;
|
|
186
|
+
const parentIdx = i => (i - 1) >> 1;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MinHeap = void 0;
|
|
4
|
+
const max_heap_1 = require("./max_heap");
|
|
5
|
+
class MinHeap extends max_heap_1.MaxHeap {
|
|
6
|
+
constructor(comparator, options) {
|
|
7
|
+
super((a, b) => -comparator(a, b), options);
|
|
8
|
+
}
|
|
9
|
+
maxElementId() {
|
|
10
|
+
throw new Error("Cannot call maxElementId on MinHeap");
|
|
11
|
+
}
|
|
12
|
+
minElementId() {
|
|
13
|
+
return super.maxElementId();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.MinHeap = MinHeap;
|
|
17
|
+
;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MinMaxHeap = void 0;
|
|
4
|
+
const max_heap_1 = require("./max_heap");
|
|
5
|
+
const min_heap_1 = require("./min_heap");
|
|
6
|
+
// This implementation of Min/Max-Heap is just a subclass of Max-Heap
|
|
7
|
+
// with a Min-Heap as an encapsulated property.
|
|
8
|
+
//
|
|
9
|
+
// Most of the operations are just proxy methods to call the same method on both
|
|
10
|
+
// heaps.
|
|
11
|
+
//
|
|
12
|
+
// This implementation takes 2*N memory but is fairly simple to write and
|
|
13
|
+
// understand. And the constant factor of a simple Heap is usually smaller
|
|
14
|
+
// compared to other two-way priority queues like Min/Max Heaps
|
|
15
|
+
// (http://www.cs.otago.ac.nz/staffpriv/mike/Papers/MinMaxHeaps/MinMaxHeaps.pdf)
|
|
16
|
+
// and Interval Heaps
|
|
17
|
+
// (http://www.cise.ufl.edu/~sahni/dsaac/enrich/c13/double.htm)
|
|
18
|
+
class MinMaxHeap extends max_heap_1.MaxHeap {
|
|
19
|
+
constructor(comparator, options) {
|
|
20
|
+
super(comparator, options);
|
|
21
|
+
this._minHeap = new min_heap_1.MinHeap(comparator, options);
|
|
22
|
+
}
|
|
23
|
+
set(id, value) {
|
|
24
|
+
super.set(id, value);
|
|
25
|
+
this._minHeap.set(id, value);
|
|
26
|
+
}
|
|
27
|
+
remove(id) {
|
|
28
|
+
super.remove(id);
|
|
29
|
+
this._minHeap.remove(id);
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
super.clear();
|
|
33
|
+
this._minHeap.clear();
|
|
34
|
+
}
|
|
35
|
+
setDefault(id, def) {
|
|
36
|
+
super.setDefault(id, def);
|
|
37
|
+
return this._minHeap.setDefault(id, def);
|
|
38
|
+
}
|
|
39
|
+
clone() {
|
|
40
|
+
const clone = new MinMaxHeap(this._comparator, { initData: this._heap });
|
|
41
|
+
return clone;
|
|
42
|
+
}
|
|
43
|
+
minElementId() {
|
|
44
|
+
return this._minHeap.minElementId();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.MinMaxHeap = MinMaxHeap;
|
|
48
|
+
;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Hook = void 0;
|
|
4
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
5
|
+
class Hook {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
options = options || {};
|
|
8
|
+
this.nextCallbackId = 0;
|
|
9
|
+
this.callbacks = Object.create(null);
|
|
10
|
+
// Whether to wrap callbacks with Meteor.bindEnvironment
|
|
11
|
+
if (options.exceptionHandler) {
|
|
12
|
+
this.exceptionHandler = options.exceptionHandler;
|
|
13
|
+
}
|
|
14
|
+
else if (options.debugPrintExceptions) {
|
|
15
|
+
if (typeof options.debugPrintExceptions !== "string") {
|
|
16
|
+
throw new Error("Hook option debugPrintExceptions should be a string");
|
|
17
|
+
}
|
|
18
|
+
this.exceptionHandler = options.debugPrintExceptions;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
register(callback) {
|
|
22
|
+
var exceptionHandler = this.exceptionHandler || function (exception) {
|
|
23
|
+
// Note: this relies on the undocumented fact that if bindEnvironment's
|
|
24
|
+
// onException throws, and you are invoking the callback either in the
|
|
25
|
+
// browser or from within a Fiber in Node, the exception is propagated.
|
|
26
|
+
throw exception;
|
|
27
|
+
};
|
|
28
|
+
callback = dontBindEnvironment(callback, exceptionHandler);
|
|
29
|
+
var id = this.nextCallbackId++;
|
|
30
|
+
this.callbacks[id] = callback;
|
|
31
|
+
return {
|
|
32
|
+
callback,
|
|
33
|
+
stop: () => {
|
|
34
|
+
delete this.callbacks[id];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// For each registered callback, call the passed iterator function
|
|
39
|
+
// with the callback.
|
|
40
|
+
//
|
|
41
|
+
// The iterator function can choose whether or not to call the
|
|
42
|
+
// callback. (For example, it might not call the callback if the
|
|
43
|
+
// observed object has been closed or terminated).
|
|
44
|
+
//
|
|
45
|
+
// The iteration is stopped if the iterator function returns a falsy
|
|
46
|
+
// value or throws an exception.
|
|
47
|
+
each(iterator) {
|
|
48
|
+
var ids = Object.keys(this.callbacks);
|
|
49
|
+
for (var i = 0; i < ids.length; ++i) {
|
|
50
|
+
var id = ids[i];
|
|
51
|
+
// check to see if the callback was removed during iteration
|
|
52
|
+
if (hasOwn.call(this.callbacks, id)) {
|
|
53
|
+
var callback = this.callbacks[id];
|
|
54
|
+
if (!iterator(callback)) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.Hook = Hook;
|
|
62
|
+
function dontBindEnvironment(func, onException, _this) {
|
|
63
|
+
if (!onException || typeof (onException) === 'string') {
|
|
64
|
+
var description = onException || "callback of async function";
|
|
65
|
+
onException = function (error) {
|
|
66
|
+
console.error("Exception in " + description, error);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return function (...args) {
|
|
70
|
+
try {
|
|
71
|
+
var ret = func.apply(_this, args);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
onException(e);
|
|
75
|
+
}
|
|
76
|
+
return ret;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._InvalidationCrossbar = exports._Crossbar = void 0;
|
|
4
|
+
const ejson_1 = require("../ejson/ejson");
|
|
5
|
+
// A "crossbar" is a class that provides structured notification registration.
|
|
6
|
+
// See _match for the definition of how a notification matches a trigger.
|
|
7
|
+
// All notifications and triggers must have a string key named 'collection'.
|
|
8
|
+
class _Crossbar {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.listenersByCollection = {};
|
|
11
|
+
this.listenersByCollectionCount = {};
|
|
12
|
+
options = options || {};
|
|
13
|
+
this.nextId = 1;
|
|
14
|
+
}
|
|
15
|
+
// msg is a trigger or a notification
|
|
16
|
+
_collectionForMessage(msg) {
|
|
17
|
+
if (!msg.hasOwnProperty('collection')) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
else if (typeof (msg.collection) === 'string') {
|
|
21
|
+
if (msg.collection === '')
|
|
22
|
+
throw Error("Message has empty collection!");
|
|
23
|
+
return msg.collection;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw Error("Message has non-string collection!");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Listen for notification that match 'trigger'. A notification
|
|
30
|
+
// matches if it has the key-value pairs in trigger as a
|
|
31
|
+
// subset. When a notification matches, call 'callback', passing
|
|
32
|
+
// the actual notification.
|
|
33
|
+
//
|
|
34
|
+
// Returns a listen handle, which is an object with a method
|
|
35
|
+
// stop(). Call stop() to stop listening.
|
|
36
|
+
//
|
|
37
|
+
// XXX It should be legal to call fire() from inside a listen()
|
|
38
|
+
// callback?
|
|
39
|
+
listen(trigger, callback) {
|
|
40
|
+
var self = this;
|
|
41
|
+
var id = self.nextId++;
|
|
42
|
+
var collection = self._collectionForMessage(trigger);
|
|
43
|
+
var record = { trigger: (0, ejson_1.clone)(trigger), callback: callback };
|
|
44
|
+
if (!self.listenersByCollection.hasOwnProperty(collection)) {
|
|
45
|
+
self.listenersByCollection[collection] = {};
|
|
46
|
+
self.listenersByCollectionCount[collection] = 0;
|
|
47
|
+
}
|
|
48
|
+
self.listenersByCollection[collection][id] = record;
|
|
49
|
+
self.listenersByCollectionCount[collection]++;
|
|
50
|
+
return {
|
|
51
|
+
stop: function () {
|
|
52
|
+
delete self.listenersByCollection[collection][id];
|
|
53
|
+
self.listenersByCollectionCount[collection]--;
|
|
54
|
+
if (self.listenersByCollectionCount[collection] === 0) {
|
|
55
|
+
delete self.listenersByCollection[collection];
|
|
56
|
+
delete self.listenersByCollectionCount[collection];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Fire the provided 'notification' (an object whose attribute
|
|
62
|
+
// values are all JSON-compatibile) -- inform all matching listeners
|
|
63
|
+
// (registered with listen()).
|
|
64
|
+
//
|
|
65
|
+
// If fire() is called inside a write fence, then each of the
|
|
66
|
+
// listener callbacks will be called inside the write fence as well.
|
|
67
|
+
//
|
|
68
|
+
// The listeners may be invoked in parallel, rather than serially.
|
|
69
|
+
fire(notification) {
|
|
70
|
+
var self = this;
|
|
71
|
+
var collection = self._collectionForMessage(notification);
|
|
72
|
+
if (!self.listenersByCollection.hasOwnProperty(collection)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
var listenersForCollection = self.listenersByCollection[collection];
|
|
76
|
+
var callbackIds = [];
|
|
77
|
+
Object.entries(listenersForCollection).forEach(function ([id, l]) {
|
|
78
|
+
if (self._matches(notification, l.trigger)) {
|
|
79
|
+
callbackIds.push(id);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Listener callbacks can yield, so we need to first find all the ones that
|
|
83
|
+
// match in a single iteration over self.listenersByCollection (which can't
|
|
84
|
+
// be mutated during this iteration), and then invoke the matching
|
|
85
|
+
// callbacks, checking before each call to ensure they haven't stopped.
|
|
86
|
+
// Note that we don't have to check that
|
|
87
|
+
// self.listenersByCollection[collection] still === listenersForCollection,
|
|
88
|
+
// because the only way that stops being true is if listenersForCollection
|
|
89
|
+
// first gets reduced down to the empty object (and then never gets
|
|
90
|
+
// increased again).
|
|
91
|
+
for (const id of callbackIds) {
|
|
92
|
+
if (listenersForCollection.hasOwnProperty(id)) {
|
|
93
|
+
listenersForCollection[id].callback(notification);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// A notification matches a trigger if all keys that exist in both are equal.
|
|
98
|
+
//
|
|
99
|
+
// Examples:
|
|
100
|
+
// N:{collection: "C"} matches T:{collection: "C"}
|
|
101
|
+
// (a non-targeted write to a collection matches a
|
|
102
|
+
// non-targeted query)
|
|
103
|
+
// N:{collection: "C", id: "X"} matches T:{collection: "C"}
|
|
104
|
+
// (a targeted write to a collection matches a non-targeted query)
|
|
105
|
+
// N:{collection: "C"} matches T:{collection: "C", id: "X"}
|
|
106
|
+
// (a non-targeted write to a collection matches a
|
|
107
|
+
// targeted query)
|
|
108
|
+
// N:{collection: "C", id: "X"} matches T:{collection: "C", id: "X"}
|
|
109
|
+
// (a targeted write to a collection matches a targeted query targeted
|
|
110
|
+
// at the same document)
|
|
111
|
+
// N:{collection: "C", id: "X"} does not match T:{collection: "C", id: "Y"}
|
|
112
|
+
// (a targeted write to a collection does not match a targeted query
|
|
113
|
+
// targeted at a different document)
|
|
114
|
+
_matches(notification, trigger) {
|
|
115
|
+
// Most notifications that use the crossbar have a string `collection` and
|
|
116
|
+
// maybe an `id` that is a string or ObjectID. We're already dividing up
|
|
117
|
+
// triggers by collection, but let's fast-track "nope, different ID" (and
|
|
118
|
+
// avoid the overly generic EJSON.equals). This makes a noticeable
|
|
119
|
+
// performance difference; see https://github.com/meteor/meteor/pull/3697
|
|
120
|
+
if (typeof (notification.id) === 'string' && typeof (trigger.id) === 'string' && notification.id !== trigger.id) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return Object.entries(trigger).every(([key, triggerValue]) => {
|
|
124
|
+
return !notification.hasOwnProperty(key) || (0, ejson_1.equals)(triggerValue, notification[key]);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports._Crossbar = _Crossbar;
|
|
129
|
+
// The "invalidation crossbar" is a specific instance used by the DDP server to
|
|
130
|
+
// implement write fence notifications. Listener callbacks on this crossbar
|
|
131
|
+
// should call beginWrite on the current write fence before they return, if they
|
|
132
|
+
// want to delay the write fence from firing (ie, the DDP method-data-updated
|
|
133
|
+
// message from being sent).
|
|
134
|
+
exports._InvalidationCrossbar = new _Crossbar({
|
|
135
|
+
factName: "invalidation-crossbar-listeners"
|
|
136
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Heartbeat options:
|
|
3
|
+
// heartbeatInterval: interval to send pings, in milliseconds.
|
|
4
|
+
// heartbeatTimeout: timeout to close the connection if a reply isn't
|
|
5
|
+
// received, in milliseconds.
|
|
6
|
+
// sendPing: function to call to send a ping on the connection.
|
|
7
|
+
// onTimeout: function to call to close the connection.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Heartbeat = void 0;
|
|
10
|
+
class Heartbeat {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.heartbeatInterval = options.heartbeatInterval;
|
|
13
|
+
this.heartbeatTimeout = options.heartbeatTimeout;
|
|
14
|
+
this._sendPing = options.sendPing;
|
|
15
|
+
this._onTimeout = options.onTimeout;
|
|
16
|
+
this._seenPacket = false;
|
|
17
|
+
this._heartbeatIntervalHandle = null;
|
|
18
|
+
this._heartbeatTimeoutHandle = null;
|
|
19
|
+
}
|
|
20
|
+
stop() {
|
|
21
|
+
this._clearHeartbeatIntervalTimer();
|
|
22
|
+
this._clearHeartbeatTimeoutTimer();
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
this.stop();
|
|
26
|
+
this._startHeartbeatIntervalTimer();
|
|
27
|
+
}
|
|
28
|
+
_startHeartbeatIntervalTimer() {
|
|
29
|
+
this._heartbeatIntervalHandle = setInterval(() => this._heartbeatIntervalFired(), this.heartbeatInterval);
|
|
30
|
+
}
|
|
31
|
+
_startHeartbeatTimeoutTimer() {
|
|
32
|
+
this._heartbeatTimeoutHandle = setTimeout(() => this._heartbeatTimeoutFired(), this.heartbeatTimeout);
|
|
33
|
+
}
|
|
34
|
+
_clearHeartbeatIntervalTimer() {
|
|
35
|
+
if (this._heartbeatIntervalHandle) {
|
|
36
|
+
clearInterval(this._heartbeatIntervalHandle);
|
|
37
|
+
this._heartbeatIntervalHandle = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
_clearHeartbeatTimeoutTimer() {
|
|
41
|
+
if (this._heartbeatTimeoutHandle) {
|
|
42
|
+
clearTimeout(this._heartbeatTimeoutHandle);
|
|
43
|
+
this._heartbeatTimeoutHandle = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// The heartbeat interval timer is fired when we should send a ping.
|
|
47
|
+
_heartbeatIntervalFired() {
|
|
48
|
+
// don't send ping if we've seen a packet since we last checked,
|
|
49
|
+
// *or* if we have already sent a ping and are awaiting a timeout.
|
|
50
|
+
// That shouldn't happen, but it's possible if
|
|
51
|
+
// `this.heartbeatInterval` is smaller than
|
|
52
|
+
// `this.heartbeatTimeout`.
|
|
53
|
+
if (!this._seenPacket && !this._heartbeatTimeoutHandle) {
|
|
54
|
+
this._sendPing();
|
|
55
|
+
// Set up timeout, in case a pong doesn't arrive in time.
|
|
56
|
+
this._startHeartbeatTimeoutTimer();
|
|
57
|
+
}
|
|
58
|
+
this._seenPacket = false;
|
|
59
|
+
}
|
|
60
|
+
// The heartbeat timeout timer is fired when we sent a ping, but we
|
|
61
|
+
// timed out waiting for the pong.
|
|
62
|
+
_heartbeatTimeoutFired() {
|
|
63
|
+
this._heartbeatTimeoutHandle = null;
|
|
64
|
+
this._onTimeout();
|
|
65
|
+
}
|
|
66
|
+
messageReceived() {
|
|
67
|
+
// Tell periodic checkin that we have seen a packet, and thus it
|
|
68
|
+
// does not need to send a ping this cycle.
|
|
69
|
+
this._seenPacket = true;
|
|
70
|
+
// If we were waiting for a pong, we got it.
|
|
71
|
+
if (this._heartbeatTimeoutHandle) {
|
|
72
|
+
this._clearHeartbeatTimeoutTimer();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.Heartbeat = Heartbeat;
|
|
77
|
+
;
|