Skip to content

ORSet element identity is fragile for Date / BigInt / Map values #57

@pathosDev

Description

@pathosDev

`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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: lowNice-to-have / niche / demand-driven

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions