Repository navigation
Expand file tree
/
Copy pathstate.ts
More file actions
135 lines (119 loc) · 5.16 KB
/
Copy pathstate.ts
File metadata and controls
135 lines (119 loc) · 5.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// What the mock backend remembers between calls.
//
// In memory, reset on reload. Handler files read and change it; nothing else
// should, so a screen behaves the same however it was reached.
import type { ConnectionInfo, ConnectionProfileSummary_Serialize } from "../../lib/bindings";
import { PROFILES, SERVER_PROFILES } from "./dataset";
/**
* Which starting point the page was opened with: `?mock`, or
* `?mock=<scenario>`. README.md lists them with the switches that combine
* with them.
*
* - `default`: saved profiles, nothing connected.
* - `empty`: a first run, with no saved profiles.
* - `connected`: "Local dev" already connected, for jumping straight to the
* editor, the grid and the schema tree.
* - `lost`: `connected`, then the server stops answering a few seconds in:
* the health checks fail and keep failing, and every read on it fails.
* - `servers`: the default profiles plus one per remaining connect outcome:
* a read-write production server, a MariaDB that connects, a wrong
* password and a server that never answers.
*/
export type Scenario = "default" | "empty" | "connected" | "lost" | "servers";
const SCENARIOS: readonly Scenario[] = ["default", "empty", "connected", "lost", "servers"];
/** Whether the scenario starts with "Local dev" connected. */
export const startsConnected = (scenario: Scenario) => scenario === "connected" || scenario === "lost";
export function scenarioFromUrl(search: string): Scenario {
const value = new URLSearchParams(search).get("mock");
return SCENARIOS.find((s) => s === value) ?? "default";
}
/**
* Switches that combine with any scenario, each its own URL parameter.
*
* - `fail=<cmd>,<cmd>`: those commands reject (see `failures.ts`). A name
* ending in `*` matches every command that starts with the rest.
* - `update=available|downloading|download-error|error`: what the updater
* finds (see handlers/plugins.ts). Absent: up to date.
* - `keyring=missing`: no OS credential store.
* - `startup=problems`: the backend reports that the connection and history
* stores could not be opened.
* - `agent=uninstalled,no-tools`: no harness installed; sessions without the
* database tools.
*/
export interface MockOptions {
fail: string[];
update: UpdateMode;
keyring: boolean;
startupProblems: boolean;
agent: string[];
}
export type UpdateMode = "none" | "available" | "downloading" | "download-error" | "error";
const UPDATE_MODES: readonly UpdateMode[] = ["none", "available", "downloading", "download-error", "error"];
export const defaultOptions = (): MockOptions => ({
fail: [],
update: "none",
keyring: true,
startupProblems: false,
agent: [],
});
const list = (value: string | null): string[] => (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
export function optionsFromUrl(search: string): MockOptions {
const params = new URLSearchParams(search);
return {
fail: list(params.get("fail")),
update: UPDATE_MODES.find((m) => m === params.get("update")) ?? "none",
keyring: params.get("keyring") !== "missing",
startupProblems: params.get("startup") === "problems",
agent: list(params.get("agent")),
};
}
export const state = {
scenario: "default" as Scenario,
options: defaultOptions(),
profiles: [] as ConnectionProfileSummary_Serialize[],
/** Live connections, by connection id. */
connections: new Map<string, ConnectionInfo>(),
/**
* Connections whose server has stopped answering, with how many health
* checks have failed in a row.
*/
down: new Map<string, number>(),
/**
* Whether the scenario's "Local dev" connection has been opened. Once it
* has, listing connections must not open it again: a user who disconnected
* it expects it to stay closed (#921).
*/
scenarioOpened: false,
/** Counter for ids the mock hands out. */
nextId: 1,
};
export function newId(prefix: string): string {
return `${prefix}-${state.nextId++}`;
}
export function resetState(scenario: Scenario, options: MockOptions = defaultOptions()): void {
state.scenario = scenario;
state.options = options;
const profiles = scenario === "empty" ? [] : scenario === "servers" ? [...PROFILES, ...SERVER_PROFILES] : PROFILES;
state.profiles = profiles.map((p) => ({ ...p }));
state.connections.clear();
state.down.clear();
state.scenarioOpened = false;
state.nextId = 1;
}
/** What a read on a connection whose server has gone fails with. */
export const LOST_CONNECTION =
"error returned from database: 2013 (HY000): Lost connection to MySQL server during query";
/**
* The live connection for an id, or the error the backend gives.
*
* A connection whose server has gone (`?mock=lost`) fails too, as every
* command that talks to the server would; `evenIfDown` is for the few that
* do not, such as disconnecting.
*/
export function liveConnection(connectionId: unknown, { evenIfDown = false } = {}): ConnectionInfo {
const conn = state.connections.get(String(connectionId));
// The backend's wording: CoreError::NotFound around the manager's message.
if (!conn) throw `Not found: Connection not found: ${String(connectionId)}`;
if (!evenIfDown && state.down.has(conn.id)) throw `Query error: ${LOST_CONNECTION}`;
return conn;
}