Replicache 12.0.0
December 02, 2022
Summary
This release improves write performance by up to 7x. It also introduces a new declarative API for indexes.
🔌 Install
npm add replicache@12.0.0
Important: There are breaking changes that need to be considered while upgrading. Please see How to Upgrade for step-by-step instructions or read below for details.
🎁 Features
- Indexes are now defined declaratively using the
indexes
parameter. This simplifies usage with tabbed browsing. See Declarative Indexes.
🧰 Fixes
- Fixed some
InvalidStateError: The database connection is closing
errors (#973). - Exported
Mutation
. This is useful when implementing Replicache backends in TypeScript. - Fixed the type of
PullRequest
'scookie
field (#1032). - Changed the type of
SubscribeOptions.onError
's parameter tounknown
to correctly reflect that in JavaScript any value can be thrown.
⚠️ Breaking Changes
- Values passed into and out of Replicache are no longer defensively cloned and should be considered immutable. See Immutability.
ReadonlyJSONObject
andJSONObject
no longer accept undefined values. See Undefined.
⚡️ Performance
- Up to 7x performance improvement since v11.3.2 when populating Replicache.
- Storing to IndexedDB got about 2x faster. This happens after a short delay so it does not impact WriteTransactions but storing to IDB happens on the main thread so it can cause jank if it takes too long.
Declarative Indexes
Before version 12, Replicache supported persistent indexes with createIndex()
and dropIndex()
.
This imperative API had problems with tabs. If you wanted to change the definition of an index, there was no good way to know when to drop the old version of the index. There could be some other tab open that needed the index.
To solve this, Replicache 12 introduces declarative indexes:
const rep = new Replicache({
name: 'foo',
indexes: {
byTitle: {
prefix: 'todo/',
jsonPointer: '/title',
},
},
});
Replicache creates an index if a client needs one that doesn't exist, and it drops indexes that haven't been used for awhile.
The createIndex
and dropIndex
methods have been deprecated.
Immutability
Replicache caches recently used values in memory for performance reasons.
Because of this, we need to prevent callers from modifying those cached values. For example, consider this mutator:
function createTodo(tx: WriteTransaction, todo: Todo) {
const {id} = todo;
await tx.put(id, todo);
}
If the caller modifies the passed todo
object after calling createTodo()
it can cause problems. The todo may have been previously read and in use elsewhere in the app. Modifications will also confuse Replicache's internal change detection used by subscribe()
.
Before version 12, Replicache prevented these issues by cloning the argument to put()
defensively. That way, if the caller later modifies it, there's no effect on Replicache or previous reads. Similarly, Replicache in many cases cloned returned values from methods like get()
.
This worked, but the clones were very expensive. The cost was especially painful in the common case where the caller didn't actually need or want to mutate anything.
In version 12 we have removed these defensive clones throughout. Now all data passed into or returned from Replicache is considered immutable, and must not be changed by the application.
Immutability Example
The following common pattern is no longer supported:
async function markCompete(tx: WriteTransaction, id: string) {
const todo = await tx.get(id);
// Modify value returned from Replicache.
// No longer supported!
// Throws TypeError in dev mode.
todo.complete = true;
await tx.put(id, todo);
}
Instead, you must clone the returned value expicitly if you want to change it:
async function markCompete(tx: WriteTransaction, id: string) {
const prev = await tx.get(id);
const next = {...prev, complete: true};
await tx.put(id, next);
}
Enforcement
To help prevent mistakenly modifying cached data, Replicache does two things:
-
In dev mode (
process.env.NODE_ENV !== "production"
) we freeze all data passed into or returned from Replicache. This causes attempts to mutate to throw an error. -
All data passed into or out of Replicache is marked
ReadonlyJSONValue
. This prevents some kinds of mutation at compile-time. When you seeReadonlyJSONValue
you should treat that data as immutable. Do not cast away the readonly-ness. If you need to modifyReadonlyJSONValue
, make a copy of it, then modify that.
We also recommend marking your own datatypes deeply readonly where possible. A simple TS type to do this can be found in this gist.
Undefined and JSON
Replicache defines JSONValue
and ReadonlyJSONValue
and these are used throughout the API.
Prior to Replicache 12, these types allowed undefined
fields on objects as a convenience, to support patterns like:
const thing: string | undefined = getThing();
rep.mutate.foo({
id: 123,
bar: thing,
});
In the above case, bar
will either have the value undefined
or a string
value.
However, undefined
is not in fact a valid JSON
type value. And by default
JSON.stringify()
ignores properties with undefined
values:
> JSON.stringify({foo:undefined})
{}
To prevent subtle bugs related to this, version 11 and earlier filtered undefined
values out when cloning inputs.
But since we are no longer cloning inputs there's no place to do this filtering. As such, we no longer support undefined
at all.
Fixing Undefined Errors
Instead of the above pattern, you should write something like:
const args: {id: number; bar?: string} = {
id: 123,
};
if (thing !== undefined) {
args.bar = thing;
}
rep.mutate.foo(args);
or using spread:
const args = {
id: 123,
...(thing !== undefined ? {bar: thing} : {}),
};
rep.mutate.foo(args);
Enforcement
In debug builds, version 12 validates JSONValue
inputs and throws an error at runtime if undefined
is found.
Unfortunately, this can't be caught at compile time with TypeScript's default settings. To catch these errors at compile time, consider enabling exactOptionalPropertyTypes in your tsconfig.
How to Upgrade to v12
Install the new version
npm add replicache@12.0.0
Test your app in development mode (compiled with NODE_ENV !== "production"
)
This will likely uncover errors like:
Error: Cannot assign to read only property
For example, many mutators prior to v12 would read data out of Replicache, modify it, then rewrite it. These must be changed to clone the read data first, mutate the clone, then write the mutated clone. See Immutability Example for an example of how to do this.
You may also encounter errors like:
Error: Invalid type: undefined, expected JSON value
This error means that you passed undefined
to a place that wants JSONValue
or ReadonlyJSONValue
. You should change the code to avoid passing undefined
. See Fixing Undefined Errors for an example.
Optional: Enable exactOptionalPropertyTypes
You can catch more instances of passing undefined
to JSONValue
at compile-time by enabling exactOptionalPropertyTypes in your tsconfig
.