Pin down singular linksTo userland JS-access contract#5024
Conversation
Add a focused integration test exercising the five RelationshipState kinds through the singular linksTo getter — present strict-equals the linked card, every non-present state surfaces as undefined with stable optional- chain and raw-access semantics, terminal sentinels keep their bucket entry across repeated reads, and a computed deriving from a broken link resolves to undefined. Also harden gc-card-store.findInstances against the link-error and link-not-found sentinels: it previously short-circuited only on not-loaded by shape, so the new terminal sentinels would fall through and have their errorDoc and reference fields walked as a generic object graph. Route the check through the canonical isNonPresentLink predicate, exported from card-api for that purpose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prettier was reformatting against the project config (the module body sits at one level, not two), and qunit/no-ok-equality requires the contract's loose `== null` to be wrapped in an explicit eslint-disable so the strict-equality lint rule doesn't fight the test's intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR pins down the userland JavaScript contract for singular linksTo fields so all non-present sentinel states surface as undefined, and updates the host store dependency traversal to use the canonical sentinel predicate.
Changes:
- Adds an integration test covering singular
linksToaccess semantics across present, not-loaded, error, not-found, and not-set states. - Updates GC dependency discovery to skip all non-present link sentinels via
isNonPresentLink. - Re-exports
isNonPresentLinkfromcard-apifor external consumers.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
packages/host/tests/integration/components/linksto-singular-js-access-contract-test.gts |
Adds focused integration coverage for singular linksTo JS access behavior and computed-field propagation. |
packages/host/app/lib/gc-card-store.ts |
Uses the shared sentinel predicate to avoid traversing non-present link sentinel internals. |
packages/base/card-api.gts |
Re-exports isNonPresentLink from the public card API surface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 1h 48m 42s ⏱️ +10s Results for commit 0068ffd. ± Comparison against earlier commit ee7632b. Realm Server Test Results 1 files ± 0 1 suites ±0 9m 55s ⏱️ -44s Results for commit 0068ffd. ± Comparison against earlier commit ee7632b. |
LinksTo.emptyValue() returns null, so an unset declared linksTo read through the field getter surfaced as `card.link === null` rather than the `undefined` every other non-present state produces. Normalize at the singular getter return so all four non-present discriminants (not-set, not-loaded, error, not-found) collapse to the same nullish shape — the ticket contract the existing tests in this PR were already asserting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The data bucket caches the empty value (currently `null`) after the field is first read, so asserting strict-undefined on the bucket entry checks the cache shape rather than the userland contract. Replace with a sentinel-object check — userland's unified nullish surface is already covered by the earlier assertions in the same test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two serialization tests asserted `card.pet === null` for an explicitly-
null `links.self` payload. That was the legacy behavior before the
userland surface was unified — every non-present state now collapses to
`undefined` through the field getter. The `relationshipMeta` back-compat
wrapper still reports `{ type: 'loaded', card: null }` for that shape so
its existing callers stay stable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Background and Goal
The
linksTogetter for the singular relationship now recognizes three sentinel kinds in its data bucket —not-loaded,link-error, andlink-not-found— and returnsundefinedfor all three so userland sees a single nullish surface regardless of the failure mode. This PR pins down that contract with a focused integration test and audits the codebase for sentinel consumers outside the two implementation files (packages/base/card-api.gts,packages/base/field-support.ts).Where to start
packages/host/tests/integration/components/linksto-singular-js-access-contract-test.gts— one test perRelationshipStatekind, exercising strict equality, loose== null, optional-chain, and raw-access semantics, plus a computed deriving from a broken declared link.packages/host/app/lib/gc-card-store.ts— the one production-code consumer the audit turned up. ItsfindInstancesshort-circuit recognizednot-loadedby shape and would have fallen through on the new terminal sentinels, walking theirerrorDoc/referencefields as a generic object graph. Now routed through the canonicalisNonPresentLinkpredicate.packages/base/card-api.gts—isNonPresentLinkadded to the re-exports so consumers outside the base have a typed entry point rather than reaching intofield-support.tsor hand-rolling a shape match.Key decisions and non-obvious mechanics
person.pet), not viagetRelationship— the diagnostic API is covered separately and the goal here is to prove that nothing else needs to know about sentinels.assert.throws(() => (person.pet as any).firstName, /undefined/, …)is intentional: the platform must NOT wrap raw property access on an undefined link — ordinary JSTypeErroris the documented behavior.getDataBucketreads outside the two implementation files are all test scaffolding for planting sentinels. No production code path other than the one fixed ingc-card-store.tsconsumed sentinels by shape.