@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.
Files changed (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -59
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -512
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1522
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. 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
- }
@@ -1,9 +0,0 @@
1
- export type SessionId = string;
2
- export type ProfileId = string;
3
- export type TargetId = string;
4
-
5
- export type SessionState = {
6
- sessionId: SessionId;
7
- profile?: ProfileId;
8
- targetId?: TargetId;
9
- };