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,184 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type {MutationObserverId} from './MutationObserverManager';
import type MutationRecord from './MutationRecord';
import ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import * as MutationObserverManager from './MutationObserverManager';
export type MutationObserverCallback = (
mutationRecords: $ReadOnlyArray<MutationRecord>,
observer: MutationObserver,
) => mixed;
type MutationObserverInit = $ReadOnly<{
subtree?: boolean,
// This is the only supported option so it's required to be `true`.
childList: true,
// Unsupported:
attributes?: boolean,
attributeFilter?: $ReadOnlyArray<string>,
attributeOldValue?: boolean,
characterData?: boolean,
characterDataOldValue?: boolean,
}>;
/**
* This is a React Native implementation for the `MutationObserver` API
* (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
*
* It only supports the `subtree` and `childList` options at the moment.
*/
export default class MutationObserver {
_callback: MutationObserverCallback;
_observationTargets: Set<ReactNativeElement> = new Set();
_mutationObserverId: ?MutationObserverId;
constructor(callback: MutationObserverCallback): void {
if (callback == null) {
throw new TypeError(
"Failed to construct 'MutationObserver': 1 argument required, but only 0 present.",
);
}
if (typeof callback !== 'function') {
throw new TypeError(
"Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'.",
);
}
this._callback = callback;
}
/**
* Configures the `MutationObserver` callback to begin receiving notifications
* of changes to the UI tree that match the given options.
* Depending on the configuration, the observer may watch a single node in the
* UI tree, or that node and some or all of its descendant nodes.
* To stop the `MutationObserver` (so that none of its callbacks will be
* triggered any longer), call `MutationObserver.disconnect()`.
*/
observe(target: ReactNativeElement, options?: MutationObserverInit): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
// Browsers force a cast of this value to boolean
if (Boolean(options?.childList) !== true) {
throw new TypeError(
"Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.",
);
}
if (options?.attributes != null) {
throw new Error(
"Failed to execute 'observe' on 'MutationObserver': attributes is not supported",
);
}
if (options?.attributeFilter != null) {
throw new Error(
"Failed to execute 'observe' on 'MutationObserver': attributeFilter is not supported",
);
}
if (options?.attributeOldValue != null) {
throw new Error(
"Failed to execute 'observe' on 'MutationObserver': attributeOldValue is not supported",
);
}
if (options?.characterData != null) {
throw new Error(
"Failed to execute 'observe' on 'MutationObserver': characterData is not supported",
);
}
if (options?.characterDataOldValue != null) {
throw new Error(
"Failed to execute 'observe' on 'MutationObserver': characterDataOldValue is not supported",
);
}
const mutationObserverId = this._getOrCreateMutationObserverId();
// As per the spec, if the target is already being observed, we "reset"
// the observation and only use the last options used.
if (this._observationTargets.has(target)) {
MutationObserverManager.unobserve(mutationObserverId, target);
}
MutationObserverManager.observe({
mutationObserverId,
target,
subtree: Boolean(options?.subtree),
});
this._observationTargets.add(target);
}
_unobserve(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (!this._observationTargets.has(target)) {
return;
}
const mutationObserverId = this._mutationObserverId;
if (mutationObserverId == null) {
return;
}
MutationObserverManager.unobserve(mutationObserverId, target);
this._observationTargets.delete(target);
if (this._observationTargets.size === 0) {
MutationObserverManager.unregisterObserver(mutationObserverId);
this._mutationObserverId = null;
}
}
/**
* Tells the observer to stop watching for mutations.
* The observer can be reused by calling its `observe()` method again.
*/
disconnect(): void {
for (const target of this._observationTargets.keys()) {
this._unobserve(target);
}
}
_getOrCreateMutationObserverId(): MutationObserverId {
let mutationObserverId = this._mutationObserverId;
if (mutationObserverId == null) {
mutationObserverId = MutationObserverManager.registerObserver(
this,
this._callback,
);
this._mutationObserverId = mutationObserverId;
}
return mutationObserverId;
}
// Only for tests
__getObserverID(): ?MutationObserverId {
return this._mutationObserverId;
}
}

View File

@@ -0,0 +1,218 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
/**
* This module handles the communication between the React Native renderer
* and all the mutation observers that are currently observing any targets.
*
* In order to reduce the communication between native and JavaScript,
* we register a single notication callback in native, and then we handle how
* to notify each entry to the right mutation observer when we receive all
* the notifications together.
*/
import type ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import type MutationObserver, {
MutationObserverCallback,
} from './MutationObserver';
import type MutationRecord from './MutationRecord';
import {
getPublicInstanceFromInternalInstanceHandle,
getShadowNode,
} from '../../src/private/webapis/dom/nodes/ReadOnlyNode';
import * as Systrace from '../Performance/Systrace';
import warnOnce from '../Utilities/warnOnce';
import {createMutationRecord} from './MutationRecord';
import NativeMutationObserver from './NativeMutationObserver';
export type MutationObserverId = number;
let nextMutationObserverId: MutationObserverId = 1;
let isConnected: boolean = false;
const registeredMutationObservers: Map<
MutationObserverId,
$ReadOnly<{observer: MutationObserver, callback: MutationObserverCallback}>,
> = new Map();
/**
* Registers the given mutation observer and returns a unique ID for it,
* which is required to start observing targets.
*/
export function registerObserver(
observer: MutationObserver,
callback: MutationObserverCallback,
): MutationObserverId {
const mutationObserverId = nextMutationObserverId;
nextMutationObserverId++;
registeredMutationObservers.set(mutationObserverId, {
observer,
callback,
});
return mutationObserverId;
}
/**
* Unregisters the given mutation observer.
* This should only be called when an observer is no longer observing any
* targets.
*/
export function unregisterObserver(
mutationObserverId: MutationObserverId,
): void {
const deleted = registeredMutationObservers.delete(mutationObserverId);
if (deleted && registeredMutationObservers.size === 0) {
// When there are no observers left, we can disconnect the native module
// so we don't need to check commits for mutations.
NativeMutationObserver?.disconnect();
isConnected = false;
}
}
export function observe({
mutationObserverId,
target,
subtree,
}: {
mutationObserverId: MutationObserverId,
target: ReactNativeElement,
subtree: boolean,
}): void {
if (NativeMutationObserver == null) {
warnNoNativeMutationObserver();
return;
}
const registeredObserver =
registeredMutationObservers.get(mutationObserverId);
if (registeredObserver == null) {
console.error(
`MutationObserverManager: could not start observing target because MutationObserver with ID ${mutationObserverId} was not registered.`,
);
return;
}
const targetShadowNode = getShadowNode(target);
if (targetShadowNode == null) {
console.error(
'MutationObserverManager: could not find reference to host node from target',
);
return;
}
if (!isConnected) {
NativeMutationObserver.connect(
notifyMutationObservers,
// We need to do this operation from native to make sure we're retaining
// the public instance immediately when mutations occur. Otherwise React
// could dereference it in the instance handle and we wouldn't be able to
// access it.
// $FlowExpectedError[incompatible-call] This is typed as (mixed) => mixed in the native module because the codegen doesn't support the actual types.
getPublicInstanceFromInternalInstanceHandle,
);
isConnected = true;
}
return NativeMutationObserver.observe({
mutationObserverId,
targetShadowNode,
subtree,
});
}
export function unobserve(
mutationObserverId: number,
target: ReactNativeElement,
): void {
if (NativeMutationObserver == null) {
warnNoNativeMutationObserver();
return;
}
const registeredObserver =
registeredMutationObservers.get(mutationObserverId);
if (registeredObserver == null) {
console.error(
`MutationObserverManager: could not stop observing target because MutationObserver with ID ${mutationObserverId} was not registered.`,
);
return;
}
const targetShadowNode = getShadowNode(target);
if (targetShadowNode == null) {
console.error(
'MutationObserverManager: could not find reference to host node from target',
);
return;
}
NativeMutationObserver.unobserve(mutationObserverId, targetShadowNode);
}
/**
* This function is called from native when there are `MutationObserver`
* entries to dispatch.
*/
function notifyMutationObservers(): void {
Systrace.beginEvent('MutationObserverManager.notifyMutationObservers');
try {
doNotifyMutationObservers();
} finally {
Systrace.endEvent();
}
}
function doNotifyMutationObservers(): void {
if (NativeMutationObserver == null) {
warnNoNativeMutationObserver();
return;
}
const nativeRecords = NativeMutationObserver.takeRecords();
const entriesByObserver: Map<
MutationObserverId,
Array<MutationRecord>,
> = new Map();
for (const nativeRecord of nativeRecords) {
let list = entriesByObserver.get(nativeRecord.mutationObserverId);
if (list == null) {
list = [];
entriesByObserver.set(nativeRecord.mutationObserverId, list);
}
list.push(createMutationRecord(nativeRecord));
}
for (const [mutationObserverId, entriesForObserver] of entriesByObserver) {
const registeredObserver =
registeredMutationObservers.get(mutationObserverId);
if (!registeredObserver) {
// This could happen if the observer is disconnected between commit
// and mount. In this case, we can just ignore the entries.
return;
}
const {observer, callback} = registeredObserver;
try {
callback.call(observer, entriesForObserver, observer);
} catch (error) {
console.error(error);
}
}
}
function warnNoNativeMutationObserver() {
warnOnce(
'missing-native-mutation-observer',
'Missing native implementation of MutationObserver',
);
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import type ReadOnlyNode from '../../src/private/webapis/dom/nodes/ReadOnlyNode';
import type {NativeMutationRecord} from './NativeMutationObserver';
import NodeList, {
createNodeList,
} from '../../src/private/webapis/dom/oldstylecollections/NodeList';
export type MutationType = 'attributes' | 'characterData' | 'childList';
/**
* The `MutationRecord` is a read-only interface that represents an individual
* DOM mutation observed by a `MutationObserver`.
*
* It is the object inside the array passed to the callback of a `MutationObserver`.
*/
export default class MutationRecord {
_target: ReactNativeElement;
_addedNodes: NodeList<ReadOnlyNode>;
_removedNodes: NodeList<ReadOnlyNode>;
constructor(nativeRecord: NativeMutationRecord) {
// $FlowExpectedError[incompatible-type] the codegen doesn't support the actual type.
const target: ReactNativeElement = nativeRecord.target;
this._target = target;
// $FlowExpectedError[incompatible-type] the codegen doesn't support the actual type.
const addedNodes: $ReadOnlyArray<ReadOnlyNode> = nativeRecord.addedNodes;
this._addedNodes = createNodeList(addedNodes);
const removedNodes: $ReadOnlyArray<ReadOnlyNode> =
// $FlowFixMe[incompatible-type]
nativeRecord.removedNodes;
this._removedNodes = createNodeList(removedNodes);
}
get addedNodes(): NodeList<ReadOnlyNode> {
return this._addedNodes;
}
get attributeName(): string | null {
return null;
}
get nextSibling(): ReadOnlyNode | null {
return null;
}
get oldValue(): mixed | null {
return null;
}
get previousSibling(): ReadOnlyNode | null {
return null;
}
get removedNodes(): NodeList<ReadOnlyNode> {
return this._removedNodes;
}
get target(): ReactNativeElement {
return this._target;
}
get type(): MutationType {
return 'childList';
}
}
export function createMutationRecord(
entry: NativeMutationRecord,
): MutationRecord {
return new MutationRecord(entry);
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "NativeMutationObserver.h"
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/primitives.h>
#include "Plugins.h"
std::shared_ptr<facebook::react::TurboModule>
NativeMutationObserverModuleProvider(
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::NativeMutationObserver>(
std::move(jsInvoker));
}
namespace facebook::react {
static UIManager& getUIManagerFromRuntime(jsi::Runtime& runtime) {
return UIManagerBinding::getBinding(runtime)->getUIManager();
}
NativeMutationObserver::NativeMutationObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeMutationObserverCxxSpec(std::move(jsInvoker)) {}
void NativeMutationObserver::observe(
jsi::Runtime& runtime,
NativeMutationObserverObserveOptions options) {
auto mutationObserverId = options.mutationObserverId;
auto subtree = options.subtree;
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options).targetShadowNode);
auto& uiManager = getUIManagerFromRuntime(runtime);
mutationObserverManager_.observe(
mutationObserverId, shadowNode, subtree, uiManager);
}
void NativeMutationObserver::unobserve(
jsi::Runtime& runtime,
MutationObserverId mutationObserverId,
jsi::Object targetShadowNode) {
auto shadowNode = shadowNodeFromValue(runtime, std::move(targetShadowNode));
mutationObserverManager_.unobserve(mutationObserverId, *shadowNode);
}
void NativeMutationObserver::connect(
jsi::Runtime& runtime,
AsyncCallback<> notifyMutationObservers,
jsi::Function getPublicInstanceFromInstanceHandle) {
auto& uiManager = getUIManagerFromRuntime(runtime);
// MutationObserver is not compatible with background executor.
// When using background executor, we commit trees outside the JS thread.
// In that case, we can't safely access the JS runtime in commit hooks to
// get references to mutated nodes (which we need to do at that point
// to ensure we are retaining removed nodes).
if (uiManager.hasBackgroundExecutor()) {
throw jsi::JSError(
runtime,
"MutationObserver: could not start observation because MutationObserver is incompatible with UIManager using background executor.");
}
getPublicInstanceFromInstanceHandle_ =
jsi::Value(runtime, getPublicInstanceFromInstanceHandle);
// This should always be called from the JS thread, as it's unsafe to call
// into JS otherwise (via `getPublicInstanceFromInstanceHandle`).
getPublicInstanceFromShadowNode_ = [&](const ShadowNode& shadowNode) {
auto instanceHandle = shadowNode.getInstanceHandle(runtime);
if (!instanceHandle.isObject() ||
!getPublicInstanceFromInstanceHandle_.isObject() ||
!getPublicInstanceFromInstanceHandle_.asObject(runtime).isFunction(
runtime)) {
return jsi::Value::null();
}
return getPublicInstanceFromInstanceHandle_.asObject(runtime)
.asFunction(runtime)
.call(runtime, instanceHandle);
};
notifyMutationObservers_ = std::move(notifyMutationObservers);
auto onMutationsCallback = [&](std::vector<MutationRecord>& records) {
return onMutations(records);
};
mutationObserverManager_.connect(uiManager, std::move(onMutationsCallback));
}
void NativeMutationObserver::disconnect(jsi::Runtime& runtime) {
auto& uiManager = getUIManagerFromRuntime(runtime);
mutationObserverManager_.disconnect(uiManager);
getPublicInstanceFromInstanceHandle_ = jsi::Value::undefined();
getPublicInstanceFromShadowNode_ = nullptr;
notifyMutationObservers_ = nullptr;
}
std::vector<NativeMutationRecord> NativeMutationObserver::takeRecords(
jsi::Runtime& /*runtime*/) {
notifiedMutationObservers_ = false;
std::vector<NativeMutationRecord> records;
pendingRecords_.swap(records);
return records;
}
std::vector<jsi::Value>
NativeMutationObserver::getPublicInstancesFromShadowNodes(
const std::vector<ShadowNode::Shared>& shadowNodes) const {
std::vector<jsi::Value> publicInstances;
publicInstances.reserve(shadowNodes.size());
for (const auto& shadowNode : shadowNodes) {
publicInstances.push_back(getPublicInstanceFromShadowNode_(*shadowNode));
}
return publicInstances;
}
void NativeMutationObserver::onMutations(std::vector<MutationRecord>& records) {
SystraceSection s("NativeMutationObserver::onMutations");
for (const auto& record : records) {
pendingRecords_.emplace_back(NativeMutationRecord{
record.mutationObserverId,
// FIXME(T157129303) Instead of assuming we can call into JS from here,
// we should use an API that explicitly indicates it.
getPublicInstanceFromShadowNode_(*record.targetShadowNode),
getPublicInstancesFromShadowNodes(record.addedShadowNodes),
getPublicInstancesFromShadowNodes(record.removedShadowNodes)});
}
notifyMutationObserversIfNecessary();
}
/**
* This method allows us to avoid scheduling multiple calls to notify observers
* in the JS thread. We schedule one and skip subsequent ones (we just append
* the records to the pending list and wait for the scheduled task to consume
* all of them).
*/
void NativeMutationObserver::notifyMutationObserversIfNecessary() {
bool dispatchNotification = false;
if (!pendingRecords_.empty() && !notifiedMutationObservers_) {
notifiedMutationObservers_ = true;
dispatchNotification = true;
}
if (dispatchNotification) {
SystraceSection s("NativeMutationObserver::notifyObservers");
notifyMutationObservers_();
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#include <react/renderer/observers/mutation/MutationObserverManager.h>
#include <react/renderer/uimanager/UIManager.h>
#include <optional>
#include <string>
#include <vector>
namespace facebook::react {
using NativeMutationObserverObserveOptions =
NativeMutationObserverCxxNativeMutationObserverObserveOptions<
// mutationObserverId
MutationObserverId,
// targetShadowNode
jsi::Object,
// subtree
bool>;
template <>
struct Bridging<NativeMutationObserverObserveOptions>
: NativeMutationObserverCxxNativeMutationObserverObserveOptionsBridging<
NativeMutationObserverObserveOptions> {};
using NativeMutationRecord = NativeMutationObserverCxxNativeMutationRecord<
// mutationObserverId
MutationObserverId,
// target
jsi::Value,
// addedNodes
std::vector<jsi::Value>,
// removedNodes
std::vector<jsi::Value>>;
template <>
struct Bridging<NativeMutationRecord>
: NativeMutationObserverCxxNativeMutationRecordBridging<
NativeMutationRecord> {};
class NativeMutationObserver
: public NativeMutationObserverCxxSpec<NativeMutationObserver>,
std::enable_shared_from_this<NativeMutationObserver> {
public:
NativeMutationObserver(std::shared_ptr<CallInvoker> jsInvoker);
void observe(
jsi::Runtime& runtime,
NativeMutationObserverObserveOptions options);
void unobserve(
jsi::Runtime& runtime,
MutationObserverId mutationObserverId,
jsi::Object targetShadowNode);
void connect(
jsi::Runtime& runtime,
AsyncCallback<> notifyMutationObservers,
jsi::Function getPublicInstanceFromInstanceHandle);
void disconnect(jsi::Runtime& runtime);
std::vector<NativeMutationRecord> takeRecords(jsi::Runtime& runtime);
private:
MutationObserverManager mutationObserverManager_{};
std::vector<NativeMutationRecord> pendingRecords_;
// This is passed to `connect` so we can retain references to public instances
// when mutation occur, before React cleans up unmounted instances.
jsi::Value getPublicInstanceFromInstanceHandle_ = jsi::Value::undefined();
std::function<jsi::Value(const ShadowNode&)> getPublicInstanceFromShadowNode_;
bool notifiedMutationObservers_{};
std::function<void()> notifyMutationObservers_;
void onMutations(std::vector<MutationRecord>& records);
void notifyMutationObserversIfNecessary();
std::vector<jsi::Value> getPublicInstancesFromShadowNodes(
const std::vector<ShadowNode::Shared>& shadowNodes) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
export * from '../../src/private/specs/modules/NativeMutationObserver';
import NativeMutationObserver from '../../src/private/specs/modules/NativeMutationObserver';
export default NativeMutationObserver;

View File

@@ -0,0 +1,327 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
/**
* This is a mock of `NativeMutationObserver` implementing the same logic as the
* native module and integrating with the existing mock for `FabricUIManager`.
* This allows us to test all the JavaScript code for IntersectionObserver in
* JavaScript as an integration test using only public APIs.
*/
import type {NodeSet} from '../../ReactNative/FabricUIManager';
import type {RootTag} from '../../ReactNative/RootTag';
import type {
InternalInstanceHandle,
Node,
} from '../../Renderer/shims/ReactNativeTypes';
import type {
MutationObserverId,
NativeMutationObserverObserveOptions,
NativeMutationRecord,
Spec,
} from '../NativeMutationObserver';
import ReadOnlyNode from '../../../src/private/webapis/dom/nodes/ReadOnlyNode';
import {
type NodeMock,
type UIManagerCommitHook,
fromNode,
getFabricUIManager,
getNodeInChildSet,
} from '../../ReactNative/__mocks__/FabricUIManager';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
let pendingRecords: Array<NativeMutationRecord> = [];
let callback: ?() => void;
let getPublicInstance: ?(instanceHandle: InternalInstanceHandle) => mixed;
let observersByRootTag: Map<
RootTag,
Map<MutationObserverId, {deep: Set<Node>, shallow: Set<Node>}>,
> = new Map();
const FabricUIManagerMock = nullthrows(getFabricUIManager());
function getMockDataFromShadowNode(node: mixed): NodeMock {
// $FlowExpectedError[incompatible-call]
return fromNode(node);
}
function castToNode(node: mixed): Node {
// $FlowExpectedError[incompatible-return]
return node;
}
const NativeMutationMock = {
observe: (options: NativeMutationObserverObserveOptions): void => {
const targetShadowNode = castToNode(options.targetShadowNode);
const rootTag = getMockDataFromShadowNode(options.targetShadowNode).rootTag;
let observers = observersByRootTag.get(rootTag);
if (observers == null) {
observers = new Map();
observersByRootTag.set(rootTag, observers);
}
let observations = observers.get(options.mutationObserverId);
if (observations == null) {
observations = {deep: new Set(), shallow: new Set()};
observers.set(options.mutationObserverId, observations);
}
const isTargetBeingObserved =
observations.deep.has(targetShadowNode) ||
observations.shallow.has(targetShadowNode);
invariant(!isTargetBeingObserved, 'unexpected duplicate call to observe');
if (options.subtree) {
observations.deep.add(targetShadowNode);
} else {
observations.shallow.add(targetShadowNode);
}
},
unobserve: (mutationObserverId: number, target: mixed): void => {
const targetShadowNode = castToNode(target);
const observers = observersByRootTag.get(
getMockDataFromShadowNode(targetShadowNode).rootTag,
);
const observations = observers?.get(mutationObserverId);
invariant(observations != null, 'unexpected call to unobserve');
const isTargetBeingObserved =
observations.deep.has(targetShadowNode) ||
observations.shallow.has(targetShadowNode);
invariant(isTargetBeingObserved, 'unexpected call to unobserve');
observations.deep.delete(targetShadowNode);
observations.shallow.delete(targetShadowNode);
},
connect: (
notifyMutationObserversCallback: () => void,
getPublicInstanceFromInstanceHandle: (
instanceHandle: InternalInstanceHandle,
) => mixed,
): void => {
invariant(callback == null, 'unexpected call to connect');
callback = notifyMutationObserversCallback;
getPublicInstance = getPublicInstanceFromInstanceHandle;
FabricUIManagerMock.__addCommitHook(NativeMutationObserverCommitHook);
},
disconnect: (): void => {
invariant(callback != null, 'unexpected call to disconnect');
callback = null;
FabricUIManagerMock.__removeCommitHook(NativeMutationObserverCommitHook);
},
takeRecords: (): $ReadOnlyArray<NativeMutationRecord> => {
const currentRecords = pendingRecords;
pendingRecords = [];
return currentRecords;
},
};
(NativeMutationMock: Spec);
export default NativeMutationMock;
const NativeMutationObserverCommitHook: UIManagerCommitHook = {
shadowTreeWillCommit: (rootTag, oldChildSet, newChildSet) => {
runMutationObservations(rootTag, oldChildSet, newChildSet);
},
};
function runMutationObservations(
rootTag: RootTag,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
): void {
const observers = observersByRootTag.get(rootTag);
if (!observers) {
return;
}
const newRecords: Array<NativeMutationRecord> = [];
for (const [mutationObserverId, observations] of observers) {
const processedNodes: Set<Node> = new Set();
for (const targetShadowNode of observations.deep) {
runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree: true,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
});
}
for (const targetShadowNode of observations.shallow) {
runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree: false,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
});
}
}
for (const record of newRecords) {
pendingRecords.push(record);
}
notifyObserversIfNecessary();
}
function findNodeOfSameFamily(list: NodeSet, node: Node): ?Node {
for (const current of list) {
if (fromNode(current).reactTag === fromNode(node).reactTag) {
return current;
}
}
return;
}
function recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode,
newNode,
newRecords,
processedNodes,
}: {
mutationObserverId: MutationObserverId,
targetShadowNode: Node,
subtree: boolean,
oldNode: Node,
newNode: Node,
newRecords: Array<NativeMutationRecord>,
processedNodes: Set<Node>,
}): void {
// If the nodes are referentially equal, their children are also the same.
if (oldNode === newNode || processedNodes.has(newNode)) {
return;
}
processedNodes.add(newNode);
const oldChildren = fromNode(oldNode).children;
const newChildren = fromNode(newNode).children;
const addedNodes = [];
const removedNodes = [];
// Check for removed nodes (and equal nodes for further inspection later)
for (const oldChild of oldChildren) {
const newChild = findNodeOfSameFamily(newChildren, oldChild);
if (newChild == null) {
removedNodes.push(oldChild);
} else if (subtree) {
recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode: oldChild,
newNode: newChild,
newRecords,
processedNodes,
});
}
}
// Check for added nodes
for (const newChild of newChildren) {
const oldChild = findNodeOfSameFamily(oldChildren, newChild);
if (oldChild == null) {
addedNodes.push(newChild);
}
}
if (addedNodes.length > 0 || removedNodes.length > 0) {
newRecords.push({
mutationObserverId: mutationObserverId,
target: nullthrows(getPublicInstance)(
getMockDataFromShadowNode(targetShadowNode).instanceHandle,
),
addedNodes: addedNodes.map(node => {
const readOnlyNode = nullthrows(getPublicInstance)(
fromNode(node).instanceHandle,
);
invariant(
readOnlyNode instanceof ReadOnlyNode,
'expected instance of ReadOnlyNode',
);
return readOnlyNode;
}),
removedNodes: removedNodes.map(node => {
const readOnlyNode = nullthrows(getPublicInstance)(
fromNode(node).instanceHandle,
);
invariant(
readOnlyNode instanceof ReadOnlyNode,
'expected instance of ReadOnlyNode',
);
return readOnlyNode;
}),
});
}
}
function runMutationObservation({
mutationObserverId,
targetShadowNode,
subtree,
oldChildSet,
newChildSet,
newRecords,
processedNodes,
}: {
mutationObserverId: MutationObserverId,
targetShadowNode: Node,
subtree: boolean,
oldChildSet: ?NodeSet,
newChildSet: NodeSet,
newRecords: Array<NativeMutationRecord>,
processedNodes: Set<Node>,
}): void {
if (!oldChildSet) {
return;
}
const oldTargetShadowNode = getNodeInChildSet(targetShadowNode, oldChildSet);
if (oldTargetShadowNode == null) {
return;
}
const newTargetShadowNode = getNodeInChildSet(targetShadowNode, newChildSet);
if (newTargetShadowNode == null) {
return;
}
recordMutations({
mutationObserverId,
targetShadowNode,
subtree,
oldNode: oldTargetShadowNode,
newNode: newTargetShadowNode,
newRecords,
processedNodes,
});
}
function notifyObserversIfNecessary(): void {
if (pendingRecords.length > 0) {
// We schedule these using regular tasks in native because microtasks are
// still not properly supported.
setTimeout(() => callback?.(), 0);
}
}