Skip to content

fix(pptx): keep source bytes resolvable after state cloning#47

Merged
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/save-source-survives-clones
May 14, 2026
Merged

fix(pptx): keep source bytes resolvable after state cloning#47
karthikmudunuri merged 1 commit into
mainfrom
karthikmudunuri/save-source-survives-clones

Conversation

@karthikmudunuri
Copy link
Copy Markdown
Member

Summary

1.12.0 shipped the master / layout / theme / font / EMF / slide-bg preservation pipeline — but it was silently no-op'ing in practice. The pipeline depended on a non-enumerable __slidewiseSourcePptx attachment on the deck. structuredClone (used by snap() for history) AND every { ...deck, ... } reducer spread strip non-enumerable properties. So:

  1. User imports PPTX → parsePptx attaches source bytes (non-enumerable)
  2. User edits one text → reducer spreads { ...deck, slides } → source attachment gone
  3. Editor saves → resolveSource returns undefinedpreserveUnknowns bails immediately → pptxgenjs writes a stripped-down deck

End user sees: 28 layouts → 1, 5 embedded fonts → 0, half the slides empty. Exactly what they reported on 1.12.0.

Fix

Move the source attachment to an enumerable identifier on the deck, with bytes held in a module-level cache:

  • parsePptx stamps deck.sourcePptxId = nanoid() (enumerable, JSON-safe string).
  • A module-level Map<string, ArrayBuffer> caches source bytes keyed by that id.
  • resolveSource consults the cache when no explicit options.source is passed, falling back to the legacy non-enumerable attachment for direct callers.

structuredClone, spread, JSON.parse(JSON.stringify(deck)) all preserve sourcePptxId because it's a plain enumerable string, so the cache lookup keeps working across every reducer mutation.

The cache is in-memory only — cross-session round-trips (page reload → rehydrate from localStorage) still need the host to re-attach source bytes via serializeDeck(deck, { source }). Documented on the field's TSDoc.

Validation

New regression test in chrome-preservation.test.ts: parses eon-deck.pptx, runs the deck through structuredClone + an object spread (mirroring the store's snap() and a reducer), saves with NO explicit source, asserts 28/28 layouts and 5/5 embedded fonts survive.

End-to-end against the user's exact scenario (/tmp/edit_sim_no_source_passed.mjs):

sourcePptxId on deck: waYqwtgvbgv-
Non-enumerable SOURCE_PPTX still there? false
Enumerable sourcePptxId still there? true
Saved zip: { masters: 1, layouts: 28, themes: 3, fonts: 5, slides: 25, media: 18 }
Slides with content: 25/25

Versus the user's broken save on 1.12.0 with the same input: 1 layout / 0 fonts / many empty slides.

Test plan

  • pnpm typecheck clean
  • pnpm --filter @textcortex/slidewise test — 38/38 passing
  • End-to-end save against eon-deck-v1.pptx with no source option produces fully-preserved output

Changeset

Patch bump on @textcortex/slidewise (1.12.0 → 1.12.1) — bug fix on a regression that landed in 1.12.0; no API breaks. Deck.sourcePptxId is a new optional field, additive.

1.12.0's chrome / EMF / slide-bg preservation relied on a non-enumerable
__slidewiseSourcePptx attachment that structuredClone and { ...deck }
reducer spreads silently strip. As soon as the user edited anything the
attachment was gone, serializeDeck had no source to inject from, and
saves fell back to pptxgenjs's lossy emitter — the exact regression
1.12.0 was supposed to fix.

parsePptx now stamps an enumerable Deck.sourcePptxId and stashes the
source bytes in a module-level cache keyed by that id. resolveSource
looks them up by id when no explicit options.source is passed. The id
survives structuredClone, spread, and JSON round-trip, so any
reducer-driven host (Zustand, Redux, useState, Immer) keeps the
preservation pipeline alive across edits.

New regression test mirrors the broken scenario (parsePptx →
structuredClone + spread → serializeDeck with no source) and asserts
all 28 eon-deck layouts + 5 embedded fonts still make it into the
saved file.
@karthikmudunuri karthikmudunuri merged commit e5c7860 into main May 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant