Skip to content

Commit 448fcf7

Browse files
authored
feat(vscode): implement daemon protocol (#915)
1 parent 44a9c15 commit 448fcf7

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed

extensions/vscode/src/daemon/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from "./protocol";
12
export * from "./dart-frog-daemon";
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Defines the protocol used by the Dart Frog daemon and custom
3+
* type guards to check if an object is a valid message.
4+
*
5+
* @see {@link https://github.com/VeryGoodOpenSource/dart_frog/blob/main/packages/dart_frog_cli/lib/src/daemon/protocol.dart Dart Frog dart's protocol}
6+
*/
7+
8+
export class DaemonMessage {
9+
/**
10+
* Decodes messages that follow the protocol.
11+
*
12+
* @param data The data to decode (usually from stdout of the Dart Frog
13+
* daemon and in JSON format).
14+
* @returns The decoded messages.
15+
*/
16+
public static decode(data: Buffer): DaemonMessage[] {
17+
const stringData = data.toString();
18+
const messages = stringData.split("\n").filter((s) => s.trim().length > 0);
19+
const parsedMessages = messages.map((message) => JSON.parse(message));
20+
21+
let daemonMessages: DaemonMessage[] = [];
22+
for (const parsedMessage of parsedMessages) {
23+
for (const message of parsedMessage) {
24+
daemonMessages.push(message as DaemonMessage);
25+
}
26+
}
27+
28+
return daemonMessages;
29+
}
30+
}
31+
32+
export abstract class DaemonRequest implements DaemonMessage {
33+
abstract method: string;
34+
abstract id: string;
35+
abstract params: any;
36+
}
37+
38+
export function isDaemonRequest(object: any): object is DaemonRequest {
39+
return typeof object.id === "string" && typeof object.method === "string";
40+
}
41+
42+
export interface DaemonResponse extends DaemonMessage {
43+
id: string;
44+
result: any;
45+
error: any;
46+
}
47+
48+
export function isDaemonResponse(object: any): object is DaemonResponse {
49+
return typeof object.id === "string" && !("method" in object);
50+
}
51+
52+
export interface DaemonEvent extends DaemonMessage {
53+
event: string;
54+
params: any;
55+
}
56+
57+
export function isDaemonEvent(object: any): object is DaemonEvent {
58+
return typeof object.event === "string";
59+
}
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import * as assert from "assert";
2+
import {
3+
DaemonMessage,
4+
isDaemonEvent,
5+
isDaemonRequest,
6+
isDaemonResponse,
7+
} from "../../../daemon";
8+
9+
suite("DaemonMessage", () => {
10+
suite("decode", () => {
11+
test("returns an empty array when data is empty", () => {
12+
const data = Buffer.from("");
13+
14+
assert.deepEqual(DaemonMessage.decode(data), []);
15+
});
16+
17+
test("decodes request", () => {
18+
const request = `[{"method": "daemon.requestVersion", "id": "12"}]`;
19+
const data = Buffer.from(request);
20+
21+
const requestObject = {
22+
method: "daemon.requestVersion",
23+
id: "12",
24+
};
25+
assert.deepEqual(DaemonMessage.decode(data), [requestObject]);
26+
});
27+
28+
test("decodes event", () => {
29+
const event = `[{"event":"daemon.ready","params":{"version":"0.0.1","processId":75941}}]`;
30+
const data = Buffer.from(event);
31+
32+
const eventObject = {
33+
event: "daemon.ready",
34+
params: {
35+
version: "0.0.1",
36+
processId: 75941,
37+
},
38+
};
39+
assert.deepEqual(DaemonMessage.decode(data), [eventObject]);
40+
});
41+
42+
test("decodes response", () => {
43+
const response = `[{"id":"12","result":{"version":"0.0.1"}}]`;
44+
const data = Buffer.from(response);
45+
46+
const responseObject = {
47+
id: "12",
48+
result: {
49+
version: "0.0.1",
50+
},
51+
};
52+
assert.deepEqual(DaemonMessage.decode(data), [responseObject]);
53+
});
54+
55+
test("decodes multiple batched messages", () => {
56+
const request = `{"method": "daemon.requestVersion", "id": "12"}`;
57+
const event = `{"event":"daemon.ready","params":{"version":"0.0.1","processId":75941}}`;
58+
const response = `{"id":"12","result":{"version":"0.0.1"}}`;
59+
const data = Buffer.from(`[${request},${event},${response}]`);
60+
61+
const requestObject = {
62+
method: "daemon.requestVersion",
63+
id: "12",
64+
};
65+
const eventObject = {
66+
event: "daemon.ready",
67+
params: {
68+
version: "0.0.1",
69+
processId: 75941,
70+
},
71+
};
72+
const responseObject = {
73+
id: "12",
74+
result: {
75+
version: "0.0.1",
76+
},
77+
};
78+
assert.deepEqual(DaemonMessage.decode(data), [
79+
requestObject,
80+
eventObject,
81+
responseObject,
82+
]);
83+
});
84+
85+
test("decodes multiple buffered messages", () => {
86+
const request = `[{"method": "daemon.requestVersion", "id": "12"}]`;
87+
const event = `[{"event":"daemon.ready","params":{"version":"0.0.1","processId":75941}}]`;
88+
const response = `[{"id":"12","result":{"version":"0.0.1"}}]`;
89+
const data = Buffer.from(`${request}\n${event}\n${response}`);
90+
91+
const requestObject = {
92+
method: "daemon.requestVersion",
93+
id: "12",
94+
};
95+
const eventObject = {
96+
event: "daemon.ready",
97+
params: {
98+
version: "0.0.1",
99+
processId: 75941,
100+
},
101+
};
102+
const responseObject = {
103+
id: "12",
104+
result: {
105+
version: "0.0.1",
106+
},
107+
};
108+
assert.deepEqual(DaemonMessage.decode(data), [
109+
requestObject,
110+
eventObject,
111+
responseObject,
112+
]);
113+
});
114+
});
115+
});
116+
117+
suite("isDaemonRequest", () => {
118+
suite("returns true when object is a valid DaemonRequest", () => {
119+
test("with all fields", () => {
120+
const request = {
121+
id: "1",
122+
method: "method",
123+
params: {},
124+
};
125+
126+
assert.equal(isDaemonRequest(request), true);
127+
});
128+
129+
test("when missing params only", () => {
130+
const request = {
131+
id: "1",
132+
method: "method",
133+
};
134+
135+
assert.equal(isDaemonRequest(request), true);
136+
});
137+
});
138+
139+
suite("returns false", () => {
140+
test("when missing id only", () => {
141+
const request = {
142+
method: "method",
143+
params: {},
144+
};
145+
146+
assert.equal(isDaemonRequest(request), false);
147+
});
148+
149+
test("when missing method only", () => {
150+
const request = {
151+
id: "1",
152+
params: {},
153+
};
154+
155+
assert.equal(isDaemonRequest(request), false);
156+
});
157+
158+
test("when object is a valid DaemonResponse", () => {
159+
const response = {
160+
id: "1",
161+
result: {},
162+
error: {},
163+
};
164+
165+
assert.equal(isDaemonResponse(response), true);
166+
assert.equal(isDaemonRequest(response), false);
167+
});
168+
169+
test("when object is a valid DaemonEvent", () => {
170+
const event = {
171+
event: "event",
172+
params: {},
173+
};
174+
175+
assert.equal(isDaemonEvent(event), true);
176+
assert.equal(isDaemonRequest(event), false);
177+
});
178+
});
179+
});
180+
181+
suite("isDaemonResponse", () => {
182+
suite("returns true when object is a valid DaemonResponse", () => {
183+
test("with all fields", () => {
184+
const response = {
185+
id: "1",
186+
result: {},
187+
error: {},
188+
};
189+
190+
assert.equal(isDaemonResponse(response), true);
191+
});
192+
193+
test("when missing error only", () => {
194+
const response = {
195+
id: "1",
196+
result: {},
197+
};
198+
199+
assert.equal(isDaemonResponse(response), true);
200+
});
201+
202+
test("when missing result only", () => {
203+
const response = {
204+
id: "1",
205+
error: {},
206+
};
207+
208+
assert.equal(isDaemonResponse(response), true);
209+
});
210+
211+
test("when missing result and errror only", () => {
212+
const response = {
213+
id: "1",
214+
};
215+
216+
assert.equal(isDaemonResponse(response), true);
217+
});
218+
});
219+
220+
suite("returns false", () => {
221+
test("when missing id only", () => {
222+
const response = {
223+
result: {},
224+
error: {},
225+
};
226+
227+
assert.equal(isDaemonResponse(response), false);
228+
});
229+
230+
test("when object is a valid DaemonRequest", () => {
231+
const request = {
232+
id: "1",
233+
method: "method",
234+
params: {},
235+
};
236+
237+
assert.equal(isDaemonRequest(request), true);
238+
assert.equal(isDaemonResponse(request), false);
239+
});
240+
241+
test("when object is a valid DaemonEvent", () => {
242+
const event = {
243+
event: "event",
244+
params: {},
245+
};
246+
247+
assert.equal(isDaemonEvent(event), true);
248+
assert.equal(isDaemonResponse(event), false);
249+
});
250+
});
251+
});
252+
253+
suite("isDaemonEvent", () => {
254+
suite("returns true when object is a valid DaemonEvent", () => {
255+
test("with all fields", () => {
256+
const event = {
257+
event: "event",
258+
params: {},
259+
};
260+
261+
assert.equal(isDaemonEvent(event), true);
262+
});
263+
264+
test("when missing params only", () => {
265+
const event = {
266+
event: "event",
267+
};
268+
269+
assert.equal(isDaemonEvent(event), true);
270+
});
271+
});
272+
273+
suite("returns false", () => {
274+
test("when missing event only", () => {
275+
const event = {
276+
params: {},
277+
};
278+
279+
assert.equal(isDaemonEvent(event), false);
280+
});
281+
282+
test("when object is a valid DaemonRequest", () => {
283+
const request = {
284+
id: "1",
285+
method: "method",
286+
params: {},
287+
};
288+
289+
assert.equal(isDaemonRequest(request), true);
290+
assert.equal(isDaemonEvent(request), false);
291+
});
292+
293+
test("when object is a valid DaemonResponse", () => {
294+
const response = {
295+
id: "1",
296+
result: {},
297+
error: {},
298+
};
299+
300+
assert.equal(isDaemonResponse(response), true);
301+
assert.equal(isDaemonEvent(response), false);
302+
});
303+
});
304+
});

0 commit comments

Comments
 (0)