@gachlab/devup 0.9.2 → 0.10.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.
- package/CHANGELOG.md +11 -0
- package/dist/control-plane/socket-server.d.ts +23 -0
- package/dist/control-plane/socket-server.d.ts.map +1 -1
- package/dist/index.js +105 -11
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/daemon.d.ts.map +1 -1
- package/dist/process/port-conflicts.d.ts +5 -3
- package/dist/process/port-conflicts.d.ts.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/hooks/useControlPlane.d.ts +6 -1
- package/dist/tui/hooks/useControlPlane.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to `@gachlab/devup` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.3] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
Critical hotfix for `devup down` against the VS Code extension.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`devup down` no longer gets SIGKILL'd because the control-plane socket hangs on streaming clients.** The control-plane server's `close()` awaited every client to disconnect on its own, but long-lived streaming subscriptions (`status.follow`, `logs.follow` — exactly what the VS Code extension uses) never close until the daemon tells them to. Result: `devup down` waited the full 10 s grace, then SIGKILL'd the daemon. SIGKILL skips the cleanup handler → all spawned services orphaned to init, ports left busy, next `devup up -d` hits EADDRINUSE on every port. Fix: track every active client socket and `destroy()` them before awaiting `server.close()`. Clean shutdowns now complete in milliseconds even with the extension connected.
|
|
14
|
+
- **Pre-boot port-conflict scan now covers web services too.** They were skipped on the assumption that dev servers handle retry themselves, but in daemon mode the user wants devup to own the configured ports — same as APIs. If a web port is held by a stray Vite/ng-serve from a previous run, the scan now flags it and offers to take it over.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- New unit test asserts `socket.close()` completes in under 2 s with an active `logs.follow` subscription.
|
|
18
|
+
|
|
8
19
|
## [0.9.2] — 2026-05-22
|
|
9
20
|
|
|
10
21
|
Critical hotfix. **All 0.9.x users should upgrade immediately.**
|
|
@@ -9,6 +9,25 @@ import type { ProcessState } from '../process/types.js';
|
|
|
9
9
|
* to the socket file already has the same uid as the devup process — no
|
|
10
10
|
* additional auth needed. Strictly local; TCP exposure is intentionally
|
|
11
11
|
* out of scope. */
|
|
12
|
+
export interface ServiceStatEntry {
|
|
13
|
+
cpu: number;
|
|
14
|
+
memMB: number;
|
|
15
|
+
}
|
|
16
|
+
export interface StatsResult {
|
|
17
|
+
services: Record<string, ServiceStatEntry>;
|
|
18
|
+
system: {
|
|
19
|
+
totalMemMB: number;
|
|
20
|
+
freeMemMB: number;
|
|
21
|
+
cpuCores: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface ProxyInfo {
|
|
25
|
+
active: boolean;
|
|
26
|
+
provider: string;
|
|
27
|
+
domain: string;
|
|
28
|
+
tls: boolean;
|
|
29
|
+
routes: Record<string, string>;
|
|
30
|
+
}
|
|
12
31
|
export interface RpcContext {
|
|
13
32
|
/** State of every service (read-only snapshot). */
|
|
14
33
|
states(): Map<string, ProcessState>;
|
|
@@ -23,6 +42,10 @@ export interface RpcContext {
|
|
|
23
42
|
watchLogs(svcName: string | null, onLine: (svc: string, line: string) => void): () => void;
|
|
24
43
|
/** Subscribe to service-state changes. Returns an unsubscribe function. */
|
|
25
44
|
watchStatus(onUpdate: (name: string, state: ProcessState) => void): () => void;
|
|
45
|
+
/** Per-service CPU/mem stats + system totals. */
|
|
46
|
+
getStats(): Promise<StatsResult>;
|
|
47
|
+
/** Active proxy configuration, or null when no proxy is running. */
|
|
48
|
+
getProxyInfo(): ProxyInfo | null;
|
|
26
49
|
}
|
|
27
50
|
export interface SocketServerHandle {
|
|
28
51
|
server: Server;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../src/control-plane/socket-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAMlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;oBAQoB;AAEpB,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,iCAAiC;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5D;2CACuC;IACvC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3F,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../src/control-plane/socket-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAMlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;oBAQoB;AAEpB,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,iCAAiC;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5D;2CACuC;IACvC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3F,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC/E,iDAAiD;IACjD,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACjC,oEAAoE;IACpE,YAAY,IAAI,SAAS,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,CAAC,CA+C7B"}
|
package/dist/index.js
CHANGED
|
@@ -720,7 +720,12 @@ async function startSocketServer(projectName, ctx, opts = {}) {
|
|
|
720
720
|
} catch {
|
|
721
721
|
}
|
|
722
722
|
}
|
|
723
|
-
const
|
|
723
|
+
const activeClients = /* @__PURE__ */ new Set();
|
|
724
|
+
const server = createServer((socket) => {
|
|
725
|
+
activeClients.add(socket);
|
|
726
|
+
socket.once("close", () => activeClients.delete(socket));
|
|
727
|
+
handleClient(socket, ctx);
|
|
728
|
+
});
|
|
724
729
|
await new Promise((resolve4, reject) => {
|
|
725
730
|
server.once("error", reject);
|
|
726
731
|
server.listen(path, () => {
|
|
@@ -737,6 +742,8 @@ async function startSocketServer(projectName, ctx, opts = {}) {
|
|
|
737
742
|
server,
|
|
738
743
|
path,
|
|
739
744
|
async close() {
|
|
745
|
+
for (const sock of activeClients) sock.destroy();
|
|
746
|
+
activeClients.clear();
|
|
740
747
|
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
741
748
|
if (existsSync5(path)) {
|
|
742
749
|
try {
|
|
@@ -840,8 +847,10 @@ async function dispatch(method, params, ctx) {
|
|
|
840
847
|
for (const [name, st] of ctx.states()) {
|
|
841
848
|
out.push(serializeState(name, st));
|
|
842
849
|
}
|
|
843
|
-
return { services: out };
|
|
850
|
+
return { services: out, proxy: ctx.getProxyInfo() };
|
|
844
851
|
}
|
|
852
|
+
case "stats":
|
|
853
|
+
return await ctx.getStats();
|
|
845
854
|
case "restart": {
|
|
846
855
|
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
847
856
|
await ctx.restart(svc);
|
|
@@ -944,7 +953,7 @@ function openStream(socketPath, method, params, onFrame, onError) {
|
|
|
944
953
|
import { spawn as spawn4 } from "child_process";
|
|
945
954
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync10, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, createReadStream } from "fs";
|
|
946
955
|
import { join as join8 } from "path";
|
|
947
|
-
import { homedir as homedir3 } from "os";
|
|
956
|
+
import { homedir as homedir3, totalmem, freemem, cpus } from "os";
|
|
948
957
|
import { setTimeout as sleep } from "timers/promises";
|
|
949
958
|
import { createInterface as createInterface3 } from "readline";
|
|
950
959
|
|
|
@@ -1867,6 +1876,7 @@ async function daemonBody(opts) {
|
|
|
1867
1876
|
const logBus = new Broadcaster();
|
|
1868
1877
|
const stateBus = new Broadcaster();
|
|
1869
1878
|
const lazyProxies = /* @__PURE__ */ new Map();
|
|
1879
|
+
const prevCpuMap = /* @__PURE__ */ new Map();
|
|
1870
1880
|
let externals = [];
|
|
1871
1881
|
let socket = null;
|
|
1872
1882
|
let healthTimer = null;
|
|
@@ -1976,7 +1986,48 @@ async function daemonBody(opts) {
|
|
|
1976
1986
|
watchLogs: (svcName, onLine) => logBus.subscribe(({ svc, text }) => {
|
|
1977
1987
|
if (svcName === null || svc === svcName) onLine(svc, text);
|
|
1978
1988
|
}),
|
|
1979
|
-
watchStatus: (onUpdate) => stateBus.subscribe(({ name, state }) => onUpdate(name, state))
|
|
1989
|
+
watchStatus: (onUpdate) => stateBus.subscribe(({ name, state }) => onUpdate(name, state)),
|
|
1990
|
+
async getStats() {
|
|
1991
|
+
const pids = [];
|
|
1992
|
+
const pidToName = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const [name, st] of mgr.state) {
|
|
1994
|
+
if (st.pid) {
|
|
1995
|
+
pids.push(st.pid);
|
|
1996
|
+
pidToName.set(st.pid, name);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const raw = pids.length ? await platform.getProcessStats(pids) : /* @__PURE__ */ new Map();
|
|
2000
|
+
const services2 = {};
|
|
2001
|
+
for (const [name] of mgr.state) {
|
|
2002
|
+
services2[name] = { cpu: 0, memMB: 0 };
|
|
2003
|
+
}
|
|
2004
|
+
for (const [pid, data] of raw) {
|
|
2005
|
+
const name = pidToName.get(pid);
|
|
2006
|
+
if (!name) continue;
|
|
2007
|
+
const prev = prevCpuMap.get(name) ?? { time: Date.now(), cpu: 0 };
|
|
2008
|
+
const cpu = calcCpuPercent(data.cpuSeconds, prev.cpu, prev.time);
|
|
2009
|
+
prevCpuMap.set(name, { time: Date.now(), cpu: data.cpuSeconds });
|
|
2010
|
+
services2[name] = { cpu: Math.round(cpu * 10) / 10, memMB: Math.round(data.rss / 1024 * 10) / 10 };
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
services: services2,
|
|
2014
|
+
system: {
|
|
2015
|
+
totalMemMB: Math.round(totalmem() / 1024 / 1024),
|
|
2016
|
+
freeMemMB: Math.round(freemem() / 1024 / 1024),
|
|
2017
|
+
cpuCores: cpus().length
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
},
|
|
2021
|
+
getProxyInfo() {
|
|
2022
|
+
if (!proxyProvider || !proxyOpts || !cliArgs.proxy) return null;
|
|
2023
|
+
return {
|
|
2024
|
+
active: true,
|
|
2025
|
+
provider: proxyProvider.name,
|
|
2026
|
+
domain: proxyOpts.domain,
|
|
2027
|
+
tls: proxyOpts.tls,
|
|
2028
|
+
routes: proxyOpts.routes
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
1980
2031
|
}, { onLog: (msg) => writeDevupLog(msg) });
|
|
1981
2032
|
healthTimer = setInterval(() => {
|
|
1982
2033
|
void mgr.checkAllHealth();
|
|
@@ -2583,9 +2634,8 @@ function parseLsof(stdout) {
|
|
|
2583
2634
|
return null;
|
|
2584
2635
|
}
|
|
2585
2636
|
async function scanPortConflicts(services) {
|
|
2586
|
-
const apis = services.filter((s) => s.type === "api");
|
|
2587
2637
|
const conflicts = [];
|
|
2588
|
-
for (const svc of
|
|
2638
|
+
for (const svc of services) {
|
|
2589
2639
|
const bindable = await isPortBindable(svc.port);
|
|
2590
2640
|
if (bindable) continue;
|
|
2591
2641
|
const holder = await findPortHolder(svc.port);
|
|
@@ -3096,8 +3146,10 @@ function useLogsPause(setPaused, logsPaused, logsScrollOffset) {
|
|
|
3096
3146
|
import { useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
3097
3147
|
import { createInterface as createInterface5 } from "readline";
|
|
3098
3148
|
import { createReadStream as createReadStream3, existsSync as existsSync15 } from "fs";
|
|
3099
|
-
|
|
3149
|
+
import { totalmem as totalmem2, freemem as freemem2, cpus as cpus2 } from "os";
|
|
3150
|
+
function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy) {
|
|
3100
3151
|
const handleRef = useRef3(null);
|
|
3152
|
+
const prevCpuMap = useRef3(/* @__PURE__ */ new Map());
|
|
3101
3153
|
useEffect5(() => {
|
|
3102
3154
|
if (!manager) return;
|
|
3103
3155
|
let handle = null;
|
|
@@ -3129,6 +3181,47 @@ function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBu
|
|
|
3129
3181
|
},
|
|
3130
3182
|
watchStatus: (onUpdate) => {
|
|
3131
3183
|
return stateBus.subscribe(({ name, state }) => onUpdate(name, state));
|
|
3184
|
+
},
|
|
3185
|
+
async getStats() {
|
|
3186
|
+
const pids = [];
|
|
3187
|
+
const pidToName = /* @__PURE__ */ new Map();
|
|
3188
|
+
for (const [name, st] of manager.state) {
|
|
3189
|
+
if (st.pid) {
|
|
3190
|
+
pids.push(st.pid);
|
|
3191
|
+
pidToName.set(st.pid, name);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
const raw = pids.length ? await platform.getProcessStats(pids) : /* @__PURE__ */ new Map();
|
|
3195
|
+
const services = {};
|
|
3196
|
+
for (const [name] of manager.state) {
|
|
3197
|
+
services[name] = { cpu: 0, memMB: 0 };
|
|
3198
|
+
}
|
|
3199
|
+
for (const [pid, data] of raw) {
|
|
3200
|
+
const name = pidToName.get(pid);
|
|
3201
|
+
if (!name) continue;
|
|
3202
|
+
const prev = prevCpuMap.current.get(name) ?? { time: Date.now(), cpu: 0 };
|
|
3203
|
+
const cpu = calcCpuPercent(data.cpuSeconds, prev.cpu, prev.time);
|
|
3204
|
+
prevCpuMap.current.set(name, { time: Date.now(), cpu: data.cpuSeconds });
|
|
3205
|
+
services[name] = { cpu: Math.round(cpu * 10) / 10, memMB: Math.round(data.rss / 1024 * 10) / 10 };
|
|
3206
|
+
}
|
|
3207
|
+
return {
|
|
3208
|
+
services,
|
|
3209
|
+
system: {
|
|
3210
|
+
totalMemMB: Math.round(totalmem2() / 1024 / 1024),
|
|
3211
|
+
freeMemMB: Math.round(freemem2() / 1024 / 1024),
|
|
3212
|
+
cpuCores: cpus2().length
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
},
|
|
3216
|
+
getProxyInfo() {
|
|
3217
|
+
if (!proxy) return null;
|
|
3218
|
+
return {
|
|
3219
|
+
active: true,
|
|
3220
|
+
provider: proxy.provider.name,
|
|
3221
|
+
domain: proxy.opts.domain,
|
|
3222
|
+
tls: proxy.opts.tls,
|
|
3223
|
+
routes: proxy.opts.routes
|
|
3224
|
+
};
|
|
3132
3225
|
}
|
|
3133
3226
|
}, { onLog: (msg) => pushLog("devup", msg, 12) });
|
|
3134
3227
|
handleRef.current = handle;
|
|
@@ -3140,7 +3233,7 @@ function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBu
|
|
|
3140
3233
|
void handle?.close();
|
|
3141
3234
|
handleRef.current = null;
|
|
3142
3235
|
};
|
|
3143
|
-
}, [manager, projectName, logSink, pushLog, logBus, stateBus]);
|
|
3236
|
+
}, [manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy]);
|
|
3144
3237
|
return handleRef;
|
|
3145
3238
|
}
|
|
3146
3239
|
|
|
@@ -3283,7 +3376,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
3283
3376
|
const statsObj = Object.fromEntries([...stats].map(([k, v]) => [k, v]));
|
|
3284
3377
|
const apis = sortServiceNames(names.filter((n) => states.get(n).svc.type === "api"), sortMode, statsObj, stObj);
|
|
3285
3378
|
const webs = sortServiceNames(names.filter((n) => states.get(n).svc.type === "web"), sortMode, statsObj, stObj);
|
|
3286
|
-
const
|
|
3379
|
+
const cpus3 = os.cpus().length;
|
|
3287
3380
|
const totalGB = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
|
|
3288
3381
|
const usedGB = (parseFloat(totalGB) - os.freemem() / 1024 / 1024 / 1024).toFixed(1);
|
|
3289
3382
|
const load = os.loadavg()[0].toFixed(2);
|
|
@@ -3336,7 +3429,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
3336
3429
|
] }),
|
|
3337
3430
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3338
3431
|
" System: ",
|
|
3339
|
-
|
|
3432
|
+
cpus3,
|
|
3340
3433
|
"c Load ",
|
|
3341
3434
|
load,
|
|
3342
3435
|
" RAM ",
|
|
@@ -3748,7 +3841,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
3748
3841
|
onToggleProxy: () => {
|
|
3749
3842
|
}
|
|
3750
3843
|
});
|
|
3751
|
-
const
|
|
3844
|
+
const proxyCtx = proxyProvider && proxyOpts ? { provider: proxyProvider, opts: proxyOpts } : null;
|
|
3845
|
+
const socketServer = useControlPlane(pm.manager, config.name, logSink, pm.pushLog, pm.logBus, pm.stateBus, platform, proxyCtx);
|
|
3752
3846
|
const shutdown = useCallback3(async () => {
|
|
3753
3847
|
lazyProxies.current.forEach((p) => p.destroy());
|
|
3754
3848
|
await socketServer.current?.close();
|