diff --git a/.changeset/thick-snakes-sink.md b/.changeset/thick-snakes-sink.md new file mode 100644 index 0000000000..16dc58ef4b --- /dev/null +++ b/.changeset/thick-snakes-sink.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +Add snapshot id report when throwing `snapshotPatchApply failed: ctx not found` error. diff --git a/packages/react/runtime/__test__/snapshotPatch.test.jsx b/packages/react/runtime/__test__/snapshotPatch.test.jsx index 7ce1c0b7b5..f13ca58837 100644 --- a/packages/react/runtime/__test__/snapshotPatch.test.jsx +++ b/packages/react/runtime/__test__/snapshotPatch.test.jsx @@ -1,4 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +// Copyright 2025 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 { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { BackgroundSnapshotInstance } from '../src/backgroundSnapshot'; import { elementTree } from './utils/nativeMethod'; @@ -17,9 +20,17 @@ import { snapshotManager, } from '../src/snapshot'; import { globalEnvManager } from './utils/envManager'; +import { addCtxNotFoundEventListener } from '../src/lifecycle/patch/error'; const HOLE = null; +beforeAll(() => { + globalEnvManager.resetEnv(); + globalEnvManager.switchToBackground(); + addCtxNotFoundEventListener(); + globalEnvManager.switchToMainThread(); +}); + beforeEach(() => { globalEnvManager.resetEnv(); }); @@ -230,7 +241,21 @@ describe('insertBefore', () => { const bsi1 = new BackgroundSnapshotInstance(snapshot1); const bsi2 = new BackgroundSnapshotInstance(snapshot2); const patch = takeGlobalSnapshotPatch(); - patch.push(SnapshotOperation.InsertBefore, 1, 100, null, SnapshotOperation.InsertBefore, 100, 2, null); + const bsi3 = new BackgroundSnapshotInstance(snapshot3); + patch.push( + SnapshotOperation.InsertBefore, + 2, + 100, + null, + SnapshotOperation.InsertBefore, + 100, + 2, + null, + SnapshotOperation.InsertBefore, + 4, + 100, + null, + ); expect(patch).toMatchInlineSnapshot(` [ 0, @@ -240,35 +265,45 @@ describe('insertBefore', () => { "__Card__:__snapshot_a94a8_test_2", 3, 1, - 1, + 2, 100, null, 1, 100, 2, null, + 1, + 4, + 100, + null, ] `); expect(snapshotInstanceManager.values.size).toEqual(1); snapshotPatchApply(patch); - expect(_ReportError).toHaveBeenCalledTimes(2); expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], { "errorCode": 1101, }, ], [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], + { + "errorCode": 1101, + }, + ], + [ + [Error: snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_a94a8_test_3'], { "errorCode": 1101, }, ], ] `); + expect(snapshotInstanceManager.values.size).toEqual(3); const si1 = snapshotInstanceManager.values.get(bsi1.__id); si1.ensureElements(); @@ -407,14 +442,14 @@ describe('removeChild', () => { `); patch = takeGlobalSnapshotPatch(); - patch.push(SnapshotOperation.RemoveChild, 1, 2, SnapshotOperation.RemoveChild, 100, 1); + patch.push(SnapshotOperation.RemoveChild, 1, 2, SnapshotOperation.RemoveChild, 2, 1); expect(patch).toMatchInlineSnapshot(` [ 2, 1, 2, 2, - 100, + 2, 1, ] `); @@ -424,13 +459,13 @@ describe('removeChild', () => { expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'], { "errorCode": 1101, }, ], [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'root'], { "errorCode": 1101, }, @@ -616,7 +651,7 @@ describe('setAttribute', () => { expect(_ReportError.mock.calls).toMatchInlineSnapshot(` [ [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], { "errorCode": 1101, }, @@ -660,7 +695,7 @@ describe('setAttribute', () => { expect(_ReportError.mock.calls[0]).toMatchInlineSnapshot(` [ - [Error: snapshotPatchApply failed: ctx not found], + [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'], { "errorCode": 1101, }, diff --git a/packages/react/runtime/src/lifecycle/patch/error.ts b/packages/react/runtime/src/lifecycle/patch/error.ts new file mode 100644 index 0000000000..ed6e6fb173 --- /dev/null +++ b/packages/react/runtime/src/lifecycle/patch/error.ts @@ -0,0 +1,61 @@ +// Copyright 2025 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 { backgroundSnapshotInstanceManager, snapshotManager } from '../../snapshot.js'; + +export const ctxNotFoundType = 'Lynx.Error.CtxNotFound'; + +const errorMsg = 'snapshotPatchApply failed: ctx not found'; + +let ctxNotFoundEventListener: ((e: RuntimeProxy.Event) => void) | null = null; + +export interface CtxNotFoundData { + id: number; +} + +export function sendCtxNotFoundEventToBackground(id: number): void { + /* v8 ignore next 3 */ + if (!lynx.getJSContext) { + throw new Error(errorMsg); + } + lynx.getJSContext().dispatchEvent({ + type: ctxNotFoundType, + data: { + id, + } as CtxNotFoundData, + }); +} + +export function reportCtxNotFound(data: CtxNotFoundData): void { + const id = data.id; + const instance = backgroundSnapshotInstanceManager.values.get(id); + + let snapshotType = 'null'; + + if (instance && instance.__snapshot_def) { + for (const [snapshotId, snapshot] of snapshotManager.values.entries()) { + if (snapshot === instance.__snapshot_def) { + snapshotType = snapshotId; + break; + } + } + } + + lynx.reportError(new Error(`${errorMsg}, snapshot type: '${snapshotType}'`)); +} + +export function addCtxNotFoundEventListener(): void { + ctxNotFoundEventListener = (e) => { + reportCtxNotFound(e.data as CtxNotFoundData); + }; + lynx.getCoreContext?.().addEventListener(ctxNotFoundType, ctxNotFoundEventListener); +} + +export function removeCtxNotFoundEventListener(): void { + const coreContext = lynx.getCoreContext?.(); + if (coreContext && ctxNotFoundEventListener) { + coreContext.removeEventListener(ctxNotFoundType, ctxNotFoundEventListener); + ctxNotFoundEventListener = null; + } +} diff --git a/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts b/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts index a913d04c2a..d309b6ebad 100644 --- a/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts +++ b/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts @@ -13,6 +13,7 @@ * order and with proper error handling. */ +import { sendCtxNotFoundEventToBackground } from './error.js'; import type { SnapshotPatch } from './snapshotPatch.js'; import { SnapshotOperation } from './snapshotPatch.js'; import { @@ -24,10 +25,6 @@ import { } from '../../snapshot.js'; import type { DynamicPartType } from '../../snapshot.js'; -function reportCtxNotFound(): void { - lynx.reportError(new Error(`snapshotPatchApply failed: ctx not found`)); -} - /** * Applies a patch of snapshot operations to the main thread. * This is the counterpart to the patch generation in the background thread. @@ -51,7 +48,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void { const child = snapshotInstanceManager.values.get(childId); const existingNode = snapshotInstanceManager.values.get(beforeId!); if (!parent || !child) { - reportCtxNotFound(); + sendCtxNotFoundEventToBackground(parent ? childId : parentId); } else { parent.insertBefore(child, existingNode); } @@ -63,7 +60,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void { const parent = snapshotInstanceManager.values.get(parentId); const child = snapshotInstanceManager.values.get(childId); if (!parent || !child) { - reportCtxNotFound(); + sendCtxNotFoundEventToBackground(parent ? childId : parentId); } else { parent.removeChild(child); } @@ -77,7 +74,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void { if (si) { si.setAttribute(dynamicPartIndex, value); } else { - reportCtxNotFound(); + sendCtxNotFoundEventToBackground(id); } break; } @@ -88,7 +85,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void { if (si) { si.setAttribute('values', values); } else { - reportCtxNotFound(); + sendCtxNotFoundEventToBackground(id); } break; } diff --git a/packages/react/runtime/src/lynx.ts b/packages/react/runtime/src/lynx.ts index fcfbd63463..05d1789ba7 100644 --- a/packages/react/runtime/src/lynx.ts +++ b/packages/react/runtime/src/lynx.ts @@ -9,12 +9,14 @@ import { initProfileHook } from './debug/profile.js'; import { document, setupBackgroundDocument } from './document.js'; import { initDelayUnmount } from './lifecycle/delayUnmount.js'; import { replaceCommitHook } from './lifecycle/patch/commit.js'; +import { addCtxNotFoundEventListener } from './lifecycle/patch/error.js'; import { injectUpdateMainThread } from './lifecycle/patch/updateMainThread.js'; import { injectCalledByNative } from './lynx/calledByNative.js'; -import { setupLynxTestingEnv } from './lynx/env.js'; +import { setupLynxEnv } from './lynx/env.js'; import { injectLepusMethods } from './lynx/injectLepusMethods.js'; import { initTimingAPI } from './lynx/performance.js'; import { injectTt } from './lynx/tt.js'; + export { runWithForce } from './lynx/runWithForce.js'; // @ts-expect-error Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature @@ -44,6 +46,7 @@ if (__BACKGROUND__) { options.document = document as any; setupBackgroundDocument(); injectTt(); + addCtxNotFoundEventListener(); if (process.env['NODE_ENV'] === 'test') {} else { @@ -53,4 +56,4 @@ if (__BACKGROUND__) { } } -setupLynxTestingEnv(); +setupLynxEnv(); diff --git a/packages/react/runtime/src/lynx/env.ts b/packages/react/runtime/src/lynx/env.ts index f86a0e6e2f..a2ba5a91a0 100644 --- a/packages/react/runtime/src/lynx/env.ts +++ b/packages/react/runtime/src/lynx/env.ts @@ -3,7 +3,7 @@ // LICENSE file in the root directory of this source tree. import type { DataProcessorDefinition } from '../lynx-api.js'; -export function setupLynxTestingEnv(): void { +export function setupLynxEnv(): void { if (!__LEPUS__) { const { initData, updateData } = lynxCoreInject.tt._params; // @ts-ignore diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index dc8a2a2f97..df9034d3f9 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -18,6 +18,7 @@ import { delayedEvents, delayedPublishEvent } from '../lifecycle/event/delayEven 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 { CHILDREN } from '../renderToOpcodes/constants.js'; @@ -36,6 +37,7 @@ function injectTt(): void { tt.callDestroyLifetimeFun = () => { destroyWorklet(); destroyBackground(); + removeCtxNotFoundEventListener(); }; tt.updateGlobalProps = updateGlobalProps; tt.updateCardData = updateCardData;