Skip to content

Commit cf509be

Browse files
committed
fix(web): satisfy lint for terminal persistence
- Split web API and ready-state modules below lint limits. - Preserve typed terminal workspace errors without catchAll discard. - Reduce task decoder and task panel complexity. SOURCE: n/a
1 parent c3c069d commit cf509be

20 files changed

Lines changed: 1017 additions & 728 deletions

packages/app/src/docker-git/api-container-tasks-codec.ts

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,49 +35,79 @@ const isTaskKind = (value: string): value is ApiContainerTaskKind =>
3535
value === "background" ||
3636
value === "system"
3737

38+
type DecodedContainerTaskFields = {
39+
readonly command: string | null
40+
readonly etime: string | null
41+
readonly etimes: number | null
42+
readonly kind: string | null
43+
readonly logAvailable: boolean | null
44+
readonly managedId: string | undefined
45+
readonly pid: number | null
46+
readonly ppid: number | null
47+
readonly tty: string | null
48+
readonly user: string | null
49+
}
50+
51+
const readContainerTaskFields = (object: NonNullable<ReturnType<typeof asObject>>): DecodedContainerTaskFields => ({
52+
command: asString(object["command"]),
53+
etime: asString(object["etime"]),
54+
etimes: readNumber(object["etimes"]),
55+
kind: asString(object["kind"]),
56+
logAvailable: readBoolean(object["logAvailable"]),
57+
managedId: asString(object["managedId"]) ?? undefined,
58+
pid: readNumber(object["pid"]),
59+
ppid: readNumber(object["ppid"]),
60+
tty: asString(object["tty"]),
61+
user: asString(object["user"])
62+
})
63+
64+
const hasCompleteContainerTaskFields = (
65+
fields: DecodedContainerTaskFields
66+
): fields is DecodedContainerTaskFields & {
67+
readonly command: string
68+
readonly etime: string
69+
readonly etimes: number
70+
readonly kind: ApiContainerTaskKind
71+
readonly logAvailable: boolean
72+
readonly pid: number
73+
readonly ppid: number
74+
readonly tty: string
75+
readonly user: string
76+
} =>
77+
[
78+
fields.command,
79+
fields.etime,
80+
fields.etimes,
81+
fields.logAvailable,
82+
fields.pid,
83+
fields.ppid,
84+
fields.tty,
85+
fields.user
86+
].every((field) => field !== null) &&
87+
fields.kind !== null &&
88+
isTaskKind(fields.kind)
89+
3890
const decodeContainerTask = (value: JsonValue): ApiContainerTask | null => {
3991
const object = asObject(value)
4092
if (object === null) {
4193
return null
4294
}
43-
44-
const pid = readNumber(object["pid"])
45-
const ppid = readNumber(object["ppid"])
46-
const user = asString(object["user"])
47-
const tty = asString(object["tty"])
48-
const etime = asString(object["etime"])
49-
const etimes = readNumber(object["etimes"])
50-
const command = asString(object["command"])
51-
const kind = asString(object["kind"])
52-
const managedId = asString(object["managedId"]) ?? undefined
53-
const logAvailable = readBoolean(object["logAvailable"])
54-
55-
if (
56-
pid === null ||
57-
ppid === null ||
58-
user === null ||
59-
tty === null ||
60-
etime === null ||
61-
etimes === null ||
62-
command === null ||
63-
kind === null ||
64-
!isTaskKind(kind) ||
65-
logAvailable === null
66-
) {
95+
const fields = readContainerTaskFields(object)
96+
if (!hasCompleteContainerTaskFields(fields)) {
6797
return null
6898
}
6999

70100
return {
71-
pid,
72-
ppid,
73-
user,
74-
tty,
75-
etime,
76-
etimes,
77-
command,
78-
kind,
79-
managedId,
80-
logAvailable
101+
command: fields.command,
102+
etime: fields.etime,
103+
etimes: fields.etimes,
104+
kind: fields.kind,
105+
logAvailable: fields.logAvailable,
106+
managedId: fields.managedId,
107+
pid: fields.pid,
108+
ppid: fields.ppid,
109+
tty: fields.tty,
110+
user: fields.user
81111
}
82112
}
83113

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import type { Effect } from "effect"
2+
13
import { type BrowserActionContext, requireSelectedProjectId, withBusy } from "./actions-shared.js"
24
import { loadProjectTaskLogs, loadProjectTasks, stopProjectTask } from "./api.js"
5+
import type { ContainerTaskSnapshot } from "./api.js"
36

47
const requireProjectIdForTasks = (context: BrowserActionContext): string | null => {
58
const projectId = requireSelectedProjectId(context)
@@ -10,6 +13,80 @@ const requireProjectIdForTasks = (context: BrowserActionContext): string | null
1013
return projectId
1114
}
1215

16+
type SelectedProjectTaskAction = {
17+
readonly context: BrowserActionContext
18+
readonly pid: number
19+
readonly projectId: string
20+
}
21+
22+
type SelectedProjectTaskBusyAction<A> = {
23+
readonly context: BrowserActionContext
24+
readonly effect: (selected: SelectedProjectTaskAction) => Effect.Effect<A, string>
25+
readonly label: string
26+
readonly onSuccess: (selected: SelectedProjectTaskAction, value: A) => void
27+
readonly pid: number
28+
}
29+
30+
const withSelectedProjectTask = (
31+
context: BrowserActionContext,
32+
pid: number,
33+
action: (selected: SelectedProjectTaskAction) => void
34+
): void => {
35+
const projectId = requireProjectIdForTasks(context)
36+
if (projectId !== null) {
37+
action({ context, pid, projectId })
38+
}
39+
}
40+
41+
const withSelectedProjectTaskBusy = <A>(
42+
{ context, effect, label, onSuccess, pid }: SelectedProjectTaskBusyAction<A>
43+
): void => {
44+
withSelectedProjectTask(context, pid, (selected) => {
45+
withBusy({
46+
context: selected.context,
47+
effect: effect(selected),
48+
label,
49+
onSuccess: (value) => {
50+
onSuccess(selected, value)
51+
}
52+
})
53+
})
54+
}
55+
56+
const removeTaskFromSnapshot = (
57+
snapshot: ContainerTaskSnapshot | null,
58+
pid: number
59+
): ContainerTaskSnapshot | null =>
60+
snapshot === null
61+
? null
62+
: {
63+
...snapshot,
64+
tasks: snapshot.tasks.filter((task) => task.pid !== pid)
65+
}
66+
67+
const stopSelectedProjectTaskEffect = (
68+
selected: SelectedProjectTaskAction
69+
): Effect.Effect<void, string> => stopProjectTask(selected.projectId, selected.pid)
70+
71+
const loadSelectedProjectTaskLogsEffect = (
72+
selected: SelectedProjectTaskAction
73+
): Effect.Effect<string, string> => loadProjectTaskLogs(selected.projectId, selected.pid, 200)
74+
75+
const applyStoppedProjectTask = (
76+
selected: SelectedProjectTaskAction
77+
): void => {
78+
selected.context.setProjectTasks((snapshot) => removeTaskFromSnapshot(snapshot, selected.pid))
79+
selected.context.setMessage(`Sent SIGTERM to PID ${selected.pid}.`)
80+
}
81+
82+
const applyLoadedProjectTaskLogs = (
83+
selected: SelectedProjectTaskAction,
84+
output: string
85+
): void => {
86+
selected.context.setProjectTaskLogs(output)
87+
selected.context.setMessage(`Loaded logs for PID ${selected.pid}.`)
88+
}
89+
1390
export const loadSelectedProjectTasks = (
1491
context: BrowserActionContext,
1592
options?: { readonly silent?: boolean }
@@ -35,43 +112,24 @@ export const stopSelectedProjectTask = (
35112
context: BrowserActionContext,
36113
pid: number
37114
) => {
38-
const projectId = requireProjectIdForTasks(context)
39-
if (projectId === null) {
40-
return
41-
}
42-
withBusy({
115+
withSelectedProjectTaskBusy({
43116
context,
44-
effect: stopProjectTask(projectId, pid),
117+
effect: stopSelectedProjectTaskEffect,
45118
label: "Stopping container task",
46-
onSuccess: () => {
47-
context.setProjectTasks((snapshot) =>
48-
snapshot === null
49-
? null
50-
: {
51-
...snapshot,
52-
tasks: snapshot.tasks.filter((task) => task.pid !== pid)
53-
}
54-
)
55-
context.setMessage(`Sent SIGTERM to PID ${pid}.`)
56-
}
119+
onSuccess: applyStoppedProjectTask,
120+
pid
57121
})
58122
}
59123

60124
export const loadSelectedProjectTaskLogs = (
61125
context: BrowserActionContext,
62126
pid: number
63127
) => {
64-
const projectId = requireProjectIdForTasks(context)
65-
if (projectId === null) {
66-
return
67-
}
68-
withBusy({
128+
withSelectedProjectTaskBusy({
69129
context,
70-
effect: loadProjectTaskLogs(projectId, pid, 200),
130+
effect: loadSelectedProjectTaskLogsEffect,
71131
label: "Loading task logs",
72-
onSuccess: (output) => {
73-
context.setProjectTaskLogs(output)
74-
context.setMessage(`Loaded logs for PID ${pid}.`)
75-
}
132+
onSuccess: applyLoadedProjectTaskLogs,
133+
pid
76134
})
77135
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Effect } from "effect"
2+
3+
import { requestJson, requestText } from "./api-http.js"
4+
import {
5+
ProjectDatabaseForwardResponseSchema,
6+
ProjectDatabaseForwardsResponseSchema,
7+
ProjectDatabaseProfileResponseSchema,
8+
ProjectDatabaseProfilesResponseSchema,
9+
ProjectDatabaseSessionResponseSchema
10+
} from "./api-schema.js"
11+
import type { ProjectDatabaseForward, ProjectDatabaseSession } from "./api-schema.js"
12+
13+
export const projectDatabaseEditorUrl = (session: ProjectDatabaseSession): string => session.editorPath
14+
15+
export const projectDatabaseExternalUrl = (forward: ProjectDatabaseForward): string =>
16+
`${forward.publicHost}:${forward.hostPort}`
17+
18+
export const loadProjectDatabaseProfiles = (projectId: string) =>
19+
requestJson(
20+
"GET",
21+
`/projects/${encodeURIComponent(projectId)}/databases/profiles`,
22+
ProjectDatabaseProfilesResponseSchema
23+
).pipe(
24+
Effect.map((response) => response.profiles)
25+
)
26+
27+
export const loadProjectDatabaseForwards = (projectId: string) =>
28+
requestJson(
29+
"GET",
30+
`/projects/${encodeURIComponent(projectId)}/databases/forwards`,
31+
ProjectDatabaseForwardsResponseSchema
32+
).pipe(
33+
Effect.map((response) => response.forwards)
34+
)
35+
36+
export const saveProjectDatabaseProfile = (
37+
projectId: string,
38+
connectionString: string,
39+
label: string | null
40+
) =>
41+
requestJson(
42+
"POST",
43+
`/projects/${encodeURIComponent(projectId)}/databases/profiles`,
44+
ProjectDatabaseProfileResponseSchema,
45+
{ connectionString, label }
46+
).pipe(
47+
Effect.map((response) => response.profile)
48+
)
49+
50+
export const deleteProjectDatabaseProfile = (
51+
projectId: string,
52+
profileId: string
53+
) =>
54+
requestText(
55+
"DELETE",
56+
`/projects/${encodeURIComponent(projectId)}/databases/profiles/${encodeURIComponent(profileId)}`
57+
).pipe(Effect.asVoid)
58+
59+
export const exposeProjectDatabaseProfile = (
60+
projectId: string,
61+
profileId: string
62+
) =>
63+
requestJson(
64+
"POST",
65+
`/projects/${encodeURIComponent(projectId)}/databases/profiles/${encodeURIComponent(profileId)}/expose`,
66+
ProjectDatabaseForwardResponseSchema
67+
).pipe(
68+
Effect.map((response) => response.forward)
69+
)
70+
71+
export const deleteProjectDatabaseForward = (
72+
projectId: string,
73+
profileId: string
74+
) =>
75+
requestText(
76+
"DELETE",
77+
`/projects/${encodeURIComponent(projectId)}/databases/profiles/${encodeURIComponent(profileId)}/expose`
78+
).pipe(Effect.asVoid)
79+
80+
export const loadProjectDatabaseSession = (projectId: string) =>
81+
requestJson(
82+
"GET",
83+
`/projects/${encodeURIComponent(projectId)}/databases/session`,
84+
ProjectDatabaseSessionResponseSchema
85+
).pipe(
86+
Effect.map((response) => response.session)
87+
)
88+
89+
export const openProjectDatabaseEditor = (projectId: string) =>
90+
requestJson(
91+
"POST",
92+
`/projects/${encodeURIComponent(projectId)}/databases/open`,
93+
ProjectDatabaseSessionResponseSchema
94+
).pipe(
95+
Effect.map((response) => response.session)
96+
)
97+
98+
export const restartProjectDatabaseEditor = (projectId: string) =>
99+
requestJson(
100+
"POST",
101+
`/projects/${encodeURIComponent(projectId)}/databases/restart`,
102+
ProjectDatabaseSessionResponseSchema
103+
).pipe(
104+
Effect.map((response) => response.session)
105+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Schema from "@effect/schema/Schema"
2+
3+
import { JsonValueSchema } from "../shared/json-schema.js"
4+
5+
export const ApiEventSchema = Schema.Struct({
6+
seq: Schema.Number,
7+
projectId: Schema.String,
8+
type: Schema.Union(
9+
Schema.Literal("snapshot"),
10+
Schema.Literal("project.created"),
11+
Schema.Literal("project.deleted"),
12+
Schema.Literal("project.deployment.status"),
13+
Schema.Literal("project.deployment.log"),
14+
Schema.Literal("project.ssh.session"),
15+
Schema.Literal("agent.started"),
16+
Schema.Literal("agent.output"),
17+
Schema.Literal("agent.exited"),
18+
Schema.Literal("agent.stopped"),
19+
Schema.Literal("agent.error")
20+
),
21+
at: Schema.String,
22+
payload: JsonValueSchema
23+
})
24+
25+
export const ProjectEventsPollResponseSchema = Schema.Struct({
26+
cursor: Schema.Number,
27+
events: Schema.Array(ApiEventSchema)
28+
})

0 commit comments

Comments
 (0)