From 09c37cdba89d4642d87f9a2cc91ed40b5ade3bb2 Mon Sep 17 00:00:00 2001 From: Yradex <11014207+Yradex@users.noreply.github.com> Date: Wed, 20 May 2026 11:37:18 +0800 Subject: [PATCH 1/2] feat(react): support ET updateCardData bridge --- .../__test__/core/lynx-update-data.test.ts | 77 +++++++++ .../background/init-data-update/index.tsx | 11 ++ .../element-template/lynx/update-data.test.ts | 108 ++++++++++++ .../element-template/native/index.test.ts | 6 + .../runtime/background/commit-hook.test.ts | 71 ++++++++ .../init-data-compiled-fixtures.test.tsx | 159 ++++++++++++++++++ .../patch/element-template-patch.test.tsx | 11 +- .../snapshot/compat/initData.test.jsx | 38 +++++ .../runtime/src/core/lynx-update-data.ts | 40 +++++ .../background/commit-hook.ts | 7 +- .../src/element-template/lynx/update-data.ts | 18 ++ .../src/element-template/native/index.ts | 2 + .../react/runtime/src/snapshot/lynx/tt.ts | 19 +-- 13 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 packages/react/runtime/__test__/core/lynx-update-data.test.ts create mode 100644 packages/react/runtime/__test__/element-template/fixtures/background/init-data-update/index.tsx create mode 100644 packages/react/runtime/__test__/element-template/lynx/update-data.test.ts create mode 100644 packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx create mode 100644 packages/react/runtime/src/core/lynx-update-data.ts create mode 100644 packages/react/runtime/src/element-template/lynx/update-data.ts diff --git a/packages/react/runtime/__test__/core/lynx-update-data.test.ts b/packages/react/runtime/__test__/core/lynx-update-data.test.ts new file mode 100644 index 0000000000..8a39b7d243 --- /dev/null +++ b/packages/react/runtime/__test__/core/lynx-update-data.test.ts @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { applyInitDataUpdateFromNative, NativeUpdateDataType } from '../../src/core/lynx-update-data.js'; + +describe('applyInitDataUpdateFromNative', () => { + let originalInitData: typeof lynx.__initData; + let originalReportError: typeof lynx.reportError; + + beforeEach(() => { + originalInitData = lynx.__initData; + originalReportError = lynx.reportError; + lynx.__initData = {}; + lynx.reportError = vi.fn(); + }); + + afterEach(() => { + lynx.__initData = originalInitData; + lynx.reportError = originalReportError; + }); + + it('COW merges update data and returns the current patch data', () => { + const previousInitData = { msg: 'init', stable: true }; + lynx.__initData = previousInitData; + + const restNewData = applyInitDataUpdateFromNative({ + msg: 'update', + next: 1, + }); + + expect(restNewData).toEqual({ msg: 'update', next: 1 }); + expect(lynx.__initData).toEqual({ msg: 'update', stable: true, next: 1 }); + expect(lynx.__initData).not.toBe(previousInitData); + expect(lynx.reportError).not.toHaveBeenCalled(); + }); + + it('clears existing initData before RESET updates', () => { + lynx.__initData = { stale: true, msg: 'init' }; + + const restNewData = applyInitDataUpdateFromNative( + { msg: 'reset' }, + { type: NativeUpdateDataType.RESET }, + ); + + expect(restNewData).toEqual({ msg: 'reset' }); + expect(lynx.__initData).toEqual({ msg: 'reset' }); + }); + + it('keeps Snapshot-compatible loose RESET matching', () => { + lynx.__initData = { stale: true, msg: 'init' }; + + const restNewData = applyInitDataUpdateFromNative( + { msg: 'reset' }, + { type: '1' as unknown as NativeUpdateDataType }, + ); + + expect(restNewData).toEqual({ msg: 'reset' }); + expect(lynx.__initData).toEqual({ msg: 'reset' }); + }); + + it('reports and strips __lynx_timing_flag from the merged data and return value', () => { + lynx.__initData = { msg: 'init' }; + + const restNewData = applyInitDataUpdateFromNative({ + msg: 'update', + __lynx_timing_flag: '__lynx_timing_actual_fmp', + }); + + expect(restNewData).toEqual({ msg: 'update' }); + expect(lynx.__initData).toEqual({ msg: 'update' }); + expect(lynx.reportError).toHaveBeenCalledTimes(1); + expect(lynx.reportError).toHaveBeenCalledWith( + new Error( + 'Received unsupported updateData with `__lynx_timing_flag` (value "__lynx_timing_actual_fmp"), the timing flag is ignored', + ), + ); + }); +}); diff --git a/packages/react/runtime/__test__/element-template/fixtures/background/init-data-update/index.tsx b/packages/react/runtime/__test__/element-template/fixtures/background/init-data-update/index.tsx new file mode 100644 index 0000000000..bcfb01a129 --- /dev/null +++ b/packages/react/runtime/__test__/element-template/fixtures/background/init-data-update/index.tsx @@ -0,0 +1,11 @@ +import { useInitData } from '@lynx-js/react/element-template'; + +export function App(): JSX.Element { + const data = useInitData() as { msg?: string }; + + return ( + + {data.msg} + + ); +} diff --git a/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts b/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts new file mode 100644 index 0000000000..9d7d2c72b1 --- /dev/null +++ b/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts @@ -0,0 +1,108 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { NativeUpdateDataType } from '../../../src/core/lynx-update-data.js'; +import { updateCardData } from '../../../src/element-template/lynx/update-data.js'; +import { ElementTemplateEnvManager } from '../test-utils/debug/envManager.js'; + +type Listener = (...args: unknown[]) => void; + +class LynxGlobalEventEmitter { + private listeners = new Map>(); + + addListener(eventName: string, listener: Listener): void { + const listeners = this.listeners.get(eventName); + if (listeners) { + listeners.add(listener); + return; + } + this.listeners.set(eventName, new Set([listener])); + } + + removeListener(eventName: string, listener: Listener): void { + const listeners = this.listeners.get(eventName); + listeners?.delete(listener); + } + + emit(eventName: string, args?: unknown[]): void { + for (const listener of this.listeners.get(eventName) ?? []) { + listener(...(args ?? [])); + } + } +} + +describe('ElementTemplate updateCardData', () => { + const envManager = new ElementTemplateEnvManager(); + let originalLynx: typeof lynx; + let emitter: LynxGlobalEventEmitter; + let reportError: ReturnType; + + function installLynx(initData: Record): void { + const baseLynx = globalThis.lynx; + vi.stubGlobal('lynx', { + ...baseLynx, + __initData: initData, + reportError, + getJSModule(moduleName: string) { + if (moduleName === 'GlobalEventEmitter') { + return emitter; + } + return baseLynx.getJSModule?.(moduleName); + }, + }); + } + + beforeEach(() => { + originalLynx = globalThis.lynx; + emitter = new LynxGlobalEventEmitter(); + reportError = vi.fn(); + envManager.resetEnv('background'); + }); + + afterEach(() => { + vi.stubGlobal('lynx', originalLynx); + }); + + it('updates initData and emits restNewData through the ET listener channel', () => { + installLynx({ msg: 'init', stable: true }); + const listener = vi.fn(); + emitter.addListener('onDataChanged', listener); + + updateCardData({ msg: 'update', next: 1 }); + + expect(lynx.__initData).toEqual({ msg: 'update', stable: true, next: 1 }); + expect(listener).toHaveBeenCalledWith({ msg: 'update', next: 1 }); + expect(reportError).not.toHaveBeenCalled(); + }); + + it('clears previous initData when RESET is requested', () => { + installLynx({ stale: true, msg: 'init' }); + const listener = vi.fn(); + emitter.addListener('onDataChanged', listener); + + updateCardData( + { msg: 'reset' }, + { type: NativeUpdateDataType.RESET }, + ); + + expect(lynx.__initData).toEqual({ msg: 'reset' }); + expect(listener).toHaveBeenCalledWith({ msg: 'reset' }); + }); + + it('reports and strips __lynx_timing_flag before emitting onDataChanged', () => { + installLynx({ msg: 'init' }); + const listener = vi.fn(); + emitter.addListener('onDataChanged', listener); + + updateCardData({ + msg: 'update', + __lynx_timing_flag: '__lynx_timing_actual_fmp', + }); + + expect(lynx.__initData).toEqual({ msg: 'update' }); + expect(listener).toHaveBeenCalledWith({ msg: 'update' }); + expect(reportError).toHaveBeenCalledTimes(1); + expect(String(reportError.mock.calls[0]?.[0]?.message ?? '')).toBe( + 'Received unsupported updateData with `__lynx_timing_flag` (value "__lynx_timing_actual_fmp"), the timing flag is ignored', + ); + }); +}); diff --git a/packages/react/runtime/__test__/element-template/native/index.test.ts b/packages/react/runtime/__test__/element-template/native/index.test.ts index 201bb44eb6..08d4825538 100644 --- a/packages/react/runtime/__test__/element-template/native/index.test.ts +++ b/packages/react/runtime/__test__/element-template/native/index.test.ts @@ -30,6 +30,7 @@ describe('element-template native index wiring', () => { vi.doUnmock('../../../src/element-template/debug/profile.js'); vi.doUnmock('../../../src/element-template/lynx/env.js'); vi.doUnmock('../../../src/element-template/lynx/performance.js'); + vi.doUnmock('../../../src/element-template/lynx/update-data.js'); vi.doUnmock('../../../src/element-template/runtime/page/root-instance.js'); }); @@ -121,6 +122,7 @@ describe('element-template native index wiring', () => { const publishEvent = vi.fn(); const publicComponentEvent = vi.fn(); const resetEventStateForRuntime = vi.fn(); + const updateCardData = vi.fn(); vi.doMock('../../../src/element-template/native/main-thread-api.js', () => ({ injectCalledByNative, @@ -149,6 +151,9 @@ describe('element-template native index wiring', () => { vi.doMock('../../../src/element-template/lynx/performance.js', () => ({ initTimingAPI, })); + vi.doMock('../../../src/element-template/lynx/update-data.js', () => ({ + updateCardData, + })); vi.doMock('../../../src/element-template/runtime/page/root-instance.js', () => ({ setRoot, })); @@ -179,6 +184,7 @@ describe('element-template native index wiring', () => { expect(globalThis.lynxCoreInject.tt.callDestroyLifetimeFun).toBe(callDestroyLifetimeFun); expect(globalThis.lynxCoreInject.tt.publishEvent).toBe(publishEvent); expect(globalThis.lynxCoreInject.tt.publicComponentEvent).toBe(publicComponentEvent); + expect(globalThis.lynxCoreInject.tt.updateCardData).toBe(updateCardData); expect(injectCalledByNative).not.toHaveBeenCalled(); expect(installElementTemplatePatchListener).not.toHaveBeenCalled(); diff --git a/packages/react/runtime/__test__/element-template/runtime/background/commit-hook.test.ts b/packages/react/runtime/__test__/element-template/runtime/background/commit-hook.test.ts index 83adff7f45..7a456c24d4 100644 --- a/packages/react/runtime/__test__/element-template/runtime/background/commit-hook.test.ts +++ b/packages/react/runtime/__test__/element-template/runtime/background/commit-hook.test.ts @@ -158,6 +158,11 @@ describe('ElementTemplate commit hook', () => { }); envManager.switchToBackground(); expect(globalCommitContext.flushOptions).toEqual({}); + + options.__c?.({} as unknown as object, []); + envManager.switchToMainThread(); + expect(updateEvents).toHaveLength(1); + envManager.switchToBackground(); }); it('dispatches triggerDataUpdated when useInitData observes a data change', () => { @@ -279,6 +284,51 @@ describe('ElementTemplate commit hook', () => { } }); + it('dispatches one data-updated payload for multiple initData readers', () => { + const dataChange = installDataChangeHarness(); + let first: unknown; + let second: unknown; + let consumed: unknown; + + try { + function App() { + first = useInitData(); + second = useInitData(); + return createElement( + InitDataProvider, + null, + createElement(InitDataConsumer, null, (initData: { msg?: string }) => { + consumed = initData; + return createElement('view'); + }), + ); + } + + lynx.__initData = { msg: 'before' }; + root.render(createElement(App, null)); + expect(first).toEqual({ msg: 'before' }); + expect(second).toEqual({ msg: 'before' }); + expect(consumed).toEqual({ msg: 'before' }); + + markElementTemplateHydrated(); + lynx.__initData = { msg: 'after' }; + dataChange.emitDataChanged(); + dataChange.flushScheduledRenders(); + + expect(first).toEqual({ msg: 'after' }); + expect(second).toEqual({ msg: 'after' }); + expect(consumed).toEqual({ msg: 'after' }); + envManager.switchToMainThread(); + expect(updateEvents).toHaveLength(1); + expect(updateEvents[0]).toMatchObject({ + ops: [], + flushOptions: { triggerDataUpdated: true }, + }); + } finally { + dataChange.restore(); + } + }); + it('skips dispatch before hydration', () => { globalCommitContext.ops = createRawTextOps(1, 'hello'); @@ -394,6 +444,27 @@ describe('ElementTemplate commit hook', () => { })); }); + it('dispatches data-updated payload while flushing ref-only effects', () => { + const ref = vi.fn(); + markElementTemplateHydrated(); + globalCommitContext.flushOptions = { triggerDataUpdated: true }; + queueRefAttrUpdate(null, ref, -2, 0); + + options.__c?.({} as unknown as object, []); + + envManager.switchToMainThread(); + expect(updateEvents).toEqual([ + { + ops: [], + flushOptions: { triggerDataUpdated: true }, + }, + ]); + envManager.switchToBackground(); + expect(ref).toHaveBeenCalledWith(expect.objectContaining({ + selector: '[ref=-2-0]', + })); + }); + it('flushes pre-hydration ref effects on commit without dispatching native ops', () => { const ref = vi.fn(); queueRefAttrUpdate(null, ref, 1, 0); diff --git a/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx b/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx new file mode 100644 index 0000000000..e97001808b --- /dev/null +++ b/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx @@ -0,0 +1,159 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createElement } from 'preact'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + installElementTemplateCommitHook, + resetElementTemplateCommitState, +} from '../../../../src/element-template/background/commit-hook.js'; +import { + installElementTemplateHydrationListener, + resetElementTemplateHydrationListener, +} from '../../../../src/element-template/background/hydration-listener.js'; +import { root } from '../../../../src/element-template/index.js'; +import { updateCardData } from '../../../../src/element-template/lynx/update-data.js'; +import { + installElementTemplatePatchListener, + resetElementTemplatePatchListener, +} from '../../../../src/element-template/native/patch-listener.js'; +import { ElementTemplateLifecycleConstant } from '../../../../src/element-template/protocol/lifecycle-constant.js'; +import type { ElementTemplateUpdateCommitContext } from '../../../../src/element-template/protocol/types.js'; +import { __page } from '../../../../src/element-template/runtime/page/page.js'; +import { clearEtAttrPlanMap } from '../../../../src/element-template/runtime/template/attr-slot-plan.js'; +import { compileFixtureSource } from '../../test-utils/debug/compiledFixtureCompiler.js'; +import { loadCompiledFixtureModule } from '../../test-utils/debug/compiledFixtureModule.js'; +import type { CompiledFixtureModuleExports } from '../../test-utils/debug/compiledFixtureModule.js'; +import { primeCompiledFixtureTemplates } from '../../test-utils/debug/compiledFixtureRegistry.js'; +import { ElementTemplateEnvManager } from '../../test-utils/debug/envManager.js'; +import { serializeToJSX } from '../../test-utils/debug/serializer.js'; + +declare const renderPage: (data?: Record) => void; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const FIXTURE = path.resolve(__dirname, '../../fixtures/background/init-data-update/index.tsx'); + +type Listener = (...args: unknown[]) => void; + +class LynxTestEventEmitter { + private listeners = new Map>(); + + addListener(eventName: string, listener: Listener): void { + const listeners = this.listeners.get(eventName); + if (listeners) { + listeners.add(listener); + return; + } + this.listeners.set(eventName, new Set([listener])); + } + + removeListener(eventName: string, listener: Listener): void { + this.listeners.get(eventName)?.delete(listener); + } + + emit(eventName: string, args?: unknown[]): void { + for (const listener of this.listeners.get(eventName) ?? []) { + listener(...(args ?? [])); + } + } +} + +function waitForRender(): Promise { + return new Promise(resolve => setTimeout(resolve, 0)); +} + +async function loadCompiledInitDataFixture(): Promise<{ + backgroundModule: CompiledFixtureModuleExports; + mainModule: CompiledFixtureModuleExports; +}> { + const mainArtifact = await compileFixtureSource(FIXTURE, { target: 'LEPUS' }); + primeCompiledFixtureTemplates(mainArtifact); + const mainModule = await loadCompiledFixtureModule(mainArtifact); + + const backgroundArtifact = await compileFixtureSource(FIXTURE, { target: 'JS' }); + const backgroundModule = await loadCompiledFixtureModule(backgroundArtifact); + + return { backgroundModule, mainModule }; +} + +describe('Compiled ET InitData updateData fixture', () => { + const envManager = new ElementTemplateEnvManager(); + let originalLynx: typeof lynx; + let emitter: LynxTestEventEmitter; + let updateEvents: ElementTemplateUpdateCommitContext[] = []; + + const onUpdate = (event: { data: unknown }) => { + updateEvents.push(event.data as ElementTemplateUpdateCommitContext); + }; + + function installInitData(initData: Record): void { + const baseLynx = globalThis.lynx; + vi.stubGlobal('lynx', { + ...baseLynx, + __initData: initData, + getJSModule(moduleName: string) { + if (moduleName === 'GlobalEventEmitter') { + return emitter; + } + return baseLynx.getJSModule?.(moduleName); + }, + }); + } + + beforeEach(() => { + vi.clearAllMocks(); + originalLynx = globalThis.lynx; + emitter = new LynxTestEventEmitter(); + updateEvents = []; + resetElementTemplateCommitState(); + clearEtAttrPlanMap(); + envManager.resetEnv('background'); + envManager.setUseElementTemplate(true); + installInitData({ msg: 'init' }); + installElementTemplateCommitHook(); + installElementTemplateHydrationListener(); + + envManager.switchToMainThread(); + installElementTemplatePatchListener(); + lynx.getJSContext().addEventListener(ElementTemplateLifecycleConstant.update, onUpdate); + envManager.switchToBackground(); + }); + + afterEach(() => { + envManager.switchToMainThread(); + lynx.getJSContext().removeEventListener(ElementTemplateLifecycleConstant.update, onUpdate); + resetElementTemplatePatchListener(); + envManager.switchToBackground(); + resetElementTemplateHydrationListener(); + resetElementTemplateCommitState(); + envManager.setUseElementTemplate(false); + vi.stubGlobal('lynx', originalLynx); + }); + + it('updates raw text after hydrated background updateCardData', async () => { + const { backgroundModule, mainModule } = await loadCompiledInitDataFixture(); + + envManager.switchToBackground(); + root.render(createElement(backgroundModule.App)); + + envManager.switchToMainThread(); + root.render(createElement(mainModule.App)); + renderPage({ msg: 'init' }); + expect(serializeToJSX(__page)).toContain('init'); + + envManager.switchToBackground(); + updateEvents = []; + updateCardData({ msg: 'update' }); + await waitForRender(); + + envManager.switchToMainThread(); + expect(updateEvents.at(-1)?.flushOptions).toMatchObject({ triggerDataUpdated: true }); + expect(updateEvents.at(-1)?.ops.length).toBeGreaterThan(0); + expect(serializeToJSX(__page)).toContain('update'); + expect((__FlushElementTree as unknown as { mock: { calls: unknown[][] } }).mock.calls.at(-1)?.[1]).toMatchObject({ + triggerDataUpdated: true, + }); + }); +}); diff --git a/packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx b/packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx index 61c4b98909..1af18dbff4 100644 --- a/packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx +++ b/packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx @@ -579,18 +579,22 @@ describe('ElementTemplate patch stream (apply)', () => { resetReportedErrors(); }); - it('still flushes update payloads without ops so flushOptions can reach native', () => { + it('still flushes update payloads with empty ops so flushOptions can reach native', () => { envManager.switchToMainThread(); installElementTemplatePatchListener(); mockSetAttributeOfElementTemplate.mockClear(); mockInsertNodeToElementTemplate.mockClear(); mockRemoveNodeFromElementTemplate.mockClear(); mockFlushElementTree.mockClear(); + lynx.performance._markTiming.mockClear(); envManager.switchToBackground(); lynx.getCoreContext().dispatchEvent({ type: ElementTemplateLifecycleConstant.update, - data: { flushOptions: {} }, + data: { + ops: [], + flushOptions: { triggerDataUpdated: true }, + }, }); envManager.switchToMainThread(); @@ -598,6 +602,7 @@ describe('ElementTemplate patch stream (apply)', () => { expect(mockInsertNodeToElementTemplate.mock.calls).toHaveLength(0); expect(mockRemoveNodeFromElementTemplate.mock.calls).toHaveLength(0); expect(mockFlushElementTree.mock.calls).toHaveLength(1); - expect(mockFlushElementTree.mock.calls[0]?.[1]).toEqual({}); + expect(mockFlushElementTree.mock.calls[0]?.[1]).toEqual({ triggerDataUpdated: true }); + expect(lynx.performance._markTiming.mock.calls).toEqual([]); }); }); diff --git a/packages/react/runtime/__test__/snapshot/compat/initData.test.jsx b/packages/react/runtime/__test__/snapshot/compat/initData.test.jsx index eb22fe8cad..7cdcf6054d 100644 --- a/packages/react/runtime/__test__/snapshot/compat/initData.test.jsx +++ b/packages/react/runtime/__test__/snapshot/compat/initData.test.jsx @@ -11,6 +11,7 @@ import { import { backgroundSnapshotInstanceToJSON } from '../utils/debug'; import { useState } from 'preact/compat'; import { useInitData, withInitDataInState } from '../../../src/lynx-api'; +import { NativeUpdateDataType } from '../../../src/snapshot/lifecycle/constant'; import { globalEnvManager } from '../utils/envManager'; /** @type {SnapshotInstance} */ @@ -148,4 +149,41 @@ describe('withInitDataInState', () => { } `); }); + + it('resets initData and strips timing flag before emitting data changes', () => { + const tt = lynxCoreInject.tt; + const emitter = lynx.getJSModule('GlobalEventEmitter'); + const listener = vi.fn(); + const originalReportError = lynx.reportError; + lynx.reportError = vi.fn(); + lynx.__initData = { + stale: true, + key4: 'old', + }; + emitter.addListener('onDataChanged', listener); + + try { + tt.updateCardData( + { + key4: 'reset', + __lynx_timing_flag: '__lynx_timing_actual_fmp', + }, + { type: NativeUpdateDataType.RESET }, + ); + + expect(lynx.__initData).toEqual({ + key4: 'reset', + }); + expect(listener).toHaveBeenCalledWith({ + key4: 'reset', + }); + expect(lynx.reportError).toHaveBeenCalledTimes(1); + expect(String(lynx.reportError.mock.calls[0]?.[0]?.message ?? '')).toBe( + 'Received unsupported updateData with `__lynx_timing_flag` (value "__lynx_timing_actual_fmp"), the timing flag is ignored', + ); + } finally { + emitter.removeListener('onDataChanged', listener); + lynx.reportError = originalReportError; + } + }); }); diff --git a/packages/react/runtime/src/core/lynx-update-data.ts b/packages/react/runtime/src/core/lynx-update-data.ts new file mode 100644 index 0000000000..f358394f0d --- /dev/null +++ b/packages/react/runtime/src/core/lynx-update-data.ts @@ -0,0 +1,40 @@ +// Copyright 2026 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. + +export const NativeUpdateDataType = { + UPDATE: 0, + RESET: 1, +} as const; + +export type NativeUpdateDataType = (typeof NativeUpdateDataType)[keyof typeof NativeUpdateDataType]; + +export interface NativeUpdateDataOptions { + type?: NativeUpdateDataType | undefined; +} + +type InitDataPatch = Record; + +export function applyInitDataUpdateFromNative( + newData: InitDataPatch, + options?: NativeUpdateDataOptions, +): InitDataPatch { + const { ['__lynx_timing_flag']: performanceTimingFlag, ...restNewData } = newData; + if (performanceTimingFlag) { + lynx.reportError( + new Error( + `Received unsupported updateData with \`__lynx_timing_flag\` (value "${performanceTimingFlag}"), the timing flag is ignored`, + ), + ); + } + + const { type = NativeUpdateDataType.UPDATE } = options ?? {}; + if (type == NativeUpdateDataType.RESET) { + lynx.__initData = {}; + } + + // COW keeps provider/consumer readers aligned with Snapshot updateData behavior. + lynx.__initData = Object.assign({}, lynx.__initData, restNewData); + + return restNewData; +} diff --git a/packages/react/runtime/src/element-template/background/commit-hook.ts b/packages/react/runtime/src/element-template/background/commit-hook.ts index ac5aef480f..7760413a7f 100644 --- a/packages/react/runtime/src/element-template/background/commit-hook.ts +++ b/packages/react/runtime/src/element-template/background/commit-hook.ts @@ -77,11 +77,11 @@ export function installElementTemplateCommitHook(): void { ) ) { const hasNativeOps = globalCommitContext.ops.length > 0; - const shouldDispatchUpdate = hasNativeOps || !isEmptyObject(globalCommitContext.flushOptions); + const hasUpdatePayload = hasNativeOps || !isEmptyObject(globalCommitContext.flushOptions); const removedSubtreesAwaitingTeardown = hasNativeOps ? takeRemovedSubtreesForPostDispatchTeardown() : []; let didFlushRefs = false; try { - if (shouldDispatchUpdate) { + if (hasUpdatePayload) { markTimingLegacy('updateDiffVdomEnd'); markTiming('diffVdomEnd'); @@ -114,7 +114,8 @@ export function installElementTemplateCommitHook(): void { ), ); } - + } + if (hasUpdatePayload) { lynx.getCoreContext().dispatchEvent({ type: ElementTemplateLifecycleConstant.update, data: { diff --git a/packages/react/runtime/src/element-template/lynx/update-data.ts b/packages/react/runtime/src/element-template/lynx/update-data.ts new file mode 100644 index 0000000000..a60c17d9d1 --- /dev/null +++ b/packages/react/runtime/src/element-template/lynx/update-data.ts @@ -0,0 +1,18 @@ +// Copyright 2026 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 { applyInitDataUpdateFromNative } from '../../core/lynx-update-data.js'; +import type { NativeUpdateDataOptions } from '../../core/lynx-update-data.js'; + +interface LynxGlobalEventEmitter { + emit: (eventName: string, args?: unknown[]) => void; +} + +export function updateCardData( + newData: Record, + options?: NativeUpdateDataOptions, +): void { + const restNewData = applyInitDataUpdateFromNative(newData, options); + (lynx.getJSModule('GlobalEventEmitter') as LynxGlobalEventEmitter).emit('onDataChanged', [restNewData]); +} diff --git a/packages/react/runtime/src/element-template/native/index.ts b/packages/react/runtime/src/element-template/native/index.ts index 6f780c0ecd..b94042dada 100644 --- a/packages/react/runtime/src/element-template/native/index.ts +++ b/packages/react/runtime/src/element-template/native/index.ts @@ -15,6 +15,7 @@ import { initElementTemplatePAPICallAlog } from '../debug/elementPAPICall.js'; import { initProfileHook } from '../debug/profile.js'; import { setupLynxEnv } from '../lynx/env.js'; import { initTimingAPI } from '../lynx/performance.js'; +import { updateCardData } from '../lynx/update-data.js'; import { publicComponentEvent, publishEvent, resetEventStateForRuntime } from '../prop-adapters/event.js'; import { setRoot } from '../runtime/page/root-instance.js'; @@ -42,6 +43,7 @@ function init(): void { lynxCoreInject.tt.callDestroyLifetimeFun = callDestroyLifetimeFun; lynxCoreInject.tt.publishEvent = publishEvent; lynxCoreInject.tt.publicComponentEvent = publicComponentEvent; + lynxCoreInject.tt.updateCardData = updateCardData; installElementTemplateCommitHook(); if (process.env['NODE_ENV'] !== 'test') { initTimingAPI(); diff --git a/packages/react/runtime/src/snapshot/lynx/tt.ts b/packages/react/runtime/src/snapshot/lynx/tt.ts index 83f141a44a..604efb686d 100644 --- a/packages/react/runtime/src/snapshot/lynx/tt.ts +++ b/packages/react/runtime/src/snapshot/lynx/tt.ts @@ -5,12 +5,13 @@ import { process, render } from 'preact'; import { PerformanceTimingFlags, PipelineOrigins, beginPipeline, markTiming } from './performance.js'; import { runWithForce } from './runWithForce.js'; +import { applyInitDataUpdateFromNative } from '../../core/lynx-update-data.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, NativeUpdateDataType } from '../lifecycle/constant.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'; @@ -285,21 +286,7 @@ function updateGlobalProps(newData: Record): void { } function updateCardData(newData: Record, options?: Record): void { - const { ['__lynx_timing_flag']: performanceTimingFlag, ...restNewData } = newData; - if (performanceTimingFlag) { - lynx.reportError( - new Error( - `Received unsupported updateData with \`__lynx_timing_flag\` (value "${performanceTimingFlag}"), the timing flag is ignored`, - ), - ); - } - const { type = NativeUpdateDataType.UPDATE } = options ?? {}; - if (type == NativeUpdateDataType.RESET) { - lynx.__initData = {}; - } - - // COW when modify `lynx.__initData` to make sure Provider & Consumer works - lynx.__initData = Object.assign({}, lynx.__initData, restNewData); + const restNewData = applyInitDataUpdateFromNative(newData, options); lynxCoreInject.tt.GlobalEventEmitter.emit('onDataChanged', [restNewData]); } From 7856cbedbe5b52ccd4897fa25f6a802a4b600dfe Mon Sep 17 00:00:00 2001 From: Yradex <11014207+Yradex@users.noreply.github.com> Date: Wed, 20 May 2026 18:02:12 +0800 Subject: [PATCH 2/2] refactor(react): share updateCardData bridge --- .../__test__/core/lynx-update-data.test.ts | 44 +++++-- .../element-template/lynx/update-data.test.ts | 108 ------------------ .../element-template/native/index.test.ts | 4 +- .../init-data-compiled-fixtures.test.tsx | 28 +---- .../__test__/test-utils/lynx-event-emitter.ts | 63 ++++++++++ .../runtime/src/core/lynx-update-data.ts | 6 +- .../src/element-template/lynx/update-data.ts | 18 --- .../src/element-template/native/index.ts | 2 +- .../react/runtime/src/snapshot/lynx/tt.ts | 7 +- packages/react/runtime/vitest.config.ts | 1 + 10 files changed, 105 insertions(+), 176 deletions(-) delete mode 100644 packages/react/runtime/__test__/element-template/lynx/update-data.test.ts create mode 100644 packages/react/runtime/__test__/test-utils/lynx-event-emitter.ts delete mode 100644 packages/react/runtime/src/element-template/lynx/update-data.ts diff --git a/packages/react/runtime/__test__/core/lynx-update-data.test.ts b/packages/react/runtime/__test__/core/lynx-update-data.test.ts index 8a39b7d243..50669a3e7c 100644 --- a/packages/react/runtime/__test__/core/lynx-update-data.test.ts +++ b/packages/react/runtime/__test__/core/lynx-update-data.test.ts @@ -1,72 +1,92 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { applyInitDataUpdateFromNative, NativeUpdateDataType } from '../../src/core/lynx-update-data.js'; +import { NativeUpdateDataType, updateCardData } from '../../src/core/lynx-update-data.js'; +import { LynxTestEventEmitter } from '../test-utils/lynx-event-emitter.js'; -describe('applyInitDataUpdateFromNative', () => { +describe('updateCardData', () => { let originalInitData: typeof lynx.__initData; let originalReportError: typeof lynx.reportError; + let originalGetJSModule: typeof lynx.getJSModule; + let emitter: LynxTestEventEmitter; beforeEach(() => { originalInitData = lynx.__initData; originalReportError = lynx.reportError; + originalGetJSModule = lynx.getJSModule; + emitter = new LynxTestEventEmitter(); lynx.__initData = {}; lynx.reportError = vi.fn(); + lynx.getJSModule = vi.fn((moduleName: string) => { + if (moduleName === 'GlobalEventEmitter') { + return emitter; + } + return originalGetJSModule(moduleName); + }) as typeof lynx.getJSModule; }); afterEach(() => { lynx.__initData = originalInitData; lynx.reportError = originalReportError; + lynx.getJSModule = originalGetJSModule; }); - it('COW merges update data and returns the current patch data', () => { + it('COW merges update data and emits the current patch data', () => { const previousInitData = { msg: 'init', stable: true }; + const listener = vi.fn(); lynx.__initData = previousInitData; + emitter.addListener('onDataChanged', listener); - const restNewData = applyInitDataUpdateFromNative({ + updateCardData({ msg: 'update', next: 1, }); - expect(restNewData).toEqual({ msg: 'update', next: 1 }); expect(lynx.__initData).toEqual({ msg: 'update', stable: true, next: 1 }); expect(lynx.__initData).not.toBe(previousInitData); + expect(listener).toHaveBeenCalledWith({ msg: 'update', next: 1 }); expect(lynx.reportError).not.toHaveBeenCalled(); }); it('clears existing initData before RESET updates', () => { + const listener = vi.fn(); lynx.__initData = { stale: true, msg: 'init' }; + emitter.addListener('onDataChanged', listener); - const restNewData = applyInitDataUpdateFromNative( + updateCardData( { msg: 'reset' }, { type: NativeUpdateDataType.RESET }, ); - expect(restNewData).toEqual({ msg: 'reset' }); expect(lynx.__initData).toEqual({ msg: 'reset' }); + expect(listener).toHaveBeenCalledWith({ msg: 'reset' }); }); it('keeps Snapshot-compatible loose RESET matching', () => { + const listener = vi.fn(); lynx.__initData = { stale: true, msg: 'init' }; + emitter.addListener('onDataChanged', listener); - const restNewData = applyInitDataUpdateFromNative( + updateCardData( { msg: 'reset' }, { type: '1' as unknown as NativeUpdateDataType }, ); - expect(restNewData).toEqual({ msg: 'reset' }); expect(lynx.__initData).toEqual({ msg: 'reset' }); + expect(listener).toHaveBeenCalledWith({ msg: 'reset' }); }); - it('reports and strips __lynx_timing_flag from the merged data and return value', () => { + it('reports and strips __lynx_timing_flag from the merged data and emitted patch', () => { + const listener = vi.fn(); lynx.__initData = { msg: 'init' }; + emitter.addListener('onDataChanged', listener); - const restNewData = applyInitDataUpdateFromNative({ + updateCardData({ msg: 'update', __lynx_timing_flag: '__lynx_timing_actual_fmp', }); - expect(restNewData).toEqual({ msg: 'update' }); expect(lynx.__initData).toEqual({ msg: 'update' }); + expect(listener).toHaveBeenCalledWith({ msg: 'update' }); expect(lynx.reportError).toHaveBeenCalledTimes(1); expect(lynx.reportError).toHaveBeenCalledWith( new Error( diff --git a/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts b/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts deleted file mode 100644 index 9d7d2c72b1..0000000000 --- a/packages/react/runtime/__test__/element-template/lynx/update-data.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { NativeUpdateDataType } from '../../../src/core/lynx-update-data.js'; -import { updateCardData } from '../../../src/element-template/lynx/update-data.js'; -import { ElementTemplateEnvManager } from '../test-utils/debug/envManager.js'; - -type Listener = (...args: unknown[]) => void; - -class LynxGlobalEventEmitter { - private listeners = new Map>(); - - addListener(eventName: string, listener: Listener): void { - const listeners = this.listeners.get(eventName); - if (listeners) { - listeners.add(listener); - return; - } - this.listeners.set(eventName, new Set([listener])); - } - - removeListener(eventName: string, listener: Listener): void { - const listeners = this.listeners.get(eventName); - listeners?.delete(listener); - } - - emit(eventName: string, args?: unknown[]): void { - for (const listener of this.listeners.get(eventName) ?? []) { - listener(...(args ?? [])); - } - } -} - -describe('ElementTemplate updateCardData', () => { - const envManager = new ElementTemplateEnvManager(); - let originalLynx: typeof lynx; - let emitter: LynxGlobalEventEmitter; - let reportError: ReturnType; - - function installLynx(initData: Record): void { - const baseLynx = globalThis.lynx; - vi.stubGlobal('lynx', { - ...baseLynx, - __initData: initData, - reportError, - getJSModule(moduleName: string) { - if (moduleName === 'GlobalEventEmitter') { - return emitter; - } - return baseLynx.getJSModule?.(moduleName); - }, - }); - } - - beforeEach(() => { - originalLynx = globalThis.lynx; - emitter = new LynxGlobalEventEmitter(); - reportError = vi.fn(); - envManager.resetEnv('background'); - }); - - afterEach(() => { - vi.stubGlobal('lynx', originalLynx); - }); - - it('updates initData and emits restNewData through the ET listener channel', () => { - installLynx({ msg: 'init', stable: true }); - const listener = vi.fn(); - emitter.addListener('onDataChanged', listener); - - updateCardData({ msg: 'update', next: 1 }); - - expect(lynx.__initData).toEqual({ msg: 'update', stable: true, next: 1 }); - expect(listener).toHaveBeenCalledWith({ msg: 'update', next: 1 }); - expect(reportError).not.toHaveBeenCalled(); - }); - - it('clears previous initData when RESET is requested', () => { - installLynx({ stale: true, msg: 'init' }); - const listener = vi.fn(); - emitter.addListener('onDataChanged', listener); - - updateCardData( - { msg: 'reset' }, - { type: NativeUpdateDataType.RESET }, - ); - - expect(lynx.__initData).toEqual({ msg: 'reset' }); - expect(listener).toHaveBeenCalledWith({ msg: 'reset' }); - }); - - it('reports and strips __lynx_timing_flag before emitting onDataChanged', () => { - installLynx({ msg: 'init' }); - const listener = vi.fn(); - emitter.addListener('onDataChanged', listener); - - updateCardData({ - msg: 'update', - __lynx_timing_flag: '__lynx_timing_actual_fmp', - }); - - expect(lynx.__initData).toEqual({ msg: 'update' }); - expect(listener).toHaveBeenCalledWith({ msg: 'update' }); - expect(reportError).toHaveBeenCalledTimes(1); - expect(String(reportError.mock.calls[0]?.[0]?.message ?? '')).toBe( - 'Received unsupported updateData with `__lynx_timing_flag` (value "__lynx_timing_actual_fmp"), the timing flag is ignored', - ); - }); -}); diff --git a/packages/react/runtime/__test__/element-template/native/index.test.ts b/packages/react/runtime/__test__/element-template/native/index.test.ts index 08d4825538..0c8d4b6fd4 100644 --- a/packages/react/runtime/__test__/element-template/native/index.test.ts +++ b/packages/react/runtime/__test__/element-template/native/index.test.ts @@ -30,7 +30,7 @@ describe('element-template native index wiring', () => { vi.doUnmock('../../../src/element-template/debug/profile.js'); vi.doUnmock('../../../src/element-template/lynx/env.js'); vi.doUnmock('../../../src/element-template/lynx/performance.js'); - vi.doUnmock('../../../src/element-template/lynx/update-data.js'); + vi.doUnmock('../../../src/core/lynx-update-data.js'); vi.doUnmock('../../../src/element-template/runtime/page/root-instance.js'); }); @@ -151,7 +151,7 @@ describe('element-template native index wiring', () => { vi.doMock('../../../src/element-template/lynx/performance.js', () => ({ initTimingAPI, })); - vi.doMock('../../../src/element-template/lynx/update-data.js', () => ({ + vi.doMock('../../../src/core/lynx-update-data.js', () => ({ updateCardData, })); vi.doMock('../../../src/element-template/runtime/page/root-instance.js', () => ({ diff --git a/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx b/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx index e97001808b..0623362616 100644 --- a/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx +++ b/packages/react/runtime/__test__/element-template/runtime/background/init-data-compiled-fixtures.test.tsx @@ -13,7 +13,7 @@ import { resetElementTemplateHydrationListener, } from '../../../../src/element-template/background/hydration-listener.js'; import { root } from '../../../../src/element-template/index.js'; -import { updateCardData } from '../../../../src/element-template/lynx/update-data.js'; +import { updateCardData } from '../../../../src/core/lynx-update-data.js'; import { installElementTemplatePatchListener, resetElementTemplatePatchListener, @@ -22,6 +22,7 @@ import { ElementTemplateLifecycleConstant } from '../../../../src/element-templa import type { ElementTemplateUpdateCommitContext } from '../../../../src/element-template/protocol/types.js'; import { __page } from '../../../../src/element-template/runtime/page/page.js'; import { clearEtAttrPlanMap } from '../../../../src/element-template/runtime/template/attr-slot-plan.js'; +import { LynxTestEventEmitter } from '../../../test-utils/lynx-event-emitter.js'; import { compileFixtureSource } from '../../test-utils/debug/compiledFixtureCompiler.js'; import { loadCompiledFixtureModule } from '../../test-utils/debug/compiledFixtureModule.js'; import type { CompiledFixtureModuleExports } from '../../test-utils/debug/compiledFixtureModule.js'; @@ -35,31 +36,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const FIXTURE = path.resolve(__dirname, '../../fixtures/background/init-data-update/index.tsx'); -type Listener = (...args: unknown[]) => void; - -class LynxTestEventEmitter { - private listeners = new Map>(); - - addListener(eventName: string, listener: Listener): void { - const listeners = this.listeners.get(eventName); - if (listeners) { - listeners.add(listener); - return; - } - this.listeners.set(eventName, new Set([listener])); - } - - removeListener(eventName: string, listener: Listener): void { - this.listeners.get(eventName)?.delete(listener); - } - - emit(eventName: string, args?: unknown[]): void { - for (const listener of this.listeners.get(eventName) ?? []) { - listener(...(args ?? [])); - } - } -} - function waitForRender(): Promise { return new Promise(resolve => setTimeout(resolve, 0)); } diff --git a/packages/react/runtime/__test__/test-utils/lynx-event-emitter.ts b/packages/react/runtime/__test__/test-utils/lynx-event-emitter.ts new file mode 100644 index 0000000000..ab84fb7d5f --- /dev/null +++ b/packages/react/runtime/__test__/test-utils/lynx-event-emitter.ts @@ -0,0 +1,63 @@ +// Copyright 2026 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. + +export type LynxTestEventListener = (...args: unknown[]) => void; + +export class LynxTestEventEmitter { + readonly listeners = new Map(); + + addListener(eventName: string, listener: LynxTestEventListener): void { + const listeners = this.listeners.get(eventName); + if (listeners) { + listeners.push(listener); + return; + } + this.listeners.set(eventName, [listener]); + } + + removeListener(eventName: string, listener: LynxTestEventListener): void { + const listeners = this.listeners.get(eventName); + if (!listeners) { + return; + } + const index = listeners.indexOf(listener); + if (index === -1) { + return; + } + listeners.splice(index, 1); + if (listeners.length === 0) { + this.listeners.delete(eventName); + } + } + + removeAllListeners(eventName?: string): void { + if (eventName) { + this.listeners.delete(eventName); + return; + } + this.clear(); + } + + emit(eventName: string, args?: unknown[]): void { + for (const listener of [...(this.listeners.get(eventName) ?? [])]) { + listener(...(args ?? [])); + } + } + + trigger(eventName: string, params: string | Record): void { + this.emit(eventName, [params]); + } + + toggle(eventName: string, ...data: unknown[]): void { + this.emit(eventName, data); + } + + clear(): void { + this.listeners.clear(); + } + + listenerCount(eventName: string): number { + return this.listeners.get(eventName)?.length ?? 0; + } +} diff --git a/packages/react/runtime/src/core/lynx-update-data.ts b/packages/react/runtime/src/core/lynx-update-data.ts index f358394f0d..c3a9be1504 100644 --- a/packages/react/runtime/src/core/lynx-update-data.ts +++ b/packages/react/runtime/src/core/lynx-update-data.ts @@ -15,10 +15,10 @@ export interface NativeUpdateDataOptions { type InitDataPatch = Record; -export function applyInitDataUpdateFromNative( +export function updateCardData( newData: InitDataPatch, options?: NativeUpdateDataOptions, -): InitDataPatch { +): void { const { ['__lynx_timing_flag']: performanceTimingFlag, ...restNewData } = newData; if (performanceTimingFlag) { lynx.reportError( @@ -36,5 +36,5 @@ export function applyInitDataUpdateFromNative( // COW keeps provider/consumer readers aligned with Snapshot updateData behavior. lynx.__initData = Object.assign({}, lynx.__initData, restNewData); - return restNewData; + lynx.getJSModule('GlobalEventEmitter').emit('onDataChanged', [restNewData]); } diff --git a/packages/react/runtime/src/element-template/lynx/update-data.ts b/packages/react/runtime/src/element-template/lynx/update-data.ts deleted file mode 100644 index a60c17d9d1..0000000000 --- a/packages/react/runtime/src/element-template/lynx/update-data.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2026 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 { applyInitDataUpdateFromNative } from '../../core/lynx-update-data.js'; -import type { NativeUpdateDataOptions } from '../../core/lynx-update-data.js'; - -interface LynxGlobalEventEmitter { - emit: (eventName: string, args?: unknown[]) => void; -} - -export function updateCardData( - newData: Record, - options?: NativeUpdateDataOptions, -): void { - const restNewData = applyInitDataUpdateFromNative(newData, options); - (lynx.getJSModule('GlobalEventEmitter') as LynxGlobalEventEmitter).emit('onDataChanged', [restNewData]); -} diff --git a/packages/react/runtime/src/element-template/native/index.ts b/packages/react/runtime/src/element-template/native/index.ts index b94042dada..fc62dc06c8 100644 --- a/packages/react/runtime/src/element-template/native/index.ts +++ b/packages/react/runtime/src/element-template/native/index.ts @@ -7,6 +7,7 @@ import { injectCalledByNative } from './main-thread-api.js'; import { installOnMtsDestruction } from './mts-destroy.js'; import { installElementTemplatePatchListener } from './patch-listener.js'; import { installMainThreadHooks } from '../../core/hooks/mainThreadImpl.js'; +import { updateCardData } from '../../core/lynx-update-data.js'; import { installElementTemplateCommitHook } from '../background/commit-hook.js'; import { setupBackgroundElementTemplateDocument } from '../background/document.js'; import { installElementTemplateHydrationListener } from '../background/hydration-listener.js'; @@ -15,7 +16,6 @@ import { initElementTemplatePAPICallAlog } from '../debug/elementPAPICall.js'; import { initProfileHook } from '../debug/profile.js'; import { setupLynxEnv } from '../lynx/env.js'; import { initTimingAPI } from '../lynx/performance.js'; -import { updateCardData } from '../lynx/update-data.js'; import { publicComponentEvent, publishEvent, resetEventStateForRuntime } from '../prop-adapters/event.js'; import { setRoot } from '../runtime/page/root-instance.js'; diff --git a/packages/react/runtime/src/snapshot/lynx/tt.ts b/packages/react/runtime/src/snapshot/lynx/tt.ts index 604efb686d..942efd5aa9 100644 --- a/packages/react/runtime/src/snapshot/lynx/tt.ts +++ b/packages/react/runtime/src/snapshot/lynx/tt.ts @@ -5,7 +5,7 @@ import { process, render } from 'preact'; import { PerformanceTimingFlags, PipelineOrigins, beginPipeline, markTiming } from './performance.js'; import { runWithForce } from './runWithForce.js'; -import { applyInitDataUpdateFromNative } from '../../core/lynx-update-data.js'; +import { updateCardData } from '../../core/lynx-update-data.js'; import { __root } from '../../root.js'; import { profileEnd, profileStart } from '../../shared/profile.js'; import { CHILDREN } from '../../shared/render-constants.js'; @@ -285,9 +285,4 @@ function updateGlobalProps(newData: Record): void { lynxCoreInject.tt.GlobalEventEmitter.emit('onGlobalPropsChanged', [lynx.__globalProps]); } -function updateCardData(newData: Record, options?: Record): void { - const restNewData = applyInitDataUpdateFromNative(newData, options); - lynxCoreInject.tt.GlobalEventEmitter.emit('onDataChanged', [restNewData]); -} - export { injectTt, flushDelayedLifecycleEvents }; diff --git a/packages/react/runtime/vitest.config.ts b/packages/react/runtime/vitest.config.ts index dd481c0aa0..9ce5995aa6 100644 --- a/packages/react/runtime/vitest.config.ts +++ b/packages/react/runtime/vitest.config.ts @@ -116,6 +116,7 @@ export default defineConfig({ 'vitest.config.ts', '__test__/element-template/**', '__test__/snapshot/utils/**', + '__test__/test-utils/**', 'lib/**', 'worklet-runtime/**', 'src/element-template/**',