Skip to content

Support for EJSON custom types in IndexedDB persistence (for Decimal128() data type) #26

Description

@imongithubnow

jam:offline fails to persist documents containing EJSON custom types (e.g. Decimal from mongo-decimal) because the structured clone algorithm used by IndexedDB cannot serialize class instances with methods.

In a Meteor app using mongo-decimal:

// server
import { Decimal } from 'meteor/mongo-decimal';

const fields = {
    amount: { type: Decimal, ... },
    // ...
};

Once the publish delivers a document to the client, jam:offline attempts to write it to IndexedDB. The browser throws:

DOMException: Function object could not be cloned.
  at wrapFunction jam_offline.js
  at finishPut idb.js
  at put idb.js
  at added load.js
  at added local_collection.js

The Decimal instance arrives on the client with prototype methods (plus, minus, toString, …) and an own-property constructor, which is the function itself. Structured clone refuses anything with functions as own properties.
This is not specific to mongo-decimal — it happens with any EJSON custom type registered via EJSON.addType.
For apps that rely on precise decimal arithmetic (accounting, finance, e-commerce), the workarounds are all painful:

  • Switch to Number: loses precision semantics, requires schema migration and rewriting of all .plus() / .minus() operations on the server.
  • Exclude the affected collections from .keep(): these collections are simply not available offline.
  • Manually stringify in each publish: breaks the reactive cursor pattern and requires reinventing observeChanges per publish.

None of these are good options if the user's intent is simply "persist what Meteor already knows how to serialize."

Proposed solution

Meteor already has a first-class mechanism for serializing custom types to wire-safe form: EJSON.toJSONValue / EJSON.fromJSONValue. The output is a plain object like { $type: "Decimal", $value: "14.95" } — fully clonable by structured clone.

I'd propose an opt-in flag on Offline.configure:

Offline.configure({
    useEJSON: true,   // default: false
});

When enabled:

  • Before store.put(doc), apply EJSON.toJSONValue(doc).
  • After store.get(...) / store.getAll(...), apply EJSON.fromJSONValue(doc) before returning to Minimongo.

The flag is opt-in to preserve backward compatibility and to avoid adding any overhead for users who don't register custom types.

Alternatives considered

  • Per-collection serialize/deserialize callbacks on .keep() — more flexible but error-prone; users must reinvent EJSON each time.
  • Always use EJSON — cleanest API but is a behavioral change for existing deployments.
  • Leave as-is, document the limitation — unfortunate for a sizable class of Meteor apps.

Thanks for jam:offline — the rest of the package works beautifully for our use case, this is the one rough edge we hit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions