@worca/ui 0.22.0 → 0.24.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/app/main.bundle.js +7298 -4688
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +5 -1
- package/app/styles.css +1315 -23
- package/app/utils/state-actions.js +33 -4
- package/app/utils/status-constants.js +11 -0
- package/bin/worca-ui.js +2 -2
- package/package.json +2 -1
- package/scripts/build-frontend.js +48 -1
- package/server/app.js +159 -1
- package/server/fleet-routes.js +1149 -0
- package/server/index.js +4 -3
- package/server/integrations/commands/fleet.js +266 -0
- package/server/integrations/commands/global.js +18 -0
- package/server/integrations/commands/parser.js +4 -1
- package/server/integrations/commands/workspace.js +295 -0
- package/server/integrations/index.js +9 -0
- package/server/integrations/renderers.js +386 -0
- package/server/integrations/rest_client.js +7 -0
- package/server/paths.js +78 -0
- package/server/project-routes.js +68 -5
- package/server/workspace-routes.js +1554 -0
- package/server/worktree-ops.js +12 -1
- package/server/worktrees-routes.js +34 -0
- package/server/ws-fleet-manifest-watcher.js +131 -0
- package/server/ws-message-router.js +20 -0
- package/server/ws-modular.js +18 -1
- package/server/ws-workspace-manifest-watcher.js +136 -0
package/server/worktree-ops.js
CHANGED
|
@@ -59,7 +59,18 @@ export async function removeWorktree(
|
|
|
59
59
|
/* ignore */
|
|
60
60
|
}
|
|
61
61
|
if (isRealDir) {
|
|
62
|
-
|
|
62
|
+
// maxRetries handles transient ENOTEMPTY/EBUSY/EPERM on macOS when a
|
|
63
|
+
// background process (Spotlight, language servers, npm install
|
|
64
|
+
// finishing) touches a deep node_modules subtree between the
|
|
65
|
+
// recursive walk's readdir and the final rmdir. Without retries,
|
|
66
|
+
// a single race surfaces "ENOTEMPTY: directory not empty, rmdir
|
|
67
|
+
// .../node_modules/lucide/dist" to the user.
|
|
68
|
+
await rm(worktreePath, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
force: true,
|
|
71
|
+
maxRetries: 5,
|
|
72
|
+
retryDelay: 200,
|
|
73
|
+
});
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
}
|
|
@@ -262,6 +262,17 @@ function _patchRegistry(worcaDir, runId, patch) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
function _isPidAlive(pid) {
|
|
266
|
+
if (!pid || typeof pid !== 'number') return false;
|
|
267
|
+
try {
|
|
268
|
+
process.kill(pid, 0);
|
|
269
|
+
return true;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err.code === 'EPERM') return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
265
276
|
async function _listWorktrees(worcaDir) {
|
|
266
277
|
const pipelinesDir = join(worcaDir, 'multi', 'pipelines.d');
|
|
267
278
|
if (!existsSync(pipelinesDir)) return [];
|
|
@@ -290,6 +301,29 @@ async function _listWorktrees(worcaDir) {
|
|
|
290
301
|
if (actual) status = actual;
|
|
291
302
|
}
|
|
292
303
|
|
|
304
|
+
// Stale-registry reconciliation: a child can die before ever writing
|
|
305
|
+
// status.json (e.g. fleet halt right after dispatch, preflight crash,
|
|
306
|
+
// SIGKILL). In that case the worktree exists but .worca/runs/ doesn't,
|
|
307
|
+
// _readWorktreeStatus returns null, and we'd fall back to reg.status
|
|
308
|
+
// which may still say "running" with a dead pid. Treat that as
|
|
309
|
+
// "interrupted" and patch the registry so this only happens once.
|
|
310
|
+
//
|
|
311
|
+
// Only reconcile when reg.pid is present — a missing pid means the
|
|
312
|
+
// entry is either from a non-standard registration path (e.g. test
|
|
313
|
+
// fixtures) or pre-dates the pid-on-registration contract, so we
|
|
314
|
+
// can't make liveness claims about it.
|
|
315
|
+
if (
|
|
316
|
+
status === 'running' &&
|
|
317
|
+
typeof reg.pid === 'number' &&
|
|
318
|
+
!_isPidAlive(reg.pid)
|
|
319
|
+
) {
|
|
320
|
+
status = 'interrupted';
|
|
321
|
+
_patchRegistry(worcaDir, reg.run_id, {
|
|
322
|
+
status: 'interrupted',
|
|
323
|
+
interrupted_reason: 'stale_pid',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
293
327
|
let ageSeconds = 0;
|
|
294
328
|
if (reg.started_at) {
|
|
295
329
|
const started = new Date(reg.started_at).getTime();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet manifest watcher — monitors ~/.worca/fleet-runs/<fleet_id>.json for changes.
|
|
3
|
+
* Emits fleet-update WS events when a fleet manifest is written (§13.5).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { effectiveFleetStatus } from './fleet-routes.js';
|
|
9
|
+
import { fleetRunsDir as resolveFleetRunsDir } from './paths.js';
|
|
10
|
+
|
|
11
|
+
const FLEET_DEBOUNCE_MS = 200;
|
|
12
|
+
|
|
13
|
+
const FAILURE_STATES = new Set(['failed', 'setup_failed', 'unrecoverable']);
|
|
14
|
+
|
|
15
|
+
function readJson(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveChildStatus(child) {
|
|
24
|
+
const { project_path, run_id } = child;
|
|
25
|
+
if (!project_path || !run_id) return 'running';
|
|
26
|
+
const registryPath = join(
|
|
27
|
+
project_path,
|
|
28
|
+
'.worca',
|
|
29
|
+
'multi',
|
|
30
|
+
'pipelines.d',
|
|
31
|
+
`${run_id}.json`,
|
|
32
|
+
);
|
|
33
|
+
const entry = readJson(registryPath);
|
|
34
|
+
return entry?.status ?? 'running';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ broadcaster: { broadcast: Function }, fleetRunsDir?: string }} deps
|
|
39
|
+
*/
|
|
40
|
+
export function createFleetManifestWatcher({
|
|
41
|
+
broadcaster,
|
|
42
|
+
fleetRunsDir: fleetRunsDirArg,
|
|
43
|
+
}) {
|
|
44
|
+
// Lazy resolution honors $WORCA_HOME (issue #162).
|
|
45
|
+
const fleetRunsDir = resolveFleetRunsDir(fleetRunsDirArg);
|
|
46
|
+
let fsWatcher = null;
|
|
47
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
48
|
+
const debounceTimers = new Map();
|
|
49
|
+
|
|
50
|
+
function broadcastFleetUpdate(fleetId, manifestPath) {
|
|
51
|
+
const manifest = readJson(manifestPath);
|
|
52
|
+
if (!manifest) return;
|
|
53
|
+
|
|
54
|
+
const rawChildren = Array.isArray(manifest.children)
|
|
55
|
+
? manifest.children
|
|
56
|
+
: [];
|
|
57
|
+
const children = rawChildren.map((child) => ({
|
|
58
|
+
run_id: child.run_id,
|
|
59
|
+
project_path: child.project_path,
|
|
60
|
+
status: resolveChildStatus(child),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const completed_children = children.filter(
|
|
64
|
+
(c) => c.status === 'completed',
|
|
65
|
+
).length;
|
|
66
|
+
const failed_children = children.filter((c) =>
|
|
67
|
+
FAILURE_STATES.has(c.status),
|
|
68
|
+
).length;
|
|
69
|
+
|
|
70
|
+
// Derive the effective status (same rules as REST) instead of broadcasting
|
|
71
|
+
// raw manifest.status — otherwise cards stay "running" forever, because
|
|
72
|
+
// run_fleet.py never writes a terminal status after it exits. Pure
|
|
73
|
+
// function: persists nothing, so we don't trigger a watch→write→watch loop.
|
|
74
|
+
const { status, halt_reason } = effectiveFleetStatus(
|
|
75
|
+
manifest,
|
|
76
|
+
children.map((c) => c.status),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
broadcaster.broadcast('fleet-update', {
|
|
80
|
+
fleet_id: fleetId,
|
|
81
|
+
status,
|
|
82
|
+
halt_reason,
|
|
83
|
+
completed_children,
|
|
84
|
+
failed_children,
|
|
85
|
+
children,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scheduleUpdate(fleetId, manifestPath) {
|
|
90
|
+
const existing = debounceTimers.get(fleetId);
|
|
91
|
+
if (existing) clearTimeout(existing);
|
|
92
|
+
debounceTimers.set(
|
|
93
|
+
fleetId,
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
debounceTimers.delete(fleetId);
|
|
96
|
+
broadcastFleetUpdate(fleetId, manifestPath);
|
|
97
|
+
}, FLEET_DEBOUNCE_MS),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (existsSync(fleetRunsDir)) {
|
|
103
|
+
fsWatcher = watch(
|
|
104
|
+
fleetRunsDir,
|
|
105
|
+
{ persistent: false },
|
|
106
|
+
(_event, filename) => {
|
|
107
|
+
if (!filename?.endsWith('.json')) return;
|
|
108
|
+
const fleetId = filename.slice(0, -5);
|
|
109
|
+
scheduleUpdate(fleetId, join(fleetRunsDir, filename));
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// fs.watch unsupported or dir unavailable — skip silently
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function destroy() {
|
|
118
|
+
if (fsWatcher) {
|
|
119
|
+
try {
|
|
120
|
+
fsWatcher.close();
|
|
121
|
+
} catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
fsWatcher = null;
|
|
125
|
+
}
|
|
126
|
+
for (const timer of debounceTimers.values()) clearTimeout(timer);
|
|
127
|
+
debounceTimers.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { destroy };
|
|
131
|
+
}
|
|
@@ -80,6 +80,22 @@ export function createMessageRouter({
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// When a run-scoped subscribe arrives with a `payload.projectId` that
|
|
84
|
+
// differs from the client's currently-bound subs.projectId, re-bind the
|
|
85
|
+
// WS to that project. Without this, the targeted project's WatcherSet
|
|
86
|
+
// stays in POLLING tier (no logWatcher / no live updates), and the
|
|
87
|
+
// backfill / live stream silently never arrive — symptoms reported by
|
|
88
|
+
// the user as "no logs / no agent prompts" on the run-detail page in
|
|
89
|
+
// global mode. We keep the previous projectId reference so demotion
|
|
90
|
+
// still happens via the normal client-count mechanism.
|
|
91
|
+
function _adoptProjectFromPayload(ws, payload) {
|
|
92
|
+
const requested = payload?.projectId;
|
|
93
|
+
if (!requested || !watcherSets.has(requested)) return;
|
|
94
|
+
const subs = clientManager.getSubs(ws);
|
|
95
|
+
if (subs?.projectId === requested) return;
|
|
96
|
+
clientManager.setProtocol(ws, subs?.protocolVersion ?? 1, requested);
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
async function handleMessage(ws, data) {
|
|
84
100
|
let json;
|
|
85
101
|
try {
|
|
@@ -147,6 +163,7 @@ export function createMessageRouter({
|
|
|
147
163
|
);
|
|
148
164
|
return;
|
|
149
165
|
}
|
|
166
|
+
_adoptProjectFromPayload(ws, req.payload);
|
|
150
167
|
const proj = resolveProject(ws, req.payload);
|
|
151
168
|
const runs = discoverRuns(proj.worcaDir);
|
|
152
169
|
const run = runs.find((r) => r.id === runId);
|
|
@@ -294,6 +311,7 @@ export function createMessageRouter({
|
|
|
294
311
|
);
|
|
295
312
|
return;
|
|
296
313
|
}
|
|
314
|
+
_adoptProjectFromPayload(ws, req.payload);
|
|
297
315
|
const proj = resolveProject(ws, req.payload);
|
|
298
316
|
if (!proj) {
|
|
299
317
|
ws.send(
|
|
@@ -336,6 +354,7 @@ export function createMessageRouter({
|
|
|
336
354
|
// subscribe-log
|
|
337
355
|
if (req.type === 'subscribe-log') {
|
|
338
356
|
const { stage, runId, iteration } = req.payload || {};
|
|
357
|
+
_adoptProjectFromPayload(ws, req.payload);
|
|
339
358
|
const proj = resolveProject(ws, req.payload);
|
|
340
359
|
const s = clientManager.ensureSubs(ws);
|
|
341
360
|
s.logStage = stage || '*';
|
|
@@ -652,6 +671,7 @@ export function createMessageRouter({
|
|
|
652
671
|
);
|
|
653
672
|
return;
|
|
654
673
|
}
|
|
674
|
+
_adoptProjectFromPayload(ws, req.payload);
|
|
655
675
|
const proj = resolveProject(ws, req.payload);
|
|
656
676
|
if (!proj.wset.beadsWatcher) {
|
|
657
677
|
ws.send(JSON.stringify(makeOk(req, { issues: [], runId })));
|
package/server/ws-modular.js
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
import { existsSync, watch } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { WebSocketServer } from 'ws';
|
|
12
|
+
import { fleetRunsDir, workspaceRunsDir } from './paths.js';
|
|
12
13
|
import { readProjects, synthesizeDefaultProject } from './project-registry.js';
|
|
13
14
|
import { TIER_FULL, TIER_POLLING, WatcherSet } from './watcher-set.js';
|
|
14
15
|
import { readProjectWorcaVersion } from './worca-setup.js';
|
|
15
16
|
import { createBroadcaster } from './ws-broadcaster.js';
|
|
16
17
|
import { createClientManager } from './ws-client-manager.js';
|
|
18
|
+
import { createFleetManifestWatcher } from './ws-fleet-manifest-watcher.js';
|
|
17
19
|
import { createMessageRouter } from './ws-message-router.js';
|
|
18
20
|
import { resolveLatestRunDir } from './ws-status-watcher.js';
|
|
21
|
+
import { createWorkspaceManifestWatcher } from './ws-workspace-manifest-watcher.js';
|
|
19
22
|
|
|
20
23
|
export { resolveLatestRunDir };
|
|
21
24
|
|
|
@@ -45,7 +48,19 @@ export function attachWsServer(httpServer, config) {
|
|
|
45
48
|
getSubs: clientManager.getSubs,
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
//
|
|
51
|
+
// 3a. Fleet manifest watcher — global, not per-project (§13.5)
|
|
52
|
+
const fleetManifestWatcher = createFleetManifestWatcher({
|
|
53
|
+
broadcaster,
|
|
54
|
+
fleetRunsDir: fleetRunsDir(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3a-ws. Workspace manifest watcher — global, separate from fleet (W-047 §13.5)
|
|
58
|
+
const workspaceManifestWatcher = createWorkspaceManifestWatcher({
|
|
59
|
+
broadcaster,
|
|
60
|
+
workspaceRunsDir: workspaceRunsDir(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 3b. Create WatcherSet(s) — one per project
|
|
49
64
|
/** @type {Map<string, WatcherSet>} */
|
|
50
65
|
const watcherSets = new Map();
|
|
51
66
|
|
|
@@ -269,6 +284,8 @@ export function attachWsServer(httpServer, config) {
|
|
|
269
284
|
|
|
270
285
|
wss.on('close', () => {
|
|
271
286
|
clientManager.destroy();
|
|
287
|
+
fleetManifestWatcher.destroy();
|
|
288
|
+
workspaceManifestWatcher.destroy();
|
|
272
289
|
if (dirWatcher) {
|
|
273
290
|
try {
|
|
274
291
|
dirWatcher.close();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace manifest watcher — monitors ~/.worca/workspace-runs/ for pointer
|
|
3
|
+
* file changes, reads the actual manifest, and broadcasts workspace-update,
|
|
4
|
+
* workspace-tier-update, and guide-conflict WS events.
|
|
5
|
+
*
|
|
6
|
+
* Separate from fleet-update per W-040 §13.5 — never multiplexed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { workspaceRunsDir as resolveWorkspaceRunsDir } from './paths.js';
|
|
12
|
+
|
|
13
|
+
const WS_DEBOUNCE_MS = 200;
|
|
14
|
+
|
|
15
|
+
function readJson(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readManifestFromPointer(wsRunsDir, wsId) {
|
|
24
|
+
const pointer = readJson(join(wsRunsDir, `${wsId}.json`));
|
|
25
|
+
if (!pointer?.workspace_root) return null;
|
|
26
|
+
const manifestPath = join(
|
|
27
|
+
pointer.workspace_root,
|
|
28
|
+
'.worca',
|
|
29
|
+
'workspace-runs',
|
|
30
|
+
wsId,
|
|
31
|
+
'workspace-manifest.json',
|
|
32
|
+
);
|
|
33
|
+
return readJson(manifestPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ broadcaster: { broadcast: Function }, workspaceRunsDir?: string }} deps
|
|
38
|
+
*/
|
|
39
|
+
export function createWorkspaceManifestWatcher({
|
|
40
|
+
broadcaster,
|
|
41
|
+
workspaceRunsDir: workspaceRunsDirArg,
|
|
42
|
+
}) {
|
|
43
|
+
// Lazy resolution honors $WORCA_HOME (issue #162).
|
|
44
|
+
const workspaceRunsDir = resolveWorkspaceRunsDir(workspaceRunsDirArg);
|
|
45
|
+
|
|
46
|
+
let fsWatcher = null;
|
|
47
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
48
|
+
const debounceTimers = new Map();
|
|
49
|
+
|
|
50
|
+
function broadcastWorkspaceUpdate(wsId) {
|
|
51
|
+
const manifest = readManifestFromPointer(workspaceRunsDir, wsId);
|
|
52
|
+
if (!manifest) return;
|
|
53
|
+
|
|
54
|
+
const children = Array.isArray(manifest.children) ? manifest.children : [];
|
|
55
|
+
const dag = manifest.dag ?? { tiers: [] };
|
|
56
|
+
|
|
57
|
+
broadcaster.broadcast('workspace-update', {
|
|
58
|
+
workspace_id: wsId,
|
|
59
|
+
workspace_name: manifest.workspace_name ?? null,
|
|
60
|
+
status: manifest.status ?? 'running',
|
|
61
|
+
halt_reason: manifest.halt_reason ?? null,
|
|
62
|
+
dag,
|
|
63
|
+
children: children.map((c) => ({
|
|
64
|
+
repo: c.repo,
|
|
65
|
+
run_id: c.run_id,
|
|
66
|
+
status: c.status,
|
|
67
|
+
tier: c.tier,
|
|
68
|
+
})),
|
|
69
|
+
integration_test: manifest.integration_test ?? null,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const tiers = dag.tiers ?? [];
|
|
73
|
+
for (const tier of tiers) {
|
|
74
|
+
broadcaster.broadcast('workspace-tier-update', {
|
|
75
|
+
workspace_id: wsId,
|
|
76
|
+
tier: tier.tier,
|
|
77
|
+
repos: tier.repos,
|
|
78
|
+
status: tier.status,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const conflicts = manifest.guide_conflicts;
|
|
83
|
+
if (Array.isArray(conflicts) && conflicts.length > 0) {
|
|
84
|
+
broadcaster.broadcast('guide-conflict', {
|
|
85
|
+
workspace_id: wsId,
|
|
86
|
+
conflicts,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scheduleUpdate(wsId) {
|
|
92
|
+
const existing = debounceTimers.get(wsId);
|
|
93
|
+
if (existing) clearTimeout(existing);
|
|
94
|
+
debounceTimers.set(
|
|
95
|
+
wsId,
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
debounceTimers.delete(wsId);
|
|
98
|
+
broadcastWorkspaceUpdate(wsId);
|
|
99
|
+
}, WS_DEBOUNCE_MS),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(workspaceRunsDir)) {
|
|
105
|
+
fsWatcher = watch(
|
|
106
|
+
workspaceRunsDir,
|
|
107
|
+
{ persistent: false },
|
|
108
|
+
(_event, filename) => {
|
|
109
|
+
if (!filename?.endsWith('.json')) return;
|
|
110
|
+
const wsId = filename.slice(0, -5);
|
|
111
|
+
scheduleUpdate(wsId);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// fs.watch unsupported or dir unavailable — skip silently
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function destroy() {
|
|
120
|
+
if (fsWatcher) {
|
|
121
|
+
try {
|
|
122
|
+
fsWatcher.close();
|
|
123
|
+
} catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
fsWatcher = null;
|
|
127
|
+
}
|
|
128
|
+
for (const timer of debounceTimers.values()) clearTimeout(timer);
|
|
129
|
+
debounceTimers.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
destroy,
|
|
134
|
+
_broadcastForTest: broadcastWorkspaceUpdate,
|
|
135
|
+
};
|
|
136
|
+
}
|