Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce @wry/key-set-map package #513

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/key-set-map/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# Ignore generated TypeScript files.
lib

# Cache for rollup-plugin-typescript2
.rpt2_cache
3 changes: 3 additions & 0 deletions packages/key-set-map/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
/lib/tests
tsconfig.json
54 changes: 54 additions & 0 deletions packages/key-set-map/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# @wry/key-set-map

Whereas the `@wry/trie` package associates values with _sequences_ of keys, the
`@wry/key-set-map` package and its `KeySetMap` class provide a similar
capability for _sets_ of keys, so the order of the input keys is no longer
important.

As with a traditional [Trie](https://en.wikipedia.org/wiki/Trie), lookups and
insertions take linear time in the size of the input set, and peek-like
operations can often bail out much more quickly, without having to look at all
the input set elements.

Since JavaScript `Set` and `Map` containers maintain insertion order, two
equivalent sets (containing identical elements) can nevertheless be detectably
different if their keys were inserted in a different order. Deciding which of
the orders is "correct" or "canonical" is a fool's errand, possible only when
there is an inherent total ordering among the elements, suggesting a
sorting-based strategy.

Because sorting is tempting as a strategy for turning sets into
canonically-ordered sequences, it's important to stress: this implementation
works without sorting set elements, and without requiring the elements to be
comparable. In fact, the lookup algorithm is asymptotically faster than it would
be if the keys had to be sorted before lookup.

Finally, to avoid taking any position on which ordering of elements is
canonical, this implementation never grants direct access to any previously
provided sets. Instead of attempting to return a canonical `Set`, the keys of
the set are associated with an arbitrary `TData` value, which is all you get
when you look up a set of keys.

## Memory management

When `WeakRef` and `FinalizationRegistry` are available, the `KeySetMap` class
automatically reclaims internal memory associated with sets containing keys that
have been garbage collected.

To that end, when keys can be garbage collected, the `KeySetMap` takes care not
to retain them strongly, acting like a `WeakMap` for object keys and like a
`Map` for non-object keys. In other words, `KeySetMap` does not prevent its
(object) keys from being garbage collected, if they are otherwise eligible.

By passing `false` for the `weakness` parameter to the `KeySetMap` constructor,
you can disable weak-key-related functionality, so the `KeySetMap` will behave
like a `Map` for all keys, regardless of whether they are objects or primitive.
This mode is not encouraged for production, but may be useful for testing,
debugging, or other diagnostic purposes.

Any `TData` objects allocated by the `KeySetMap` may outlive their associated
sets of keys, and retaining a strong reference to the `TData` object by itself
does not prevent garbage collection and removal of object keys. However, as long
as all keys remain reachable and are not removed from the `KeySetMap` with
`remove` or `removeSet`, the set of keys will remain in the `KeySetMap` and thus
retain a reference to the associated `TData`.
31 changes: 31 additions & 0 deletions packages/key-set-map/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions packages/key-set-map/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@wry/key-set-map",
"private": true,
"version": "0.1.0",
"author": "Ben Newman <[email protected]>",
"description": "Trie-like data structure using sets rather than sequences of keys",
"license": "MIT",
"type": "module",
"main": "lib/bundle.cjs",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [],
"homepage": "https://github.com/benjamn/wryware",
"repository": {
"type": "git",
"url": "git+https://github.com/benjamn/wryware.git"
},
"bugs": {
"url": "https://github.com/benjamn/wryware/issues"
},
"scripts": {
"build": "npm run clean && npm run tsc && npm run rollup",
"clean": "rimraf lib",
"tsc": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"prepare": "npm run build",
"test:cjs": "../../shared/test.sh lib/tests/bundle.cjs",
"test:esm": "../../shared/test.sh lib/tests/bundle.js",
"test": "npm run test:esm && npm run test:cjs"
},
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">=8"
}
}
22 changes: 22 additions & 0 deletions packages/key-set-map/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { build } from "../../shared/rollup.config.js";

// This package doesn't use the lib/es5 directory, so we need to override the
// default export from ../../shared/rollup.config.js (as we also do in the
// @wry/equality package).
export default [
build(
"lib/index.js",
"lib/bundle.cjs",
"cjs"
),
build(
"lib/tests/main.js",
"lib/tests/bundle.js",
"esm"
),
build(
"lib/tests/main.js",
"lib/tests/bundle.cjs",
"cjs"
),
];
48 changes: 48 additions & 0 deletions packages/key-set-map/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const arrayForEach = Array.prototype.forEach;

export const {
prototype: {
hasOwnProperty,
toString: objectToString,
},
} = Object;

// If no makeData function is supplied, the looked-up data will be an empty,
// null-prototype Object.
export function defaultMakeData(): any {
return Object.create(null);
}

export function isObjRef(value: any): value is object {
if (value) {
switch (typeof value) {
case "object":
case "function":
return true;
}
}
return false;
}

const SET_TO_STRING_TAG = objectToString.call(new Set);

export function assertSet(set: any): asserts set is Set<any> {
const toStringTag = objectToString.call(set);
if (toStringTag !== SET_TO_STRING_TAG) {
throw new TypeError(`Not a Set: ${toStringTag}`);
}
}

const KNOWN: unique symbol = Symbol("KeySetMap.KNOWN");

export function makeKnownWeakRef<T extends object>(key: T): WeakRef<T> {
return Object.assign(new WeakRef(key), { [KNOWN]: true });
}

export function isKnownWeakRef(ref: unknown): ref is WeakRef<object> {
return (
ref instanceof WeakRef &&
KNOWN in ref &&
ref[KNOWN] === true
);
}
Loading