diff --git a/.watchmanconfig b/.watchmanconfig index 4a519b118a..a29cb1875f 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -3,6 +3,8 @@ "react-native/node_modules", "packages/realm-web", "packages/realm-web-integration-tests", - ".wireit" + ".wireit", + "packages/realm/.wireit", + "integration-tests/environments/react-native-test-app/.wireit" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9643464e..585e7f3c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,8 @@ realm.syncSession?.addProgressNotification( * File format: generates Realms with format v24 (reads and upgrades file format v10). ### Internal - - - +* Adding a CallInvoker-based scheduler for Core on React Native and removing the "flush ui queue" workaround. ([#6791](https://github.com/realm/realm-js/pull/6791)) +* Refactors throwing uncaught exceptions from callbacks dispatched onto the event loop from C++ on React Native. ([#6772](https://github.com/realm/realm-js/issues/6772)) ## 12.11.1 (2024-06-25) diff --git a/integration-tests/environments/react-native-test-app/ios/Podfile.lock b/integration-tests/environments/react-native-test-app/ios/Podfile.lock index 3f1ec42c0a..0139d93405 100644 --- a/integration-tests/environments/react-native-test-app/ios/Podfile.lock +++ b/integration-tests/environments/react-native-test-app/ios/Podfile.lock @@ -1449,7 +1449,7 @@ SPEC CHECKSUMS: ReactNativeHost: fd9d65f6ee7947f468537d35b3fefe8b2bf546da ReactTestApp-DevSupport: 33a7ae9bf22161f359629a9607f5ef27e9c91fd0 ReactTestApp-Resources: d200e68756fa45c648f369210bd7ee0c14759f5a - RealmJS: b7ed9bcfaac0946479403b684d158d922f41384e + RealmJS: 298cc4766e917bd415fe0cfbfd1f4b198ab812f4 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: ae3c32c514802d30f687a04a6a35b348506d411f diff --git a/package-lock.json b/package-lock.json index a662b53275..72edaa3489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -305,6 +305,7 @@ "version": "0.74.83", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.74.83.tgz", "integrity": "sha512-GgvgHS3Aa2J8/mp1uC/zU8HuTh8ZT5jz7a4mVMWPw7+rGyv70Ba8uOVBq6UH2Q08o617IATYc+0HfyzAfm4n0w==", + "dev": true, "dependencies": { "@babel/parser": "^7.20.0", "glob": "^7.1.1", @@ -321,174 +322,11 @@ "@babel/preset-env": "^7.1.6" } }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/community-cli-plugin": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.74.84.tgz", - "integrity": "sha512-GBKE+1sUh86fS2XXV46gMCNHMc1KetshMbYJ0AhDhldpaILZHqRBX50mdVsiYVvkzp4QjM0nmYqefuJ9NVwicQ==", - "dependencies": { - "@react-native-community/cli-server-api": "13.6.8", - "@react-native-community/cli-tools": "13.6.8", - "@react-native/dev-middleware": "0.74.84", - "@react-native/metro-babel-transformer": "0.74.84", - "chalk": "^4.0.0", - "execa": "^5.1.1", - "metro": "^0.80.3", - "metro-config": "^0.80.3", - "metro-core": "^0.80.3", - "node-fetch": "^2.2.0", - "querystring": "^0.2.1", - "readline": "^1.3.0" - }, - "engines": { - "node": ">=18" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/community-cli-plugin/node_modules/@react-native/babel-plugin-codegen": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.74.84.tgz", - "integrity": "sha512-UR4uiii5szIJA84mSC6GJOfYKDq7/ThyetOQT62+BBcyGeHVtHlNLNRzgaMeLqIQaT8Fq4pccMI+7QqLOMXzdw==", - "dependencies": { - "@react-native/codegen": "0.74.84" - }, - "engines": { - "node": ">=18" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/community-cli-plugin/node_modules/@react-native/babel-preset": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.74.84.tgz", - "integrity": "sha512-WUfu6Y4aGuVdocQZvx33BJiQWFH6kRCHYbZfBn2psgFrSRLgQWEQrDCxqPFObNAVSayM0rNhp2FvI5K/Eyeqlg==", - "dependencies": { - "@babel/core": "^7.20.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.18.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", - "@babel/plugin-proposal-numeric-separator": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.20.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-export-default-from": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.18.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-syntax-optional-chaining": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.20.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.20.0", - "@babel/plugin-transform-flow-strip-types": "^7.20.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.5.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "@babel/template": "^7.0.0", - "@react-native/babel-plugin-codegen": "0.74.84", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/community-cli-plugin/node_modules/@react-native/codegen": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.74.84.tgz", - "integrity": "sha512-0hXlnu9i0o8v+gXKQi+x6T471L85kCDwW4WrJiYAeOheWrQdNNW6rC3g8+LL7HXAf7QcHGU/8/d57iYfdVK2BQ==", - "dependencies": { - "@babel/parser": "^7.20.0", - "glob": "^7.1.1", - "hermes-parser": "0.19.1", - "invariant": "^2.2.4", - "jscodeshift": "^0.14.0", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/community-cli-plugin/node_modules/@react-native/metro-babel-transformer": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.74.84.tgz", - "integrity": "sha512-YtVGq7jkgyUECv5yt4BOFbOXyW4ddUn8+dnwGGpJKdfhXYL5o5++AxNdE+2x+SZdkj3JUVekGKPwRabFECABaw==", - "dependencies": { - "@babel/core": "^7.20.0", - "@react-native/babel-preset": "0.74.84", - "hermes-parser": "0.19.1", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/dev-middleware": { - "version": "0.74.84", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.74.84.tgz", - "integrity": "sha512-veYw/WmyrAOQHUiIeULzn2duJQnXDPiKq2jZ/lcmDo6jsLirpp+Q73lx09TYgy/oVoPRuV0nfmU3x9B6EV/7qQ==", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.74.84", - "@rnx-kit/chromium-edge-launcher": "^1.0.0", - "chrome-launcher": "^0.15.2", - "connect": "^3.6.5", - "debug": "^2.2.0", - "node-fetch": "^2.2.0", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "selfsigned": "^2.4.1", - "serve-static": "^1.13.1", - "temp-dir": "^2.0.0", - "ws": "^6.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "integration-tests/environments/react-native-test-app/node_modules/@react-native/dev-middleware/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "integration-tests/environments/react-native-test-app/node_modules/@react-native/js-polyfills": { "version": "0.74.83", "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.74.83.tgz", "integrity": "sha512-/t74n8r6wFhw4JEoOj3bN71N1NDLqaawB75uKAsSjeCwIR9AfCxlzZG0etsXtOexkY9KMeZIQ7YwRPqUdNXuqw==", + "dev": true, "engines": { "node": ">=18" } @@ -531,6 +369,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -549,12 +388,14 @@ "integration-tests/environments/react-native-test-app/node_modules/hermes-estree": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.19.1.tgz", - "integrity": "sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==" + "integrity": "sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==", + "dev": true }, "integration-tests/environments/react-native-test-app/node_modules/hermes-parser": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.19.1.tgz", "integrity": "sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==", + "dev": true, "dependencies": { "hermes-estree": "0.19.1" } @@ -563,6 +404,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -578,14 +420,6 @@ "node": ">= 8" } }, - "integration-tests/environments/react-native-test-app/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "integration-tests/tests": { "name": "@realm/integration-tests", "version": "0.1.0", @@ -4808,14 +4642,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/@react-native-community/cli-debugger-ui": { - "version": "13.6.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-13.6.6.tgz", - "integrity": "sha512-Vv9u6eS4vKSDAvdhA0OiQHoA7y39fiPIgJ6biT32tN4avHDtxlc6TWZGiqv7g98SBvDWvoVAmdPLcRf3kU+c8g==", - "dependencies": { - "serve-static": "^1.13.1" - } - }, "node_modules/@react-native-community/cli-platform-apple": { "version": "13.6.8", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-13.6.8.tgz", @@ -4858,96 +4684,6 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-server-api": { - "version": "13.6.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-13.6.6.tgz", - "integrity": "sha512-ZtCXxoFlM7oDv3iZ3wsrT3SamhtUJuIkX2WePLPlN5bcbq7zimbPm2lHyicNJtpcGQ5ymsgpUWPCNZsWQhXBqQ==", - "dependencies": { - "@react-native-community/cli-debugger-ui": "13.6.6", - "@react-native-community/cli-tools": "13.6.6", - "compression": "^1.7.1", - "connect": "^3.6.5", - "errorhandler": "^1.5.1", - "nocache": "^3.0.1", - "pretty-format": "^26.6.2", - "serve-static": "^1.13.1", - "ws": "^6.2.2" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/@react-native-community/cli-tools": { - "version": "13.6.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-13.6.6.tgz", - "integrity": "sha512-ptOnn4AJczY5njvbdK91k4hcYazDnGtEPrqIwEI+k/CTBHNdb27Rsm2OZ7ye6f7otLBqF8gj/hK6QzJs8CEMgw==", - "dependencies": { - "appdirsjs": "^1.2.4", - "chalk": "^4.1.2", - "execa": "^5.0.0", - "find-up": "^5.0.0", - "mime": "^2.4.1", - "node-fetch": "^2.6.0", - "open": "^6.2.0", - "ora": "^5.4.1", - "semver": "^7.5.2", - "shell-quote": "^1.7.3", - "sudo-prompt": "^9.0.0" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@react-native/babel-plugin-codegen": { "version": "0.73.2", "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.73.2.tgz", @@ -5080,14 +4816,6 @@ "node": "*" } }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.74.83", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.74.83.tgz", - "integrity": "sha512-RGQlVUegBRxAUF9c1ss1ssaHZh6CO+7awgtI9sDeU0PzDZY/40ImoPD5m0o0SI6nXoVzbPtcMGzU+VO590pRfA==", - "engines": { - "node": ">=18" - } - }, "node_modules/@react-native/eslint-config": { "version": "0.74.83", "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.74.83.tgz", diff --git a/packages/realm/bindgen/src/realm_js_jsi_helpers.h b/packages/realm/bindgen/src/realm_js_jsi_helpers.h index a107bdf55c..275a2c9137 100644 --- a/packages/realm/bindgen/src/realm_js_jsi_helpers.h +++ b/packages/realm/bindgen/src/realm_js_jsi_helpers.h @@ -4,18 +4,9 @@ #include #include -#include - namespace realm::js::JSI { namespace { -struct FlushMicrotaskQueueGuard { - ~FlushMicrotaskQueueGuard() - { - realm::js::flush_ui_workaround::flush_ui_queue(); - } -}; - namespace jsi = facebook::jsi; template struct HostRefWrapper : jsi::HostObject { diff --git a/packages/realm/bindgen/src/templates/jsi.ts b/packages/realm/bindgen/src/templates/jsi.ts index c0f462ff18..3b37eba700 100644 --- a/packages/realm/bindgen/src/templates/jsi.ts +++ b/packages/realm/bindgen/src/templates/jsi.ts @@ -652,8 +652,6 @@ function convertFromJsi(addon: JsiAddon, type: Type, expr: string): string { { _thread.assertOnSameThread(); auto& _env = ${addon.get()}->m_rt; - // TODO consider not flushing when calling back into JS from withing a JS->CPP call. - FlushMicrotaskQueueGuard guard; return ${c( type.ret, `_cb->call( diff --git a/packages/realm/binding/android/CMakeLists.txt b/packages/realm/binding/android/CMakeLists.txt index c64f2660f6..e637afec0d 100644 --- a/packages/realm/binding/android/CMakeLists.txt +++ b/packages/realm/binding/android/CMakeLists.txt @@ -13,7 +13,7 @@ endif() add_library(realm-js-android-binding SHARED ../jsi/jsi_init.cpp - ../jsi/flush_ui_queue_workaround.cpp + ../jsi/react_scheduler.cpp src/main/cpp/platform.cpp src/main/cpp/jni_utils.cpp src/main/cpp/io_realm_react_RealmReactModule.cpp diff --git a/packages/realm/binding/android/src/main/cpp/io_realm_react_RealmReactModule.cpp b/packages/realm/binding/android/src/main/cpp/io_realm_react_RealmReactModule.cpp index 71a3b31aab..a0220a61c9 100644 --- a/packages/realm/binding/android/src/main/cpp/io_realm_react_RealmReactModule.cpp +++ b/packages/realm/binding/android/src/main/cpp/io_realm_react_RealmReactModule.cpp @@ -24,7 +24,7 @@ #include #include "jsi_init.h" -#include "flush_ui_queue_workaround.h" +#include "react_scheduler.h" #include "platform.hpp" #include "jni_utils.hpp" @@ -108,8 +108,8 @@ extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_setDefaul realm::JsPlatformHelpers::default_realm_file_directory().c_str()); } -extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_injectCallInvoker(JNIEnv* env, jobject thiz, - jobject call_invoker) +extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_createScheduler(JNIEnv* env, jobject thiz, + jobject call_invoker) { // React Native uses the fbjni library for handling JNI, which has the concept of "hybrid objects", // which are Java objects containing a pointer to a C++ object. The CallInvokerHolder, which has the @@ -119,6 +119,9 @@ extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_injectCal // 1. Get the Java object referred to by the mHybridData field of the Java holder object auto callInvokerHolderClass = env->FindClass("com/facebook/react/turbomodule/core/CallInvokerHolderImpl"); + if (!env->IsInstanceOf(call_invoker, callInvokerHolderClass)) { + throw std::invalid_argument("Expected call_invoker to be CallInvokerHolderImpl"); + } auto hybridDataField = env->GetFieldID(callInvokerHolderClass, "mHybridData", "Lcom/facebook/jni/HybridData;"); auto hybridDataObj = env->GetObjectField(call_invoker, hybridDataField); @@ -136,14 +139,16 @@ extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_injectCal // 4. Cast the mNativePointer back to its C++ type auto nativePointer = reinterpret_cast(nativePointerValue); - // 5. Inject the JS call invoker for the workaround to use - realm::js::flush_ui_workaround::inject_js_call_invoker(nativePointer->getCallInvoker()); + // 5. Create the scheduler from the JS call invoker + __android_log_print(ANDROID_LOG_VERBOSE, "Realm", "Creating scheduler"); + realm::js::react_scheduler::create_scheduler(nativePointer->getCallInvoker()); } extern "C" JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_invalidateCaches(JNIEnv* env, jobject thiz) { - // Disable the flush ui workaround - realm::js::flush_ui_workaround::reset_js_call_invoker(); + __android_log_print(ANDROID_LOG_VERBOSE, "Realm", "Resetting scheduler"); + // Reset the scheduler to prevent invocations using an old runtime + realm::js::react_scheduler::reset_scheduler(); __android_log_print(ANDROID_LOG_VERBOSE, "Realm", "Invalidating caches"); #if DEBUG // Immediately close any open sync sessions to prevent race condition with new diff --git a/packages/realm/binding/android/src/main/java/io/realm/react/RealmReactModule.java b/packages/realm/binding/android/src/main/java/io/realm/react/RealmReactModule.java index 3f122083d6..7700e34405 100644 --- a/packages/realm/binding/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/packages/realm/binding/android/src/main/java/io/realm/react/RealmReactModule.java @@ -77,7 +77,7 @@ public boolean injectModuleIntoJSGlobal() { } CallInvokerHolder jsCallInvokerHolder = reactContext.getCatalystInstance().getJSCallInvokerHolder(); - injectCallInvoker(jsCallInvokerHolder); + createScheduler(jsCallInvokerHolder); // Get the javascript runtime and inject our native module with it JavaScriptContextHolder jsContext = reactContext.getJavaScriptContextHolder(); @@ -103,11 +103,9 @@ public boolean injectModuleIntoJSGlobal() { private native void injectModuleIntoJSGlobal(long runtimePointer); /** - * Passes the React Native jsCallInvokerHolder over to C++ so we can setup our UI queue flushing. - * This is needed as a workaround for https://github.com/facebook/react-native/issues/33006 - * where we call the invokeAsync method to flush the React Native UI queue whenever we call from C++ to JS. + * Passes the React Native jsCallInvokerHolder over to C++ so we can setup a scheduler. */ - private native void injectCallInvoker(CallInvokerHolder callInvoker); + private native void createScheduler(CallInvokerHolder callInvoker); private native void invalidateCaches(); } diff --git a/packages/realm/binding/apple/RealmReactModule.mm b/packages/realm/binding/apple/RealmReactModule.mm index b68ecb1fa2..f2fe199c17 100644 --- a/packages/realm/binding/apple/RealmReactModule.mm +++ b/packages/realm/binding/apple/RealmReactModule.mm @@ -31,7 +31,7 @@ #import #import "jsi/jsi_init.h" -#import "flush_ui_queue_workaround.h" +#import "jsi/react_scheduler.h" // the part of the RCTCxxBridge private class we care about @interface RCTBridge (Realm_RCTCxxBridge) @@ -54,7 +54,8 @@ - (dispatch_queue_t)methodQueue { } - (void)invalidate { - realm::js::flush_ui_workaround::reset_js_call_invoker(); + // Reset the scheduler to prevent invocations using an old runtime + realm::js::react_scheduler::reset_scheduler(); #if DEBUG // Immediately close any open sync sessions to prevent race condition with new // JS thread when hot reloading @@ -74,12 +75,8 @@ - (void)dealloc { RCTBridge* bridge = [RCTBridge currentBridge]; auto &rt = *static_cast(bridge.runtime); - // Since https://github.com/facebook/react-native/pull/43396 this should only be needed when bridgeless is not enabled. - // but unfortunately, that doesn't seem to be the case. - // See https://github.com/facebook/react-native/pull/43396#issuecomment-2178586017 for context - // If it was, we could use the enablement of "microtasks" to avoid the overhead of calling the invokeAsync on every call from C++ into JS. - // if (!facebook::react::ReactNativeFeatureFlags::enableMicrotasks()) { - realm::js::flush_ui_workaround::inject_js_call_invoker([bridge jsCallInvoker]); + // Create a scheduler wrapping the call invoker and pass this to realm core + realm::js::react_scheduler::create_scheduler([bridge jsCallInvoker]); auto exports = jsi::Object(rt); realm_jsi_init(rt, exports); diff --git a/packages/realm/binding/jsi/flush_ui_queue_workaround.cpp b/packages/realm/binding/jsi/flush_ui_queue_workaround.cpp deleted file mode 100644 index 4ff1261f7c..0000000000 --- a/packages/realm/binding/jsi/flush_ui_queue_workaround.cpp +++ /dev/null @@ -1,62 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// 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. -// -//////////////////////////////////////////////////////////////////////////// - -#include "flush_ui_queue_workaround.h" - -#include - -namespace realm::js::flush_ui_workaround { -// Calls are debounced using the waiting_for_ui_flush bool, so if an async flush is already -// pending when another JS to C++ call happens, we don't call invokeAsync again. This works -// because the work is performed before the microtask queue is flushed - see sequence -// diagram at https://bit.ly/3kexhHm. It might be possible to further optimize this, -// e.g. only flush the queue a maximum of once per frame, but this seems reasonable. -bool waiting_for_ui_flush = false; -std::shared_ptr js_invoker_{}; - -void inject_js_call_invoker(std::shared_ptr js_invoker) -{ - js_invoker_ = js_invoker; -} - -void reset_js_call_invoker() -{ - // TODO: Consider taking an invoker as an argument and only resetting if it matches js_invoker_ - // This would ensure the pointer isn't resetting from a subsequent assignment from the constructor - js_invoker_.reset(); -} - -void flush_ui_queue() -{ - if (js_invoker_ && !waiting_for_ui_flush) { - waiting_for_ui_flush = true; - // Calling invokeAsync tells React Native to execute the lambda passed - // in on the JS thread, and then flush the internal "microtask queue", which has the - // effect of flushing any pending UI updates. - // - // We call this after we have called into JS from C++, in order to ensure that the RN - // UI updates in response to any changes from Realm. We need to do this as we bypass - // the usual RN bridge mechanism for communicating between C++ and JS, so without doing - // this RN has no way to know that a change has occurred which might require an update - // (see #4389, facebook/react-native#33006). - js_invoker_->invokeAsync([&] { - waiting_for_ui_flush = false; - }); - } -} -} // namespace realm::js::flush_ui_workaround diff --git a/packages/realm/binding/jsi/react_scheduler.cpp b/packages/realm/binding/jsi/react_scheduler.cpp new file mode 100644 index 0000000000..6b0e359f30 --- /dev/null +++ b/packages/realm/binding/jsi/react_scheduler.cpp @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +#include "react_scheduler.h" + +#include + +#include +#include + +#include + +using Scheduler = realm::util::Scheduler; + +namespace { + +std::shared_ptr scheduler_{}; + +class ReactScheduler : public realm::util::Scheduler { +public: + ReactScheduler(std::shared_ptr js_call_invoker) + : m_js_call_invoker(js_call_invoker) + { + } + + bool is_on_thread() const noexcept override + { + return m_id == std::this_thread::get_id(); + } + + bool is_same_as(const Scheduler* other) const noexcept override + { + auto o = dynamic_cast(other); + return (o && (o->m_js_call_invoker == m_js_call_invoker)); + } + + bool can_invoke() const noexcept override + { + return true; + } + + void invoke(realm::util::UniqueFunction&& func) override + { + m_js_call_invoker->invokeAsync( + // Using normal priority to avoid blocking rendering, user-input and other higher priority tasks + facebook::react::SchedulerPriority::NormalPriority, [ptr = func.release()] { + (realm::util::UniqueFunction(ptr))(); + }); + } + +private: + std::shared_ptr m_js_call_invoker; + std::thread::id m_id = std::this_thread::get_id(); +}; + +std::shared_ptr get_scheduler() +{ + if (scheduler_) { + REALM_ASSERT(scheduler_->is_on_thread()); + return scheduler_; + } + else { + return Scheduler::make_platform_default(); + } +} + +} // namespace + +namespace realm::js::react_scheduler { + + +void create_scheduler(std::shared_ptr js_call_invoker) +{ + scheduler_ = std::make_shared(js_call_invoker); + Scheduler::set_default_factory(get_scheduler); +} + +void reset_scheduler() +{ + scheduler_.reset(); +} + +} // namespace realm::js::react_scheduler diff --git a/packages/realm/binding/jsi/flush_ui_queue_workaround.h b/packages/realm/binding/jsi/react_scheduler.h similarity index 77% rename from packages/realm/binding/jsi/flush_ui_queue_workaround.h rename to packages/realm/binding/jsi/react_scheduler.h index 5889a58b44..4555bb57e6 100644 --- a/packages/realm/binding/jsi/flush_ui_queue_workaround.h +++ b/packages/realm/binding/jsi/react_scheduler.h @@ -20,8 +20,7 @@ #include -namespace realm::js::flush_ui_workaround { -void inject_js_call_invoker(std::shared_ptr js_invoker); -void reset_js_call_invoker(); -void flush_ui_queue(); -} // namespace realm::js::flush_ui_workaround +namespace realm::js::react_scheduler { +void create_scheduler(std::shared_ptr js_invoker); +void reset_scheduler(); +} // namespace realm::js::react_scheduler