diff --git a/docs/api/interfaces/mongo-dispose-opts.md b/docs/api/interfaces/mongo-dispose-opts.md new file mode 100644 index 000000000..2b3e851bd --- /dev/null +++ b/docs/api/interfaces/mongo-dispose-opts.md @@ -0,0 +1,20 @@ +--- +id: mongo-memory-dispose-opts +title: 'DisposeOptions' +--- + +API Documentation of `DisposeOptions`-Interface + +## Values for `DisposeOptions` + +### enabled + +Typings: `enabled?: boolean` + +Set whether to stop the manager on `[Symbol.asyncDispose]` calls. + +### cleanup + +Typings: `cleanup?: Cleanup` + +Set custom cleanup options to be used for disposal, see [`cleanup` function](../classes/mongo-memory-server.md#cleanup) (the same options apply for the replset). diff --git a/docs/api/interfaces/mongo-memory-server-opts.md b/docs/api/interfaces/mongo-memory-server-opts.md index 28cd8de5e..0cfe8f81b 100644 --- a/docs/api/interfaces/mongo-memory-server-opts.md +++ b/docs/api/interfaces/mongo-memory-server-opts.md @@ -32,3 +32,9 @@ Set custom spawn options for spawning processes, uses [`SpawnOptions`](https://n Typings: `auth?: AutomaticAuth` Set custom Authentication options for the instance, uses [`AutomaticAuth`](./mongo-memory-server-automaticauth.md). + +### dispose + +Typings: `dispose?: DisposeOptions` + +Set custom behavior for when `[Symbol.asyncDispose]` is called, uses [`DisposeOptions`](./mongo-dispose-opts.md). diff --git a/docs/api/interfaces/replset-opts.md b/docs/api/interfaces/replset-opts.md index 665d0183f..27e7f57f9 100644 --- a/docs/api/interfaces/replset-opts.md +++ b/docs/api/interfaces/replset-opts.md @@ -91,3 +91,9 @@ Set how many ReplSet members to spawn, this number will be deducted from length :::tip It is recommended to set this number to a **odd** number, and try to never have it be **even**, see [MongoDB Deploy an Odd Number of Members](https://www.mongodb.com/docs/v5.2/core/replica-set-architectures/#deploy-an-odd-number-of-members). ::: + +### dispose + +Typings: `dispose?: DisposeOptions` + +Set custom behavior for when `[Symbol.asyncDispose]` is called, uses [`DisposeOptions`](./mongo-dispose-opts.md). diff --git a/docs/guides/faq.md b/docs/guides/faq.md index 883fc3512..d43648c2d 100644 --- a/docs/guides/faq.md +++ b/docs/guides/faq.md @@ -21,3 +21,14 @@ Since `8.4.0` objects can also be used instead of just booleans for parameter in For Example `.stop({ doCleanup: false })` can be used instead of `.stop(false)`. ::: + +### Does this package support Explicit Resource Management? + +Yes, `[Symbol.asyncDispose]` is implemented for all manager classes, behavior can be configured via `dispose` options: + +- [`MongoMemoryServerOpts.dispose`](../api/interfaces/mongo-memory-server-opts.md#dispose) +- [`ReplSetOpts.dispose`](../api/interfaces/replset-opts.md#dispose) + +:::note +Note that when using `await using server =` that `[Symbol.asyncDispose]` is called at the end of the scope even if the value is reassigned to something out of the current scope. +::: diff --git a/packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts b/packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts index ff0d6b6a2..345afd4fa 100644 --- a/packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts +++ b/packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts @@ -1,5 +1,10 @@ import { EventEmitter } from 'events'; -import { MongoMemoryServer, AutomaticAuth, MongoMemoryServerOpts } from './MongoMemoryServer'; +import { + MongoMemoryServer, + AutomaticAuth, + MongoMemoryServerOpts, + DisposeOptions, +} from './MongoMemoryServer'; import { assertion, authDefault, @@ -92,6 +97,10 @@ export interface ReplSetOpts { * @default {} */ configSettings?: MongoMemoryReplSetConfigSettings; + /** + * Options for automatic dispose for "Explicit Resource Management" + */ + dispose?: DisposeOptions; } /** @@ -264,6 +273,7 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced spawn: {}, storageEngine, configSettings: {}, + dispose: {}, }; // force overwrite "storageEngine" because it is transformed already this._replSetOpts = { ...defaults, ...val, storageEngine }; @@ -799,6 +809,13 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced log('_waitForPrimary: detected one primary instance '); } + + // Symbol for "Explicit Resource Management" + async [Symbol.asyncDispose]() { + if (this.replSetOpts.dispose?.enabled ?? true) { + await this.stop(this.replSetOpts.dispose?.cleanup); + } + } } export default MongoMemoryReplSet; diff --git a/packages/mongodb-memory-server-core/src/MongoMemoryServer.ts b/packages/mongodb-memory-server-core/src/MongoMemoryServer.ts index 148a636b9..9f9ad7d83 100644 --- a/packages/mongodb-memory-server-core/src/MongoMemoryServer.ts +++ b/packages/mongodb-memory-server-core/src/MongoMemoryServer.ts @@ -43,6 +43,27 @@ export interface MongoMemoryServerOpts { * Defining this enables automatic user creation */ auth?: AutomaticAuth; + /** + * Options for automatic dispose for "Explicit Resource Management" + */ + dispose?: DisposeOptions; +} + +/** + * Options to configure `Symbol.asyncDispose` behavior + */ +export interface DisposeOptions { + /** + * Set whether to run the dispose hook or not. + * Note that this only applies when `Symbol.asyncDispose` is actually called + * @default true + */ + enabled?: boolean; + /** + * Pass custom options for cleanup + * @default { doCleanup: true, force: false } + */ + cleanup?: Cleanup; } export interface AutomaticAuth { @@ -838,6 +859,13 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced { ? this.auth.enable : false; // if "this._replSetOpts.auth.enable" is not defined, default to false } + + // Symbol for "Explicit Resource Management" + async [Symbol.asyncDispose]() { + if (this.opts.dispose?.enabled ?? true) { + await this.stop(this.opts.dispose?.cleanup); + } + } } export default MongoMemoryServer; diff --git a/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryReplSet.test.ts b/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryReplSet.test.ts index 2ebc7a7ff..a4d7cbee5 100644 --- a/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryReplSet.test.ts +++ b/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryReplSet.test.ts @@ -442,6 +442,7 @@ describe('MongoMemoryReplSet', () => { spawn: {}, storageEngine: 'wiredTiger', configSettings: {}, + dispose: {}, }); replSet.replSetOpts = { auth: { enable: true } }; // @ts-expect-error because "_replSetOpts" is protected @@ -457,6 +458,7 @@ describe('MongoMemoryReplSet', () => { spawn: {}, storageEngine: 'wiredTiger', configSettings: {}, + dispose: {}, }); }); @@ -802,4 +804,81 @@ describe('MongoMemoryReplSet', () => { await server.stop(); }); }); + + describe('asyncDispose', () => { + it('should work by default', async () => { + jest.spyOn(MongoMemoryReplSet.prototype, 'start'); + jest.spyOn(MongoMemoryReplSet.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryReplSet.create(); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped); + expect(outer.servers.length).toStrictEqual(0); + expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(1); + // expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + }); + + it('should be able to be disabled', async () => { + jest.spyOn(MongoMemoryReplSet.prototype, 'start'); + jest.spyOn(MongoMemoryReplSet.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryReplSet.create({ + replSet: { dispose: { enabled: false } }, + }); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.running); + expect(outer.servers.length).toStrictEqual(1); + expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(0); + // expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + await outer.stop(); + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped); + }); + + it('should be able to set custom cleanup', async () => { + jest.spyOn(MongoMemoryReplSet.prototype, 'start'); + jest.spyOn(MongoMemoryReplSet.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryReplSet.create({ + replSet: { + dispose: { cleanup: { doCleanup: false } }, + }, + }); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped); + expect(outer.servers.length).toStrictEqual(1); + expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(1); + // expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + await outer.cleanup({ doCleanup: true }); + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped); + expect(outer.servers.length).toStrictEqual(0); + }); + }); }); diff --git a/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryServer.test.ts b/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryServer.test.ts index 0a95f75ab..22266ed24 100644 --- a/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryServer.test.ts +++ b/packages/mongodb-memory-server-core/src/__tests__/MongoMemoryServer.test.ts @@ -1188,4 +1188,73 @@ describe('MongoMemoryServer', () => { await server.stop(); }); }); + + describe('asyncDispose', () => { + it('should work by default', async () => { + jest.spyOn(MongoMemoryServer.prototype, 'start'); + jest.spyOn(MongoMemoryServer.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryServer.create(); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryServerStates.new); + expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(1); + // expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + }); + + it('should be able to be disabled', async () => { + jest.spyOn(MongoMemoryServer.prototype, 'start'); + jest.spyOn(MongoMemoryServer.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryServer.create({ dispose: { enabled: false } }); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + expect(outer.state).toStrictEqual(MongoMemoryServerStates.running); + expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(0); + // expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + await outer.stop(); + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryServerStates.new); + }); + + it('should be able to set custom cleanup', async () => { + jest.spyOn(MongoMemoryServer.prototype, 'start'); + jest.spyOn(MongoMemoryServer.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose); + { + await using server = await MongoMemoryServer.create({ + dispose: { cleanup: { doCleanup: false } }, + }); + // use the value and test that it actually runs, as "getUri" will throw is not in "running" state + server.getUri(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryServerStates.stopped); + expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1); + expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(1); + // expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + await outer.cleanup({ doCleanup: true }); + // not "stopped" because of cleanup + expect(outer.state).toStrictEqual(MongoMemoryServerStates.new); + }); + }); }); diff --git a/packages/mongodb-memory-server-core/src/util/MongoInstance.ts b/packages/mongodb-memory-server-core/src/util/MongoInstance.ts index 17c55bedd..27d954096 100644 --- a/packages/mongodb-memory-server-core/src/util/MongoInstance.ts +++ b/packages/mongodb-memory-server-core/src/util/MongoInstance.ts @@ -725,6 +725,11 @@ export class MongoInstance extends EventEmitter implements ManagerBase { ); } } + + /// Symbol for "Explicit Resource Management" + async [Symbol.asyncDispose]() { + await this.stop(); + } } export default MongoInstance; diff --git a/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts b/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts index e7dbc4c4f..68c9c9f36 100644 --- a/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts +++ b/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts @@ -780,4 +780,28 @@ describe('MongodbInstance', () => { await mongod.stop(); } }); + + describe('asyncDispose', () => { + it('should work', async () => { + jest.spyOn(MongodbInstance.prototype, 'start'); + jest.spyOn(MongodbInstance.prototype, 'stop'); + let outer; + // would like to test this, but jest seemingly does not support spying on symbols + // jest.spyOn(MongodbInstance.prototype, Symbol.asyncDispose); + { + const gotPort = await getFreePort(27333); + await using server = await MongodbInstance.create({ + instance: { port: gotPort, dbPath: tmpDir }, + binary: { version }, + }); + expect(server.mongodProcess).toBeTruthy(); + // reassignment still calls dispose at the *current* scope + outer = server; + } + expect(outer.mongodProcess).not.toBeTruthy(); + expect(MongodbInstance.prototype.start).toHaveBeenCalledTimes(1); + expect(MongodbInstance.prototype.stop).toHaveBeenCalledTimes(1); + // expect(MongodbInstance.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/mongodb-memory-server-core/src/util/resolveConfig.ts b/packages/mongodb-memory-server-core/src/util/resolveConfig.ts index 9ee31e62a..49b309fa3 100644 --- a/packages/mongodb-memory-server-core/src/util/resolveConfig.ts +++ b/packages/mongodb-memory-server-core/src/util/resolveConfig.ts @@ -5,6 +5,12 @@ import * as path from 'path'; import { readFileSync } from 'fs'; import { isNullOrUndefined } from './utils'; +// polyfills +// @ts-expect-error they are marked "read-only", but are set-able if not implemented by the runtime +Symbol.dispose ??= Symbol('Symbol.dispose'); +// @ts-expect-error they are marked "read-only", but are set-able if not implemented by the runtime +Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose'); + const log = debug('MongoMS:ResolveConfig'); /** Enum of all possible config options */ diff --git a/packages/mongodb-memory-server-core/src/util/utils.ts b/packages/mongodb-memory-server-core/src/util/utils.ts index c936428f2..71eb67d65 100644 --- a/packages/mongodb-memory-server-core/src/util/utils.ts +++ b/packages/mongodb-memory-server-core/src/util/utils.ts @@ -268,6 +268,8 @@ export abstract class ManagerBase { abstract start(forceSamePort: boolean): Promise; abstract start(): Promise; abstract stop(cleanup: Cleanup): Promise; + // Symbol for "Explicit Resource Management" + abstract [Symbol.asyncDispose](): Promise; } /** diff --git a/website/sidebars.js b/website/sidebars.js index 8b4d0a2e4..de3327ed0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -17,6 +17,7 @@ module.exports = { 'api/interfaces/mongo-memory-instance-replicamemberconfig', 'api/interfaces/mongo-memory-binary-opts', 'api/interfaces/mongo-memory-replset-opts', + 'api/interfaces/mongo-memory-dispose-opts', 'api/interfaces/replset-opts', ], },