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; | |
} | |
} |