diff --git a/src/layered-storage/common.ts b/src/layered-storage/common.ts index b030a541..26745686 100644 --- a/src/layered-storage/common.ts +++ b/src/layered-storage/common.ts @@ -1,13 +1,15 @@ export type KeyRange = number | string | symbol; -export type KeyValueLookup = Record; export type LayerRange = number; export type Segment = boolean | number | object | string | symbol; -export type KeyValueEntry = { - [Key in keyof KV]: readonly [Key, KV[Key]]; -}[keyof KV]; -export type FilteredKeyValueEntry< - KV extends KeyValueLookup, - Keys extends keyof KV + +export type KeyValueLookup = Record< + Keys, + boolean | number | object | string | symbol +>; + +export type KeyValueEntry< + KV extends object, + Key extends keyof KV = keyof KV > = { - [Key in Keys]: readonly [Key, KV[Key]]; -}[Keys]; + [Key in keyof KV]: readonly [Key, KV[Key]]; +}[Key]; diff --git a/src/layered-storage/core.ts b/src/layered-storage/core.ts index b6dd4020..3a71aef9 100644 --- a/src/layered-storage/core.ts +++ b/src/layered-storage/core.ts @@ -1,9 +1,9 @@ import { + KeyRange, + KeyValueEntry, KeyValueLookup, LayerRange, - Segment, - KeyValueEntry, - FilteredKeyValueEntry + Segment } from "./common"; const entriesByKeyPriority = ( @@ -11,61 +11,60 @@ const entriesByKeyPriority = ( b: [number, unknown] ): number => b[0] - a[0]; -type SegmentData = Map; -type LayerData = Map>; -type Data = Map< - Layer, - LayerData +type SegmentData< + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV +> = TypedMap; +type LayerData, Keys extends KeyRange> = Map< + Segment, + SegmentData >; - -interface TypedMap - extends Map { - get(key: Key): undefined | KV[Key]; - set(key: Key, value: KV[Key]): this; +type Data< + Layer extends LayerRange, + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV +> = Map>; + +interface TypedMap< + KV extends Record, + Keys extends KeyRange = keyof KV +> extends Map { + get(key: Key): undefined | KV[Key]; + set(key: Key, value: KV[Key]): this; } -interface CachedValue { - has: boolean; - value: undefined | Value; -} -interface EmptyCacheValue { - has: false; - value: undefined; -} -const emptyCacheValue: EmptyCacheValue = { - has: false, - value: undefined -}; - /** * Internal core to handle simple data storage, mutation and retrieval. Also - * handles the special monolithic segment. + * handles the special global segment. * - * @typeParam KV - Sets the value types associeated with their keys. + * @typeParam Layer - The allowed layers. + * (TS only, ignored in JS). + * @typeParam KV - The value types associeated with their keys. * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. + * @typeParam Keys - The allowed keys. * (TS only, ignored in JS). */ export class LayeredStorageCore< - KV extends KeyValueLookup, - Layer extends LayerRange + Layer extends LayerRange, + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV > { /** * This is a special segment that is used as fallback if the requested * segment doesn't have a value in given layer. */ - public readonly monolithic = Symbol("Monolithic"); + public readonly globalSegment = Symbol("Global Segment"); /** * Data stored as layer → segment → key → value. */ - private readonly _data: Data = new Map(); + private readonly _data: Data = new Map(); /** * An ordered list of layer datas. The highest priority (equals highest * number) layer is first. */ - private _layerDatas: LayerData[] = []; + private _layerDatas: LayerData[] = []; /** * A set of segments that keeps track what segments have data in the storage. @@ -77,8 +76,9 @@ export class LayeredStorageCore< */ private readonly _validators: TypedMap< { - [Key in keyof KV]: ((value: KV[Key]) => true | string)[]; - } + [Key in Keys]: ((value: KV[Key]) => true | string)[]; + }, + Keys > = new Map(); /** @@ -86,17 +86,15 @@ export class LayeredStorageCore< */ private readonly _setExpanders: TypedMap< { - [Key in keyof KV]: (value: KV[Key]) => readonly KeyValueEntry[]; - } + [Key in Keys]: (value: KV[Key]) => readonly KeyValueEntry[]; + }, + Keys > = new Map(); /** * An expander for each key. */ - private readonly _deleteExpanders: Map< - keyof KV, - readonly (keyof KV)[] - > = new Map(); + private readonly _deleteExpanders: Map = new Map(); /** * This is called whenever a validity test fails. @@ -105,7 +103,7 @@ export class LayeredStorageCore< * @param value - The invalid value itself. * @param message - The message returned by the validator that failed. */ - private _invalidHandler: ( + private _invalidHandler: ( key: Key, value: KV[Key], message: string @@ -115,11 +113,14 @@ export class LayeredStorageCore< /** * This is used to speed up retrieval of data upon request. Thanks to this - * quering data from the storage is always just `Map.get().get()` away. + * quering data from the storage is almost always just `Map.get().get()` away. + * + * @remarks + * The `null` stands for value that was looked up but is not set. */ private readonly _topLevelCache = new Map< Segment, - TypedMap<{ [Key in keyof KV]: CachedValue }> + TypedMap<{ [Key in Keys]: KV[Key] | null }, Keys> >(); /** @@ -128,11 +129,11 @@ export class LayeredStorageCore< * @param segment - Which segment to clean. * @param key - The key that was subject to the mutation. */ - private _cleanCache(segment: Segment, key: keyof KV): void { - if (segment === this.monolithic) { + private _cleanCache(segment: Segment, key: Keys): void { + if (segment === this.globalSegment) { // Run the search for each cached segment to clean the cached top level - // value for each of them. The reason for this is that the monolithic - // segment affects all other segments. + // value for each of them. The reason for this is that the global segment + // affects all other segments. for (const cache of this._topLevelCache) { const sCache = cache[1]; @@ -163,76 +164,6 @@ export class LayeredStorageCore< } } - /** - * Find a top level value. - * - * @param segment - Which segment to look into (monolithic is always used as - * fallback on each level). - * @param key - The key identifying requested value. - * - * @returns Whether such value exists (`has`) and the value itself (`value`). - */ - private _findValue( - segment: Segment, - key: Key - ): CachedValue | EmptyCacheValue { - let segmentCache = this._topLevelCache.get(segment); - if (typeof segmentCache === "undefined") { - segmentCache = new Map(); - this._topLevelCache.set(segment, segmentCache); - } - - // Return cached value if it exists. - const cached = segmentCache.get(key); - if (typeof cached !== "undefined") { - return cached; - } - - // Search the layers from highest to lowest priority. - for (const layerData of this._layerDatas) { - if (typeof layerData === "undefined") { - // Empty layer. - continue; - } - - // Check the segment and quit if found. - const segmentData = layerData.get(segment); - if (typeof segmentData !== "undefined") { - const value = segmentData.get(key); - if (typeof value !== "undefined" || segmentData.has(key)) { - const cachedValue = { has: true, value }; - - // Save to the cache. - segmentCache.set(key, cachedValue); - - return cachedValue; - } - } - - // Check the monolithic and quit if found. - const monolithicData = layerData.get(this.monolithic); - if (typeof monolithicData !== "undefined") { - const value = monolithicData.get(key); - if (typeof value !== "undefined" || monolithicData.has(key)) { - const cachedValue = { has: true, value }; - - // Save to the cache. - segmentCache.set(key, cachedValue); - - return cachedValue; - } - } - } - - // If nothing was found by now there are no values for the key. - - // Save to the cache. - segmentCache.set(key, emptyCacheValue); - - // Return the empty value. - return emptyCacheValue; - } - /** * Fetch the key value map for given segment on given layer. Nonexistent * layers and segments will be automatically created and the new instances @@ -246,7 +177,7 @@ export class LayeredStorageCore< private _getLSData( layer: Layer, segment: Segment - ): { layerData: LayerData; segmentData: SegmentData } { + ): { layerData: LayerData; segmentData: SegmentData } { // Get or create the requested layer. let layerData = this._data.get(layer); if (typeof layerData === "undefined") { @@ -255,7 +186,7 @@ export class LayeredStorageCore< this._layerDatas = [...this._data.entries()] .sort(entriesByKeyPriority) - .map((pair): LayerData => pair[1]); + .map((pair): LayerData => pair[1]); } // Get or create the requested segment on the layer. @@ -273,28 +204,81 @@ export class LayeredStorageCore< /** * Retrieve a value. * - * @param segment - Which segment to search through in addition to the monolithic part of the storage. + * @param segment - Which segment to search through in addition to the global + * segment which is used as the fallback on each level. * @param key - The key corresponding to the requested value. * - * @returns The value or undefined if not found. + * @returns The value or undefined if it wasn't found. */ - public get( + public get( segment: Segment, key: Key ): KV[Key] | undefined { - return this._findValue(segment, key).value; + let segmentCache = this._topLevelCache.get(segment); + if (typeof segmentCache === "undefined") { + segmentCache = new Map(); + this._topLevelCache.set(segment, segmentCache); + } + + // Return cached value if it exists. + const cached = segmentCache.get(key); + if (typeof cached !== "undefined") { + // TODO: The non null assertion shouldn't be necessary. + return cached === null ? void 0 : cached!; + } + + // Search the layers from highest to lowest priority. + for (const layerData of this._layerDatas) { + if (typeof layerData === "undefined") { + // Empty layer. + continue; + } + + // Check the segment and quit if found. + const segmentData = layerData.get(segment); + if (typeof segmentData !== "undefined") { + const value = segmentData.get(key); + if (typeof value !== "undefined") { + // Save to the cache. + segmentCache.set(key, value); + + return value; + } + } + + // Check the global segment and quit if found. + const globalData = layerData.get(this.globalSegment); + if (typeof globalData !== "undefined") { + const value = globalData.get(key); + if (typeof value !== "undefined") { + // Save to the cache. + segmentCache.set(key, value); + + return value; + } + } + } + + // If nothing was found by now there are no values for the key. + + // Save to the cache. + segmentCache.set(key, null); + + // Return the empty value. + return; } /** * Check if a value is present. * - * @param segment - Which segment to search through in addition to the monolithic part of the storage. + * @param segment - Which segment to search through in addition to the global + * segment which is used as the fallback on each level. * @param key - The key corresponding to the requested value. * * @returns True if found, false otherwise. */ - public has(segment: Segment, key: Key): boolean { - return this._findValue(segment, key).has; + public has(segment: Segment, key: Key): boolean { + return typeof this.get(segment, key) !== "undefined"; } /** @@ -305,7 +289,7 @@ export class LayeredStorageCore< * @param key - Key that can be used to retrieve or overwrite this value later. * @param value - The value to be saved. */ - public set( + public set( layer: Layer, segment: Segment, key: Key, @@ -346,7 +330,7 @@ export class LayeredStorageCore< * @param segment - Which segment to delete from. * @param key - The key that identifies the value to be deleted. */ - public delete( + public delete( layer: Layer, segment: Segment, key: Key @@ -392,7 +376,7 @@ export class LayeredStorageCore< for (const layerData of this._data.values()) { const sourceSegmentData = layerData.get(sourceSegment); if (sourceSegmentData) { - const sourceSegmentCopy: SegmentData = new Map(); + const sourceSegmentCopy: SegmentData = new Map(); for (const entry of sourceSegmentData.entries()) { sourceSegmentCopy.set(entry[0], entry[1]); } @@ -428,7 +412,7 @@ export class LayeredStorageCore< * value and a message from the failed validator. */ public setInvalidHandler( - handler: ( + handler: ( key: Key, value: KV[Key], message: string @@ -446,7 +430,7 @@ export class LayeredStorageCore< * @param replace - If true existing validators will be replaced, if false an * error will be thrown if some validators already exist for given key. */ - public setValidators( + public setValidators( key: Key, validators: ((value: KV[Key]) => true | string)[], replace: boolean @@ -473,10 +457,10 @@ export class LayeredStorageCore< * @param replace - If true existing expander will be replaced, if false an * error will be thrown if an expander already exists for given key. */ - public setExpander( + public setExpander( key: Key, affects: readonly Affects[], - expander: (value: KV[Key]) => readonly FilteredKeyValueEntry[], + expander: (value: KV[Key]) => readonly KeyValueEntry[], replace: boolean ): void { if ( @@ -496,12 +480,14 @@ export class LayeredStorageCore< * @param key - Which key this value belongs to. * @param value - The value to be expanded. * + * @throws If the value is invalid and the invalid handler throws. + * * @returns Expanded key value pairs or empty array for invalid input. */ - public expandSet( + public expandSet( key: Key, value: KV[Key] - ): readonly KeyValueEntry[] { + ): readonly KeyValueEntry[] { const validators = this._validators.get(key); if (validators) { for (const validator of validators) { @@ -535,7 +521,7 @@ export class LayeredStorageCore< * * @returns Expanded key value pairs or empty array for invalid input. */ - public expandDelete(key: Key): readonly (keyof KV)[] { + public expandDelete(key: Key): readonly Keys[] { return this._deleteExpanders.get(key) || [key]; } diff --git a/src/layered-storage/index.ts b/src/layered-storage/index.ts index c1f2c4a8..588561c4 100644 --- a/src/layered-storage/index.ts +++ b/src/layered-storage/index.ts @@ -1,7 +1,3 @@ -export { - LayeredStorage, - LayeredStorageSegmentTransaction, - LayeredStorageTransaction -} from "./layered-storage"; +export { LayeredStorage, LayeredStorageTransaction } from "./layered-storage"; export { LayeredStorageSegment } from "./segment"; export * from "./common"; diff --git a/src/layered-storage/layered-storage.ts b/src/layered-storage/layered-storage.ts index 0c9e4df6..d5a1f2ae 100644 --- a/src/layered-storage/layered-storage.ts +++ b/src/layered-storage/layered-storage.ts @@ -1,248 +1,43 @@ import { + KeyRange, KeyValueLookup, LayerRange, Segment, - FilteredKeyValueEntry + KeyValueEntry } from "./common"; import { LayeredStorageCore } from "./core"; import { LayeredStorageSegment } from "./segment"; -import { - LayeredStorageSegmentTransaction, - LayeredStorageTransaction, - MonolithicTransaction, - SegmentTransaction -} from "./transactions"; +import { LayeredStorageTransaction } from "./transactions"; -export { - LayeredStorageSegmentTransaction, - LayeredStorageTransaction, - MonolithicTransaction, - SegmentTransaction -}; +export { LayeredStorageTransaction }; /** * Stores data in layers and optionally segments. * * @remarks - * - Higher layers override lowerlayers. + * - Higher layers override lower layers. * - Each layer can be segmented using arbitrary values. - * - Segmented value overrides monolithic (nonsegmented) value. + * - Segmented value overrides global (nonsegmented) value. * - * @typeParam KV - Sets the value types associeated with their keys. + * @typeParam Layer - The allowed layers. + * (TS only, ignored in JS). + * @typeParam KV - The value types associeated with their keys. * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. + * @typeParam Keys - The allowed keys. * (TS only, ignored in JS). */ export class LayeredStorage< - KV extends KeyValueLookup, - Layer extends LayerRange + Layer extends LayerRange, + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV > { - private _core = new LayeredStorageCore(); - - /** - * @param segment - Which segment to search through in addition to the monolithic part of the storage. - * @param key - The key corresponding to the requested value. - */ - public get( - segment: Segment, - key: Key - ): KV[Key] | undefined; - /** - * @param key - The key corresponding to the requested value. - */ - public get(key: Key): KV[Key] | undefined; - /** - * Retrieve a value. - * - * @param rest - Either a key only for monolithic or segment and key for - * specific segment. - * - * @returns The value or undefined if not found. - */ - public get( - ...rest: [Key] | [Segment, Key] - ): KV[Key] | undefined { - return rest.length === 1 - ? this._core.get(this._core.monolithic, rest[0]) - : this._core.get(rest[0], rest[1]); - } - - /** - * @param segment - Which segment to search through in addition to the monolithic part of the storage. - * @param key - The key corresponding to the requested value. - */ - public has(segment: Segment, key: Key): boolean; - /** - * @param key - The key corresponding to the requested value. - */ - public has(key: Key): boolean; - /** - * Check if a value is present. - * - * @param rest - Either a key only for monolithic or segment and key for - * specific segment. - * - * @returns True if found, false otherwise. - */ - public has(...rest: [keyof KV] | [Segment, keyof KV]): boolean { - return rest.length === 1 - ? this._core.has(this._core.monolithic, rest[0]) - : this._core.has(rest[0], rest[1]); - } - - /** - * @param layer - Which layer to save the value into. - * @param segment - Which segment to save the value into. - * @param key - Key that can be used to retrieve or overwrite this value later. - * @param value - The value to be saved. - */ - public set( - layer: Layer, - segment: Segment, - key: Key, - value: KV[Key] - ): void; - /** - * @param layer - Which layer to save the value into. - * @param key - Key that can be used to retrieve or overwrite this value later. - * @param value - The value to be saved. - */ - public set( - layer: Layer, - key: Key, - value: KV[Key] - ): void; - /** - * Save a value. - * - * @param rest - Either layer, key and value only for monolithic or with - * segment for specific segment. - */ - public set( - ...rest: [Layer, Segment, Key, KV[Key]] | [Layer, Key, KV[Key]] - ): void { - this.runTransaction( - (transaction): void => - void (rest.length === 3 - ? transaction.set(rest[0], rest[1], rest[2]) - : transaction.set(rest[0], rest[1], rest[2], rest[3])) - ); - } + private readonly _core = new LayeredStorageCore(); - /** - * @param layer - Which layer to delete from. - * @param segment - Which segment to delete from. - * @param key - The key that identifies the value to be deleted. - */ - public delete( - layer: Layer, - segment: Segment, - key: Key - ): KV[Key]; - /** - * @param layer - Which layer to delete from. - * @param key - The key that identifies the value to be deleted. - */ - public delete(layer: Layer, key: Key): KV[Key]; - /** - * Delete a value from the storage. - * - * @param rest - Either a key only for monolithic or segment and key for - * specific segment. - */ - public delete( - ...rest: [Layer, Segment, Key] | [Layer, Key] - ): void { - this.runTransaction( - (transaction): void => - void (rest.length === 2 - ? transaction.delete(rest[0], rest[1]) - : transaction.delete(rest[0], rest[1], rest[2])) - ); - } - - public openTransaction( - segment: Segment - ): LayeredStorageSegmentTransaction; - public openTransaction(): LayeredStorageTransaction; - /** - * Open a new transaction. - * - * @remarks - * The transaction accumulates changes but doesn't change the content of the - * storage until commit is called. - * - * @param segment - If segment is passed the transaction will be locked to - * given segment. Otherwise the monolithic portion and all the segments will - * be accessible. - * - * @returns The new transaction that can be used to set or delete values. - */ - public openTransaction( - segment?: Segment - ): - | LayeredStorageSegmentTransaction - | LayeredStorageTransaction { - return segment == null - ? new MonolithicTransaction(this._core) - : new SegmentTransaction(this._core, segment); - } - - /** - * @param segment - The segment that this transaction will be limited to. - * @param callback - This callback will be called with the transaction as - * it's sole parameter. - */ - public runTransaction( - segment: Segment, - callback: (transaction: LayeredStorageSegmentTransaction) => void - ): void; - /** - * @param callback - This callback will be called with the transaction as - * it's sole parameter. - */ - public runTransaction( - callback: (transaction: LayeredStorageTransaction) => void - ): void; - /** - * Run a new transaction. - * - * @remarks - * This is the same as `openTransaction` except that it automatically commits - * when the callback finishes execution. It is still possible to commit - * within the body of the callback though. - * - * @param rest - Either a callback only for monolithic or segment and - * callback for specific segment. - */ - public runTransaction( - ...rest: - | [ - Segment, - (transaction: LayeredStorageSegmentTransaction) => void - ] - | [(transaction: LayeredStorageTransaction) => void] - ): void { - if (rest.length === 1) { - const callback = rest[0]; - - const transaction = this.openTransaction(); - - // If the following throws uncommited changes will be discarded. - callback(transaction); - - transaction.commit(); - } else { - const [segment, callback] = rest; - - const transaction = this.openTransaction(segment); - - // If the following throws uncommited changes will be discarded. - callback(transaction); - - transaction.commit(); - } - } + public readonly global = new LayeredStorageSegment( + this, + this._core, + this._core.globalSegment + ); /** * Create a new segmented instance for working with a single segment. @@ -251,8 +46,8 @@ export class LayeredStorage< * * @returns A new segmented instance permanently bound to this instance. */ - public openSegment(segment: Segment): LayeredStorageSegment { - return new LayeredStorageSegment(this, segment); + public openSegment(segment: Segment): LayeredStorageSegment { + return new LayeredStorageSegment(this, this._core, segment); } /** @@ -269,9 +64,9 @@ export class LayeredStorage< public cloneSegment( sourceSegment: Segment, targetSegment: Segment - ): LayeredStorageSegment { + ): LayeredStorageSegment { this._core.cloneSegmentData(sourceSegment, targetSegment); - return new LayeredStorageSegment(this, targetSegment); + return new LayeredStorageSegment(this, this._core, targetSegment); } /** @@ -290,7 +85,7 @@ export class LayeredStorage< * value and a message from the failed validator. */ public setInvalidHandler( - handler: ( + handler: ( key: Key, value: KV[Key], message: string @@ -308,7 +103,7 @@ export class LayeredStorage< * @param replace - If true existing validators will be replaced, if false an * error will be thrown if some validators already exist for given key. */ - public setValidators( + public setValidators( key: Key, validators: ((value: KV[Key]) => true | string)[], replace = false @@ -327,10 +122,10 @@ export class LayeredStorage< * @param replace - If true existing expander will be relaced, if false an * error will be thrown if an expander already exists for given key. */ - public setExpander( + public setExpander( key: Key, affects: readonly Affects[], - expander: (value: KV[Key]) => readonly FilteredKeyValueEntry[], + expander: (value: KV[Key]) => readonly KeyValueEntry[], replace = false ): void { this._core.setExpander(key, affects, expander, replace); diff --git a/src/layered-storage/segment.ts b/src/layered-storage/segment.ts index 6b16a8c1..6889e14e 100644 --- a/src/layered-storage/segment.ts +++ b/src/layered-storage/segment.ts @@ -1,31 +1,34 @@ -import { KeyValueLookup, LayerRange, Segment } from "./common"; -import { - LayeredStorage, - LayeredStorageSegmentTransaction -} from "./layered-storage"; +import { KeyValueLookup, LayerRange, Segment, KeyRange } from "./common"; +import { LayeredStorage, LayeredStorageTransaction } from "./layered-storage"; +import { LayeredStorageCore } from "./core"; /** * This is similar as `LayeredStorage` except that it is permanently bound to * given `LayeredStorage` and can only access a single `Segment`. * - * @typeParam KV - Sets the value types associeated with their keys. + * @typeParam Layer - The allowed layers. * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. + * @typeParam KV - The value types associeated with their keys. + * (TS only, ignored in JS). + * @typeParam Keys - The allowed keys. * (TS only, ignored in JS). */ export class LayeredStorageSegment< - KV extends KeyValueLookup, - Layer extends LayerRange + Layer extends LayerRange, + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV > { /** * Create a new storage instance for given segment. * * @param _layeredStorage - The storage that this instance will be bound to. - * @param _segment - The segment this instance will manage. + * @param _core - The core of the Layered Storage instance. + * @param segment - The segment this instance will manage. */ public constructor( - private _layeredStorage: LayeredStorage, - private _segment: Segment + private readonly _layeredStorage: LayeredStorage, + private readonly _core: LayeredStorageCore, + public readonly segment: Segment ) {} /** @@ -35,8 +38,8 @@ export class LayeredStorageSegment< * * @returns The value or undefined if not found. */ - public get(key: Key): KV[Key] | undefined { - return this._layeredStorage.get(this._segment, key); + public get(key: Key): KV[Key] | undefined { + return this._core.get(this.segment, key); } /** @@ -46,8 +49,8 @@ export class LayeredStorageSegment< * * @returns True if found, false otherwise. */ - public has(key: Key): boolean { - return this._layeredStorage.has(this._segment, key); + public has(key: Key): boolean { + return this._core.has(this.segment, key); } /** @@ -57,12 +60,10 @@ export class LayeredStorageSegment< * @param key - Key that can be used to retrieve or overwrite this value later. * @param value - The value to be saved. */ - public set( - layer: Layer, - key: Key, - value: KV[Key] - ): void { - this._layeredStorage.set(layer, this._segment, key, value); + public set(layer: Layer, key: Key, value: KV[Key]): void { + this.runTransaction((transaction): void => { + transaction.set(layer, key, value); + }); } /** @@ -71,8 +72,10 @@ export class LayeredStorageSegment< * @param layer - Which layer to delete from. * @param key - The key that identifies the value to be deleted. */ - public delete(layer: Layer, key: Key): void { - this._layeredStorage.delete(layer, this._segment, key); + public delete(layer: Layer, key: Key): void { + this.runTransaction((transaction): void => { + transaction.delete(layer, key); + }); } /** @@ -87,8 +90,8 @@ export class LayeredStorageSegment< */ public cloneSegment( targetSegment: Segment - ): LayeredStorageSegment { - return this._layeredStorage.cloneSegment(this._segment, targetSegment); + ): LayeredStorageSegment { + return this._layeredStorage.cloneSegment(this.segment, targetSegment); } /** @@ -100,8 +103,11 @@ export class LayeredStorageSegment< * * @returns The new transaction that can be used to set or delete values. */ - public openTransaction(): LayeredStorageSegmentTransaction { - return this._layeredStorage.openTransaction(this._segment); + public openTransaction(): LayeredStorageTransaction { + return new LayeredStorageTransaction( + this._core, + this.segment + ); } /** @@ -116,15 +122,21 @@ export class LayeredStorageSegment< * it's sole argument. */ public runTransaction( - callback: (transaction: LayeredStorageSegmentTransaction) => void + callback: (transaction: LayeredStorageTransaction) => void ): void { - this._layeredStorage.runTransaction(this._segment, callback); + const transaction = this.openTransaction(); + + // If the following throws uncommited changes will get out of scope and will + // be discarded and garbage collected. + callback(transaction); + + transaction.commit(); } /** * Delete all data belonging to this segment. */ public close(): void { - this._layeredStorage.deleteSegmentData(this._segment); + this._layeredStorage.deleteSegmentData(this.segment); } } diff --git a/src/layered-storage/transactions.ts b/src/layered-storage/transactions.ts index 31a576e0..5d8efa07 100644 --- a/src/layered-storage/transactions.ts +++ b/src/layered-storage/transactions.ts @@ -1,16 +1,21 @@ -import { KeyValueLookup, LayerRange, Segment } from "./common"; +import { KeyValueLookup, LayerRange, Segment, KeyRange } from "./common"; import { LayeredStorageCore } from "./core"; /** - * This is used through composition to create monolithic and segmented - * transactions without massive code duplicities. + * A transaction working with a single segment. * - * @typeParam KV - Sets the value types associeated with their keys. + * @typeParam Layer - The allowed layers. + * (TS only, ignored in JS). + * @typeParam KV - The value types associeated with their keys. * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. + * @typeParam Keys - The allowed keys. * (TS only, ignored in JS). */ -class TransactionCore { +export class LayeredStorageTransaction< + Layer extends LayerRange, + KV extends KeyValueLookup, + Keys extends KeyRange = keyof KV +> { /** * Functions that perform requested mutations when executed without any * arguments or this. Intended to be filled bound set and delete methods from @@ -19,58 +24,45 @@ class TransactionCore { private _actions: (() => void)[] = []; /** - * Create a new instance of transaction core. + * Create a new transaction for a segment of given storage. * * @param _storageCore - The core that this instance will save mutations to. + * @param _segment - The segment this instance will manage. */ - public constructor(private _storageCore: LayeredStorageCore) {} + public constructor( + private readonly _storageCore: LayeredStorageCore, + private readonly _segment: Segment + ) {} /** - * Queue set mutation. + * Queue a value to be set. * * @param layer - Which layer to save the value into. - * @param segment - Which segment to save the value into. * @param key - Key that can be used to retrieve or overwrite this value later. * @param value - The value to be saved. */ - public set( - layer: Layer, - segment: Segment, - key: Key, - value: KV[Key] - ): void { + public set(layer: Layer, key: Key, value: KV[Key]): void { const expandedPairs = this._storageCore.expandSet(key, value); for (const expanded of expandedPairs) { - this._actions.push( - this._storageCore.set.bind( - this._storageCore, - layer, - segment, - expanded[0], - expanded[1] - ) - ); + this._actions.push((): void => { + this._storageCore.set(layer, this._segment, expanded[0], expanded[1]); + }); } } /** - * Queue delete mutation. + * Queue a value to be deleted. * * @param layer - Which layer to delete from. - * @param segment - Which segment to delete from. * @param key - The key that identifies the value to be deleted. */ - public delete( - layer: Layer, - segment: Segment, - key: Key - ): void { + public delete(layer: Layer, key: Key): void { for (const expandedKey of this._storageCore.expandDelete(key)) { this._actions.push( this._storageCore.delete.bind( this._storageCore, layer, - segment, + this._segment, expandedKey ) ); @@ -94,214 +86,3 @@ class TransactionCore { this._actions = []; } } - -/** - * A transaction working with the whole storage. - * - * @typeParam KV - Sets the value types associeated with their keys. - * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. - * (TS only, ignored in JS). - */ -export interface LayeredStorageTransaction< - KV extends KeyValueLookup, - Layer extends LayerRange -> { - /** - * Queue a value to be set. - * - * @param layer - Which layer to save the value into. - * @param segment - Which segment to save the value into. - * @param key - Key that can be used to retrieve or overwrite this value later. - * @param value - The value to be saved. - */ - set( - layer: Layer, - segment: Segment, - key: Key, - value: KV[Key] - ): void; - /** - * Queue a value to be set. - * - * @param layer - Which layer to save the value into. - * @param key - Key that can be used to retrieve or overwrite this value later. - * @param value - The value to be saved. - */ - set(layer: Layer, key: Key, value: KV[Key]): void; - - /** - * Queue a value to be deleted. - * - * @param layer - Which layer to delete from. - * @param segment - Which segment to delete from. - * @param key - The key that identifies the value to be deleted. - */ - delete(layer: Layer, segment: Segment, key: Key): void; - /** - * Queue a value to be deleted. - * - * @param layer - Which layer to delete from. - * @param key - The key that identifies the value to be deleted. - */ - delete(layer: Layer, key: Key): void; - - /** - * Commit all queued operations. - */ - commit(): void; - - /** - * Discard all queued operations. - */ - abort(): void; -} - -/** @inheritdoc */ -export class MonolithicTransaction< - KV extends KeyValueLookup, - Layer extends LayerRange -> implements LayeredStorageTransaction { - private readonly _transactionCore: TransactionCore; - private readonly _monolithic: symbol; - - /** - * Create a new transaction for given storage. - * - * @param storageCore - The core that this instance will save mutations to. - */ - public constructor(storageCore: LayeredStorageCore) { - this._monolithic = storageCore.monolithic; - this._transactionCore = new TransactionCore(storageCore); - } - - public set( - layer: Layer, - segment: Segment, - key: Key, - value: KV[Key] - ): void; - public set( - layer: Layer, - key: Key, - value: KV[Key] - ): void; - /** @inheritdoc */ - public set( - ...rest: [Layer, Segment, Key, KV[Key]] | [Layer, Key, KV[Key]] - ): void { - return rest.length === 3 - ? this._transactionCore.set(rest[0], this._monolithic, rest[1], rest[2]) - : this._transactionCore.set(rest[0], rest[1], rest[2], rest[3]); - } - - public delete( - layer: Layer, - segment: Segment, - key: Key - ): void; - public delete(layer: Layer, key: Key): void; - /** @inheritdoc */ - public delete( - ...rest: [Layer, Segment, Key] | [Layer, Key] - ): void { - return rest.length === 2 - ? this._transactionCore.delete(rest[0], this._monolithic, rest[1]) - : this._transactionCore.delete(rest[0], rest[1], rest[2]); - } - - /** @inheritdoc */ - public commit(): void { - return this._transactionCore.commit(); - } - - /** @inheritdoc */ - public abort(): void { - return this._transactionCore.abort(); - } -} - -/** @inheritdoc */ -export interface LayeredStorageSegmentTransaction< - KV extends KeyValueLookup, - Layer extends LayerRange -> { - /** - * Queue a value to be set. - * - * @param layer - Which layer to save the value into. - * @param key - Key that can be used to retrieve or overwrite this value later. - * @param value - The value to be saved. - */ - set(layer: Layer, key: Key, value: KV[Key]): void; - - /** - * Queue a value to be deleted. - * - * @param layer - Which layer to delete from. - * @param key - The key that identifies the value to be deleted. - */ - delete(layer: Layer, key: Key): void; - - /** - * Commit all queued operations. - */ - commit(): void; - - /** - * Discard all queued operations. - */ - abort(): void; -} - -/** - * A transaction working with a single segment. - * - * @typeParam KV - Sets the value types associeated with their keys. - * (TS only, ignored in JS). - * @typeParam Layer - Sets the allowed layers. - * (TS only, ignored in JS). - */ -export class SegmentTransaction< - KV extends KeyValueLookup, - Layer extends LayerRange -> implements LayeredStorageSegmentTransaction { - private readonly _transactionCore: TransactionCore; - - /** - * Create a new transaction for a segment of given storage. - * - * @param storageCore - The core that this instance will save mutations to. - * @param _segment - The segment this instance will manage. - */ - public constructor( - storageCore: LayeredStorageCore, - private readonly _segment: Segment - ) { - this._transactionCore = new TransactionCore(storageCore); - } - - /** @inheritdoc */ - public set( - layer: Layer, - key: Key, - value: KV[Key] - ): void { - return this._transactionCore.set(layer, this._segment, key, value); - } - - /** @inheritdoc */ - public delete(layer: Layer, key: Key): void { - return this._transactionCore.delete(layer, this._segment, key); - } - - /** @inheritdoc */ - public commit(): void { - return this._transactionCore.commit(); - } - - /** @inheritdoc */ - public abort(): void { - return this._transactionCore.abort(); - } -} diff --git a/test/layered-storage/all-combined.ts b/test/layered-storage/all-combined.ts index bc11a45f..79502099 100644 --- a/test/layered-storage/all-combined.ts +++ b/test/layered-storage/all-combined.ts @@ -5,7 +5,7 @@ import { expect } from "chai"; type KV = Record; const expectedResult = deepFreeze({ - monolithic: { + global: { "test.value1": 5, "test.value2": undefined, "test.value3": undefined @@ -28,127 +28,82 @@ const expectedResult = deepFreeze({ }); const expectedResultMinusC = deepFreeze({ - monolithic: expectedResult.monolithic, + global: expectedResult.global, a: expectedResult.a, b: expectedResult.b, - c: expectedResult.monolithic + c: expectedResult.global }); const expectedResultMinusAC = deepFreeze({ - monolithic: expectedResult.monolithic, - a: expectedResult.monolithic, + global: expectedResult.global, + a: expectedResult.global, b: expectedResult.b, - c: expectedResult.monolithic + c: expectedResult.global }); const expectedResultMinusABC = deepFreeze({ - monolithic: expectedResult.monolithic, - a: expectedResult.monolithic, - b: expectedResult.monolithic, - c: expectedResult.monolithic + global: expectedResult.global, + a: expectedResult.global, + b: expectedResult.global, + c: expectedResult.global }); /** * Test all mutatins including segmented mutations with Layered Storage. */ export function allCombined(): void { - describe("All combined", function(): void { - it("Main instance only", function(): void { - const ls = new LayeredStorage(); - - const getData = (): unknown => { - const data: any = {}; - for (const segment of ["monolithic", "a", "b", "c"]) { - data[segment] = {}; - for (const key of ["test.value1", "test.value2", "test.value3"]) { - data[segment][key] = - segment === "monolithic" ? ls.get(key) : ls.get(segment, key); - } + it("All combined", function(): void { + const ls = new LayeredStorage<1 | 4 | 9, KV, keyof KV>(); + + const a = ls.openSegment("a"); + const b = ls.openSegment("b"); + const c = ls.openSegment("c"); + const segments = { a, b, c }; + + const getData = (): unknown => { + const data: any = {}; + for (const segment of [ + "global" as const, + "a" as const, + "b" as const, + "c" as const + ]) { + data[segment] = {}; + for (const key of ["test.value1", "test.value2", "test.value3"]) { + data[segment][key] = + segment === "global" + ? ls.global.get(key) + : segments[segment].get(key); } - - return data; - }; - - ls.set(1, "b", "test.value3", 6); - ls.set(1, "c", "test.value2", 7); - ls.set(1, "a", "test.value1", 1); - ls.delete(4, "b", "test.value1"); - ls.set(4, "test.value1", 4); - ls.set(4, "b", "test.value3", 9); - ls.delete(4, "c", "test.value1"); - ls.delete(9, "test.value1"); - ls.set(9, "test.value3", 3); - ls.set(4, "a", "test.value2", 2); - ls.set(9, "b", "test.value2", 8); - ls.delete(9, "test.value3"); - ls.set(9, "test.value1", 5); - ls.delete(4, "a", "test.value1"); - ls.set(9, "c", "test.value1", 3); - expect(getData()).to.deep.equal(expectedResult); - - ls.deleteSegmentData("c"); - expect(getData()).to.deep.equal(expectedResultMinusC); - - ls.deleteSegmentData("a"); - expect(getData()).to.deep.equal(expectedResultMinusAC); - - ls.deleteSegmentData("b"); - expect(getData()).to.deep.equal(expectedResultMinusABC); - }); - - it("Main and segment instances", function(): void { - const ls = new LayeredStorage(); - - const a = ls.openSegment("a"); - const b = ls.openSegment("b"); - const c = ls.openSegment("c"); - const segments = { a, b, c }; - - const getData = (): unknown => { - const data: any = {}; - for (const segment of [ - "monolithic" as const, - "a" as const, - "b" as const, - "c" as const - ]) { - data[segment] = {}; - for (const key of ["test.value1", "test.value2", "test.value3"]) { - data[segment][key] = - segment === "monolithic" - ? ls.get(key) - : segments[segment].get(key); - } - } - - return data; - }; - - b.set(1, "test.value3", 6); - c.set(1, "test.value2", 7); - a.set(1, "test.value1", 1); - b.delete(4, "test.value1"); - ls.set(4, "test.value1", 4); - b.set(4, "test.value3", 9); - c.delete(4, "test.value1"); - ls.delete(9, "test.value1"); - ls.set(9, "test.value3", 3); - a.set(4, "test.value2", 2); - b.set(9, "test.value2", 8); - ls.delete(9, "test.value3"); - ls.set(9, "test.value1", 5); - a.delete(4, "test.value1"); - c.set(9, "test.value1", 3); - expect(getData()).to.deep.equal(expectedResult); - - c.close(); - expect(getData()).to.deep.equal(expectedResultMinusC); - - a.close(); - expect(getData()).to.deep.equal(expectedResultMinusAC); - - b.close(); - expect(getData()).to.deep.equal(expectedResultMinusABC); - }); + } + + return data; + }; + + b.set(1, "test.value3", 6); + c.set(1, "test.value2", 7); + a.set(1, "test.value1", 1); + b.delete(4, "test.value1"); + ls.global.set(4, "test.value1", 4); + b.set(4, "test.value3", 9); + c.delete(4, "test.value1"); + ls.global.delete(9, "test.value1"); + ls.global.set(9, "test.value3", 3); + a.set(4, "test.value2", 2); + b.set(9, "test.value2", 8); + ls.global.delete(9, "test.value3"); + ls.global.set(9, "test.value1", 5); + a.delete(4, "test.value1"); + c.set(9, "test.value1", 3); + expect(getData()).to.deep.equal(expectedResult); + + c.close(); + expect(getData()).to.deep.equal(expectedResultMinusC); + + a.close(); + expect(getData()).to.deep.equal(expectedResultMinusAC); + + b.close(); + expect(getData()).to.deep.equal(expectedResultMinusABC); }); } diff --git a/test/layered-storage/cloning.ts b/test/layered-storage/cloning.ts index 8222c85f..4462fcaa 100644 --- a/test/layered-storage/cloning.ts +++ b/test/layered-storage/cloning.ts @@ -16,25 +16,25 @@ export function cloning(): void { const configs: { name: string; clone( - ls: LayeredStorage, - s1: LayeredStorageSegment - ): LayeredStorageSegment; + ls: LayeredStorage<0 | 1 | 2, KV, keyof KV>, + s1: LayeredStorageSegment<0 | 1 | 2, KV, keyof KV> + ): LayeredStorageSegment<0 | 1 | 2, KV, keyof KV>; }[] = [ { name: "From main instance", - clone: (ls): LayeredStorageSegment => + clone: (ls): LayeredStorageSegment<0 | 1 | 2, KV, keyof KV> => ls.cloneSegment(1, 2) }, { name: "From segment instance", - clone: (_ls, s1): LayeredStorageSegment => + clone: (_ls, s1): LayeredStorageSegment<0 | 1 | 2, KV, keyof KV> => s1.cloneSegment(2) } ]; configs.forEach(({ name, clone }): void => { it(name, function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0 | 1 | 2, KV, keyof KV>(); const s1 = ls.openSegment(1); @@ -71,7 +71,7 @@ export function cloning(): void { }); it("Cloning into existing segment", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1, KV, keyof KV>(); const s1 = ls.openSegment(1); const s2 = ls.openSegment(2); diff --git a/test/layered-storage/expanders.ts b/test/layered-storage/expanders.ts index 244a9a6a..8549c023 100644 --- a/test/layered-storage/expanders.ts +++ b/test/layered-storage/expanders.ts @@ -1,7 +1,4 @@ -import { - LayeredStorage, - FilteredKeyValueEntry -} from "../../src/layered-storage"; +import { KeyValueEntry, LayeredStorage } from "../../src/layered-storage"; import { expect } from "chai"; interface KV { @@ -12,7 +9,7 @@ interface KV { } /** - * Test that values can be set and retrieved from single monolithic layer. + * Test that values can be set and retrieved from single global layer. */ export function expanders(): void { describe("Expanders", function(): void { @@ -23,7 +20,7 @@ export function expanders(): void { ] as const; const expander = ( input: string - ): readonly FilteredKeyValueEntry< + ): readonly KeyValueEntry< KV, "test.boolean" | "test.number" | "test.string" >[] => { @@ -39,24 +36,29 @@ export function expanders(): void { }; it("Without validation", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); ls.setExpander("test", expanderAffects, expander); const testValue = "false 7 seven"; - ls.set(0, "test", testValue); + ls.global.set(0, "test", testValue); - expect(ls.has("test"), "The raw value should not be saved.").to.be.false; + expect(ls.global.has("test"), "The raw value should not be saved.").to.be + .false; expect( - [ls.get("test.boolean"), ls.get("test.number"), ls.get("test.string")], + [ + ls.global.get("test.boolean"), + ls.global.get("test.number"), + ls.global.get("test.string") + ], "The expanded values from the expander should be returned." ).to.deep.equal([false, 7, "seven"]); }); it("Invalid short value", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); ls.setValidators("test", [ (value): true | string => @@ -68,13 +70,13 @@ export function expanders(): void { const testValue = "false7seven"; expect( - (): void => void ls.set(0, "test", testValue), + (): void => void ls.global.set(0, "test", testValue), "Invalid values should not pass validation." ).to.throw(); }); it("Invalid expanded value", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); ls.setValidators("test.number", [ (value): true | string => value < 7 || "Invalid input." @@ -85,27 +87,35 @@ export function expanders(): void { const testValue = "false 7 seven"; expect( - (): void => void ls.set(0, "test", testValue), + (): void => void ls.global.set(0, "test", testValue), "Invalid values should not pass validation." ).to.throw(); }); it("Delete expanded values", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); ls.setExpander("test", expanderAffects, expander); const testValue = "false 7 seven"; - ls.set(0, "test", testValue); + ls.global.set(0, "test", testValue); expect( - [ls.has("test.boolean"), ls.has("test.number"), ls.has("test.string")], + [ + ls.global.has("test.boolean"), + ls.global.has("test.number"), + ls.global.has("test.string") + ], "All expanded values should be set." ).deep.equal([true, true, true]); - ls.delete(0, "test"); + ls.global.delete(0, "test"); expect( - [ls.has("test.boolean"), ls.has("test.number"), ls.has("test.string")], + [ + ls.global.has("test.boolean"), + ls.global.has("test.number"), + ls.global.has("test.string") + ], "All expanded values should be deleted." ).deep.equal([false, false, false]); }); diff --git a/test/layered-storage/multiple-keys.ts b/test/layered-storage/multiple-keys.ts index b16d4200..bbd664f9 100644 --- a/test/layered-storage/multiple-keys.ts +++ b/test/layered-storage/multiple-keys.ts @@ -18,97 +18,97 @@ export function multipleKeys(): void { const testValue3: KV["test.value3"] = "abc"; it("Set and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<3, KV, keyof KV>(); - ls.set(3, "test.value1", testValue1); - ls.set(3, "test.value2", testValue2); - ls.set(3, "test.value3", testValue3); + ls.global.set(3, "test.value1", testValue1); + ls.global.set(3, "test.value2", testValue2); + ls.global.set(3, "test.value3", testValue3); expect( - ls.get("test.value1"), + ls.global.get("test.value1"), "The same value that was set should be returned." ).to.equal(testValue1); expect( - ls.get("test.value2"), + ls.global.get("test.value2"), "The same value that was set should be returned." ).to.equal(testValue2); expect( - ls.get("test.value3"), + ls.global.get("test.value3"), "The same value that was set should be returned." ).to.equal(testValue3); }); it("Set and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<3, KV, keyof KV>(); - ls.set(3, "test.value1", testValue1); - ls.set(3, "test.value2", testValue2); - ls.set(3, "test.value3", testValue3); + ls.global.set(3, "test.value1", testValue1); + ls.global.set(3, "test.value2", testValue2); + ls.global.set(3, "test.value3", testValue3); expect( - ls.has("test.value1"), + ls.global.has("test.value1"), "This value was set and should be reported as present." ).to.be.true; expect( - ls.has("test.value2"), + ls.global.has("test.value2"), "This value was set and should be reported as present." ).to.be.true; expect( - ls.has("test.value3"), + ls.global.has("test.value3"), "This value was set and should be reported as present." ).to.be.true; }); it("Set, delete and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<3, KV, keyof KV>(); expect( - ls.get("test.value2"), + ls.global.get("test.value2"), "There is no value yet so it should be undefined." ).to.be.undefined; - ls.set(3, "test.value1", testValue1); + ls.global.set(3, "test.value1", testValue1); expect( - ls.get("test.value2"), + ls.global.get("test.value2"), "Different value was set, undefined should be returned." ).to.be.undefined; - ls.set(3, "test.value2", testValue2); + ls.global.set(3, "test.value2", testValue2); expect( - ls.get("test.value2"), + ls.global.get("test.value2"), "The value that was set should also be returned." ).to.equal(testValue2); - ls.delete(3, "test.value2"); + ls.global.delete(3, "test.value2"); expect( - ls.get("test.value2"), + ls.global.get("test.value2"), "Undefined should be returned for deleted values." ).to.be.undefined; }); it("Set, delete and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<3, KV, keyof KV>(); expect( - ls.has("test.value2"), + ls.global.has("test.value2"), "There is no value yet so it should be false." ).to.be.false; - ls.set(3, "test.value1", testValue1); + ls.global.set(3, "test.value1", testValue1); expect( - ls.has("test.value2"), + ls.global.has("test.value2"), "Different value was set, false should be returned." ).to.be.false; - ls.set(3, "test.value2", testValue2); + ls.global.set(3, "test.value2", testValue2); expect( - ls.has("test.value2"), + ls.global.has("test.value2"), "True should be returned for existing values." ).to.be.true; - ls.delete(3, "test.value2"); + ls.global.delete(3, "test.value2"); expect( - ls.has("test.value2"), + ls.global.has("test.value2"), "False should be returned for deleted values." ).to.be.false; }); diff --git a/test/layered-storage/multiple-layers.ts b/test/layered-storage/multiple-layers.ts index 5e28a886..e0a6892b 100644 --- a/test/layered-storage/multiple-layers.ts +++ b/test/layered-storage/multiple-layers.ts @@ -30,110 +30,110 @@ export function multipleLayers(): void { }); it("Set and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 2 | 3 | 4, KV, keyof KV>(); - ls.set(1, "test.value", testValue1); + ls.global.set(1, "test.value", testValue1); expect( - ls.get("test.value"), + ls.global.get("test.value"), "The first layer should be returned since it's the highest." ).to.equal(testValue1); - ls.set(2, "test.value", testValue2); + ls.global.set(2, "test.value", testValue2); expect( - ls.get("test.value"), + ls.global.get("test.value"), "The second layer should be returned since it's the highest." ).to.equal(testValue2); - ls.set(4, "test.value", testValue4); + ls.global.set(4, "test.value", testValue4); expect( - ls.get("test.value"), + ls.global.get("test.value"), "The fourth layer should be returned since it's the highest now." ).to.equal(testValue4); }); it("Set and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 2 | 3 | 4, KV, keyof KV>(); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is no value yet so it shouldn't be reported as empty." ).to.be.false; - ls.set(3, "test.value", testValue3); + ls.global.set(3, "test.value", testValue3); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is one value so it should be reported as present." ).to.be.true; - ls.set(2, "test.value", testValue2); + ls.global.set(2, "test.value", testValue2); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There are two value so it should be reported as present." ).to.be.true; }); it("Set, delete and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 2 | 3 | 4, KV, keyof KV>(); expect( - ls.get("test.value"), + ls.global.get("test.value"), "There is no value yet so it should be undefined." ).to.be.undefined; - ls.set(3, "test.value", testValue3); + ls.global.set(3, "test.value", testValue3); expect( - ls.get("test.value"), + ls.global.get("test.value"), "Layer three has a value that should be returned." ).to.equal(testValue3); - ls.set(2, "test.value", testValue2); + ls.global.set(2, "test.value", testValue2); expect( - ls.get("test.value"), + ls.global.get("test.value"), "Layer three has a value that should be returned." ).to.equal(testValue3); - ls.delete(3, "test.value"); + ls.global.delete(3, "test.value"); expect( - ls.get("test.value"), + ls.global.get("test.value"), "Layer two has a value that should be returned." ).to.equal(testValue2); - ls.delete(2, "test.value"); + ls.global.delete(2, "test.value"); expect( - ls.get("test.value"), + ls.global.get("test.value"), "There isn't any value anymore so it should be undefined." ).to.be.undefined; }); it("Set, delete and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 2 | 3 | 4, KV, keyof KV>(); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is no value yet so it should be reported as empty." ).to.be.false; - ls.set(3, "test.value", testValue3); + ls.global.set(3, "test.value", testValue3); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is one value so it should be reported as present." ).to.be.true; - ls.set(2, "test.value", testValue2); + ls.global.set(2, "test.value", testValue2); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There are two value so it should be reported as present." ).to.be.true; - ls.delete(2, "test.value"); + ls.global.delete(2, "test.value"); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is one value so it should be reported as present." ).to.be.true; - ls.delete(3, "test.value"); + ls.global.delete(3, "test.value"); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There isn't any value anymore so it should be reported as empty." ).to.be.false; }); diff --git a/test/layered-storage/other.ts b/test/layered-storage/other.ts index c9358c06..abb23775 100644 --- a/test/layered-storage/other.ts +++ b/test/layered-storage/other.ts @@ -7,7 +7,9 @@ type KV = Record; * Other tests that don't fit elsewhere. */ export function other(): void { - const getStructCount = (ls: LayeredStorage): number => + const getStructCount = ( + ls: LayeredStorage<1 | 4 | 9, KV, keyof KV> + ): number => [ // Ignore private property access errors. It's no big deal since this // is a unit test. @@ -23,21 +25,21 @@ export function other(): void { ); }, 0); - const getCacheSize = (ls: LayeredStorage): number => + const getCacheSize = (ls: LayeredStorage<1 | 4 | 9, KV, keyof KV>): number => // Ignore private property access errors. It's no big deal since this // is a unit test. // @ts-ignore ls._core._topLevelCache.size; it("Empty data structure purging", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 4 | 9, KV, keyof KV>(); ([1, 4, 9] as const).forEach((layer): void => { - ls.set(layer, "test.value1", 1); - ls.set(layer, "test.value2", 2); + ls.global.set(layer, "test.value1", 1); + ls.global.set(layer, "test.value2", 2); ["a", "b", "c"].forEach((segment): void => { - ls.set(layer, segment, "test.value1", 1); - ls.set(layer, segment, "test.value2", 2); + ls.openSegment(segment).set(layer, "test.value1", 1); + ls.openSegment(segment).set(layer, "test.value2", 2); }); }); @@ -48,9 +50,9 @@ export function other(): void { ); ([1, 4, 9] as const).forEach((layer): void => { - ls.delete(layer, "test.value1"); + ls.global.delete(layer, "test.value1"); ["a", "b", "c"].forEach((segment): void => { - ls.delete(layer, segment, "test.value1"); + ls.openSegment(segment).delete(layer, "test.value1"); }); }); @@ -61,9 +63,9 @@ export function other(): void { ); ([1, 4, 9] as const).forEach((layer): void => { - ls.delete(layer, "test.value2"); + ls.global.delete(layer, "test.value2"); ["b"].forEach((segment): void => { - ls.delete(layer, segment, "test.value2"); + ls.openSegment(segment).delete(layer, "test.value2"); }); }); @@ -75,7 +77,7 @@ export function other(): void { ([1, 4, 9] as const).forEach((layer): void => { ["a", "c"].forEach((segment): void => { - ls.delete(layer, segment, "test.value2"); + ls.openSegment(segment).delete(layer, "test.value2"); }); }); @@ -85,52 +87,63 @@ export function other(): void { }); it("Cache purging", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1, KV, keyof KV>(); expect(getCacheSize(ls)).to.equal(0); - ls.set(1, "test.value1", 7); - ls.set(1, "a", "test.value1", 7); - ls.set(1, "b", "test.value1", 7); - ls.set(1, "c", "test.value1", 7); + ls.global.set(1, "test.value1", 7); + ls.openSegment("a").set(1, "test.value1", 7); + ls.openSegment("b").set(1, "test.value1", 7); + ls.openSegment("c").set(1, "test.value1", 7); expect(getCacheSize(ls)).to.equal(0); - ls.get("test.value1"); - ls.get("a", "test.value1"); - ls.get("b", "test.value1"); - ls.get("c", "test.value1"); - ls.get("test.value2"); - ls.get("a", "test.value2"); - ls.get("b", "test.value2"); - ls.get("c", "test.value2"); + ls.global.get("test.value1"); + ls.openSegment("a").get("test.value1"); + ls.openSegment("b").get("test.value1"); + ls.openSegment("c").get("test.value1"); + ls.global.get("test.value2"); + ls.openSegment("a").get("test.value2"); + ls.openSegment("b").get("test.value2"); + ls.openSegment("c").get("test.value2"); expect(getCacheSize(ls)).to.equal(4); - ls.set(1, "test.value1", 7); - ls.set(1, "a", "test.value1", 7); - ls.set(1, "b", "test.value1", 7); - ls.set(1, "c", "test.value1", 7); + ls.global.set(1, "test.value1", 7); + ls.openSegment("a").set(1, "test.value1", 7); + ls.openSegment("b").set(1, "test.value1", 7); + ls.openSegment("c").set(1, "test.value1", 7); expect(getCacheSize(ls)).to.equal(4); - ls.set(1, "test.value2", 7); + ls.global.set(1, "test.value2", 7); expect(getCacheSize(ls)).to.equal(0); }); it("Empty data structure creation", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<1 | 4 | 9, KV, keyof KV>(); - ls.set(4, "c", "test.value1", 1); + ls.openSegment("c").set(4, "test.value1", 1); - ls.get("test.value1"); - ls.get("b", "test.value1"); - ls.has("test.value2"); - ls.has("a", "test.value2"); + ls.global.get("test.value1"); + ls.openSegment("b").get("test.value1"); + ls.global.has("test.value2"); + ls.openSegment("a").has("test.value2"); expect(getStructCount(ls)).to.equal( 3 // 1 layer, 1 segment, 1 value ); }); + + it("Segment storage reports it's segment", function(): void { + const ls = new LayeredStorage<1 | 4 | 9, KV, keyof KV>(); + + expect( + ls.openSegment("$"), + "Each segment should exposes a property with the name of the segment." + ) + .have.ownProperty("segment") + .that.equals("$"); + }); } diff --git a/test/layered-storage/segmented-layer.ts b/test/layered-storage/segmented-layer.ts index c4b141a7..d7943c8f 100644 --- a/test/layered-storage/segmented-layer.ts +++ b/test/layered-storage/segmented-layer.ts @@ -30,114 +30,116 @@ export function segmentedLayer(): void { const c = Symbol("C"); it("Get without set", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); - ls.set(7, "test.value", testValueA); + ls.global.set(7, "test.value", testValueA); expect( - ls.get(b, "test.value"), - "Monolithic value should be used if the segment doesn't exist." + ls.openSegment(b).get("test.value"), + "Global value should be used if the segment doesn't exist." ).to.equal(testValueA); }); it("Get without set after unrelated set", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); - ls.set(7, "test.value", testValueA); - ls.set(7, b, "unrelated.value", testValueB); + ls.global.set(7, "test.value", testValueA); + ls.openSegment(b).set(7, "unrelated.value", testValueB); expect( - ls.get(b, "test.value"), - "Monolithic value should be used if the segment doesn't have it's own." + ls.openSegment(b).get("test.value"), + "Global value should be used if the segment doesn't have it's own." ).to.equal(testValueA); }); it("Set and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); - ls.set(7, a, "test.value", testValueA); - ls.set(7, b, "test.value", testValueB); - ls.set(7, c, "test.value", testValueC); + ls.openSegment(a).set(7, "test.value", testValueA); + ls.openSegment(b).set(7, "test.value", testValueB); + ls.openSegment(c).set(7, "test.value", testValueC); expect( - ls.get("test.value"), + ls.global.get("test.value"), "Only segmented values were set, this should be undefined." ).to.be.undefined; expect( - ls.get(a, "test.value"), + ls.openSegment(a).get("test.value"), "The A segment should return A test value." ).to.equal(testValueA); expect( - ls.get(b, "test.value"), + ls.openSegment(b).get("test.value"), "The B segment should return B test value." ).to.equal(testValueB); expect( - ls.get(c, "test.value"), + ls.openSegment(c).get("test.value"), "The C segment should return C test value." ).to.equal(testValueC); }); it("Set and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); - ls.set(7, b, "test.value", testValueB); + ls.openSegment(b).set(7, "test.value", testValueB); - expect(ls.has("test.value"), "Only B segment was set and should be true.") - .to.be.false; + expect( + ls.global.has("test.value"), + "Only B segment was set and should be true." + ).to.be.false; expect( - ls.has(a, "test.value"), + ls.openSegment(a).has("test.value"), "Only B segment was set and should be true." ).to.be.false; expect( - ls.has(b, "test.value"), + ls.openSegment(b).has("test.value"), "Only B segment was set and should be true." ).to.be.true; expect( - ls.has(c, "test.value"), + ls.openSegment(c).has("test.value"), "Only B segment was set and should be true." ).to.be.false; }); it("Set, delete and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); expect( - ls.get(c, "test.value"), + ls.openSegment(c).get("test.value"), "There is no value yet so it should be undefined." ).to.be.undefined; - ls.set(7, c, "test.value", testValueC); + ls.openSegment(c).set(7, "test.value", testValueC); expect( - ls.get(c, "test.value"), + ls.openSegment(c).get("test.value"), "Layer 7 segment C has a value that should be returned." ).to.equal(testValueC); - ls.delete(7, c, "test.value"); + ls.openSegment(c).delete(7, "test.value"); expect( - ls.get(c, "test.value"), + ls.openSegment(c).get("test.value"), "There isn't any value anymore so it should be undefined." ).to.be.undefined; }); it("Set, delete and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<7, KV, keyof KV>(); expect( - ls.get(c, "test.value"), + ls.openSegment(c).get("test.value"), "There is no value yet so it should be undefined." ).to.be.undefined; - ls.set(7, c, "test.value", testValueC); + ls.openSegment(c).set(7, "test.value", testValueC); expect( - ls.has(c, "test.value"), + ls.openSegment(c).has("test.value"), "Layer 7 segment C has a value therefore it should return true." ).to.be.true; - ls.delete(7, c, "test.value"); + ls.openSegment(c).delete(7, "test.value"); expect( - ls.has(c, "test.value"), + ls.openSegment(c).has("test.value"), "There isn't any value anymore so it should return false." ).to.be.false; }); @@ -146,10 +148,11 @@ export function segmentedLayer(): void { [undefined, null, "string", true, false, {}].forEach( (layer: any): void => { it("" + layer, function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); expect( - (): void => void ls.set(layer, b, "test.value", testValueB), + (): void => + void ls.openSegment(b).set(layer, "test.value", testValueB), "Layers have to be ordered which is only possible with numbers as that's the only thing that has universally accepted indisputable order." ).to.throw(); }); diff --git a/test/layered-storage/single-layer.ts b/test/layered-storage/single-layer.ts index 165f2432..2dafaeda 100644 --- a/test/layered-storage/single-layer.ts +++ b/test/layered-storage/single-layer.ts @@ -7,7 +7,7 @@ interface KV { } /** - * Test that values can be set and retrieved from single monolithic layer. + * Test that values can be set and retrieved from single global layer. */ export function singleLayer(): void { describe("Single layer", function(): void { @@ -17,63 +17,63 @@ export function singleLayer(): void { }); it("Set and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); - ls.set(0, "test.value", testValue); + ls.global.set(0, "test.value", testValue); expect( - ls.get("test.value"), + ls.global.get("test.value"), "The same value that was set should be returned." ).to.equal(testValue); }); it("Set and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); - ls.set(0, "test.value", testValue); + ls.global.set(0, "test.value", testValue); expect( - ls.has("test.value"), + ls.global.has("test.value"), "The value should be reported as present after being set." ).to.be.true; }); it("Set, delete and get", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); expect( - ls.get("test.value"), + ls.global.get("test.value"), "There is no value yet so it should be undefined." ).to.be.undefined; - ls.set(0, "test.value", testValue); + ls.global.set(0, "test.value", testValue); expect( - ls.get("test.value"), + ls.global.get("test.value"), "The same value that was set should be returned." ).to.equal(testValue); - ls.delete(0, "test.value"); + ls.global.delete(0, "test.value"); expect( - ls.get("test.value"), + ls.global.get("test.value"), "Undefined should be returned for deleted values." ).to.be.undefined; }); it("Set, delete and has", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); expect( - ls.has("test.value"), + ls.global.has("test.value"), "There is no value yet so it should be reported as empty." ).to.be.false; - ls.set(0, "test.value", testValue); + ls.global.set(0, "test.value", testValue); expect( - ls.has("test.value"), + ls.global.has("test.value"), "The value should be reported as present after being set." ).to.be.true; - ls.delete(0, "test.value"); + ls.global.delete(0, "test.value"); expect( - ls.has("test.value"), + ls.global.has("test.value"), "The value should be reported as not present after being deleted." ).to.be.false; }); @@ -82,10 +82,10 @@ export function singleLayer(): void { [undefined, null, "string", true, false, {}].forEach( (layer: any): void => { it("" + layer, function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); expect( - (): void => void ls.set(layer, "test.value", testValue), + (): void => void ls.global.set(layer, "test.value", testValue), "Layers have to be ordered which is only possible with numbers as that's the only thing that has universally accepted indisputable order." ).to.throw(); }); diff --git a/test/layered-storage/validation.ts b/test/layered-storage/validation.ts index 4a812a4e..3c2e21a8 100644 --- a/test/layered-storage/validation.ts +++ b/test/layered-storage/validation.ts @@ -3,15 +3,15 @@ import { expect } from "chai"; interface KV { "test.boolean": boolean; - "test.fail": unknown; + "test.fail": any; "test.integer": number; "test.number": number; - "test.pass": unknown; + "test.pass": any; "test.string": string; } /** - * Test that values can be set and retrieved from single monolithic layer. + * Test that values can be set and retrieved from single global layer. */ export function validation(): void { describe("Validation", function(): void { @@ -44,7 +44,7 @@ export function validation(): void { [true, "test.string", "test"] as const ].forEach(([valid, key, value]): void => { it(`${key}: ${value}`, function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); // Add handler. if (handler != null) { @@ -79,27 +79,27 @@ export function validation(): void { if (valid) { expect((): void => { - ls.set(0, key, value); + ls.global.set(0, key, value); }, "No error should be thrown for valid values.").to.not.throw(); expect( - ls.get(key), + ls.global.get(key), "Valid values should be saved in the storage." ).to.equal(value); } else { if (throws != null) { expect((): void => { - ls.set(0, key, value); + ls.global.set(0, key, value); }, "If the handler throws the set should throw too.").to.throw( TypeError, `${key}: ${value} (it's not valid)` ); } else { expect((): void => { - ls.set(0, key, value); + ls.global.set(0, key, value); }, "If the handler doesn't throw neither should the set.").to.not.throw(); } expect( - ls.has(key), + ls.global.has(key), "Invalid values should not be saved in the storage." ).to.be.false; } @@ -109,7 +109,7 @@ export function validation(): void { }); it("Setting validators twice", function(): void { - const ls = new LayeredStorage(); + const ls = new LayeredStorage<0, KV, keyof KV>(); expect((): void => { ls.setValidators("test.fail", [