UNPKG

5.4 kBJavaScriptView Raw
1//
2
3const { EventEmitter } = require('events');
4const generateId = require('./generate-id');
5
6
7
8
9
10
11/**
12 * Class representing a Observed Remove Map
13 *
14 * Implements all methods and iterators of the native `Map` object in addition to the following.
15 * See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map}
16 */
17class ObservedRemoveMap extends EventEmitter {
18
19
20
21
22
23
24
25
26 constructor(entries , options = {}) {
27 super();
28 this.maxAge = typeof options.maxAge === 'undefined' ? 5000 : options.maxAge;
29 this.bufferPublishing = typeof options.bufferPublishing === 'undefined' ? 30 : options.bufferPublishing;
30 this.publishTimeout = null;
31 this.pairs = new Map();
32 this.deletions = new Map();
33 this.insertQueue = [];
34 this.deleteQueue = [];
35 if (!entries) {
36 return;
37 }
38 for (const [key, value] of entries) {
39 this.set(key, value);
40 }
41 }
42
43 /* :: @@iterator() { return ({} ); } */
44 // $FlowFixMe: computed property
45 [Symbol.iterator]() {
46 return this.entries();
47 }
48
49 dequeue() {
50 if (this.publishTimeout) {
51 return;
52 }
53 if (this.bufferPublishing > 0) {
54 this.publishTimeout = setTimeout(() => this.publish(), this.bufferPublishing);
55 } else {
56 this.publish();
57 }
58 }
59
60 publish() {
61 this.publishTimeout = null;
62 const insertQueue = this.insertQueue;
63 const deleteQueue = this.deleteQueue;
64 this.insertQueue = [];
65 this.deleteQueue = [];
66 this.sync([insertQueue, deleteQueue]);
67 }
68
69 flush() {
70 const maxAgeString = (Date.now() - this.maxAge).toString(36).padStart(9, '0');
71 for (const [id] of this.deletions) {
72 if (id < maxAgeString) {
73 this.deletions.delete(id);
74 }
75 }
76 }
77
78 /**
79 * Emit a 'publish' event containing a specified queue or all of the set's insertions and deletions.
80 * @param {Array<Array<any>>} queue - Array of insertions and deletions
81 * @return {void}
82 */
83 sync(queue = this.dump()) {
84 this.emit('publish', queue);
85 }
86
87 /**
88 * Return an array containing all of the map's insertions and deletions.
89 * @return {[Array<*>, Array<*>]>}
90 */
91 dump() {
92 return [[...this.pairs], [...this.deletions]];
93 }
94
95 process(queue , skipFlush = false) {
96 const [insertions, deletions] = queue;
97 for (const [id, key] of deletions) {
98 this.deletions.set(id, key);
99 }
100 for (const [key, [id, value]] of insertions) {
101 if (this.deletions.has(id)) {
102 continue;
103 }
104 const pair = this.pairs.get(key);
105 if (!pair || (pair && pair[0] < id)) {
106 if (typeof value === 'undefined') {
107 this.pairs.set(key, [id]);
108 } else {
109 this.pairs.set(key, [id, value]);
110 }
111 this.emit('set', key, value, pair && pair[1] ? pair[1] : undefined);
112 } else if (pair && pair[0] === id) {
113 this.emit('affirm', key, value, pair ? pair[1] : undefined);
114 }
115 }
116 for (const [id, key] of deletions) {
117 const pair = this.pairs.get(key);
118 if (pair && pair[0] === id) {
119 this.pairs.delete(key);
120 this.emit('delete', key, pair[1]);
121 }
122 }
123 if (!skipFlush) {
124 this.flush();
125 }
126 }
127
128 set(key , value , id = generateId()) {
129 const pair = this.pairs.get(key);
130 const insertMessage = typeof value === 'undefined' ? [key, [id]] : [key, [id, value]];
131 if (pair) {
132 const deleteMessage = [pair[0], key];
133 this.process([[insertMessage], [deleteMessage]], true);
134 this.deleteQueue.push(deleteMessage);
135 } else {
136 this.process([[insertMessage], []], true);
137 }
138 this.insertQueue.push(insertMessage);
139 this.dequeue();
140 return this;
141 }
142
143 get(key ) { // eslint-disable-line consistent-return
144 const pair = this.pairs.get(key);
145 if (pair) {
146 return pair[1];
147 }
148 }
149
150 delete(key ) {
151 const pair = this.pairs.get(key);
152 if (pair) {
153 const message = [pair[0], key];
154 this.process([[], [message]], true);
155 this.deleteQueue.push(message);
156 this.dequeue();
157 }
158 }
159
160 clear() {
161 for (const key of this.keys()) {
162 this.delete(key);
163 }
164 }
165
166 * entries() {
167 for (const [key, [id, value]] of this.pairs) { // eslint-disable-line no-unused-vars
168 yield [key, value];
169 }
170 }
171
172 forEach(callback , thisArg ) {
173 if (thisArg) {
174 for (const [key, value] of this.entries()) {
175 callback.bind(thisArg)(value, key, this);
176 }
177 } else {
178 for (const [key, value] of this.entries()) {
179 callback(value, key, this);
180 }
181 }
182 }
183
184 has(key ) {
185 return !!this.pairs.get(key);
186 }
187
188 keys() {
189 return this.pairs.keys();
190 }
191
192 * values() {
193 for (const [id, value] of this.pairs.values()) { // eslint-disable-line no-unused-vars
194 yield value;
195 }
196 }
197
198 get size() {
199 return this.pairs.size;
200 }
201}
202
203module.exports = ObservedRemoveMap;