Fix StartOS 0.4 TypeScript packaging to match SDK API

This commit is contained in:
MacPro
2026-04-09 15:10:44 -05:00
parent 68ec875ee7
commit 8298c083c7
3436 changed files with 867051 additions and 92 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 adamhamlin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+188
View File
@@ -0,0 +1,188 @@
# Deep Equality Javascript Data Structures
[![npm version](https://badge.fury.io/js/deep-equality-data-structures.svg)](https://badge.fury.io/js/deep-equality-data-structures)
[![CI Status Badge](https://github.com/adamhamlin/deep-equality-data-structures/actions/workflows/ci.yaml/badge.svg)](https://github.com/adamhamlin/deep-equality-data-structures/actions/workflows/ci.yaml)
A drop-in replacement for ES native `Map` and `Set` with deep equality support for objects.
## Install
```bash
npm install deep-equality-data-structures
```
## Why?
ES `Map` and `Set` only support referential equality:
```typescript
interface MyType {
a: number;
}
const set = new Set<MyType>();
set.add({ a: 1 });
set.add({ a: 1 });
set.size; // 2
```
Now, using deep equality:
```typescript
import { DeepSet } from 'deep-equality-data-structures';
interface MyType {
a: number;
}
const set = new DeepSet<MyType>();
set.add({ a: 1 });
set.add({ a: 1 });
set.size; // 1
```
## How?
This project relies on the [object-hash](https://github.com/puleos/object-hash) library to normalize object types to unique strings.
## Comparable Interface
The following supplemental comparisons/methods are included:
- `equals`
- `contains`
- `union`
- `intersection`
- `difference`
```typescript
// COMPARISONS
const set1 = new DeepSet([{ a: 1 }, { b: 2 }]);
const set2 = new DeepSet([{ b: 2 }, { a: 1 }]);
set1.equals(set2); // true
const set3 = new DeepSet([{ a: 1 }]);
set1.equals(set3); // false
set1.contains(set3); // true
// SET OPERATIONS (available for maps, too)
const set3 = new DeepSet([{ a: 1 }, { b: 2 }]);
const set4 = new DeepSet([{ b: 2 }, { c: 3 }]);
set3.union(set4); // DeepSet([{ a: 1 }, { b: 2 }, { c: 3 }])
set3.intersection(set4); // DeepSet([{ b: 2 }])
set3.difference(set4); // DeepSet([{ a: 1 }])
```
## Configuration Options
The default settings should be suitable for most use cases, but behavior can be configured.
```typescript
new DeepSet<V>(values?, options?)
new DeepMap<K,V>(entries?, options?)
```
The `options` argument is a superset of the options defined for [object-hash](https://github.com/puleos/object-hash#hashvalue-options), with the same defaults (exception: the default algoritm is `md5`). There are also library-specific options.
### Library-specific options:
- `transformer` - a custom function that transforms Map keys/Set values prior to hashing. It does not affect the values that are stored.
```typescript
type MyType = { val: number; other: number };
const a: MyType = { val: 1, other: 1 };
const b: MyType = { val: 1, other: 2 };
const transformer = (obj: MyType) => ({ val: obj.val });
const set = new DeepSet([a, b]);
set.size; // 2
const set = new DeepSet([a, b], { transformer });
set.size; // 1
[...set.values()]; // [{ val: 1, other: 2 }]
```
- `mapValueTransformer` - a custom function that transforms Map values prior to hashing. This is only relevant to the `.equals`/`.contains` operations from the [Comparable interface](#comparable-interface), as well as the [Bi-Directional DeepMap](#bi-directional-deepmap). It does not affect the values that are stored.
```typescript
type MyType = { val: number; other: number };
const a: MyType = { val: 1, other: 1 };
const b: MyType = { val: 1, other: 2 };
const mapValueTransformer = (obj: MyType) => ({ val: obj.val });
const map1 = new DeepMap([[1, a]]);
const map2 = new DeepMap([[1, b]]);
map1.equals(map2); // false
const map1 = new DeepMap([[1, a]], { mapValueTransformer });
const map2 = new DeepMap([[1, b]], { mapValueTransformer });
map1.equals(map2); // true
[...map1.entries()]; // [[1, { val: 1, other: 1 }]]
[...map2.entries()]; // [[1, { val: 1, other: 2 }]]
```
- `useToJsonTransform` - if true, only use JSON-serializable properties when computing hashes, equality, etc. (default: false)
> _NOTE: This transform will always be applied BEFORE `transformer` and `mapValueTransformer`, if applicable._
```typescript
class A {
constructor(public x: number) {}
}
class B {
constructor(public x: number) {}
}
const a = new A(45);
const b = new B(45);
const set = new DeepSet([a, b]);
set.size; // 2
const set = new DeepSet([a, b], { useToJsonTransform: true });
set.size; // 1
```
- `caseInsensitive` - If true, all string values--including keys/values within objects and arrays--will be evaluated as case-insensitive. (default: false)
> _NOTE: This transform will always be applied AFTER `transformer` and `mapValueTransformer`, if applicable. For objects, it will be applied before `replacer` (from object-hash options)._
```typescript
const a = { key: 'value' };
const b = { key: 'VALUE' };
const set = new DeepSet([a, b]);
set.size; // 2
const set = new DeepSet([a, b], { caseInsensitive: true });
set.size; // 1
```
## Bi-Directional DeepMap
This library also exposes a `BiDirectionalDeepMap` class, which supports O(1) lookups by both keys and values. It provides the following extended API:
- _`hasValue(val: V): boolean`_: Returns true if `val` exists as a value in the map
- _`getKeyByValue(val: V): K | undefined`_: Returns the key associated with `val` if it exists
- _`deleteByValue(val: V): boolean`_: Removes the key-value pair whose value is `val` and returns true if found
### Caveats
Note that this "two-way" map has the traditional caveats:
- There is a ~2x memory footprint
- Keys and values must be 1-to-1, meaning each key must have a distinct value and vice versa. This implementation will error if attempting to set a key-value pair whose _value_ is already present in the map with a different _key_.
## Static Utility Methods
- _`areEqual(values, options?)`_: Returns true if all elements in `values` are equal. This can be useful when you need to quickly
test equality of more than 2 values, or when you want to specify an equality transform (via `options.transformer`).
## Notes/Caveats
- This still supports primitive keys/values like traditional `Map`/`Set`.
- Don't mutate objects stored in the data structure. The internal representation is not affected by this mutation, so behavior may be unexpected.
- Don't mutate objects in the user-supplied `transformer` or `mapValueTransformer` functions. It will affect the stored version.
- This implementation does not explicitly "handle" key collisions. However, with the default algorithm (MD5), even if a map contained one TRILLION entries, the probability of a collision on the next insert is only 0.000000000000001. If you need better odds, use SHA1, SHA256, etc.
## CI/CD
Using Github Actions, the CI build will run on all pull requests and pushes/merges to main.
This project uses [Conventional Commits](https://www.conventionalcommits.org/) and [standard-version](https://github.com/conventional-changelog/standard-version) to facilitate versioning and changelogs.
+5
View File
@@ -0,0 +1,5 @@
export { DeepSet } from './src/set';
export { DeepMap } from './src/map';
export { BiDirectionalDeepMap } from './src/map.bi-directional';
export { Options } from './src/options';
export { areEqual } from './src/areEqual';
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.areEqual = exports.BiDirectionalDeepMap = exports.DeepMap = exports.DeepSet = void 0;
var set_1 = require("./src/set");
Object.defineProperty(exports, "DeepSet", { enumerable: true, get: function () { return set_1.DeepSet; } });
var map_1 = require("./src/map");
Object.defineProperty(exports, "DeepMap", { enumerable: true, get: function () { return map_1.DeepMap; } });
var map_bi_directional_1 = require("./src/map.bi-directional");
Object.defineProperty(exports, "BiDirectionalDeepMap", { enumerable: true, get: function () { return map_bi_directional_1.BiDirectionalDeepMap; } });
var areEqual_1 = require("./src/areEqual");
Object.defineProperty(exports, "areEqual", { enumerable: true, get: function () { return areEqual_1.areEqual; } });
+9
View File
@@ -0,0 +1,9 @@
import { Options } from './options';
/**
* Static utility for doing a one-time equality check across the provided values.
* @param values list whose elements will be compared to each other
* @param options configuration options
* @returns true if every element in `values` is equal to every other element
* @throws {Error} if `values` list is empty
*/
export declare function areEqual<V, TxV = V>(values: V[], options?: Options<V, null, TxV, null>): boolean;
+19
View File
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.areEqual = areEqual;
const errors_1 = require("./errors");
const set_1 = require("./set");
/**
* Static utility for doing a one-time equality check across the provided values.
* @param values list whose elements will be compared to each other
* @param options configuration options
* @returns true if every element in `values` is equal to every other element
* @throws {Error} if `values` list is empty
*/
function areEqual(values, options) {
if (values.length === 0) {
throw new errors_1.DeepEqualityDataStructuresError('Empty values list passed to areEqual function');
}
const set = new set_1.DeepSet(values, options);
return set.size === 1;
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Interface for comparable maps/sets
*/
export interface Comparable<T> {
equals(other: T): boolean;
contains(other: T): boolean;
union(other: T): T;
intersection(other: T): T;
difference(other: T): T;
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+2
View File
@@ -0,0 +1,2 @@
export declare class DeepEqualityDataStructuresError extends Error {
}
+6
View File
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeepEqualityDataStructuresError = void 0;
class DeepEqualityDataStructuresError extends Error {
}
exports.DeepEqualityDataStructuresError = DeepEqualityDataStructuresError;
@@ -0,0 +1,38 @@
import { DeepMap } from './map';
import { Options } from './options';
/**
* A DeepMap implementation that supports O(1) lookups by both keys and values
* NOTE: All key-value pairs must be 1-to-1
*/
export declare class BiDirectionalDeepMap<K, V, TxK = K, TxV = V> extends DeepMap<K, V, TxK, TxV> {
private readonly valueMap;
/**
* @param entries optional list of key-value pairs to initialize the map
* @param options configuration options
*/
constructor(entries?: readonly (readonly [K, V])[] | null, options?: Options<K, V, TxK, TxV>);
/**
* @inheritdoc
*/
set(key: K, val: V): this;
/**
* @inheritdoc
*/
delete(key: K): boolean;
/**
* @inheritdoc
*/
clear(): void;
/**
* @returns true if the given value is present in the key-value map.
*/
hasValue(val: V): boolean;
/**
* @returns the key associated with the specified value
*/
getKeyByValue(val: V): K | undefined;
/**
* @returns true if a value in the map existed and has been removed, else false
*/
deleteByValue(val: V): boolean;
}
@@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BiDirectionalDeepMap = void 0;
const errors_1 = require("./errors");
const map_1 = require("./map");
const utils_1 = require("./utils");
/**
* A DeepMap implementation that supports O(1) lookups by both keys and values
* NOTE: All key-value pairs must be 1-to-1
*/
class BiDirectionalDeepMap extends map_1.DeepMap {
/**
* @param entries optional list of key-value pairs to initialize the map
* @param options configuration options
*/
constructor(entries, options) {
super(entries, options);
const valueEntries = entries ? entries.map(([key, val]) => [this.normalizeValue(val), key]) : null;
this.valueMap = new Map(valueEntries);
}
/**
* @inheritdoc
*/
set(key, val) {
// Enforce 1-to-1: Don't allow writing a value which is already present in the map for a different key
const preexistingValueKey = this.getKeyByValue(val);
if (preexistingValueKey !== undefined && this.normalizeKey(preexistingValueKey) !== this.normalizeKey(key)) {
throw new errors_1.DeepEqualityDataStructuresError(`Could not set key='${(0, utils_1.stringify)(key)}': The value='${(0, utils_1.stringify)(val)}' is already associated with key='${(0, utils_1.stringify)(preexistingValueKey)}'`);
}
this.valueMap.set(this.normalizeValue(val), key);
return super.set(key, val);
}
/**
* @inheritdoc
*/
delete(key) {
const val = this.get(key);
if (val) {
this.valueMap.delete(this.normalizeValue(val));
}
return super.delete(key);
}
/**
* @inheritdoc
*/
clear() {
this.valueMap.clear();
super.clear();
}
// BI-DIRECTIONAL API
/**
* @returns true if the given value is present in the key-value map.
*/
hasValue(val) {
return this.valueMap.has(this.normalizeValue(val));
}
/**
* @returns the key associated with the specified value
*/
getKeyByValue(val) {
return this.valueMap.get(this.normalizeValue(val));
}
/**
* @returns true if a value in the map existed and has been removed, else false
*/
deleteByValue(val) {
const key = this.getKeyByValue(val);
return key ? this.delete(key) : false;
}
}
exports.BiDirectionalDeepMap = BiDirectionalDeepMap;
+99
View File
@@ -0,0 +1,99 @@
import { Comparable } from './comparable';
import { Normalized } from './normalizer';
import { Options } from './options';
/**
* A Map implementation that supports deep equality for object keys.
*/
export declare class DeepMap<K, V, TxK = K, TxV = V> extends Map<K, V> implements Comparable<DeepMap<K, V, TxK, TxV>> {
private options;
private readonly normalizer;
private readonly map;
/**
* @param entries optional list of key-value pairs to initialize the map
* @param options configuration options
*/
constructor(entries?: readonly (readonly [K, V])[] | null, options?: Options<K, V, TxK, TxV>);
/**
* Getter for number of kev-value pairs in the map.
* @inheritdoc
*/
get size(): number;
/**
* Returns true if the given key is present in the map.
* @inheritdoc
*/
has(key: K): boolean;
/**
* @inheritdoc
*/
set(key: K, val: V): this;
/**
* @inheritdoc
*/
get(key: K): V | undefined;
/**
* @inheritdoc
*/
delete(key: K): boolean;
/**
* Clear all key-value pairs from the map.
* @inheritdoc
*/
clear(): void;
/**
* @inheritdoc
*/
forEach(callbackfn: (val: V, key: K, map: Map<K, V>) => void): void;
/**
* @yields the next key-value pair in the map
* @inheritdoc
*/
[Symbol.iterator](): IterableIterator<[K, V]>;
/**
* @yields the next key-value pair in the map
* @inheritdoc
*/
entries(): IterableIterator<[K, V]>;
/**
* @inheritdoc
*/
keys(): IterableIterator<K>;
/**
* @inheritdoc
*/
values(): IterableIterator<V>;
/**
* @param other the map to compare against
* @returns true if the entries of `other` are the same as this map
*/
equals(other: this): boolean;
/**
* @param other the map to compare against
* @returns true if the entries of `other` are all contained in this map
*/
contains(other: this): boolean;
/**
* @param other the map to compare against
* @returns a new map whose keys are the union of keys between `this` and `other` maps.
*
* NOTE: If both maps prescribe the same key, the key-value pair from `this` will be retained.
*/
union(other: this): DeepMap<K, V, TxK, TxV>;
/**
* @param other the map to compare against
* @returns a new map containing all key-value pairs in `this` that are also present in `other`.
*/
intersection(other: this): DeepMap<K, V, TxK, TxV>;
/**
* @param other the map to compare against
* @returns a new map containing all key-value pairs in `this` that are not present in `other`.
*/
difference(other: this): DeepMap<K, V, TxK, TxV>;
protected normalizeKey(input: K): Normalized<TxK>;
protected normalizeValue(input: V): Normalized<TxV>;
private validateUsingSameOptionsAs;
/**
* @returns true if the key is present in the provided map w/ the specified value
*/
private keyValuePairIsPresentIn;
}
+171
View File
@@ -0,0 +1,171 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeepMap = void 0;
const errors_1 = require("./errors");
const normalizer_1 = require("./normalizer");
/**
* A Map implementation that supports deep equality for object keys.
*/
class DeepMap extends Map {
// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
/**
* @param entries optional list of key-value pairs to initialize the map
* @param options configuration options
*/
constructor(entries, options = {}) {
super();
this.options = options;
this.normalizer = new normalizer_1.Normalizer(options);
const transformedEntries = entries
? entries.map(([key, val]) => [this.normalizeKey(key), { key, val }])
: null;
this.map = new Map(transformedEntries);
}
/**
* Getter for number of kev-value pairs in the map.
* @inheritdoc
*/
get size() {
return this.map.size;
}
/**
* Returns true if the given key is present in the map.
* @inheritdoc
*/
has(key) {
return this.map.has(this.normalizeKey(key));
}
/**
* @inheritdoc
*/
set(key, val) {
this.map.set(this.normalizeKey(key), { key, val });
return this;
}
/**
* @inheritdoc
*/
get(key) {
var _a;
return (_a = this.map.get(this.normalizeKey(key))) === null || _a === void 0 ? void 0 : _a.val;
}
/**
* @inheritdoc
*/
delete(key) {
return this.map.delete(this.normalizeKey(key));
}
/**
* Clear all key-value pairs from the map.
* @inheritdoc
*/
clear() {
this.map.clear();
}
/**
* @inheritdoc
*/
forEach(callbackfn) {
this.map.forEach((pair, _key, _internalMap) => {
callbackfn(pair.val, pair.key, this);
});
}
/**
* @yields the next key-value pair in the map
* @inheritdoc
*/
*[Symbol.iterator]() {
for (const [_hashStr, pair] of this.map[Symbol.iterator]()) {
yield [pair.key, pair.val];
}
}
/**
* @yields the next key-value pair in the map
* @inheritdoc
*/
*entries() {
for (const entry of this[Symbol.iterator]()) {
yield entry;
}
}
/**
* @inheritdoc
*/
*keys() {
for (const [key, _val] of this[Symbol.iterator]()) {
yield key;
}
}
/**
* @inheritdoc
*/
*values() {
for (const [_key, val] of this[Symbol.iterator]()) {
yield val;
}
}
/**
* @param other the map to compare against
* @returns true if the entries of `other` are the same as this map
*/
equals(other) {
this.validateUsingSameOptionsAs(other);
return this.size === other.size && this.contains(other);
}
/**
* @param other the map to compare against
* @returns true if the entries of `other` are all contained in this map
*/
contains(other) {
this.validateUsingSameOptionsAs(other);
return [...other.entries()].every(([key, val]) => this.keyValuePairIsPresentIn(key, val, this));
}
/**
* @param other the map to compare against
* @returns a new map whose keys are the union of keys between `this` and `other` maps.
*
* NOTE: If both maps prescribe the same key, the key-value pair from `this` will be retained.
*/
union(other) {
this.validateUsingSameOptionsAs(other);
return new DeepMap([...other.entries(), ...this.entries()], this.options);
}
/**
* @param other the map to compare against
* @returns a new map containing all key-value pairs in `this` that are also present in `other`.
*/
intersection(other) {
this.validateUsingSameOptionsAs(other);
const intersectingPairs = [...this.entries()].filter(([key, val]) => this.keyValuePairIsPresentIn(key, val, other));
return new DeepMap(intersectingPairs, this.options);
}
/**
* @param other the map to compare against
* @returns a new map containing all key-value pairs in `this` that are not present in `other`.
*/
difference(other) {
this.validateUsingSameOptionsAs(other);
const differencePairs = [...this.entries()].filter(([key, val]) => !this.keyValuePairIsPresentIn(key, val, other));
return new DeepMap(differencePairs, this.options);
}
// PRIVATE/PROTECTED METHODS FOLLOW...
normalizeKey(input) {
return this.normalizer.normalizeKey(input);
}
normalizeValue(input) {
return this.normalizer.normalizeValue(input);
}
validateUsingSameOptionsAs(other) {
if (this.normalizer.getOptionsChecksum() !== other['normalizer'].getOptionsChecksum()) {
throw new errors_1.DeepEqualityDataStructuresError('Structures must use same options for Comparable interface operations');
}
}
/**
* @returns true if the key is present in the provided map w/ the specified value
*/
keyValuePairIsPresentIn(key, val, mapToCheck) {
const checkVal = mapToCheck.get(key);
return checkVal !== undefined && this.normalizeValue(checkVal) === this.normalizeValue(val);
}
}
exports.DeepMap = DeepMap;
+42
View File
@@ -0,0 +1,42 @@
import { Options } from './options';
/**
* Result of object-hash hashing function
*/
type HashedObject = string;
/**
* Type for normalized input.
*/
export type Normalized<T> = HashedObject | T;
/**
* Class that normalizes object types to strings via hashing
*/
export declare class Normalizer<K, V, TxK, TxV> {
private readonly objectHashOptions;
private readonly caseInsensitive;
private readonly keyTransformer;
private readonly valueTransformer;
private readonly optionsChecksum;
constructor(options?: Options<K, V, TxK, TxV>);
/**
* @returns the checksum for the options passed to this Normalizer
*/
getOptionsChecksum(): string;
/**
* Normalize the input by transforming and then hashing the result (if an object)
* @param input the input to normalize
* @returns the normalized result
*/
normalizeKey(input: K): Normalized<TxK>;
/**
* Normalize the input by transforming and then hashing the result (if an object)
* @param input the input to normalize
* @returns the normalized result
*/
normalizeValue(input: V): Normalized<TxV>;
private normalizeHelper;
/**
* Returns true if the input is a javascript object.
*/
private static isObject;
}
export {};
+88
View File
@@ -0,0 +1,88 @@
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Normalizer = void 0;
const object_hash_1 = __importDefault(require("object-hash"));
const options_1 = require("./options");
const transformers_1 = require("./transformers");
const utils_1 = require("./utils");
/**
* Class that normalizes object types to strings via hashing
*/
class Normalizer {
constructor(options = {}) {
this.optionsChecksum = (0, object_hash_1.default)(options);
const _a = (0, options_1.getOptionsWithDefaults)(options), { transformer, mapValueTransformer, useToJsonTransform, caseInsensitive } = _a, objectHashOptions = __rest(_a, ["transformer", "mapValueTransformer", "useToJsonTransform", "caseInsensitive"]);
this.objectHashOptions = objectHashOptions;
this.caseInsensitive = caseInsensitive;
this.keyTransformer = useToJsonTransform
? (0, utils_1.chain)([transformers_1.Transformers.jsonSerializeDeserialize, transformer])
: transformer;
this.valueTransformer = useToJsonTransform
? (0, utils_1.chain)([transformers_1.Transformers.jsonSerializeDeserialize, mapValueTransformer])
: mapValueTransformer;
if (caseInsensitive) {
// NOTE: This block ensures case-insensitivity inside objects only.
// See normalizeHelper() for logic which handles primitive strings
const caseInsensitiveReplacer = (val) => typeof val === 'string' ? val.toLowerCase() : val;
const { replacer } = this.objectHashOptions;
this.objectHashOptions.replacer = replacer
? (0, utils_1.chain)([caseInsensitiveReplacer, replacer])
: caseInsensitiveReplacer;
}
}
/**
* @returns the checksum for the options passed to this Normalizer
*/
getOptionsChecksum() {
return this.optionsChecksum;
}
/**
* Normalize the input by transforming and then hashing the result (if an object)
* @param input the input to normalize
* @returns the normalized result
*/
normalizeKey(input) {
return this.normalizeHelper(this.keyTransformer(input));
}
/**
* Normalize the input by transforming and then hashing the result (if an object)
* @param input the input to normalize
* @returns the normalized result
*/
normalizeValue(input) {
return this.normalizeHelper(this.valueTransformer(input));
}
normalizeHelper(input) {
if (Normalizer.isObject(input)) {
return (0, object_hash_1.default)(input, this.objectHashOptions);
}
else if (this.caseInsensitive && typeof input === 'string') {
return input.toLowerCase();
}
else {
// Primitive value, don't hash
return input;
}
}
/**
* Returns true if the input is a javascript object.
*/
static isObject(input) {
return typeof input === 'object' && input !== null;
}
}
exports.Normalizer = Normalizer;
+34
View File
@@ -0,0 +1,34 @@
import { NormalOption as ObjectHashOptions } from 'object-hash';
import { TransformFunction } from './transformers';
import { Require } from './utils';
/**
* Library options
*/
interface DeepEqualityDataStructuresOptions<K, V, TxK, TxV> {
/**
* A function that transforms Map keys or Set values prior to normalization.
*
* NOTE: The caller is responsible for not mutating object inputs.
*/
transformer?: TransformFunction<K, TxK>;
/**
* A function that transforms Map values prior to normalization.
*
* NOTE: The caller is responsible for not mutating object inputs.
*/
mapValueTransformer?: TransformFunction<V, TxV>;
/**
* If true, objects will be JSON-serialized/deserialized into "plain" objects prior to hashing.
*/
useToJsonTransform?: boolean;
/**
* If true, all string values (including keys/values within objects and arrays) will use case-insensitive equality comparisons.
*/
caseInsensitive?: boolean;
}
export type Options<K, V, TxK, TxV> = ObjectHashOptions & DeepEqualityDataStructuresOptions<K, V, TxK, TxV>;
/**
* Given the specified options, resolve default values as appropriate.
*/
export declare function getOptionsWithDefaults<K, V, TxK, TxV>(options: Options<K, V, TxK, TxV>): Require<Options<K, V, TxK, TxV>, keyof DeepEqualityDataStructuresOptions<K, V, TxK, TxV>>;
export {};
+12
View File
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOptionsWithDefaults = getOptionsWithDefaults;
const transformers_1 = require("./transformers");
/**
* Given the specified options, resolve default values as appropriate.
*/
function getOptionsWithDefaults(options) {
return Object.assign({
// Default options
algorithm: 'md5', transformer: transformers_1.Transformers.identity, mapValueTransformer: transformers_1.Transformers.identity, useToJsonTransform: false, caseInsensitive: false }, options);
}
+83
View File
@@ -0,0 +1,83 @@
import { Comparable } from './comparable';
import { Options } from './options';
/**
* A Set implementation that supports deep equality for values.
*/
export declare class DeepSet<V, TxV = V> extends Set<V> implements Comparable<DeepSet<V, TxV>> {
private options?;
private readonly map;
/**
* @param values optional list of values to initialize the set
* @param options configuration options
*/
constructor(values?: readonly V[] | null, options?: Options<V, null, TxV, null> | undefined);
/**
* Getter for number of elements in the set.
* @inheritdoc
*/
get size(): number;
/**
* Returns true if the given value is present in the set.
* @inheritdoc
*/
has(val: V): boolean;
/**
* @inheritdoc
*/
add(val: V): this;
/**
* @inheritdoc
*/
delete(val: V): boolean;
/**
* Clear all values from the map.
* @inheritdoc
*/
clear(): void;
/**
* @inheritdoc
*/
forEach(callbackfn: (val: V, val2: V, set: Set<V>) => void): void;
/**
* @inheritdoc
*/
[Symbol.iterator](): IterableIterator<V>;
/**
* @inheritdoc
*/
entries(): IterableIterator<[V, V]>;
/**
* @inheritdoc
*/
keys(): IterableIterator<V>;
/**
* @inheritdoc
*/
values(): IterableIterator<V>;
/**
* @param other the set to compare against
* @returns true if the values of `other` are the same as this set
*/
equals(other: this): boolean;
/**
* @param other the set to compare against
* @returns true if the values of `other` are all contained in this set
*/
contains(other: this): boolean;
/**
* @param other the set to compare against
* @returns a new set whose values are the union of `this` and `other`.
*/
union(other: this): DeepSet<V, TxV>;
/**
* @param other the set to compare against
* @returns a new set containing all values in `this` that are also in `other`.
*/
intersection(other: this): DeepSet<V, TxV>;
/**
* @param other the set to compare against
* @returns a new set containing all values in `this` that are not also in `other`.
*/
difference(other: this): DeepSet<V, TxV>;
private getSetFromMapKeys;
}
+131
View File
@@ -0,0 +1,131 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeepSet = void 0;
const map_1 = require("./map");
/**
* A Set implementation that supports deep equality for values.
*/
class DeepSet extends Set {
// NOTE: This is actually a thin wrapper. We're not using super other than to drive the (typed) API contract.
/**
* @param values optional list of values to initialize the set
* @param options configuration options
*/
constructor(values, options) {
super();
this.options = options;
const transformedEntries = values ? values.map((el) => [el, null]) : null;
this.map = new map_1.DeepMap(transformedEntries, options);
}
/**
* Getter for number of elements in the set.
* @inheritdoc
*/
get size() {
return this.map.size;
}
/**
* Returns true if the given value is present in the set.
* @inheritdoc
*/
has(val) {
return this.map.has(val);
}
/**
* @inheritdoc
*/
add(val) {
this.map.set(val, null);
return this;
}
/**
* @inheritdoc
*/
delete(val) {
return this.map.delete(val);
}
/**
* Clear all values from the map.
* @inheritdoc
*/
clear() {
this.map.clear();
}
/**
* @inheritdoc
*/
forEach(callbackfn) {
this.map.forEach((_mapVal, mapKey, _map) => {
callbackfn(mapKey, mapKey, this);
});
}
/**
* @inheritdoc
*/
*[Symbol.iterator]() {
for (const [key, _val] of this.map[Symbol.iterator]()) {
yield key;
}
}
/**
* @inheritdoc
*/
*entries() {
for (const val of this[Symbol.iterator]()) {
yield [val, val];
}
}
/**
* @inheritdoc
*/
*keys() {
for (const val of this[Symbol.iterator]()) {
yield val;
}
}
/**
* @inheritdoc
*/
*values() {
yield* this.keys();
}
/**
* @param other the set to compare against
* @returns true if the values of `other` are the same as this set
*/
equals(other) {
return this.map.equals(other['map']);
}
/**
* @param other the set to compare against
* @returns true if the values of `other` are all contained in this set
*/
contains(other) {
return this.map.contains(other['map']);
}
/**
* @param other the set to compare against
* @returns a new set whose values are the union of `this` and `other`.
*/
union(other) {
return this.getSetFromMapKeys(this.map.union(other['map']));
}
/**
* @param other the set to compare against
* @returns a new set containing all values in `this` that are also in `other`.
*/
intersection(other) {
return this.getSetFromMapKeys(this.map.intersection(other['map']));
}
/**
* @param other the set to compare against
* @returns a new set containing all values in `this` that are not also in `other`.
*/
difference(other) {
return this.getSetFromMapKeys(this.map.difference(other['map']));
}
getSetFromMapKeys(map) {
return new DeepSet([...map.keys()], this.options);
}
}
exports.DeepSet = DeepSet;
@@ -0,0 +1,5 @@
export type TransformFunction<T, R> = (input: T) => R;
export declare class Transformers {
static identity<T, R = T>(input: T): R;
static jsonSerializeDeserialize<T, R = T>(obj: T): R;
}
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Transformers = void 0;
class Transformers {
static identity(input) {
// Just make the types happy :)
return input;
}
static jsonSerializeDeserialize(obj) {
return JSON.parse(JSON.stringify(obj));
}
}
exports.Transformers = Transformers;
+20
View File
@@ -0,0 +1,20 @@
/**
* Require the keys K from T if optional.
*/
export type Require<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
/**
* Format an unknown value to a string for display
*/
export declare function stringify(value: unknown): string;
/**
* Chain a list of functions
* @returns a function that accepts the args of the first function in the functions list. Each subsequent function is invoked with
* the return value of the previous function.
*/
export declare function chain<TArgs extends any[], T1>(functions: [(...args: TArgs) => T1]): (...args: TArgs) => T1;
export declare function chain<TArgs extends any[], T1, T2>(functions: [(...args: TArgs) => T1, (arg: T1) => T2]): (...args: TArgs) => T2;
export declare function chain<TArgs extends any[], T1, T2, T3>(functions: [(...args: TArgs) => T1, (arg: T1) => T2, (arg: T2) => T3]): (...args: TArgs) => T3;
export declare function chain<TArgs extends any[], T1, T2, T3, T4>(functions: [(...args: TArgs) => T1, (arg: T1) => T2, (arg: T2) => T3, (arg: T3) => T4]): (...args: TArgs) => T4;
export declare function chain<TArgs extends any[], T1, T2, T3, T4, T5>(functions: [(...args: TArgs) => T1, (arg: T1) => T2, (arg: T2) => T3, (arg: T3) => T4, (arg: T4) => T5]): (...args: TArgs) => T5;
+25
View File
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.stringify = stringify;
exports.chain = chain;
/**
* Format an unknown value to a string for display
*/
function stringify(value) {
if (value !== null && typeof value === 'object') {
// For objects, defer an overridden toString, otherwise use JSON.stringify
return 'toString' in value && ![Object.prototype.toString, Array.prototype.toString].includes(value.toString)
? value.toString()
: JSON.stringify(value);
}
else {
return String(value);
}
}
function chain(functions) {
const [head, ...tail] = functions;
return (...args) => {
return tail.reduce((acc, fn) => fn(acc), head(...args));
};
}
/* eslint-enable @typescript-eslint/no-explicit-any */
+61
View File
@@ -0,0 +1,61 @@
{
"name": "deep-equality-data-structures",
"version": "2.0.0",
"description": "Javascript data structures (e.g., Map, Set) that support deep object equality",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"prepare": "husky && npm run compile",
"_lint": "eslint --fix",
"_lint:check": "eslint",
"_format": " prettier --write --ignore-unknown",
"_format:check": "prettier --check --ignore-unknown",
"fix": "npm run _lint . && npm run _format .",
"check": "npm run _lint:check . && npm run _format:check .",
"compile": "tsc -p ./",
"watch": "npm run compile -- -watch",
"test": "jest",
"pretest:ci": "npm run check",
"test:ci": "npm test"
},
"lint-staged": {
"*.ts": "npm run _lint -- --cache",
"*": "npm run _format"
},
"repository": {
"type": "git",
"url": "git+https://github.com/adamhamlin/deep-equality-data-structures.git"
},
"keywords": [
"deep equality",
"deep",
"equality",
"map",
"set",
"data structure"
],
"author": "Adam C Hamlin <achamlin@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/adamhamlin/deep-equality-data-structures/issues"
},
"homepage": "https://github.com/adamhamlin/deep-equality-data-structures#readme",
"dependencies": {
"object-hash": "^3.0.0"
},
"devDependencies": {
"@adamhamlin/eslint-config": "^1.4.1",
"@tsconfig/recommended": "^1.0.7",
"@types/jest": "^29.5.12",
"@types/object-hash": "^3.0.6",
"husky": "^9.1.5",
"jest": "^29.7.0",
"lint-staged": "^15.2.10",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}