(op: EditOp, origin: unknown)
| 387 | |
| 388 | // fallow-ignore-next-line complexity |
| 389 | private _dispatch(op: EditOp, origin: unknown): MutationResult { |
| 390 | const result = applyOp(this.parsed, op); |
| 391 | const { forward, inverse } = result; |
| 392 | |
| 393 | if (forward.length === 0 && inverse.length === 0) { |
| 394 | if (this.batchDepth === 0) this.changeHandlers.forEach((h) => h()); |
| 395 | return result; |
| 396 | } |
| 397 | |
| 398 | this.elementsCache = null; |
| 399 | |
| 400 | // Update override-set from forward patches |
| 401 | for (const p of forward) { |
| 402 | const key = pathToKey(p.path); |
| 403 | if (key !== null) { |
| 404 | this.overrides[key] = |
| 405 | p.op === "remove" |
| 406 | ? null |
| 407 | : (p.value as string | number | boolean | Record<string, unknown> | null); |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | // Purge orphan property keys for removed elements so the override-set stays |
| 412 | // compact and a future T3 session doesn't replay stale properties onto a |
| 413 | // non-existent element. Override-set keys use decoded scoped ids ("hf-host/hf-leaf") |
| 414 | // while path segments use RFC 6902 encoding ("hf-host~1hf-leaf") — decode before compare. |
| 415 | for (const p of forward) { |
| 416 | const elemMatch = /^\/elements\/([^/]+)$/.exec(p.path); |
| 417 | if (p.op === "remove" && elemMatch) { |
| 418 | // Decode RFC 6902 escaping: ~1 → /, ~0 → ~ |
| 419 | const id = elemMatch[1]!.replace(/~1/g, "/").replace(/~0/g, "~"); |
| 420 | for (const key of Object.keys(this.overrides)) { |
| 421 | // Purge property sub-keys (e.g. "hf-x.style.color") but preserve |
| 422 | // the removal marker itself (key === id, set to null in the loop above). |
| 423 | if (key.startsWith(`${id}.`) || key.startsWith(`${id}/`)) { |
| 424 | delete this.overrides[key]; |
| 425 | } |
| 426 | } |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | if (this.batchDepth > 0) { |
| 431 | this.batchForward.push(...forward); |
| 432 | this.batchInverse.push(...inverse); |
| 433 | if (!this.batchOpTypes.includes(op.type)) this.batchOpTypes.push(op.type); |
| 434 | } else { |
| 435 | // Reverse the inverse list (parity with batch() below): an op that emits |
| 436 | // multiple patches whose undo order matters — same path (reorderElements |
| 437 | // with a duplicate target), an aliased multi-target, or a nested |
| 438 | // parent+child removeElement — must undo in reverse application order, or |
| 439 | // undo lands on an intermediate value / drops a subtree. Harmless for the |
| 440 | // common single-patch / independent-path case. |
| 441 | const event = buildPatchEvent(forward, [...inverse].reverse(), origin, [op.type]); |
| 442 | this.patchHandlers.forEach((h) => h(event)); |
| 443 | this.changeHandlers.forEach((h) => h()); |
| 444 | } |
| 445 | |
| 446 | return result; |
no test coverage detected