@flrande/browserctl 0.5.0-dev.22.1 → 0.6.0
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/dist/client.d.ts +34 -0
- package/dist/client.js +138 -0
- package/dist/commandRegistry.d.ts +16 -0
- package/dist/commandRegistry.js +21 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/runCli.d.ts +5 -0
- package/dist/runCli.js +170 -0
- package/package.json +32 -59
- package/INSTALL-CN.md +0 -92
- package/INSTALL.md +0 -92
- package/LICENSE +0 -21
- package/README-CN.md +0 -69
- package/README.md +0 -69
- package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
- package/apps/browserctl/src/commands/act.test.ts +0 -71
- package/apps/browserctl/src/commands/act.ts +0 -64
- package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
- package/apps/browserctl/src/commands/common.test.ts +0 -87
- package/apps/browserctl/src/commands/common.ts +0 -191
- package/apps/browserctl/src/commands/console-list.test.ts +0 -102
- package/apps/browserctl/src/commands/console-list.ts +0 -108
- package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
- package/apps/browserctl/src/commands/cookie-get.ts +0 -18
- package/apps/browserctl/src/commands/cookie-set.ts +0 -22
- package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
- package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
- package/apps/browserctl/src/commands/dom-query.ts +0 -18
- package/apps/browserctl/src/commands/download-trigger.ts +0 -22
- package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
- package/apps/browserctl/src/commands/download-wait.ts +0 -27
- package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
- package/apps/browserctl/src/commands/frame-list.ts +0 -16
- package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
- package/apps/browserctl/src/commands/har-export.test.ts +0 -112
- package/apps/browserctl/src/commands/har-export.ts +0 -120
- package/apps/browserctl/src/commands/memory-delete.ts +0 -20
- package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
- package/apps/browserctl/src/commands/memory-list.ts +0 -90
- package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
- package/apps/browserctl/src/commands/memory-purge.ts +0 -16
- package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
- package/apps/browserctl/src/commands/memory-status.ts +0 -16
- package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
- package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
- package/apps/browserctl/src/commands/network-list.test.ts +0 -110
- package/apps/browserctl/src/commands/network-list.ts +0 -112
- package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
- package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
- package/apps/browserctl/src/commands/profile-list.ts +0 -16
- package/apps/browserctl/src/commands/profile-use.ts +0 -18
- package/apps/browserctl/src/commands/response-body.ts +0 -24
- package/apps/browserctl/src/commands/screenshot.ts +0 -16
- package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
- package/apps/browserctl/src/commands/session-drop.ts +0 -16
- package/apps/browserctl/src/commands/session-list.test.ts +0 -81
- package/apps/browserctl/src/commands/session-list.ts +0 -70
- package/apps/browserctl/src/commands/snapshot.ts +0 -16
- package/apps/browserctl/src/commands/status.ts +0 -10
- package/apps/browserctl/src/commands/storage-get.ts +0 -20
- package/apps/browserctl/src/commands/storage-set.ts +0 -22
- package/apps/browserctl/src/commands/tab-close.ts +0 -20
- package/apps/browserctl/src/commands/tab-focus.ts +0 -20
- package/apps/browserctl/src/commands/tab-open.ts +0 -19
- package/apps/browserctl/src/commands/tabs.ts +0 -13
- package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
- package/apps/browserctl/src/commands/trace-get.ts +0 -62
- package/apps/browserctl/src/commands/upload-arm.ts +0 -26
- package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-element.ts +0 -76
- package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
- package/apps/browserctl/src/commands/wait-text.ts +0 -93
- package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-url.ts +0 -76
- package/apps/browserctl/src/daemon-client.test.ts +0 -512
- package/apps/browserctl/src/daemon-client.ts +0 -632
- package/apps/browserctl/src/e2e.test.ts +0 -103
- package/apps/browserctl/src/main.dispatch.test.ts +0 -461
- package/apps/browserctl/src/main.test.ts +0 -334
- package/apps/browserctl/src/main.ts +0 -957
- package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
- package/apps/browserctl/src/test-port.ts +0 -26
- package/apps/browserd/src/bootstrap.ts +0 -432
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
- package/apps/browserd/src/container.ts +0 -3088
- package/apps/browserd/src/main.test.ts +0 -1522
- package/apps/browserd/src/main.ts +0 -7
- package/apps/browserd/src/test-port.ts +0 -26
- package/apps/browserd/src/tool-matrix.test.ts +0 -887
- package/bin/browserctl.cjs +0 -21
- package/bin/browserd.cjs +0 -21
- package/extensions/chrome-relay/README-CN.md +0 -39
- package/extensions/chrome-relay/README.md +0 -39
- package/extensions/chrome-relay/background.js +0 -1687
- package/extensions/chrome-relay/manifest.json +0 -15
- package/extensions/chrome-relay/popup.html +0 -369
- package/extensions/chrome-relay/popup.js +0 -972
- package/packages/core/src/bootstrap.test.ts +0 -10
- package/packages/core/src/driver-registry.test.ts +0 -45
- package/packages/core/src/driver-registry.ts +0 -22
- package/packages/core/src/driver.ts +0 -47
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/navigation-memory.test.ts +0 -259
- package/packages/core/src/navigation-memory.ts +0 -360
- package/packages/core/src/ref-cache.test.ts +0 -61
- package/packages/core/src/ref-cache.ts +0 -28
- package/packages/core/src/session-store.test.ts +0 -82
- package/packages/core/src/session-store.ts +0 -138
- package/packages/core/src/types.ts +0 -9
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
- package/packages/driver-chrome-relay/src/index.ts +0 -26
- package/packages/driver-managed/src/index.ts +0 -22
- package/packages/driver-managed/src/managed-driver.test.ts +0 -183
- package/packages/driver-managed/src/managed-driver.ts +0 -341
- package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
- package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
- package/packages/driver-remote-cdp/src/index.ts +0 -19
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
- package/packages/protocol/src/envelope.test.ts +0 -25
- package/packages/protocol/src/envelope.ts +0 -31
- package/packages/protocol/src/errors.test.ts +0 -17
- package/packages/protocol/src/errors.ts +0 -11
- package/packages/protocol/src/index.ts +0 -3
- package/packages/protocol/src/tools.ts +0 -3
- package/packages/transport-mcp-stdio/src/index.ts +0 -3
- package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
- package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
- package/packages/transport-mcp-stdio/src/server.ts +0 -183
- package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
const DEFAULT_TTL_DAYS = 30;
|
|
2
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
3
|
-
|
|
4
|
-
export type NavigationSignal =
|
|
5
|
-
| { kind: "urlPattern"; value: string }
|
|
6
|
-
| { kind: "selector"; value: string }
|
|
7
|
-
| { kind: "hop"; from: string; to: string };
|
|
8
|
-
|
|
9
|
-
export type NavigationMemoryEntry = {
|
|
10
|
-
id: string;
|
|
11
|
-
domain: string;
|
|
12
|
-
profileId: string;
|
|
13
|
-
intentKey: string;
|
|
14
|
-
signals: NavigationSignal[];
|
|
15
|
-
confidence: number;
|
|
16
|
-
createdAt: string;
|
|
17
|
-
lastHitAt: string;
|
|
18
|
-
expireAt: string;
|
|
19
|
-
hitCount: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type NavigationMemoryMode = "off" | "ask" | "auto";
|
|
23
|
-
|
|
24
|
-
export type NavigationMemoryStoreOptions = {
|
|
25
|
-
path: string;
|
|
26
|
-
mode?: NavigationMemoryMode;
|
|
27
|
-
ttlDays?: number;
|
|
28
|
-
now?: () => number;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type NavigationMemoryResolveInput = {
|
|
32
|
-
domain: string;
|
|
33
|
-
profileId: string;
|
|
34
|
-
intentKey: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type NavigationMemoryResolveResult =
|
|
38
|
-
| { hit: false; entry?: undefined }
|
|
39
|
-
| { hit: true; entry: NavigationMemoryEntry };
|
|
40
|
-
|
|
41
|
-
export type NavigationMemoryUpsertInput = {
|
|
42
|
-
domain: string;
|
|
43
|
-
profileId: string;
|
|
44
|
-
intentKey: string;
|
|
45
|
-
signals: NavigationSignal[];
|
|
46
|
-
confidence?: number;
|
|
47
|
-
confirmed?: boolean;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type NavigationMemoryUpsertResult = {
|
|
51
|
-
created: boolean;
|
|
52
|
-
entry: NavigationMemoryEntry;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export type NavigationMemoryListInput = {
|
|
56
|
-
domain?: string;
|
|
57
|
-
profileId?: string;
|
|
58
|
-
intentKey?: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type NavigationMemoryStatus = {
|
|
62
|
-
path: string;
|
|
63
|
-
mode: NavigationMemoryMode;
|
|
64
|
-
ttlDays: number;
|
|
65
|
-
totalEntries: number;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export type NavigationMemoryStore = {
|
|
69
|
-
resolve(input: NavigationMemoryResolveInput): Promise<NavigationMemoryResolveResult>;
|
|
70
|
-
upsert(input: NavigationMemoryUpsertInput): Promise<NavigationMemoryUpsertResult>;
|
|
71
|
-
list(filter?: NavigationMemoryListInput): Promise<NavigationMemoryEntry[]>;
|
|
72
|
-
inspect(id: string): Promise<NavigationMemoryEntry | undefined>;
|
|
73
|
-
delete(id: string): Promise<boolean>;
|
|
74
|
-
purgeExpired(): Promise<number>;
|
|
75
|
-
setMode(mode: NavigationMemoryMode): Promise<NavigationMemoryStatus>;
|
|
76
|
-
setTtlDays(ttlDays: number): Promise<NavigationMemoryStatus>;
|
|
77
|
-
status(): NavigationMemoryStatus;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
function toIso(timestampMs: number): string {
|
|
81
|
-
return new Date(timestampMs).toISOString();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parseTime(value: string): number {
|
|
85
|
-
const parsed = Date.parse(value);
|
|
86
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function normalizeTtlDays(value: number | undefined): number {
|
|
90
|
-
if (value === undefined) {
|
|
91
|
-
return DEFAULT_TTL_DAYS;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Math.max(0, Math.trunc(value));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function normalizeConfidence(value: number | undefined): number {
|
|
98
|
-
if (value === undefined || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
99
|
-
return 1;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (value <= 0) {
|
|
103
|
-
return 0;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (value >= 1) {
|
|
107
|
-
return 1;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function toExpireAt(nowMs: number, ttlDays: number): string {
|
|
114
|
-
return toIso(nowMs + normalizeTtlDays(ttlDays) * DAY_MS);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function isScopeMatch(
|
|
118
|
-
entry: NavigationMemoryEntry,
|
|
119
|
-
input: Pick<NavigationMemoryResolveInput, "domain" | "profileId" | "intentKey">
|
|
120
|
-
): boolean {
|
|
121
|
-
return entry.domain === input.domain && entry.profileId === input.profileId && entry.intentKey === input.intentKey;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function cloneSignal(signal: NavigationSignal): NavigationSignal {
|
|
125
|
-
if (signal.kind === "hop") {
|
|
126
|
-
return {
|
|
127
|
-
kind: "hop",
|
|
128
|
-
from: signal.from,
|
|
129
|
-
to: signal.to
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
kind: signal.kind,
|
|
135
|
-
value: signal.value
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function cloneEntry(entry: NavigationMemoryEntry): NavigationMemoryEntry {
|
|
140
|
-
return {
|
|
141
|
-
...entry,
|
|
142
|
-
signals: entry.signals.map((signal) => cloneSignal(signal))
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isExpired(entry: NavigationMemoryEntry, nowMs: number): boolean {
|
|
147
|
-
return parseTime(entry.expireAt) <= nowMs;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function isNewer(a: NavigationMemoryEntry, b: NavigationMemoryEntry): boolean {
|
|
151
|
-
return parseTime(a.lastHitAt) > parseTime(b.lastHitAt);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function createNavigationMemoryStore(options: NavigationMemoryStoreOptions): NavigationMemoryStore {
|
|
155
|
-
if (options.path.trim().length === 0) {
|
|
156
|
-
throw new Error("Navigation memory path must not be empty.");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const now = options.now ?? (() => Date.now());
|
|
160
|
-
let mode: NavigationMemoryMode = options.mode ?? "auto";
|
|
161
|
-
let ttlDays = normalizeTtlDays(options.ttlDays);
|
|
162
|
-
const entries = new Map<string, NavigationMemoryEntry>();
|
|
163
|
-
let nextId = 1;
|
|
164
|
-
|
|
165
|
-
function createId(): string {
|
|
166
|
-
const id = `memory:${nextId}`;
|
|
167
|
-
nextId += 1;
|
|
168
|
-
return id;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function pruneExpired(): number {
|
|
172
|
-
const currentTime = now();
|
|
173
|
-
let removed = 0;
|
|
174
|
-
for (const [entryId, entry] of entries.entries()) {
|
|
175
|
-
if (!isExpired(entry, currentTime)) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
entries.delete(entryId);
|
|
180
|
-
removed += 1;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return removed;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function findScopedEntry(input: NavigationMemoryResolveInput): NavigationMemoryEntry | undefined {
|
|
187
|
-
let selected: NavigationMemoryEntry | undefined;
|
|
188
|
-
for (const entry of entries.values()) {
|
|
189
|
-
if (!isScopeMatch(entry, input)) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (selected === undefined) {
|
|
194
|
-
selected = entry;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (entry.confidence > selected.confidence || (entry.confidence === selected.confidence && isNewer(entry, selected))) {
|
|
199
|
-
selected = entry;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return selected;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getStatus(): NavigationMemoryStatus {
|
|
207
|
-
const currentTime = now();
|
|
208
|
-
const totalEntries = Array.from(entries.values()).filter((entry) => !isExpired(entry, currentTime)).length;
|
|
209
|
-
return {
|
|
210
|
-
path: options.path,
|
|
211
|
-
mode,
|
|
212
|
-
ttlDays,
|
|
213
|
-
totalEntries
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function assertUpsertAllowed(input: NavigationMemoryUpsertInput): void {
|
|
218
|
-
if (mode === "auto") {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (mode === "ask") {
|
|
223
|
-
if (input.confirmed === true) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
throw new Error("Navigation memory write requires confirmation in ask mode.");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
throw new Error("Navigation memory write is disabled when mode is off.");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
async resolve(input: NavigationMemoryResolveInput): Promise<NavigationMemoryResolveResult> {
|
|
235
|
-
if (mode === "off") {
|
|
236
|
-
return { hit: false };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
pruneExpired();
|
|
240
|
-
const existing = findScopedEntry(input);
|
|
241
|
-
if (existing === undefined) {
|
|
242
|
-
return { hit: false };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const currentTime = now();
|
|
246
|
-
const updated: NavigationMemoryEntry = {
|
|
247
|
-
...existing,
|
|
248
|
-
lastHitAt: toIso(currentTime),
|
|
249
|
-
expireAt: toExpireAt(currentTime, ttlDays),
|
|
250
|
-
hitCount: existing.hitCount + 1
|
|
251
|
-
};
|
|
252
|
-
entries.set(updated.id, updated);
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
hit: true,
|
|
256
|
-
entry: cloneEntry(updated)
|
|
257
|
-
};
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
async upsert(input: NavigationMemoryUpsertInput): Promise<NavigationMemoryUpsertResult> {
|
|
261
|
-
assertUpsertAllowed(input);
|
|
262
|
-
pruneExpired();
|
|
263
|
-
const currentTime = now();
|
|
264
|
-
const confidence = normalizeConfidence(input.confidence);
|
|
265
|
-
const signals = input.signals.map((signal) => cloneSignal(signal));
|
|
266
|
-
const existing = findScopedEntry(input);
|
|
267
|
-
|
|
268
|
-
if (existing !== undefined) {
|
|
269
|
-
const updated: NavigationMemoryEntry = {
|
|
270
|
-
...existing,
|
|
271
|
-
signals,
|
|
272
|
-
confidence,
|
|
273
|
-
expireAt: toExpireAt(currentTime, ttlDays)
|
|
274
|
-
};
|
|
275
|
-
entries.set(updated.id, updated);
|
|
276
|
-
return {
|
|
277
|
-
created: false,
|
|
278
|
-
entry: cloneEntry(updated)
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const createdAt = toIso(currentTime);
|
|
283
|
-
const created: NavigationMemoryEntry = {
|
|
284
|
-
id: createId(),
|
|
285
|
-
domain: input.domain,
|
|
286
|
-
profileId: input.profileId,
|
|
287
|
-
intentKey: input.intentKey,
|
|
288
|
-
signals,
|
|
289
|
-
confidence,
|
|
290
|
-
createdAt,
|
|
291
|
-
lastHitAt: createdAt,
|
|
292
|
-
expireAt: toExpireAt(currentTime, ttlDays),
|
|
293
|
-
hitCount: 0
|
|
294
|
-
};
|
|
295
|
-
entries.set(created.id, created);
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
created: true,
|
|
299
|
-
entry: cloneEntry(created)
|
|
300
|
-
};
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
async list(filter?: NavigationMemoryListInput): Promise<NavigationMemoryEntry[]> {
|
|
304
|
-
pruneExpired();
|
|
305
|
-
return Array.from(entries.values())
|
|
306
|
-
.filter((entry) => {
|
|
307
|
-
if (filter === undefined) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
if (filter.domain !== undefined && filter.domain !== entry.domain) {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
if (filter.profileId !== undefined && filter.profileId !== entry.profileId) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
if (filter.intentKey !== undefined && filter.intentKey !== entry.intentKey) {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
return true;
|
|
320
|
-
})
|
|
321
|
-
.map((entry) => cloneEntry(entry));
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
async inspect(id: string): Promise<NavigationMemoryEntry | undefined> {
|
|
325
|
-
pruneExpired();
|
|
326
|
-
const entry = entries.get(id);
|
|
327
|
-
return entry === undefined ? undefined : cloneEntry(entry);
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
async delete(id: string): Promise<boolean> {
|
|
331
|
-
return entries.delete(id);
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
async purgeExpired(): Promise<number> {
|
|
335
|
-
return pruneExpired();
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
async setMode(nextMode: NavigationMemoryMode): Promise<NavigationMemoryStatus> {
|
|
339
|
-
mode = nextMode;
|
|
340
|
-
return getStatus();
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
async setTtlDays(nextTtlDays: number): Promise<NavigationMemoryStatus> {
|
|
344
|
-
pruneExpired();
|
|
345
|
-
ttlDays = normalizeTtlDays(nextTtlDays);
|
|
346
|
-
const refreshedExpireAt = toExpireAt(now(), ttlDays);
|
|
347
|
-
for (const [entryId, entry] of entries.entries()) {
|
|
348
|
-
entries.set(entryId, {
|
|
349
|
-
...entry,
|
|
350
|
-
expireAt: refreshedExpireAt
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
return getStatus();
|
|
354
|
-
},
|
|
355
|
-
|
|
356
|
-
status(): NavigationMemoryStatus {
|
|
357
|
-
return getStatus();
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { RefCache, createRefKey } from "./ref-cache";
|
|
4
|
-
|
|
5
|
-
describe("RefCache", () => {
|
|
6
|
-
it("uses sessionId|profile|targetId as cache key", () => {
|
|
7
|
-
const cache = new RefCache<string>();
|
|
8
|
-
|
|
9
|
-
cache.set("session:a", "managed", "target:1", "value-a");
|
|
10
|
-
cache.set("session:a", "managed", "target:2", "value-b");
|
|
11
|
-
cache.set("session:a", "chrome-relay", "target:1", "value-c");
|
|
12
|
-
|
|
13
|
-
expect(cache.get("session:a", "managed", "target:1")).toBe("value-a");
|
|
14
|
-
expect(cache.get("session:a", "managed", "target:2")).toBe("value-b");
|
|
15
|
-
expect(cache.get("session:a", "chrome-relay", "target:1")).toBe("value-c");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("does not collide across sessionIds", () => {
|
|
19
|
-
const cache = new RefCache<string>();
|
|
20
|
-
|
|
21
|
-
cache.set("session:a", "managed", "target:1", "value-a");
|
|
22
|
-
cache.set("session:b", "managed", "target:1", "value-b");
|
|
23
|
-
|
|
24
|
-
expect(cache.get("session:a", "managed", "target:1")).toBe("value-a");
|
|
25
|
-
expect(cache.get("session:b", "managed", "target:1")).toBe("value-b");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("escapes key segments before joining with |", () => {
|
|
29
|
-
expect(createRefKey("session|a", "managed|x", "target|1")).toBe("session%7Ca|managed%7Cx|target%7C1");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("does not collide when identifiers contain |", () => {
|
|
33
|
-
const cache = new RefCache<string>();
|
|
34
|
-
|
|
35
|
-
cache.set("session|a", "managed", "target", "value-a");
|
|
36
|
-
cache.set("session", "a|managed", "target", "value-b");
|
|
37
|
-
|
|
38
|
-
expect(cache.get("session|a", "managed", "target")).toBe("value-a");
|
|
39
|
-
expect(cache.get("session", "a|managed", "target")).toBe("value-b");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("returns undefined for unknown key", () => {
|
|
43
|
-
const cache = new RefCache<string>();
|
|
44
|
-
|
|
45
|
-
expect(cache.get("missing", "managed", "target:1")).toBeUndefined();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("deletes an existing key", () => {
|
|
49
|
-
const cache = new RefCache<string>();
|
|
50
|
-
cache.set("session:a", "managed", "target:1", "value-a");
|
|
51
|
-
|
|
52
|
-
expect(cache.delete("session:a", "managed", "target:1")).toBe(true);
|
|
53
|
-
expect(cache.get("session:a", "managed", "target:1")).toBeUndefined();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns false when deleting an unknown key", () => {
|
|
57
|
-
const cache = new RefCache<string>();
|
|
58
|
-
|
|
59
|
-
expect(cache.delete("missing", "managed", "target:1")).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ProfileId, SessionId, TargetId } from "./types";
|
|
2
|
-
|
|
3
|
-
function escapeKeySegment(value: string): string {
|
|
4
|
-
return encodeURIComponent(value);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function createRefKey(sessionId: SessionId, profile: ProfileId, targetId: TargetId): string {
|
|
8
|
-
return `${escapeKeySegment(sessionId)}|${escapeKeySegment(profile)}|${escapeKeySegment(targetId)}`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class RefCache<TValue> {
|
|
12
|
-
private readonly refs = new Map<string, TValue>();
|
|
13
|
-
|
|
14
|
-
get(sessionId: SessionId, profile: ProfileId, targetId: TargetId): TValue | undefined {
|
|
15
|
-
const key = createRefKey(sessionId, profile, targetId);
|
|
16
|
-
return this.refs.get(key);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
set(sessionId: SessionId, profile: ProfileId, targetId: TargetId, value: TValue): void {
|
|
20
|
-
const key = createRefKey(sessionId, profile, targetId);
|
|
21
|
-
this.refs.set(key, value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
delete(sessionId: SessionId, profile: ProfileId, targetId: TargetId): boolean {
|
|
25
|
-
const key = createRefKey(sessionId, profile, targetId);
|
|
26
|
-
return this.refs.delete(key);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { SessionStore } from "./session-store";
|
|
4
|
-
|
|
5
|
-
describe("SessionStore", () => {
|
|
6
|
-
it("binds profile to session", () => {
|
|
7
|
-
const store = new SessionStore();
|
|
8
|
-
|
|
9
|
-
store.useProfile("cli:local", "managed");
|
|
10
|
-
|
|
11
|
-
expect(store.get("cli:local")?.profile).toBe("managed");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("uses sessionId as the only store key", () => {
|
|
15
|
-
const store = new SessionStore();
|
|
16
|
-
|
|
17
|
-
store.useProfile("session:a", "managed");
|
|
18
|
-
store.useProfile("session:b", "chrome-relay");
|
|
19
|
-
|
|
20
|
-
expect(store.get("session:a")).toEqual({
|
|
21
|
-
sessionId: "session:a",
|
|
22
|
-
profile: "managed"
|
|
23
|
-
});
|
|
24
|
-
expect(store.get("session:b")).toEqual({
|
|
25
|
-
sessionId: "session:b",
|
|
26
|
-
profile: "chrome-relay"
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("returns undefined for unknown session", () => {
|
|
31
|
-
const store = new SessionStore();
|
|
32
|
-
|
|
33
|
-
expect(store.get("missing")).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("deletes an existing session", () => {
|
|
37
|
-
const store = new SessionStore();
|
|
38
|
-
store.useProfile("cli:local", "managed");
|
|
39
|
-
|
|
40
|
-
expect(store.delete("cli:local")).toBe(true);
|
|
41
|
-
expect(store.get("cli:local")).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns false when deleting an unknown session", () => {
|
|
45
|
-
const store = new SessionStore();
|
|
46
|
-
|
|
47
|
-
expect(store.delete("missing")).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("expires sessions after ttl and returns undefined", () => {
|
|
51
|
-
let now = 1_000;
|
|
52
|
-
const store = new SessionStore({
|
|
53
|
-
ttlMs: 100,
|
|
54
|
-
now: () => now
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
store.useProfile("session:ttl", "managed");
|
|
58
|
-
now += 150;
|
|
59
|
-
|
|
60
|
-
expect(store.get("session:ttl")).toBeUndefined();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("cleans up expired sessions and keeps active ones", () => {
|
|
64
|
-
let now = 1_000;
|
|
65
|
-
const store = new SessionStore({
|
|
66
|
-
ttlMs: 100,
|
|
67
|
-
now: () => now
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
store.useProfile("session:old", "managed");
|
|
71
|
-
now += 50;
|
|
72
|
-
store.useProfile("session:active", "managed");
|
|
73
|
-
now += 60;
|
|
74
|
-
|
|
75
|
-
expect(store.cleanupExpired()).toBe(1);
|
|
76
|
-
expect(store.get("session:old")).toBeUndefined();
|
|
77
|
-
expect(store.get("session:active")).toEqual({
|
|
78
|
-
sessionId: "session:active",
|
|
79
|
-
profile: "managed"
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { ProfileId, SessionId, SessionState, TargetId } from "./types";
|
|
2
|
-
|
|
3
|
-
function createEmptySession(sessionId: SessionId): SessionState {
|
|
4
|
-
return { sessionId };
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type SessionStoreOptions = {
|
|
8
|
-
ttlMs?: number;
|
|
9
|
-
now?: () => number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type SessionEntry = {
|
|
13
|
-
state: SessionState;
|
|
14
|
-
touchedAt: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type SessionSnapshot = {
|
|
18
|
-
state: SessionState;
|
|
19
|
-
touchedAt: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export class SessionStore {
|
|
23
|
-
private readonly sessions = new Map<SessionId, SessionEntry>();
|
|
24
|
-
private readonly ttlMs: number;
|
|
25
|
-
private readonly now: () => number;
|
|
26
|
-
|
|
27
|
-
constructor(options: SessionStoreOptions = {}) {
|
|
28
|
-
this.ttlMs = options.ttlMs === undefined ? 0 : Math.max(0, Math.trunc(options.ttlMs));
|
|
29
|
-
this.now = options.now ?? (() => Date.now());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private isExpired(entry: SessionEntry, now: number): boolean {
|
|
33
|
-
return this.ttlMs > 0 && now - entry.touchedAt > this.ttlMs;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
private readActiveEntry(sessionId: SessionId, touch = false): SessionEntry | undefined {
|
|
37
|
-
const existing = this.sessions.get(sessionId);
|
|
38
|
-
if (existing === undefined) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const currentTime = this.now();
|
|
43
|
-
if (this.isExpired(existing, currentTime)) {
|
|
44
|
-
this.sessions.delete(sessionId);
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (touch) {
|
|
49
|
-
const touchedEntry: SessionEntry = {
|
|
50
|
-
state: existing.state,
|
|
51
|
-
touchedAt: currentTime
|
|
52
|
-
};
|
|
53
|
-
this.sessions.set(sessionId, touchedEntry);
|
|
54
|
-
return touchedEntry;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return existing;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
get(sessionId: SessionId): SessionState | undefined {
|
|
61
|
-
return this.readActiveEntry(sessionId)?.state;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
useProfile(sessionId: SessionId, profile: ProfileId): SessionState {
|
|
65
|
-
const current = this.readActiveEntry(sessionId, true)?.state ?? createEmptySession(sessionId);
|
|
66
|
-
const next = { ...current, profile };
|
|
67
|
-
|
|
68
|
-
this.sessions.set(sessionId, {
|
|
69
|
-
state: next,
|
|
70
|
-
touchedAt: this.now()
|
|
71
|
-
});
|
|
72
|
-
return next;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
useTarget(sessionId: SessionId, targetId: TargetId): SessionState {
|
|
76
|
-
const current = this.readActiveEntry(sessionId, true)?.state ?? createEmptySession(sessionId);
|
|
77
|
-
const next = { ...current, targetId };
|
|
78
|
-
|
|
79
|
-
this.sessions.set(sessionId, {
|
|
80
|
-
state: next,
|
|
81
|
-
touchedAt: this.now()
|
|
82
|
-
});
|
|
83
|
-
return next;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
delete(sessionId: SessionId): boolean {
|
|
87
|
-
return this.sessions.delete(sessionId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
cleanupExpired(): number {
|
|
91
|
-
if (this.ttlMs <= 0 || this.sessions.size === 0) {
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const currentTime = this.now();
|
|
96
|
-
let removedCount = 0;
|
|
97
|
-
for (const [sessionId, entry] of this.sessions.entries()) {
|
|
98
|
-
if (!this.isExpired(entry, currentTime)) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.sessions.delete(sessionId);
|
|
103
|
-
removedCount += 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return removedCount;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
has(sessionId: SessionId): boolean {
|
|
110
|
-
return this.readActiveEntry(sessionId) !== undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
listSnapshots(): SessionSnapshot[] {
|
|
114
|
-
const snapshots: SessionSnapshot[] = [];
|
|
115
|
-
const currentTime = this.now();
|
|
116
|
-
for (const [sessionId, entry] of this.sessions.entries()) {
|
|
117
|
-
if (this.isExpired(entry, currentTime)) {
|
|
118
|
-
this.sessions.delete(sessionId);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
snapshots.push({
|
|
123
|
-
state: entry.state,
|
|
124
|
-
touchedAt: entry.touchedAt
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return snapshots;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
list(): SessionState[] {
|
|
132
|
-
return this.listSnapshots().map((snapshot) => snapshot.state);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
size(): number {
|
|
136
|
-
return this.listSnapshots().length;
|
|
137
|
-
}
|
|
138
|
-
}
|