@spzhongwin/skill-logger-plugin 1.0.8 → 1.0.10
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/openclaw.plugin.json +50 -50
- package/package.json +37 -37
- package/src/active-skills.test.ts +32 -32
- package/src/active-skills.ts +77 -77
- package/src/config-sync.test.ts +165 -165
- package/src/config-sync.ts +544 -544
- package/src/hooks.test.ts +251 -251
- package/src/hooks.ts +517 -517
- package/src/http.ts +61 -61
- package/src/identity.ts +64 -64
- package/src/index.test.ts +53 -53
- package/src/index.ts +226 -226
- package/src/integration.test.ts +119 -119
- package/src/matcher.test.ts +170 -170
- package/src/matcher.ts +393 -393
- package/src/paths.test.ts +57 -57
- package/src/paths.ts +84 -84
- package/src/reporter.test.ts +139 -139
- package/src/reporter.ts +298 -298
- package/src/sample-config.json +72 -72
- package/src/semver.test.ts +23 -23
- package/src/semver.ts +60 -60
- package/src/skill-version.ts +53 -53
- package/src/types.ts +198 -198
- package/src/updater.test.ts +237 -237
- package/src/updater.ts +433 -406
- package/src/ws-client.test.ts +37 -37
- package/src/ws-client.ts +642 -598
- package/test-ws.ts +17 -17
- package/tsconfig.json +14 -14
- package/dist/active-skills.js +0 -67
- package/dist/active-skills.test.js +0 -29
- package/dist/config-sync.js +0 -439
- package/dist/config-sync.test.js +0 -145
- package/dist/hooks.js +0 -337
- package/dist/hooks.test.js +0 -123
- package/dist/http.js +0 -54
- package/dist/identity.js +0 -56
- package/dist/index.js +0 -2564
- package/dist/index.test.js +0 -39
- package/dist/integration.test.js +0 -102
- package/dist/matcher.js +0 -362
- package/dist/matcher.test.js +0 -139
- package/dist/paths.js +0 -62
- package/dist/paths.test.js +0 -49
- package/dist/reporter.js +0 -267
- package/dist/reporter.test.js +0 -128
- package/dist/semver.js +0 -64
- package/dist/semver.test.js +0 -21
- package/dist/skill-version.js +0 -23
- package/dist/types.js +0 -9
- package/dist/updater.js +0 -352
- package/dist/updater.test.js +0 -212
- package/dist/ws-client.js +0 -484
package/openclaw.plugin.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "skill-logger-plugin",
|
|
3
|
-
"name": "Skill Logger",
|
|
4
|
-
"description": "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
|
|
5
|
-
"activation": {
|
|
6
|
-
"onStartup": true
|
|
7
|
-
},
|
|
8
|
-
"configSchema": {
|
|
9
|
-
"type": "object",
|
|
10
|
-
"additionalProperties": true,
|
|
11
|
-
"properties": {
|
|
12
|
-
"pluginId": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "自定义插件 ID"
|
|
15
|
-
},
|
|
16
|
-
"pluginName": {
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "自定义插件名称"
|
|
19
|
-
},
|
|
20
|
-
"platformBaseUrl": {
|
|
21
|
-
"type": "string",
|
|
22
|
-
"description": "拉取 skill 标准配置的服务地址;缺省时使用内置静态桩"
|
|
23
|
-
},
|
|
24
|
-
"reportBaseUrl": {
|
|
25
|
-
"type": "string",
|
|
26
|
-
"description": "批量上报的服务地址;缺省时只落本地、不上报"
|
|
27
|
-
},
|
|
28
|
-
"authToken": {
|
|
29
|
-
"type": "string",
|
|
30
|
-
"description": "拉取/上报接口的 Authorization 头值(如 'Bearer xxx')"
|
|
31
|
-
},
|
|
32
|
-
"recordUnattributed": {
|
|
33
|
-
"type": "boolean",
|
|
34
|
-
"description": "是否记录无法归属到具体功能点的 exec 调用(默认 true)"
|
|
35
|
-
},
|
|
36
|
-
"enableFileLog": {
|
|
37
|
-
"type": "boolean",
|
|
38
|
-
"description": "是否开启插件专属文件日志(落盘为 skill-logger.err),用于排查 WS 连接和指令运行情况"
|
|
39
|
-
},
|
|
40
|
-
"autoUpdateSkills": {
|
|
41
|
-
"type": "boolean",
|
|
42
|
-
"description": "是否开启 skill 版本自动更新(检测到本地落后于平台最新版本时下载并覆盖各 workspace 下的副本);默认开启,仅显式设为 false 才关闭"
|
|
43
|
-
},
|
|
44
|
-
"versionCheckIntervalMinutes": {
|
|
45
|
-
"type": "number",
|
|
46
|
-
"description": "版本检查+自动更新的周期(分钟)。每隔该周期按本地所有已装 skill 向服务端拉最新版本并按需更新;默认 30。本地扫描发现固定 3 分钟,与此独立"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "skill-logger-plugin",
|
|
3
|
+
"name": "Skill Logger",
|
|
4
|
+
"description": "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": true,
|
|
11
|
+
"properties": {
|
|
12
|
+
"pluginId": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "自定义插件 ID"
|
|
15
|
+
},
|
|
16
|
+
"pluginName": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "自定义插件名称"
|
|
19
|
+
},
|
|
20
|
+
"platformBaseUrl": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "拉取 skill 标准配置的服务地址;缺省时使用内置静态桩"
|
|
23
|
+
},
|
|
24
|
+
"reportBaseUrl": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "批量上报的服务地址;缺省时只落本地、不上报"
|
|
27
|
+
},
|
|
28
|
+
"authToken": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "拉取/上报接口的 Authorization 头值(如 'Bearer xxx')"
|
|
31
|
+
},
|
|
32
|
+
"recordUnattributed": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "是否记录无法归属到具体功能点的 exec 调用(默认 true)"
|
|
35
|
+
},
|
|
36
|
+
"enableFileLog": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"description": "是否开启插件专属文件日志(落盘为 skill-logger.err),用于排查 WS 连接和指令运行情况"
|
|
39
|
+
},
|
|
40
|
+
"autoUpdateSkills": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"description": "是否开启 skill 版本自动更新(检测到本地落后于平台最新版本时下载并覆盖各 workspace 下的副本);默认开启,仅显式设为 false 才关闭"
|
|
43
|
+
},
|
|
44
|
+
"versionCheckIntervalMinutes": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "版本检查+自动更新的周期(分钟)。每隔该周期按本地所有已装 skill 向服务端拉最新版本并按需更新;默认 30。本地扫描发现固定 3 分钟,与此独立"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@spzhongwin/skill-logger-plugin",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"exports": "./dist/index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
8
|
-
"typecheck": "tsc --noEmit",
|
|
9
|
-
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:ws"
|
|
10
|
-
},
|
|
11
|
-
"openclaw": {
|
|
12
|
-
"extensions": [
|
|
13
|
-
"./dist/index.js"
|
|
14
|
-
],
|
|
15
|
-
"contracts": {
|
|
16
|
-
"tools": [
|
|
17
|
-
"report_skill_error"
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.3.28",
|
|
22
|
-
"minGatewayVersion": "2026.3.28"
|
|
23
|
-
},
|
|
24
|
-
"build": {
|
|
25
|
-
"openclawVersion": "2026.3.28",
|
|
26
|
-
"pluginSdkVersion": "2026.3.28"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/node": "^22.19.20",
|
|
31
|
-
"@types/ws": "^8.18.1",
|
|
32
|
-
"typescript": "^5.9.3"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"ws": "^8.21.0"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@spzhongwin/skill-logger-plugin",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": "./dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:ws"
|
|
10
|
+
},
|
|
11
|
+
"openclaw": {
|
|
12
|
+
"extensions": [
|
|
13
|
+
"./dist/index.js"
|
|
14
|
+
],
|
|
15
|
+
"contracts": {
|
|
16
|
+
"tools": [
|
|
17
|
+
"report_skill_error"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.3.28",
|
|
22
|
+
"minGatewayVersion": "2026.3.28"
|
|
23
|
+
},
|
|
24
|
+
"build": {
|
|
25
|
+
"openclawVersion": "2026.3.28",
|
|
26
|
+
"pluginSdkVersion": "2026.3.28"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.19.20",
|
|
31
|
+
"@types/ws": "^8.18.1",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ws": "^8.21.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { ActiveSkills } from "./active-skills.ts";
|
|
4
|
-
|
|
5
|
-
describe("ActiveSkills", () => {
|
|
6
|
-
it("标记并取回激活 skill", () => {
|
|
7
|
-
const a = new ActiveSkills();
|
|
8
|
-
a.markActive("s1", "skA");
|
|
9
|
-
a.markActive("s1", "skB");
|
|
10
|
-
assert.deepEqual([...a.getActive("s1")].sort(), ["skA", "skB"]);
|
|
11
|
-
assert.deepEqual([...a.getActive("s2")], []);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("TTL 过期后不再返回", () => {
|
|
15
|
-
let t = 1000;
|
|
16
|
-
const a = new ActiveSkills({ ttlMs: 100, now: () => t });
|
|
17
|
-
a.markActive("s", "skA");
|
|
18
|
-
t = 1050;
|
|
19
|
-
assert.deepEqual([...a.getActive("s")], ["skA"]);
|
|
20
|
-
t = 2000;
|
|
21
|
-
assert.deepEqual([...a.getActive("s")], []);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("超出 session 上限按 LRU 淘汰最旧", () => {
|
|
25
|
-
const a = new ActiveSkills({ maxSessions: 2 });
|
|
26
|
-
a.markActive("s1", "x");
|
|
27
|
-
a.markActive("s2", "x");
|
|
28
|
-
a.markActive("s3", "x"); // 淘汰 s1
|
|
29
|
-
assert.deepEqual([...a.getActive("s1")], []);
|
|
30
|
-
assert.deepEqual([...a.getActive("s3")], ["x"]);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { ActiveSkills } from "./active-skills.ts";
|
|
4
|
+
|
|
5
|
+
describe("ActiveSkills", () => {
|
|
6
|
+
it("标记并取回激活 skill", () => {
|
|
7
|
+
const a = new ActiveSkills();
|
|
8
|
+
a.markActive("s1", "skA");
|
|
9
|
+
a.markActive("s1", "skB");
|
|
10
|
+
assert.deepEqual([...a.getActive("s1")].sort(), ["skA", "skB"]);
|
|
11
|
+
assert.deepEqual([...a.getActive("s2")], []);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("TTL 过期后不再返回", () => {
|
|
15
|
+
let t = 1000;
|
|
16
|
+
const a = new ActiveSkills({ ttlMs: 100, now: () => t });
|
|
17
|
+
a.markActive("s", "skA");
|
|
18
|
+
t = 1050;
|
|
19
|
+
assert.deepEqual([...a.getActive("s")], ["skA"]);
|
|
20
|
+
t = 2000;
|
|
21
|
+
assert.deepEqual([...a.getActive("s")], []);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("超出 session 上限按 LRU 淘汰最旧", () => {
|
|
25
|
+
const a = new ActiveSkills({ maxSessions: 2 });
|
|
26
|
+
a.markActive("s1", "x");
|
|
27
|
+
a.markActive("s2", "x");
|
|
28
|
+
a.markActive("s3", "x"); // 淘汰 s1
|
|
29
|
+
assert.deepEqual([...a.getActive("s1")], []);
|
|
30
|
+
assert.deepEqual([...a.getActive("s3")], ["x"]);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/active-skills.ts
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 按 session 记录「已触发(激活)的 skill」,供 matcher 在通用命令(curl、共享 CLI)
|
|
3
|
-
* 出现多候选时消歧——优先归属到本 session 已激活的 skill。
|
|
4
|
-
*
|
|
5
|
-
* 内存态,带 TTL + 容量上限,防止长跑 gateway 进程里无限增长。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
type SkillEntry = { name: string; ts: number };
|
|
9
|
-
|
|
10
|
-
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 分钟
|
|
11
|
-
const DEFAULT_MAX_SESSIONS = 500;
|
|
12
|
-
|
|
13
|
-
export type ActiveSkillsOptions = {
|
|
14
|
-
ttlMs?: number;
|
|
15
|
-
maxSessions?: number;
|
|
16
|
-
/** 注入时钟,便于测试。 */
|
|
17
|
-
now?: () => number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export class ActiveSkills {
|
|
21
|
-
private readonly ttlMs: number;
|
|
22
|
-
private readonly maxSessions: number;
|
|
23
|
-
private readonly now: () => number;
|
|
24
|
-
/** sessionKey -> (skillName -> entry)。用 Map 保留插入顺序以便 LRU 淘汰。 */
|
|
25
|
-
private readonly bySession = new Map<string, Map<string, SkillEntry>>();
|
|
26
|
-
|
|
27
|
-
constructor(opts: ActiveSkillsOptions = {}) {
|
|
28
|
-
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
29
|
-
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
30
|
-
this.now = opts.now ?? Date.now;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 标记某 session 触发了某 skill。 */
|
|
34
|
-
markActive(sessionKey: string | undefined, skillName: string): void {
|
|
35
|
-
const key = sessionKey || "__nosession__";
|
|
36
|
-
let skills = this.bySession.get(key);
|
|
37
|
-
if (!skills) {
|
|
38
|
-
skills = new Map();
|
|
39
|
-
this.bySession.set(key, skills);
|
|
40
|
-
} else {
|
|
41
|
-
// 触碰即刷新 LRU 顺序
|
|
42
|
-
this.bySession.delete(key);
|
|
43
|
-
this.bySession.set(key, skills);
|
|
44
|
-
}
|
|
45
|
-
skills.set(skillName, { name: skillName, ts: this.now() });
|
|
46
|
-
this.evictIfNeeded();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 取某 session 当前仍在 TTL 内的已激活 skill 集合。 */
|
|
50
|
-
getActive(sessionKey: string | undefined): Set<string> {
|
|
51
|
-
const key = sessionKey || "__nosession__";
|
|
52
|
-
const skills = this.bySession.get(key);
|
|
53
|
-
const out = new Set<string>();
|
|
54
|
-
if (!skills) return out;
|
|
55
|
-
const cutoff = this.now() - this.ttlMs;
|
|
56
|
-
for (const [name, entry] of skills) {
|
|
57
|
-
if (entry.ts >= cutoff) out.add(name);
|
|
58
|
-
else skills.delete(name);
|
|
59
|
-
}
|
|
60
|
-
if (skills.size === 0) this.bySession.delete(key);
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** session 结束时清掉其激活记录(释放内存 + 避免跨会话误判)。 */
|
|
65
|
-
clearSession(sessionKey: string | undefined): void {
|
|
66
|
-
this.bySession.delete(sessionKey || "__nosession__");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 超出 session 上限时,按 LRU 淘汰最旧的 session。 */
|
|
70
|
-
private evictIfNeeded(): void {
|
|
71
|
-
while (this.bySession.size > this.maxSessions) {
|
|
72
|
-
const oldest = this.bySession.keys().next().value;
|
|
73
|
-
if (oldest === undefined) break;
|
|
74
|
-
this.bySession.delete(oldest);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 按 session 记录「已触发(激活)的 skill」,供 matcher 在通用命令(curl、共享 CLI)
|
|
3
|
+
* 出现多候选时消歧——优先归属到本 session 已激活的 skill。
|
|
4
|
+
*
|
|
5
|
+
* 内存态,带 TTL + 容量上限,防止长跑 gateway 进程里无限增长。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type SkillEntry = { name: string; ts: number };
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 分钟
|
|
11
|
+
const DEFAULT_MAX_SESSIONS = 500;
|
|
12
|
+
|
|
13
|
+
export type ActiveSkillsOptions = {
|
|
14
|
+
ttlMs?: number;
|
|
15
|
+
maxSessions?: number;
|
|
16
|
+
/** 注入时钟,便于测试。 */
|
|
17
|
+
now?: () => number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class ActiveSkills {
|
|
21
|
+
private readonly ttlMs: number;
|
|
22
|
+
private readonly maxSessions: number;
|
|
23
|
+
private readonly now: () => number;
|
|
24
|
+
/** sessionKey -> (skillName -> entry)。用 Map 保留插入顺序以便 LRU 淘汰。 */
|
|
25
|
+
private readonly bySession = new Map<string, Map<string, SkillEntry>>();
|
|
26
|
+
|
|
27
|
+
constructor(opts: ActiveSkillsOptions = {}) {
|
|
28
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
29
|
+
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
30
|
+
this.now = opts.now ?? Date.now;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 标记某 session 触发了某 skill。 */
|
|
34
|
+
markActive(sessionKey: string | undefined, skillName: string): void {
|
|
35
|
+
const key = sessionKey || "__nosession__";
|
|
36
|
+
let skills = this.bySession.get(key);
|
|
37
|
+
if (!skills) {
|
|
38
|
+
skills = new Map();
|
|
39
|
+
this.bySession.set(key, skills);
|
|
40
|
+
} else {
|
|
41
|
+
// 触碰即刷新 LRU 顺序
|
|
42
|
+
this.bySession.delete(key);
|
|
43
|
+
this.bySession.set(key, skills);
|
|
44
|
+
}
|
|
45
|
+
skills.set(skillName, { name: skillName, ts: this.now() });
|
|
46
|
+
this.evictIfNeeded();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 取某 session 当前仍在 TTL 内的已激活 skill 集合。 */
|
|
50
|
+
getActive(sessionKey: string | undefined): Set<string> {
|
|
51
|
+
const key = sessionKey || "__nosession__";
|
|
52
|
+
const skills = this.bySession.get(key);
|
|
53
|
+
const out = new Set<string>();
|
|
54
|
+
if (!skills) return out;
|
|
55
|
+
const cutoff = this.now() - this.ttlMs;
|
|
56
|
+
for (const [name, entry] of skills) {
|
|
57
|
+
if (entry.ts >= cutoff) out.add(name);
|
|
58
|
+
else skills.delete(name);
|
|
59
|
+
}
|
|
60
|
+
if (skills.size === 0) this.bySession.delete(key);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** session 结束时清掉其激活记录(释放内存 + 避免跨会话误判)。 */
|
|
65
|
+
clearSession(sessionKey: string | undefined): void {
|
|
66
|
+
this.bySession.delete(sessionKey || "__nosession__");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 超出 session 上限时,按 LRU 淘汰最旧的 session。 */
|
|
70
|
+
private evictIfNeeded(): void {
|
|
71
|
+
while (this.bySession.size > this.maxSessions) {
|
|
72
|
+
const oldest = this.bySession.keys().next().value;
|
|
73
|
+
if (oldest === undefined) break;
|
|
74
|
+
this.bySession.delete(oldest);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|