@blackglory/structures 0.13.3 → 0.13.4
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/package.json +3 -2
- package/src/array.ts +19 -0
- package/src/big-map.ts +98 -0
- package/src/big-set.ts +72 -0
- package/src/bit-set.ts +124 -0
- package/src/box.ts +15 -0
- package/src/cons.ts +15 -0
- package/src/dynamic-typed-array.ts +201 -0
- package/src/emitter.ts +76 -0
- package/src/expirable-map.ts +122 -0
- package/src/hash-map.ts +34 -0
- package/src/hash-set.ts +38 -0
- package/src/index.ts +24 -0
- package/src/lru-map.ts +69 -0
- package/src/queue.ts +30 -0
- package/src/radix-tree.ts +264 -0
- package/src/sorted-set.ts +34 -0
- package/src/sparse-map.ts +83 -0
- package/src/sparse-set.ts +67 -0
- package/src/string-radix-tree.ts +263 -0
- package/src/string-trie-map.ts +102 -0
- package/src/tlru-map.ts +162 -0
- package/src/trie-map.ts +102 -0
- package/src/typed-bit-set.ts +129 -0
- package/src/typed-sparse-map.ts +97 -0
- package/src/typed-sparse-set.ts +71 -0
- package/src/utils/skip-list.ts +195 -0
- package/src/utils/trailing-zeros.ts +15 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class SparseSet implements Iterable<number> {
|
|
2
|
+
private indexToValue: number[] = []
|
|
3
|
+
private valueToIndex: Array<number | undefined>
|
|
4
|
+
|
|
5
|
+
get [Symbol.toStringTag](): string {
|
|
6
|
+
return this.constructor.name
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
[Symbol.iterator](): IterableIterator<number> {
|
|
10
|
+
return this.indexToValue[Symbol.iterator]()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get size(): number {
|
|
14
|
+
return this.indexToValue.length
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor(array: number[] = []) {
|
|
18
|
+
const valueToIndex: Array<number | undefined> = []
|
|
19
|
+
if (array.length > 0) {
|
|
20
|
+
for (const [index, value] of array.entries()) {
|
|
21
|
+
valueToIndex[value] = index
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.valueToIndex = valueToIndex
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
values(): IterableIterator<number> {
|
|
29
|
+
return this.indexToValue[Symbol.iterator]()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
has(value: number): boolean {
|
|
33
|
+
// 跟一般的实现不同, 不需要访问values数组.
|
|
34
|
+
return value in this.valueToIndex
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
add(value: number): void {
|
|
38
|
+
if (!this.has(value)) {
|
|
39
|
+
const index = this.indexToValue.length
|
|
40
|
+
this.indexToValue.push(value)
|
|
41
|
+
this.valueToIndex[value] = index
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
delete(value: number): boolean {
|
|
46
|
+
if (this.has(value)) {
|
|
47
|
+
const lastValue = this.indexToValue.pop()!
|
|
48
|
+
if (value === lastValue) {
|
|
49
|
+
delete this.valueToIndex[value]
|
|
50
|
+
} else {
|
|
51
|
+
const index = this.valueToIndex[value]!
|
|
52
|
+
this.indexToValue[index] = lastValue
|
|
53
|
+
this.valueToIndex[lastValue] = index
|
|
54
|
+
// 跟一般的实现不同, 直接删除值的索引.
|
|
55
|
+
delete this.valueToIndex[value]
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
} else {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear(): void {
|
|
64
|
+
this.indexToValue.length = 0
|
|
65
|
+
this.valueToIndex.length = 0
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { toArray, first, map, find } from 'iterable-operator'
|
|
2
|
+
import { isntUndefined } from 'extra-utils'
|
|
3
|
+
|
|
4
|
+
class TreeNode<T> {
|
|
5
|
+
children = new Map<string, TreeNode<T>>()
|
|
6
|
+
|
|
7
|
+
constructor(public value?: T) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class StringRadixTree<T> {
|
|
11
|
+
private root = new TreeNode<T>()
|
|
12
|
+
|
|
13
|
+
get [Symbol.toStringTag](): string {
|
|
14
|
+
return this.constructor.name
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
entries(): IterableIterator<[key: string, value: T]> {
|
|
18
|
+
return dfs(this.root, '')
|
|
19
|
+
|
|
20
|
+
function* dfs(
|
|
21
|
+
node: TreeNode<T>
|
|
22
|
+
, path: string
|
|
23
|
+
): IterableIterator<[key: string, value: T]> {
|
|
24
|
+
for (const [prefix, childNode] of node.children) {
|
|
25
|
+
const newPath = path + prefix
|
|
26
|
+
if (isntUndefined(childNode.value)) {
|
|
27
|
+
yield [newPath, childNode.value]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
yield* dfs(childNode, newPath)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
keys(): IterableIterator<string> {
|
|
36
|
+
return map(this.entries(), ([key]) => key)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
values(): IterableIterator<T> {
|
|
40
|
+
return map(this.entries(), ([, value]) => value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
set(key: string, value: T): this {
|
|
44
|
+
set(key, value, this.root)
|
|
45
|
+
return this
|
|
46
|
+
|
|
47
|
+
function set(key: string, value: T, node: TreeNode<T>): void {
|
|
48
|
+
const commonPrefix = findCommonPrefix(node.children.keys(), key)
|
|
49
|
+
if (isntUndefined(commonPrefix)) {
|
|
50
|
+
const { prefix, commonPartLength } = commonPrefix
|
|
51
|
+
if (prefix.length === commonPartLength) {
|
|
52
|
+
// 现有节点的前缀整个都被使用的情况, 不需要在这一级创建新的前缀节点.
|
|
53
|
+
const prefixNode = node.children.get(prefix)!
|
|
54
|
+
|
|
55
|
+
if (key.length === prefix.length) {
|
|
56
|
+
// 前缀节点就是目标节点.
|
|
57
|
+
prefixNode.value = value
|
|
58
|
+
} else {
|
|
59
|
+
// 前缀节点不是目标节点
|
|
60
|
+
const newKey = key.slice(prefix.length)
|
|
61
|
+
set(newKey, value, prefixNode)
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// 并非整个现有节点的前缀都被使用, 说明需要在这一级创建新的前缀节点,
|
|
65
|
+
// 并将原前缀节点作为子节点接在新前缀节点之后.
|
|
66
|
+
const oldPrefixNode = node.children.get(prefix)!
|
|
67
|
+
const commonPrefix = prefix.slice(0, commonPartLength)
|
|
68
|
+
const newPrefixNode = new TreeNode<T>()
|
|
69
|
+
node.children.set(commonPrefix, newPrefixNode)
|
|
70
|
+
newPrefixNode.children.set(prefix.slice(commonPartLength), oldPrefixNode)
|
|
71
|
+
newPrefixNode.children.set(key.slice(commonPartLength), new TreeNode<T>(value))
|
|
72
|
+
node.children.delete(prefix)
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
node.children.set(key, new TreeNode(value))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
has(key: string): boolean {
|
|
81
|
+
return has(key, this.root)
|
|
82
|
+
|
|
83
|
+
function has(key: string, node: TreeNode<T>): boolean {
|
|
84
|
+
const matchedPrefix = matchPrefix(node.children.keys(), key)
|
|
85
|
+
if (isntUndefined(matchedPrefix)) {
|
|
86
|
+
const restKey = key.slice(matchedPrefix.length)
|
|
87
|
+
if (restKey.length === 0) {
|
|
88
|
+
return true
|
|
89
|
+
} else {
|
|
90
|
+
return has(restKey, node.children.get(matchedPrefix)!)
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(key: string): T | undefined {
|
|
99
|
+
return get(key, this.root)
|
|
100
|
+
|
|
101
|
+
function get(key: string, node: TreeNode<T>): T | undefined {
|
|
102
|
+
const matchedPrefix = matchPrefix(node.children.keys(), key)
|
|
103
|
+
if (isntUndefined(matchedPrefix)) {
|
|
104
|
+
const restKey = key.slice(matchedPrefix.length)
|
|
105
|
+
const nextNode = node.children.get(matchedPrefix)!
|
|
106
|
+
if (restKey.length === 0) {
|
|
107
|
+
return nextNode.value
|
|
108
|
+
} else {
|
|
109
|
+
return get(restKey, nextNode)
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
delete(key: string): boolean {
|
|
118
|
+
return _delete(key, this.root)
|
|
119
|
+
|
|
120
|
+
function _delete(key: string, node: TreeNode<T>): boolean {
|
|
121
|
+
const matchedPrefix = matchPrefix(node.children.keys(), key)
|
|
122
|
+
if (isntUndefined(matchedPrefix)) {
|
|
123
|
+
if (key.length === matchedPrefix.length) {
|
|
124
|
+
// 下一个子节点就是目标节点.
|
|
125
|
+
const targetNode = node.children.get(matchedPrefix)!
|
|
126
|
+
if (targetNode.children.size === 0) {
|
|
127
|
+
// 要删除的目标节点不是一个父节点, 可以直接删除.
|
|
128
|
+
// 与目标节点共享相同前缀的其他节点可能会因此这次的删除而导致发生与父节点的合并.
|
|
129
|
+
node.children.delete(matchedPrefix)
|
|
130
|
+
} else {
|
|
131
|
+
// 要删除的目标节点是一个父节点, 首先删除目标节点包含的值, 然后检查它的子节点是否只有一个.
|
|
132
|
+
delete node.value
|
|
133
|
+
if (targetNode.children.size === 1) {
|
|
134
|
+
// 如果子节点只有一个, 说明目标节点的子节点可以与目标节点合并.
|
|
135
|
+
// 即`(prefix, child)`合并为`(prefixchild)`.
|
|
136
|
+
const [key, value] = first(targetNode.children.entries())!
|
|
137
|
+
node.children.set(matchedPrefix + key, value)
|
|
138
|
+
node.children.delete(matchedPrefix)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return true
|
|
142
|
+
} else {
|
|
143
|
+
// 下一个子节点不是目标节点.
|
|
144
|
+
const nextNode = node.children.get(matchedPrefix)!
|
|
145
|
+
const nextKey = key.slice(matchedPrefix.length)
|
|
146
|
+
const deleted = _delete(nextKey, nextNode)
|
|
147
|
+
if (deleted) {
|
|
148
|
+
// 最终找到了目标节点并删除了它, 现在执行与nextNode有关的合并操作, 以删除不必要的节点.
|
|
149
|
+
if (!('value' in nextNode)) {
|
|
150
|
+
// 只有在nextNode自身不具有值的情况下, nextNode才是能够被合并的.
|
|
151
|
+
switch (nextNode.children.size) {
|
|
152
|
+
case 0: {
|
|
153
|
+
// 自身没有值的nextNode节点是不必要的, 因为它不是一个父节点.
|
|
154
|
+
node.children.delete(matchedPrefix)
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
case 1: {
|
|
158
|
+
// 自身没有值的nextNode节点是不必要的, 虽然它是一个父节点, 但它只有一个子节点.
|
|
159
|
+
// 这说明nextNode的子节点可以与nextNode合并, 替代nextNode当前的位置.
|
|
160
|
+
// 即`(prefix, child)`合并为`(prefixchild)`.
|
|
161
|
+
const [key, value] = first(nextNode.children.entries())!
|
|
162
|
+
node.children.set(matchedPrefix + key, value)
|
|
163
|
+
node.children.delete(matchedPrefix)
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
default: {
|
|
167
|
+
// 自身没有值, 但起前缀作用的nextNode节点是必要的,
|
|
168
|
+
// 但在有后代被删除的情况下, 可能会需要合并子节点的前缀.
|
|
169
|
+
// 在此检查子节点的前缀, 如果子节点具有共同前缀, 则合并前缀.
|
|
170
|
+
const entries = toArray(nextNode.children.entries())
|
|
171
|
+
const keys = entries.map(([key]) => key)
|
|
172
|
+
const commonPrefix = getCommonPrefix(keys)
|
|
173
|
+
if (commonPrefix) {
|
|
174
|
+
const newPrefix = matchedPrefix + commonPrefix
|
|
175
|
+
const childNode = new TreeNode<T>()
|
|
176
|
+
for (const [key, value] of entries) {
|
|
177
|
+
childNode.children.set(key.slice(commonPrefix.length), value)
|
|
178
|
+
}
|
|
179
|
+
node.children.set(newPrefix, childNode)
|
|
180
|
+
node.children.delete(matchedPrefix)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true
|
|
186
|
+
} else {
|
|
187
|
+
// 最终没有找到目标节点.
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 查找prefixes里与path的前缀匹配的项目.
|
|
200
|
+
*
|
|
201
|
+
* @param prefixes 所有前缀都不能在首部有重合.
|
|
202
|
+
*/
|
|
203
|
+
export function matchPrefix(prefixes: Iterable<string>, path: string): string | undefined {
|
|
204
|
+
return find(prefixes, prefix => path.startsWith(prefix))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 查找prefixes与path的共同前缀, 结果可能是其中一整个prefix, 或者其中一个prefix的前缀部分.
|
|
209
|
+
*
|
|
210
|
+
* @param prefixes 所有前缀都不能在首部有重合.
|
|
211
|
+
*/
|
|
212
|
+
export function findCommonPrefix(prefixes: Iterable<string>, path: string): {
|
|
213
|
+
prefix: string
|
|
214
|
+
commonPartLength: number
|
|
215
|
+
} | undefined {
|
|
216
|
+
for (const prefix of prefixes) {
|
|
217
|
+
let commonPartLength = 0
|
|
218
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
219
|
+
if (path[i] === prefix[i]) {
|
|
220
|
+
commonPartLength++
|
|
221
|
+
} else {
|
|
222
|
+
if (commonPartLength > 0) {
|
|
223
|
+
return { prefix, commonPartLength }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (commonPartLength > 0) {
|
|
228
|
+
return { prefix, commonPartLength }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 返回一组路径的共同前缀.
|
|
235
|
+
*/
|
|
236
|
+
export function getCommonPrefix(paths: string[]): string {
|
|
237
|
+
if (paths.length > 0) {
|
|
238
|
+
const commonPrefix: string[] = []
|
|
239
|
+
|
|
240
|
+
loop: for (
|
|
241
|
+
let charIndex = 0, firstPathLength = paths[0].length
|
|
242
|
+
; charIndex < firstPathLength
|
|
243
|
+
; charIndex++
|
|
244
|
+
) {
|
|
245
|
+
const char = paths[0][charIndex]
|
|
246
|
+
for (let pathIndex = 1; pathIndex < paths.length; pathIndex++) {
|
|
247
|
+
const path = paths[pathIndex]
|
|
248
|
+
if (charIndex < path.length) {
|
|
249
|
+
if (path[charIndex] !== char) {
|
|
250
|
+
break loop
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
break loop
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
commonPrefix.push(char)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return commonPrefix.join('')
|
|
260
|
+
} else {
|
|
261
|
+
return ''
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { zip, toArray, map } from 'iterable-operator'
|
|
2
|
+
import { isntUndefined } from 'extra-utils'
|
|
3
|
+
|
|
4
|
+
class TrieNode<T> {
|
|
5
|
+
children = new Map<string, TrieNode<T>>()
|
|
6
|
+
|
|
7
|
+
constructor(public value?: T) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class StringTrieMap<T> {
|
|
11
|
+
private root = new TrieNode<T>()
|
|
12
|
+
|
|
13
|
+
get [Symbol.toStringTag](): string {
|
|
14
|
+
return this.constructor.name
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
entries(): IterableIterator<[key: string, value: T]> {
|
|
18
|
+
return dfs(this.root, '')
|
|
19
|
+
|
|
20
|
+
function* dfs(node: TrieNode<T>, path: string): IterableIterator<[key: string, value: T]> {
|
|
21
|
+
for (const [char, childNode] of node.children) {
|
|
22
|
+
const newPath = path + char
|
|
23
|
+
if (isntUndefined(childNode.value)) {
|
|
24
|
+
yield [newPath, childNode.value]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
yield* dfs(childNode, newPath)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
keys(): IterableIterator<string> {
|
|
33
|
+
return map(this.entries(), ([key]) => key)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
values(): IterableIterator<T> {
|
|
37
|
+
return map(this.entries(), ([_, value]) => value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set(key: string, value: T): this {
|
|
41
|
+
let node = this.root
|
|
42
|
+
for (const char of key) {
|
|
43
|
+
if (!node.children.has(char)) {
|
|
44
|
+
node.children.set(char, new TrieNode<T>())
|
|
45
|
+
}
|
|
46
|
+
node = node.children.get(char)!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
node.value = value
|
|
50
|
+
return this
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
has(key: string): boolean {
|
|
54
|
+
let node = this.root
|
|
55
|
+
for (const char of key) {
|
|
56
|
+
if (node.children.has(char)) {
|
|
57
|
+
node = node.children.get(char)!
|
|
58
|
+
} else {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return node.value !== undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get(key: string): T | undefined {
|
|
67
|
+
let node = this.root
|
|
68
|
+
for (const char of key) {
|
|
69
|
+
if (node.children.has(char)) {
|
|
70
|
+
node = node.children.get(char)!
|
|
71
|
+
} else {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return node.value
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
delete(key: string): boolean {
|
|
80
|
+
const parentNodes: TrieNode<T>[] = []
|
|
81
|
+
let node = this.root
|
|
82
|
+
for (const char of key) {
|
|
83
|
+
if (node.children.has(char)) {
|
|
84
|
+
parentNodes.push(node)
|
|
85
|
+
node = node.children.get(char)!
|
|
86
|
+
} else {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete node.value
|
|
92
|
+
|
|
93
|
+
// 如果节点没有后缀, 则代表这是末端节点, 往前删除其前缀
|
|
94
|
+
if (node.children.size === 0) {
|
|
95
|
+
for (const [char, parentNode] of toArray(zip(key, parentNodes)).reverse()) {
|
|
96
|
+
parentNode.children.delete(char)
|
|
97
|
+
if (parentNode.children.size !== 0) break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/tlru-map.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { assert } from '@blackglory/errors'
|
|
2
|
+
import { first } from 'iterable-operator'
|
|
3
|
+
import { setSchedule } from 'extra-timers'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TLRUMap = LRUMap + ExpirableMap
|
|
7
|
+
*/
|
|
8
|
+
export class TLRUMap<K, V> {
|
|
9
|
+
private limit: number
|
|
10
|
+
private map = new Map<K, V>()
|
|
11
|
+
private cancelClearTimeout?: () => void
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 按过期时间升序排列所有项目的元数据
|
|
15
|
+
*/
|
|
16
|
+
protected itemMetadataSortedByExpirationTime: Array<{
|
|
17
|
+
key: K
|
|
18
|
+
expirationTime: number
|
|
19
|
+
}> = []
|
|
20
|
+
|
|
21
|
+
get [Symbol.toStringTag](): string {
|
|
22
|
+
return this.constructor.name
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get size(): number {
|
|
26
|
+
return this.map.size
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(limit: number) {
|
|
30
|
+
assert(Number.isInteger(limit), 'The parameter limit must be an integer')
|
|
31
|
+
assert(limit > 0, 'The parameter limit must be a positive value')
|
|
32
|
+
|
|
33
|
+
this.limit = limit
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set(key: K, value: V, timeToLive: number = Infinity): this {
|
|
37
|
+
if (this.map.has(key)) {
|
|
38
|
+
this.updateItem(key, value)
|
|
39
|
+
this.removeItemMetadata(key)
|
|
40
|
+
this.addItemMetadata(key, Date.now() + timeToLive)
|
|
41
|
+
} else {
|
|
42
|
+
if (this.map.size === this.limit) {
|
|
43
|
+
this.map.delete(this.getColdestKey()!)
|
|
44
|
+
}
|
|
45
|
+
this.map.set(key, value)
|
|
46
|
+
this.addItemMetadata(key, Date.now() + timeToLive)
|
|
47
|
+
}
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
has(key: K): boolean {
|
|
52
|
+
return this.map.has(key)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get(key: K): V | undefined {
|
|
56
|
+
if (this.has(key)) {
|
|
57
|
+
const value = this.map.get(key)!
|
|
58
|
+
this.updateItem(key, value)
|
|
59
|
+
return value
|
|
60
|
+
} else {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
delete(key: K): boolean {
|
|
66
|
+
const result = this.map.delete(key)
|
|
67
|
+
if (result) {
|
|
68
|
+
const index = this.itemMetadataSortedByExpirationTime.findIndex(x => x.key === key)
|
|
69
|
+
this.itemMetadataSortedByExpirationTime.splice(index, 1)
|
|
70
|
+
|
|
71
|
+
// 如果被删除的项目是第一个项目, 则需要重新规划过期回调
|
|
72
|
+
if (index === 0) {
|
|
73
|
+
this.rescheduleClearTimeout()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear(): void {
|
|
80
|
+
this.map.clear()
|
|
81
|
+
this.cancelClearTimeout?.()
|
|
82
|
+
this.itemMetadataSortedByExpirationTime = []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 重新插入项目, 这会使项目置于#map迭代器的末端.
|
|
87
|
+
*/
|
|
88
|
+
private updateItem(key: K, value: V): void {
|
|
89
|
+
this.map.delete(key)
|
|
90
|
+
this.map.set(key, value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private addItemMetadata(key: K, expirationTime: number) {
|
|
94
|
+
for (let i = 0; i < this.itemMetadataSortedByExpirationTime.length; i++) {
|
|
95
|
+
const item = this.itemMetadataSortedByExpirationTime[i]
|
|
96
|
+
if (expirationTime < item.expirationTime) {
|
|
97
|
+
this.itemMetadataSortedByExpirationTime.splice(i, 0, { key, expirationTime })
|
|
98
|
+
|
|
99
|
+
// 如果本次插入导致原先的第一个项目不再是第一个项目, 则需要重新规划过期回调
|
|
100
|
+
if (i === 0) {
|
|
101
|
+
this.rescheduleClearTimeout()
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 如果代码运行到此处, 意味着数组要么为空, 要么新项目过期时间大于数组内的所有项目, 故直接插入到数组尾端.
|
|
108
|
+
this.itemMetadataSortedByExpirationTime.push({ key, expirationTime })
|
|
109
|
+
// 如果新插入的项目是唯一而项目, 则需要重新规划过期回调
|
|
110
|
+
if (this.itemMetadataSortedByExpirationTime.length === 1) {
|
|
111
|
+
this.rescheduleClearTimeout()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private removeItemMetadata(key: K) {
|
|
116
|
+
const index = this.itemMetadataSortedByExpirationTime.findIndex(x => x.key === key)
|
|
117
|
+
if (index >= 0) {
|
|
118
|
+
this.itemMetadataSortedByExpirationTime.splice(index, 1)
|
|
119
|
+
|
|
120
|
+
// 如果被移除的是第一个项目, 则需要重新规划过期回调
|
|
121
|
+
if (index === 0) {
|
|
122
|
+
this.rescheduleClearTimeout()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private clearExpiredItems(timestamp: number) {
|
|
128
|
+
const indexOfFirstUnexpiredItem = this.itemMetadataSortedByExpirationTime.findIndex(
|
|
129
|
+
x => x.expirationTime > timestamp
|
|
130
|
+
)
|
|
131
|
+
const expiredItemKeys =
|
|
132
|
+
indexOfFirstUnexpiredItem >= 0
|
|
133
|
+
? this.itemMetadataSortedByExpirationTime.splice(0, indexOfFirstUnexpiredItem)
|
|
134
|
+
: this.itemMetadataSortedByExpirationTime.splice(0, this.itemMetadataSortedByExpirationTime.length)
|
|
135
|
+
expiredItemKeys.forEach(x => this.map.delete(x.key))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private rescheduleClearTimeout() {
|
|
139
|
+
this.cancelClearTimeout?.()
|
|
140
|
+
|
|
141
|
+
if (this.itemMetadataSortedByExpirationTime.length > 0) {
|
|
142
|
+
const item = this.itemMetadataSortedByExpirationTime[0]
|
|
143
|
+
if (Number.isFinite(item.expirationTime)) {
|
|
144
|
+
const cancel = setSchedule(item.expirationTime, () => {
|
|
145
|
+
this.clearExpiredItems(Date.now())
|
|
146
|
+
this.rescheduleClearTimeout()
|
|
147
|
+
})
|
|
148
|
+
this.cancelClearTimeout = () => {
|
|
149
|
+
cancel()
|
|
150
|
+
this.cancelClearTimeout = undefined
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Return the earliest key inserted in the Map
|
|
158
|
+
*/
|
|
159
|
+
private getColdestKey(): K | undefined {
|
|
160
|
+
return first(this.map.keys())
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/trie-map.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { zip, toArray, map } from 'iterable-operator'
|
|
2
|
+
import { isntUndefined } from 'extra-utils'
|
|
3
|
+
|
|
4
|
+
class TrieNode<T, U> {
|
|
5
|
+
children = new Map<T, TrieNode<T, U>>()
|
|
6
|
+
|
|
7
|
+
constructor(public value?: U) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class TrieMap<K extends Iterable<T>, V, T = unknown> {
|
|
11
|
+
private root = new TrieNode<T, V>()
|
|
12
|
+
|
|
13
|
+
get [Symbol.toStringTag](): string {
|
|
14
|
+
return this.constructor.name
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* entries(): IterableIterator<[key: T[], value: V]> {
|
|
18
|
+
yield* dfs(this.root, [])
|
|
19
|
+
|
|
20
|
+
function* dfs(node: TrieNode<T, V>, path: T[]): IterableIterator<[key: T[], value: V]> {
|
|
21
|
+
for (const [subPath, childNode] of node.children) {
|
|
22
|
+
const newPath = [...path, subPath]
|
|
23
|
+
if (isntUndefined(childNode.value)) {
|
|
24
|
+
yield [newPath, childNode.value]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
yield* dfs(childNode, newPath)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
keys(): IterableIterator<T[]> {
|
|
33
|
+
return map(this.entries(), ([key]) => key)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
values(): IterableIterator<V> {
|
|
37
|
+
return map(this.entries(), ([_, value]) => value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set(key: K, value: V): this {
|
|
41
|
+
let node = this.root
|
|
42
|
+
for (const unitOfKey of key) {
|
|
43
|
+
if (!node.children.has(unitOfKey)) {
|
|
44
|
+
node.children.set(unitOfKey, new TrieNode<T, V>())
|
|
45
|
+
}
|
|
46
|
+
node = node.children.get(unitOfKey)!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
node.value = value
|
|
50
|
+
return this
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
has(key: K): boolean {
|
|
54
|
+
let node = this.root
|
|
55
|
+
for (const unitOfKey of key) {
|
|
56
|
+
if (node.children.has(unitOfKey)) {
|
|
57
|
+
node = node.children.get(unitOfKey)!
|
|
58
|
+
} else {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return node.value !== undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get(key: K): V | undefined {
|
|
67
|
+
let node = this.root
|
|
68
|
+
for (const unitOfKey of key) {
|
|
69
|
+
if (node.children.has(unitOfKey)) {
|
|
70
|
+
node = node.children.get(unitOfKey)!
|
|
71
|
+
} else {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return node.value
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
delete(key: K): boolean {
|
|
80
|
+
const parentNodes: TrieNode<T, V>[] = []
|
|
81
|
+
let node = this.root
|
|
82
|
+
for (const unitOfKey of key) {
|
|
83
|
+
if (node.children.has(unitOfKey)) {
|
|
84
|
+
parentNodes.push(node)
|
|
85
|
+
node = node.children.get(unitOfKey)!
|
|
86
|
+
} else {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete node.value
|
|
92
|
+
|
|
93
|
+
// 如果节点没有后缀, 则代表这是末端节点, 往前删除其前缀
|
|
94
|
+
if (node.children.size === 0) {
|
|
95
|
+
for (const [unitOfKey, parentNode] of toArray(zip(key, parentNodes)).reverse()) {
|
|
96
|
+
parentNode.children.delete(unitOfKey)
|
|
97
|
+
if (parentNode.children.size !== 0) break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|