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.
jam:offlinefails to persist documents containing EJSON custom types (e.g.Decimalfrommongo-decimal) because the structured clone algorithm used by IndexedDB cannot serialize class instances with methods.In a Meteor app using
mongo-decimal:Once the publish delivers a document to the client,
jam:offlineattempts to write it to IndexedDB. The browser throws:The
Decimalinstance arrives on the client with prototype methods (plus,minus,toString, …) and an own-propertyconstructor, 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 viaEJSON.addType.For apps that rely on precise decimal arithmetic (accounting, finance, e-commerce), the workarounds are all painful:
Number: loses precision semantics, requires schema migration and rewriting of all.plus()/.minus()operations on the server..keep(): these collections are simply not available offline.observeChangesper 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:When enabled:
store.put(doc), applyEJSON.toJSONValue(doc).store.get(...)/store.getAll(...), applyEJSON.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
serialize/deserializecallbacks on.keep()— more flexible but error-prone; users must reinvent EJSON each time.Thanks for
jam:offline— the rest of the package works beautifully for our use case, this is the one rough edge we hit.