Skip to content

Commit baf6109

Browse files
authored
refactor: Clean Up CSpell Worker (#8372)
1 parent 6156f85 commit baf6109

11 files changed

Lines changed: 177 additions & 313 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type { MessagePort } from 'node:worker_threads';
2+
import { MessageChannel, Worker } from 'node:worker_threads';
3+
4+
import type { CSpellRPCClient } from 'cspell-lib/cspell-rpc';
5+
import { createCSpellRPCClient } from 'cspell-lib/cspell-rpc';
6+
7+
export function startCSpellWorker(): CSpellWorker {
8+
const messageChannel = new MessageChannel();
9+
const { port1, port2 } = messageChannel;
10+
11+
const worker = new Worker(new URL('worker.js', import.meta.url), {
12+
workerData: { port: port1 },
13+
transferList: [port1],
14+
stderr: true,
15+
stdout: true,
16+
});
17+
18+
return new CSpellWorkerImpl({ worker, port: port2 });
19+
}
20+
21+
export interface CSpellWorker {
22+
ready: Promise<boolean>;
23+
ok: (timeout?: number) => Promise<boolean>;
24+
client: CSpellRPCClient;
25+
status: Map<string, number>;
26+
[Symbol.asyncDispose](): Promise<void>;
27+
}
28+
29+
interface CSpellWorkerInstance {
30+
worker: Worker;
31+
port: MessagePort;
32+
}
33+
34+
class CSpellWorkerImpl implements CSpellWorker {
35+
#terminated: boolean = false;
36+
#worker: Worker;
37+
#ready: Promise<boolean>;
38+
#client: CSpellRPCClient;
39+
#handleMessage: (message: unknown) => void;
40+
#listeners: Set<(message: unknown) => boolean> = new Set();
41+
#status: Map<string, number> = new Map();
42+
43+
constructor(instance: CSpellWorkerInstance) {
44+
this.#handleMessage = (message: unknown) => this.#processMessage(message);
45+
this.#ready = this.#waitForReady();
46+
this.#worker = instance.worker;
47+
this.#worker.on('message', this.#handleMessage);
48+
this.#client = createCSpellRPCClient(instance.port);
49+
this.#worker.once('exit', () => this.terminate());
50+
}
51+
52+
ok(timeout?: number): Promise<boolean> {
53+
const p = new Promise<boolean>((resolve) => {
54+
let done = false;
55+
setTimeout(() => {
56+
if (done) return;
57+
done = true;
58+
resolve(false);
59+
}, timeout);
60+
this.#listeners.add((message: unknown) => {
61+
if (done) return true;
62+
if (message === 'status:ok') {
63+
done = true;
64+
resolve(true);
65+
}
66+
return done;
67+
});
68+
});
69+
this.#worker.postMessage('status:ok');
70+
return p;
71+
}
72+
73+
get ready(): Promise<boolean> {
74+
return this.#ready;
75+
}
76+
77+
get client(): CSpellRPCClient {
78+
return this.#client;
79+
}
80+
81+
get status(): Map<string, number> {
82+
return new Map(this.#status);
83+
}
84+
85+
get isTerminated(): boolean {
86+
return this.#terminated;
87+
}
88+
89+
#waitForReady(): Promise<boolean> {
90+
return new Promise((resolve) => {
91+
const listener = (message: unknown): boolean => {
92+
if (message === 'status:ready') {
93+
resolve(true);
94+
return true;
95+
}
96+
return false;
97+
};
98+
this.#listeners.add(listener);
99+
});
100+
}
101+
102+
#processMessage = (message: unknown): void => {
103+
if (typeof message === 'string') {
104+
this.#status.set(message, performance.now());
105+
}
106+
for (const listener of this.#listeners) {
107+
const removeListener = listener(message);
108+
if (removeListener) {
109+
this.#listeners.delete(listener);
110+
}
111+
}
112+
};
113+
114+
terminate(): Promise<void> {
115+
return this[Symbol.asyncDispose]();
116+
}
117+
118+
async [Symbol.asyncDispose](): Promise<void> {
119+
if (this.#terminated) return;
120+
this.#terminated = true;
121+
this.#client[Symbol.dispose]();
122+
await this.#worker.terminate();
123+
return;
124+
}
125+
}
Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,35 @@
11
import { describe, expect, test } from 'vitest';
22

3-
import { startCSpellWorker, startSimpleRPCWorker } from '../dist/index.js';
3+
import { startCSpellWorker } from '../dist/index.js';
44

55
const oc = (...params: Parameters<typeof expect.objectContaining>) => expect.objectContaining(...params);
66

77
describe('Index', () => {
8-
test('Create Simple Server', async () => {
9-
const { worker, client, ok, online } = startSimpleRPCWorker();
10-
await online;
11-
12-
await expect(ok(1000)).resolves.toBe(true);
13-
14-
const api = client.api;
15-
await expect(api.add(2, 3)).resolves.toBe(5);
16-
await expect(api.mul(2, 3)).resolves.toBe(6);
17-
await expect(api.sub(2, 3)).resolves.toBe(-1);
18-
await expect(api.div(33, 3)).resolves.toBe(11);
19-
await expect(api.sleep(2)).resolves.toBe(undefined);
20-
await expect(api.error('My Error')).rejects.toEqual(new Error('My Error'));
21-
22-
client[Symbol.dispose]();
23-
worker.terminate();
24-
});
25-
268
test('Create CSpell Server', async () => {
27-
const { worker, client, ok, online } = startCSpellWorker();
28-
await online;
9+
await using worker = startCSpellWorker();
10+
await worker.ready;
11+
const client = worker.client;
2912

30-
await expect(ok(1000)).resolves.toBe(true);
13+
await expect(worker.ok(1000)).resolves.toBe(true);
3114
await expect(client.isOK()).resolves.toBe(true);
3215

33-
client[Symbol.dispose]();
34-
worker.terminate();
16+
const status = worker.status;
17+
expect(status.size).toBeGreaterThan(0);
18+
expect([...status.keys()]).toEqual([
19+
'status:starting',
20+
'status:server:starting',
21+
'status:server:ready',
22+
'status:ready',
23+
'status:ok',
24+
]);
3525
});
3626

3727
test('Spell check a document.', async () => {
38-
const { worker, client, ok, online } = startCSpellWorker();
39-
await online;
28+
await using worker = startCSpellWorker();
29+
await worker.ready;
30+
const client = worker.client;
4031

41-
await expect(ok(1000)).resolves.toBe(true);
32+
await expect(worker.ok(1000)).resolves.toBe(true);
4233

4334
await expect(client.isOK()).resolves.toBe(true);
4435

@@ -48,8 +39,5 @@ describe('Index', () => {
4839
const result = await api.spellCheckDocument(doc, {}, {});
4940
expect(result).toBeDefined();
5041
expect(result).toEqual(oc({ document: oc(doc), issues: [], errors: undefined }));
51-
52-
client[Symbol.dispose]();
53-
worker.terminate();
5442
});
5543
});
Lines changed: 2 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,2 @@
1-
import { MessageChannel, Worker } from 'node:worker_threads';
2-
3-
import type { CSpellRPCClient } from 'cspell-lib/cspell-rpc';
4-
import { createCSpellRPCClient } from 'cspell-lib/cspell-rpc';
5-
6-
import type { SimpleRPCClient } from './simpleServer.js';
7-
import { startSimpleRPCClient } from './simpleServer.js';
8-
9-
export interface CSpellWorkerInstance {
10-
worker: Worker;
11-
client: CSpellRPCClient;
12-
ok: (timeout?: number) => Promise<boolean>;
13-
online: Promise<void>;
14-
}
15-
16-
export function startCSpellWorker(): CSpellWorkerInstance {
17-
const messageChannel = new MessageChannel();
18-
const { port1, port2 } = messageChannel;
19-
20-
const worker = new Worker(new URL('worker.js', import.meta.url), {
21-
workerData: { port: port1 },
22-
transferList: [port1],
23-
stderr: true,
24-
stdout: true,
25-
});
26-
27-
const online = workerOnline(worker);
28-
const client = createCSpellRPCClient(port2);
29-
const ok = createWorkerOk(worker);
30-
31-
return { worker, client, ok, online };
32-
}
33-
34-
export interface SimpleWorkerInstance {
35-
worker: Worker;
36-
client: SimpleRPCClient;
37-
ok: (timeout?: number) => Promise<boolean>;
38-
online: Promise<void>;
39-
}
40-
41-
export function startSimpleRPCWorker(): SimpleWorkerInstance {
42-
const messageChannel = new MessageChannel();
43-
const { port1, port2 } = messageChannel;
44-
45-
const worker = new Worker(new URL('simpleWorker.js', import.meta.url), {
46-
workerData: { port: port1 },
47-
transferList: [port1],
48-
stderr: true,
49-
stdout: true,
50-
});
51-
52-
const online = workerOnline(worker);
53-
const client = startSimpleRPCClient(port2);
54-
const ok = createWorkerOk(worker);
55-
56-
return { worker, client, ok, online };
57-
}
58-
59-
function createWorkerOk(worker: Worker, defaultTimeout: number = 1000): (timeout?: number) => Promise<boolean> {
60-
const ok = (timeout: number = defaultTimeout): Promise<boolean> => {
61-
const promise = new Promise<boolean>((resolve) => {
62-
let resolved: boolean = false;
63-
let t: NodeJS.Timeout | undefined = setTimeout(() => r(false), timeout);
64-
const r = (v: boolean) => {
65-
if (!resolved) {
66-
resolved = true;
67-
resolve(v);
68-
}
69-
if (t) {
70-
clearTimeout(t);
71-
}
72-
t = undefined;
73-
};
74-
worker.once('message', (message: unknown) => {
75-
r(message === 'ok');
76-
});
77-
});
78-
79-
worker.postMessage('ok');
80-
81-
return promise;
82-
};
83-
84-
return ok;
85-
}
86-
87-
function workerOnline(worker: Worker): Promise<void> {
88-
return new Promise((resolve) => {
89-
worker.once('online', () => resolve());
90-
});
91-
}
1+
export type { CSpellWorker } from './cspellWorker.js';
2+
export { startCSpellWorker } from './cspellWorker.js';

packages/cspell-worker/src/simpleServer.test.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/cspell-worker/src/simpleServer.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

packages/cspell-worker/src/simpleWorker.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)