UNPKG

4.77 kBPlain TextView Raw
1import { findIndex } from 'lodash'
2
3/**
4 * A key-value map where the members' keys represent prefixes.
5 *
6 * Example:
7 * const map = new PrefixMap()
8 * map.insert("foo", 1)
9 * map.insert("bar", 2)
10 * map.get("foo") // ⇒ 1
11 * map.get("foo.bar") // ⇒ 1 ("foo" is the longest known prefix of "foo.bar")
12 * map.get("bar") // ⇒ 2
13 * map.get("bar.foo") // ⇒ 2 ("bar" is the longest known prefix of "bar.foo")
14 * map.get("random") // ⇒ null
15 */
16export default class PrefixMap<T> {
17 protected prefixes: string[]
18 protected items: { [key: string]: T }
19
20 constructor () {
21 this.prefixes = []
22 this.items = {}
23 }
24
25 keys () { return this.prefixes }
26
27 size () { return this.prefixes.length }
28
29 /**
30 * Find the value of the longest matching prefix key.
31 */
32 resolve (key: string) {
33 const prefix = this.resolvePrefix(key)
34
35 return typeof prefix !== 'undefined' ? this.items[prefix] : undefined
36 }
37
38 /**
39 * Find the longest matching prefix key.
40 */
41 resolvePrefix (key: string) {
42 // Exact match
43 if (this.items[key]) return key // redundant; optimization?
44 // prefix match (the list is in descending length order, and secondarily, reverse-alphabetically)
45 const index = findIndex(this.prefixes, (e: string) => key.startsWith(e + '.'))
46 if (index === -1) return undefined
47 const prefix = this.prefixes[index]
48 return prefix
49 }
50
51 get (prefix: string): T | undefined { return this.items[prefix] }
52
53 /**
54 * Look up all keys that start with a certain prefix.
55 */
56 * getKeysStartingWith (prefix: string): IterableIterator<string> {
57 // TODO: This could be done *much* more efficiently
58 const predicate = (key: string) => key.startsWith(prefix)
59 let index = -1
60 // tslint:disable-next-line:no-conditional-assignment
61 while ((index = findIndex(this.prefixes, predicate, index + 1)) !== -1) {
62 yield this.prefixes[index]
63 }
64 }
65
66 * getKeysPrefixesOf (search: string): IterableIterator<string> {
67 const predicate = (key: string) => search.startsWith(key + '.')
68 let index = -1
69 // tslint:disable-next-line:no-conditional-assignment
70 while ((index = findIndex(this.prefixes, predicate, index + 1)) !== -1) {
71 yield this.prefixes[index]
72 }
73 }
74
75 /**
76 * @param {function(item, key)} fn
77 */
78 each (fn: (item: T, key: string) => void) {
79 for (const prefix of this.prefixes) {
80 fn(this.items[prefix], prefix)
81 }
82 }
83
84 /**
85 * Insert the prefix while keeping the prefixes sorted first in length order
86 * and if two prefixes are the same length, sort them in reverse alphabetical order
87 */
88 insert (prefix: string, item: T) {
89 if (!this.items[prefix]) {
90 const index = findIndex(this.prefixes, (e) => {
91 if (prefix.length === e.length) {
92 return prefix > e
93 }
94 return prefix.length > e.length
95 })
96
97 if (index === -1) {
98 this.prefixes.push(prefix)
99 } else {
100 this.prefixes.splice(index, 0, prefix)
101 }
102 }
103 this.items[prefix] = item
104 return item
105 }
106
107 delete (prefix: string) {
108 const index = this.prefixes.indexOf(prefix)
109 if (this.prefixes[index] === prefix) this.prefixes.splice(index, 1)
110 delete this.items[prefix]
111 }
112
113 toJSON () {
114 return this.items
115 }
116
117 /**
118 * Find the shortest unambiguous prefix of an ILP address in a prefix map.
119 *
120 * This let's us figure out what addresses the selected route applies to. For
121 * example, the most specific route for destination "a.b.c" might be "a", but
122 * that doesn't mean that that route applies to any destination starting with
123 * "a" because there may be a more specific route like "a.c".
124 *
125 * So we would call this utility function to find out that the least specific
126 * prefix for which there are no other more specific routes is "a.b".
127 *
128 * In order to force a minimum prefix, it can be passed as the third parameter.
129 * This function may make it even more specific if necessary to make it
130 * unambiguous, but it will never return a less specific prefix.
131 */
132 getShortestUnambiguousPrefix (address: string, prefix = '') {
133 if (!address.startsWith(prefix)) {
134 throw new Error(`address must start with prefix. address=${address} prefix=${prefix}`)
135 }
136
137 this.keys().forEach((secondPrefix: string) => {
138 if (secondPrefix === prefix) {
139 return
140 }
141
142 while (secondPrefix.startsWith(prefix)) {
143 if (secondPrefix === prefix) {
144 return
145 }
146
147 const nextSegmentEnd = address.indexOf('.', prefix.length + 1)
148
149 if (nextSegmentEnd === -1) {
150 prefix = address
151 return
152 } else {
153 prefix = address.slice(0, nextSegmentEnd)
154 }
155 }
156 })
157
158 return prefix
159 }
160}