1 |
|
2 |
|
3 | const { EventEmitter } = require('events');
|
4 | const generateId = require('./generate-id');
|
5 |
|
6 | type Options = {
|
7 | maxAge?:number,
|
8 | bufferPublishing?:number
|
9 | };
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | class ObservedRemoveMap<K, V> extends EventEmitter {
|
18 | maxAge: number;
|
19 | bufferPublishing: number;
|
20 | pairs: Map<K, [string, V]>;
|
21 | deletions: Map<string, K>;
|
22 | deleteQueue: Array<*>;
|
23 | insertQueue: Array<*>;
|
24 | publishTimeout: null | TimeoutID;
|
25 |
|
26 | constructor(entries?: Iterable<[K, V]>, options?: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 |
|
44 |
|
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 |
|
80 |
|
81 |
|
82 |
|
83 | sync(queue?: [Array<*>, Array<*>] = this.dump()) {
|
84 | this.emit('publish', queue);
|
85 | }
|
86 |
|
87 | |
88 |
|
89 |
|
90 |
|
91 | dump():[Array<*>, Array<*>] {
|
92 | return [[...this.pairs], [...this.deletions]];
|
93 | }
|
94 |
|
95 | process(queue:[Array<*>, Array<*>], skipFlush?: boolean = 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:K, value:V, id?: string = 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:K): V | void {
|
138 | const pair = this.pairs.get(key);
|
139 | if (pair) {
|
140 | return pair[1];
|
141 | }
|
142 | }
|
143 |
|
144 | delete(key:K):void {
|
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(): void {
|
155 | for (const key of this.keys()) {
|
156 | this.delete(key);
|
157 | }
|
158 | }
|
159 |
|
160 | * entries():Iterator<[K, V]> {
|
161 | for (const [key, [id, value]] of this.pairs) {
|
162 | yield [key, value];
|
163 | }
|
164 | }
|
165 |
|
166 | forEach(callback:Function, thisArg?:any):void {
|
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:K): boolean {
|
179 | return !!this.pairs.get(key);
|
180 | }
|
181 |
|
182 | keys():Iterator<K> {
|
183 | return this.pairs.keys();
|
184 | }
|
185 |
|
186 | * values():Iterator<V> {
|
187 | for (const [id, value] of this.pairs.values()) {
|
188 | yield value;
|
189 | }
|
190 | }
|
191 |
|
192 | get size():number {
|
193 | return this.pairs.size;
|
194 | }
|
195 | }
|
196 |
|
197 | module.exports = ObservedRemoveMap;
|