UNPKG

5.07 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(): Iterator<[K, V]> { return ({}: any); } */
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 this.pairs.set(key, [id, value]);
107 this.emit('set', key, value, pair ? pair[1] : undefined);
108 }
109 }
110 for (const [id, key] of deletions) {
111 const pair = this.pairs.get(key);
112 if (pair && pair[0] === id) {
113 this.pairs.delete(key);
114 this.emit('delete', key, pair[1]);
115 }
116 }
117 if (!skipFlush) {
118 this.flush();
119 }
120 }
121
122 set(key , value , id = generateId()) {
123 const pair = this.pairs.get(key);
124 const insertMessage = [key, [id, value]];
125 if (pair) {
126 const deleteMessage = [pair[0], key];
127 this.process([[insertMessage], [deleteMessage]], true);
128 this.deleteQueue.push(deleteMessage);
129 } else {
130 this.process([[insertMessage], []], true);
131 }
132 this.insertQueue.push(insertMessage);
133 this.dequeue();
134 return this;
135 }
136
137 get(key ) { // eslint-disable-line consistent-return
138 const pair = this.pairs.get(key);
139 if (pair) {
140 return pair[1];
141 }
142 }
143
144 delete(key ) {
145 const pair = this.pairs.get(key);
146 if (pair) {
147 const message = [pair[0], key];
148 this.process([[], [message]], true);
149 this.deleteQueue.push(message);
150 this.dequeue();
151 }
152 }
153
154 clear() {
155 for (const key of this.keys()) {
156 this.delete(key);
157 }
158 }
159
160 * entries() {
161 for (const [key, [id, value]] of this.pairs) { // eslint-disable-line no-unused-vars
162 yield [key, value];
163 }
164 }
165
166 forEach(callback , thisArg ) {
167 if (thisArg) {
168 for (const [key, value] of this.entries()) {
169 callback.bind(thisArg)(value, key, this);
170 }
171 } else {
172 for (const [key, value] of this.entries()) {
173 callback(value, key, this);
174 }
175 }
176 }
177
178 has(key ) {
179 return !!this.pairs.get(key);
180 }
181
182 keys() {
183 return this.pairs.keys();
184 }
185
186 * values() {
187 for (const [id, value] of this.pairs.values()) { // eslint-disable-line no-unused-vars
188 yield value;
189 }
190 }
191
192 get size() {
193 return this.pairs.size;
194 }
195}
196
197module.exports = ObservedRemoveMap;