diff --git a/CHANGELOG.md b/CHANGELOG.md index c2255350f93..80a1382ccd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Improved performance of RQL queries on a non-linked string property using `>`, `>=`, `<`, `<=` operators and fixed behavior that a null string should be evaluated as less than everything, previously nulls were not matched. ([realm/realm-core#3939](https://github.com/realm/realm-core/issues/3939)) * Added support for using aggregate operations on Mixed properties in queries. ([realm/realm-core#7398](https://github.com/realm/realm-core/pull/7398)) * Improved file compaction performance on platforms with page sizes greater than 4k (for example arm64 Apple platforms) for files less than 256 pages in size. ([realm/realm-core#7492](https://github.com/realm/realm-core/pull/7492)) +* Added a static `Realm.shutdown()` method, which closes all Realms, cancels all pending `Realm.open` calls, clears internal caches, resets the logger and collects garbage. Call this method to free up the event loop and allow Node.js to perform a graceful exit. ([#6571](https://github.com/realm/realm-js/pull/6571), since v12.0.0) ### Fixed * Aligned Dictionaries to Lists and Sets when they get cleared. ([#6205](https://github.com/realm/realm-core/issues/6205), since v10.3.0-rc.1) diff --git a/integration-tests/tests/src/node/clean-exit.ts b/integration-tests/tests/src/node/clean-exit.ts index 95ad5d0a28f..d520815b92a 100644 --- a/integration-tests/tests/src/node/clean-exit.ts +++ b/integration-tests/tests/src/node/clean-exit.ts @@ -16,16 +16,49 @@ // //////////////////////////////////////////////////////////////////////////// -import { execSync } from "child_process"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { assert } from "chai"; +import module from "node:module"; -describe("Clean exit for Node.js scripts", function () { +const require = module.createRequire(import.meta.url); +const realmPackagePath = require.resolve("realm"); +assert(realmPackagePath, "Expected to resolve Realm package"); + +function expectCleanExit(source: string, timeout: number) { + execFileSync(process.execPath, ["--eval", source], { + timeout, + cwd: fs.mkdtempSync(path.join(os.tmpdir(), "realm-exit-test-")), + stdio: "inherit", + env: { + REALM_PACKAGE_PATH: realmPackagePath, + }, + }); +} + +describe("clean exits in Node.js", function () { // Repro for https://github.com/realm/realm-js/issues/4535 - currently still failing - it.skip("exits cleanly when creating a new Realm.App", function (this: RealmContext) { - execSync( - `node -e 'const Realm = require("realm"); const app = new Realm.App({ id: "myapp-abcde" }); Realm.clearTestState();'`, - { - timeout: Math.min(this.timeout(), 5000), - }, + it("exits when creating Realm", function (this: RealmContext) { + expectCleanExit( + ` + const Realm = require(process.env.REALM_PACKAGE_PATH); + const realm = new Realm(); + Realm.shutdown(); + `, + Math.min(this.timeout(), 5000), + ); + }); + + it("exits when creating Realm.App", function (this: RealmContext) { + expectCleanExit( + ` + const Realm = require(process.env.REALM_PACKAGE_PATH); + new Realm.App({ id: "myapp-abcde" }); + Realm.shutdown(); + `, + Math.min(this.timeout(), 5000), ); }); }); diff --git a/integration-tests/tests/src/setup-globals.ts b/integration-tests/tests/src/setup-globals.ts index 780a5efb155..246c9ef75d9 100644 --- a/integration-tests/tests/src/setup-globals.ts +++ b/integration-tests/tests/src/setup-globals.ts @@ -74,3 +74,7 @@ const { defaultLogLevel = "off" } = environment; Realm.setLogLevel(defaultLogLevel); Realm.flags.THROW_ON_GLOBAL_REALM = true; + +after(() => { + Realm.shutdown(); +}); diff --git a/packages/realm/bindgen/vendor/realm-core b/packages/realm/bindgen/vendor/realm-core index 374dd672af3..cd53bc85008 160000 --- a/packages/realm/bindgen/vendor/realm-core +++ b/packages/realm/bindgen/vendor/realm-core @@ -1 +1 @@ -Subproject commit 374dd672af357732dccc135fecc905406fec3223 +Subproject commit cd53bc85008b7dc963363e3b11862c0db90fe8f0 diff --git a/packages/realm/src/ProgressRealmPromise.ts b/packages/realm/src/ProgressRealmPromise.ts index 3d58faf5ebb..2164013b133 100644 --- a/packages/realm/src/ProgressRealmPromise.ts +++ b/packages/realm/src/ProgressRealmPromise.ts @@ -69,7 +69,6 @@ export class ProgressRealmPromise implements Promise { * @internal */ public static cancelAll() { - assert(flags.ALLOW_CLEAR_TEST_STATE, "Set the flags.ALLOW_CLEAR_TEST_STATE = true before calling this."); for (const promiseRef of ProgressRealmPromise.instances) { promiseRef.deref()?.cancel(); } diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index b0d047247e9..f66e5d7373c 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -59,6 +59,7 @@ import { flags, fromBindingRealmSchema, fs, + garbageCollection, normalizeObjectSchema, normalizeRealmSchema, toArrayBuffer, @@ -175,12 +176,10 @@ export class Realm { } /** - * Clears the state by closing and deleting any Realm in the default directory and logout all users. - * NOTE: Not a part of the public API and it's primarily used from the library's tests. - * @private + * Closes all Realms, cancels all pending {@link Realm.open} calls, clears internal caches, resets the logger and collects garbage. + * Call this method to free up the event loop and allow Node.js to perform a graceful exit. */ - public static clearTestState(): void { - assert(flags.ALLOW_CLEAR_TEST_STATE, "Set the flags.ALLOW_CLEAR_TEST_STATE = true before calling this."); + public static shutdown() { // Close any realms not already closed for (const realmRef of Realm.internals) { const realm = realmRef.deref(); @@ -193,6 +192,18 @@ export class Realm { binding.App.clearCachedApps(); ProgressRealmPromise.cancelAll(); + binding.Logger.setDefaultLogger(null); + garbageCollection.collect(); + } + + /** + * Clears the state by closing and deleting any Realm in the default directory and logout all users. + * NOTE: Not a part of the public API and it's primarily used from the library's tests. + * @private + */ + public static clearTestState(): void { + assert(flags.ALLOW_CLEAR_TEST_STATE, "Set the flags.ALLOW_CLEAR_TEST_STATE = true before calling this."); + Realm.shutdown(); // Delete all Realm files in the default directory const defaultDirectoryPath = fs.getDefaultDirectoryPath(); fs.removeRealmFilesFromDirectory(defaultDirectoryPath); diff --git a/packages/realm/src/platform.ts b/packages/realm/src/platform.ts index 92c3f1377bd..ed6de8360c3 100644 --- a/packages/realm/src/platform.ts +++ b/packages/realm/src/platform.ts @@ -26,3 +26,5 @@ export { fs } from "./platform/file-system"; export { network } from "./platform/network"; /** @internal */ export { syncProxyConfig } from "./platform/sync-proxy-config"; +/** @internal */ +export { garbageCollection } from "./platform/garbage-collection"; diff --git a/packages/realm/src/platform/garbage-collection.ts b/packages/realm/src/platform/garbage-collection.ts new file mode 100644 index 00000000000..c849ffeb423 --- /dev/null +++ b/packages/realm/src/platform/garbage-collection.ts @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +type ShutdownType = { collect: () => void }; + +export const garbageCollection: ShutdownType = { + collect() {}, +}; + +export function inject(value: ShutdownType) { + Object.freeze(Object.assign(garbageCollection, value)); +} diff --git a/packages/realm/src/platform/node/garbage-collection.ts b/packages/realm/src/platform/node/garbage-collection.ts new file mode 100644 index 00000000000..403c81ba083 --- /dev/null +++ b/packages/realm/src/platform/node/garbage-collection.ts @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import v8 from "node:v8"; +import vm from "node:vm"; + +import { inject } from "../garbage-collection"; + +inject({ + collect() { + // Ensure we have the gc function available + v8.setFlagsFromString("--expose_gc"); + const gc = vm.runInNewContext("gc"); + // Garbage collect + process.nextTick(gc); + }, +}); diff --git a/packages/realm/src/platform/node/index.ts b/packages/realm/src/platform/node/index.ts index 1ff57402ff0..ecaf4eca957 100644 --- a/packages/realm/src/platform/node/index.ts +++ b/packages/realm/src/platform/node/index.ts @@ -21,6 +21,7 @@ import "./fs"; import "./device-info"; import "./sync-proxy-config"; import "./custom-inspect"; +import "./garbage-collection"; import { Realm } from "../../Realm"; export = Realm;