Skip to content

Commit f2e8106

Browse files
clentfortjamesgpearce
authored andcommitted
fix(partykit): preserve delete tombstones
Encode PartyKit messages with undefined-aware JSON and decode them without using a reviver so tombstone keys are preserved. This keeps delete changes intact across transport instead of dropping row and cell deletions during parse. Fixes: #281
1 parent 5f29b2b commit f2e8106

2 files changed

Lines changed: 44 additions & 6 deletions

File tree

src/common/json.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {object} from './obj.ts';
2-
import {isInstanceOf, isUndefined} from './other.ts';
2+
import {isArray, isInstanceOf, isUndefined} from './other.ts';
33
import {UNDEFINED} from './strings.ts';
44

55
export const jsonString = JSON.stringify;
@@ -13,5 +13,36 @@ export const jsonStringWithMap = (obj: unknown): string =>
1313
export const jsonStringWithUndefined = (obj: unknown): string =>
1414
jsonString(obj, (_key, value) => (isUndefined(value) ? UNDEFINED : value));
1515

16-
export const jsonParseWithUndefined = (str: string): any =>
17-
jsonParse(str, (_key, value) => (value === UNDEFINED ? undefined : value));
16+
const replaceUndefinedString = (obj: any): any => {
17+
if (obj === UNDEFINED) {
18+
return undefined;
19+
}
20+
if (isArray(obj)) {
21+
return obj.map(replaceUndefinedString);
22+
}
23+
if (isInstanceOf(obj, Object)) {
24+
object
25+
.entries(obj)
26+
.forEach(
27+
([key, value]) => ((obj as any)[key] = replaceUndefinedString(value)),
28+
);
29+
}
30+
return obj;
31+
};
32+
33+
export const jsonParseWithUndefined = (str: string): any => {
34+
// Do not use a JSON.parse reviver for this mapping. It removes properties
35+
// with undefined values, which is not what we want.
36+
//
37+
// That would remove tombstone keys such as {rowId: undefined} and break
38+
// delete propagation.
39+
//
40+
// > If the reviver function returns undefined (or returns no value - for
41+
// > example, if execution falls off the end of the function), the property is
42+
// > deleted from the object."
43+
// See
44+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#the_reviver_parameter
45+
// Related bug report:
46+
// https://github.com/tinyplex/tinybase/issues/281
47+
return replaceUndefinedString(jsonParse(str));
48+
};

src/persisters/common/partykit.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import {jsonParse, jsonStringWithMap} from '../../common/json.ts';
1+
import {
2+
jsonParseWithUndefined,
3+
jsonStringWithUndefined,
4+
} from '../../common/json.ts';
25
import {isString, size, slice} from '../../common/other.ts';
36
import {T, V, strStartsWith} from '../../common/strings.ts';
47

@@ -15,7 +18,9 @@ export const construct = (
1518
type: MessageType | StorageKeyType,
1619
payload: any,
1720
): string =>
18-
prefix + type + (isString(payload) ? payload : jsonStringWithMap(payload));
21+
prefix +
22+
type +
23+
(isString(payload) ? payload : jsonStringWithUndefined(payload));
1924

2025
export const deconstruct = (
2126
prefix: string,
@@ -26,7 +31,9 @@ export const deconstruct = (
2631
return strStartsWith(message, prefix)
2732
? [
2833
message[prefixSize] as MessageType | StorageKeyType,
29-
(stringified ? jsonParse : String)(slice(message, prefixSize + 1)),
34+
(stringified ? jsonParseWithUndefined : String)(
35+
slice(message, prefixSize + 1),
36+
),
3037
]
3138
: undefined;
3239
};

0 commit comments

Comments
 (0)