Skip to content

fix: keep a design this build cannot open instead of erasing it - #54

Merged
xevrion merged 1 commit into
xevrion:mainfrom
yashksaini-coder:fix/saved-designs-survive-rejection
Sep 5, 2026
Merged

fix: keep a design this build cannot open instead of erasing it#54
xevrion merged 1 commit into
xevrion:mainfrom
yashksaini-coder:fix/saved-designs-survive-rejection

Conversation

@yashksaini-coder

Copy link
Copy Markdown
Contributor

Closes #53.

Every mutation on the shelf is read, change, write, and the read drops rows that fail to parse.
That is right for display and wrong for storage: a row dropped on the way in was a row deleted on
the way out. Saving an unrelated design erased someone else's, permanently, and so did renaming or
deleting a different one. Deleting one row of two wiped both.

Measured before, two rows with the first unopenable:

raw storage                    [ "Week 3 coursework", "Fine one" ]
saveDesign("Something new")    [ "Something new", "Fine one" ]
deleteDesign("b")              [ ]

After, the unopenable row is still there in both cases, byte for byte.

What changed

readShelf returns what parsed and what did not, and saveDesign, deleteDesign and
renameDesign carry the second half back into storage untouched. parseEntry is unchanged and
still does one job; loadDesigns keeps its signature and is now readShelf().designs.

The rows come back identical rather than normalised, which is the point of keeping them. A row
this build cannot open is not necessarily a row that is gone: it may be a newer format, or a
design tripped by a bug a later build fixes. That is the case for the duplicate-id designs in #46,
where the bug costs the design and this one made the loss permanent.

The unreadable half is capped at MAX_SAVED, for the same reason the readable half is. Rows
nobody can open must not grow without limit in a shared 5MB budget, and past the cap the oldest
bytes go, which is the trade the readable half already makes.

What I did not change

The shelf still does not show these rows, because there is nothing useful to render for one. It
is worth deciding separately whether a row that says it cannot be opened would be better than a
row that is simply absent, since silence is what made this invisible in the first place. That is a
copy and design question rather than a storage one, so it is not in here.

Tests

savedDesigns.rejected.test.ts, nine cases. The fixture is a topology with duplicate node ids,
which is the shape a palette-add produces after a reload, so this is the failure that actually
happens rather than hand-corrupted JSON.

It pins that such a row stays off the shelf, survives a save, a rename, a delete and a
save-over-by-name, and comes back identical; that the readable half still saves, renames, deletes
and evicts at the cap as before; and that the unreadable half stays bounded.

Seven of the nine fail when the old read is put back. The two that pass are the two that should:
the row is still hidden, and the readable half still behaves. Checked by reverting.

904 tests pass. typecheck, lint and format:check clean, lint on the same 26 warnings as main.

Every mutation on the shelf is read, change, write, and the read drops rows
that fail to parse. That is right for display and wrong for storage: a row
dropped on the way in was a row deleted on the way out. Saving an unrelated
design erased someone else's, permanently, with nothing said, and so did
renaming or deleting a different one. Deleting one row of two wiped both.

readShelf now returns what parsed and what did not, and the three mutations
carry the second half back into storage untouched. parseEntry is unchanged
and still does one job.

A row this build cannot open is not necessarily a row that is gone. It may
be a newer format, or a design tripped by a bug a later build fixes, so
keeping the bytes leaves that recovery possible where rewriting the shelf
without them does not. That matters most for the duplicate-id designs in
issue xevrion#46: that bug costs the design, this one made the loss permanent.

The unreadable half is capped at MAX_SAVED for the same reason the readable
half is. localStorage is a shared 5MB budget, and rows nobody can open must
not grow without limit.

Measured before the fix, two rows on the shelf with the first unopenable:

  saveDesign("Something new")  ->  [ "Something new", "Fine one" ]
  deleteDesign("b")            ->  [ ]

and after it, the unopenable row is still there byte for byte in both cases.

savedDesigns.rejected.test.ts pins it: that such a row stays off the shelf
because it cannot be shown, that it survives a save, a rename, a delete and
a save-over-by-name, that it comes back identical, and that the readable
half still saves, renames, deletes and evicts at the cap exactly as before.
Seven of the nine fail when the old read is put back.

Closes xevrion#53.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@yashksaini-coder is attempting to deploy a commit to the whoarrryou's projects Team on Vercel.

A member of the Team first needs to authorize it.

@xevrion
xevrion merged commit e0241b0 into xevrion:main Sep 5, 2026
6 of 7 checks passed
@xevrion

xevrion commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Merged. Keeping the bytes rather than normalising them is the right call, and the duplicate-id fixture is a good choice given #46. Thanks!

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.

A saved design the validator rejects is erased from storage by the next save or delete

2 participants