// Copyright 2024 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { process, render } from 'preact';
import { runWithForce } from './runWithForce.js';
import { updateGlobalProps as updateGlobalPropsCore } from '../../core/globalProps.js';
import { updateCardData } from '../../core/lynx-update-data.js';
import { PerformanceTimingFlags, PipelineOrigins, beginPipeline, markTiming } from '../../core/performance.js';
import {
delayedRunOnMainThreadData,
takeDelayedRunOnMainThreadData,
} from '../../core/thread-function-call/main-thread.js';
import { __root } from '../../root.js';
import { profileEnd, profileStart } from '../../shared/profile.js';
import { CHILDREN } from '../../shared/render-constants.js';
import { printSnapshotInstanceToString } from '../debug/printSnapshot.js';
import { getSnapshotVNodeSource } from '../debug/vnodeSource.js';
import { LifecycleConstant } from '../lifecycle/constant.js';
import type { FirstScreenData } from '../lifecycle/constant.js';
import { destroyBackground } from '../lifecycle/destroy.js';
import { delayedEvents, delayedPublishEvent } from '../lifecycle/event/delayEvents.js';
import { delayLifecycleEvent, delayedLifecycleEvents } from '../lifecycle/event/delayLifecycleEvents.js';
import { commitPatchUpdate, genCommitTaskId, globalCommitTaskMap } from '../lifecycle/patch/commit.js';
import type { PatchList } from '../lifecycle/patch/commit.js';
import { removeCtxNotFoundEventListener } from '../lifecycle/patch/error.js';
import { runDelayedUiOps } from '../lifecycle/ref/delay.js';
import { reloadBackground } from '../lifecycle/reload.js';
import {
BackgroundSnapshotInstance,
backgroundSnapshotInstanceManager,
hydrate,
} from '../snapshot/backgroundSnapshot.js';
import type { SerializedSnapshotInstance } from '../snapshot/types.js';
import { destroyWorklet } from '../worklet/destroy.js';
import { sendMTRefInitValueToMainThread } from '../worklet/ref/updateInitValue.js';
export { runWithForce };
function injectTt(): void {
const tt = lynx.getApp();
tt.OnLifecycleEvent = onLifecycleEvent;
tt.publishEvent = delayedPublishEvent;
tt.publicComponentEvent = delayedPublicComponentEvent;
tt.callDestroyLifetimeFun = () => {
removeCtxNotFoundEventListener();
destroyWorklet();
destroyBackground();
};
tt.updateGlobalProps = updateGlobalProps;
tt.updateCardData = updateCardData;
tt.onAppReload = reloadBackground;
tt.processCardConfig = () => {
// used to updateTheme, no longer rely on this function
};
}
function onLifecycleEvent([type, data]: [LifecycleConstant, unknown]) {
const hasRootRendered = CHILDREN in __root;
// never called `render(, __root)`
// happens if user call `root.render()` async
if (!hasRootRendered) {
delayLifecycleEvent(type, data);
return;
}
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
profileStart(`OnLifecycleEvent::${type}`);
}
try {
onLifecycleEventImpl(type, data);
} catch (e) {
lynx.reportError(e as Error);
}
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
profileEnd();
}
}
function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void {
switch (type) {
case LifecycleConstant.firstScreen: {
let processErr;
try {
process();
} catch (e) {
processErr = e;
}
const { root: lepusSide, firstScreenEventIdSwap } = data as FirstScreenData;
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
profileStart('ReactLynx::hydrate');
}
beginPipeline(true, PipelineOrigins.reactLynxHydrate, PerformanceTimingFlags.reactLynxHydrate);
markTiming('hydrateParseSnapshotStart');
const before = JSON.parse(lepusSide) as SerializedSnapshotInstance;
if (typeof __ALOG__ !== 'undefined' && __ALOG__) {
console.alog?.(
'[ReactLynxDebug] MTS -> BTS OnLifecycleEvent:\n' + JSON.stringify(
{
...data as object,
// use parsed lepusSide to avoid extra escape characters ('\\')
root: before,
},
null,
2,
),
);
console.alog?.(
'[ReactLynxDebug] SnapshotInstance tree for first screen hydration:\n'
+ printSnapshotInstanceToString(before),
);
console.alog?.(
'[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration:\n'
+ printSnapshotInstanceToString(__root as BackgroundSnapshotInstance),
);
}
markTiming('hydrateParseSnapshotEnd');
markTiming('diffVdomStart');
const snapshotPatch = hydrate(
before,
__root as BackgroundSnapshotInstance,
);
if (typeof __ALOG__ !== 'undefined' && __ALOG__) {
console.alog?.(
'[ReactLynxDebug] BackgroundSnapshotInstance after hydration:\n'
+ printSnapshotInstanceToString(__root as BackgroundSnapshotInstance),
);
}
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
profileEnd();
}
markTiming('diffVdomEnd');
// TODO: It seems `delayedEvents` and `delayedLifecycleEvents` should be merged into one array to ensure the proper order of events.
flushDelayedLifecycleEvents();
if (delayedEvents) {
delayedEvents.forEach((args) => {
const [handlerName, data] = args;
// eslint-disable-next-line prefer-const
let [idStr, ...rest] = handlerName.split(':');
while (firstScreenEventIdSwap[idStr!]) idStr = firstScreenEventIdSwap[idStr!]?.toString();
try {
publishEvent([idStr, ...rest].join(':'), data);
} catch (e) {
lynx.reportError(e as Error);
}
});
delayedEvents.length = 0;
}
lynx.getApp().publishEvent = publishEvent;
lynx.getApp().publicComponentEvent = publicComponentEvent;
// console.debug("********** After hydration:");
// printSnapshotInstance(__root as BackgroundSnapshotInstance);
const commitTaskId = genCommitTaskId();
const patchList: PatchList = {
patchList: [{ snapshotPatch, id: commitTaskId }],
};
if (delayedRunOnMainThreadData.length) {
patchList.delayedRunOnMainThreadData = takeDelayedRunOnMainThreadData();
}
const obj = commitPatchUpdate(patchList, { isHydration: true });
sendMTRefInitValueToMainThread();
lynx.getNativeApp().callLepusMethod(LifecycleConstant.patchUpdate, obj, () => {
globalCommitTaskMap.forEach((commitTask, id) => {
if (id > commitTaskId) {
return;
}
commitTask();
globalCommitTaskMap.delete(id);
});
});
runDelayedUiOps();
if (processErr) {
throw processErr;
}
break;
}
case LifecycleConstant.globalEventFromLepus: {
const [eventName, params] = data as [string, Record];
lynx.getJSModule('GlobalEventEmitter').trigger(eventName, params);
break;
}
case LifecycleConstant.publishEvent: {
const { handlerName, data: d } = data as { handlerName: string; data: EventDataType };
lynx.getApp().publishEvent(handlerName, d);
break;
}
}
}
let flushingDelayedLifecycleEvents = false;
function flushDelayedLifecycleEvents(): void {
// avoid stackoverflow
if (flushingDelayedLifecycleEvents) return;
flushingDelayedLifecycleEvents = true;
if (delayedLifecycleEvents) {
delayedLifecycleEvents.forEach((e) => {
onLifecycleEvent(e);
});
delayedLifecycleEvents.length = 0;
}
flushingDelayedLifecycleEvents = false;
}
function publishEvent(handlerName: string, data: EventDataType) {
lynx.getApp().callBeforePublishEvent?.(data);
let snapshotId: number | undefined;
const getSnapshotId = () => snapshotId ??= Number(handlerName.split(':')[0]);
const eventHandler = backgroundSnapshotInstanceManager.getValueBySign(
handlerName,
) as ((data: unknown) => void) | undefined;
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
const currentSnapshotId = getSnapshotId();
profileStart(`ReactLynx::publishEvent`, {
args: {
handlerName,
type: data.type,
snapshotType: backgroundSnapshotInstanceManager.values.get(
currentSnapshotId,
)?.type ?? '',
source: getSnapshotVNodeSource(currentSnapshotId) ?? '',
jsFunctionName: eventHandler?.name ?? '',
},
});
}
if (typeof __ALOG__ !== 'undefined' && __ALOG__) {
const currentSnapshotId = getSnapshotId();
console.alog?.(
`[ReactLynxDebug] BTS received event:\n` + JSON.stringify(
{
handlerName,
type: data.type,
snapshotType: backgroundSnapshotInstanceManager.values.get(
currentSnapshotId,
)?.type ?? '',
jsFunctionName: eventHandler?.name ?? '',
},
null,
2,
),
);
}
if (eventHandler) {
try {
eventHandler(data);
} catch (e) {
lynx.reportError(e as Error);
}
}
if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) {
profileEnd();
}
}
function publicComponentEvent(_componentId: string, handlerName: string, data: EventDataType) {
publishEvent(handlerName, data);
}
function delayedPublicComponentEvent(_componentId: string, handlerName: string, data: EventDataType) {
delayedPublishEvent(handlerName, data);
}
function updateGlobalProps(newData: Record): void {
updateGlobalPropsCore(newData, {
// Snapshot force render consumes any sync setState dirty flags produced by
// onGlobalPropsChanged listeners, avoiding an extra diff pass.
forceRerender: () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
runWithForce(() => render(__root.__jsx, __root as any));
},
});
}
export { injectTt, flushDelayedLifecycleEvents };