-
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathcontext-registry.ts
More file actions
138 lines (104 loc) · 3.4 KB
/
context-registry.ts
File metadata and controls
138 lines (104 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
export class Context {
public constructor() {}
[key: string]: unknown;
}
export function parentPath(path: string): string {
return String(path.split("/").slice(0, -1).join("/")) || "/";
}
/**
* Deep-clones an object for caching purposes.
* Plain objects and class instances have their own enumerable properties
* recursively cloned (preserving the prototype chain). Functions are copied
* by reference since they are not structurally comparable data.
*/
function cloneForCache(value: unknown): unknown {
if (value === null || typeof value === "function") {
return value;
}
if (typeof value !== "object") {
return value;
}
if (Array.isArray(value)) {
return (value as unknown[]).map(cloneForCache);
}
const proto = Object.getPrototypeOf(value) as object | null;
const clone: Record<string, unknown> =
proto !== null && proto !== Object.prototype
? (Object.create(proto) as Record<string, unknown>)
: {};
for (const key of Object.keys(value)) {
clone[key] = cloneForCache((value as Record<string, unknown>)[key]);
}
return clone;
}
export class ContextRegistry extends EventTarget {
private readonly entries = new Map<string, Context>();
private readonly cache = new Map<string, Context>();
private readonly seen = new Set<string>();
public constructor() {
super();
this.add("/", {});
}
private getContextIgnoreCase(map: Map<string, Context>, key: string) {
const lowerCaseKey = key.toLowerCase();
for (const currentKey of map.keys()) {
if (currentKey.toLowerCase() === lowerCaseKey) {
return map.get(currentKey);
}
}
return undefined;
}
public add(path: string, context: Context): void {
this.entries.set(path, context);
this.cache.set(path, cloneForCache(context) as Context);
this.dispatchEvent(new Event("context-changed"));
}
/**
* Removes the context entry for the given path and dispatches a
* "context-changed" event so that listeners (e.g. the scenario-context type
* generator) can regenerate type files in response to the removal.
*
* @param path - The route path whose context entry should be deleted
* (e.g. "/pets").
*/
public remove(path: string): void {
this.entries.delete(path);
this.cache.delete(path);
this.seen.delete(path);
this.dispatchEvent(new Event("context-changed"));
}
public find(path: string): Context {
return (
this.getContextIgnoreCase(this.entries, path) ??
this.find(parentPath(path))
);
}
public update(path: string, updatedContext?: Context): void {
if (updatedContext === undefined) {
return;
}
if (!this.seen.has(path)) {
this.seen.add(path);
this.add(path, updatedContext);
return;
}
const context = this.find(path);
for (const property in updatedContext) {
if (updatedContext[property] !== this.cache.get(path)?.[property]) {
context[property] = updatedContext[property];
}
}
Object.setPrototypeOf(context, Object.getPrototypeOf(updatedContext));
this.cache.set(path, cloneForCache(updatedContext) as Context);
}
public getAllPaths(): string[] {
return Array.from(this.entries.keys());
}
public getAllContexts(): Record<string, Context> {
const result: Record<string, Context> = {};
for (const [path, context] of this.entries.entries()) {
result[path] = context;
}
return result;
}
}