Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-snakes-sink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react": patch
---

Add snapshot id report when throwing `snapshotPatchApply failed: ctx not found` error.
59 changes: 47 additions & 12 deletions packages/react/runtime/__test__/snapshotPatch.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
});
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
]
`);
Expand All @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down
61 changes: 61 additions & 0 deletions packages/react/runtime/src/lifecycle/patch/error.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -77,7 +74,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void {
if (si) {
si.setAttribute(dynamicPartIndex, value);
} else {
reportCtxNotFound();
sendCtxNotFoundEventToBackground(id);
}
break;
}
Expand All @@ -88,7 +85,7 @@ export function snapshotPatchApply(snapshotPatch: SnapshotPatch): void {
if (si) {
si.setAttribute('values', values);
} else {
reportCtxNotFound();
sendCtxNotFoundEventToBackground(id);
}
break;
}
Expand Down
7 changes: 5 additions & 2 deletions packages/react/runtime/src/lynx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,6 +46,7 @@ if (__BACKGROUND__) {
options.document = document as any;
setupBackgroundDocument();
injectTt();
addCtxNotFoundEventListener();

if (process.env['NODE_ENV'] === 'test') {}
else {
Expand All @@ -53,4 +56,4 @@ if (__BACKGROUND__) {
}
}

setupLynxTestingEnv();
setupLynxEnv();
2 changes: 1 addition & 1 deletion packages/react/runtime/src/lynx/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/react/runtime/src/lynx/tt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -36,6 +37,7 @@ function injectTt(): void {
tt.callDestroyLifetimeFun = () => {
destroyWorklet();
destroyBackground();
removeCtxNotFoundEventListener();
};
tt.updateGlobalProps = updateGlobalProps;
tt.updateCardData = updateCardData;
Expand Down
Loading