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 | declare maxAge: number;
|
19 | declare bufferPublishing: number;
|
20 | declare pairs: Map<K, Array<*>>;
|
21 | declare deletions: Map<string, K>;
|
22 | declare deleteQueue: Array<*>;
|
23 | declare insertQueue: Array<*>;
|
24 | declare 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 | 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:K, value:V, id?: string = 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:K): V | void {
|
144 | const pair = this.pairs.get(key);
|
145 | if (pair) {
|
146 | return pair[1];
|
147 | }
|
148 | }
|
149 |
|
150 | delete(key:K):void {
|
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(): void {
|
161 | for (const key of this.keys()) {
|
162 | this.delete(key);
|
163 | }
|
164 | }
|
165 |
|
166 | * entries():Iterator<[K, V]> {
|
167 | for (const [key, [id, value]] of this.pairs) {
|
168 | yield [key, value];
|
169 | }
|
170 | }
|
171 |
|
172 | forEach(callback:Function, thisArg?:any):void {
|
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:K): boolean {
|
185 | return !!this.pairs.get(key);
|
186 | }
|
187 |
|
188 | keys():Iterator<K> {
|
189 | return this.pairs.keys();
|
190 | }
|
191 |
|
192 | * values():Iterator<V> {
|
193 | for (const [id, value] of this.pairs.values()) {
|
194 | yield value;
|
195 | }
|
196 | }
|
197 |
|
198 | get size():number {
|
199 | return this.pairs.size;
|
200 | }
|
201 | }
|
202 |
|
203 | module.exports = ObservedRemoveMap;
|