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