diff --git a/README.md b/README.md index feb4739..7107a71 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The app exercises: - Threaded-runtime list surfaces with FlashList and LegendList. - Whole-screen threaded rendering for chat-style flows. - Shared state across runtimes through `@react-native-runtimes/state`. -- Runtime prewarming, headless tasks, and a two-runtime architecture example. +- Runtime prewarming, scheduled runtime functions, and a two-runtime architecture example. ## Running diff --git a/bun.lock b/bun.lock index 7e1078c..17813cd 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "NativeComposeChat", @@ -48,6 +49,9 @@ "packages/core": { "name": "@react-native-runtimes/core", "version": "0.0.1", + "devDependencies": { + "nitrogen": "^0.35.7", + }, "peerDependencies": { "react": "*", "react-native": "*", @@ -804,7 +808,7 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], @@ -888,7 +892,7 @@ "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -1620,7 +1624,7 @@ "string-natural-compare": ["string-natural-compare@3.0.1", "", {}, "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1748,7 +1752,7 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1762,9 +1766,9 @@ "yaml": ["yaml@2.9.0", "", { "bin": "bin.mjs" }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1840,12 +1844,18 @@ "@react-native-harness/jest/jest-util": ["jest-util@30.4.1", "", { "dependencies": { "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.3" } }, "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw=="], + "@react-native-harness/jest/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@react-native-harness/platform-android/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@react-native-harness/platform-apple/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@react-native-harness/platform-apple/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@react-native-harness/runtime/event-target-shim": ["event-target-shim@6.0.2", "", {}, "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA=="], + "@react-native/codegen/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], "@ts-morph/common/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -1874,6 +1884,8 @@ "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1912,6 +1924,8 @@ "jest-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-cli/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "jest-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1954,12 +1968,12 @@ "metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], + "metro/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "nitrogen/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], - "node-exports-info/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1970,6 +1984,8 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-native/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1984,11 +2000,15 @@ "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -2026,6 +2046,24 @@ "@react-native-harness/jest/jest-util/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "@react-native-harness/jest/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@react-native-harness/jest/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@react-native-harness/jest/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "@react-native-harness/platform-apple/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@react-native-harness/platform-apple/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@react-native-harness/platform-apple/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "@react-native/codegen/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@react-native/codegen/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@react-native/codegen/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], @@ -2038,6 +2076,8 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -2058,6 +2098,12 @@ "jest-cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "jest-cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jest-cli/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "jest-cli/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "jest-config/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2092,6 +2138,8 @@ "logkitty/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "logkitty/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "logkitty/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], "logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], @@ -2100,22 +2148,32 @@ "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - "nitrogen/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "metro/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "nitrogen/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "metro/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "nitrogen/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "metro/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "react-native/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "react-native/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "react-native/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.85.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@react-native/babel-plugin-codegen": "0.85.3", "babel-plugin-syntax-hermes-parser": "0.33.3", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw=="], @@ -2130,27 +2188,45 @@ "@react-native-harness/jest/jest-util/@jest/types/@jest/schemas": ["@jest/schemas@30.4.1", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q=="], + "@react-native-harness/jest/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@react-native-harness/jest/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@react-native-harness/platform-apple/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@react-native-harness/platform-apple/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@react-native/codegen/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@react-native/codegen/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "errorhandler/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "jest-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jest-cli/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "logkitty/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "logkitty/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "nitrogen/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "metro/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "nitrogen/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "metro/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "nitrogen/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "nitrogen/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "react-native/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "react-native/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -2170,20 +2246,40 @@ "@react-native-harness/jest/jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], - "logkitty/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@react-native-harness/jest/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@react-native-harness/platform-apple/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "nitrogen/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@react-native/codegen/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "nitrogen/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "jest-cli/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "nitrogen/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "logkitty/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "metro/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "react-native/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.85.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.29.0", "hermes-parser": "0.33.3", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "tinyglobby": "^0.2.15", "yargs": "^17.6.2" } }, "sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA=="], "logkitty/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@react-native-harness/bundler-metro/@react-native/metro-config/@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], } } diff --git a/example/__tests__/cross-runtime.harness.ts b/example/__tests__/cross-runtime.harness.ts index a42e0fb..489726b 100644 --- a/example/__tests__/cross-runtime.harness.ts +++ b/example/__tests__/cross-runtime.harness.ts @@ -12,8 +12,8 @@ // // What we observed when the suite was enabled (Android, dev mode, harness on // emulator): -// - The main runtime registers `ThreadedRuntimeFunctionRunner` / -// `ThreadedRuntimeHeadlessTaskRunner` as callable modules when +// - The main runtime registers `ThreadedRuntimeFunctionRunner` as a callable +// module when // `@react-native-runtimes/core` is imported (top of ThreadedRuntime.tsx). // - The worker runtime's JS bundle is loaded but the app's user-bundle code // (`__r(0)` -> index.js -> .threaded-runtime/entry.js -> @react-native-runtimes/core) diff --git a/example/__tests__/runtime-function.harness.ts b/example/__tests__/runtime-function.harness.ts index 3ff586c..115d65c 100644 --- a/example/__tests__/runtime-function.harness.ts +++ b/example/__tests__/runtime-function.harness.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'react-native-harness'; -import { call, runtimeFunction } from '@react-native-runtimes/core'; +import { call, runtimeFunction, schedule } from '@react-native-runtimes/core'; describe('runtimeFunction()', () => { it('returns a callable that invokes the wrapped fn in-place', () => { @@ -12,6 +12,11 @@ describe('runtimeFunction()', () => { expect(typeof wrapped.runOn).toBe('function'); }); + it('attaches a scheduleOn method to the returned function', () => { + const wrapped = runtimeFunction(() => {}); + expect(typeof wrapped.scheduleOn).toBe('function'); + }); + it('does not attach __runtimeFunction metadata when no id is provided', () => { const wrapped = runtimeFunction(() => 1); expect(wrapped.__runtimeFunction).toBeUndefined(); @@ -41,6 +46,11 @@ describe('runtimeFunction.withId() / .named()', () => { expect(typeof fn.runOn).toBe('function'); }); + it('still exposes scheduleOn on the constructed function', () => { + const fn = runtimeFunction.withId('test/withId.scheduleOn', () => {}); + expect(typeof fn.scheduleOn).toBe('function'); + }); + it('treats withId(fn) with empty id as still annotated', () => { // Empty id is falsy in attachRuntimeFunction, so no metadata is attached. const fn = runtimeFunction.withId('', () => 0); @@ -61,3 +71,17 @@ describe('call(fn).on(runtime)', () => { expect(typeof invoker).toBe('function'); }); }); + +describe('schedule(fn).on(runtime)', () => { + it('returns a builder with an .on(runtimeName) method', () => { + const fn = runtimeFunction(() => {}); + const builder = schedule(fn); + expect(typeof builder.on).toBe('function'); + }); + + it('.on(runtime) returns an invoker function', () => { + const fn = runtimeFunction(() => {}); + const invoker = schedule(fn).on('any-runtime'); + expect(typeof invoker).toBe('function'); + }); +}); diff --git a/example/android/app/src/main/java/com/nativecomposechat/MainApplication.kt b/example/android/app/src/main/java/com/nativecomposechat/MainApplication.kt index 72d843c..7d38ea5 100644 --- a/example/android/app/src/main/java/com/nativecomposechat/MainApplication.kt +++ b/example/android/app/src/main/java/com/nativecomposechat/MainApplication.kt @@ -51,7 +51,7 @@ class MainApplication : Application(), ReactApplication { applicationContext, "two-runtimes-business-runtime", ) - ThreadedRuntime.dispatchHeadlessTask( + ThreadedRuntime.schedule( applicationContext, "two-runtimes-business-runtime", "twoRuntimes:startBusinessRuntime", diff --git a/example/ios/NativeComposeChat.xcodeproj/project.pbxproj b/example/ios/NativeComposeChat.xcodeproj/project.pbxproj index 6f390a0..a09ee41 100644 --- a/example/ios/NativeComposeChat.xcodeproj/project.pbxproj +++ b/example/ios/NativeComposeChat.xcodeproj/project.pbxproj @@ -393,7 +393,7 @@ "-DFOLLY_HAVE_CLOCK_GETTIME=1", "-DRCT_REMOVE_LEGACY_ARCH=1", ); - PODFILE_DIR = "/Users/riteshshukla/Desktop/development/opensource/react-native-runtimes/example/ios"; + PODFILE_DIR = "/Users/szymon.chmal/Projects/react-native-runtimes/example/ios"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -468,7 +468,7 @@ "-DFOLLY_HAVE_CLOCK_GETTIME=1", "-DRCT_REMOVE_LEGACY_ARCH=1", ); - PODFILE_DIR = "/Users/riteshshukla/Desktop/development/opensource/react-native-runtimes/example/ios"; + PODFILE_DIR = "/Users/szymon.chmal/Projects/react-native-runtimes/example/ios"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ENABLE_EXPLICIT_MODULES = NO; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5cfbc3f..7a47ba7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2261,7 +2261,7 @@ SPEC CHECKSUMS: Ease: 91720d5a28047201547bdcd0747db99a27e01fa7 FBLazyVector: 9266e314e3d76052e236e5a089812482de2aa92e HarnessUI: 7f27d320d7f2ac3fde60ffff25f0baedb40aac4c - hermes-engine: 3c82959cae1a915edfe4922eb2e7a6093bafef22 + hermes-engine: 9e9c9fb1815b9cd60bf21030fb500d9067dadcba NativeComposeThreadedRuntime: 4b8350496d42f24047077b5b4204a7d1ab3050c5 NativeComposeThreadedZustand: f06b87ae6025c9d7b8ae10d3ea606b4b08854f75 NitroModules: d9e08ab81a7b5f6d16acbbc6ea6091e45ce34a9f @@ -2273,7 +2273,7 @@ SPEC CHECKSUMS: React: 01173c5d780d9bbfe022f6347d90de9a9917c980 React-callinvoker: 0e8cf2535617aefe9649d77ced059084a8ed371c React-Core: 7c73a86183b3adbe000a5642dca4ecf3c7180562 - React-Core-prebuilt: a73df132b7e1c1fb55447b7794150bcbeb0d4af2 + React-Core-prebuilt: ab3d0e8e358d63d7719fb8170bc014ca1c34dda6 React-CoreModules: 2cdef98304cbd163282170161bd292714e35b24a React-cxxreact: 375824e8216b8209538fdc3d5725b036779e3a86 React-debug: 73ce5e5d53291fab2b76a24e121595853cc56b4f @@ -2337,7 +2337,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 43c8afa3c7a753e9ae311c5f05885175378ae682 ReactCodegen: a65ae5b76c474ca9091fc6f0daf77252e75d6e79 ReactCommon: d0213c6af9f4383453edb61862105a504ce2d0fd - ReactNativeDependencies: 0a6f0cf7e0238dea3f2933dbb2613590143e7296 + ReactNativeDependencies: 6270afb21f2634b8f40643b427a797d4ba88a841 Yoga: 6876206cf24ad7b7625bbbc9c7fccb4c2ca8a174 PODFILE CHECKSUM: 5e017a3ef51cf1fa8f57fe3231df084e671c0783 diff --git a/example/src/examples/twoRuntimesArchitecture.ts b/example/src/examples/twoRuntimesArchitecture.ts index bad791e..8ac451d 100644 --- a/example/src/examples/twoRuntimesArchitecture.ts +++ b/example/src/examples/twoRuntimesArchitecture.ts @@ -1,7 +1,7 @@ import { ThreadedRuntime, - registerThreadedHeadlessTask, runtimeFunction, + schedule, usingRuntime, } from '@react-native-runtimes/core'; import { createSharedStore } from '@react-native-runtimes/state'; @@ -40,8 +40,8 @@ type TwoRuntimeBusinessTaskPayload = { export const TWO_RUNTIMES_BUSINESS_RUNTIME_NAME = 'two-runtimes-business-runtime'; -const TWO_RUNTIMES_BUSINESS_TASK = 'twoRuntimes:startBusinessRuntime'; -const TWO_RUNTIMES_SYNC_TASK = 'twoRuntimes:syncNow'; +export const TWO_RUNTIMES_START_BUSINESS_FUNCTION = + 'twoRuntimes:startBusinessRuntime'; const initialBusinessStatus: TwoRuntimeBusinessStatus = { bootedAt: null, @@ -160,9 +160,9 @@ async function publishBusinessSnapshot( let businessLoop: ReturnType | null = null; -registerThreadedHeadlessTask( - TWO_RUNTIMES_BUSINESS_TASK, - async ({ payload }) => { +export const startTwoRuntimeBusinessLoop = runtimeFunction.named( + TWO_RUNTIMES_START_BUSINESS_FUNCTION, + async (payload: TwoRuntimeBusinessTaskPayload) => { await twoRuntimeArchitectureStore.hydrate(); if (businessLoop) { @@ -181,14 +181,6 @@ registerThreadedHeadlessTask( }, ); -registerThreadedHeadlessTask( - TWO_RUNTIMES_SYNC_TASK, - async ({ payload }) => { - await twoRuntimeArchitectureStore.hydrate(); - await publishBusinessSnapshot(payload.command ?? 'manual sync', payload); - }, -); - export const syncTwoRuntimeBusinessSnapshot = runtimeFunction( async (payload: TwoRuntimeBusinessTaskPayload) => { await twoRuntimeArchitectureStore.hydrate(); @@ -218,12 +210,11 @@ export async function startTwoRuntimeBusinessRuntime(startedBy: string) { await ThreadedRuntime.prewarmBusinessRuntime( TWO_RUNTIMES_BUSINESS_RUNTIME_NAME, ); - await ThreadedRuntime.runHeadlessTask(TWO_RUNTIMES_BUSINESS_TASK, { - runtimeName: TWO_RUNTIMES_BUSINESS_RUNTIME_NAME, - payload: { - enqueuedAt: Date.now(), - startedBy, - }, + await schedule(startTwoRuntimeBusinessLoop).on( + TWO_RUNTIMES_BUSINESS_RUNTIME_NAME, + )({ + enqueuedAt: Date.now(), + startedBy, }); } diff --git a/packages/core/README.md b/packages/core/README.md index 0776beb..f5bc81d 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -13,13 +13,13 @@ The package owns the JS registry and host API: `@react-native-runtimes/core/metro` - `registerLazyThreadedComponent(name, loadComponent)` - `registerThreadedComponent(name, Component)` -- `registerThreadedHeadlessTask(name, task)` - `runtimeFunction(fn)` - `call(runtimeFunction).on(runtimeName)(...args)` +- `schedule(runtimeFunction).on(runtimeName)(...args)` - `usingRuntime(runtimeName).run(() => runtimeFunctionCall(...))` - `ThreadedReactSurface` - `ThreadedRuntimeHost` -- `ThreadedRuntime.prewarm/preload/runHeadlessTask/run/destroy/destroyAll/getRuntimeNames` +- `ThreadedRuntime.prewarm/preload/call/schedule/destroy/destroyAll/getRuntimeNames` ## Setup @@ -28,18 +28,18 @@ The package owns the JS registry and host API: Add the Metro wrapper from this package to your app's `metro.config.js`: ```js -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); -const { withThreadedRuntime } = require('@react-native-runtimes/core/metro'); +const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config"); +const { withThreadedRuntime } = require("@react-native-runtimes/core/metro"); const config = {}; module.exports = withThreadedRuntime( mergeConfig(getDefaultConfig(__dirname), config), { - roots: ['App.tsx', 'src'], - generatedDir: '.threaded-runtime', - generatedEntry: 'entry.js', - }, + roots: ["App.tsx", "src"], + generatedDir: ".threaded-runtime", + generatedEntry: "entry.js", + } ); ``` @@ -60,7 +60,7 @@ Load the generated entry only in the secondary runtime path: ```js if (global.__THREADED_RUNTIME_ENV__ || global._is_it_a_list_env === true) { - require('./.threaded-runtime/entry'); + require("./.threaded-runtime/entry"); } ``` @@ -68,18 +68,18 @@ The generated entry registers lazy component loaders and the `ThreadedRuntimeHost` root: ```js -import { AppRegistry } from 'react-native'; +import { AppRegistry } from "react-native"; import { ThreadedRuntimeHost, registerLazyThreadedComponent, -} from '@react-native-runtimes/core'; +} from "@react-native-runtimes/core"; registerLazyThreadedComponent( - 'MessageList', - () => require('../src/MessageList').MessageList, + "MessageList", + () => require("../src/MessageList").MessageList ); -AppRegistry.registerComponent('ThreadedRuntimeHost', () => ThreadedRuntimeHost); +AppRegistry.registerComponent("ThreadedRuntimeHost", () => ThreadedRuntimeHost); ``` You can split runtime-only startup code into root files: @@ -107,7 +107,7 @@ import { OnRuntime, ThreadedScreen, threadedComponent, -} from '@react-native-runtimes/core'; +} from "@react-native-runtimes/core"; type MessageListProps = { conversationId: string; @@ -137,10 +137,10 @@ runtime, use `ThreadedScreen`: ```tsx export const ConversationScreen = threadedComponent( - 'ConversationScreen', + "ConversationScreen", function ConversationScreen(props) { return ; - }, + } ); ('hydrateConversation', async ({ payload, runtimeName }) => { - const messages = await loadMessages(payload.conversationId, payload.limit); - await messagesStore.setSubtreeState(payload.conversationId, messages, true); - console.info(`Hydrated ${payload.conversationId} on ${runtimeName}`); -}); +import { runtimeFunction, schedule } from "@react-native-runtimes/core"; +import { messagesStore } from "./messagesStore"; + +export const hydrateConversation = runtimeFunction.named( + "messages.hydrateConversation", + async (payload: { conversationId: string; limit: number }) => { + const messages = await loadMessages(payload.conversationId, payload.limit); + await messagesStore.setSubtreeState(payload.conversationId, messages, true); + } +); ``` -Dispatch it from the main runtime: +Schedule it from the main runtime: ```tsx -import { ThreadedRuntime } from '@react-native-runtimes/core'; - -await ThreadedRuntime.runHeadlessTask('hydrateConversation', { - runtimeName: 'conversation-worker-runtime', - payload: { - conversationId, - limit: 50, - }, +await schedule(hydrateConversation).on("conversation-worker-runtime")({ + conversationId, + limit: 50, }); ``` -`runHeadlessTask` starts or reuses the named runtime and asks that runtime to -invoke the registered task. If the runtime is still starting, native queues the -task and flushes it when that runtime is ready. The returned promise resolves -when native accepts the dispatch; it does not wait for the async task body to -finish. Pass durable output through shared native state, storage, or native -modules. +`schedule(...)` starts or reuses the named runtime and asks that runtime to +invoke the registered function. If the runtime is still starting, native queues +the function and flushes it when that runtime is ready. The returned promise +resolves when native accepts the scheduled work; it does not wait for the async +function body to finish. Pass durable output through shared native state, +storage, or native modules. -Headless tasks are useful for: +Scheduled runtime functions are useful for: - warming shared stores before a threaded screen opens - fetching or decoding data away from the main JS runtime @@ -223,7 +219,7 @@ Headless tasks are useful for: - keeping a runtime hot without attaching a `Threaded` surface If you only need to make startup faster, `ThreadedRuntime.prewarm(runtimeName)` -is still enough. Use `runHeadlessTask` when you need actual JS work to execute. +is still enough. Use `schedule(...)` when you need actual JS work to execute. ## Await Runtime Functions @@ -240,7 +236,7 @@ body: ```tsx async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; } @@ -252,14 +248,14 @@ function with a scheduled alias: ```tsx export const sum_ = runtimeFunction.withId( - 'src/math.sum_', + "src/math.sum_", async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; - }, + } ); -const sum = call(sum_).on('background'); +const sum = call(sum_).on("background"); const result = await sum(5, 1); ``` @@ -275,7 +271,7 @@ When the caller should choose the runtime, export a runtime function and schedul it with `call(fn).on(runtimeName)(...args)`: ```tsx -import { call, runtimeFunction } from '@react-native-runtimes/core'; +import { call, runtimeFunction } from "@react-native-runtimes/core"; function fibonacciNumber(n: number) { if (n < 2) { @@ -295,14 +291,14 @@ export const fibonacci = runtimeFunction((n: number) => { }; }); -const result = await call(fibonacci).on('fibonacci-worker-runtime')(38); +const result = await call(fibonacci).on("fibonacci-worker-runtime")(38); ``` The `call(fn).on(runtimeName)(...args)` form is compile-time syntax. The Metro transformer rewrites it before the app runs: ```tsx -await fibonacci.runOn('fibonacci-worker-runtime', 38); +await fibonacci.runOn("fibonacci-worker-runtime", 38); ``` ### Function Directive Details @@ -313,12 +309,12 @@ the first statement in the function body: ```tsx async function refreshCache(key: string) { - 'background'; + "background"; await cacheStore.hydrate(); return cacheStore.get(key); } -const value = await refreshCache('settings'); +const value = await refreshCache("settings"); ``` That source keeps call sites ordinary while still scheduling the work on the @@ -327,15 +323,15 @@ the original function with a scheduled alias: ```tsx export const refreshCache_ = runtimeFunction.withId( - 'src/cache.refreshCache_', + "src/cache.refreshCache_", async function refreshCache(key: string) { - 'background'; + "background"; await cacheStore.hydrate(); return cacheStore.get(key); - }, + } ); -const refreshCache = call(refreshCache_).on('background'); +const refreshCache = call(refreshCache_).on("background"); ``` Prefer this shortcut for fixed-runtime helpers. Prefer @@ -355,8 +351,8 @@ export name, then generates a registration that looks like this: ```tsx registerRuntimeFunction( - 'src/examples/fibonacciRuntimeFunction.fibonacci', - () => require('./src/examples/fibonacciRuntimeFunction').fibonacci, + "src/examples/fibonacciRuntimeFunction.fibonacci", + () => require("./src/examples/fibonacciRuntimeFunction").fibonacci ); ``` @@ -369,14 +365,14 @@ The `call(...).on(...)` helper accepts one exported runtime function and forward the arguments to that function on the target runtime: ```tsx -await call(fibonacci).on('fibonacci-worker-runtime')(38); +await call(fibonacci).on("fibonacci-worker-runtime")(38); ``` For a fixed-runtime helper, use a top-level function directive: ```tsx async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; } @@ -386,9 +382,9 @@ await sum(5, 1); The callback form is still supported when you prefer the runtime-first shape: ```tsx -import { usingRuntime } from '@react-native-runtimes/core'; +import { usingRuntime } from "@react-native-runtimes/core"; -await usingRuntime('fibonacci-worker-runtime').run(() => fibonacci(38)); +await usingRuntime("fibonacci-worker-runtime").run(() => fibonacci(38)); ``` For explicit stable ids, use `runtimeFunction.named` or @@ -396,10 +392,10 @@ For explicit stable ids, use `runtimeFunction.named` or ```tsx export const fibonacci = runtimeFunction.named( - 'examples.fibonacci', + "examples.fibonacci", (n: number) => { return fibonacciNumber(n); - }, + } ); ``` @@ -415,23 +411,23 @@ Current constraints: before calling them - synchronous functions avoid the extra Promise hop on the target runtime -## Native Headless Dispatch +## Native Schedule -Native code can dispatch the same registered headless tasks. The caller chooses -which named runtime handles the task. If that runtime has been prewarmed but is -not ready yet, the dispatch is queued and flushed after startup. If it has not -been created yet, native creates and starts it. +Native code can schedule the same registered runtime functions by id. The caller +chooses which named runtime handles the function. If that runtime has been +prewarmed but is not ready yet, the function is queued and flushed after +startup. If it has not been created yet, native creates and starts it. Kotlin: ```kotlin import com.nativecompose.threadedruntime.ThreadedRuntime -ThreadedRuntime.dispatchHeadlessTask( +ThreadedRuntime.schedule( context = applicationContext, runtimeName = "conversation-worker-runtime", - taskName = "hydrateConversation", - payloadJson = """{"conversationId":"inbox","limit":50}""", + functionId = "messages.hydrateConversation", + argsJson = """[{"conversationId":"inbox","limit":50}]""", ) ``` @@ -440,10 +436,10 @@ Swift: ```swift import NativeComposeThreadedRuntime -ThreadedRuntime.dispatchHeadlessTask( +ThreadedRuntime.schedule( withRuntimeName: "conversation-worker-runtime", - taskName: "hydrateConversation", - payloadJson: #"{"conversationId":"inbox","limit":50}"# + functionId: "messages.hydrateConversation", + argsJson: #"[{"conversationId":"inbox","limit":50}]"# ) ``` @@ -452,12 +448,12 @@ C++ on Android: ```cpp #include -nativecompose::threadedruntime::dispatchHeadlessTask( +nativecompose::threadedruntime::schedule( env, applicationContext, "conversation-worker-runtime", - "hydrateConversation", - R"({"conversationId":"inbox","limit":50})"); + "messages.hydrateConversation", + R"([{"conversationId":"inbox","limit":50}])"); ``` C++/Objective-C++ on Apple platforms: @@ -465,10 +461,10 @@ C++/Objective-C++ on Apple platforms: ```cpp #include -nativecompose::threadedruntime::dispatchHeadlessTask( +nativecompose::threadedruntime::schedule( "conversation-worker-runtime", - "hydrateConversation", - R"({"conversationId":"inbox","limit":50})"); + "messages.hydrateConversation", + R"([{"conversationId":"inbox","limit":50}])"); ``` Generator rules: @@ -485,7 +481,7 @@ Generator rules: The Metro helper is exported from the package as: ```js -const { withThreadedRuntime } = require('@react-native-runtimes/core/metro'); +const { withThreadedRuntime } = require("@react-native-runtimes/core/metro"); ``` In same-bundle mode the generated lazy registry avoids eagerly initializing @@ -501,23 +497,23 @@ code must not mount the main app by itself. This is useful when you are not usin the Metro generated registry. ```tsx -import { registerThreadedComponent } from '@react-native-runtimes/core'; +import { registerThreadedComponent } from "@react-native-runtimes/core"; function ExpensivePanel({ runtimeName }: { runtimeName?: string }) { - return ; + return ; } -registerThreadedComponent('ExpensivePanel', ExpensivePanel); +registerThreadedComponent("ExpensivePanel", ExpensivePanel); ``` ## Mount A Threaded Surface ```tsx -import { ThreadedReactSurface } from '@react-native-runtimes/core'; +import { ThreadedReactSurface } from "@react-native-runtimes/core"; require('@react-native-runtimes/core').ThreadedRuntimeHost, + "ThreadedRuntimeHost", + () => require("@react-native-runtimes/core").ThreadedRuntimeHost ); } ``` @@ -564,8 +560,8 @@ The native module exposes: - `prewarmRuntimeWithOptions(runtimeName, kind, useMainNativeModules)` - `prewarmBusinessRuntime(runtimeName)` - `preloadRuntime(runtimeName)` -- `dispatchHeadlessTask(runtimeName, taskName, payloadJson)` -- `runHeadlessTask(runtimeName, taskName, payloadJson)` +- `call(runtimeName, functionId, argsJson)` +- `schedule(runtimeName, functionId, argsJson)` - `destroyRuntime(runtimeName)` - `destroyAllRuntimes()` - `getRuntimeNames()` @@ -612,10 +608,10 @@ ThreadedRuntime.prewarmBusinessRuntime(applicationContext, "business-runtime") That runtime receives `global.__THREADED_RUNTIME_ENV__` before the bundle runs: ```tsx -if (global.__THREADED_RUNTIME_ENV__?.kind === 'business-runtime') { - require('./src/businessRuntimeEntry'); +if (global.__THREADED_RUNTIME_ENV__?.kind === "business-runtime") { + require("./src/businessRuntimeEntry"); } else { - require('./src/mainRuntimeEntry'); + require("./src/mainRuntimeEntry"); } ``` diff --git a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt index 95bc117..80aa4e4 100644 --- a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt +++ b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt @@ -35,19 +35,13 @@ object ThreadedRuntime { const val DEFAULT_HOST_APP_NAME = "ThreadedRuntimeHost" const val DEFAULT_RUNTIME_KIND = "threaded-runtime" const val BUSINESS_RUNTIME_KIND = "business-runtime" - private const val HEADLESS_TASK_RUNNER_MODULE = "ThreadedRuntimeHeadlessTaskRunner" private const val RUNTIME_FUNCTION_RUNNER_MODULE = "ThreadedRuntimeFunctionRunner" private const val LOG_TAG = "ThreadedRuntime" - private data class HeadlessTaskRequest( - val taskName: String, - val payloadJson: String, - ) - - private data class RuntimeFunctionCallRequest( + private data class RuntimeFunctionRequest( val functionId: String, val argsJson: String, - val callId: String, + val callId: String?, ) internal data class RuntimeOptions( @@ -58,9 +52,8 @@ object ThreadedRuntime { private val lock = Any() private val hosts = mutableMapOf() private val runtimeOptions = mutableMapOf() - private val pendingHeadlessTasks = mutableMapOf>() - private val pendingRuntimeFunctionCalls = - mutableMapOf>() + private val pendingRuntimeFunctionRequests = + mutableMapOf>() private val pendingRuntimeFunctionPromises = mutableMapOf() private val startingRuntimes = mutableSetOf() private val startedRuntimes = mutableSetOf() @@ -150,8 +143,7 @@ object ThreadedRuntime { val normalizedRuntimeName = runtimeName.orDefaultRuntimeName() val host = synchronized(lock) { - pendingHeadlessTasks.remove(normalizedRuntimeName) - pendingRuntimeFunctionCalls.remove(normalizedRuntimeName) + pendingRuntimeFunctionRequests.remove(normalizedRuntimeName) startingRuntimes.remove(normalizedRuntimeName) startedRuntimes.remove(normalizedRuntimeName) runtimeOptions.remove(normalizedRuntimeName) @@ -161,26 +153,21 @@ object ThreadedRuntime { } @JvmStatic - fun runHeadlessTask( - context: Context, - runtimeName: String, - taskName: String, - payloadJson: String, - ) = dispatchHeadlessTask(context, runtimeName, taskName, payloadJson) - - @JvmStatic - fun dispatchHeadlessTask( + fun call( context: Context, runtimeName: String?, - taskName: String, - payloadJson: String?, + functionId: String, + argsJson: String?, + promise: Promise, ) { val normalizedRuntimeName = runtimeName.orDefaultRuntimeName() + val callId = UUID.randomUUID().toString() val appContext = context.applicationContext synchronized(lock) { - pendingHeadlessTasks + pendingRuntimeFunctionPromises[callId] = promise + pendingRuntimeFunctionRequests .getOrPut(normalizedRuntimeName) { mutableListOf() } - .add(HeadlessTaskRequest(taskName, payloadJson ?: "null")) + .add(RuntimeFunctionRequest(functionId, argsJson ?: "[]", callId)) } dispatchExecutor.execute { val host = ensureHost(appContext, null, normalizedRuntimeName) @@ -188,25 +175,22 @@ object ThreadedRuntime { } Log.i( LOG_TAG, - "headless task queued runtimeName=$normalizedRuntimeName taskName=$taskName") + "runtime function queued runtimeName=$normalizedRuntimeName functionId=$functionId callId=$callId") } @JvmStatic - fun callRuntimeFunction( + fun schedule( context: Context, runtimeName: String?, functionId: String, argsJson: String?, - promise: Promise, ) { val normalizedRuntimeName = runtimeName.orDefaultRuntimeName() - val callId = UUID.randomUUID().toString() val appContext = context.applicationContext synchronized(lock) { - pendingRuntimeFunctionPromises[callId] = promise - pendingRuntimeFunctionCalls + pendingRuntimeFunctionRequests .getOrPut(normalizedRuntimeName) { mutableListOf() } - .add(RuntimeFunctionCallRequest(functionId, argsJson ?: "[]", callId)) + .add(RuntimeFunctionRequest(functionId, argsJson ?: "[]", null)) } dispatchExecutor.execute { val host = ensureHost(appContext, null, normalizedRuntimeName) @@ -214,7 +198,7 @@ object ThreadedRuntime { } Log.i( LOG_TAG, - "runtime function queued runtimeName=$normalizedRuntimeName functionId=$functionId callId=$callId") + "runtime function scheduled runtimeName=$normalizedRuntimeName functionId=$functionId") } @JvmStatic @@ -288,8 +272,7 @@ object ThreadedRuntime { } if (!shouldStart) { - flushHeadlessTasks(runtimeName, host) - flushRuntimeFunctionCalls(runtimeName, host) + flushRuntimeFunctionRequests(runtimeName, host) return } @@ -302,8 +285,7 @@ object ThreadedRuntime { startingRuntimes.remove(runtimeName) startedRuntimes.add(runtimeName) } - flushHeadlessTasks(runtimeName, host) - flushRuntimeFunctionCalls(runtimeName, host) + flushRuntimeFunctionRequests(runtimeName, host) } catch (error: Throwable) { synchronized(lock) { startingRuntimes.remove(runtimeName) } Log.e(LOG_TAG, "runtime start failed runtimeName=$runtimeName", error) @@ -311,13 +293,13 @@ object ThreadedRuntime { } } - private fun flushHeadlessTasks(runtimeName: String, host: ReactHost) { + private fun flushRuntimeFunctionRequests(runtimeName: String, host: ReactHost) { val requests = synchronized(lock) { if (!startedRuntimes.contains(runtimeName)) { return } - pendingHeadlessTasks.remove(runtimeName)?.toList().orEmpty() + pendingRuntimeFunctionRequests.remove(runtimeName)?.toList().orEmpty() } if (requests.isEmpty()) { return @@ -326,39 +308,14 @@ object ThreadedRuntime { dispatchExecutor.execute { requests.forEach { request -> try { - invokeHeadlessTask(host, runtimeName, request) + invokeRuntimeFunctionRequest(host, runtimeName, request) } catch (error: Throwable) { - Log.e( - LOG_TAG, - "headless task dispatch failed runtimeName=$runtimeName taskName=${request.taskName}", - error, - ) - } - } - } - } - - private fun flushRuntimeFunctionCalls(runtimeName: String, host: ReactHost) { - val requests = - synchronized(lock) { - if (!startedRuntimes.contains(runtimeName)) { - return + request.callId?.let { + completeRuntimeFunctionCall( + it, + null, + "{\"message\":\"${jsonEscape(error.message ?: "Runtime function dispatch failed")}\"}") } - pendingRuntimeFunctionCalls.remove(runtimeName)?.toList().orEmpty() - } - if (requests.isEmpty()) { - return - } - - dispatchExecutor.execute { - requests.forEach { request -> - try { - invokeRuntimeFunctionCall(host, runtimeName, request) - } catch (error: Throwable) { - completeRuntimeFunctionCall( - request.callId, - null, - "{\"message\":\"${jsonEscape(error.message ?: "Runtime function dispatch failed")}\"}") Log.e( LOG_TAG, "runtime function dispatch failed runtimeName=$runtimeName functionId=${request.functionId}", @@ -369,43 +326,32 @@ object ThreadedRuntime { } } - private fun invokeHeadlessTask( - host: ReactHost, - runtimeName: String, - request: HeadlessTaskRequest, - ) { - val args = - Arguments.fromJavaArgs(arrayOf(request.taskName, request.payloadJson, runtimeName)) - as NativeArray - val method = resolveCallFunctionOnModuleMethod(host) - val callTask = - method.invoke(host, HEADLESS_TASK_RUNNER_MODULE, "run", args) - as? com.facebook.react.interfaces.TaskInterface<*> - callTask?.waitForCompletion(5, TimeUnit.SECONDS) - callTask?.getError()?.let { throw it } - Log.i( - LOG_TAG, - "headless task dispatched runtimeName=$runtimeName taskName=${request.taskName}") - } - - private fun invokeRuntimeFunctionCall( + private fun invokeRuntimeFunctionRequest( host: ReactHost, runtimeName: String, - request: RuntimeFunctionCallRequest, + request: RuntimeFunctionRequest, ) { + val callId = request.callId val args = - Arguments.fromJavaArgs( - arrayOf(request.functionId, request.argsJson, request.callId, runtimeName)) + if (callId == null) { + Arguments.fromJavaArgs(arrayOf(request.functionId, request.argsJson, runtimeName)) + } else { + Arguments.fromJavaArgs(arrayOf(request.functionId, request.argsJson, callId, runtimeName)) + } as NativeArray val method = resolveCallFunctionOnModuleMethod(host) val callTask = - method.invoke(host, RUNTIME_FUNCTION_RUNNER_MODULE, "run", args) + method.invoke( + host, + RUNTIME_FUNCTION_RUNNER_MODULE, + if (callId == null) "schedule" else "run", + args) as? com.facebook.react.interfaces.TaskInterface<*> callTask?.waitForCompletion(5, TimeUnit.SECONDS) callTask?.getError()?.let { throw it } Log.i( LOG_TAG, - "runtime function dispatched runtimeName=$runtimeName functionId=${request.functionId} callId=${request.callId}") + "runtime function dispatched runtimeName=$runtimeName functionId=${request.functionId} callId=${callId ?: ""}") } private fun resolveCallFunctionOnModuleMethod(host: ReactHost): Method { diff --git a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntimeBridgeModule.kt b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntimeBridgeModule.kt index d555171..e26e2c3 100644 --- a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntimeBridgeModule.kt +++ b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntimeBridgeModule.kt @@ -48,50 +48,40 @@ class ThreadedRuntimeBridgeModule(private val reactContext: ReactApplicationCont } @ReactMethod - fun runHeadlessTask( + fun call( runtimeName: String?, - taskName: String, - payloadJson: String?, + functionId: String, + argsJson: String?, promise: Promise, ) { try { - ThreadedRuntime.runHeadlessTask( + ThreadedRuntime.call( reactContext, runtimeName.orDefaultRuntimeName(), - taskName, - payloadJson ?: "null", + functionId, + argsJson ?: "[]", + promise, ) - promise.resolve(null) } catch (error: Throwable) { - promise.reject("ERR_THREADED_RUNTIME_HEADLESS_TASK", error) + promise.reject("ERR_THREADED_RUNTIME_FUNCTION", error) } } @ReactMethod - fun dispatchHeadlessTask( - runtimeName: String?, - taskName: String, - payloadJson: String?, - promise: Promise, - ) { - runHeadlessTask(runtimeName, taskName, payloadJson, promise) - } - - @ReactMethod - fun callRuntimeFunction( + fun schedule( runtimeName: String?, functionId: String, argsJson: String?, promise: Promise, ) { try { - ThreadedRuntime.callRuntimeFunction( + ThreadedRuntime.schedule( reactContext, runtimeName.orDefaultRuntimeName(), functionId, argsJson ?: "[]", - promise, ) + promise.resolve(null) } catch (error: Throwable) { promise.reject("ERR_THREADED_RUNTIME_FUNCTION", error) } diff --git a/packages/core/cpp/HybridThreadedRuntimeFunctions.cpp b/packages/core/cpp/HybridThreadedRuntimeFunctions.cpp index a51694e..041ef97 100644 --- a/packages/core/cpp/HybridThreadedRuntimeFunctions.cpp +++ b/packages/core/cpp/HybridThreadedRuntimeFunctions.cpp @@ -17,11 +17,19 @@ namespace margelo::nitro::threadedruntime { -std::shared_ptr> HybridThreadedRuntimeFunctions::run( +std::shared_ptr> HybridThreadedRuntimeFunctions::call( const std::string& runtimeName, const std::string& functionId, const std::string& argsJson) { - return ::nativecompose::threadedruntime::callRuntimeFunctionOnRuntime( + return ::nativecompose::threadedruntime::callOnRuntime( + runtimeName, functionId, argsJson); +} + +std::shared_ptr> HybridThreadedRuntimeFunctions::schedule( + const std::string& runtimeName, + const std::string& functionId, + const std::string& argsJson) { + return ::nativecompose::threadedruntime::scheduleRuntimeFunctionOnRuntime( runtimeName, functionId, argsJson); } diff --git a/packages/core/cpp/HybridThreadedRuntimeFunctions.hpp b/packages/core/cpp/HybridThreadedRuntimeFunctions.hpp index af371d0..b3cb91f 100644 --- a/packages/core/cpp/HybridThreadedRuntimeFunctions.hpp +++ b/packages/core/cpp/HybridThreadedRuntimeFunctions.hpp @@ -22,7 +22,12 @@ class HybridThreadedRuntimeFunctions final public: HybridThreadedRuntimeFunctions() : HybridObject(TAG) {} - std::shared_ptr> run( + std::shared_ptr> call( + const std::string& runtimeName, + const std::string& functionId, + const std::string& argsJson) override; + + std::shared_ptr> schedule( const std::string& runtimeName, const std::string& functionId, const std::string& argsJson) override; diff --git a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.cpp b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.cpp index db9ddae..7a3085c 100644 --- a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.cpp +++ b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.cpp @@ -148,6 +148,46 @@ void callInTargetRuntime( resolveFromValue(runtime, promise, result); } +void scheduleInTargetRuntime( + Runtime &runtime, + const std::string &functionId, + const std::string &argsJson) +{ + auto global = runtime.global(); + auto callValue = global.getProperty(runtime, "__rnrCallRuntimeFunction"); + if (!callValue.isObject() || !callValue.asObject(runtime).isFunction(runtime)) { + throw std::runtime_error("__rnrCallRuntimeFunction is not installed in target runtime"); + } + + auto callFunction = callValue.asObject(runtime).asFunction(runtime); + auto result = callFunction.call( + runtime, + String::createFromUtf8(runtime, functionId), + String::createFromUtf8(runtime, argsJson.empty() ? "[]" : argsJson)); + + if (result.isObject()) { + auto resultObject = result.asObject(runtime); + if (isThenable(runtime, resultObject)) { + auto logRejection = Function::createFromHostFunction( + runtime, + PropNameID::forAscii(runtime, "__rnrLogScheduledRuntimeFunctionError"), + 1, + [functionId](Runtime &rt, const Value &, const Value *args, size_t count) -> Value { + auto message = count > 0 ? errorToMessage(rt, args[0]) : "Runtime function rejected"; + auto console = rt.global().getPropertyAsObject(rt, "console"); + auto warn = console.getPropertyAsFunction(rt, "warn"); + warn.call( + rt, + String::createFromUtf8(rt, "Scheduled runtime function \"" + functionId + "\" failed"), + String::createFromUtf8(rt, message)); + return Value::undefined(); + }); + auto then = resultObject.getProperty(runtime, "then").asObject(runtime).asFunction(runtime); + then.callWithThis(runtime, resultObject, Value::undefined(), logRejection); + } + } +} + } // namespace void registerRuntimeDispatcher( @@ -165,7 +205,7 @@ void unregisterRuntimeDispatcher(const std::string &runtimeName) runtimeDispatchers.erase(runtimeName); } -std::shared_ptr> callRuntimeFunctionOnRuntime( +std::shared_ptr> callOnRuntime( const std::string &runtimeName, const std::string &functionId, const std::string &argsJson) @@ -207,4 +247,58 @@ std::shared_ptr> callRuntimeFunctionOnRuntime( return promise; } +std::shared_ptr> scheduleRuntimeFunctionOnRuntime( + const std::string &runtimeName, + const std::string &functionId, + const std::string &argsJson) +{ + auto promise = Promise::create(); + + Runtime *targetRuntime = nullptr; + std::shared_ptr targetDispatcher; + { + std::lock_guard lock(runtimeDispatchersMutex); + auto iterator = runtimeDispatchers.find(runtimeName); + if (iterator == runtimeDispatchers.end()) { + promise->reject(std::make_exception_ptr(std::runtime_error( + "No runtime dispatcher registered for \"" + runtimeName + "\""))); + return promise; + } + targetRuntime = iterator->second.runtime; + targetDispatcher = iterator->second.dispatcher.lock(); + } + + if (targetRuntime == nullptr || targetDispatcher == nullptr) { + promise->reject(std::make_exception_ptr(std::runtime_error( + "Runtime dispatcher expired for \"" + runtimeName + "\""))); + return promise; + } + + try { + targetDispatcher->runAsync([targetRuntime, functionId, argsJson]() { + try { + scheduleInTargetRuntime(*targetRuntime, functionId, argsJson); + } catch (const std::exception &error) { + // Keep scheduled calls fire-and-forget: failures are reported on the + // target runtime, not to the scheduling caller. + try { + auto &runtime = *targetRuntime; + auto console = runtime.global().getPropertyAsObject(runtime, "console"); + auto warn = console.getPropertyAsFunction(runtime, "warn"); + warn.call( + runtime, + String::createFromUtf8(runtime, "Scheduled runtime function \"" + functionId + "\" failed"), + String::createFromUtf8(runtime, error.what())); + } catch (...) { + } + } + }); + promise->resolve(); + } catch (...) { + promise->reject(std::current_exception()); + } + + return promise; +} + } // namespace nativecompose::threadedruntime diff --git a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.h b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.h index 9aad0bc..ae4a077 100644 --- a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.h +++ b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionScheduler.h @@ -25,7 +25,12 @@ void registerRuntimeDispatcher( void unregisterRuntimeDispatcher(const std::string &runtimeName); -std::shared_ptr> callRuntimeFunctionOnRuntime( +std::shared_ptr> callOnRuntime( + const std::string &runtimeName, + const std::string &functionId, + const std::string &argsJson); + +std::shared_ptr> scheduleRuntimeFunctionOnRuntime( const std::string &runtimeName, const std::string &functionId, const std::string &argsJson); diff --git a/packages/core/cpp/nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h b/packages/core/cpp/nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h index a6d3e0e..cf5d74d 100644 --- a/packages/core/cpp/nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h +++ b/packages/core/cpp/nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h @@ -9,12 +9,12 @@ namespace nativecompose::threadedruntime { #if defined(__ANDROID__) -inline void dispatchHeadlessTask( +inline void schedule( JNIEnv *env, jobject context, const std::string &runtimeName, - const std::string &taskName, - const std::string &payloadJson) + const std::string &functionId, + const std::string &argsJson) { jclass runtimeClass = env->FindClass("com/nativecompose/threadedruntime/ThreadedRuntime"); @@ -22,28 +22,28 @@ inline void dispatchHeadlessTask( return; } - jmethodID dispatchMethod = env->GetStaticMethodID( + jmethodID scheduleMethod = env->GetStaticMethodID( runtimeClass, - "dispatchHeadlessTask", + "schedule", "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - if (dispatchMethod == nullptr) { + if (scheduleMethod == nullptr) { env->DeleteLocalRef(runtimeClass); return; } jstring runtimeNameValue = env->NewStringUTF(runtimeName.c_str()); - jstring taskNameValue = env->NewStringUTF(taskName.c_str()); - jstring payloadJsonValue = env->NewStringUTF(payloadJson.c_str()); + jstring functionIdValue = env->NewStringUTF(functionId.c_str()); + jstring argsJsonValue = env->NewStringUTF(argsJson.c_str()); env->CallStaticVoidMethod( runtimeClass, - dispatchMethod, + scheduleMethod, context, runtimeNameValue, - taskNameValue, - payloadJsonValue); + functionIdValue, + argsJsonValue); - env->DeleteLocalRef(payloadJsonValue); - env->DeleteLocalRef(taskNameValue); + env->DeleteLocalRef(argsJsonValue); + env->DeleteLocalRef(functionIdValue); env->DeleteLocalRef(runtimeNameValue); env->DeleteLocalRef(runtimeClass); } @@ -93,10 +93,10 @@ inline void prewarmBusinessRuntime( prewarmRuntime(env, context, runtimeName, "business-runtime", true); } #elif defined(__APPLE__) -void dispatchHeadlessTask( +void schedule( const std::string &runtimeName, - const std::string &taskName, - const std::string &payloadJson); + const std::string &functionId, + const std::string &argsJson); void prewarmRuntime(const std::string &runtimeName); void prewarmRuntime( diff --git a/packages/core/ios/ThreadedRuntime.h b/packages/core/ios/ThreadedRuntime.h index d6c67e8..2206820 100644 --- a/packages/core/ios/ThreadedRuntime.h +++ b/packages/core/ios/ThreadedRuntime.h @@ -15,17 +15,14 @@ NS_ASSUME_NONNULL_BEGIN kind:(nullable NSString *)kind useMainNativeModules:(BOOL)useMainNativeModules; + (void)prewarmBusinessRuntime:(nullable NSString *)runtimeName; -+ (void)dispatchHeadlessTaskWithRuntimeName:(nullable NSString *)runtimeName - taskName:(NSString *)taskName - payloadJson:(nullable NSString *)payloadJson; -+ (void)runHeadlessTaskWithRuntimeName:(nullable NSString *)runtimeName - taskName:(NSString *)taskName - payloadJson:(nullable NSString *)payloadJson; -+ (void)callRuntimeFunctionWithRuntimeName:(nullable NSString *)runtimeName - functionId:(NSString *)functionId - argsJson:(nullable NSString *)argsJson - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject; ++ (void)callWithRuntimeName:(nullable NSString *)runtimeName + functionId:(NSString *)functionId + argsJson:(nullable NSString *)argsJson + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; ++ (void)scheduleWithRuntimeName:(nullable NSString *)runtimeName + functionId:(NSString *)functionId + argsJson:(nullable NSString *)argsJson; + (void)completeRuntimeFunctionCallWithCallId:(NSString *)callId resultJson:(nullable NSString *)resultJson errorJson:(nullable NSString *)errorJson; diff --git a/packages/core/ios/ThreadedRuntime.mm b/packages/core/ios/ThreadedRuntime.mm index 6e01cc0..66a33a2 100644 --- a/packages/core/ios/ThreadedRuntime.mm +++ b/packages/core/ios/ThreadedRuntime.mm @@ -16,7 +16,6 @@ static NSString *const ThreadedRuntimeDefaultHostAppName = @"ThreadedRuntimeHost"; static NSString *const ThreadedRuntimeDefaultRuntimeKind = @"threaded-runtime"; static NSString *const ThreadedRuntimeBusinessRuntimeKind = @"business-runtime"; -static NSString *const ThreadedRuntimeHeadlessTaskRunnerModule = @"ThreadedRuntimeHeadlessTaskRunner"; static NSString *const ThreadedRuntimeFunctionRunnerModule = @"ThreadedRuntimeFunctionRunner"; @interface ThreadedRuntime (Private) @@ -213,17 +212,7 @@ @implementation ThreadedRuntime return kinds; } -static NSMutableDictionary *> *> *ThreadedRuntimePendingHeadlessTasks() -{ - static NSMutableDictionary *> *> *tasks; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - tasks = [NSMutableDictionary new]; - }); - return tasks; -} - -static NSMutableDictionary *> *> *ThreadedRuntimePendingFunctionCalls() +static NSMutableDictionary *> *> *ThreadedRuntimePendingFunctionRequests() { static NSMutableDictionary *> *> *calls; static dispatch_once_t onceToken; @@ -341,24 +330,30 @@ + (void)prewarmBusinessRuntime:(NSString *)runtimeName useMainNativeModules:YES]; } -+ (void)dispatchHeadlessTaskWithRuntimeName:(NSString *)runtimeName - taskName:(NSString *)taskName - payloadJson:(NSString *)payloadJson ++ (void)callWithRuntimeName:(NSString *)runtimeName + functionId:(NSString *)functionId + argsJson:(NSString *)argsJson + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { NSString *normalizedRuntimeName = [self normalizeRuntimeName:runtimeName]; - NSDictionary *task = @{ - @"taskName" : taskName ?: @"", - @"payloadJson" : payloadJson ?: @"null", + NSString *callId = [NSUUID UUID].UUIDString; + NSDictionary *request = @{ + @"functionId" : functionId ?: @"", + @"argsJson" : argsJson ?: @"[]", + @"callId" : callId, }; @synchronized(self) { + ThreadedRuntimeFunctionResolves()[callId] = [resolve copy]; + ThreadedRuntimeFunctionRejects()[callId] = [reject copy]; NSMutableArray *> *pending = - ThreadedRuntimePendingHeadlessTasks()[normalizedRuntimeName]; + ThreadedRuntimePendingFunctionRequests()[normalizedRuntimeName]; if (pending == nil) { pending = [NSMutableArray new]; - ThreadedRuntimePendingHeadlessTasks()[normalizedRuntimeName] = pending; + ThreadedRuntimePendingFunctionRequests()[normalizedRuntimeName] = pending; } - [pending addObject:task]; + [pending addObject:request]; } dispatch_async(ThreadedRuntimeQueue(), ^{ @@ -366,39 +361,30 @@ + (void)dispatchHeadlessTaskWithRuntimeName:(NSString *)runtimeName [self startRuntimeAndFlushWithRuntimeName:normalizedRuntimeName host:host]; }); NSLog( - @"[ThreadedRuntime] headless task queued runtimeName=%@ taskName=%@", - normalizedRuntimeName, - taskName); - RCTLogInfo( - @"[ThreadedRuntime] headless task queued runtimeName=%@ taskName=%@", + @"[ThreadedRuntime] runtime function queued runtimeName=%@ functionId=%@ callId=%@", normalizedRuntimeName, - taskName); + functionId, + callId); } -+ (void)callRuntimeFunctionWithRuntimeName:(NSString *)runtimeName - functionId:(NSString *)functionId - argsJson:(NSString *)argsJson - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject ++ (void)scheduleWithRuntimeName:(NSString *)runtimeName + functionId:(NSString *)functionId + argsJson:(NSString *)argsJson { NSString *normalizedRuntimeName = [self normalizeRuntimeName:runtimeName]; - NSString *callId = [NSUUID UUID].UUIDString; - NSDictionary *call = @{ + NSDictionary *request = @{ @"functionId" : functionId ?: @"", @"argsJson" : argsJson ?: @"[]", - @"callId" : callId, }; @synchronized(self) { - ThreadedRuntimeFunctionResolves()[callId] = [resolve copy]; - ThreadedRuntimeFunctionRejects()[callId] = [reject copy]; NSMutableArray *> *pending = - ThreadedRuntimePendingFunctionCalls()[normalizedRuntimeName]; + ThreadedRuntimePendingFunctionRequests()[normalizedRuntimeName]; if (pending == nil) { pending = [NSMutableArray new]; - ThreadedRuntimePendingFunctionCalls()[normalizedRuntimeName] = pending; + ThreadedRuntimePendingFunctionRequests()[normalizedRuntimeName] = pending; } - [pending addObject:call]; + [pending addObject:request]; } dispatch_async(ThreadedRuntimeQueue(), ^{ @@ -406,10 +392,9 @@ + (void)callRuntimeFunctionWithRuntimeName:(NSString *)runtimeName [self startRuntimeAndFlushWithRuntimeName:normalizedRuntimeName host:host]; }); NSLog( - @"[ThreadedRuntime] runtime function queued runtimeName=%@ functionId=%@ callId=%@", + @"[ThreadedRuntime] runtime function scheduled runtimeName=%@ functionId=%@", normalizedRuntimeName, - functionId, - callId); + functionId); } + (void)completeRuntimeFunctionCallWithCallId:(NSString *)callId @@ -438,19 +423,11 @@ + (void)completeRuntimeFunctionCallWithCallId:(NSString *)callId resolve(resultJson ?: @"null"); } -+ (void)runHeadlessTaskWithRuntimeName:(NSString *)runtimeName - taskName:(NSString *)taskName - payloadJson:(NSString *)payloadJson -{ - [self dispatchHeadlessTaskWithRuntimeName:runtimeName taskName:taskName payloadJson:payloadJson]; -} - + (void)destroyRuntime:(NSString *)runtimeName { NSString *normalizedRuntimeName = [self normalizeRuntimeName:runtimeName]; @synchronized(self) { - [ThreadedRuntimePendingHeadlessTasks() removeObjectForKey:normalizedRuntimeName]; - [ThreadedRuntimePendingFunctionCalls() removeObjectForKey:normalizedRuntimeName]; + [ThreadedRuntimePendingFunctionRequests() removeObjectForKey:normalizedRuntimeName]; [ThreadedRuntimeStartingRuntimeNames() removeObject:normalizedRuntimeName]; [ThreadedRuntimeStartedRuntimeNames() removeObject:normalizedRuntimeName]; } @@ -469,8 +446,7 @@ + (void)destroyAllRuntimes [ThreadedRuntimeTurboModuleDelegates() removeAllObjects]; [ThreadedRuntimeKinds() removeAllObjects]; @synchronized(self) { - [ThreadedRuntimePendingHeadlessTasks() removeAllObjects]; - [ThreadedRuntimePendingFunctionCalls() removeAllObjects]; + [ThreadedRuntimePendingFunctionRequests() removeAllObjects]; [ThreadedRuntimeStartingRuntimeNames() removeAllObjects]; [ThreadedRuntimeStartedRuntimeNames() removeAllObjects]; } @@ -502,8 +478,7 @@ + (void)startRuntimeAndFlushWithRuntimeName:(NSString *)runtimeName host:(RCTHos BOOL shouldStart = NO; @synchronized(self) { if ([ThreadedRuntimeStartedRuntimeNames() containsObject:runtimeName]) { - [self flushHeadlessTasksWithRuntimeName:runtimeName host:host]; - [self flushRuntimeFunctionCallsWithRuntimeName:runtimeName host:host]; + [self flushRuntimeFunctionRequestsWithRuntimeName:runtimeName host:host]; return; } if (![ThreadedRuntimeStartingRuntimeNames() containsObject:runtimeName]) { @@ -539,61 +514,34 @@ + (void)runtimeDidStartWithRuntimeName:(NSString *)runtimeName host:(RCTHost *)h [ThreadedRuntimeStartingRuntimeNames() removeObject:normalizedRuntimeName]; [ThreadedRuntimeStartedRuntimeNames() addObject:normalizedRuntimeName]; } - [self flushHeadlessTasksWithRuntimeName:normalizedRuntimeName host:host]; - [self flushRuntimeFunctionCallsWithRuntimeName:normalizedRuntimeName host:host]; + [self flushRuntimeFunctionRequestsWithRuntimeName:normalizedRuntimeName host:host]; } -+ (void)flushHeadlessTasksWithRuntimeName:(NSString *)runtimeName host:(RCTHost *)host ++ (void)flushRuntimeFunctionRequestsWithRuntimeName:(NSString *)runtimeName host:(RCTHost *)host { - NSArray *> *tasks; + NSArray *> *requests; @synchronized(self) { if (![ThreadedRuntimeStartedRuntimeNames() containsObject:runtimeName]) { return; } - tasks = [ThreadedRuntimePendingHeadlessTasks()[runtimeName] copy]; - [ThreadedRuntimePendingHeadlessTasks() removeObjectForKey:runtimeName]; + requests = [ThreadedRuntimePendingFunctionRequests()[runtimeName] copy]; + [ThreadedRuntimePendingFunctionRequests() removeObjectForKey:runtimeName]; } - for (NSDictionary *task in tasks) { - NSString *taskName = task[@"taskName"] ?: @""; - NSString *payloadJson = task[@"payloadJson"] ?: @"null"; - [host callFunctionOnJSModule:ThreadedRuntimeHeadlessTaskRunnerModule - method:@"run" - args:@[ taskName, payloadJson, runtimeName ]]; - NSLog( - @"[ThreadedRuntime] headless task dispatched runtimeName=%@ taskName=%@", - runtimeName, - taskName); - RCTLogInfo( - @"[ThreadedRuntime] headless task dispatched runtimeName=%@ taskName=%@", - runtimeName, - taskName); - } -} - -+ (void)flushRuntimeFunctionCallsWithRuntimeName:(NSString *)runtimeName host:(RCTHost *)host -{ - NSArray *> *calls; - @synchronized(self) { - if (![ThreadedRuntimeStartedRuntimeNames() containsObject:runtimeName]) { - return; - } - calls = [ThreadedRuntimePendingFunctionCalls()[runtimeName] copy]; - [ThreadedRuntimePendingFunctionCalls() removeObjectForKey:runtimeName]; - } - - for (NSDictionary *call in calls) { - NSString *functionId = call[@"functionId"] ?: @""; - NSString *argsJson = call[@"argsJson"] ?: @"[]"; - NSString *callId = call[@"callId"] ?: @""; + for (NSDictionary *request in requests) { + NSString *functionId = request[@"functionId"] ?: @""; + NSString *argsJson = request[@"argsJson"] ?: @"[]"; + NSString *callId = request[@"callId"]; + NSString *method = callId.length > 0 ? @"run" : @"schedule"; + NSArray *args = callId.length > 0 ? @[ functionId, argsJson, callId, runtimeName ] : @[ functionId, argsJson, runtimeName ]; [host callFunctionOnJSModule:ThreadedRuntimeFunctionRunnerModule - method:@"run" - args:@[ functionId, argsJson, callId, runtimeName ]]; + method:method + args:args]; RCTLogInfo( @"[ThreadedRuntime] runtime function dispatched runtimeName=%@ functionId=%@ callId=%@", runtimeName, functionId, - callId); + callId ?: @""); } } @@ -709,37 +657,25 @@ + (void)configureRuntimeKind:(NSString *)kind runtimeName:(NSString *)runtimeNam } } -RCT_EXPORT_METHOD(runHeadlessTask - : (NSString *)runtimeName taskName - : (NSString *)taskName payloadJson - : (NSString *)payloadJson resolver - : (RCTPromiseResolveBlock)resolve rejecter - : (RCTPromiseRejectBlock)reject) -{ - @try { - [ThreadedRuntime runHeadlessTaskWithRuntimeName:runtimeName taskName:taskName payloadJson:payloadJson]; - resolve(nil); - } @catch (NSException *exception) { - reject(@"ERR_THREADED_RUNTIME_HEADLESS_TASK", exception.reason, nil); - } -} - -RCT_EXPORT_METHOD(dispatchHeadlessTask - : (NSString *)runtimeName taskName - : (NSString *)taskName payloadJson - : (NSString *)payloadJson resolver +RCT_EXPORT_METHOD(call + : (NSString *)runtimeName functionId + : (NSString *)functionId argsJson + : (NSString *)argsJson resolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { @try { - [ThreadedRuntime dispatchHeadlessTaskWithRuntimeName:runtimeName taskName:taskName payloadJson:payloadJson]; - resolve(nil); + [ThreadedRuntime callWithRuntimeName:runtimeName + functionId:functionId + argsJson:argsJson + resolve:resolve + reject:reject]; } @catch (NSException *exception) { - reject(@"ERR_THREADED_RUNTIME_HEADLESS_TASK", exception.reason, nil); + reject(@"ERR_THREADED_RUNTIME_FUNCTION", exception.reason, nil); } } -RCT_EXPORT_METHOD(callRuntimeFunction +RCT_EXPORT_METHOD(schedule : (NSString *)runtimeName functionId : (NSString *)functionId argsJson : (NSString *)argsJson resolver @@ -747,11 +683,8 @@ + (void)configureRuntimeKind:(NSString *)kind runtimeName:(NSString *)runtimeNam : (RCTPromiseRejectBlock)reject) { @try { - [ThreadedRuntime callRuntimeFunctionWithRuntimeName:runtimeName - functionId:functionId - argsJson:argsJson - resolve:resolve - reject:reject]; + [ThreadedRuntime scheduleWithRuntimeName:runtimeName functionId:functionId argsJson:argsJson]; + resolve(nil); } @catch (NSException *exception) { reject(@"ERR_THREADED_RUNTIME_FUNCTION", exception.reason, nil); } diff --git a/packages/core/ios/ThreadedRuntimeDispatcher.mm b/packages/core/ios/ThreadedRuntimeDispatcher.mm index 69d6831..727cce6 100644 --- a/packages/core/ios/ThreadedRuntimeDispatcher.mm +++ b/packages/core/ios/ThreadedRuntimeDispatcher.mm @@ -9,14 +9,14 @@ return [NSString stringWithUTF8String:value.c_str()]; } -void dispatchHeadlessTask( +void schedule( const std::string &runtimeName, - const std::string &taskName, - const std::string &payloadJson) + const std::string &functionId, + const std::string &argsJson) { - [ThreadedRuntime dispatchHeadlessTaskWithRuntimeName:NSStringFromStdString(runtimeName) - taskName:NSStringFromStdString(taskName) - payloadJson:NSStringFromStdString(payloadJson)]; + [ThreadedRuntime scheduleWithRuntimeName:NSStringFromStdString(runtimeName) + functionId:NSStringFromStdString(functionId) + argsJson:NSStringFromStdString(argsJson)]; } void prewarmRuntime(const std::string &runtimeName) diff --git a/packages/core/metro.js b/packages/core/metro.js index 1020dd3..8a96994 100644 --- a/packages/core/metro.js +++ b/packages/core/metro.js @@ -356,6 +356,7 @@ function scanRuntimeFunctions(file, projectRoot) { sourceType: 'module', }); const runtimeFunctions = []; + const stringConstants = collectStringConstants(ast); traverse(ast, { Program(pathRef) { @@ -406,7 +407,7 @@ function scanRuntimeFunctions(file, projectRoot) { exportName: declarator.id.name, file, id: - explicitRuntimeFunctionId(declarator.init) ?? + explicitRuntimeFunctionId(declarator.init, stringConstants) ?? runtimeFunctionId(file, projectRoot, declarator.id.name), }); }); @@ -416,6 +417,27 @@ function scanRuntimeFunctions(file, projectRoot) { return runtimeFunctions; } +function collectStringConstants(ast) { + const constants = new Map(); + + ast.program.body.forEach(node => { + if (node.type !== 'VariableDeclaration' || node.kind !== 'const') { + return; + } + + node.declarations.forEach(declarator => { + if ( + declarator.id.type === 'Identifier' && + declarator.init?.type === 'StringLiteral' + ) { + constants.set(declarator.id.name, declarator.init.value); + } + }); + }); + + return constants; +} + function runtimeFunctionShortcutName(functionName) { return `${functionName}_`; } @@ -506,7 +528,7 @@ function isRuntimeFunctionCall(node) { ); } -function explicitRuntimeFunctionId(node) { +function explicitRuntimeFunctionId(node, stringConstants = new Map()) { if (!node || node.type !== 'CallExpression') { return null; } @@ -522,7 +544,16 @@ function explicitRuntimeFunctionId(node) { return null; } const idNode = node.arguments[0]; - return idNode?.type === 'StringLiteral' ? idNode.value : null; + if (idNode?.type === 'StringLiteral') { + return idNode.value; + } + if (idNode?.type === 'Identifier' && stringConstants.has(idNode.name)) { + return stringConstants.get(idNode.name); + } + throw new Error( + 'runtimeFunction.named(...) and runtimeFunction.withId(...) must use ' + + 'a string literal id or a same-file const string id', + ); } function runtimeFunctionId(file, projectRoot, exportName) { diff --git a/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.cpp b/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.cpp index db933b0..3dd09c5 100644 --- a/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.cpp +++ b/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.cpp @@ -14,7 +14,8 @@ namespace margelo::nitro::threadedruntime { HybridObject::loadHybridMethods(); // load custom methods/properties registerHybrids(this, [](Prototype& prototype) { - prototype.registerHybridMethod("run", &HybridThreadedRuntimeFunctionsSpec::run); + prototype.registerHybridMethod("call", &HybridThreadedRuntimeFunctionsSpec::call); + prototype.registerHybridMethod("schedule", &HybridThreadedRuntimeFunctionsSpec::schedule); }); } diff --git a/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.hpp b/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.hpp index fa7bb81..775d1a6 100644 --- a/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.hpp +++ b/packages/core/nitrogen/generated/shared/c++/HybridThreadedRuntimeFunctionsSpec.hpp @@ -49,7 +49,8 @@ namespace margelo::nitro::threadedruntime { public: // Methods - virtual std::shared_ptr> run(const std::string& runtimeName, const std::string& functionId, const std::string& argsJson) = 0; + virtual std::shared_ptr> call(const std::string& runtimeName, const std::string& functionId, const std::string& argsJson) = 0; + virtual std::shared_ptr> schedule(const std::string& runtimeName, const std::string& functionId, const std::string& argsJson) = 0; protected: // Hybrid Setup diff --git a/packages/core/runtime-function-babel-plugin.js b/packages/core/runtime-function-babel-plugin.js index 965e14e..10206e3 100644 --- a/packages/core/runtime-function-babel-plugin.js +++ b/packages/core/runtime-function-babel-plugin.js @@ -51,6 +51,14 @@ function isRuntimeFunctionCall(node) { return runtimeFunctionNameFromCall(node) !== undefined; } +function isBareRuntimeFunctionCall(node) { + return ( + node?.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'runtimeFunction' + ); +} + function runtimeFunctionShortcutName(functionName) { return `${functionName}_`; } @@ -99,7 +107,7 @@ function onRuntimeChildNameFromJsxElement(node) { return null; } - const children = node.children.filter(child => { + const children = node.children.filter((child) => { if (child.type === 'JSXText') { return child.value.trim().length > 0; } @@ -197,7 +205,8 @@ function extractCallOnRuntimeCall(node) { if ( callExpression.type !== 'CallExpression' || callExpression.callee.type !== 'Identifier' || - callExpression.callee.name !== 'call' + (callExpression.callee.name !== 'call' && + callExpression.callee.name !== 'schedule') ) { return null; } @@ -213,6 +222,8 @@ function extractCallOnRuntimeCall(node) { } return { + methodName: + callExpression.callee.name === 'schedule' ? 'scheduleOn' : 'runOn', scheduledFunction, runtimeArg, args: node.arguments, @@ -237,7 +248,7 @@ function ensureRuntimeShortcutImports(programPath, t) { const bodyPaths = programPath.get('body'); const lastImportPath = bodyPaths - .filter(bodyPath => bodyPath.isImportDeclaration()) + .filter((bodyPath) => bodyPath.isImportDeclaration()) .at(-1); if (lastImportPath) { @@ -266,7 +277,7 @@ function ensureThreadedComponentImport(programPath, t) { const bodyPaths = programPath.get('body'); const lastImportPath = bodyPaths - .filter(bodyPath => bodyPath.isImportDeclaration()) + .filter((bodyPath) => bodyPath.isImportDeclaration()) .at(-1); if (lastImportPath) { @@ -379,7 +390,7 @@ module.exports = function runtimeFunctionBabelPlugin({ types: t }) { collectOnRuntimeComponentNames(programPath); const runtimeShortcutReplacements = []; const threadedComponentReplacements = []; - programPath.get('body').forEach(bodyPath => { + programPath.get('body').forEach((bodyPath) => { let functionPath = bodyPath; let exportAlias = false; if (bodyPath.isExportNamedDeclaration()) { @@ -458,7 +469,7 @@ module.exports = function runtimeFunctionBabelPlugin({ types: t }) { runtimeShortcutImports = ensureRuntimeShortcutImports(programPath, t); } - threadedComponentReplacements.forEach(replacement => { + threadedComponentReplacements.forEach((replacement) => { replacement.bodyPath.replaceWith( createThreadedComponentDeclaration({ functionNode: replacement.functionNode, @@ -469,7 +480,7 @@ module.exports = function runtimeFunctionBabelPlugin({ types: t }) { ); }); - runtimeShortcutReplacements.forEach(replacement => { + runtimeShortcutReplacements.forEach((replacement) => { replacement.bodyPath.replaceWithMultiple( createRuntimeShortcutStatements({ callIdentifier: runtimeShortcutImports.callIdentifier, @@ -497,19 +508,14 @@ module.exports = function runtimeFunctionBabelPlugin({ types: t }) { return; } - declaration.declarations.forEach(declarator => { + declaration.declarations.forEach((declarator) => { if ( declarator.id.type !== 'Identifier' || - !isRuntimeFunctionCall(declarator.init) + !isBareRuntimeFunctionCall(declarator.init) ) { return; } - const existingName = runtimeFunctionNameFromCall(declarator.init); - if (existingName) { - return; - } - const id = runtimeFunctionId( state.file.opts.filename, state.opts.projectRoot, @@ -537,7 +543,7 @@ module.exports = function runtimeFunctionBabelPlugin({ types: t }) { t.callExpression( t.memberExpression( callOnRuntimeCall.scheduledFunction, - t.identifier('runOn'), + t.identifier(callOnRuntimeCall.methodName), ), [callOnRuntimeCall.runtimeArg, ...callOnRuntimeCall.args], ), diff --git a/packages/core/src/ThreadedRuntime.tsx b/packages/core/src/ThreadedRuntime.tsx index d4641b9..73780e6 100644 --- a/packages/core/src/ThreadedRuntime.tsx +++ b/packages/core/src/ThreadedRuntime.tsx @@ -18,7 +18,6 @@ const DEFAULT_HOST_APP_NAME = 'ThreadedRuntimeHost'; const DEFAULT_RUNTIME_KIND = 'threaded-runtime'; const BUSINESS_RUNTIME_KIND = 'business-runtime'; const THREADED_SCREEN_STYLE: ViewStyle = { flex: 1 }; -const HEADLESS_TASK_RUNNER_MODULE = 'ThreadedRuntimeHeadlessTaskRunner'; const RUNTIME_FUNCTION_RUNNER_MODULE = 'ThreadedRuntimeFunctionRunner'; export type ThreadedComponent> = @@ -31,17 +30,8 @@ export type ThreadedComponent> = type ThreadedComponentLoader = () => ComponentType; type ThreadedComponentRegistry = Map; type RuntimeFunctionLoader = () => RuntimeFunction; -export type ThreadedHeadlessTaskContext = { - payload: Payload; - runtimeName: ThreadedRuntimeName; - taskName: string; -}; -export type ThreadedHeadlessTask = ( - context: ThreadedHeadlessTaskContext, -) => void | Promise; const threadedComponents: ThreadedComponentRegistry = new Map(); -const threadedHeadlessTasks = new Map>(); const runtimeFunctions = new Map(); const loadedRuntimeFunctions = new Map>(); @@ -60,6 +50,16 @@ type ThreadedRuntimeFunctionsNitro = { functionId: string, argsJson: string, ) => Promise; + call?: ( + runtimeName: string, + functionId: string, + argsJson: string, + ) => Promise; + schedule?: ( + runtimeName: string, + functionId: string, + argsJson: string, + ) => Promise; }; type ThreadedRuntimeNativeModule = { @@ -70,21 +70,16 @@ type ThreadedRuntimeNativeModule = { kind: string, useMainNativeModules: boolean, ) => Promise; - runHeadlessTask?: ( - runtimeName: string, - taskName: string, - payloadJson: string, - ) => Promise; - dispatchHeadlessTask?: ( - runtimeName: string, - taskName: string, - payloadJson: string, - ) => Promise; - callRuntimeFunction?: ( + call?: ( runtimeName: string, functionId: string, argsJson: string, ) => Promise; + schedule?: ( + runtimeName: string, + functionId: string, + argsJson: string, + ) => Promise; completeRuntimeFunctionCall?: ( callId: string, resultJson: string | null, @@ -205,12 +200,13 @@ export type ThreadedRuntimePrewarmOptions = { kind?: string; useMainNativeModules?: boolean; }; -export type ThreadedHeadlessTaskOptions = { - payload?: Payload; - runtimeName?: ThreadedRuntimeName; -}; type AnyFunction = (...args: any[]) => any; +type VoidReturningFunction = Awaited< + ReturnType +> extends void + ? TFunction + : never; export type RuntimeFunctionMetadata = { id: string; @@ -222,7 +218,14 @@ export type RuntimeFunction = TFunction & { runtimeName: ThreadedRuntimeName, ...args: Parameters ): Promise>>; -}; +} & (Awaited> extends void + ? { + scheduleOn( + runtimeName: ThreadedRuntimeName, + ...args: Parameters + ): Promise; + } + : {}); export type RuntimeFunctionCallBuilder = { on( @@ -232,6 +235,12 @@ export type RuntimeFunctionCallBuilder = { ) => Promise>>; }; +export type RuntimeFunctionScheduleBuilder = { + on( + runtimeName: ThreadedRuntimeName, + ): (...args: Parameters) => Promise; +}; + export type RuntimeFunctionFactory = { (fn: TFunction): RuntimeFunction; withId( @@ -295,13 +304,6 @@ export function registerLazyThreadedComponent( threadedComponents.set(name, loadComponent as ThreadedComponentLoader); } -export function registerThreadedHeadlessTask( - name: string, - task: ThreadedHeadlessTask, -) { - threadedHeadlessTasks.set(name, task as ThreadedHeadlessTask); -} - export function registerRuntimeFunction( id: string, loadFunction: () => RuntimeFunction, @@ -323,7 +325,15 @@ function attachRuntimeFunction( runtimeFn.__runtimeFunction = { id }; } runtimeFn.runOn = (runtimeName, ...args) => - ThreadedRuntime.run(runtimeName, runtimeFn, ...args); + ThreadedRuntime.call(runtimeName, runtimeFn, ...args); + Object.assign(runtimeFn, { + scheduleOn: (runtimeName: ThreadedRuntimeName, ...args: unknown[]) => + ThreadedRuntime.schedule( + runtimeName, + runtimeFn as RuntimeFunction<(...args: any[]) => void>, + ...args, + ), + }); return runtimeFn; } @@ -346,7 +356,18 @@ export function call( ): RuntimeFunctionCallBuilder { return { on(runtimeName) { - return (...args) => ThreadedRuntime.run(runtimeName, fn, ...args); + return (...args) => ThreadedRuntime.call(runtimeName, fn, ...args); + }, + }; +} + +export function schedule( + fn: RuntimeFunction & + RuntimeFunction>, +): RuntimeFunctionScheduleBuilder { + return { + on(runtimeName) { + return (...args) => ThreadedRuntime.schedule(runtimeName, fn, ...args); }, }; } @@ -524,43 +545,6 @@ export function ThreadedRuntimeHost({ return ; } -function runRegisteredHeadlessTask( - taskName: string, - payloadJson: string, - runtimeName: string, -) { - const task = threadedHeadlessTasks.get(taskName); - if (!task) { - console.warn(`No threaded headless task registered for "${taskName}"`); - return; - } - - let payload: unknown; - try { - payload = payloadJson ? JSON.parse(payloadJson) : undefined; - } catch (error) { - console.warn( - `Invalid payload for threaded headless task "${taskName}"`, - error, - ); - payload = undefined; - } - - try { - void Promise.resolve( - task({ - payload, - runtimeName, - taskName, - }), - ).catch(error => { - console.warn(`Threaded headless task "${taskName}" failed`, error); - }); - } catch (error) { - console.warn(`Threaded headless task "${taskName}" failed`, error); - } -} - async function runRegisteredRuntimeFunction( functionId: string, argsJson: string, @@ -586,6 +570,22 @@ async function runRegisteredRuntimeFunction( } } +function scheduleRegisteredRuntimeFunction( + functionId: string, + argsJson: string, + _runtimeName: string, +) { + try { + void Promise.resolve( + callRegisteredRuntimeFunction(functionId, argsJson), + ).catch((error) => { + console.warn(`Scheduled runtime function "${functionId}" failed`, error); + }); + } catch (error) { + console.warn(`Scheduled runtime function "${functionId}" failed`, error); + } +} + function loadRegisteredRuntimeFunction(functionId: string) { const cached = loadedRuntimeFunctions.get(functionId); if (cached) { @@ -700,29 +700,7 @@ export const ThreadedRuntime = { }); }, - runHeadlessTask( - taskName: string, - options: ThreadedHeadlessTaskOptions = {}, - ) { - if (Platform.OS !== 'android' && Platform.OS !== 'ios') { - return Promise.resolve(); - } - - const runtimeName = options.runtimeName ?? DEFAULT_RUNTIME_NAME; - const payloadJson = JSON.stringify(options.payload ?? null); - const nativeDispatch = - nativeRuntime?.dispatchHeadlessTask ?? nativeRuntime?.runHeadlessTask; - if (!nativeDispatch) { - return Promise.reject( - new Error( - 'ThreadedRuntime native module does not support headless tasks', - ), - ); - } - return nativeDispatch(runtimeName, taskName, payloadJson); - }, - - async run( + async call( runtimeName: ThreadedRuntimeName, fn: RuntimeFunction, ...args: Parameters @@ -745,6 +723,20 @@ export const ThreadedRuntime = { const argsJson = JSON.stringify(args); const runtimeNitro = getRuntimeFunctionsNitro(); + if (runtimeNitro?.call) { + try { + const resultJson = await runtimeNitro.call( + runtimeName, + functionId, + argsJson, + ); + return JSON.parse(resultJson) as Awaited>; + } catch (error) { + if (!isRuntimeDispatcherMissing(error)) { + throw error; + } + } + } if (runtimeNitro?.run) { try { const resultJson = await runtimeNitro.run( @@ -760,8 +752,8 @@ export const ThreadedRuntime = { } } - const callRuntimeFunction = nativeRuntime?.callRuntimeFunction; - if (!callRuntimeFunction) { + const nativeCall = nativeRuntime?.call; + if (!nativeCall) { return Promise.reject>>( new Error( 'ThreadedRuntime native module does not support runtime functions', @@ -769,29 +761,82 @@ export const ThreadedRuntime = { ); } - const resultJson = await callRuntimeFunction( - runtimeName, - functionId, - argsJson, - ); + const resultJson = await nativeCall(runtimeName, functionId, argsJson); return JSON.parse(resultJson) as Awaited>; }, - call( + async schedule( runtimeName: ThreadedRuntimeName, - fn: RuntimeFunction, + fn: RuntimeFunction & + RuntimeFunction>, ...args: Parameters - ) { - return ThreadedRuntime.run(runtimeName, fn, ...args); + ): Promise { + if (Platform.OS !== 'android' && Platform.OS !== 'ios') { + try { + void Promise.resolve(fn(...args)).catch((error) => { + console.warn( + `Scheduled runtime function failed on "${runtimeName}"`, + error, + ); + }); + } catch (error) { + console.warn( + `Scheduled runtime function failed on "${runtimeName}"`, + error, + ); + } + return; + } + + const functionId = fn.__runtimeFunction?.id; + if (!functionId) { + return Promise.reject( + new Error( + 'Runtime function is missing generated metadata. Make sure it is ' + + 'exported as runtimeFunction(...) and Metro uses withThreadedRuntime(...).', + ), + ); + } + + const argsJson = JSON.stringify(args); + const runtimeNitro = getRuntimeFunctionsNitro(); + if (runtimeNitro?.schedule) { + try { + await runtimeNitro.schedule(runtimeName, functionId, argsJson); + return; + } catch (error) { + if (!isRuntimeDispatcherMissing(error)) { + throw error; + } + } + } + + const scheduleRuntimeFunction = nativeRuntime?.schedule; + if (!scheduleRuntimeFunction) { + return Promise.reject( + new Error( + 'ThreadedRuntime native module does not support scheduled runtime functions', + ), + ); + } + + await scheduleRuntimeFunction(runtimeName, functionId, argsJson); }, runtime(runtimeName: ThreadedRuntimeName) { return { - run( + call( fn: RuntimeFunction, ...args: Parameters ) { - return ThreadedRuntime.run(runtimeName, fn, ...args); + return ThreadedRuntime.call(runtimeName, fn, ...args); + }, + schedule( + fn: RuntimeFunction & + RuntimeFunction>, + ...args: Parameters + ) { + return ThreadedRuntime.schedule(runtimeName, fn, ...args); }, }; }, @@ -846,10 +891,7 @@ function installThreadedRuntimeEventEmitterFallback() { installThreadedRuntimeEventEmitterFallback(); -registerCallableModule(HEADLESS_TASK_RUNNER_MODULE, () => ({ - run: runRegisteredHeadlessTask, -})); - registerCallableModule(RUNTIME_FUNCTION_RUNNER_MODULE, () => ({ run: runRegisteredRuntimeFunction, + schedule: scheduleRegisteredRuntimeFunction, })); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 52550e4..691cefd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,10 +6,10 @@ export { MAIN_RUNTIME_NAME, registerLazyThreadedComponent, registerRuntimeFunction, - registerThreadedHeadlessTask, registerThreadedComponent, OnRuntime, runtimeFunction, + schedule, threadedComponent, Threaded, ThreadedReactSurface, @@ -18,9 +18,6 @@ export { ThreadedScreen, usingRuntime, type CurrentRuntimeInfo, - type ThreadedHeadlessTaskContext, - type ThreadedHeadlessTask, - type ThreadedHeadlessTaskOptions, type ThreadedComponent, type OnRuntimeProps, type ThreadedProps, @@ -29,6 +26,7 @@ export { type ThreadedScreenProps, type RuntimeFunction, type RuntimeFunctionCallBuilder, + type RuntimeFunctionScheduleBuilder, type RuntimeFunctionFactory, type RuntimeFunctionMetadata, } from './ThreadedRuntime'; diff --git a/packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts b/packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts index a1377f4..95bea7d 100644 --- a/packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts +++ b/packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts @@ -2,9 +2,14 @@ import type { HybridObject } from 'react-native-nitro-modules'; export interface ThreadedRuntimeFunctions extends HybridObject<{ ios: 'c++'; android: 'c++' }> { - run( + call( runtimeName: string, functionId: string, argsJson: string, ): Promise; + schedule( + runtimeName: string, + functionId: string, + argsJson: string, + ): Promise; } diff --git a/website/docs/examples.md b/website/docs/examples.md index eae7cef..2509ed5 100644 --- a/website/docs/examples.md +++ b/website/docs/examples.md @@ -9,7 +9,7 @@ Use a runtime function when the caller should await a result from another JS runtime: ```tsx -import { call, runtimeFunction } from '@react-native-runtimes/core'; +import { call, runtimeFunction } from "@react-native-runtimes/core"; function fibonacciNumber(n: number): number { if (n < 2) { @@ -27,7 +27,7 @@ export const fibonacci = runtimeFunction((n: number) => { }; }); -const result = await call(fibonacci).on('fibonacci-worker-runtime')(38); +const result = await call(fibonacci).on("fibonacci-worker-runtime")(38); ``` The sample app includes this as the **Fibonacci** screen. It is the smallest @@ -43,16 +43,16 @@ inside the threaded runtime: function MessageList({ conversationId }: { conversationId: string }) { const messages = useDatabaseQuery(() => db.messages - .where('conversationId') + .where("conversationId") .equals(conversationId) - .sortBy('createdAt'), + .sortBy("createdAt") ); return ( item.id} + keyExtractor={(item) => item.id} renderItem={renderMessage} /> ); @@ -77,12 +77,12 @@ Use the main runtime as a data producer and a threaded runtime as the consumer when network and UI ownership are separate: ```tsx -const pokemonItems = pokemonStore.path('pokemonItems'); +const pokemonItems = pokemonStore.path("pokemonItems"); async function fetchMore() { const page = await fetchNextPage(); - await pokemonItems.update(items => [...items, ...page]); + await pokemonItems.update((items) => [...items, ...page]); } ``` @@ -108,21 +108,21 @@ Use shared Zustand paths when two runtimes should update the same logical state: ```tsx const sharedTreeStore = createSharedStore({ - name: 'threaded-tree-demo', + name: "threaded-tree-demo", initialState: { nodes: initialNodeColors, interaction: { - lastNode: 'root', - lastRuntime: 'initial', + lastNode: "root", + lastRuntime: "initial", presses: 0, }, }, - subtrees: ['nodes', 'interaction'], + subtrees: ["nodes", "interaction"], }); -const nodes = sharedTreeStore.path('nodes'); +const nodes = sharedTreeStore.path("nodes"); const interaction = - sharedTreeStore.path('interaction'); + sharedTreeStore.path("interaction"); ``` Both runtimes render the same tree and write through path handles: @@ -160,7 +160,7 @@ The sample app includes this as **Shared tree**. ```tsx const CHAT_RUNTIME_NAMES = conversations.map( - conversation => `conversation-${conversation.id}-runtime`, + (conversation) => `conversation-${conversation.id}-runtime` ); function ConversationPicker() { @@ -188,7 +188,7 @@ function ConversationPicker() { return ( { + onOpen={(conversationId) => { void ThreadedRuntime.prewarm(`conversation-${conversationId}-runtime`); setSelectedId(conversationId); }} @@ -197,22 +197,22 @@ function ConversationPicker() { } ``` -## Headless Hydration Before Opening A Screen +## Scheduled Hydration Before Opening A Screen ```tsx +import { schedule } from "@react-native-runtimes/core"; +import { hydrateConversation } from "./hydrateConversation"; + async function prepareAndOpen(conversationId: string) { const runtimeName = `conversation-${conversationId}-runtime`; await ThreadedRuntime.prewarm(runtimeName); - await ThreadedRuntime.runHeadlessTask('hydrateConversation', { - runtimeName, - payload: { - conversationId, - limit: 50, - }, + await schedule(hydrateConversation).on(runtimeName)({ + conversationId, + limit: 50, }); - navigation.navigate('Conversation', { conversationId }); + navigation.navigate("Conversation", { conversationId }); } ``` @@ -222,11 +222,11 @@ This can be called before the runtime is ready. Native queues the request and flushes it once startup completes. ```kotlin -ThreadedRuntime.dispatchHeadlessTask( +ThreadedRuntime.schedule( applicationContext, "conversation-release-room-runtime", - "hydrateConversation", - """{"conversationId":"release-room","limit":50}""", + "messages.hydrateConversation", + """[{"conversationId":"release-room","limit":50}]""", ) ``` diff --git a/website/docs/intro.md b/website/docs/intro.md index fc5c4d1..cb17a7a 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -6,7 +6,7 @@ slug: / Native Compose Runtimes provides two React Native libraries: -- `@react-native-runtimes/core` mounts selected React components, whole screens, or headless tasks on named secondary React Native runtimes. +- `@react-native-runtimes/core` mounts selected React components, whole screens, and scheduled runtime functions on named secondary React Native runtimes. - `@react-native-runtimes/state` provides a small Zustand-like shared store backed by native C++ state so multiple runtimes can read and update the same data. The main use cases are chat screens, expensive list renderers, background preparation, and shared state that must survive across runtime boundaries. @@ -19,5 +19,5 @@ Use threaded rendering when the main JS runtime should stay responsive while ano - A threaded component is a React component registered with `threadedComponent`. - A threaded surface is a native view that asks the named runtime to render a registered component. - A prewarmed runtime is started before it is visible. -- A headless task runs JS on a named runtime without mounting UI. +- A scheduled runtime function runs JS on a named runtime without mounting UI or returning a value to the caller. - Shared Zustand stores synchronize state between the main runtime and secondary runtimes. diff --git a/website/docs/threaded-runtime/background-work.md b/website/docs/threaded-runtime/background-work.md new file mode 100644 index 0000000..761389b --- /dev/null +++ b/website/docs/threaded-runtime/background-work.md @@ -0,0 +1,112 @@ +--- +id: background-work +title: Background Work With Runtime Functions +--- + +Run background JS on a named threaded runtime by registering a +`runtimeFunction` and invoking it with `schedule`. Scheduled functions do not +return values to the caller; they should publish durable output through shared +state, storage, or native modules. + +```tsx +import { runtimeFunction, schedule } from "@react-native-runtimes/core"; +import { messagesStore } from "./messagesStore"; + +export const hydrateConversation = runtimeFunction.named( + "messages.hydrateConversation", + async ({ + conversationId, + limit, + }: { + conversationId: string; + limit: number; + }) => { + const messages = await loadMessages(conversationId, limit); + + await messagesStore + .path(["conversations", conversationId]) + .set(messages, true); + } +); + +await schedule(hydrateConversation).on("conversation-worker-runtime")({ + conversationId: "release-room", + limit: 50, +}); +``` + +Native starts or reuses the named runtime. If the runtime is still starting, +the function is queued and flushed after startup. The returned JS promise +resolves when native accepts the scheduled work, not when the async function +body finishes. + +## Await Results + +Use `call` when the caller needs a return value: + +```tsx +import { call, runtimeFunction } from "@react-native-runtimes/core"; + +export const readConversation = runtimeFunction(async ({ conversationId }) => { + const messages = messagesStore.path([ + "conversations", + conversationId, + ]); + + await messages.hydrate(); + return messages.get(); +}); + +const messages = await call(readConversation).on("conversation-worker-runtime")( + { conversationId: "inbox" } +); +``` + +## Native Schedule + +Native callers schedule explicitly named runtime functions by id. + +Android Kotlin: + +```kotlin +ThreadedRuntime.schedule( + context = applicationContext, + runtimeName = "conversation-worker-runtime", + functionId = "messages.hydrateConversation", + argsJson = """[{"conversationId":"release-room","limit":50}]""", +) +``` + +Android C++: + +```cpp +#include + +nativecompose::threadedruntime::schedule( + env, + applicationContext, + "conversation-worker-runtime", + "messages.hydrateConversation", + R"([{"conversationId":"release-room","limit":50}])" +); +``` + +Apple C++ or Objective-C++: + +```cpp +#include + +nativecompose::threadedruntime::schedule( + "conversation-worker-runtime", + "messages.hydrateConversation", + R"([{"conversationId":"release-room","limit":50}])" +); +``` + +## Typical Flow + +1. Prewarm the runtime while the user is still on the previous screen. +2. Schedule a runtime function to hydrate data on that runtime. +3. Store durable output in shared state or native storage. +4. Open a `ThreadedScreen` using the same runtime name. +5. The screen reads the already-warmed data from the shared store. diff --git a/website/docs/threaded-runtime/business-logic-executor.md b/website/docs/threaded-runtime/business-logic-executor.md index ceeaf80..3c4c174 100644 --- a/website/docs/threaded-runtime/business-logic-executor.md +++ b/website/docs/threaded-runtime/business-logic-executor.md @@ -68,26 +68,26 @@ Use this file for module-scope work that must exist before native queues are flushed: ```tsx title="index.background.ts" -import { registerThreadedHeadlessTask } from '@react-native-runtimes/core'; -import { business } from './src/businessStore'; +import { runtimeFunction } from "@react-native-runtimes/core"; +import { business } from "./src/businessStore"; -registerThreadedHeadlessTask<{ reason: string }>( - 'business:refresh', - async ({ payload }) => { +export const refreshBusiness = runtimeFunction.named( + "business:refresh", + async ({ reason }: { reason: string }) => { await business.hydrate(); - await business.update(state => ({ - lastRefreshReason: payload.reason, + await business.update((state) => ({ + lastRefreshReason: reason, refreshCount: state.refreshCount + 1, })); - }, + } ); void business.hydrate(); ``` Keep UI imports out of `index.background.ts`. Treat it as the background -runtime bootstrap: register headless tasks, hydrate stores, start app-lifetime -queues, and install background-only listeners. +runtime bootstrap: export named runtime functions, hydrate stores, start +app-lifetime queues, and install background-only listeners. The file suffix matches the runtime name. If your native prewarm uses a different name, use that same suffix: @@ -106,7 +106,7 @@ Put shared state in `@react-native-runtimes/state` so `main` and `background` can both read and write the same data. ```tsx -import { createSharedStore } from '@react-native-runtimes/state'; +import { createSharedStore } from "@react-native-runtimes/state"; type BusinessState = { business: BusinessSnapshot; @@ -118,12 +118,12 @@ type BusinessSnapshot = { }; type BusinessAction = { - type: 'refreshRequested'; + type: "refreshRequested"; reason: string; }; export const businessStore = createSharedStore({ - name: 'business', + name: "business", initialState: { business: { lastRefreshReason: null, @@ -132,7 +132,7 @@ export const businessStore = createSharedStore({ }, slices: { business: (state, action) => { - if (action.type !== 'refreshRequested') { + if (action.type !== "refreshRequested") { return state; } @@ -143,12 +143,12 @@ export const businessStore = createSharedStore({ }, }, persist: { - key: 'business-v1', - subtrees: ['business'], + key: "business-v1", + subtrees: ["business"], }, }); -export const business = businessStore.path('business'); +export const business = businessStore.path("business"); ``` Read it from UI on the main runtime: @@ -157,7 +157,7 @@ Read it from UI on the main runtime: function BusinessStatus() { const state = business.use(); - return {state.lastRefreshReason ?? 'idle'}; + return {state.lastRefreshReason ?? "idle"}; } ``` @@ -168,10 +168,10 @@ in module/global scope and make `'background'` the first statement. ```tsx async function refreshBusinessState(reason: string) { - 'background'; + "background"; await business.hydrate(); - await business.update(state => ({ + await business.update((state) => ({ lastRefreshReason: reason, refreshCount: state.refreshCount + 1, })); @@ -179,7 +179,7 @@ async function refreshBusinessState(reason: string) { return business.get(); } -const state = await refreshBusinessState('manual'); +const state = await refreshBusinessState("manual"); ``` Metro rewrites that to a registered runtime function and a local scheduled @@ -187,14 +187,14 @@ alias: ```tsx export const refreshBusinessState_ = runtimeFunction.withId( - 'src/business.refreshBusinessState_', + "src/business.refreshBusinessState_", async function refreshBusinessState(reason: string) { - 'background'; + "background"; // function body - }, + } ); -const refreshBusinessState = call(refreshBusinessState_).on('background'); +const refreshBusinessState = call(refreshBusinessState_).on("background"); ``` ## Main Runtime Functions @@ -205,15 +205,15 @@ main runtime. ```tsx async function markRefreshVisible(reason: string) { - 'main'; + "main"; - await business.update(state => ({ + await business.update((state) => ({ lastRefreshReason: reason, refreshCount: state.refreshCount + 1, })); } -await markRefreshVisible('background-sync-complete'); +await markRefreshVisible("background-sync-complete"); ``` ## When To Use This Pattern @@ -232,7 +232,6 @@ payloads through function arguments; pass ids or storage references instead. Functions that use `'background'` or `'main'` must be declared at module scope, so Metro can rewrite and register them before calls are made. -Use `ThreadedRuntime.runHeadlessTask(...)` only for native fire-and-forget jobs -that must be queued before JavaScript has a convenient caller. For normal JS -request/response work, prefer the function directive or -`call(fn).on(runtimeName)(...args)`. +Use `schedule(fn).on(runtimeName)(...args)` for fire-and-forget work that +reports progress through shared state. Use `call(fn).on(runtimeName)(...args)` +when the caller needs a result. diff --git a/website/docs/threaded-runtime/headless-runtime.md b/website/docs/threaded-runtime/headless-runtime.md deleted file mode 100644 index 3e57f2c..0000000 --- a/website/docs/threaded-runtime/headless-runtime.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -id: headless-runtime -title: Headless Background Runtime With Prewarm ---- - -A headless task runs JS on a named threaded runtime without mounting any UI. Use it when a runtime should fetch, hydrate, decode, or update shared state before a screen opens. - -The same mechanism can also run heavier business logic away from the main JS -runtime. See [Business Logic Executor](./business-logic-executor.md) for a -crypto-style job queue example. - -Register a task in code loaded by the threaded bundle: - -```tsx -import { registerThreadedHeadlessTask } from '@react-native-runtimes/core'; -import { messagesStore } from './messagesStore'; - -registerThreadedHeadlessTask<{ - conversationId: string; - limit: number; -}>('hydrateConversation', async ({ payload, runtimeName }) => { - const messages = await loadMessages(payload.conversationId, payload.limit); - - await messagesStore - .path(['conversations', payload.conversationId]) - .set(messages, true); - - console.info(`Hydrated ${payload.conversationId} on ${runtimeName}`); -}); -``` - -Dispatch the task from JS: - -```tsx -import { ThreadedRuntime } from '@react-native-runtimes/core'; - -await ThreadedRuntime.runHeadlessTask('hydrateConversation', { - runtimeName: 'conversation-worker-runtime', - payload: { - conversationId: 'release-room', - limit: 50, - }, -}); -``` - -Native starts or reuses the named runtime. If the runtime is still starting, the task is queued and flushed after startup. The returned promise resolves when native accepts the dispatch, not when the async task body finishes. - -## Await Runtime Functions - -For request/response work, use an awaitable runtime function instead: - -```tsx -import { call, runtimeFunction } from '@react-native-runtimes/core'; - -export const hydrateConversation = runtimeFunction( - async ({ conversationId }) => { - const messages = messagesStore.path([ - 'conversations', - conversationId, - ]); - - await messages.hydrate(); - return messages.get(); - }, -); - -const messages = await call(hydrateConversation).on( - 'conversation-worker-runtime', -)({ conversationId: 'inbox' }); -``` - -The Metro wrapper assigns the exported function a stable id and rewrites the -`call(...).on(...)` call to dispatch it to the requested runtime. -See [Scheduling Functions on Another Runtime](./scheduling-functions.md) for the -full API contract and lookup model. - -## Native Dispatch - -Android Kotlin: - -```kotlin -ThreadedRuntime.dispatchHeadlessTask( - context = applicationContext, - runtimeName = "conversation-worker-runtime", - taskName = "hydrateConversation", - payloadJson = """{"conversationId":"release-room","limit":50}""", -) -``` - -iOS Swift: - -```swift -ThreadedRuntime.dispatchHeadlessTask( - withRuntimeName: "conversation-worker-runtime", - taskName: "hydrateConversation", - payloadJson: #"{"conversationId":"release-room","limit":50}"# -) -``` - -Android C++: - -```cpp -#include - -nativecompose::threadedruntime::dispatchHeadlessTask( - env, - applicationContext, - "conversation-worker-runtime", - "hydrateConversation", - R"({"conversationId":"release-room","limit":50})" -); -``` - -Apple C++ or Objective-C++: - -```cpp -#include - -nativecompose::threadedruntime::dispatchHeadlessTask( - "conversation-worker-runtime", - "hydrateConversation", - R"({"conversationId":"release-room","limit":50})" -); -``` - -## Typical Flow - -1. Prewarm the runtime while the user is still on the previous screen. -2. Dispatch a headless hydration task to that runtime. -3. Store durable output in shared state or native storage. -4. Open a `ThreadedScreen` using the same runtime name. -5. The screen reads the already-warmed data from the shared store. diff --git a/website/docs/threaded-runtime/scheduling-functions.md b/website/docs/threaded-runtime/scheduling-functions.md index 6754e2b..337356a 100644 --- a/website/docs/threaded-runtime/scheduling-functions.md +++ b/website/docs/threaded-runtime/scheduling-functions.md @@ -3,8 +3,10 @@ id: scheduling-functions title: Scheduling Functions on Another Runtime --- -Use awaitable runtime functions when one runtime needs a return value from code -executed on another named runtime. +Use runtime functions when one runtime needs to execute registered code on +another named runtime. `call` is awaitable and returns the function result. +`schedule` is fire-and-forget: it resolves when native accepts or queues the +work, and the scheduled function must return `void | Promise`. ## Function Used Only On A Single Thread @@ -14,7 +16,7 @@ body: ```tsx async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; } @@ -26,14 +28,14 @@ function with a scheduled alias: ```tsx export const sum_ = runtimeFunction.withId( - 'src/math.sum_', + "src/math.sum_", async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; - }, + } ); -const sum = call(sum_).on('background'); +const sum = call(sum_).on("background"); const result = await sum(5, 1); ``` @@ -45,11 +47,11 @@ the caller should choose the runtime. ## Function Used On Different Runtimes -When the caller should choose the runtime, export a runtime function and schedule +When the caller should choose the runtime, export a runtime function and invoke it with `call(fn).on(runtimeName)(...args)`: ```tsx -import { call, runtimeFunction } from '@react-native-runtimes/core'; +import { call, runtimeFunction } from "@react-native-runtimes/core"; function fibonacciNumber(n: number) { if (n < 2) { @@ -69,14 +71,33 @@ export const fibonacci = runtimeFunction((n: number) => { }; }); -const result = await call(fibonacci).on('fibonacci-worker-runtime')(38); +const result = await call(fibonacci).on("fibonacci-worker-runtime")(38); ``` -The `call(fn).on(runtimeName)(...args)` form is syntax for Metro to transform. -It is rewritten to a direct call on the registered runtime function: +Use `schedule` for work that reports progress through shared state, storage, or +native modules instead of returning a value: ```tsx -const result = await fibonacci.runOn('fibonacci-worker-runtime', 38); +import { runtimeFunction, schedule } from "@react-native-runtimes/core"; + +export const refreshCache = runtimeFunction.named( + "cache.refresh", + async (key: string) => { + await cacheStore.hydrate(); + await cacheStore.refresh(key); + } +); + +await schedule(refreshCache).on("background")("settings"); +``` + +The `call(fn).on(runtimeName)(...args)` and +`schedule(fn).on(runtimeName)(...args)` forms are syntax for Metro to transform. +They are rewritten to direct calls on the registered runtime function: + +```tsx +const result = await fibonacci.runOn("fibonacci-worker-runtime", 38); +await refreshCache.scheduleOn("background", "settings"); ``` ## Function Directive Details @@ -87,12 +108,12 @@ the first statement in the function body: ```tsx async function refreshCache(key: string) { - 'background'; + "background"; await cacheStore.hydrate(); return cacheStore.get(key); } -const value = await refreshCache('settings'); +const value = await refreshCache("settings"); ``` That source keeps call sites ordinary while still scheduling the work on the @@ -101,15 +122,15 @@ the original function with a scheduled alias: ```tsx export const refreshCache_ = runtimeFunction.withId( - 'src/cache.refreshCache_', + "src/cache.refreshCache_", async function refreshCache(key: string) { - 'background'; + "background"; await cacheStore.hydrate(); return cacheStore.get(key); - }, + } ); -const refreshCache = call(refreshCache_).on('background'); +const refreshCache = call(refreshCache_).on("background"); ``` Prefer this shortcut for fixed-runtime helpers. Prefer @@ -118,8 +139,9 @@ Prefer this shortcut for fixed-runtime helpers. Prefer ## Why Wrap With `runtimeFunction`? `runtimeFunction` marks a function as callable from another runtime. It attaches -the generated function id, exposes the typed `.runOn(runtimeName, ...args)` API, -and gives Metro a clear export boundary to register. +the generated function id, exposes the typed `.runOn(runtimeName, ...args)` and +`.scheduleOn(runtimeName, ...args)` APIs, and gives Metro a clear export +boundary to register. Metro can generate the stable id for this: @@ -144,13 +166,13 @@ Runtime functions are not sent as source code. Metro gives each exported ```tsx registerRuntimeFunction( - 'src/examples/fibonacciRuntimeFunction.fibonacci', - () => require('./src/examples/fibonacciRuntimeFunction').fibonacci, + "src/examples/fibonacciRuntimeFunction.fibonacci", + () => require("./src/examples/fibonacciRuntimeFunction").fibonacci ); ``` Every runtime loads the same bundle and installs the same registration table. -When the caller schedules a function, native sends: +When the caller invokes a function, native sends: - the target runtime name - the stable function id @@ -162,17 +184,24 @@ result back to the caller. ## Supported Shapes -The primary shape uses `call(fn).on(runtimeName)(...args)`: +The primary awaitable shape uses `call(fn).on(runtimeName)(...args)`: + +```tsx +await call(fibonacci).on("fibonacci-worker-runtime")(38); +``` + +The primary fire-and-forget shape uses +`schedule(fn).on(runtimeName)(...args)`: ```tsx -await call(fibonacci).on('fibonacci-worker-runtime')(38); +await schedule(refreshCache).on("background")("settings"); ``` For a fixed-runtime helper, use a top-level function directive: ```tsx async function sum(a: number, b: number) { - 'background'; + "background"; return a + b; } @@ -183,9 +212,9 @@ The callback form is still supported when you prefer the runtime-first shape. The callback must contain exactly one call to one exported runtime function: ```tsx -import { usingRuntime } from '@react-native-runtimes/core'; +import { usingRuntime } from "@react-native-runtimes/core"; -await usingRuntime('fibonacci-worker-runtime').run(() => fibonacci(38)); +await usingRuntime("fibonacci-worker-runtime").run(() => fibonacci(38)); ``` Use an explicit id when the generated file-path id should not be part of your @@ -193,24 +222,23 @@ public API: ```tsx export const fibonacci = runtimeFunction.named( - 'examples.fibonacci', + "examples.fibonacci", (n: number) => { return fibonacciNumber(n); - }, + } ); ``` Current constraints: - arguments and return values must be JSON-serializable -- scheduled functions must be exported and wrapped in `runtimeFunction`, or use - the top-level function directive shortcut +- runtime functions must be exported and wrapped in `runtimeFunction`, or use + the top-level function directive shortcut for awaitable calls +- functions passed to `schedule(...)` must return `void | Promise` - directive shortcut functions must be declared in module/global scope - inline lambdas and non-exported functions are not supported - closures are not captured; pass all inputs as arguments - directive shortcut functions are rewritten to `const` aliases, so define them before calling them -- synchronous functions avoid the extra Promise hop on the target runtime - -Use `ThreadedRuntime.runHeadlessTask(...)` instead when the caller only needs to -enqueue work and observe progress through shared state. +- synchronous functions avoid the extra Promise hop on the target runtime when + called with `call(...)` diff --git a/website/sidebars.js b/website/sidebars.js index 51b0c5f..20a4312 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -11,7 +11,7 @@ module.exports = { 'threaded-runtime/render-components', 'threaded-runtime/pass-props', 'threaded-runtime/prewarming', - 'threaded-runtime/headless-runtime', + 'threaded-runtime/background-work', 'threaded-runtime/scheduling-functions', 'threaded-runtime/business-logic-executor', ],