Skip to content

Commit 118dea9

Browse files
committed
♻️ extract session management logic into plugin/sessionManager.ts
1 parent 74bbef3 commit 118dea9

3 files changed

Lines changed: 126 additions & 106 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"components",
2727
"composables",
2828
"!composables/__tests__",
29+
"plugin",
2930
"setup",
3031
"styles",
3132
"types.ts",

plugin/sessionManager.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2+
import { tmpdir } from 'node:os'
3+
import { join, resolve } from 'node:path'
4+
5+
import { resolvePort } from '../composables/resolvePort'
6+
import type { StartRequest } from '../types'
7+
8+
const COLOR_THEMES = {
9+
dark: 'Default Dark Modern',
10+
light: 'Default Light Modern',
11+
} as const
12+
13+
const DEFAULT_PORT = 9000
14+
const DEFAULT_START_TIMEOUT_MS = 30_000
15+
16+
type SessionEntry = {
17+
close: () => Promise<void>
18+
port: number
19+
url: string
20+
userDataDir?: string
21+
}
22+
23+
type SendEvent = (session: string, url: string, state: 'running' | 'error', error?: string) => void
24+
25+
export class SessionManager {
26+
private sessions = new Map<string, SessionEntry>()
27+
private usedPorts = new Set<number>()
28+
29+
async start(request: StartRequest, root: string, send: SendEvent): Promise<void> {
30+
const {
31+
colorScheme,
32+
defaultFolder,
33+
defaultPort = DEFAULT_PORT,
34+
fontSize,
35+
hideActivityBar,
36+
hideMinimap,
37+
hideStatusBar,
38+
port: requestedPort,
39+
session,
40+
startTimeout = DEFAULT_START_TIMEOUT_MS,
41+
} = request
42+
43+
const existing = this.sessions.get(session)
44+
if (existing) {
45+
send(session, existing.url, 'running')
46+
return
47+
}
48+
49+
const port = resolvePort(this.usedPorts, requestedPort, defaultPort)
50+
this.usedPorts.add(port)
51+
52+
try {
53+
const { startCodeServer } = await import('coderaft')
54+
55+
const absoluteFolder = defaultFolder ? resolve(root, defaultFolder) : root
56+
const resolvedFolder = existsSync(absoluteFolder) ? absoluteFolder : root
57+
58+
const settings: Record<string, unknown> = {}
59+
if (colorScheme) settings['workbench.colorTheme'] = COLOR_THEMES[colorScheme]
60+
if (fontSize) settings['editor.fontSize'] = fontSize
61+
if (hideMinimap) settings['editor.minimap.enabled'] = false
62+
if (hideActivityBar) settings['workbench.activityBar.location'] = 'hidden'
63+
if (hideStatusBar) settings['workbench.statusBar.visible'] = false
64+
65+
let userDataDir: string | undefined
66+
if (Object.keys(settings).length > 0) {
67+
userDataDir = mkdtempSync(join(tmpdir(), 'livecode-'))
68+
mkdirSync(join(userDataDir, 'User'), { recursive: true })
69+
writeFileSync(join(userDataDir, 'User', 'settings.json'), JSON.stringify(settings))
70+
}
71+
72+
const handle = await Promise.race([
73+
startCodeServer({
74+
defaultFolder: resolvedFolder,
75+
host: '127.0.0.1',
76+
port,
77+
...(userDataDir ? { vscode: { 'user-data-dir': userDataDir } } : {}),
78+
}),
79+
new Promise<never>((_, reject) =>
80+
setTimeout(() => reject(new Error(`timeout after ${startTimeout}ms`)), startTimeout),
81+
),
82+
])
83+
84+
this.sessions.set(session, {
85+
close: () => handle.close(),
86+
port,
87+
url: handle.url,
88+
userDataDir,
89+
})
90+
console.log(`[livecode] Session "${session}" running at ${handle.url}`)
91+
send(session, handle.url, 'running')
92+
} catch (err) {
93+
this.usedPorts.delete(port)
94+
const message = err instanceof Error ? err.message : String(err)
95+
console.error(`[livecode] Failed to start session "${session}": ${message}`)
96+
send(session, '', 'error', message)
97+
}
98+
}
99+
100+
stop(session: string): void {
101+
const entry = this.sessions.get(session)
102+
if (!entry) return
103+
entry.close().catch(() => {})
104+
if (entry.userDataDir) rmSync(entry.userDataDir, { recursive: true, force: true })
105+
this.sessions.delete(session)
106+
this.usedPorts.delete(entry.port)
107+
console.log(`[livecode] Session "${session}" stopped`)
108+
}
109+
110+
cleanup(): void {
111+
for (const [, entry] of this.sessions) {
112+
entry.close().catch(() => {})
113+
if (entry.userDataDir) rmSync(entry.userDataDir, { recursive: true, force: true })
114+
}
115+
this.sessions.clear()
116+
this.usedPorts.clear()
117+
}
118+
}

vite.config.ts

Lines changed: 7 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2-
import { tmpdir } from 'node:os'
3-
import { join, resolve } from 'node:path'
4-
51
import { defineConfig } from 'vite'
62

73
import { resolvePort } from './composables/resolvePort'
4+
import { SessionManager } from './plugin/sessionManager'
85
import type { StartedEvent, StartRequest } from './types'
96

107
export { resolvePort }
118

12-
const DEFAULT_PORT = 9000
13-
const DEFAULT_START_TIMEOUT_MS = 30_000
14-
159
let previousSigintHandler: (() => void) | null = null
1610
let previousSigtermHandler: (() => void) | null = null
1711
let previousExitHandler: (() => void) | null = null
@@ -22,19 +16,9 @@ export default defineConfig({
2216
name: 'slidev-addon-livecode',
2317

2418
configureServer(server) {
25-
const root = server.config.root
26-
const COLOR_THEMES = {
27-
dark: 'Default Dark Modern',
28-
light: 'Default Light Modern',
29-
} as const
30-
31-
const sessions = new Map<
32-
string,
33-
{ close: () => Promise<void>; port: number; url: string; userDataDir?: string }
34-
>()
35-
const usedPorts = new Set<number>()
19+
const manager = new SessionManager()
3620

37-
const sendStartedEvent = (
21+
const send = (
3822
session: string,
3923
url: string,
4024
state: 'running' | 'error',
@@ -43,98 +27,15 @@ export default defineConfig({
4327
server.ws.send('livecode:started', { error, session, state, url } satisfies StartedEvent)
4428
}
4529

46-
server.ws.on('livecode:start', async (request: StartRequest) => {
47-
const {
48-
colorScheme,
49-
defaultFolder,
50-
defaultPort = DEFAULT_PORT,
51-
fontSize,
52-
hideActivityBar,
53-
hideMinimap,
54-
hideStatusBar,
55-
port: requestedPort,
56-
session,
57-
startTimeout = DEFAULT_START_TIMEOUT_MS,
58-
} = request
59-
60-
const existing = sessions.get(session)
61-
if (existing) {
62-
sendStartedEvent(session, existing.url, 'running')
63-
return
64-
}
65-
66-
const port = resolvePort(usedPorts, requestedPort, defaultPort)
67-
usedPorts.add(port)
68-
69-
try {
70-
const { startCodeServer } = await import('coderaft')
71-
72-
const absoluteFolder = defaultFolder ? resolve(root, defaultFolder) : root
73-
const resolvedFolder = existsSync(absoluteFolder) ? absoluteFolder : root
74-
75-
const settings: Record<string, unknown> = {}
76-
if (colorScheme) settings['workbench.colorTheme'] = COLOR_THEMES[colorScheme]
77-
if (fontSize) settings['editor.fontSize'] = fontSize
78-
if (hideMinimap) settings['editor.minimap.enabled'] = false
79-
if (hideActivityBar) settings['workbench.activityBar.location'] = 'hidden'
80-
if (hideStatusBar) settings['workbench.statusBar.visible'] = false
81-
82-
let userDataDir: string | undefined
83-
if (Object.keys(settings).length > 0) {
84-
userDataDir = mkdtempSync(join(tmpdir(), 'livecode-'))
85-
mkdirSync(join(userDataDir, 'User'), { recursive: true })
86-
writeFileSync(join(userDataDir, 'User', 'settings.json'), JSON.stringify(settings))
87-
}
88-
89-
const handle = await Promise.race([
90-
startCodeServer({
91-
defaultFolder: resolvedFolder,
92-
host: '127.0.0.1',
93-
port,
94-
...(userDataDir ? { vscode: { 'user-data-dir': userDataDir } } : {}),
95-
}),
96-
new Promise<never>((_, reject) =>
97-
setTimeout(
98-
() => reject(new Error(`timeout after ${startTimeout}ms`)),
99-
startTimeout,
100-
),
101-
),
102-
])
103-
104-
sessions.set(session, {
105-
close: () => handle.close(),
106-
port,
107-
url: handle.url,
108-
userDataDir,
109-
})
110-
console.log(`[livecode] Session "${session}" running at ${handle.url}`)
111-
sendStartedEvent(session, handle.url, 'running')
112-
} catch (err) {
113-
usedPorts.delete(port)
114-
const message = err instanceof Error ? err.message : String(err)
115-
console.error(`[livecode] Failed to start session "${session}": ${message}`)
116-
sendStartedEvent(session, '', 'error', message)
117-
}
30+
server.ws.on('livecode:start', (request: StartRequest) => {
31+
manager.start(request, server.config.root, send)
11832
})
11933

12034
server.ws.on('livecode:stop', ({ session }: { session: string }) => {
121-
const entry = sessions.get(session)
122-
if (!entry) return
123-
entry.close().catch(() => {})
124-
if (entry.userDataDir) rmSync(entry.userDataDir, { recursive: true, force: true })
125-
sessions.delete(session)
126-
usedPorts.delete(entry.port)
127-
console.log(`[livecode] Session "${session}" stopped`)
35+
manager.stop(session)
12836
})
12937

130-
const cleanup = () => {
131-
for (const [, entry] of sessions) {
132-
entry.close().catch(() => {})
133-
if (entry.userDataDir) rmSync(entry.userDataDir, { recursive: true, force: true })
134-
}
135-
sessions.clear()
136-
usedPorts.clear()
137-
}
38+
const cleanup = () => manager.cleanup()
13839

13940
const sigintHandler = () => {
14041
cleanup()

0 commit comments

Comments
 (0)