Skip to content

fix: copy Set drafts of an outer create() call and warn about create() on a draft - #186

Merged
unadlib merged 5 commits into
mainfrom
fix/create-on-draft-warning
Oct 6, 2026
Merged

unadlib merged 5 commits into
mainfrom
fix/create-on-draft-warning

Conversation

@unadlib

@unadlib unadlib commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Refs #160. Part of #168.

Summary

#160 reports that a helper calling create() on a draft returns a value whose unchanged values are objects of the base state, so writing to them after the result is assigned back changes the base state. Immer avoids it because its nested produce returns drafts of the outer call, and it freezes results by default. Aligning Mutative with that (#185) changes what nested results hold everywhere, across auto-freeze, patches, strict mode, Sets, create(base) drafts and async recipes, and each review of that change found new regressions against main, for a pattern that one issue reports. This PR takes the other outcome that #160 lists as acceptable: it documents the behavior and warns about it in development builds. It also fixes a Set copy error found along the way.

Changes, one commit per item

  1. fix: copy Set drafts whose original is a draft of an outer create() call. Copying a Set draft whose original is a Set draft of an outer call, as in create({ set: draft.set }, …), called Set.prototype.difference on the outer draft, which has no Set internals, so any change or iteration threw TypeError: Method Set.prototype.difference called on incompatible receiver (main and 1.3.0). It now copies through the draft's values(). The new Set test throws on main; a Map test covers the same path for Maps, which already worked.
  2. fix: warn once in development builds that create() on a draft drafts a copy of it. create(draft, recipe) still drafts current(draft). Development builds now warn once per process when create() receives a draft with a recipe, with a link to the docs section of item 3; production builds are unchanged.
  3. docs: describe create() on a draft and the objects of the base state that its result holds. README and create.md: a "create() on a draft" section with the Nested create() on draft element shares references with original base object #160 helper pattern, why writing to the result's unchanged values changes the base state, the safe places to make such changes (in the helper's recipe, or through the outer draft before calling it), and the development warning.
  4. build: refresh the size baseline for the development warning of create() on a draft. The development artifacts carry the warning text; the baseline names the commit of item 2, and the size-limit caps are unchanged.
  5. fix: link the warning of create() on a draft to the docs instead of the issue. The code keeps no issue links: the warning points to the docs section, and the comment and test follow. The development artifacts change by a few bytes, within the size budget.

Verification

  • The full Node CI sequence passes locally: lint, format, build, the benchmark checks, test:benchmarks, size, test:package, test:build-watch, type-check and test (4,659 tests). Every commit passes lint, format, type checks and the suite on its own, and coverage of src stays at 100%.
  • A regression fuzzer of nested create() calls, not committed: 9,000 random programs over synchronous recipes, async recipes before and after an await, and create(base) drafts as the outer layer, with strict mode, auto-freeze and patches on either side. Helpers change their draft, return a value or call create() again on it, and their results are assigned back, elsewhere or discarded; create() also receives plain objects holding drafts, and create(base) drafts are placed in the state. The reference build is main before this PR (6b08f57), not npm 1.3.0 as an earlier version of this description said: that checkout's package.json still says 1.3.0. In a re-run on the merged code, none of the 6,674 programs that the reference runs cleanly fails or leaves a revoked draft here. Against npm 1.3.0 itself, 14 of the 6,319 programs that it runs cleanly differ here, the same 14 as on main before this PR, all of them create() calls that receive a plain object holding drafts: 8 leave a revoked draft in the state, 5 in nested patch values, and 1 changes the base state.

Size

Measurement main This PR Δ
Production CJS artifact (Brotli) 8,106 8,114 +8
Development CJS artifact (Brotli) 14,300 14,436 +136

size-limit measures 8.3, 7.27 and 8.16 kB against the unchanged caps of 8.4, 7.4 and 8.3 kB.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Coverage after merging fix/create-on-draft-warning into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   apply.ts100%100%100%100%
   array.ts100%100%100%100%
   constant.ts100%100%100%100%
   create.ts100%100%100%100%
   current.ts100%100%100%100%
   draft.ts100%100%100%100%
   draftify.ts100%100%100%100%
   error.ts100%100%100%100%
   index.ts100%100%100%100%
   interface.ts100%100%100%100%
   internal.ts100%100%100%100%
   makeCreator.ts100%100%100%100%
   map.ts100%100%100%100%
   original.ts100%100%100%100%
   patch.ts100%100%100%100%
   rawReturn.ts100%100%100%100%
   set.ts100%100%100%100%
   unsafe.ts100%100%100%100%
src/utils
   cast.ts100%100%100%100%
   copy.ts100%100%100%100%
   deepFreeze.ts100%100%100%100%
   draft.ts100%100%100%100%
   finalize.ts100%100%100%100%
   forEach.ts100%100%100%100%
   index.ts100%100%100%100%
   mark.ts100%100%100%100%
   marker.ts100%100%100%100%
   proto.ts100%100%100%100%

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Coverage after merging fix/create-on-draft-warning into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   apply.ts100%100%100%100%
   array.ts100%100%100%100%
   constant.ts100%100%100%100%
   create.ts100%100%100%100%
   current.ts100%100%100%100%
   draft.ts100%100%100%100%
   draftify.ts100%100%100%100%
   error.ts100%100%100%100%
   index.ts100%100%100%100%
   interface.ts100%100%100%100%
   internal.ts100%100%100%100%
   makeCreator.ts100%100%100%100%
   map.ts100%100%100%100%
   original.ts100%100%100%100%
   patch.ts100%100%100%100%
   rawReturn.ts100%100%100%100%
   set.ts100%100%100%100%
   unsafe.ts100%100%100%100%
src/utils
   cast.ts100%100%100%100%
   copy.ts100%100%100%100%
   deepFreeze.ts100%100%100%100%
   draft.ts100%100%100%100%
   finalize.ts100%100%100%100%
   forEach.ts100%100%100%100%
   index.ts100%100%100%100%
   mark.ts100%100%100%100%
   marker.ts100%100%100%100%
   proto.ts100%100%100%100%

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