Skip to content

Commit 513db15

Browse files
kriszypclaude
andcommitted
fix: copy-on-mutate frozen records in the save and source-resolve write paths
With typed structures disabled (storage.randomAccessFields off), decoded records are classic-encoded and frozen by freezeData. Several write paths stamped created/updated times and the primary key onto the record in place, which throws ("Cannot assign to read only property") when the record is frozen — e.g. when a record decoded during transaction-log replay is re-saved. This surfaced broadly in integration tests under the new default-off. Records are intentionally immutable (5.2 record caching relies on it), so the fix is copy-on-mutate: shallow-copy a frozen record before stamping it — in the table save validate callback and the source/caching resolve write. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 65fad22 commit 513db15

1 file changed

Lines changed: 10 additions & 0 deletions

File tree

resources/Table.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,12 @@ export function makeTable(options) {
16661666
if (fullUpdate || (recordUpdate && hasChanges(this.#changes === recordUpdate ? this : recordUpdate))) {
16671667
if (!(context as any)?.source) {
16681668
transaction.checkOverloaded();
1669+
// Records are intentionally immutable: decoded records are frozen (and 5.2 record
1670+
// caching relies on it), so mutating in place would corrupt cached/shared state.
1671+
// validate() coerces values and we stamp created/updated times + the primary key
1672+
// below, so copy-on-mutate when recordUpdate is frozen (e.g. a record decoded during
1673+
// log replay) instead of writing through the frozen object.
1674+
if (Object.isFrozen(recordUpdate)) recordUpdate = { ...recordUpdate };
16691675
this.validate(recordUpdate, !fullUpdate);
16701676
if (updatedTimeProperty) {
16711677
recordUpdate[updatedTimeProperty.name] =
@@ -4382,6 +4388,10 @@ export function makeTable(options) {
43824388
}
43834389
}
43844390
if (typeof updatedRecord.toJSON === 'function') updatedRecord = updatedRecord.toJSON();
4391+
// updatedRecord may still be a frozen record (e.g. a reused existingRecord); copy-on-mutate
4392+
// before stamping the primary key and created/updated times below (records are immutable —
4393+
// 5.2 record caching relies on it — so we must not write through the frozen object).
4394+
if (Object.isFrozen(updatedRecord)) updatedRecord = { ...updatedRecord };
43854395
if (primaryKey && updatedRecord[primaryKey] !== id) updatedRecord[primaryKey] = id;
43864396
}
43874397
resolved = true;

0 commit comments

Comments
 (0)