Skip to content

Commit 6ae7ff5

Browse files
comments
1 parent 412829f commit 6ae7ff5

File tree

7 files changed

+253
-10
lines changed

7 files changed

+253
-10
lines changed

src/change_stream.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,10 @@ export class ChangeStream<
551551
extends TypedEventEmitter<ChangeStreamEvents<TSchema, TChange>>
552552
implements AsyncDisposable
553553
{
554-
/** @beta */
554+
/**
555+
* @beta
556+
* @experimental
557+
*/
555558
declare [Symbol.asyncDispose]: () => Promise<void>;
556559
/** @internal */
557560
async asyncDispose() {

src/cursor/abstract_cursor.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,10 @@ export abstract class AbstractCursor<
279279
return !!this.cursorClient.topology?.loadBalanced;
280280
}
281281

282-
/** @beta */
282+
/**
283+
* @beta
284+
* @experimental
285+
*/
283286
declare [Symbol.asyncDispose]: () => Promise<void>;
284287
/** @internal */
285288
async asyncDispose() {

src/mongo_client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,10 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
405405
this.checkForNonGenuineHosts();
406406
}
407407

408-
/** @beta */
408+
/**
409+
* @beta
410+
* @experimental
411+
*/
409412
declare [Symbol.asyncDispose]: () => Promise<void>;
410413
/** @internal */
411414
async asyncDispose() {

src/resource_management.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
* @public
33
*/
44
export interface AsyncDisposable {
5-
/** @beta */
5+
/**
6+
* @beta
7+
* @experimental
8+
*/
69
[Symbol.asyncDispose](): Promise<void>;
710

811
/**
@@ -28,6 +31,7 @@ export function configureResourceManagement(target: AsyncDisposable) {
2831

2932
/**
3033
* @beta
34+
* @experimental
3135
*
3236
* Attaches `Symbol.asyncDispose` methods to the MongoClient, Cursors, sessions and change streams
3337
* if Symbol.asyncDispose is defined.

src/sessions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ export class ClientSession
290290
maybeClearPinnedConnection(this, { force: true, ...options });
291291
}
292292
}
293-
/** @beta */
293+
/**
294+
* @beta
295+
* @experimental
296+
*/
294297
declare [Symbol.asyncDispose]: () => Promise<void>;
295298
/** @internal */
296299
async asyncDispose() {

test/explicit-resource-management/main.test.ts

Lines changed: 231 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { describe, it } from 'mocha';
33
import { GridFSBucket, MongoClient } from 'mongodb/lib/beta';
44
import { Readable } from 'stream';
55
import { pipeline } from 'stream/promises';
6+
import { expect } from 'chai';
67
import { setTimeout } from 'timers/promises';
8+
import { createReadStream } from 'fs';
9+
import { join } from 'path';
710

811
async function setUpCollection(client: MongoClient) {
912
const collection = client.db('foo').collection<{ name: string }>('bar');
@@ -17,8 +20,31 @@ async function setUpCollection(client: MongoClient) {
1720
describe('explicit resource management feature integration tests', function () {
1821
describe('MongoClient', function () {
1922
it('does not crash or error when used with await-using syntax', async function () {
20-
await using client = new MongoClient(process.env.MONGODB_URI!);
21-
await client.connect();
23+
await using client = new MongoClient(process.env.MONGODB_URI!);
24+
await client.connect();
25+
})
26+
27+
it('always cleans up the client, regardless of thrown errors', async function () {
28+
const error = await (async () => {
29+
await using client = new MongoClient(process.env.MONGODB_URI!);
30+
await client.connect();
31+
32+
throw new Error('error thrown');
33+
})().catch(e => e);
34+
35+
expect(error).to.match(/error thrown/);
36+
});
37+
38+
it('works if client is explicitly closed', async function () {
39+
const expected = await (async () => {
40+
await using client = new MongoClient(process.env.MONGODB_URI!);
41+
await client.connect();
42+
await client.close();
43+
44+
return 'not error';
45+
})();
46+
47+
expect(expected).to.equal('not error');
2248
})
2349
})
2450

@@ -33,15 +59,82 @@ describe('explicit resource management feature integration tests', function () {
3359
await cursor.next();
3460
})
3561

36-
describe('cursor streams', function() {
37-
it('does not crash or error when used with await-using syntax', async function() {
62+
it('always cleans up the cursor, regardless of thrown errors', async function () {
63+
const error = await (async () => {
64+
await using client = new MongoClient(process.env.MONGODB_URI!);
65+
await client.connect();
66+
67+
const collection = await setUpCollection(client);
68+
69+
await using cursor = collection.find();
70+
await cursor.next();
71+
72+
throw new Error('error thrown');
73+
})().catch(e => e);
74+
75+
expect(error).to.match(/error thrown/);
76+
});
77+
78+
it('works if cursor is explicitly closed', async function () {
79+
const expected = await (async () => {
80+
await using client = new MongoClient(process.env.MONGODB_URI!);
81+
await client.connect();
82+
83+
const collection = await setUpCollection(client);
84+
85+
await using cursor = collection.find();
86+
await cursor.next();
87+
88+
await cursor.close();
89+
90+
return 'not error';
91+
})();
92+
93+
expect(expected).to.equal('not error');
94+
})
95+
96+
describe('cursor streams', function () {
97+
it('does not crash or error when used with await-using syntax', async function () {
3898
await using client = new MongoClient(process.env.MONGODB_URI!);
3999
await client.connect();
40100

41101
const collection = await setUpCollection(client);
42102

43103
await using readable = collection.find().stream();
44104
})
105+
106+
it('always cleans up the stream, regardless of thrown errors', async function () {
107+
const error = await (async () => {
108+
await using client = new MongoClient(process.env.MONGODB_URI!);
109+
await client.connect();
110+
111+
const collection = await setUpCollection(client);
112+
113+
await using readable = collection.find().stream();
114+
115+
throw new Error('error thrown');
116+
})().catch(e => e);
117+
118+
expect(error).to.match(/error thrown/);
119+
});
120+
121+
it('works if stream is explicitly closed', async function () {
122+
const expected = await (async () => {
123+
await using client = new MongoClient(process.env.MONGODB_URI!);
124+
await client.connect();
125+
126+
const collection = await setUpCollection(client);
127+
128+
await using readable = collection.find().stream();
129+
130+
readable.destroy();
131+
132+
return 'not error';
133+
})();
134+
135+
expect(expected).to.equal('not error');
136+
})
137+
45138
})
46139
})
47140

@@ -52,6 +145,34 @@ describe('explicit resource management feature integration tests', function () {
52145

53146
await using session = client.startSession();
54147
})
148+
149+
it('always cleans up the session, regardless of thrown errors', async function () {
150+
const error = await (async () => {
151+
await using client = new MongoClient(process.env.MONGODB_URI!);
152+
await client.connect();
153+
154+
await using session = client.startSession();
155+
156+
throw new Error('error thrown');
157+
})().catch(e => e);
158+
159+
expect(error).to.match(/error thrown/);
160+
});
161+
162+
it('works if session is explicitly closed', async function () {
163+
const expected = await (async () => {
164+
await using client = new MongoClient(process.env.MONGODB_URI!);
165+
await client.connect();
166+
167+
await using session = client.startSession();
168+
169+
await session.endSession();
170+
171+
return 'not error';
172+
})();
173+
174+
expect(expected).to.equal('not error');
175+
})
55176
})
56177

57178
describe('ChangeStreams', function () {
@@ -65,6 +186,41 @@ describe('explicit resource management feature integration tests', function () {
65186
setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' }));
66187
await cs.next();
67188
})
189+
190+
it('always cleans up the change stream, regardless of thrown errors', async function () {
191+
const error = await (async () => {
192+
await using client = new MongoClient(process.env.MONGODB_URI!);
193+
await client.connect();
194+
195+
const collection = await setUpCollection(client);
196+
await using cs = collection.watch();
197+
198+
setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' }));
199+
await cs.next();
200+
201+
throw new Error('error thrown');
202+
})().catch(e => e);
203+
204+
expect(error).to.match(/error thrown/);
205+
});
206+
207+
it('works if change stream is explicitly closed', async function () {
208+
const expected = await (async () => {
209+
await using client = new MongoClient(process.env.MONGODB_URI!);
210+
await client.connect();
211+
212+
const collection = await setUpCollection(client);
213+
await using cs = collection.watch();
214+
215+
setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' }));
216+
await cs.next();
217+
await cs.close();
218+
219+
return 'not error';
220+
})();
221+
222+
expect(expected).to.equal('not error');
223+
})
68224
});
69225

70226
describe('GridFSDownloadStream', function () {
@@ -78,5 +234,76 @@ describe('explicit resource management feature integration tests', function () {
78234

79235
await using downloadStream = bucket.openDownloadStreamByName('foo.txt');
80236
})
237+
238+
it('always cleans up the stream, regardless of thrown errors', async function () {
239+
const error = await (async () => {
240+
await using client = new MongoClient(process.env.MONGODB_URI!);
241+
await client.connect();
242+
243+
const bucket = new GridFSBucket(client.db('foo'));
244+
const uploadStream = bucket.openUploadStream('foo.txt')
245+
await pipeline(Readable.from("AAAAAAA".split('')), uploadStream);
246+
247+
await using downloadStream = bucket.openDownloadStreamByName('foo.txt');
248+
249+
throw new Error('error thrown');
250+
})().catch(e => e);
251+
252+
expect(error).to.match(/error thrown/);
253+
});
254+
255+
it('works if stream is explicitly closed', async function () {
256+
const expected = await (async () => {
257+
await using client = new MongoClient(process.env.MONGODB_URI!);
258+
await client.connect();
259+
260+
const bucket = new GridFSBucket(client.db('foo'));
261+
const uploadStream = bucket.openUploadStream('foo.txt')
262+
await pipeline(Readable.from("AAAAAAA".split('')), uploadStream);
263+
264+
await using downloadStream = bucket.openDownloadStreamByName('foo.txt');
265+
266+
await downloadStream.abort();
267+
268+
return 'not error';
269+
})();
270+
271+
expect(expected).to.equal('not error');
272+
})
273+
274+
it('throws premature close error if explicitly destroyed early', async function () {
275+
// Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from
276+
// Nodejs' readable implementation. This behavior matches the behavior for other readable streams
277+
// (see the below test).
278+
const expected = await (async () => {
279+
await using client = new MongoClient(process.env.MONGODB_URI!);
280+
await client.connect();
281+
282+
const bucket = new GridFSBucket(client.db('foo'));
283+
const uploadStream = bucket.openUploadStream('foo.txt')
284+
await pipeline(Readable.from("AAAAAAA".split('')), uploadStream);
285+
286+
await using downloadStream = bucket.openDownloadStreamByName('foo.txt');
287+
288+
downloadStream.destroy();
289+
290+
return 'not error';
291+
})().catch(e => e);
292+
293+
expect(expected).to.match(/Premature close/);
294+
})
295+
296+
it('throws premature close error if explicitly destroyed early (builtin stream)', async function () {
297+
// Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from
298+
// Nodejs' readable implementation. This behavior matches the behavior for other readable streams (ie - ReadFileStream)
299+
const expected = await (async () => {
300+
await using readStream = createReadStream(join(__dirname, 'main.test.ts'));
301+
readStream.destroy();
302+
303+
return 'not error';
304+
})().catch(e => e);
305+
306+
expect(expected).to.match(/Premature close/);
307+
})
81308
});
82309
})

test/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Below is a summary of the types of test automation in this repo.
3232
| TypeScript Definition | `/test/types` | The TypeScript definition tests verify the type definitions are correct. | `npm run check:tsd` |
3333
| GitHub Actions | `/test/action` | Tests that run as GitHub Actions such as dependency checking. | Currently, only `npm run check:dependencies` but could be expanded to more in the future. |
3434
| Code Examples | `/test/integration/node-specific/examples` | Code examples that are also paired with tests that show they are working examples. | Currently, `npm run check:lambda` to test the AWS Lambda example with default auth and `npm run check:lambda:aws` to test the AWS Lambda example with AWS auth. |
35-
| Explicit Resource Management | `/test/integration/explicit-resource-management` | Tests that use explicit resource management with the driver's disposable resources. | `bash .evergreen/run-resource-management-feature-integration.sh` |
35+
| Explicit Resource Management | `/test/explicit-resource-management` | Tests that use explicit resource management with the driver's disposable resources. | `bash .evergreen/run-resource-management-feature-integration.sh` |
3636

3737
### Spec Tests
3838

0 commit comments

Comments
 (0)