diff --git a/.changeset/reactlynx-snapshot-debugging.md b/.changeset/reactlynx-snapshot-debugging.md new file mode 100644 index 0000000..5814e1a --- /dev/null +++ b/.changeset/reactlynx-snapshot-debugging.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/skill-reactlynx-snapshot-debugging": minor +--- + +Add the `reactlynx-snapshot-debugging` skill: a troubleshooting guide for ReactLynx 3 Snapshot runtime errors (`snapshotPatchApply failed: ctx not found` and `Snapshot not found`). It covers the dual-thread hydrate/snapshotPatch model, a rule-out-upstream-crash diagnosis workflow, and snapshotPatch decoding. For capturing the dual-thread traffic it leads with the built-in ALog macros (`REACT_ALOG` / `REACT_ALOG_ELEMENT_API`, with version gating from `@lynx-js/react` 0.111.2) and driving the `lynx-devtool` skill to dump and analyze both threads directly, falling back to manual entry-file instrumentation only on older runtimes. Wired into the `reactlynx` plugin. diff --git a/package.json b/package.json index e8172fb..a8430a5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@lynx-js/skill-lynx-typescript": "workspace:*", "@lynx-js/skill-lynx-ui": "3.133.4", "@lynx-js/skill-reactlynx-best-practices": "workspace:*", + "@lynx-js/skill-reactlynx-snapshot-debugging": "workspace:*", "@lynx-js/skill-rspeedy-bundle-size": "workspace:*", "@lynx-js/skill-vanilla-lynx": "workspace:*" }, diff --git a/packages/plugins/reactlynx/package.json b/packages/plugins/reactlynx/package.json index 383df28..2200649 100644 --- a/packages/plugins/reactlynx/package.json +++ b/packages/plugins/reactlynx/package.json @@ -18,7 +18,8 @@ "build": "build-plugin" }, "dependencies": { - "@lynx-js/skill-reactlynx-best-practices": "workspace:*" + "@lynx-js/skill-reactlynx-best-practices": "workspace:*", + "@lynx-js/skill-reactlynx-snapshot-debugging": "workspace:*" }, "devDependencies": { "build-plugin": "workspace:*" diff --git a/packages/skills/reactlynx-snapshot-debugging/SKILL.md b/packages/skills/reactlynx-snapshot-debugging/SKILL.md new file mode 100644 index 0000000..bb1f5e9 --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/SKILL.md @@ -0,0 +1,82 @@ +--- +name: reactlynx-snapshot-debugging +description: | + Use this Skill to diagnose and fix ReactLynx 3 (RL3) Snapshot runtime errors that surface during first-screen hydrate or subsequent state updates. RL3 compiles JSX into Snapshot definitions and drives the UI through dual-thread hydrate + snapshotPatch, so these errors are usually a downstream symptom of a different crash on the main thread or background thread. + + Trigger Scenarios: + - The runtime logs "snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_...'". + - The runtime logs "Error: Snapshot not found: __Card__:__snapshot_..." (or "Snapshot not found"). + - A ReactLynx page renders blank / stops updating after hydrate, or a "not a function" / "cannot read property '0' of undefined" error appears in the hydrate call stack. + - The user needs to inspect dual-thread communication: the OnLifecycleEvent SnapshotInstance tree (main -> background) or the rLynxChange snapshotPatch (background -> main), or decode a raw snapshotPatch array. + - The user mentions RL3 snapshot, createSnapshot, snapshotPatchApply, SnapshotInstance, hydrate, rLynxChange, or a bug report about a ReactLynx snapshot error. +--- + +# ReactLynx 3 Snapshot Error Debugging + +Diagnose and fix RL3 Snapshot runtime errors. This is a troubleshooting guide, not a code generator: read the evidence, find the *root* crash, then fix that. + +## Background: how RL3 snapshots work + +To reduce vnode count and improve performance, RL3 compiles JSX into **Snapshot** definitions. A snapshot definition is created by `createSnapshot(...)` at the very top level of the main-thread bundle (`main-thread.js` / `lepus.js`): + +```js +const __snapshot_d9459_61989_1 = (__webpack_require__(".../react/runtime/lib/internal.js").createSnapshot)( + "__snapshot_d9459_61989_1", + function () { /* create elements */ }, + [ /* update dynamic parts */ ], + ... +); +``` + +The runtime then renders through a **dual-thread** flow: + +- **Main thread** builds a `SnapshotInstance` tree from these definitions and owns the real UI (Element PAPI). +- **Background thread** builds a `BackgroundSnapshotInstance` tree from your React render. +- **`OnLifecycleEvent` (`rLynxFirstScreen`) + hydrate**: matches the main-thread tree against the background tree, diffs them, and sends a `snapshotPatch` to the main thread to run `snapshotPatchApply` and reconcile the UI. +- **`rLynxChange`**: every later update (hydrate result and `setState`) is delivered as a `snapshotPatch` over `rLynxChange`, which the main thread applies via `snapshotPatchApply`. + +Because the whole UI hangs off this pipeline, a crash *anywhere upstream* (a main-thread definition that never got created, or a background hydrate that threw) shows up later as a confusing snapshot error. + +## The first rule: rule out unrelated errors first + +**Most snapshot errors are a secondary symptom.** Before analyzing the snapshot error itself, confirm there are **no other Errors** on either thread: + +- **Main thread** (`main-thread.js` / `lepus.js`): if it throws before finishing, every `createSnapshot(...)` after the throw never runs, so those snapshot *definitions* are missing. Later patches that reference them fail. +- **Background thread**: if it throws so that `hydrate` doesn't complete, the main thread never gets the correct initial patch, and *every* subsequent `snapshotPatchApply` operates on a wrong/empty tree. + +So the correct order is: **find and fix the first non-snapshot Error on each thread, then re-check whether the snapshot error is gone.** Only treat a snapshot error as a framework bug after both threads are otherwise clean. + +## Diagnosis workflow + +1. Collect the full error log from **both threads**, in order. Note which thread each line came from and the call stack (especially anything inside `hydrate` / `onHydrate` / `onLifecycleEventImpl`). +2. Look for an **earlier, non-snapshot error** (e.g. `not a function`, `cannot read property '0' of undefined`). If present, that is almost certainly the root cause — fix it first. See `references/diagnosis-workflow.md`. +3. To inspect the dual-thread traffic, prefer the **built-in ALog macros** (available since `@lynx-js/react` **0.111.2**): rebuild/run with `REACT_ALOG=true` (add `REACT_ALOG_ELEMENT_API=true`, `@lynx-js/react` >= 0.116.3, for Element PAPI tracing) so the runtime emits `[ReactLynxDebug]` diagnostics on both threads. **With the user's consent, you can dump and analyze these logs yourself** by driving the [`lynx-devtool`](../lynx-devtool/SKILL.md) skill (`get-console --thread main` / `--thread background`, reload to catch first-screen hydrate). The manual instrumentation snippet is only needed on `@lynx-js/react` < 0.111.2 (no built-in ALog). All of this is in `references/debug-instrumentation.md`. +4. Decode any raw `snapshotPatch` array using `references/snapshot-patch-format.md`. +5. Match the specific error to its playbook below. +6. If the case is complex and none of the above resolves it, escalate upstream with a minimal reproduction (see references). + +You are not limited to handing the user a script: when a device/emulator is connected, offer to enable ALog, dump both threads via lynx-devtool, and analyze the root cause directly — only after the user agrees, since reloading restarts their session. + +## Error catalog + +| Error message | Where it throws | Read | +| --- | --- | --- | +| `snapshotPatchApply failed: ctx not found, snapshot type: '...'` | inside `snapshotPatchApply` (InsertBefore / RemoveChild / SetAttribute / SetAttributes) | `references/error-ctx-not-found.md` | +| `Error: Snapshot not found: __Card__:__snapshot_...` | `SnapshotInstance` constructor | `references/error-snapshot-not-found.md` | + +Both playbooks share the same first move: **rule out an upstream crash** (the "first rule" above). + +## Tooling reference + +- `references/debug-instrumentation.md` — how to capture dual-thread logs: the built-in ALog macros (`REACT_ALOG` / `REACT_ALOG_ELEMENT_API`, with version requirements) first, dumping + analyzing them yourself via `lynx-devtool`, and the manual instrumentation snippet ([gist](https://gist.github.com/upupming/9f8c5d006dbfaccc225ca6b2ad32e8b5)) as a fallback only for `@lynx-js/react` < 0.111.2. Plus how to read the output. +- `references/snapshot-patch-format.md` — the `snapshotPatch` opcode table and how to pretty-print a raw patch. +- `references/diagnosis-workflow.md` — the end-to-end rule-out procedure, including the Lynx debugger's "Stop at Entry" (main-thread / MTS) setting to get accurate main-thread line numbers. + +## Source of truth + +Pinned RL3 runtime source (lynx-family/lynx-stack @ `e9e7c093`): + +- `snapshotPatchApply` — [packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts#L51](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts#L51) +- `SnapshotInstance` constructor — [packages/react/runtime/src/snapshot.ts#L293](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/snapshot.ts#L293) +- `snapshotPatch` op definitions — [packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts#L22](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts#L22) +- `prettyFormatSnapshotPatch` — [packages/react/runtime/src/debug/formatPatch.ts#L8](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/debug/formatPatch.ts#L8) diff --git a/packages/skills/reactlynx-snapshot-debugging/evals/evals.json b/packages/skills/reactlynx-snapshot-debugging/evals/evals.json new file mode 100644 index 0000000..e7eeb0e --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/evals/evals.json @@ -0,0 +1,48 @@ +{ + "skill_name": "reactlynx-snapshot-debugging", + "description": "Task evals for diagnosing and fixing ReactLynx 3 Snapshot runtime errors (ctx not found / Snapshot not found) via dual-thread hydrate and snapshotPatch analysis.", + "evals": [ + { + "id": 1, + "name": "ctx-not-found-root-cause", + "prompt": "我的 ReactLynx 3 页面白屏,日志里主线程先报 not a function,后台线程在 hydrate 时报 cannot read property '0' of undefined,最后主线程报 snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_d9459_61989_1'。怎么排查和修复?", + "expected_output": "An explanation that ctx not found is a downstream symptom: the real root cause is the earlier main-thread 'not a function' crash which leaves the main-thread SnapshotInstance tree empty and breaks hydrate, so subsequent snapshotPatchApply cannot find the ctx. Guidance to rule out the upstream error first, use the Lynx debugger's 'Stop at Entry' (main-thread / MTS) to find the real main-thread throw location, and gate background-only code (e.g. wrap the offending call in `if (__BACKGROUND__) { ... }`) so unsupported native/JSB code is not bundled into the main thread.", + "expectations": [ + "The answer says ctx not found is usually a secondary symptom and the first/earlier non-snapshot error (the main-thread 'not a function') must be found and fixed first.", + "The answer explains that a main-thread crash prevents createSnapshot definitions from running so the main-thread SnapshotInstance tree ends up empty (only root), and that a background hydrate throw breaks subsequent snapshotPatchApply.", + "The answer recommends enabling the Lynx debugger's 'Stop at Entry' (main-thread / MTS) and reloading to get accurate main-thread line/column numbers for the real throw.", + "The answer recommends gating background-only / unsupported-on-main-thread code (for example wrapping it in `if (__BACKGROUND__) { ... }`, or its deprecated alias `__JS__`) so it is not compiled into the main-thread bundle.", + "The answer identifies snapshotPatchApply as the throw site and does not claim the fix is inside snapshotPatchApply itself." + ], + "files": [] + }, + { + "id": 2, + "name": "snapshot-not-found-investigation", + "prompt": "ReactLynx 3 线上报 Error: Snapshot not found: __Card__:__snapshot_d6204_98358_1,但本地开发没问题。这是什么原因,怎么查?", + "expected_output": "An explanation that 'Snapshot not found' throws in the SnapshotInstance constructor because the Snapshot definition for that type is not registered, and that a prod-only reproduction points at a compile/bundling problem. Guidance to first rule out an earlier main-thread crash, then grep the built lepus.js/main-thread.js for the exact snapshot type string; if absent it is a build/bundling issue, if present analyze OnLifecycleEvent/rLynxChange traffic; escalate upstream with a minimal reproduction if unresolved.", + "expectations": [ + "The answer says the error throws in the SnapshotInstance constructor and means the Snapshot definition (the type) is missing/not registered, distinct from ctx not found.", + "The answer says to first rule out another main-thread error that terminates the bundle early and drops later createSnapshot definitions.", + "The answer tells the user to search the built main-thread output (lepus.js / main-thread.js) for the exact type '__snapshot_d6204_98358_1'.", + "The answer says a dev-passes / prod-fails pattern indicates a compile or bundling problem, and that if the type is present at build time the next step is analyzing OnLifecycleEvent and rLynxChange.", + "The answer suggests escalating upstream (e.g. a lynx-stack issue with a minimal reproduction) for complex/unresolved cases." + ], + "files": [] + }, + { + "id": 3, + "name": "decode-snapshot-patch-and-instrument", + "prompt": "我想搞清楚 ReactLynx 3 后台线程发给主线程的 snapshotPatch 到底做了什么,比如 [0, \"__Card__:__snapshot_fffe1_test_3\", 2, 0, \"__Card__:__snapshot_fffe1_test_4\", 7, 1, 2, 7, null, 1, -1, 2, null]。怎么反解?还有怎么打印双线程通信数据?", + "expected_output": "A decoding of the flat snapshotPatch using the opcode table (0=CreateElement[type,id], 1=InsertBefore[parentId,childId,beforeId], 2=RemoveChild, 3=SetAttribute, 4=SetAttributes), yielding CreateElement id 2, CreateElement id 7, InsertBefore child 7 into parent 2, InsertBefore child 2 into parent -1 (root). Plus guidance to install the entry-file instrumentation that wraps OnLifecycleEvent (main->background SnapshotInstance tree) and rLynxChange (background->main snapshotPatch, pretty-formatted) and logs Element PAPI calls.", + "expectations": [ + "The answer decodes the array as a sequence of ops by reading one opcode then consuming its parameters, producing at least: CreateElement id 2, CreateElement id 7, InsertBefore into parent 2, InsertBefore into parent -1.", + "The answer gives the opcode meanings, at minimum 0=CreateElement, 1=InsertBefore, 2=RemoveChild, 3=SetAttribute, 4=SetAttributes.", + "The answer notes parentId -1 is the root.", + "The answer explains you can instrument the entry (index.tsx) to wrap OnLifecycleEvent and rLynxChange to print the SnapshotInstance tree and the pretty-formatted snapshotPatch.", + "The answer connects a missing CreateElement for a referenced id to the ctx not found error." + ], + "files": [] + } + ] +} diff --git a/packages/skills/reactlynx-snapshot-debugging/evals/trigger_eval.json b/packages/skills/reactlynx-snapshot-debugging/evals/trigger_eval.json new file mode 100644 index 0000000..444a61e --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/evals/trigger_eval.json @@ -0,0 +1,38 @@ +[ + { + "query": "ReactLynx 报 snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_d9459_61989_1',怎么修?", + "should_trigger": true + }, + { + "query": "线上报 Error: Snapshot not found: __Card__:__snapshot_d6204_98358_1,本地没问题,帮我排查。", + "should_trigger": true + }, + { + "query": "RL3 页面 hydrate 之后白屏,后台线程报 cannot read property '0' of undefined,主线程也有报错,怎么定位根因?", + "should_trigger": true + }, + { + "query": "怎么打印 ReactLynx 双线程通信里的 OnLifecycleEvent SnapshotInstance 树和 rLynxChange 的 snapshotPatch?", + "should_trigger": true + }, + { + "query": "帮我把这个 snapshotPatch 数组反解成可读的操作列表。", + "should_trigger": true + }, + { + "query": "ReactLynx 里 useLayoutEffect 为什么不适合做布局测量?", + "should_trigger": false + }, + { + "query": "帮我用 vanilla Lynx Element PAPI 写一个计数器卡片。", + "should_trigger": false + }, + { + "query": "Lynx DevTool 怎么拉 console 日志?", + "should_trigger": false + }, + { + "query": "帮我把 rspeedy 的包体积优化一下。", + "should_trigger": false + } +] diff --git a/packages/skills/reactlynx-snapshot-debugging/package.json b/packages/skills/reactlynx-snapshot-debugging/package.json new file mode 100644 index 0000000..fab363e --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/package.json @@ -0,0 +1,23 @@ +{ + "name": "@lynx-js/skill-reactlynx-snapshot-debugging", + "version": "0.0.0", + "description": "Diagnose and fix ReactLynx 3 (RL3) Snapshot runtime errors such as 'snapshotPatchApply failed: ctx not found' and 'Snapshot not found', using dual-thread hydrate/snapshotPatch analysis.", + "repository": { + "url": "https://github.com/lynx-community/skills" + }, + "type": "module", + "files": [ + "SKILL.md", + "scripts", + "references", + "examples", + "reference.md", + "examples.md", + "rules" + ], + "scripts": {}, + "devDependencies": {}, + "engines": { + "node": ">=18" + } +} diff --git a/packages/skills/reactlynx-snapshot-debugging/references/debug-instrumentation.md b/packages/skills/reactlynx-snapshot-debugging/references/debug-instrumentation.md new file mode 100644 index 0000000..aa58ee4 --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/references/debug-instrumentation.md @@ -0,0 +1,275 @@ +# Capturing and analyzing dual-thread logs + +To find the root cause you need the two threads' traffic: the `OnLifecycleEvent` (`rLynxFirstScreen`) **SnapshotInstance tree** (main -> background, used by hydrate), the `rLynxChange` **snapshotPatch** (background -> main), and optionally every main-thread **Element PAPI** call. + +There are two ways to get this. **Prefer Method A** (built-in, maintained by the framework). Use Method B only as a fallback. + +## Method A (preferred): built-in ALog macros + +ReactLynx has a native logging channel (ALog) gated by build-time macros. See `lynx-website` `docs/en/react/build-time-macros.mdx` (the "Logging macros" section). Both are **off by default** — enable them at build time via env vars: + +| Env var | Macro | Emits | +| --- | --- | --- | +| `REACT_ALOG=true` | `__ALOG__` | detailed `[ReactLynxDebug]` diagnostics on **both threads** — the framework's own trace of hydrate and the snapshotPatch flow. | +| `REACT_ALOG_ELEMENT_API=true` | `__ALOG_ELEMENT_API__` | every [Element PAPI](https://lynxjs.org/api/engine/element-api.html) call (create / append / update). Very noisy; enable only when you need to see how the element tree is built. | + +```bash +# rebuild (or dev) with the framework diagnostics on +REACT_ALOG=true rspeedy dev +# add Element PAPI tracing when you need the low-level tree ops +REACT_ALOG=true REACT_ALOG_ELEMENT_API=true rspeedy build +``` + +The `[ReactLynxDebug]` tag is the same one the manual snippet below prints — `REACT_ALOG=true` is the built-in equivalent of hand-instrumenting the runtime, so reach for it first. It emits the same signals: `MTS -> BTS OnLifecycleEvent`, the SnapshotInstance tree for first-screen hydration, the background tree before/after hydration, and `BTS -> MTS updateMainThread` (the snapshotPatch). + +### Version requirements (check the project's `@lynx-js/react` first) + +| Feature | Available since | Introduced by | +| --- | --- | --- | +| `REACT_ALOG` / `__ALOG__` (`[ReactLynxDebug]` dual-thread diagnostics) | `@lynx-js/react` **0.111.2** (needs `@lynx-js/react-webpack-plugin` >= 0.6.19) | [#1164](https://github.com/lynx-family/lynx-stack/pull/1164), 2025-07-17 | +| `REACT_ALOG_ELEMENT_API` / `__ALOG_ELEMENT_API__` as a **separate** toggle | `@lynx-js/react` **0.116.3** | [#2192](https://github.com/lynx-family/lynx-stack/pull/2192) | + +Notes: + +- On **0.111.2 – 0.116.2**, Element PAPI calls were logged together with `REACT_ALOG=true` (there was no separate env var; #2192 later split them out and made element-api logging off-by-default). +- On **`@lynx-js/react` < 0.111.2** there is no built-in ALog — that is the *only* case that needs the manual snippet (Method B). + +Check the project's version with `cat node_modules/@lynx-js/react/package.json | grep version` (or your lockfile) before deciding. + +## Dump and analyze the logs yourself with lynx-devtool + +Once ALog is on, you do not have to ask the user to copy logs around. **With the user's consent**, drive the [`lynx-devtool`](../../lynx-devtool/SKILL.md) skill to dump both threads directly and analyze the root cause yourself: + +1. Confirm the app is running with `REACT_ALOG=true` (and, if needed, `REACT_ALOG_ELEMENT_API=true`) and a device/emulator is connected. +2. Find the target: `list-clients`, then `list-sessions`. +3. Reload to capture the first-screen hydrate (the critical window): `cdp --session -m Page.reload '{}'` (or reopen the page). +4. Dump each thread, filtering for the framework tag: + ```bash + node /scripts/index.mjs get-console --thread main # main-thread.js / lepus.js + node /scripts/index.mjs get-console --thread background # JS runtime + ``` + Grep the output for `[ReactLynxDebug]` (and `[ReactLynxDebug-Element]` when Element API logging is on). +5. Analyze the dump using "How to read the output" below and the per-error playbooks, then report the root cause. + +> Always get the user's agreement before connecting to their device and reloading their page — a reload restarts their session. See the `lynx-devtool` skill for the full command surface (`get-console` levels/threads, CDP, component tree, etc.). + +### Worked walkthrough (lynx-devtool, `ctx not found`) + +App rebuilt with `REACT_ALOG=true`, device connected. `
` = the `lynx-devtool` skill's `scripts/index.mjs`. + +1. Find the client and session: + ```bash + node
list-clients + # → clientId 1 (com.example.app) + node
list-sessions --client 1 + # → sessionId 42 (…/template.js) + ``` +2. Reload to capture first-screen hydrate (with the user's consent — it restarts their page): + ```bash + node
cdp --session 42 -m Page.reload '{}' + ``` +3. Dump both threads and keep only the framework lines: + ```bash + node
get-console --thread main | grep -iA2 'ReactLynxDebug\|not a function\|exception' + node
get-console --thread background | grep -iA2 'ReactLynxDebug\|cannot read' + ``` +4. Read the dump — a typical `ctx not found` chain looks like: + ```text + [main-thread] ReportError: TypeError: not a function ← the real root cause, fires first + [main-thread] [ReactLynxDebug] SnapshotInstance tree for first screen hydration: + [main-thread] | -1(root): null ← empty tree: main thread crashed before createSnapshot + [background] [ReactLynxDebug] MTS -> BTS OnLifecycleEvent: rLynxFirstScreen … + [background] TypeError: cannot read property '0' of undefined ← hydrate throws on the empty tree + [background] [ReactLynxDebug] BTS -> MTS updateMainThread: patchList:[{ snapshotPatch:[ InsertBefore … __snapshot_d9459_61989_1 … ] }] + [main-thread] snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_d9459_61989_1' + ``` +5. Conclusion: the `snapshotPatchApply` line is the *last* symptom, not the cause. The main-thread `not a function` fired first and left the SnapshotInstance tree empty (`| -1(root)` only), so hydrate threw and the follow-up patch referenced a `__snapshot_…` the main thread never created. **Fix the main-thread `not a function`** (see `error-ctx-not-found.md` / `diagnosis-workflow.md`), then re-dump: the tree should now hydrate with real children and the error is gone. + +## Method B (fallback): manual instrumentation + +Use this only when the project's `@lynx-js/react` is **< 0.111.2** (no built-in ALog), or when you truly cannot rebuild with the env vars. On any version that supports `REACT_ALOG`, prefer Method A. Install this snippet at the **very top** of your entry file (`index.tsx`), before `root.render()`. + +Source: . + +> **Import path**: `__root` comes from the ReactLynx runtime internal entry, `@lynx-js/react/internal`. If your project uses a different alias for the `@lynx-js/react` package, adjust the import accordingly. + +```tsx +import { __root } from '@lynx-js/react/internal'; + +// Add this before your `root.render()` statement in your entry tsx file +{ + const SnapshotOperation = { + CreateElement: 0, + InsertBefore: 1, + RemoveChild: 2, + SetAttribute: 3, + SetAttributes: 4, + DEV_ONLY_AddSnapshot: 100, + DEV_ONLY_RegisterWorklet: 101, + }; + const SnapshotOperationParams = /* @__PURE__ */ { + [SnapshotOperation.CreateElement]: { name: 'CreateElement', params: ['type', /* string */ 'id' /* number */] }, + [SnapshotOperation.InsertBefore]: { + name: 'InsertBefore', + params: ['parentId', /* number */ 'childId', /* number */ 'beforeId' /* number | undefined */], + }, + [SnapshotOperation.RemoveChild]: { name: 'RemoveChild', params: ['parentId', /* number */ 'childId' /* number */] }, + [SnapshotOperation.SetAttribute]: { + name: 'SetAttribute', + params: ['id', /* number */ 'dynamicPartIndex', /* number */ 'value' /* any */], + }, + [SnapshotOperation.SetAttributes]: { name: 'SetAttributes', params: ['id', /* number */ 'values' /* any */] }, + [SnapshotOperation.DEV_ONLY_AddSnapshot]: { + name: 'DEV_ONLY_AddSnapshot', + params: [ + 'uniqID', /* string */ + 'create', /* string */ + 'update', /* string[] */ + 'slot', /* [DynamicPartType, number][] */ + 'cssId', /* number | undefined */ + 'entryName', /* string | undefined */ + ], + }, + [SnapshotOperation.DEV_ONLY_RegisterWorklet]: { + name: 'DEV_ONLY_RegisterWorklet', + params: ['hash', /* string */ 'fnStr' /* string */], + }, + }; + + function prettyFormatSnapshotPatch(snapshotPatch) { + if (!snapshotPatch) { + return []; + } + const result = []; + for (let i = 0; i < snapshotPatch.length;) { + const op = snapshotPatch[i]; + const config = SnapshotOperationParams[op]; + if (config) { + const formattedOp = { op: config.name }; + config.params.forEach((param, index) => { + formattedOp[param] = snapshotPatch[i + 1 + index]; + }); + result.push(formattedOp); + i += 1 + config.params.length; + } else { + throw new Error(`Unknown snapshot operation: ${op}`); + } + } + return result; + } + + function printSnapshotInstance(instance, log = console.alog) { + const impl = (instance, level) => { + let msg = ''; + for (let i = 0; i < level; ++i) { + msg += ' '; + } + msg += `| ${instance.id ?? instance.__id}(${instance.type}): ${JSON.stringify(instance.values ?? instance.__values)}`; + log(msg); + for (const c of (instance.childNodes ?? instance.children ?? [])) { + impl(c, level + 1); + } + }; + impl(instance, 0); + } + + if (__JS__) { + const oldOnLifecycleEvent = lynxCoreInject.tt.OnLifecycleEvent; + lynxCoreInject.tt.OnLifecycleEvent = (...args) => { + const printArgs = [...args]; + if (args[0][0] === 'rLynxFirstScreen') { + if (typeof args[0][1].root === 'string') { + printArgs[0] = args[0].slice(); + const root = JSON.parse(args[0][1].root); + printArgs[0][1] = { ...args[0][1], root }; + console.alog('[ReactLynxDebug] SnapshotInstance tree for first screen hydration:'); + printSnapshotInstance(root); + } + } + console.alog('[ReactLynxDebug] OnLifecycleEvent', JSON.stringify(printArgs, null, 2)); + + if (args[0][0] === 'rLynxFirstScreen') { + console.alog('[ReactLynxDebug] BackgroundSnapshotInstance tree before hydrate:'); + printSnapshotInstance(__root); + } + + oldOnLifecycleEvent(...args); + + if (args[0][0] === 'rLynxFirstScreen') { + console.alog('[ReactLynxDebug] BackgroundSnapshotInstance tree after hydrate:'); + printSnapshotInstance(__root); + } + }; + } else { + console.alog('[ReactLynxDebug] globalThis.rLynxChange', globalThis.rLynxChange, typeof globalThis.rLynxChange); + const oldRLynxChange = globalThis.rLynxChange; + globalThis.rLynxChange = (...args) => { + const printArgs = [...args]; + if (typeof args[0].data === 'string') { + const parsedData = JSON.parse(args[0].data); + printArgs[0] = { + ...args[0], + data: { + ...parsedData, + patchList: parsedData.patchList.map(patch => ({ + ...patch, + snapshotPatch: prettyFormatSnapshotPatch(patch.snapshotPatch), + })), + }, + }; + } + console.alog('[ReactLynxDebug] rLynxChange', JSON.stringify(printArgs, null, 2)); + oldRLynxChange(...args); + }; + + const api = [ + '__CreatePage', '__CreateElement', '__CreateWrapperElement', '__CreateText', + '__CreateImage', '__CreateView', '__CreateRawText', '__CreateList', + '__AppendElement', '__InsertElementBefore', '__RemoveElement', '__ReplaceElement', + '__FirstElement', '__LastElement', '__NextElement', '__GetPageElement', + '__GetTemplateParts', '__AddDataset', '__SetDataset', '__GetDataset', + '__SetAttribute', '__GetAttributes', '__GetAttributeByName', '__GetAttributeNames', + '__SetClasses', '__SetCSSId', '__AddInlineStyle', '__SetInlineStyles', + '__AddEvent', '__SetID', '__GetElementUniqueID', '__GetTag', + '__FlushElementTree', '__UpdateListCallbacks', '__OnLifecycleEvent', + '__QueryComponent', '__SetGestureDetector', + ]; + + let count = 0; + api.forEach(api => { + const old = globalThis[api]; + globalThis[api] = (...args) => { + const printArgs = [...args]; + if ( + api === '__AppendElement' || api === '__InsertElementBefore' + || api === '__ReplaceElement' || api === '__RemoveElement' + ) { + printArgs[0] = __GetTag(args[0]); + printArgs[1] = __GetTag(args[1]); + } + if (api === '__InsertElementBefore') { + printArgs[2] = __GetTag(args[2]); + } + + console.alog('[ReactLynxDebug-Element] API', ++count, api, ...printArgs); + + lynx.performance.profileStart(api, { args: { args: JSON.stringify(args) } }); + const ans = old(...args); + lynx.performance.profileEnd(); + return ans; + }; + }); + } +} +``` + +## How to read the output + +Applies to both methods (`[ReactLynxDebug]` lines from ALog or the snippet): + +- **`SnapshotInstance tree for first screen hydration`** — the main thread's tree at hydrate time. If it is just `| -1(root): ...` with no children, the main thread crashed before creating its snapshots — go fix that crash (see `diagnosis-workflow.md`). +- **`BackgroundSnapshotInstance tree before/after hydrate`** — the background tree. Compare the two: after a healthy hydrate it should mirror the main-thread tree. If hydrate threw, "after" is incomplete. +- **`rLynxChange` / `snapshotPatch`** — the operations the background is asking the main thread to apply. Search it for the `type` in your error message (e.g. `__snapshot_d9459_61989_1`). If a patch tries to `InsertBefore` into an id whose `CreateElement` never happened (because hydrate aborted), you get `ctx not found`. +- **`[ReactLynxDebug-Element]`** — the exact Element PAPI sequence executed on the main thread, useful to confirm what actually rendered. + +`console.alog` is the async log channel; view it via `lynx-devtool get-console` or the client's log console. diff --git a/packages/skills/reactlynx-snapshot-debugging/references/diagnosis-workflow.md b/packages/skills/reactlynx-snapshot-debugging/references/diagnosis-workflow.md new file mode 100644 index 0000000..f2ed4af --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/references/diagnosis-workflow.md @@ -0,0 +1,60 @@ +# Diagnosis workflow: rule out the upstream crash first + +Snapshot errors are usually a downstream symptom. Follow this procedure before concluding it is a framework bug. + +## 1. Read both threads' errors, in order + +Collect the raw log and separate the lines by thread: + +- **Main thread** = `main-thread.js` / `lepus.js`. In the client this often appears as `ReportErrorWithMsg` / `QuickContext::Execute() exception` / `lepusng exception`. +- **Background thread** = the JS runtime. Errors here carry a normal JS stack. + +Order matters. A typical failing sequence looks like: + +1. Main thread throws first (e.g. `TypeError: not a function`). +2. Background thread throws during hydrate (e.g. `cannot read property '0' of undefined`, stack goes `helper` -> `hydrate` -> `onLifecycleEventImpl`). +3. Main thread then logs `snapshotPatchApply failed: ctx not found`. + +The third error is the noisy one, but the **first** error is the root cause. + +## 2. Two upstream failure modes + +- **Main-thread throws before finishing.** `createSnapshot(...)` calls run at the top level of the main-thread bundle. If the bundle throws partway, every snapshot definition *after* the throw is never registered. The main-thread `SnapshotInstance` tree ends up empty (only a `root`, `{"id":-1,"type":"root"}`). +- **Background hydrate throws.** If `hydrate` doesn't complete, the initial diff/patch is wrong or missing, and every later `snapshotPatchApply` operates on a broken tree. + +In both modes, fixing the first error makes the snapshot error disappear. + +## 3. Get accurate main-thread line numbers + +The client-reported main-thread stack is often unhelpful (minified, wrong columns). To get the real throw location, use your Lynx debugger's **"Stop at Entry"** setting for the main thread (MTS): + +1. Enable "Stop at Entry" (main-thread / MTS) in the debugger. +2. Reload the page. Execution pauses at the main-thread entry. +3. Step/continue until the exception; now the reported row/column points at the real offending call in `lepus.js` / `main-thread.js`. +4. Map that back to source to find the call site. + +## 4. Worked example (representative) + +Symptom chain: main thread `not a function` -> background `cannot read property '0' of undefined` in `hydrate` -> main thread `snapshotPatchApply failed: ctx not found`. + +Root cause found by pausing at the main-thread entry: the main-thread bundle called a native/JSB method on an analytics-logging SDK (something like `bridgeLogger.app.sendLog(...)`), but on the main thread that SDK module was aliased to an **empty stub** (the main thread does not support that native capability, so the code should never have been bundled into it). The call resolved to `undefined` → `not a function`. The failing call originated from a top-level `analyticsLogger.start()` in a component's render path. + +Fix: guard the background-only call so it is not bundled/executed on the main thread: + +```tsx +// before +analyticsLogger.start(); + +// after +if (__BACKGROUND__) { + analyticsLogger.start(); +} +``` + +`__BACKGROUND__` is true only on the background thread, so the main thread no longer pulls in the unsupported native path. Once the main thread renders its `SnapshotInstance` tree correctly, the `ctx not found` error is gone. (`__JS__` is the deprecated alias of `__BACKGROUND__`.) + +> General rule: code that depends on background-only capabilities (JSB, logging/analytics SDKs, native modules) must be gated (`__BACKGROUND__`, `'background only'`, etc.) so it is not compiled into the main-thread bundle. See the `reactlynx-best-practices` skill's background-only guidance. + +## 5. When you cannot find a root cause + +If both threads look clean and the analysis of `OnLifecycleEvent` / `rLynxChange` (see `debug-instrumentation.md`) still does not explain the mismatch, it may be a framework bug or an unhandled edge case. Open an issue on [lynx-family/lynx-stack](https://github.com/lynx-family/lynx-stack/issues) with a minimal reproduction, the full dual-thread log, and the decoded snapshotPatch. diff --git a/packages/skills/reactlynx-snapshot-debugging/references/error-ctx-not-found.md b/packages/skills/reactlynx-snapshot-debugging/references/error-ctx-not-found.md new file mode 100644 index 0000000..df1f3c4 --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/references/error-ctx-not-found.md @@ -0,0 +1,45 @@ +# Error: `snapshotPatchApply failed: ctx not found` + +Full message shape: + +``` +snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_d9459_61989_1' +``` + +- **Where it throws**: inside `snapshotPatchApply`, reachable from the `InsertBefore`, `RemoveChild`, `SetAttribute`, and `SetAttributes` branches — [snapshotPatchApply.ts#L51](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts#L51). +- **Root cause**: while applying a patch from the background thread, the runtime cannot find the `SnapshotInstance` for the id the patch references, so there is no `ctx`. This almost always means the main-thread and background-thread `SnapshotInstance` trees are **out of sync** — usually caused by an *earlier* error, occasionally a framework bug. + +## Playbook + +1. **Rule out an upstream crash first** (`diagnosis-workflow.md`). This error is the tail end of a chain far more often than it is the primary fault. +2. Capture the dual-thread logs (`debug-instrumentation.md`): enable `REACT_ALOG=true`, then dump both threads via `lynx-devtool` and reload to catch first-screen hydrate. +3. Inspect the `SnapshotInstance tree for first screen hydration`. If it only contains `root` (`{"id":-1,"type":"root"}`), the main thread crashed before creating its snapshot definitions — find and fix that main-thread error. +4. Inspect the `rLynxChange` patch and search it for the `type` in the message (e.g. `__snapshot_d9459_61989_1`). If that patch tries to insert *into* that snapshot but never `CreateElement`-d it (because hydrate already emitted — and then aborted on — that create), the main thread has nothing to attach to → `ctx not found`. + +## Worked example (the guide's case) + +Observed, top to bottom: + +1. Main thread (`lepus.js`): `not a function`. +2. Background thread: `cannot read property '0' of undefined`, stack `helper -> hydrate -> onLifecycleEventImpl`. +3. Main thread: `snapshotPatchApply failed: ctx not found, snapshot type: '__Card__:__snapshot_d9459_61989_1'`. + +Two obvious red flags: a main-thread `not a function` (very likely to break things downstream), and a **throw inside hydrate** (which stops `snapshotPatchApply` from running correctly for everything after). + +Why `ctx not found` specifically: the background hydrate *should* have produced a `CreateElement __snapshot_d9459_61989_1` operation and sent it to the main thread. But hydrate threw partway, so that `CreateElement` was never delivered. The background then sent a follow-up `rLynxChange` patch that assumes `__snapshot_d9459_61989_1` already exists and tries to insert into it — the main thread has no such element, so it throws `ctx not found`. (Confirmed by decoding that follow-up patch: the reported `type` does not appear as a `CreateElement` in it; the patch is built *on top of* a hydrate result that never landed.) + +Breakpointing the hydrate throw showed the underlying framework-level trigger: the background `BackgroundSnapshotInstance` had `values` but the main-thread `SnapshotInstance` did not, so hydrate indexed into a missing `values` entry (`before.values[index]` → `after.key` on `undefined`). RL3 did not tolerate that shape — but that mismatch itself came from the main thread rendering an empty tree due to its own crash. + +**Root cause & fix**: the main-thread `not a function` came from a native/JSB call on a background-only SDK (e.g. `bridgeLogger.app.sendLog(...)`), because the main thread had that SDK module aliased to an empty stub (the main thread does not support that native capability; that code should not be in the main-thread bundle). It was pulled in by a top-level `analyticsLogger.start()` in a component's render path. Gating it to the background thread fixes the whole chain: + +```tsx +if (__BACKGROUND__) { + analyticsLogger.start(); +} +``` + +Once the main thread renders its `SnapshotInstance` tree correctly, hydrate matches, and `ctx not found` disappears. + +## Takeaway + +`ctx not found` is a **synchronization** failure, not usually a bug at the throw site. Find the first error (most often a main-thread crash that leaves the tree empty, or a background hydrate throw) and fix that. Only if both threads are clean and the tree/patch analysis still shows an unhandled shape should you treat it as a framework issue and open an upstream issue with a minimal reproduction. diff --git a/packages/skills/reactlynx-snapshot-debugging/references/error-snapshot-not-found.md b/packages/skills/reactlynx-snapshot-debugging/references/error-snapshot-not-found.md new file mode 100644 index 0000000..4e25a29 --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/references/error-snapshot-not-found.md @@ -0,0 +1,29 @@ +# Error: `Snapshot not found: __Card__:__snapshot_...` + +Full message shape: + +``` +Error: Snapshot not found: __Card__:__snapshot_d6204_98358_1 +``` + +- **Where it throws**: the `SnapshotInstance` constructor — [snapshot.ts#L293](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/snapshot.ts#L293). +- **Root cause**: the runtime is asked to create a `SnapshotInstance` of type `__snapshot_d6204_98358_1`, but there is **no registered Snapshot definition** for that type. Distinct from `ctx not found`: there the instance is missing; here the *definition* itself is missing. + +## Investigation directions + +1. **Is there another main-thread error?** A main-thread crash that terminates the bundle early prevents later `createSnapshot(...)` calls from running, so their definitions are absent. Fix that first (`diagnosis-workflow.md`). +2. **Does `__snapshot_d6204_98358_1` exist in the main-thread output?** Grep the built `lepus.js` / `main-thread.js` for the exact type string. + - **Absent** → this is a **compile/bundling problem**. It frequently reproduces **only in production** (dev and prod differ in how snapshots are emitted). Check your build config, minifier/tree-shaking, and any code that strips or renames snapshot definitions. + - **Present** → the definition exists but was not registered at runtime when needed; move to step 3. +3. **Analyze `OnLifecycleEvent` and `rLynxChange`** by capturing dual-thread logs (`debug-instrumentation.md`: `REACT_ALOG=true` + `lynx-devtool` dump). Trace which step asks for the missing type and what the tree looked like just before, to find where the definition should have been registered but was not. +4. **Complex / unresolved** → open an issue on [lynx-family/lynx-stack](https://github.com/lynx-family/lynx-stack/issues) with a minimal reproduction, the dual-thread log, and the decoded patch. + +## Contrast with `ctx not found` + +| | `Snapshot not found` | `ctx not found` | +| --- | --- | --- | +| Throws in | `SnapshotInstance` constructor | `snapshotPatchApply` | +| Missing thing | the Snapshot **definition** (type not registered) | the Snapshot **instance** (id has no ctx) | +| Typical cause | main-thread early crash, or compile/bundling (prod-only) | trees out of sync after an upstream crash/hydrate throw | + +Both start the same way: **rule out other errors, confirm the type/id exists in the main-thread output, then analyze the dual-thread traffic.** diff --git a/packages/skills/reactlynx-snapshot-debugging/references/snapshot-patch-format.md b/packages/skills/reactlynx-snapshot-debugging/references/snapshot-patch-format.md new file mode 100644 index 0000000..222ecb7 --- /dev/null +++ b/packages/skills/reactlynx-snapshot-debugging/references/snapshot-patch-format.md @@ -0,0 +1,47 @@ +# Decoding a snapshotPatch + +A `snapshotPatch` is a **flat array** where each operation is an opcode followed by its positional arguments. It is compact but unreadable by hand, e.g.: + +```ts +[ + 0, "__Card__:__snapshot_fffe1_test_3", 2, + 0, "__Card__:__snapshot_fffe1_test_4", 7, + 1, 2, 7, null, + 1, -1, 2, null, +] +``` + +## Opcode table + +Source of truth: [snapshotPatch.ts](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts#L22). + +| Opcode | Operation | Params (in order) | +| --- | --- | --- | +| `0` | `CreateElement` | `type` (string), `id` (number) | +| `1` | `InsertBefore` | `parentId`, `childId`, `beforeId` (number \| undefined) | +| `2` | `RemoveChild` | `parentId`, `childId` | +| `3` | `SetAttribute` | `id`, `dynamicPartIndex`, `value` (any) | +| `4` | `SetAttributes` | `id`, `values` (any) | +| `100` | `DEV_ONLY_AddSnapshot` | `uniqID`, `create`, `update`, `slot`, `cssId?`, `entryName?` | +| `101` | `DEV_ONLY_RegisterWorklet` | `hash`, `fnStr` | + +To decode: read one opcode, consume exactly its parameter count, repeat. Example — `0, "__Card__:__snapshot_fffe1_test_3", 2` means `CreateElement(type="__Card__:__snapshot_fffe1_test_3", id=2)`. + +## Pretty-printer + +The `debug-instrumentation.md` snippet already pretty-prints `rLynxChange` patches. Standalone, the same helper (mirrors [`prettyFormatSnapshotPatch`](https://github.com/lynx-family/lynx-stack/blob/e9e7c093db36fbae51dc964afc1de4600a8b44a1/packages/react/runtime/src/debug/formatPatch.ts#L8)) turns the array above into: + +```json +[ + { "op": "CreateElement", "type": "__Card__:__snapshot_fffe1_test_3", "id": 2 }, + { "op": "CreateElement", "type": "__Card__:__snapshot_fffe1_test_4", "id": 7 }, + { "op": "InsertBefore", "parentId": 2, "childId": 7, "beforeId": null }, + { "op": "InsertBefore", "parentId": -1, "childId": 2, "beforeId": null } +] +``` + +`parentId: -1` is the `root`. So this patch creates two elements and mounts element `2` under root and element `7` under element `2`. + +## Using it to explain an error + +When you see `snapshotPatchApply failed: ctx not found, snapshot type: 'X'`, decode the failing patch and look for the `id` the failing op references (`InsertBefore.parentId`, `RemoveChild.parentId`, `SetAttribute.id`, ...). If there is **no earlier `CreateElement` for that id** in the applied history, the element the patch expects is missing — which is exactly what happens when hydrate aborted before emitting that `CreateElement`. See `error-ctx-not-found.md`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96fe2cc..00f1002 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@lynx-js/skill-reactlynx-best-practices': specifier: workspace:* version: link:packages/skills/reactlynx-best-practices + '@lynx-js/skill-reactlynx-snapshot-debugging': + specifier: workspace:* + version: link:packages/skills/reactlynx-snapshot-debugging '@lynx-js/skill-rspeedy-bundle-size': specifier: workspace:* version: link:packages/skills/rspeedy-bundle-size @@ -219,6 +222,9 @@ importers: '@lynx-js/skill-reactlynx-best-practices': specifier: workspace:* version: link:../../skills/reactlynx-best-practices + '@lynx-js/skill-reactlynx-snapshot-debugging': + specifier: workspace:* + version: link:../../skills/reactlynx-snapshot-debugging devDependencies: build-plugin: specifier: workspace:* @@ -295,6 +301,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/skills/reactlynx-snapshot-debugging: {} + packages/skills/rspeedy-bundle-size: {} packages/skills/vanilla-lynx: {} @@ -2204,6 +2212,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}