Version 1.5.2 - #85
Merged
Merged
Conversation
…nd-cell-guards Bugfix/derive cell overload fix and cell guards
…e/remove List.set()/deriveList reused a keyConfig'd item's key across a content change at a shared index instead of retiring it, so a key could silently end up pointing at unrelated content. createCollection's applyChanges() had a related but separate issue: change/remove entries it could not resolve to a key without a content-based keyConfig were silently dropped instead of surfacing an error. diffPositional() now mints a fresh key on content mismatch when a keyConfig is given (matching splice()'s existing semantics), while the no-keyConfig positional case is unchanged. createCollection now throws UnresolvableKeyError for a change/remove entry it cannot resolve, instead of silently no-oping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pin was stuck at 1.4.1 while latest published is 1.5.1, adding three minor releases of accumulated drift to every measured ratio. The list-key-reuse fix on this branch only touches list.ts/collection.ts and cannot affect signalCreation, confirming the CI failure was drift, not a real regression.
Bugfix/list key reuse
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Added
Cell<T>/MutableCell<T>types,isCell(x)/isMutableCell(x)guards: The 1.x bridge for ADR-0018 §8's shape-indexedCelltype.Cell<T> = State<T> | Memo<T> | Task<T> | Sensor<T>— a genuine structural narrowing ofSignal<T>, not just a rename: each origin already carries a distinctSymbol.toStringTagliteral ('State' | 'Memo' | 'Task' | 'Sensor'), so the union excludesList<T>/Store<T>/Collection<T>at the type level with no runtime tag change.MutableCell<T> = State<T>, an alias matchingcreateCell's existing return value.isCell/isMutableCellcheckSymbol.toStringTagmembership;isSignal/isMutableSignalkeep their unchanged umbrella meaning.deriveCell's overloads now returnCell<T>instead of the widerSignal<T>, andcreateCellnow returnsMutableCell<T>instead ofState<T>— both widening-safe, since everyCell/MutableCellvalue already satisfiesSignal/MutableSignalstructurally, so no existing caller's code breaks.UnresolvableKeyError: New error class, exported from the package root. Thrown bycreateCollection's (and external-pushderiveList's)applyChanges({ change, remove })when an entry cannot be matched to an existing key. See theFixedentry below.Fixed
deriveCell(input, options?)mis-inferred a zero/single-arg async callback's return type asPromise<T>(src/nodes/cell.ts, formerlysrc/signal.ts): The overloads declared the syncMemoCallback<T>form before the asyncTaskCallback<T>form. A zero/single-argasync () => Tcallback is structurally assignable toMemoCallback<T>too (fewer parameters is always fine), and TypeScript's overload resolution picks the first structural match — soTunified toPromise<...>instead of the resolved value type. For example,deriveCell(async () => new Map<string, number>())inferredSignal<Promise<Map<string, number>>>instead ofSignal<Map<string, number>>. The deprecatedcreateComputedalready orderedTaskCallbackbeforeMemoCallbackto avoid exactly this;deriveCell's overloads are now reordered to match. Type-inference-only fix — no runtime behavior change.src/nodes/list.ts,src/nodes/collection.ts):MutableList.deriveCollection(),DerivedList.deriveCollection(), the deprecated free functionderiveCollection(), andderiveList()'s per-item overloads (the current v2.0-facing API) all declared a single-arg sync callback(sourceValue: T) => Rbefore the two-arg async callback(sourceValue: T, abort: AbortSignal) => Promise<R>. SinceRis unconstrained, a single-arg async callback that ignoresabort— a common shape — structurally matched the sync overload first, unifyingRtoPromise<X>instead ofX. For example,deriveList(source, async (item) => ({ value: item.id }))inferredDerivedList<Promise<{ value: string }>>instead ofDerivedList<{ value: string }>. Each pair is now reordered so the async overload comes first, matching thederiveCellfix above.deriveList's whole-array overloads andderiveStorewere not affected — their sync-form return type is a concreteT[]/UnknownRecordshape, which already blocks the bad unification. Type-inference-only fix — no runtime behavior change.List.set()/deriveListreused akeyConfig'd item's key across a content change instead of retiring it (src/nodes/list.ts,src/nodes/collection.ts): Previously,diffPositional()— used byMutableList.set()and, throughkeyedAdapter'sensureKeys(), byderiveListderiving from a plainSignal<T[]>— reusedprevKeys[i]at any shared index whereitemEqualsfailed, regardless of whether akeyConfigwas configured, emitting achangeunder the old key instead of aremove+add. A key could therefore end up silently pointing at unrelated content with no structural event ever firing, undermining a consumer that keys external resources — DOM nodes, caches — by list key. Now, with akeyConfig(string prefix or function), a content mismatch at a shared index retires the old key and mints a fresh one viagenerateKey(), matchingsplice()'s existing semantics. The no-keyConfigcase is unchanged by design: array position stays the identity, so the key at each index stays the same regardless of content.createCollection'sapplyChanges({ change, remove })silently dropped an entry it could not resolve to a key (src/nodes/collection.ts): Previously, without a content-based (function)keyConfig,resolveKey()could only match achange/removeentry by object reference — unworkable for externally-sourced data such as parsed JSON, which is rarely reference-equal across messages — so an unresolvable entry silently no-op'd (if (!key) continue), with no error and no diagnostic. A content-basedkeyConfigwas and remains unaffected: a genuinely nonexistent key still no-ops gracefully, the same asList.remove()on a nonexistent key. Now an unresolvable entry throwsUnresolvableKeyErrorinstead.onChanges()'schangeandremoveloops resolve keys for the whole batch before mutating anything, mirroring the existingadd-loop staging, so a batch containing an unresolvable entry throws before any of it is applied.