feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,49 @@
#include "RNRuntimeDecorator.h"
#include "ReanimatedVersion.h"
namespace reanimated {
void RNRuntimeDecorator::decorate(
jsi::Runtime &rnRuntime,
const std::shared_ptr<NativeReanimatedModule> &nativeReanimatedModule,
const bool isReducedMotion) {
rnRuntime.global().setProperty(rnRuntime, "_WORKLET", false);
jsi::Runtime &uiRuntime = nativeReanimatedModule->getUIRuntime();
auto workletRuntimeValue =
rnRuntime.global()
.getPropertyAsObject(rnRuntime, "ArrayBuffer")
.asFunction(rnRuntime)
.callAsConstructor(rnRuntime, {static_cast<double>(sizeof(void *))});
uintptr_t *workletRuntimeData = reinterpret_cast<uintptr_t *>(
workletRuntimeValue.getObject(rnRuntime).getArrayBuffer(rnRuntime).data(
rnRuntime));
workletRuntimeData[0] = reinterpret_cast<uintptr_t>(&uiRuntime);
rnRuntime.global().setProperty(
rnRuntime, "_WORKLET_RUNTIME", workletRuntimeValue);
#ifdef RCT_NEW_ARCH_ENABLED
constexpr auto isFabric = true;
#else
constexpr auto isFabric = false;
#endif // RCT_NEW_ARCH_ENABLED
rnRuntime.global().setProperty(rnRuntime, "_IS_FABRIC", isFabric);
rnRuntime.global().setProperty(
rnRuntime, "_IS_BRIDGELESS", nativeReanimatedModule->isBridgeless());
#ifndef NDEBUG
checkJSVersion(rnRuntime, nativeReanimatedModule->getJSLogger());
#endif // NDEBUG
injectReanimatedCppVersion(rnRuntime);
rnRuntime.global().setProperty(
rnRuntime, "_REANIMATED_IS_REDUCED_MOTION", isReducedMotion);
rnRuntime.global().setProperty(
rnRuntime,
jsi::PropNameID::forAscii(rnRuntime, "__reanimatedModuleProxy"),
jsi::Object::createFromHostObject(rnRuntime, nativeReanimatedModule));
}
} // namespace reanimated

View File

@@ -0,0 +1,21 @@
#pragma once
#include <jsi/jsi.h>
#include <memory>
#include "NativeReanimatedModule.h"
using namespace facebook;
namespace reanimated {
class RNRuntimeDecorator {
public:
static void decorate(
jsi::Runtime &rnRuntime,
const std::shared_ptr<NativeReanimatedModule> &nativeReanimatedModule,
const bool isReducedMotion);
};
} // namespace reanimated

View File

@@ -0,0 +1,140 @@
#include "ReanimatedHermesRuntime.h"
// Only include this file in Hermes-enabled builds as some platforms (like tvOS)
// don't support hermes and it causes the compilation to fail.
#if JS_RUNTIME_HERMES
#include <cxxreact/MessageQueueThread.h>
#include <jsi/decorator.h>
#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <utility>
#if __has_include(<reacthermes/HermesExecutorFactory.h>)
#include <reacthermes/HermesExecutorFactory.h>
#else // __has_include(<hermes/hermes.h>) || ANDROID
#include <hermes/hermes.h>
#endif
namespace reanimated {
using namespace facebook;
using namespace react;
#if HERMES_ENABLE_DEBUGGER
#if REACT_NATIVE_MINOR_VERSION >= 73
using namespace facebook::hermes::inspector_modern;
#else
using namespace facebook::hermes::inspector;
#endif
#endif // HERMES_ENABLE_DEBUGGER
#if HERMES_ENABLE_DEBUGGER
class HermesExecutorRuntimeAdapter : public RuntimeAdapter {
public:
explicit HermesExecutorRuntimeAdapter(
facebook::hermes::HermesRuntime &hermesRuntime,
const std::shared_ptr<MessageQueueThread> &thread)
: hermesRuntime_(hermesRuntime), thread_(std::move(thread)) {}
virtual ~HermesExecutorRuntimeAdapter() {
// This is required by iOS, because there is an assertion in the destructor
// that the thread was indeed `quit` before
thread_->quitSynchronous();
}
#if REACT_NATIVE_MINOR_VERSION >= 71
facebook::hermes::HermesRuntime &getRuntime() override {
return hermesRuntime_;
}
#else
facebook::jsi::Runtime &getRuntime() override {
return hermesRuntime_;
}
facebook::hermes::debugger::Debugger &getDebugger() override {
return hermesRuntime_.getDebugger();
}
#endif // REACT_NATIVE_MINOR_VERSION
// This is not empty in the original implementation, but we decided to tickle
// the runtime by running a small piece of code on every frame as using this
// required us to hold a refernce to the runtime inside this adapter which
// caused issues while reloading the app.
void tickleJs() override {}
public:
facebook::hermes::HermesRuntime &hermesRuntime_;
std::shared_ptr<MessageQueueThread> thread_;
};
#endif // HERMES_ENABLE_DEBUGGER
ReanimatedHermesRuntime::ReanimatedHermesRuntime(
std::unique_ptr<facebook::hermes::HermesRuntime> runtime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name)
: jsi::WithRuntimeDecorator<ReanimatedReentrancyCheck>(
*runtime,
reentrancyCheck_),
runtime_(std::move(runtime)) {
#if HERMES_ENABLE_DEBUGGER
auto adapter =
std::make_unique<HermesExecutorRuntimeAdapter>(*runtime_, jsQueue);
#if REACT_NATIVE_MINOR_VERSION >= 71
debugToken_ = chrome::enableDebugging(std::move(adapter), name);
#else
chrome::enableDebugging(std::move(adapter), name);
#endif // REACT_NATIVE_MINOR_VERSION
#else
// This is required by iOS, because there is an assertion in the destructor
// that the thread was indeed `quit` before
jsQueue->quitSynchronous();
#endif // HERMES_ENABLE_DEBUGGER
#ifndef NDEBUG
facebook::hermes::HermesRuntime *wrappedRuntime = runtime_.get();
jsi::Value evalWithSourceMap = jsi::Function::createFromHostFunction(
*runtime_,
jsi::PropNameID::forAscii(*runtime_, "evalWithSourceMap"),
3,
[wrappedRuntime](
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
auto code = std::make_shared<const jsi::StringBuffer>(
args[0].asString(rt).utf8(rt));
std::string sourceURL;
if (count > 1 && args[1].isString()) {
sourceURL = args[1].asString(rt).utf8(rt);
}
std::shared_ptr<const jsi::Buffer> sourceMap;
if (count > 2 && args[2].isString()) {
sourceMap = std::make_shared<const jsi::StringBuffer>(
args[2].asString(rt).utf8(rt));
}
return wrappedRuntime->evaluateJavaScriptWithSourceMap(
code, sourceMap, sourceURL);
});
runtime_->global().setProperty(
*runtime_, "evalWithSourceMap", evalWithSourceMap);
#endif // NDEBUG
}
ReanimatedHermesRuntime::~ReanimatedHermesRuntime() {
#if HERMES_ENABLE_DEBUGGER
// We have to disable debugging before the runtime is destroyed.
#if REACT_NATIVE_MINOR_VERSION >= 71
chrome::disableDebugging(debugToken_);
#else
chrome::disableDebugging(*runtime_);
#endif // REACT_NATIVE_MINOR_VERSION
#endif // HERMES_ENABLE_DEBUGGER
}
} // namespace reanimated
#endif // JS_RUNTIME_HERMES

View File

@@ -0,0 +1,149 @@
#pragma once
// JS_RUNTIME_HERMES is only set on Android so we have to check __has_include
// on iOS.
#if __APPLE__ && \
(__has_include( \
<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>))
#define JS_RUNTIME_HERMES 1
#endif
// Only include this file in Hermes-enabled builds as some platforms (like tvOS)
// don't support hermes and it causes the compilation to fail.
#if JS_RUNTIME_HERMES
#include <cxxreact/MessageQueueThread.h>
#include <jsi/decorator.h>
#include <jsi/jsi.h>
#include <atomic>
#include <memory>
#include <string>
#include <thread>
#if __has_include(<reacthermes/HermesExecutorFactory.h>)
#include <reacthermes/HermesExecutorFactory.h>
#else // __has_include(<hermes/hermes.h>) || ANDROID
#include <hermes/hermes.h>
#endif
#if HERMES_ENABLE_DEBUGGER
#if REACT_NATIVE_MINOR_VERSION >= 73
#include <hermes/inspector-modern/chrome/Registration.h>
#else
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/Registration.h>
#endif
#endif // HERMES_ENABLE_DEBUGGER
namespace reanimated {
using namespace facebook;
using namespace react;
#if HERMES_ENABLE_DEBUGGER
#if REACT_NATIVE_MINOR_VERSION >= 73
using namespace facebook::hermes::inspector_modern;
#else
using namespace facebook::hermes::inspector;
#endif
#endif // HERMES_ENABLE_DEBUGGER
// ReentrancyCheck is copied from React Native
// from ReactCommon/hermes/executor/HermesExecutorFactory.cpp
// https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp
struct ReanimatedReentrancyCheck {
// This is effectively a very subtle and complex assert, so only
// include it in builds which would include asserts.
#ifndef NDEBUG
ReanimatedReentrancyCheck() : tid(std::thread::id()), depth(0) {}
void before() {
std::thread::id this_id = std::this_thread::get_id();
std::thread::id expected = std::thread::id();
// A note on memory ordering: the main purpose of these checks is
// to observe a before/before race, without an intervening after.
// This will be detected by the compare_exchange_strong atomicity
// properties, regardless of memory order.
//
// For everything else, it is easiest to think of 'depth' as a
// proxy for any access made inside the VM. If access to depth
// are reordered incorrectly, the same could be true of any other
// operation made by the VM. In fact, using acquire/release
// memory ordering could create barriers which mask a programmer
// error. So, we use relaxed memory order, to avoid masking
// actual ordering errors. Although, in practice, ordering errors
// of this sort would be surprising, because the decorator would
// need to call after() without before().
if (tid.compare_exchange_strong(
expected, this_id, std::memory_order_relaxed)) {
// Returns true if tid and expected were the same. If they
// were, then the stored tid referred to no thread, and we
// atomically saved this thread's tid. Now increment depth.
assert(depth == 0 && "[Reanimated] No thread id, but depth != 0");
++depth;
} else if (expected == this_id) {
// If the stored tid referred to a thread, expected was set to
// that value. If that value is this thread's tid, that's ok,
// just increment depth again.
assert(depth != 0 && "[Reanimated] Thread id was set, but depth == 0");
++depth;
} else {
// The stored tid was some other thread. This indicates a bad
// programmer error, where VM methods were called on two
// different threads unsafely. Fail fast (and hard) so the
// crash can be analyzed.
__builtin_trap();
}
}
void after() {
assert(
tid.load(std::memory_order_relaxed) == std::this_thread::get_id() &&
"[Reanimated] No thread id in after()");
if (--depth == 0) {
// If we decremented depth to zero, store no-thread into tid.
std::thread::id expected = std::this_thread::get_id();
bool didWrite = tid.compare_exchange_strong(
expected, std::thread::id(), std::memory_order_relaxed);
assert(didWrite && "[Reanimated] Decremented to zero, but no tid write");
}
}
std::atomic<std::thread::id> tid;
// This is not atomic, as it is only written or read from the owning
// thread.
unsigned int depth;
#endif // NDEBUG
};
// This is in fact a subclass of jsi::Runtime! WithRuntimeDecorator is a
// template class that is a subclass of DecoratedRuntime which is also a
// template class that then inherits its template, which in this case is
// jsi::Runtime. So the inheritance is: ReanimatedHermesRuntime ->
// WithRuntimeDecorator -> DecoratedRuntime -> jsi::Runtime You can find out
// more about this in ReactCommon/jsi/jsi/Decorator.h or by following this link:
// https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/jsi/jsi/decorator.h
class ReanimatedHermesRuntime
: public jsi::WithRuntimeDecorator<ReanimatedReentrancyCheck> {
public:
ReanimatedHermesRuntime(
std::unique_ptr<facebook::hermes::HermesRuntime> runtime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name);
~ReanimatedHermesRuntime();
private:
std::unique_ptr<facebook::hermes::HermesRuntime> runtime_;
ReanimatedReentrancyCheck reentrancyCheck_;
#if HERMES_ENABLE_DEBUGGER
#if REACT_NATIVE_MINOR_VERSION >= 71
chrome::DebugSessionToken debugToken_;
#endif // REACT_NATIVE_MINOR_VERSION >= 71
#endif // HERMES_ENABLE_DEBUGGER
};
} // namespace reanimated
#endif // JS_RUNTIME_HERMES

View File

@@ -0,0 +1,56 @@
#include "ReanimatedRuntime.h"
#include <cxxreact/MessageQueueThread.h>
#include <jsi/jsi.h>
#include <memory>
#include <utility>
#if JS_RUNTIME_HERMES
#include "ReanimatedHermesRuntime.h"
#elif JS_RUNTIME_V8
#include <v8runtime/V8RuntimeFactory.h>
#else
#if REACT_NATIVE_MINOR_VERSION >= 71
#include <jsc/JSCRuntime.h>
#else
#include <jsi/JSCRuntime.h>
#endif // REACT_NATIVE_MINOR_VERSION
#endif // JS_RUNTIME
namespace reanimated {
using namespace facebook;
using namespace react;
std::shared_ptr<jsi::Runtime> ReanimatedRuntime::make(
jsi::Runtime &rnRuntime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name) {
(void)rnRuntime; // used only for V8
#if JS_RUNTIME_HERMES
// We don't call `jsQueue->quitSynchronous()` here, since it will be done
// later in ReanimatedHermesRuntime
auto runtime = facebook::hermes::makeHermesRuntime();
return std::make_shared<ReanimatedHermesRuntime>(
std::move(runtime), jsQueue, name);
#elif JS_RUNTIME_V8
// This is required by iOS, because there is an assertion in the destructor
// that the thread was indeed `quit` before.
jsQueue->quitSynchronous();
auto config = std::make_unique<rnv8::V8RuntimeConfig>();
config->enableInspector = false;
config->appName = name;
return rnv8::createSharedV8Runtime(&rnRuntime, std::move(config));
#else
// This is required by iOS, because there is an assertion in the destructor
// that the thread was indeed `quit` before
jsQueue->quitSynchronous();
return facebook::jsc::makeJSCRuntime();
#endif
}
} // namespace reanimated

View File

@@ -0,0 +1,30 @@
#pragma once
// JS_RUNTIME_HERMES is only set on Android so we have to check __has_include
// on iOS.
#if __APPLE__ && \
(__has_include( \
<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>))
#define JS_RUNTIME_HERMES 1
#endif
#include <cxxreact/MessageQueueThread.h>
#include <jsi/jsi.h>
#include <memory>
#include <string>
namespace reanimated {
using namespace facebook;
using namespace react;
class ReanimatedRuntime {
public:
static std::shared_ptr<jsi::Runtime> make(
jsi::Runtime &rnRuntime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name);
};
} // namespace reanimated

View File

@@ -0,0 +1,191 @@
# Hermes Runtime initialization
_Last updated_: 13/09/2022 by @Kwasow
This document describes the current way of initializing Hermes and connecting
it to the debugger. The work I did was mainly based on
[HermesExecutorFactory.cpp](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp)
from React Native.
## Runtime initialization
If you take a look at `NativeProxy` (both on Android and iOS) you'll find
that it only makes a call to `ReanimatedRuntime::make(jsQueue)`. This
static function will return the correct runtime based on the user's configuration.
The initialization process is pretty simple and has only been moved out of
`NativeProxy` into `ReanimatedRuntime` without any major changes.
## Hermes runtime debugging
To enable debugging on the Hermes runtime we need to do two things:
1. Include source maps in JavaScript files
This part is done purely in JavaScript via the Babel plugin. The `makeWorklet`
function received an AST tree, which is aware of the modifications it made to
the code and therefore can generate the necessary source maps. It is important
that when we want to create a string from the AST we use the `generate` function
and enable source map generation so line mappings are not lost. Then when
transforming that code (ex. with `transformSync`) we have to pass the source
map as input so it can be updated.
Source map settings should always be set to `inline` so they are automatically
appended to the source code. The generated source map will be a base64 encoded
json.
A workletized function would look like this (after formattings):
```js
function _f(number) {
console.log(_WORKLET, number);
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBYXNCLFNBQUNBLEVBQUQsQ0FBQ0EsTUFBRCxFQUFvQjtBQUV0Q0MsU0FBTyxDQUFDQyxHQUFSRCxDQUFZRSxRQUFaRixFQUFzQkQsTUFBdEJDO0FBRmtCIiwibmFtZXMiOlsibnVtYmVyIiwiY29uc29sZSIsImxvZyIsIl9XT1JLTEVUIl0sInNvdXJjZXMiOlsiL1VzZXJzL2thcm9sL0dpdC9yZWFjdC1uYXRpdmUtcmVhbmltYXRlZC9GYWJyaWNFeGFtcGxlL3NyYy9Xb3JrbGV0RXhhbXBsZS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLyogZ2xvYmFsIF9XT1JLTEVUICovXG5pbXBvcnQgeyBCdXR0b24sIFZpZXcsIFN0eWxlU2hlZXQgfSBmcm9tICdyZWFjdC1uYXRpdmUnO1xuaW1wb3J0IHtcbiAgcnVuT25KUyxcbiAgcnVuT25VSSxcbiAgdXNlRGVyaXZlZFZhbHVlLFxuICB1c2VTaGFyZWRWYWx1ZSxcbn0gZnJvbSAncmVhY3QtbmF0aXZlLXJlYW5pbWF0ZWQnO1xuXG5pbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnO1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBXb3JrbGV0RXhhbXBsZSgpIHtcbiAgLy8gcnVuT25VSSBkZW1vXG4gIGNvbnN0IHNvbWVXb3JrbGV0ID0gKG51bWJlcjogbnVtYmVyKSA9PiB7XG4gICAgJ3dvcmtsZXQnO1xuICAgIGNvbnNvbGUubG9nKF9XT1JLTEVULCBudW1iZXIpOyAvLyBfV09SS0xFVCBzaG91bGQgYmUgdHJ1ZVxuICB9O1xuXG4gIGNvbnN0IGhhbmRsZVByZXNzMSA9ICgpID0+IHtcbiAgICBydW5PblVJKHNvbWVXb3JrbGV0KShNYXRoLnJhbmRvbSgpKTtcbiAgfTtcblxuICAvLyBydW5PbkpTIGRlbW9cbiAgY29uc3QgeCA9IHVzZVNoYXJlZFZhbHVlKDApO1xuXG4gIGNvbnN0IHNvbWVGdW5jdGlvbiA9IChudW1iZXI6IG51bWJlcikgPT4ge1xuICAgIGNvbnNvbGUubG9nKF9XT1JLTEVULCBudW1iZXIpOyAvLyBfV09SS0xFVCBzaG91bGQgYmUgZmFsc2VcbiAgfTtcblxuICB1c2VEZXJpdmVkVmFsdWUoKCkgPT4ge1xuICAgIHJ1bk9uSlMoc29tZUZ1bmN0aW9uKSh4LnZhbHVlKTtcbiAgfSk7XG5cbiAgY29uc3QgaGFuZGxlUHJlc3MyID0gKCkgPT4ge1xuICAgIHgudmFsdWUgPSBNYXRoLnJhbmRvbSgpO1xuICB9O1xuXG4gIHJldHVybiAoXG4gICAgPFZpZXcgc3R5bGU9e3N0eWxlcy5jb250YWluZXJ9PlxuICAgICAgPEJ1dHRvbiBvblByZXNzPXtoYW5kbGVQcmVzczF9IHRpdGxlPVwicnVuT25VSSBkZW1vXCIgLz5cbiAgICAgIDxCdXR0b24gb25QcmVzcz17aGFuZGxlUHJlc3MyfSB0aXRsZT1cInJ1bk9uSlMgZGVtb1wiIC8+XG4gICAgPC9WaWV3PlxuICApO1xufVxuXG5jb25zdCBzdHlsZXMgPSBTdHlsZVNoZWV0LmNyZWF0ZSh7XG4gIGNvbnRhaW5lcjoge1xuICAgIGZsZXg6IDEsXG4gICAgYWxpZ25JdGVtczogJ2NlbnRlcicsXG4gICAganVzdGlmeUNvbnRlbnQ6ICdjZW50ZXInLFxuICB9LFxufSk7XG4iXX0=
```
And the base64 string after decoding is:
```json
{
"version": 3,
"mappings": "AAasB,SAACA,EAAD,CAACA,MAAD,EAAoB;AAEtCC,SAAO,CAACC,GAARD,CAAYE,QAAZF,EAAsBD,MAAtBC;AAFkB",
"names": ["number", "console", "log", "_WORKLET"],
"sources": [
"/Users/karol/Git/react-native-reanimated/FabricExample/src/WorkletExample.tsx"
],
"sourcesContent": [
"/* global _WORKLET */\nimport { Button, View, StyleSheet } from 'react-native';\nimport {\n runOnJS,\n runOnUI,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\nimport React from 'react';\n\nexport default function WorkletExample() {\n // runOnUI demo\n const someWorklet = (number: number) => {\n 'worklet';\n console.log(_WORKLET, number); // _WORKLET should be true\n };\n\n const handlePress1 = () => {\n runOnUI(someWorklet)(Math.random());\n };\n\n // runOnJS demo\n const x = useSharedValue(0);\n\n const someFunction = (number: number) => {\n console.log(_WORKLET, number); // _WORKLET should be false\n };\n\n useDerivedValue(() => {\n runOnJS(someFunction)(x.value);\n });\n\n const handlePress2 = () => {\n x.value = Math.random();\n };\n\n return (\n <View style={styles.container}>\n <Button onPress={handlePress1} title=\"runOnUI demo\" />\n <Button onPress={handlePress2} title=\"runOnJS demo\" />\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n"
]
}
```
We run jest tests in release mode, because source maps will contain absolute
paths, which will be different on every machine and therefore would also alter
worklet hashes. Running in release mode prevents this.
2. Enable debugging on the runtime object
This is done by creating an adapter (`HermesExecutorRuntimeAdapter` inside of
`ReanimatedHermesRuntime.cpp`) which holds the runtime and allows the debugger
to communicate with it. The adapter is managed by a `Connection` (`ConnectionDemux`)
object, but this is not important in our case. We just have to make a call
to `facebook::hermes::inspector::chrome::enableDebugging()` and pass the adapter
and runtime name as parameters.
It is important to also `disableDebugging()` before the runtime is destroyed.
Failing to do so will probably crash the app as the debugger will try to
connect to a non-existent runtime.
The runtime should also be destroyed before the Reanimated module, because
otherwise there might be weird BAD_ACCESS errors when the gc gets it
hand on the runtime.
## Metro endpoint
Flipper and Chrome DevTools in general use the `localhost:8081/json` (where `8081`
is the port metro is running on) endpoint of metro to get the list of debuggable
targets (runtimes). For a normal React Native app the output would look something
like this:
```json
[
{
"id": "0-1",
"description": "org.reactjs.native.example.FabricExample",
"title": "Hermes React Native",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D1",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=0&page=1",
"vm": "Hermes"
},
{
"id": "0--1",
"description": "org.reactjs.native.example.FabricExample",
"title": "React Native Experimental (Improved Chrome Reloads)",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D-1",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=0&page=-1",
"vm": "don't use"
}
]
```
For an Android app with Reanimated it should include the Reanimated runtime like
this:
```json
[
{
"id": "0-2",
"description": "com.fabricexample",
"title": "Reanimated Runtime",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D3",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=1&page=2",
"vm": "Hermes"
},
{
"id": "0-1",
"description": "com.fabricexample",
"title": "Hermes React Native",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D2",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=1&page=1",
"vm": "Hermes"
},
{
"id": "0--1",
"description": "com.fabricexample",
"title": "React Native Experimental (Improved Chrome Reloads)",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D-1",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=1&page=-1",
"vm": "don't use"
},
{
"id": "0--2",
"description": "com.fabricexample",
"title": "Reanimated Runtime Experimental (Improved Chrome Reloads)",
"faviconUrl": "https://reactjs.org/favicon.ico",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D-2",
"type": "node",
"webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=1&page=-2",
"vm": "don't use"
}
]
```
Runtimes with negative IDs are 'virtual' - they are just references to the real
runtimes but their IDs don't change after a reload. If we were to connect to
the normal runtime and reload the app it would crash, as the debugger would try
to communicate with a non-existent runtime. These 'virtual' runtimes are made
and managed by metro (PR: [facebook/metro#864](https://github.com/facebook/metro/pull/864)).
## Known issues
**IFrame sandboxing**
Source maps always define a `sources` array, which contain names of files used
to generate the source map. For Chrome DevTools this is sufficient as it will
read files from disk, but the `IFrame` interface used by Flipper is sandboxed
and doesn't allow filesystem access. Therefore we also need to include the files
content in the `sourcesContent` array.
**Chrome version 105.0.5195.102 doesn't load source maps**
This version of Chrome introduced a regression into DevTools that broke source
maps loading for node.js apps. This issue is not caused by Reanimated in any
way and should be fixed by Chrome developers in later versions.
The issue was tracked here: https://bugs.chromium.org/p/chromium/issues/detail?id=1358497
**App reloads don't work**
On iOS the app will crash on every reload if a debugger is connected to the runtime.
On Android it will also crash but only after a few reloads.

View File

@@ -0,0 +1,149 @@
#include "WorkletRuntime.h"
#include "JSISerializer.h"
#include "ReanimatedRuntime.h"
#include "WorkletRuntimeCollector.h"
#include "WorkletRuntimeDecorator.h"
#include <jsi/decorator.h>
namespace reanimated {
class AroundLock {
const std::shared_ptr<std::recursive_mutex> mutex_;
public:
explicit AroundLock(const std::shared_ptr<std::recursive_mutex> &mutex)
: mutex_(mutex) {}
void before() const {
mutex_->lock();
}
void after() const {
mutex_->unlock();
}
};
class LockableRuntime : public jsi::WithRuntimeDecorator<AroundLock> {
AroundLock aroundLock_;
std::shared_ptr<jsi::Runtime> runtime_;
public:
explicit LockableRuntime(
std::shared_ptr<jsi::Runtime> &runtime,
const std::shared_ptr<std::recursive_mutex> &runtimeMutex)
: jsi::WithRuntimeDecorator<AroundLock>(*runtime, aroundLock_),
aroundLock_(runtimeMutex),
runtime_(std::move(runtime)) {}
};
static std::shared_ptr<jsi::Runtime> makeRuntime(
jsi::Runtime &runtime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name,
const bool supportsLocking,
const std::shared_ptr<std::recursive_mutex> &runtimeMutex) {
auto reanimatedRuntime = ReanimatedRuntime::make(runtime, jsQueue, name);
if (supportsLocking) {
return std::make_shared<LockableRuntime>(reanimatedRuntime, runtimeMutex);
} else {
return reanimatedRuntime;
}
}
WorkletRuntime::WorkletRuntime(
jsi::Runtime &rnRuntime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::shared_ptr<JSScheduler> &jsScheduler,
const std::string &name,
const bool supportsLocking,
const std::string &valueUnpackerCode)
: runtimeMutex_(std::make_shared<std::recursive_mutex>()),
runtime_(makeRuntime(
rnRuntime,
jsQueue,
name,
supportsLocking,
runtimeMutex_)),
#ifndef NDEBUG
supportsLocking_(supportsLocking),
#endif
name_(name) {
jsi::Runtime &rt = *runtime_;
WorkletRuntimeCollector::install(rt);
WorkletRuntimeDecorator::decorate(rt, name, jsScheduler);
auto codeBuffer = std::make_shared<const jsi::StringBuffer>(
"(" + valueUnpackerCode + "\n)");
auto valueUnpacker = rt.evaluateJavaScript(codeBuffer, "valueUnpacker")
.asObject(rt)
.asFunction(rt);
rt.global().setProperty(rt, "__valueUnpacker", valueUnpacker);
}
jsi::Value WorkletRuntime::executeSync(
jsi::Runtime &rt,
const jsi::Value &worklet) const {
assert(
supportsLocking_ &&
("[Reanimated] Runtime \"" + name_ + "\" doesn't support locking.")
.c_str());
auto shareableWorklet = extractShareableOrThrow<ShareableWorklet>(
rt,
worklet,
"[Reanimated] Only worklets can be executed synchronously on UI runtime.");
auto lock = std::unique_lock<std::recursive_mutex>(*runtimeMutex_);
jsi::Runtime &uiRuntime = getJSIRuntime();
auto result = runGuarded(shareableWorklet);
auto shareableResult = extractShareableOrThrow(uiRuntime, result);
lock.unlock();
return shareableResult->getJSValue(rt);
}
jsi::Value WorkletRuntime::get(
jsi::Runtime &rt,
const jsi::PropNameID &propName) {
auto name = propName.utf8(rt);
if (name == "toString") {
return jsi::Function::createFromHostFunction(
rt,
propName,
0,
[this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t)
-> jsi::Value {
return jsi::String::createFromUtf8(rt, toString());
});
}
if (name == "name") {
return jsi::String::createFromUtf8(rt, name_);
}
return jsi::Value::undefined();
}
std::vector<jsi::PropNameID> WorkletRuntime::getPropertyNames(
jsi::Runtime &rt) {
std::vector<jsi::PropNameID> result;
result.push_back(jsi::PropNameID::forUtf8(rt, "toString"));
result.push_back(jsi::PropNameID::forUtf8(rt, "name"));
return result;
}
std::shared_ptr<WorkletRuntime> extractWorkletRuntime(
jsi::Runtime &rt,
const jsi::Value &value) {
return value.getObject(rt).getHostObject<WorkletRuntime>(rt);
}
void scheduleOnRuntime(
jsi::Runtime &rt,
const jsi::Value &workletRuntimeValue,
const jsi::Value &shareableWorkletValue) {
auto workletRuntime = extractWorkletRuntime(rt, workletRuntimeValue);
auto shareableWorklet = extractShareableOrThrow<ShareableWorklet>(
rt,
shareableWorkletValue,
"[Reanimated] Function passed to `_scheduleOnRuntime` is not a shareable worklet. Please make sure that `processNestedWorklets` option in Reanimated Babel plugin is enabled.");
workletRuntime->runAsyncGuarded(shareableWorklet);
}
} // namespace reanimated

View File

@@ -0,0 +1,85 @@
#pragma once
#include <cxxreact/MessageQueueThread.h>
#include <jsi/jsi.h>
#include "AsyncQueue.h"
#include "JSScheduler.h"
#include "Shareables.h"
#include <memory>
#include <string>
#include <thread>
#include <utility>
#include <vector>
using namespace facebook;
using namespace react;
namespace reanimated {
class WorkletRuntime : public jsi::HostObject,
public std::enable_shared_from_this<WorkletRuntime> {
public:
explicit WorkletRuntime(
jsi::Runtime &rnRuntime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::shared_ptr<JSScheduler> &jsScheduler,
const std::string &name,
const bool supportsLocking,
const std::string &valueUnpackerCode);
jsi::Runtime &getJSIRuntime() const {
return *runtime_;
}
template <typename... Args>
inline jsi::Value runGuarded(
const std::shared_ptr<ShareableWorklet> &shareableWorklet,
Args &&...args) const {
jsi::Runtime &rt = *runtime_;
return runOnRuntimeGuarded(
rt, shareableWorklet->getJSValue(rt), std::forward<Args>(args)...);
}
void runAsyncGuarded(
const std::shared_ptr<ShareableWorklet> &shareableWorklet) {
if (queue_ == nullptr) {
queue_ = std::make_shared<AsyncQueue>(name_);
}
queue_->push(
[=, self = shared_from_this()] { self->runGuarded(shareableWorklet); });
}
jsi::Value executeSync(jsi::Runtime &rt, const jsi::Value &worklet) const;
std::string toString() const {
return "[WorkletRuntime \"" + name_ + "\"]";
}
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override;
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt) override;
private:
const std::shared_ptr<std::recursive_mutex> runtimeMutex_;
const std::shared_ptr<jsi::Runtime> runtime_;
#ifndef NDEBUG
const bool supportsLocking_;
#endif
const std::string name_;
std::shared_ptr<AsyncQueue> queue_;
};
// This function needs to be non-inline to avoid problems with dynamic_cast on
// Android
std::shared_ptr<WorkletRuntime> extractWorkletRuntime(
jsi::Runtime &rt,
const jsi::Value &value);
void scheduleOnRuntime(
jsi::Runtime &rt,
const jsi::Value &workletRuntimeValue,
const jsi::Value &shareableWorkletValue);
} // namespace reanimated

View File

@@ -0,0 +1,36 @@
#pragma once
#include "WorkletRuntimeRegistry.h"
#include <jsi/jsi.h>
#include <memory>
namespace reanimated {
class WorkletRuntimeCollector : public jsi::HostObject {
// When worklet runtime is created, we inject an instance of this class as a
// `jsi::HostObject` into the global object. When worklet runtime is
// terminated, the object is garbage-collected, which runs the C++ destructor.
// In the destructor, we unregister the worklet runtime from the registry.
public:
explicit WorkletRuntimeCollector(jsi::Runtime &runtime) : runtime_(runtime) {
WorkletRuntimeRegistry::registerRuntime(runtime_);
}
~WorkletRuntimeCollector() {
WorkletRuntimeRegistry::unregisterRuntime(runtime_);
}
static void install(jsi::Runtime &rt) {
auto collector = std::make_shared<WorkletRuntimeCollector>(rt);
auto object = jsi::Object::createFromHostObject(rt, collector);
rt.global().setProperty(rt, "__workletRuntimeCollector", object);
}
private:
jsi::Runtime &runtime_;
};
} // namespace reanimated

View File

@@ -0,0 +1,149 @@
#include "WorkletRuntimeDecorator.h"
#include "JSISerializer.h"
#include "ReanimatedJSIUtils.h"
#include "Shareables.h"
#include "WorkletRuntime.h"
#ifdef ANDROID
#include "Logger.h"
#else
#include "Common/cpp/hidden_headers/Logger.h"
#endif
namespace reanimated {
static inline double performanceNow() {
// copied from JSExecutor.cpp
auto time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
time.time_since_epoch())
.count();
constexpr double NANOSECONDS_IN_MILLISECOND = 1000000.0;
return duration / NANOSECONDS_IN_MILLISECOND;
}
void WorkletRuntimeDecorator::decorate(
jsi::Runtime &rt,
const std::string &name,
const std::shared_ptr<JSScheduler> &jsScheduler) {
// resolves "ReferenceError: Property 'global' doesn't exist at ..."
rt.global().setProperty(rt, "global", rt.global());
rt.global().setProperty(rt, "_WORKLET", true);
rt.global().setProperty(rt, "_LABEL", jsi::String::createFromAscii(rt, name));
#ifdef RCT_NEW_ARCH_ENABLED
constexpr auto isFabric = true;
#else
constexpr auto isFabric = false;
#endif // RCT_NEW_ARCH_ENABLED
rt.global().setProperty(rt, "_IS_FABRIC", isFabric);
#ifndef NDEBUG
auto evalWithSourceUrl = [](jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
auto code = std::make_shared<const jsi::StringBuffer>(
args[0].asString(rt).utf8(rt));
std::string url;
if (count > 1 && args[1].isString()) {
url = args[1].asString(rt).utf8(rt);
}
return rt.evaluateJavaScript(code, url);
};
rt.global().setProperty(
rt,
"evalWithSourceUrl",
jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "evalWithSourceUrl"),
1,
evalWithSourceUrl));
#endif // NDEBUG
jsi_utils::installJsiFunction(
rt, "_toString", [](jsi::Runtime &rt, const jsi::Value &value) {
return jsi::String::createFromUtf8(rt, stringifyJSIValue(rt, value));
});
jsi_utils::installJsiFunction(
rt, "_log", [](jsi::Runtime &rt, const jsi::Value &value) {
Logger::log(stringifyJSIValue(rt, value));
});
jsi_utils::installJsiFunction(
rt,
"_makeShareableClone",
[](jsi::Runtime &rt,
const jsi::Value &value,
const jsi::Value &nativeStateSource) {
auto shouldRetainRemote = jsi::Value::undefined();
return reanimated::makeShareableClone(
rt, value, shouldRetainRemote, nativeStateSource);
});
jsi_utils::installJsiFunction(
rt,
"_scheduleOnJS",
[jsScheduler](
jsi::Runtime &rt,
const jsi::Value &remoteFun,
const jsi::Value &argsValue) {
auto shareableRemoteFun = extractShareableOrThrow<
ShareableRemoteFunction>(
rt,
remoteFun,
"[Reanimated] Incompatible object passed to scheduleOnJS. It is only allowed to schedule worklets or functions defined on the React Native JS runtime this way.");
auto shareableArgs = argsValue.isUndefined()
? nullptr
: extractShareableOrThrow<ShareableArray>(
rt, argsValue, "[Reanimated] Args must be an array.");
jsScheduler->scheduleOnJS([=](jsi::Runtime &rt) {
auto remoteFun = shareableRemoteFun->getJSValue(rt);
if (shareableArgs == nullptr) {
// fast path for remote function w/o arguments
remoteFun.asObject(rt).asFunction(rt).call(rt);
} else {
auto argsArray =
shareableArgs->getJSValue(rt).asObject(rt).asArray(rt);
auto argsSize = argsArray.size(rt);
// number of arguments is typically relatively small so it is ok to
// to use VLAs here, hence disabling the lint rule
jsi::Value args[argsSize]; // NOLINT(runtime/arrays)
for (size_t i = 0; i < argsSize; i++) {
args[i] = argsArray.getValueAtIndex(rt, i);
}
remoteFun.asObject(rt).asFunction(rt).call(rt, args, argsSize);
}
});
});
jsi_utils::installJsiFunction(
rt,
"_scheduleOnRuntime",
[](jsi::Runtime &rt,
const jsi::Value &workletRuntimeValue,
const jsi::Value &shareableWorkletValue) {
reanimated::scheduleOnRuntime(
rt, workletRuntimeValue, shareableWorkletValue);
});
jsi::Object performance(rt);
performance.setProperty(
rt,
"now",
jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "now"),
0,
[](jsi::Runtime &runtime,
const jsi::Value &,
const jsi::Value *args,
size_t count) { return jsi::Value(performanceNow()); }));
rt.global().setProperty(rt, "performance", performance);
}
} // namespace reanimated

View File

@@ -0,0 +1,22 @@
#pragma once
#include "JSScheduler.h"
#include <jsi/jsi.h>
#include <memory>
#include <string>
using namespace facebook;
namespace reanimated {
class WorkletRuntimeDecorator {
public:
static void decorate(
jsi::Runtime &rt,
const std::string &name,
const std::shared_ptr<JSScheduler> &jsScheduler);
};
} // namespace reanimated

View File

@@ -0,0 +1,8 @@
#include "WorkletRuntimeRegistry.h"
namespace reanimated {
std::set<jsi::Runtime *> WorkletRuntimeRegistry::registry_{};
std::mutex WorkletRuntimeRegistry::mutex_{};
} // namespace reanimated

View File

@@ -0,0 +1,39 @@
#pragma once
#include <jsi/jsi.h>
#include <mutex>
#include <set>
using namespace facebook;
namespace reanimated {
class WorkletRuntimeRegistry {
private:
static std::set<jsi::Runtime *> registry_;
static std::mutex mutex_; // Protects `registry_`.
WorkletRuntimeRegistry() {} // private ctor
static void registerRuntime(jsi::Runtime &runtime) {
std::lock_guard<std::mutex> lock(mutex_);
registry_.insert(&runtime);
}
static void unregisterRuntime(jsi::Runtime &runtime) {
std::lock_guard<std::mutex> lock(mutex_);
registry_.erase(&runtime);
}
friend class WorkletRuntimeCollector;
public:
static bool isRuntimeAlive(jsi::Runtime *runtime) {
assert(runtime != nullptr);
std::lock_guard<std::mutex> lock(mutex_);
return registry_.find(runtime) != registry_.end();
}
};
} // namespace reanimated