Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/conat/core/patterns.ts
1710 views
1
import { isEqual } from "lodash";
2
import { getLogger } from "@cocalc/conat/client";
3
import { EventEmitter } from "events";
4
import { hash_string } from "@cocalc/util/misc";
5
6
type Index = { [pattern: string]: Index | string };
7
8
const logger = getLogger("pattern");
9
10
export class Patterns<T> extends EventEmitter {
11
private patterns: { [pattern: string]: T } = {};
12
private index: Index = {};
13
14
constructor() {
15
super();
16
this.setMaxListeners(1000);
17
}
18
19
close = () => {
20
this.emit("closed");
21
this.patterns = {};
22
this.index = {};
23
};
24
25
hash = (hashT: (x: T) => number): number => {
26
let h = 0;
27
for (const pattern in this.patterns) {
28
h += hash_string(pattern) + hashT(this.patterns[pattern]);
29
}
30
return h;
31
};
32
33
serialize = (fromT?: (x: T) => any) => {
34
let patterns: { [pattern: string]: any };
35
if (fromT != null) {
36
patterns = {};
37
for (const pattern in this.patterns) {
38
patterns[pattern] = fromT(this.patterns[pattern]);
39
}
40
} else {
41
patterns = this.patterns;
42
}
43
44
return { patterns, index: this.index };
45
};
46
47
deserialize = (
48
{ patterns, index }: { patterns: { [pattern: string]: any }; index: Index },
49
toT?: (x: any) => T,
50
) => {
51
if (toT != null) {
52
for (const pattern in patterns) {
53
patterns[pattern] = toT(patterns[pattern]); // make it of type T
54
}
55
}
56
this.patterns = patterns;
57
this.index = index;
58
this.emit("change");
59
};
60
61
// mutate this by merging in data from p.
62
merge = (p: Patterns<T>) => {
63
for (const pattern in p.patterns) {
64
const t = p.patterns[pattern];
65
this.set(pattern, t);
66
}
67
this.emit("change");
68
};
69
70
matches = (subject: string): string[] => {
71
return matchUsingIndex(this.index, subject.split("."));
72
};
73
74
// return true if there is at least one match
75
hasMatch = (subject: string): boolean => {
76
return matchUsingIndex(this.index, subject.split("."), true).length > 0;
77
};
78
79
hasPattern = (pattern: string): boolean => {
80
return this.patterns[pattern] !== undefined;
81
};
82
83
matchesTest = (subject: string): string[] => {
84
const a = this.matches(subject);
85
const b = this.matchNaive(subject);
86
a.sort();
87
b.sort();
88
if (!isEqual(a, b)) {
89
logger.debug("BUG in PATTERN MATCHING!!!", {
90
subject,
91
a,
92
b,
93
index: this.index,
94
patterns: Object.keys(this.patterns),
95
});
96
}
97
return b;
98
};
99
100
matchNaive = (subject: string): string[] => {
101
const v: string[] = [];
102
for (const pattern in this.patterns) {
103
if (matchesPattern(pattern, subject)) {
104
v.push(pattern);
105
}
106
}
107
return v;
108
};
109
110
get = (pattern: string): T | undefined => {
111
return this.patterns[pattern];
112
};
113
114
set = (pattern: string, t: T) => {
115
this.patterns[pattern] = t;
116
setIndex(this.index, pattern.split("."), pattern);
117
this.emit("change");
118
};
119
120
delete = (pattern: string) => {
121
delete this.patterns[pattern];
122
deleteIndex(this.index, pattern.split("."));
123
};
124
}
125
126
function setIndex(index: Index, segments: string[], pattern) {
127
if (segments.length == 0) {
128
index[""] = pattern;
129
return;
130
}
131
if (segments[0] == ">") {
132
// there can't be anything after it
133
index[">"] = pattern;
134
return;
135
}
136
const v = index[segments[0]];
137
if (v === undefined) {
138
const idx: Index = {};
139
setIndex(idx, segments.slice(1), pattern);
140
index[segments[0]] = idx;
141
return;
142
}
143
if (typeof v == "string") {
144
// already set
145
return;
146
}
147
setIndex(v, segments.slice(1), pattern);
148
}
149
150
function deleteIndex(index: Index, segments: string[]) {
151
const ind = index[segments[0]];
152
if (ind === undefined) {
153
return;
154
}
155
if (typeof ind != "string") {
156
deleteIndex(ind, segments.slice(1));
157
// if there is anything still stored in ind
158
// besides ind[''], we do NOT delete it.
159
for (const key in ind) {
160
if (key != "") {
161
return;
162
}
163
}
164
}
165
delete index[segments[0]];
166
}
167
168
// todo deal with >
169
function matchUsingIndex(
170
index: Index,
171
segments: string[],
172
atMostOne = false,
173
): string[] {
174
if (segments.length == 0) {
175
const p = index[""];
176
if (p === undefined) {
177
return [];
178
} else if (typeof p === "string") {
179
return [p];
180
} else {
181
throw Error("bug");
182
}
183
}
184
const matches: string[] = [];
185
const subject = segments[0];
186
for (const pattern of ["*", ">", subject]) {
187
if (index[pattern] !== undefined) {
188
const p = index[pattern];
189
if (typeof p == "string") {
190
// end of this pattern -- matches if segments also
191
// stops *or* this pattern is >
192
if (segments.length == 1) {
193
matches.push(p);
194
if (atMostOne) {
195
return matches;
196
}
197
} else if (pattern == ">") {
198
matches.push(p);
199
if (atMostOne) {
200
return matches;
201
}
202
}
203
} else {
204
for (const s of matchUsingIndex(p, segments.slice(1), atMostOne)) {
205
matches.push(s);
206
if (atMostOne) {
207
return matches;
208
}
209
}
210
}
211
}
212
}
213
return matches;
214
}
215
216
export function matchesSegment(pattern, subject): boolean {
217
if (pattern == "*" || pattern == ">") {
218
return true;
219
}
220
return pattern == subject;
221
}
222
223
export function matchesPattern(pattern, subject): boolean {
224
const subParts = subject.split(".");
225
const patParts = pattern.split(".");
226
let i = 0,
227
j = 0;
228
while (i < subParts.length && j < patParts.length) {
229
if (patParts[j] === ">") return true;
230
if (patParts[j] !== "*" && patParts[j] !== subParts[i]) return false;
231
i++;
232
j++;
233
}
234
235
return i === subParts.length && j === patParts.length;
236
}
237
238