Repository navigation
fix(serialization): surface @computed scalars on default reads + guard cyclic @enumerable serialization (#1484) - #1601
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses issue #1484 by surfacing computed scalar attributes on default reads and introducing a cycle guard for cyclic-enumerable serialization to prevent stack overflows. The review feedback points out critical issues in the serialization guard implementation, specifically a potential global state leak on error and a crash when serializing composite primary keys containing BigInt values. It also suggests using optional chaining in test assertions to avoid masking server errors.
|
Reviewed; no blockers found. All 8 prior inline threads are resolved and verified fixed in current code (global-state-leak + BigInt crash guard, object-typed-PK cycle guard, |
…key (#1484) Address review on #1601: - Move the guarded-path state setup (Map allocation, primaryKey getter, id-key normalization) inside the try so a throw can never leak structSerializationVisited as a non-null Map for the rest of the thread's serializations (would falsely treat later top-level reads as nested). Track membership with an `added` flag. - Composite/array primary keys containing a BigInt no longer crash JSON.stringify — normalize via a bigint->string replacer. - Tests: optional chaining on response-body assertions (A1/B1/B2/B3) so a non-JSON/empty response can't mask the real server error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cyclic serialization (#1484) Address PR #1601 review (cb1kenobi): - Normalize any non-primitive primary key (object/Date/Bytes, not just arrays) to a stable string key so the id-keyed cycle guard collapses object-PK cycles instead of overflowing. - Route ANY surfaced non-table @computed through the guarded serialization path, not only Any/untyped ones: a resolver can return a live cyclic entity regardless of its declared scalar type. Renamed hasUnconstrainedComputed -> hasSurfacedComputed. - Add integration test B4 (typed String @computed returning a live entity cycle) which overflowed on the fast path before this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Code looks right, but the unit failures are this PR's: the basicREST computed-index assertion asserts the pre-fix behavior and needs updating, cache-test's 400 needs a look, and the new test imports sent with Claude Fable 5 |
…key (#1484) Address review on #1601: - Move the guarded-path state setup (Map allocation, primaryKey getter, id-key normalization) inside the try so a throw can never leak structSerializationVisited as a non-null Map for the rest of the thread's serializations (would falsely treat later top-level reads as nested). Track membership with an `added` flag. - Composite/array primary keys containing a BigInt no longer crash JSON.stringify — normalize via a bigint->string replacer. - Tests: optional chaining on response-body assertions (A1/B1/B2/B3) so a non-JSON/empty response can't mask the real server error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cyclic serialization (#1484) Address PR #1601 review (cb1kenobi): - Normalize any non-primitive primary key (object/Date/Bytes, not just arrays) to a stable string key so the id-keyed cycle guard collapses object-PK cycles instead of overflowing. - Route ANY surfaced non-table @computed through the guarded serialization path, not only Any/untyped ones: a resolver can return a live cyclic entity regardless of its declared scalar type. Renamed hasUnconstrainedComputed -> hasSurfacedComputed. - Add integration test B4 (typed String @computed returning a live entity cycle) which overflowed on the fast path before this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
367d850 to
29e7f60
Compare
|
CI was red on 3 things after the rebase — pushed fixes for the two that were real, left the third alone since it's yours to weigh in on:
Didn't touch anything else — just wanted CI green so you can review the actual diff without noise. Let me know if you want the — KrAIs (Claude Sonnet 5) |
…key (#1484) Address review on #1601: - Move the guarded-path state setup (Map allocation, primaryKey getter, id-key normalization) inside the try so a throw can never leak structSerializationVisited as a non-null Map for the rest of the thread's serializations (would falsely treat later top-level reads as nested). Track membership with an `added` flag. - Composite/array primary keys containing a BigInt no longer crash JSON.stringify — normalize via a bigint->string replacer. - Tests: optional chaining on response-body assertions (A1/B1/B2/B3) so a non-JSON/empty response can't mask the real server error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cyclic serialization (#1484) Address PR #1601 review (cb1kenobi): - Normalize any non-primitive primary key (object/Date/Bytes, not just arrays) to a stable string key so the id-keyed cycle guard collapses object-PK cycles instead of overflowing. - Route ANY surfaced non-table @computed through the guarded serialization path, not only Any/untyped ones: a resolver can return a live cyclic entity regardless of its declared scalar type. Renamed hasUnconstrainedComputed -> hasSurfacedComputed. - Add integration test B4 (typed String @computed returning a live entity cycle) which overflowed on the fast path before this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6209269 to
6294f28
Compare
…d cyclic @enumerable serialization (#1484) REST GET silently dropped @computed attributes on the default (no-select) read because their getters are non-enumerable and JSON.stringify skips inherited getters. Naively making them enumerable would (a) install the whole-prototype- chain `for..in` toJSON on every such table (3.4–32x slower, scaling with chain depth) and (b) risk unbounded recursion, since non-enumerability is the only thing preventing @enumerable relationship cycles from overflowing. Unify both concerns in the struct toJSON (installed only when a table has an @enumerable or non-table @computed attribute): - Bounded enumeration: own keys + a known name list (every @enumerable attribute plus @computed attributes whose type is not a table type), replacing the O(inherited-enumerables) `for..in`. Proven equivalent — msgpackr struct prototypes carry no enumerable inherited props. - Cycle guard: @enumerable table-typed edges form a graph; detectCyclicEnumerable (lazy, self-loop aware) flags tables that can cycle. Those get a path-scoped, id-keyed guard (Map<tableClass, Set<idKey>>) that collapses a repeated ancestor to a { [primaryKey]: id } reference stub; resolveStructForJSON fully resolves nested structs so none escape to native stringify after unwind. Acyclic tables keep a guard-free fast path. Table-typedness keys off the attribute `definition` (set early) rather than `.tableClass` (assigned later — unset for self/forward refs at collection time), resolving tableClass lazily at first serialization. Composite/array primary keys are normalized for the visited set. Tables with an unconstrained (`Any`/untyped) @computed take the guarded path, since such a resolver could return a live cyclic entity the static graph can't see. Name lists rebuild on schema reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6294f28 to
740a295
Compare
Closes #1484.
Default reads now surface non-table
@computedattributes, and cyclic@enumerablerelationships serialize repeated ancestors as primary-key reference stubs instead of overflowing. The bounded per-table serializer keeps acyclic enumerable tables on a guard-free fast path and uses a path-scoped(table, primary key)guard only when runtime cycles are possible.The guarded path preserves transport-native values: it identifies both promoted classic msgpackr records and structon random-access records through a shared
RecordObjectruntime base, recursively materializes only Harper records and ordinary containers, and leavesDate,Buffer/typed arrays, andArrayBufferintact for CBOR and MessagePack. Per-encoder record subclasses keep table-specific computed getters isolated.Reviewer attention:
resources/Table.tsowns the cycle guard and guarded materialization;resources/RecordEncoder.tsowns the shared runtime base. A computed resolver's declared scalar type cannot constrain its JS return value, so every surfaced non-table computed takes the guarded path.Coverage includes mutual/self/runtime-computed cycles, classic msgpackr and structon records, CBOR and MessagePack fidelity, and RocksDB + LMDB. No documentation change is needed: this restores declared computed-field behavior and preserves the existing binary transport contract.
Generated with Claude Opus on the original implementation and Codex on the native-serialization follow-up. Final follow-up review: Gemini 3.6 Flash via direct API found no unresolved issues; the local Claude CLI review route was unavailable (execution timeout).