@flrande/browserctl 0.1.0-dev.7.1
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/LICENSE +21 -0
- package/README-CN.md +66 -0
- package/README.md +66 -0
- package/apps/browserctl/src/commands/a11y-snapshot.ts +20 -0
- package/apps/browserctl/src/commands/act.ts +20 -0
- package/apps/browserctl/src/commands/common.test.ts +87 -0
- package/apps/browserctl/src/commands/common.ts +191 -0
- package/apps/browserctl/src/commands/console-list.ts +20 -0
- package/apps/browserctl/src/commands/cookie-clear.ts +18 -0
- package/apps/browserctl/src/commands/cookie-get.ts +18 -0
- package/apps/browserctl/src/commands/cookie-set.ts +22 -0
- package/apps/browserctl/src/commands/dialog-arm.ts +20 -0
- package/apps/browserctl/src/commands/dom-query-all.ts +18 -0
- package/apps/browserctl/src/commands/dom-query.ts +18 -0
- package/apps/browserctl/src/commands/download-trigger.ts +22 -0
- package/apps/browserctl/src/commands/download-wait.test.ts +67 -0
- package/apps/browserctl/src/commands/download-wait.ts +27 -0
- package/apps/browserctl/src/commands/element-screenshot.ts +20 -0
- package/apps/browserctl/src/commands/frame-list.ts +16 -0
- package/apps/browserctl/src/commands/frame-snapshot.ts +18 -0
- package/apps/browserctl/src/commands/network-wait-for.ts +100 -0
- package/apps/browserctl/src/commands/profile-list.ts +16 -0
- package/apps/browserctl/src/commands/profile-use.ts +18 -0
- package/apps/browserctl/src/commands/response-body.ts +24 -0
- package/apps/browserctl/src/commands/screenshot.ts +16 -0
- package/apps/browserctl/src/commands/snapshot.ts +16 -0
- package/apps/browserctl/src/commands/status.ts +10 -0
- package/apps/browserctl/src/commands/storage-get.ts +20 -0
- package/apps/browserctl/src/commands/storage-set.ts +22 -0
- package/apps/browserctl/src/commands/tab-close.ts +20 -0
- package/apps/browserctl/src/commands/tab-focus.ts +20 -0
- package/apps/browserctl/src/commands/tab-open.ts +19 -0
- package/apps/browserctl/src/commands/tabs.ts +13 -0
- package/apps/browserctl/src/commands/upload-arm.ts +26 -0
- package/apps/browserctl/src/daemon-client.test.ts +253 -0
- package/apps/browserctl/src/daemon-client.ts +632 -0
- package/apps/browserctl/src/e2e.test.ts +99 -0
- package/apps/browserctl/src/main.test.ts +215 -0
- package/apps/browserctl/src/main.ts +372 -0
- package/apps/browserctl/src/smoke.test.ts +16 -0
- package/apps/browserctl/src/smoke.ts +5 -0
- package/apps/browserd/src/bootstrap.ts +432 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +275 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +506 -0
- package/apps/browserd/src/container.ts +1531 -0
- package/apps/browserd/src/main.test.ts +864 -0
- package/apps/browserd/src/main.ts +7 -0
- package/bin/browserctl.cjs +21 -0
- package/bin/browserd.cjs +21 -0
- package/extensions/chrome-relay/README-CN.md +38 -0
- package/extensions/chrome-relay/README.md +38 -0
- package/extensions/chrome-relay/background.js +1687 -0
- package/extensions/chrome-relay/manifest.json +15 -0
- package/extensions/chrome-relay/popup.html +369 -0
- package/extensions/chrome-relay/popup.js +972 -0
- package/package.json +51 -0
- package/packages/core/src/bootstrap.test.ts +10 -0
- package/packages/core/src/driver-registry.test.ts +45 -0
- package/packages/core/src/driver-registry.ts +22 -0
- package/packages/core/src/driver.ts +47 -0
- package/packages/core/src/index.ts +5 -0
- package/packages/core/src/ref-cache.test.ts +61 -0
- package/packages/core/src/ref-cache.ts +28 -0
- package/packages/core/src/session-store.test.ts +49 -0
- package/packages/core/src/session-store.ts +33 -0
- package/packages/core/src/types.ts +9 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
- package/packages/driver-chrome-relay/src/index.ts +26 -0
- package/packages/driver-managed/src/index.ts +22 -0
- package/packages/driver-managed/src/managed-driver.test.ts +59 -0
- package/packages/driver-managed/src/managed-driver.ts +125 -0
- package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
- package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
- package/packages/driver-remote-cdp/src/index.ts +19 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
- package/packages/protocol/src/envelope.test.ts +25 -0
- package/packages/protocol/src/envelope.ts +31 -0
- package/packages/protocol/src/errors.test.ts +17 -0
- package/packages/protocol/src/errors.ts +11 -0
- package/packages/protocol/src/index.ts +3 -0
- package/packages/protocol/src/tools.ts +3 -0
- package/packages/transport-mcp-stdio/src/index.ts +3 -0
- package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
- package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
- package/packages/transport-mcp-stdio/src/server.ts +183 -0
- package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
- package/scripts/smoke.ps1 +127 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flrande/browserctl",
|
|
3
|
+
"version": "0.1.0-dev.7.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"bin": {
|
|
6
|
+
"browserctl": "bin/browserctl.cjs",
|
|
7
|
+
"browserd": "bin/browserd.cjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"apps/browserctl/src",
|
|
11
|
+
"apps/browserd/src",
|
|
12
|
+
"packages/core/src",
|
|
13
|
+
"packages/driver-chrome-relay/src",
|
|
14
|
+
"packages/driver-managed/src",
|
|
15
|
+
"packages/driver-remote-cdp/src",
|
|
16
|
+
"packages/protocol/src",
|
|
17
|
+
"packages/transport-mcp-stdio/src",
|
|
18
|
+
"extensions/chrome-relay",
|
|
19
|
+
"scripts/smoke.ps1",
|
|
20
|
+
"bin",
|
|
21
|
+
"README.md",
|
|
22
|
+
"README-CN.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"!**/*.test.ts",
|
|
25
|
+
"!**/vitest.config.ts",
|
|
26
|
+
"!**/node_modules/**",
|
|
27
|
+
"!**/.vite/**"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.17.4",
|
|
31
|
+
"playwright-core": "^1.52.0",
|
|
32
|
+
"tsx": "^4.21.0",
|
|
33
|
+
"ws": "^8.19.0",
|
|
34
|
+
"zod": "^4.3.6"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.15.30",
|
|
38
|
+
"typescript": "^5.6.3",
|
|
39
|
+
"vitest": "^2.1.8"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "pnpm run test:unit",
|
|
43
|
+
"test:unit": "vitest run --config vitest.config.ts",
|
|
44
|
+
"test:contract": "vitest run --config vitest.contract.config.ts",
|
|
45
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
46
|
+
"test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e",
|
|
47
|
+
"build": "pnpm publish --dry-run --no-git-checks",
|
|
48
|
+
"typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
|
|
49
|
+
"lint": "node ./scripts/lint.mjs"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("workspace bootstrap", () => {
|
|
4
|
+
it("loads test runner", () => {
|
|
5
|
+
const state = expect.getState();
|
|
6
|
+
|
|
7
|
+
expect(state.currentTestName).toContain("loads test runner");
|
|
8
|
+
expect(state.testPath).toContain("bootstrap.test.ts");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { BrowserDriver } from "./driver";
|
|
4
|
+
import { DriverRegistry } from "./driver-registry";
|
|
5
|
+
|
|
6
|
+
function createDriver(): BrowserDriver {
|
|
7
|
+
return {
|
|
8
|
+
status: async () => ({ kind: "test" }),
|
|
9
|
+
listProfiles: async () => [],
|
|
10
|
+
listTabs: async () => [],
|
|
11
|
+
openTab: async () => "target:1",
|
|
12
|
+
focusTab: async () => {},
|
|
13
|
+
closeTab: async () => {},
|
|
14
|
+
snapshot: async () => ({ html: "" }),
|
|
15
|
+
act: async () => ({ ok: true }),
|
|
16
|
+
armUpload: async () => {},
|
|
17
|
+
armDialog: async () => {},
|
|
18
|
+
waitDownload: async () => ({ path: "download.bin" }),
|
|
19
|
+
triggerDownload: async () => {}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("DriverRegistry", () => {
|
|
24
|
+
it("returns a registered driver", () => {
|
|
25
|
+
const registry = new DriverRegistry();
|
|
26
|
+
const driver = createDriver();
|
|
27
|
+
|
|
28
|
+
registry.register("managed", driver);
|
|
29
|
+
|
|
30
|
+
expect(registry.get("managed")).toBe(driver);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws when registering a duplicate driver name", () => {
|
|
34
|
+
const registry = new DriverRegistry();
|
|
35
|
+
registry.register("managed", createDriver());
|
|
36
|
+
|
|
37
|
+
expect(() => registry.register("managed", createDriver())).toThrowError("Driver already registered: managed");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws when the requested driver is not registered", () => {
|
|
41
|
+
const registry = new DriverRegistry();
|
|
42
|
+
|
|
43
|
+
expect(() => registry.get("missing")).toThrowError("Unknown driver: missing");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BrowserDriver } from "./driver";
|
|
2
|
+
|
|
3
|
+
export class DriverRegistry {
|
|
4
|
+
readonly #drivers = new Map<string, BrowserDriver>();
|
|
5
|
+
|
|
6
|
+
register(name: string, driver: BrowserDriver): void {
|
|
7
|
+
if (this.#drivers.has(name)) {
|
|
8
|
+
throw new Error(`Driver already registered: ${name}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.#drivers.set(name, driver);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get(name: string): BrowserDriver {
|
|
15
|
+
const driver = this.#drivers.get(name);
|
|
16
|
+
if (!driver) {
|
|
17
|
+
throw new Error(`Unknown driver: ${name}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return driver;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ProfileId, TargetId } from "./types";
|
|
2
|
+
|
|
3
|
+
export type DriverScalar = string | number | boolean | null;
|
|
4
|
+
export type DriverValue = DriverScalar | DriverObject | DriverValue[];
|
|
5
|
+
export type DriverObject = { [key: string]: DriverValue };
|
|
6
|
+
|
|
7
|
+
export type DriverStatus = DriverObject;
|
|
8
|
+
export type DriverSnapshot = DriverObject;
|
|
9
|
+
export type DriverActionPayload = DriverObject;
|
|
10
|
+
export type DriverActionResult = DriverObject;
|
|
11
|
+
export type DriverDownloadResult = DriverObject;
|
|
12
|
+
export type DriverScreenshot = DriverObject;
|
|
13
|
+
|
|
14
|
+
export type DriverAction<TPayload extends DriverObject = DriverActionPayload> = {
|
|
15
|
+
type: string;
|
|
16
|
+
payload?: TPayload;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type BrowserDriverScreenshot<
|
|
20
|
+
TScreenshot extends DriverObject = DriverScreenshot
|
|
21
|
+
> = {
|
|
22
|
+
screenshot(targetId: TargetId, profile?: ProfileId): Promise<TScreenshot>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface BrowserDriver<
|
|
26
|
+
TStatus extends DriverObject = DriverStatus,
|
|
27
|
+
TSnapshot extends DriverObject = DriverSnapshot,
|
|
28
|
+
TAction extends DriverAction = DriverAction,
|
|
29
|
+
TActionResult extends DriverObject = DriverActionResult,
|
|
30
|
+
TDownload extends DriverObject = DriverDownloadResult
|
|
31
|
+
> {
|
|
32
|
+
status(): Promise<TStatus>;
|
|
33
|
+
listProfiles(): Promise<ProfileId[]>;
|
|
34
|
+
listTabs(profile?: ProfileId): Promise<TargetId[]>;
|
|
35
|
+
|
|
36
|
+
openTab(url: string, profile?: ProfileId): Promise<TargetId>;
|
|
37
|
+
focusTab(targetId: TargetId, profile?: ProfileId): Promise<void>;
|
|
38
|
+
closeTab(targetId: TargetId, profile?: ProfileId): Promise<void>;
|
|
39
|
+
|
|
40
|
+
snapshot(targetId: TargetId, profile?: ProfileId): Promise<TSnapshot>;
|
|
41
|
+
act(action: TAction, targetId: TargetId, profile?: ProfileId): Promise<TActionResult>;
|
|
42
|
+
|
|
43
|
+
armUpload(targetId: TargetId, files: string[], profile?: ProfileId): Promise<void>;
|
|
44
|
+
armDialog(targetId: TargetId, profile?: ProfileId): Promise<void>;
|
|
45
|
+
waitDownload(targetId: TargetId, profile?: ProfileId): Promise<TDownload>;
|
|
46
|
+
triggerDownload(targetId: TargetId, profile?: ProfileId): Promise<void>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProfileId, SessionId, SessionState, TargetId } from "./types";
|
|
2
|
+
|
|
3
|
+
function createEmptySession(sessionId: SessionId): SessionState {
|
|
4
|
+
return { sessionId };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class SessionStore {
|
|
8
|
+
private readonly sessions = new Map<SessionId, SessionState>();
|
|
9
|
+
|
|
10
|
+
get(sessionId: SessionId): SessionState | undefined {
|
|
11
|
+
return this.sessions.get(sessionId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
useProfile(sessionId: SessionId, profile: ProfileId): SessionState {
|
|
15
|
+
const current = this.sessions.get(sessionId) ?? createEmptySession(sessionId);
|
|
16
|
+
const next = { ...current, profile };
|
|
17
|
+
|
|
18
|
+
this.sessions.set(sessionId, next);
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
useTarget(sessionId: SessionId, targetId: TargetId): SessionState {
|
|
23
|
+
const current = this.sessions.get(sessionId) ?? createEmptySession(sessionId);
|
|
24
|
+
const next = { ...current, targetId };
|
|
25
|
+
|
|
26
|
+
this.sessions.set(sessionId, next);
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
delete(sessionId: SessionId): boolean {
|
|
31
|
+
return this.sessions.delete(sessionId);
|
|
32
|
+
}
|
|
33
|
+
}
|