Skip to content

Instantly share code, notes, and snippets.

@arv
Last active February 10, 2023 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arv/887921a9f77084619a36c65643f678ae to your computer and use it in GitHub Desktop.
Save arv/887921a9f77084619a36c65643f678ae to your computer and use it in GitHub Desktop.
Replicache MemStore

MemStore

This is the MemStore implementaion as used in Replicache 12.1

This depends on files not included in this gist but these should be pretty self describing.

import {RWLock} from '@rocicorp/lock';
import type {FrozenJSONValue} from '../json.js';
import {promiseVoid} from '../resolved-promises.js';
import {ReadImpl} from './read-impl.js';
import type {Read, Store, Write} from './store.js';
import {WriteImpl} from './write-impl.js';
type StorageMap = Map<string, FrozenJSONValue>;
type Value = {readonly lock: RWLock; readonly map: StorageMap};
const stores = new Map<string, Value>();
export function clearAllMemStoresForTesting(): void {
stores.clear();
}
/**
* A named in-memory Store implementation.
*
* Two (or more) named memory stores with the same name will share the same
* underlying storage. They will also share the same read/write locks, so that
* only one write transaction can be running at the same time.
*
* @experimental This class is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export class MemStore implements Store {
private readonly _map: StorageMap;
private readonly _rwLock: RWLock;
private _closed = false;
constructor(name: string) {
const entry = stores.get(name);
let lock: RWLock;
let map: StorageMap;
if (entry) {
({lock, map} = entry);
} else {
lock = new RWLock();
map = new Map();
stores.set(name, {lock, map});
}
this._rwLock = lock;
this._map = map;
}
async read(): Promise<Read> {
const release = await this._rwLock.read();
return new ReadImpl(this._map, release);
}
async withRead<R>(fn: (read: Read) => R | Promise<R>): Promise<R> {
const read = await this.read();
try {
return await fn(read);
} finally {
read.release();
}
}
async write(): Promise<Write> {
const release = await this._rwLock.write();
return new WriteImpl(this._map, release);
}
async withWrite<R>(fn: (write: Write) => R | Promise<R>): Promise<R> {
const write = await this.write();
try {
return await fn(write);
} finally {
write.release();
}
}
close(): Promise<void> {
this._closed = true;
return promiseVoid;
}
get closed(): boolean {
return this._closed;
}
}
import type {FrozenJSONValue} from '../json.js';
import type {Read} from './store.js';
export class ReadImpl implements Read {
private readonly _map: Map<string, FrozenJSONValue>;
private readonly _release: () => void;
private _closed = false;
constructor(map: Map<string, FrozenJSONValue>, release: () => void) {
this._map = map;
this._release = release;
}
release() {
this._release();
this._closed = true;
}
get closed(): boolean {
return this._closed;
}
has(key: string): Promise<boolean> {
return Promise.resolve(this._map.has(key));
}
get(key: string): Promise<FrozenJSONValue | undefined> {
return Promise.resolve(this._map.get(key));
}
}
import type {ReadonlyJSONValue} from '../json.js';
/**
* Store defines a transactional key/value store that Replicache stores all data
* within.
*
* For correct operation of Replicache, implementations of this interface must
* provide [strict
* serializable](https://jepsen.io/consistency/models/strict-serializable)
* transactions.
*
* Informally, read and write transactions must behave like a ReadWrite Lock -
* multiple read transactions are allowed in parallel, or one write.
* Additionally writes from a transaction must appear all at one, atomically.
*
* @experimental This interface is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export interface Store {
read(): Promise<Read>;
withRead<R>(f: (read: Read) => R | Promise<R>): Promise<R>;
write(): Promise<Write>;
withWrite<R>(f: (write: Write) => R | Promise<R>): Promise<R>;
close(): Promise<void>;
closed: boolean;
}
/**
* Factory function for creating {@link Store} instances.
*
* The name is used to identify the store. If the same name is used for multiple
* stores, they should share the same data. It is also desirable to have these
* stores share an {@link RWLock}.
*
* @experimental This type is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export type CreateStore = (name: string) => Store;
/**
* This interface is used so that we can release the lock when the transaction
* is done.
*
* @experimental This interface is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export interface Release {
release(): void;
}
/**
* @experimental This interface is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export interface Read extends Release {
has(key: string): Promise<boolean>;
// This returns ReadonlyJSONValue instead of FrozenJSONValue because we don't
// want to FrozenJSONValue to be part of our public API. Our implementations
// really return FrozenJSONValue but it is not required by the interface.
get(key: string): Promise<ReadonlyJSONValue | undefined>;
closed: boolean;
}
/**
* @experimental This interface is experimental and might be removed or changed
* in the future without following semver versioning. Please be cautious.
*/
export interface Write extends Read {
put(key: string, value: ReadonlyJSONValue): Promise<void>;
del(key: string): Promise<void>;
commit(): Promise<void>;
}
import {FrozenJSONValue, ReadonlyJSONValue, deepFreeze} from '../json.js';
import {promiseFalse, promiseTrue, promiseVoid} from '../resolved-promises.js';
import type {Read} from './store.js';
export const deleteSentinel = Symbol();
export type DeleteSentinel = typeof deleteSentinel;
export class WriteImplBase {
protected readonly _pending: Map<string, FrozenJSONValue | DeleteSentinel> =
new Map();
private readonly _read: Read;
constructor(read: Read) {
this._read = read;
}
has(key: string): Promise<boolean> {
switch (this._pending.get(key)) {
case undefined:
return this._read.has(key);
case deleteSentinel:
return promiseFalse;
default:
return promiseTrue;
}
}
async get(key: string): Promise<FrozenJSONValue | undefined> {
const v = this._pending.get(key);
switch (v) {
case deleteSentinel:
return undefined;
case undefined: {
const v = await this._read.get(key);
return deepFreeze(v);
}
default:
return v;
}
}
put(key: string, value: ReadonlyJSONValue): Promise<void> {
this._pending.set(key, deepFreeze(value));
return promiseVoid;
}
del(key: string): Promise<void> {
this._pending.set(key, deleteSentinel);
return promiseVoid;
}
release(): void {
this._read.release();
}
get closed(): boolean {
return this._read.closed;
}
}
import type {FrozenJSONValue} from '../json.js';
import {promiseVoid} from '../resolved-promises.js';
import {ReadImpl} from './read-impl.js';
import type {Write} from './store.js';
import {deleteSentinel, WriteImplBase} from './write-impl-base.js';
export class WriteImpl extends WriteImplBase implements Write {
private readonly _map: Map<string, FrozenJSONValue>;
constructor(map: Map<string, FrozenJSONValue>, release: () => void) {
super(new ReadImpl(map, release));
this._map = map;
}
commit(): Promise<void> {
// HOT. Do not allocate entry tuple and destructure.
this._pending.forEach((value, key) => {
if (value === deleteSentinel) {
this._map.delete(key);
} else {
this._map.set(key, value);
}
});
this._pending.clear();
this.release();
return promiseVoid;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment