Skip to content

Commit 7232708

Browse files
committed
feat: add public close methods
Add method to close transport and writer. Before there was no public way to close the transport/reader/writer created by attach for a socket, and the connection would hold the node process open. For example, this script would not exit (without an explicit process.exit(0)) ``` attach({ socket: pathToNvimSocket }) ``` This change adds .close to transport and NeovimClient which ends the writer. The other side of the connection should then close the reader which triggers detach and the cleanup code. I added asyncDispose methods for folks using modern javascript runtimes that support explicit resource management: ``` await using client = attach({ socket: pathToNvimSocket }) ``` Other wise you can call close ``` try { const client = attach({ socket: pathToNvimSocket }) } finally { await client.close() } ```
1 parent e0568e3 commit 7232708

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

packages/neovim/src/api/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Transport } from '../utils/transport';
66
import { VimValue } from '../types/VimValue';
77
import { Neovim } from './Neovim';
88
import { Buffer } from './Buffer';
9+
import { ASYNC_DISPOSE_SYMBOL } from '../utils/util';
910

1011
const REGEX_BUF_EVENT = /nvim_buf_(.*)_event/;
1112

@@ -249,4 +250,15 @@ export class NeovimClient extends Neovim {
249250

250251
return false;
251252
}
253+
254+
async close(): Promise<void> {
255+
await this.transport.close()
256+
}
257+
258+
/**
259+
* @see close
260+
*/
261+
async [ASYNC_DISPOSE_SYMBOL](): Promise<void> {
262+
await this.close();
263+
}
252264
}

packages/neovim/src/utils/transport.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventEmitter } from 'node:events';
2-
import { Readable, Writable } from 'node:stream';
2+
import { PassThrough, Readable, Writable } from 'node:stream';
33
import * as msgpack from '@msgpack/msgpack';
44
import expect from 'expect';
55
import { attach } from '../attach/attach';
@@ -17,8 +17,8 @@ describe('transport', () => {
1717
});
1818

1919
// Create fake reader/writer and send a (broken) message.
20-
const fakeReader = new Readable({ read() {} });
21-
const fakeWriter = new Writable({ write() {} });
20+
const fakeReader = new Readable({ read() { } });
21+
const fakeWriter = new Writable({ write() { } });
2222

2323
const nvim = attach({ reader: fakeReader, writer: fakeWriter });
2424
void nvim; // eslint-disable-line no-void
@@ -27,4 +27,17 @@ describe('transport', () => {
2727
const msg = msgpack.encode(invalidPayload);
2828
fakeReader.push(Buffer.from(msg.buffer, msg.byteOffset, msg.byteLength));
2929
});
30+
31+
it('closes transport and cleans up pending requests', async () => {
32+
const socket = new PassThrough()
33+
34+
const nvim = attach({ reader: socket, writer: socket });
35+
36+
// Close the transport
37+
const closePromise = nvim.close();
38+
39+
// Verify close promise resolves
40+
await expect(closePromise).resolves.toBeUndefined();
41+
});
3042
});
43+

packages/neovim/src/utils/transport.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { inspect } from 'node:util';
77

88
import { encode, decode, ExtensionCodec, decodeMultiStream } from '@msgpack/msgpack';
99
import { Metadata } from '../api/types';
10+
import { ASYNC_DISPOSE_SYMBOL } from './util';
1011

1112
export let exportsForTesting: any; // eslint-disable-line import/no-mutable-exports
1213
// .mocharc.js sets NODE_ENV=test.
@@ -88,7 +89,7 @@ class Transport extends EventEmitter {
8889
this.reader = reader;
8990
this.client = client;
9091

91-
this.reader.on('end', () => {
92+
this.reader.once('end', () => {
9293
this.emit('detach');
9394
});
9495

@@ -179,6 +180,25 @@ class Transport extends EventEmitter {
179180
this.writer.write(this.encodeToBuffer([1, 0, 'Invalid message type', null]));
180181
}
181182
}
183+
184+
/**
185+
* Close the transport.
186+
*
187+
* Ends the writer, the other end of the connection should close our reader which cleans up
188+
* remaining resources.
189+
*/
190+
async close(): Promise<void> {
191+
return new Promise((resolve) => {
192+
this.writer.end(resolve)
193+
});
194+
}
195+
196+
/**
197+
* @see close
198+
*/
199+
async [ASYNC_DISPOSE_SYMBOL](): Promise<void> {
200+
await this.close();
201+
}
182202
}
183203

184204
export { Transport };

packages/neovim/src/utils/util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ export function partialClone(
4141

4242
return clonedObj;
4343
}
44+
45+
/**
46+
* Polyfill for Symbol.asyncDispose if not available in the runtime.
47+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncDispose
48+
*/
49+
export const ASYNC_DISPOSE_SYMBOL = Symbol.asyncDispose ?? Symbol.for('Symbol.asyncDispose');

0 commit comments

Comments
 (0)