Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/conat/monitor/usage.ts
1452 views
1
import json from "json-stable-stringify";
2
import { EventEmitter } from "events";
3
import type { JSONValue } from "@cocalc/util/types";
4
import { ConatError } from "@cocalc/conat/core/client";
5
import { getLogger } from "@cocalc/conat/client";
6
7
const logger = getLogger("monitor:usage");
8
9
interface Options {
10
resource: string;
11
maxPerUser?: number;
12
max?: number;
13
log?: (...args) => void;
14
}
15
16
export class UsageMonitor extends EventEmitter {
17
private options: Options;
18
private total = 0;
19
private perUser: { [user: string]: number } = {};
20
21
constructor(options: Options) {
22
super();
23
this.options = options;
24
logger.debug("creating usage monitor", this.options);
25
this.initLogging();
26
}
27
28
stats = () => {
29
return { total: this.total, perUser: this.perUser };
30
};
31
32
close = () => {
33
this.removeAllListeners();
34
this.perUser = {};
35
};
36
37
private toJson = (user: JSONValue) => json(user) ?? "";
38
39
private initLogging = () => {
40
const { log } = this.options;
41
if (log == null) {
42
return;
43
}
44
this.on("total", (total, limit) => {
45
log("usage", this.options.resource, { total, limit });
46
});
47
this.on("add", (user, count, limit) => {
48
log("usage", this.options.resource, "add", { user, count, limit });
49
});
50
this.on("delete", (user, count, limit) => {
51
log("usage", this.options.resource, "delete", { user, count, limit });
52
});
53
this.on("deny", (user, limit, type) => {
54
log("usage", this.options.resource, "not allowed due to hitting limit", {
55
type,
56
user,
57
limit,
58
});
59
});
60
};
61
62
add = (user: JSONValue) => {
63
const u = this.toJson(user);
64
let count = this.perUser[u] ?? 0;
65
if (this.options.max && this.total >= this.options.max) {
66
this.emit("deny", user, this.options.max, "global");
67
throw new ConatError(
68
`There is a global limit of ${this.options.max} ${this.options.resource}. Please close browser tabs or files or come back later.`,
69
// http error code "429 Too Many Requests."
70
{ code: 429 },
71
);
72
}
73
if (this.options.maxPerUser && count >= this.options.maxPerUser) {
74
this.emit("deny", this.options.maxPerUser, "per-user");
75
throw new ConatError(
76
`There is a per user limit of ${this.options.maxPerUser} ${this.options.resource}. Please close browser tabs or files or come back later.`,
77
// http error code "429 Too Many Requests."
78
{ code: 429 },
79
);
80
}
81
this.total += 1;
82
count++;
83
this.perUser[u] = count;
84
this.emit("total", this.total, this.options.max);
85
this.emit("add", user, count, this.options.maxPerUser);
86
};
87
88
delete = (user: JSONValue) => {
89
this.total -= 1;
90
const u = this.toJson(user);
91
let count = (this.perUser[u] ?? 0) - 1;
92
if (count <= 0) {
93
delete this.perUser[u];
94
} else {
95
this.perUser[u] = count;
96
}
97
this.emit("total", this.total);
98
this.emit("delete", user, count);
99
};
100
}
101
102