`GSet` and `ORSet` use `JSON.stringify(element)` as the element-identity key. This works for plain-JSON values (numbers, strings, plain objects, arrays) but degrades for types that JSON doesn't roundtrip cleanly:
- `Date` — `JSON.stringify(new Date())` produces an ISO string. Two `Date` instances representing the same instant deduplicate correctly (good), BUT comparing the round-tripped value via `new Date(str)` doesn't equal the original via referential / typeof check.
- `BigInt` — `JSON.stringify` THROWS `TypeError: Do not know how to serialize a BigInt`. An `add(replicaId, 42n)` call propagates the throw to the user's mailbox.
- `Map` / `Set` — `JSON.stringify(new Map([[ 'a', 1 ]]))` returns `"{}"` because Map isn't enumerable as own properties. Different Maps deduplicate identically (wrong).
Impact: users who try to put non-trivial values into a `GSet` / `ORSet` get either a throw (BigInt) or silent over-deduplication (Map / Set).
Fix options:
A. Document the limitation in JSDoc + README, and stop there. Lowest cost, ships now.
B. Allow custom identity via a constructor option:
```ts
const set = ORSet.empty({ identity: (e) => e.id });
```
Each call to `add / remove / has` uses `identity(e)` instead of `JSON.stringify(e)`. Backwards-compatible default falls back to `JSON.stringify`.
C. Custom `toJSON` hook on the element type — implementations get full control over how their values serialize. Requires users to implement the hook for each value type they put in a CRDT.
Recommendation: A (docs) + B (custom identity option) together. C alone is more invasive than the value justifies.
Components:
| File |
Task |
| `src/crdt/GSet.ts`, `src/crdt/ORSet.ts` |
Optional `identity` callback in factory; JSDoc on the JSON-shape limitation. |
| `tests/unit/crdt/CrdtProperties.test.ts` |
New cases: ORSet with custom identity, BigInt-rejection on default identity. |
Estimate: 1 day.
Verification:
- New test: `ORSet.empty({ identity: (e) => e.sku })` deduplicates correctly across structurally-different but logically-same items.
- Doc: a clear "use `identity` for non-JSON values" example in the ORSet JSDoc.
`GSet` and `ORSet` use `JSON.stringify(element)` as the element-identity key. This works for plain-JSON values (numbers, strings, plain objects, arrays) but degrades for types that JSON doesn't roundtrip cleanly:
Impact: users who try to put non-trivial values into a `GSet` / `ORSet` get either a throw (BigInt) or silent over-deduplication (Map / Set).
Fix options:
A. Document the limitation in JSDoc + README, and stop there. Lowest cost, ships now.
B. Allow custom identity via a constructor option:
```ts
const set = ORSet.empty({ identity: (e) => e.id });
```
Each call to `add / remove / has` uses `identity(e)` instead of `JSON.stringify(e)`. Backwards-compatible default falls back to `JSON.stringify`.
C. Custom `toJSON` hook on the element type — implementations get full control over how their values serialize. Requires users to implement the hook for each value type they put in a CRDT.
Recommendation: A (docs) + B (custom identity option) together. C alone is more invasive than the value justifies.
Components:
Estimate: 1 day.
Verification: