event-storage 0.7.2 → 0.9.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/src/Consumer.js CHANGED
@@ -1,14 +1,28 @@
1
1
  const stream = require('stream');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const mkdirpSync = require('mkdirp').sync;
5
- const { assert } = require('./util');
4
+ const { assert, ensureDirectory } = require('./util');
6
5
 
7
6
  const Storage = require('./Storage/ReadableStorage');
8
7
  const MAX_CATCHUP_BATCH = 10;
9
8
 
10
9
  /**
11
- * Implements an event-driven durable Consumer that provides at-least-once delivery semantics.
10
+ * Safely unlink a file and ignore if it doesn't exist.
11
+ * @param {string} filename
12
+ */
13
+ const safeUnlink = (filename) => {
14
+ /* istanbul ignore next */
15
+ try {
16
+ fs.unlinkSync(filename);
17
+ } catch (e) {
18
+ if (e.code !== "ENOENT") {
19
+ throw e;
20
+ }
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Implements an event-driven durable Consumer that provides at-least-once delivery semantics or exactly-once processing semantics if only using setState().
12
26
  */
13
27
  class Consumer extends stream.Readable {
14
28
 
@@ -16,9 +30,10 @@ class Consumer extends stream.Readable {
16
30
  * @param {Storage} storage The storage to create the consumer for.
17
31
  * @param {string} indexName The name of the index to consume.
18
32
  * @param {string} identifier The unique name to identify this consumer.
33
+ * @param {object} [initialState] The initial state of the consumer.
19
34
  * @param {number} [startFrom] The revision to start from within the index to consume.
20
35
  */
21
- constructor(storage, indexName, identifier, startFrom = 0) {
36
+ constructor(storage, indexName, identifier, initialState = {}, startFrom = 0) {
22
37
  super({ objectMode: true });
23
38
 
24
39
  assert(storage instanceof Storage, 'Must provide a storage for the consumer.');
@@ -26,7 +41,7 @@ class Consumer extends stream.Readable {
26
41
  assert(typeof identifier === 'string' && identifier !== '', 'Must specify an identifier name for the consumer.');
27
42
 
28
43
  this.initializeStorage(storage, indexName, identifier);
29
- this.restoreState(startFrom);
44
+ this.restoreState(initialState, startFrom);
30
45
  this.handler = this.handleNewDocument.bind(this);
31
46
  this.on('error', () => (this.handleDocument = false));
32
47
  }
@@ -43,10 +58,8 @@ class Consumer extends stream.Readable {
43
58
  this.indexName = indexName;
44
59
  const consumerDirectory = path.join(this.storage.indexDirectory, 'consumers');
45
60
  this.fileName = path.join(consumerDirectory, this.storage.storageFile + '.' + indexName + '.' + identifier);
46
- if (!fs.existsSync(consumerDirectory)) {
47
- mkdirpSync(consumerDirectory);
48
- } else {
49
- this.cleanUpFailedWrites(consumerDirectory);
61
+ if (ensureDirectory(consumerDirectory)) {
62
+ this.cleanUpFailedWrites();
50
63
  }
51
64
  }
52
65
 
@@ -60,28 +73,34 @@ class Consumer extends stream.Readable {
60
73
  const files = fs.readdirSync(consumerDirectory);
61
74
  for (let file of files) {
62
75
  if (file.startsWith(consumerNamePrefix)) {
63
- fs.unlinkSync(path.join(consumerDirectory, file));
76
+ safeUnlink(path.join(consumerDirectory, file));
64
77
  }
65
78
  }
66
79
  }
67
80
 
68
81
  /**
69
82
  * @private
83
+ * @param {object} initialState The initial state if no persisted state exists.
70
84
  * @param {number} startFrom The revision to start from within the index to consume.
71
85
  */
72
- restoreState(startFrom) {
86
+ restoreState(initialState, startFrom) {
73
87
  /* istanbul ignore if */
74
88
  if (!this.fileName) {
75
89
  return;
76
90
  }
91
+ if (typeof initialState === 'number') {
92
+ startFrom = initialState;
93
+ initialState = {};
94
+ }
77
95
  try {
78
96
  const consumerData = fs.readFileSync(this.fileName);
79
97
  this.position = consumerData.readInt32LE(0);
80
98
  this.state = JSON.parse(consumerData.toString('utf8', 4));
81
99
  } catch (e) {
82
100
  this.position = startFrom;
83
- this.state = {};
101
+ this.state = initialState;
84
102
  }
103
+ Object.freeze(this.state);
85
104
 
86
105
  this.persisting = null;
87
106
  this.consuming = false;
@@ -91,13 +110,18 @@ class Consumer extends stream.Readable {
91
110
  * Update the state of this consumer transactionally with the position.
92
111
  * May only be called from within the document handling callback.
93
112
  *
94
- * @param {object} newState
113
+ * @param {object|function(object):object} newState
114
+ * @param {boolean} [persist] Set to false if this state update should not be persisted yet
95
115
  * @api
96
116
  */
97
- setState(newState) {
117
+ setState(newState, persist = true) {
98
118
  assert(this.handleDocument, 'Called setState outside of document handler!');
99
119
 
120
+ if (typeof newState === 'function') {
121
+ newState = newState(this.state);
122
+ }
100
123
  this.state = Object.freeze(newState);
124
+ this.doPersist = persist;
101
125
  }
102
126
 
103
127
  /**
@@ -113,6 +137,7 @@ class Consumer extends stream.Readable {
113
137
  return;
114
138
  }
115
139
 
140
+ /* istanbul ignore if */
116
141
  if (this.position !== position - 1) {
117
142
  return;
118
143
  }
@@ -123,7 +148,9 @@ class Consumer extends stream.Readable {
123
148
  this.stop();
124
149
  }
125
150
  this.position = position;
126
- this.persist();
151
+ if (this.doPersist) {
152
+ this.persist();
153
+ }
127
154
  }
128
155
 
129
156
  /**
@@ -141,7 +168,7 @@ class Consumer extends stream.Readable {
141
168
  const consumerData = Buffer.allocUnsafe(4 + consumerState.length);
142
169
  consumerData.writeInt32LE(this.position, 0);
143
170
  consumerData.write(consumerState, 4, consumerState.length, 'utf-8');
144
- var tmpFile = this.fileName + '.' + this.position;
171
+ const tmpFile = this.fileName + '.' + this.position;
145
172
  this.persisting = null;
146
173
  /* istanbul ignore if */
147
174
  if (fs.existsSync(tmpFile)) {
@@ -151,10 +178,10 @@ class Consumer extends stream.Readable {
151
178
  fs.writeFileSync(tmpFile, consumerData);
152
179
  // If the write fails (half-way), the consumer state file will not be corrupted
153
180
  fs.renameSync(tmpFile, this.fileName);
154
- this.emit('persisted');
181
+ this.emit('persisted', consumerState);
155
182
  } catch (e) {
156
183
  /* istanbul ignore next */
157
- fs.unlinkSync(tmpFile);
184
+ safeUnlink(tmpFile);
158
185
  }
159
186
  });
160
187
  }
@@ -237,6 +264,29 @@ class Consumer extends stream.Readable {
237
264
  this.handleDocument = false;
238
265
  }
239
266
 
267
+ /**
268
+ * Reset this projection to restart processing all documents again.
269
+ * NOTE: This will overwrite the current state of the projection and hence be destructive.
270
+ * @param {object} [initialState] The initial state of the consumer.
271
+ * @param {number} [startFrom] The revision to start from within the index to consume.
272
+ * @api
273
+ */
274
+ reset(initialState = {}, startFrom = 0) {
275
+ if (typeof initialState === 'number') {
276
+ startFrom = initialState;
277
+ initialState = {};
278
+ }
279
+ const restart = this.consuming;
280
+ this.stop();
281
+ this.state = Object.freeze(initialState);
282
+ this.position = startFrom;
283
+ this.persist();
284
+ if (restart) {
285
+ this.start();
286
+ }
287
+ }
288
+
289
+ // noinspection JSUnusedGlobalSymbols
240
290
  /**
241
291
  * Readable stream implementation.
242
292
  * @private