Initial Sample.

This commit is contained in:
2024-06-03 20:23:50 +05:30
parent ef2b65f673
commit 5269ec3c66
2575 changed files with 282312 additions and 0 deletions

21
graphql-subscription/node_modules/@wry/caches/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2021 Ben Newman <ben@eloper.dev>
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.

View File

@@ -0,0 +1,9 @@
# @wry/caches
Various cache implementations, including but not limited to
* `StrongCache`: A standard `Map`-like cache with a least-recently-used (LRU)
eviction policy and a callback hook for removed entries.
* `WeakCache`: Another LRU cache that holds its keys only weakly, so entries can be removed
once no longer retained elsewhere in the application.

View File

@@ -0,0 +1,245 @@
'use strict';
function defaultDispose$1() { }
var StrongCache = /** @class */ (function () {
function StrongCache(max, dispose) {
if (max === void 0) { max = Infinity; }
if (dispose === void 0) { dispose = defaultDispose$1; }
this.max = max;
this.dispose = dispose;
this.map = new Map();
this.newest = null;
this.oldest = null;
}
StrongCache.prototype.has = function (key) {
return this.map.has(key);
};
StrongCache.prototype.get = function (key) {
var node = this.getNode(key);
return node && node.value;
};
Object.defineProperty(StrongCache.prototype, "size", {
get: function () {
return this.map.size;
},
enumerable: false,
configurable: true
});
StrongCache.prototype.getNode = function (key) {
var node = this.map.get(key);
if (node && node !== this.newest) {
var older = node.older, newer = node.newer;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
};
StrongCache.prototype.set = function (key, value) {
var node = this.getNode(key);
if (node) {
return node.value = value;
}
node = {
key: key,
value: value,
newer: null,
older: this.newest
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.map.set(key, node);
return node.value;
};
StrongCache.prototype.clean = function () {
while (this.oldest && this.map.size > this.max) {
this.delete(this.oldest.key);
}
};
StrongCache.prototype.delete = function (key) {
var node = this.map.get(key);
if (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.map.delete(key);
this.dispose(node.value, key);
return true;
}
return false;
};
return StrongCache;
}());
function noop() { }
var defaultDispose = noop;
var _WeakRef = typeof WeakRef !== "undefined"
? WeakRef
: function (value) {
return { deref: function () { return value; } };
};
var _WeakMap = typeof WeakMap !== "undefined" ? WeakMap : Map;
var _FinalizationRegistry = typeof FinalizationRegistry !== "undefined"
? FinalizationRegistry
: function () {
return {
register: noop,
unregister: noop,
};
};
var finalizationBatchSize = 10024;
var WeakCache = /** @class */ (function () {
function WeakCache(max, dispose) {
if (max === void 0) { max = Infinity; }
if (dispose === void 0) { dispose = defaultDispose; }
var _this = this;
this.max = max;
this.dispose = dispose;
this.map = new _WeakMap();
this.newest = null;
this.oldest = null;
this.unfinalizedNodes = new Set();
this.finalizationScheduled = false;
this.size = 0;
this.finalize = function () {
var iterator = _this.unfinalizedNodes.values();
for (var i = 0; i < finalizationBatchSize; i++) {
var node = iterator.next().value;
if (!node)
break;
_this.unfinalizedNodes.delete(node);
var key = node.key;
delete node.key;
node.keyRef = new _WeakRef(key);
_this.registry.register(key, node, node);
}
if (_this.unfinalizedNodes.size > 0) {
queueMicrotask(_this.finalize);
}
else {
_this.finalizationScheduled = false;
}
};
this.registry = new _FinalizationRegistry(this.deleteNode.bind(this));
}
WeakCache.prototype.has = function (key) {
return this.map.has(key);
};
WeakCache.prototype.get = function (key) {
var node = this.getNode(key);
return node && node.value;
};
WeakCache.prototype.getNode = function (key) {
var node = this.map.get(key);
if (node && node !== this.newest) {
var older = node.older, newer = node.newer;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
};
WeakCache.prototype.set = function (key, value) {
var node = this.getNode(key);
if (node) {
return (node.value = value);
}
node = {
key: key,
value: value,
newer: null,
older: this.newest,
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.scheduleFinalization(node);
this.map.set(key, node);
this.size++;
return node.value;
};
WeakCache.prototype.clean = function () {
while (this.oldest && this.size > this.max) {
this.deleteNode(this.oldest);
}
};
WeakCache.prototype.deleteNode = function (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.size--;
var key = node.key || (node.keyRef && node.keyRef.deref());
this.dispose(node.value, key);
if (!node.keyRef) {
this.unfinalizedNodes.delete(node);
}
else {
this.registry.unregister(node);
}
if (key)
this.map.delete(key);
};
WeakCache.prototype.delete = function (key) {
var node = this.map.get(key);
if (node) {
this.deleteNode(node);
return true;
}
return false;
};
WeakCache.prototype.scheduleFinalization = function (node) {
this.unfinalizedNodes.add(node);
if (!this.finalizationScheduled) {
this.finalizationScheduled = true;
queueMicrotask(this.finalize);
}
};
return WeakCache;
}());
exports.StrongCache = StrongCache;
exports.WeakCache = WeakCache;
//# sourceMappingURL=bundle.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,245 @@
'use strict';
function defaultDispose$1() { }
var StrongCache = /** @class */ (function () {
function StrongCache(max, dispose) {
if (max === void 0) { max = Infinity; }
if (dispose === void 0) { dispose = defaultDispose$1; }
this.max = max;
this.dispose = dispose;
this.map = new Map();
this.newest = null;
this.oldest = null;
}
StrongCache.prototype.has = function (key) {
return this.map.has(key);
};
StrongCache.prototype.get = function (key) {
var node = this.getNode(key);
return node && node.value;
};
Object.defineProperty(StrongCache.prototype, "size", {
get: function () {
return this.map.size;
},
enumerable: false,
configurable: true
});
StrongCache.prototype.getNode = function (key) {
var node = this.map.get(key);
if (node && node !== this.newest) {
var older = node.older, newer = node.newer;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
};
StrongCache.prototype.set = function (key, value) {
var node = this.getNode(key);
if (node) {
return node.value = value;
}
node = {
key: key,
value: value,
newer: null,
older: this.newest
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.map.set(key, node);
return node.value;
};
StrongCache.prototype.clean = function () {
while (this.oldest && this.map.size > this.max) {
this.delete(this.oldest.key);
}
};
StrongCache.prototype.delete = function (key) {
var node = this.map.get(key);
if (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.map.delete(key);
this.dispose(node.value, key);
return true;
}
return false;
};
return StrongCache;
}());
function noop() { }
var defaultDispose = noop;
var _WeakRef = typeof WeakRef !== "undefined"
? WeakRef
: function (value) {
return { deref: function () { return value; } };
};
var _WeakMap = typeof WeakMap !== "undefined" ? WeakMap : Map;
var _FinalizationRegistry = typeof FinalizationRegistry !== "undefined"
? FinalizationRegistry
: function () {
return {
register: noop,
unregister: noop,
};
};
var finalizationBatchSize = 10024;
var WeakCache = /** @class */ (function () {
function WeakCache(max, dispose) {
if (max === void 0) { max = Infinity; }
if (dispose === void 0) { dispose = defaultDispose; }
var _this = this;
this.max = max;
this.dispose = dispose;
this.map = new _WeakMap();
this.newest = null;
this.oldest = null;
this.unfinalizedNodes = new Set();
this.finalizationScheduled = false;
this.size = 0;
this.finalize = function () {
var iterator = _this.unfinalizedNodes.values();
for (var i = 0; i < finalizationBatchSize; i++) {
var node = iterator.next().value;
if (!node)
break;
_this.unfinalizedNodes.delete(node);
var key = node.key;
delete node.key;
node.keyRef = new _WeakRef(key);
_this.registry.register(key, node, node);
}
if (_this.unfinalizedNodes.size > 0) {
queueMicrotask(_this.finalize);
}
else {
_this.finalizationScheduled = false;
}
};
this.registry = new _FinalizationRegistry(this.deleteNode.bind(this));
}
WeakCache.prototype.has = function (key) {
return this.map.has(key);
};
WeakCache.prototype.get = function (key) {
var node = this.getNode(key);
return node && node.value;
};
WeakCache.prototype.getNode = function (key) {
var node = this.map.get(key);
if (node && node !== this.newest) {
var older = node.older, newer = node.newer;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
};
WeakCache.prototype.set = function (key, value) {
var node = this.getNode(key);
if (node) {
return (node.value = value);
}
node = {
key: key,
value: value,
newer: null,
older: this.newest,
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.scheduleFinalization(node);
this.map.set(key, node);
this.size++;
return node.value;
};
WeakCache.prototype.clean = function () {
while (this.oldest && this.size > this.max) {
this.deleteNode(this.oldest);
}
};
WeakCache.prototype.deleteNode = function (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.size--;
var key = node.key || (node.keyRef && node.keyRef.deref());
this.dispose(node.value, key);
if (!node.keyRef) {
this.unfinalizedNodes.delete(node);
}
else {
this.registry.unregister(node);
}
if (key)
this.map.delete(key);
};
WeakCache.prototype.delete = function (key) {
var node = this.map.get(key);
if (node) {
this.deleteNode(node);
return true;
}
return false;
};
WeakCache.prototype.scheduleFinalization = function (node) {
this.unfinalizedNodes.add(node);
if (!this.finalizationScheduled) {
this.finalizationScheduled = true;
queueMicrotask(this.finalize);
}
};
return WeakCache;
}());
exports.StrongCache = StrongCache;
exports.WeakCache = WeakCache;
//# sourceMappingURL=bundle.cjs.map

View File

@@ -0,0 +1,8 @@
export interface CommonCache<K, V> {
has(key: K): boolean;
get(key: K): V | undefined;
set(key: K, value: V): V;
delete(key: K): boolean;
clean(): void;
readonly size: number;
}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=common.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,3 @@
export type { CommonCache } from "./common.js";
export { StrongCache } from "./strong.js";
export { WeakCache } from "./weak.js";

View File

@@ -0,0 +1,3 @@
export { StrongCache } from "./strong.js";
export { WeakCache } from "./weak.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}

View File

@@ -0,0 +1,16 @@
import type { CommonCache } from "./common";
export declare class StrongCache<K = any, V = any> implements CommonCache<K, V> {
private max;
dispose: (value: V, key: K) => void;
private map;
private newest;
private oldest;
constructor(max?: number, dispose?: (value: V, key: K) => void);
has(key: K): boolean;
get(key: K): V | undefined;
get size(): number;
private getNode;
set(key: K, value: V): V;
clean(): void;
delete(key: K): boolean;
}

View File

@@ -0,0 +1,86 @@
function defaultDispose() { }
export class StrongCache {
constructor(max = Infinity, dispose = defaultDispose) {
this.max = max;
this.dispose = dispose;
this.map = new Map();
this.newest = null;
this.oldest = null;
}
has(key) {
return this.map.has(key);
}
get(key) {
const node = this.getNode(key);
return node && node.value;
}
get size() {
return this.map.size;
}
getNode(key) {
const node = this.map.get(key);
if (node && node !== this.newest) {
const { older, newer } = node;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
}
set(key, value) {
let node = this.getNode(key);
if (node) {
return node.value = value;
}
node = {
key,
value,
newer: null,
older: this.newest
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.map.set(key, node);
return node.value;
}
clean() {
while (this.oldest && this.map.size > this.max) {
this.delete(this.oldest.key);
}
}
delete(key) {
const node = this.map.get(key);
if (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.map.delete(key);
this.dispose(node.value, key);
return true;
}
return false;
}
}
//# sourceMappingURL=strong.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"strong.js","sourceRoot":"","sources":["../src/strong.ts"],"names":[],"mappings":"AASA,SAAS,cAAc,KAAI,CAAC;AAE5B,MAAM,OAAO,WAAW;IAKtB,YACU,MAAM,QAAQ,EACf,UAAsC,cAAc;QADnD,QAAG,GAAH,GAAG,CAAW;QACf,YAAO,GAAP,OAAO,CAA6C;QANrD,QAAG,GAAG,IAAI,GAAG,EAAiB,CAAC;QAC/B,WAAM,GAAsB,IAAI,CAAC;QACjC,WAAM,GAAsB,IAAI,CAAC;IAKtC,CAAC;IAEG,GAAG,CAAC,GAAM;QACf,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAEM,GAAG,CAAC,GAAM;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC;IAC5B,CAAC;IAED,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;IACvB,CAAC;IAEO,OAAO,CAAC,GAAM;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAE/B,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE;YAChC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;YAE9B,IAAI,KAAK,EAAE;gBACT,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;aACrB;YAED,IAAI,KAAK,EAAE;gBACT,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;aACrB;YAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;YACzB,IAAI,CAAC,KAAM,CAAC,KAAK,GAAG,IAAI,CAAC;YAEzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YAEnB,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE;gBACxB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;aACrB;SACF;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,GAAG,CAAC,GAAM,EAAE,KAAQ;QACzB,IAAI,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,IAAI,EAAE;YACR,OAAO,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;SAC3B;QAED,IAAI,GAAG;YACL,GAAG;YACH,KAAK;YACL,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,IAAI,CAAC,MAAM;SACnB,CAAC;QAEF,IAAI,IAAI,CAAC,MAAM,EAAE;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;SAC1B;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;QAElC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAExB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAEM,KAAK;QACV,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE;YAC9C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;SAC9B;IACH,CAAC;IAEM,MAAM,CAAC,GAAM;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,IAAI,EAAE;YACR,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE;gBACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;aAC1B;YAED,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE;gBACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;aAC1B;YAED,IAAI,IAAI,CAAC,KAAK,EAAE;gBACd,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;aAC/B;YAED,IAAI,IAAI,CAAC,KAAK,EAAE;gBACd,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;aAC/B;YAED,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAE9B,OAAO,IAAI,CAAC;SACb;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}

View File

@@ -0,0 +1,22 @@
import type { CommonCache } from "./common";
export declare class WeakCache<K extends object = any, V = any> implements CommonCache<K, V> {
private max;
dispose: (value: V, key?: K) => void;
private map;
private registry;
private newest;
private oldest;
private unfinalizedNodes;
private finalizationScheduled;
size: number;
constructor(max?: number, dispose?: (value: V, key?: K) => void);
has(key: K): boolean;
get(key: K): V | undefined;
private getNode;
set(key: K, value: V): V;
clean(): void;
private deleteNode;
delete(key: K): boolean;
private scheduleFinalization;
private finalize;
}

View File

@@ -0,0 +1,143 @@
function noop() { }
const defaultDispose = noop;
const _WeakRef = typeof WeakRef !== "undefined"
? WeakRef
: function (value) {
return { deref: () => value };
};
const _WeakMap = typeof WeakMap !== "undefined" ? WeakMap : Map;
const _FinalizationRegistry = typeof FinalizationRegistry !== "undefined"
? FinalizationRegistry
: function () {
return {
register: noop,
unregister: noop,
};
};
const finalizationBatchSize = 10024;
export class WeakCache {
constructor(max = Infinity, dispose = defaultDispose) {
this.max = max;
this.dispose = dispose;
this.map = new _WeakMap();
this.newest = null;
this.oldest = null;
this.unfinalizedNodes = new Set();
this.finalizationScheduled = false;
this.size = 0;
this.finalize = () => {
const iterator = this.unfinalizedNodes.values();
for (let i = 0; i < finalizationBatchSize; i++) {
const node = iterator.next().value;
if (!node)
break;
this.unfinalizedNodes.delete(node);
const key = node.key;
delete node.key;
node.keyRef = new _WeakRef(key);
this.registry.register(key, node, node);
}
if (this.unfinalizedNodes.size > 0) {
queueMicrotask(this.finalize);
}
else {
this.finalizationScheduled = false;
}
};
this.registry = new _FinalizationRegistry(this.deleteNode.bind(this));
}
has(key) {
return this.map.has(key);
}
get(key) {
const node = this.getNode(key);
return node && node.value;
}
getNode(key) {
const node = this.map.get(key);
if (node && node !== this.newest) {
const { older, newer } = node;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
}
set(key, value) {
let node = this.getNode(key);
if (node) {
return (node.value = value);
}
node = {
key,
value,
newer: null,
older: this.newest,
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.scheduleFinalization(node);
this.map.set(key, node);
this.size++;
return node.value;
}
clean() {
while (this.oldest && this.size > this.max) {
this.deleteNode(this.oldest);
}
}
deleteNode(node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.size--;
const key = node.key || (node.keyRef && node.keyRef.deref());
this.dispose(node.value, key);
if (!node.keyRef) {
this.unfinalizedNodes.delete(node);
}
else {
this.registry.unregister(node);
}
if (key)
this.map.delete(key);
}
delete(key) {
const node = this.map.get(key);
if (node) {
this.deleteNode(node);
return true;
}
return false;
}
scheduleFinalization(node) {
this.unfinalizedNodes.add(node);
if (!this.finalizationScheduled) {
this.finalizationScheduled = true;
queueMicrotask(this.finalize);
}
}
}
//# sourceMappingURL=weak.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"weak.js","sourceRoot":"","sources":["../src/weak.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,41 @@
{
"name": "@wry/caches",
"version": "1.0.1",
"author": "Ben Newman <ben@eloper.dev>",
"description": "Various cache implementations",
"license": "MIT",
"type": "module",
"sideEffects": false,
"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:before && npm run tsc && npm run rollup && npm run clean:after",
"clean:before": "rimraf lib",
"tsc": "npm run tsc:es5 && npm run tsc:esm",
"tsc:es5": "tsc -p tsconfig.es5.json",
"tsc:esm": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"clean:after": "rimraf lib/es5",
"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"
},
"gitHead": "fc1400e8db54eb763822c69748c866909363a2c3"
}

View File

@@ -0,0 +1 @@
export { default } from "../../shared/rollup.config.js";

View File

@@ -0,0 +1,8 @@
export interface CommonCache<K, V> {
has(key: K): boolean;
get(key: K): V | undefined;
set(key: K, value: V): V;
delete(key: K): boolean;
clean(): void;
readonly size: number;
}

View File

@@ -0,0 +1,3 @@
export type { CommonCache } from "./common.js";
export { StrongCache } from "./strong.js";
export { WeakCache } from "./weak.js";

View File

@@ -0,0 +1,121 @@
import type { CommonCache } from "./common";
interface Node<K, V> {
key: K;
value: V;
newer: Node<K, V> | null;
older: Node<K, V> | null;
}
function defaultDispose() {}
export class StrongCache<K = any, V = any> implements CommonCache<K, V> {
private map = new Map<K, Node<K, V>>();
private newest: Node<K, V> | null = null;
private oldest: Node<K, V> | null = null;
constructor(
private max = Infinity,
public dispose: (value: V, key: K) => void = defaultDispose,
) {}
public has(key: K): boolean {
return this.map.has(key);
}
public get(key: K): V | undefined {
const node = this.getNode(key);
return node && node.value;
}
public get size() {
return this.map.size;
}
private getNode(key: K): Node<K, V> | undefined {
const node = this.map.get(key);
if (node && node !== this.newest) {
const { older, newer } = node;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older!.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
}
public set(key: K, value: V): V {
let node = this.getNode(key);
if (node) {
return node.value = value;
}
node = {
key,
value,
newer: null,
older: this.newest
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.map.set(key, node);
return node.value;
}
public clean() {
while (this.oldest && this.map.size > this.max) {
this.delete(this.oldest.key);
}
}
public delete(key: K): boolean {
const node = this.map.get(key);
if (node) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.map.delete(key);
this.dispose(node.value, key);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,2 @@
import "./strong.js";
import "./weak.js";

View File

@@ -0,0 +1,108 @@
import * as assert from "assert";
import { StrongCache } from "../strong.js";
describe("least-recently-used cache", function () {
it("can hold lots of elements", function () {
const cache = new StrongCache;
const count = 1000000;
for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
}
cache.clean();
assert.strictEqual((cache as any).map.size, count);
assert.ok(cache.has(0));
assert.ok(cache.has(count - 1));
assert.strictEqual(cache.get(43), "43");
});
it("evicts excess old elements", function () {
const max = 10;
const evicted = [];
const cache = new StrongCache(max, (value, key) => {
assert.strictEqual(String(key), value);
evicted.push(key);
});
const count = 100;
const keys = [];
for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
keys.push(i);
}
cache.clean();
assert.strictEqual((cache as any).map.size, max);
assert.strictEqual(evicted.length, count - max);
for (let i = count - max; i < count; ++i) {
assert.ok(cache.has(i));
}
});
it("can cope with small max values", function () {
const cache = new StrongCache(2);
function check(...sequence: number[]) {
cache.clean();
let entry = (cache as any).newest;
const forwards = [];
while (entry) {
forwards.push(entry.key);
entry = entry.older;
}
assert.deepEqual(forwards, sequence);
const backwards = [];
entry = (cache as any).oldest;
while (entry) {
backwards.push(entry.key);
entry = entry.newer;
}
backwards.reverse();
assert.deepEqual(backwards, sequence);
sequence.forEach(function (n) {
assert.strictEqual((cache as any).map.get(n).value, n + 1);
});
if (sequence.length > 0) {
assert.strictEqual((cache as any).newest.key, sequence[0]);
assert.strictEqual((cache as any).oldest.key,
sequence[sequence.length - 1]);
}
}
cache.set(1, 2);
check(1);
cache.set(2, 3);
check(2, 1);
cache.set(3, 4);
check(3, 2);
cache.get(2);
check(2, 3);
cache.set(4, 5);
check(4, 2);
assert.strictEqual(cache.has(1), false);
assert.strictEqual(cache.get(2), 3);
assert.strictEqual(cache.has(3), false);
assert.strictEqual(cache.get(4), 5);
cache.delete(2);
check(4);
cache.delete(4);
check();
assert.strictEqual((cache as any).newest, null);
assert.strictEqual((cache as any).oldest, null);
});
});

View File

@@ -0,0 +1,170 @@
import * as assert from "assert";
import { WeakCache } from "../weak.js";
describe("weak least-recently-used cache", function () {
it("can hold lots of elements", async function () {
this.timeout(10000);
const cache = new WeakCache();
const count = 1000000;
const keys = [];
for (let i = 0; i < count; ++i) {
const key = {};
cache.set(key, String(i));
keys[i] = key;
}
await waitForCache(cache);
cache.clean();
assert.strictEqual(cache.size, count);
assert.ok(cache.has(keys[0]));
assert.ok(cache.has(keys[count - 1]));
assert.strictEqual(cache.get(keys[43]), "43");
});
it("evicts excess old elements", function () {
const max = 10;
const evicted = [];
const cache = new WeakCache(max, (value, key) => {
assert.strictEqual(key.valueOf(), value.valueOf());
evicted.push(key);
});
const count = 100;
const keys = [];
for (let i = 0; i < count; ++i) {
const key = new String(i);
cache.set(key, String(i));
keys[i] = key;
}
cache.clean();
assert.strictEqual((cache as any).size, max);
assert.strictEqual(evicted.length, count - max);
for (let i = count - max; i < count; ++i) {
assert.ok(cache.has(keys[i]));
}
});
it("evicts elements that are garbage collected", async function () {
const cache = new WeakCache();
const count = 100;
const keys: Array<String | null> = [];
for (let i = 0; i < count; ++i) {
keys[i] = new String(i);
cache.set(keys[i], String(i));
}
assert.strictEqual(cache.size, 100);
await waitForCache(cache);
assert.strictEqual(cache.size, 100);
for (let i = 0; i < 50; ++i) {
keys[i] = null;
}
return gcPromise(() => {
return cache.size > 50 ? null : () => {
assert.strictEqual(cache.size, 50);
assert.strictEqual(keys.length, 100);
assert.strictEqual(new Set(keys).size, 51);
};
});
});
function gcPromise(test: () => null | (() => void)) {
return new Promise<void>(function (resolve, reject) {
function pollGC() {
global.gc!();
const testCallback = test();
if (!testCallback) {
setTimeout(pollGC, 20);
} else try {
testCallback();
resolve();
} catch (e) {
reject(e);
}
}
pollGC();
});
}
it("can cope with small max values", async function () {
const cache = new WeakCache(2);
const keys = Array(10)
.fill(null)
.map((_, i) => new Number(i));
async function check(...sequence: number[]) {
await waitForCache(cache);
cache.clean();
let entry = cache["newest"];
const forwards = [];
while (entry) {
forwards.push(entry.keyRef?.deref());
entry = entry.older;
}
assert.deepEqual(forwards.map(Number), sequence);
const backwards = [];
entry = cache["oldest"];
while (entry) {
backwards.push(entry.keyRef?.deref());
entry = entry.newer;
}
backwards.reverse();
assert.deepEqual(backwards.map(Number), sequence);
sequence.forEach(function (n) {
assert.strictEqual(cache["map"].get(keys[n])?.value, n + 1);
});
if (sequence.length > 0) {
assert.strictEqual(
cache["oldest"]?.keyRef?.deref().valueOf(),
sequence[sequence.length - 1]
);
}
}
cache.set(keys[1], 2);
await check(1);
cache.set(keys[2], 3);
await check(2, 1);
cache.set(keys[3], 4);
await check(3, 2);
cache.get(keys[2]);
await check(2, 3);
cache.set(keys[4], 5);
await check(4, 2);
assert.strictEqual(cache.has(keys[1]), false);
assert.strictEqual(cache.get(keys[2]), 3);
assert.strictEqual(cache.has(keys[3]), false);
assert.strictEqual(cache.get(keys[4]), 5);
cache.delete(keys[2]);
await check(4);
cache.delete(keys[4]);
await check();
assert.strictEqual((cache as any).newest, null);
assert.strictEqual((cache as any).oldest, null);
});
});
async function waitForCache(cache: WeakCache) {
while (cache["finalizationScheduled"]) {
await new Promise<void>(queueMicrotask);
}
}

View File

@@ -0,0 +1,200 @@
import type { CommonCache } from "./common";
interface PartialNode<K extends object, V> {
value: V;
newer: Node<K, V> | null;
older: Node<K, V> | null;
}
interface UnfinalizedNode<K extends object, V> extends PartialNode<K, V> {
keyRef?: undefined;
key: K;
}
interface FullNode<K extends object, V> extends PartialNode<K, V> {
keyRef: WeakRef<K>;
key?: undefined;
}
type Node<K extends object, V> = FullNode<K, V> | UnfinalizedNode<K, V>;
function noop() {}
const defaultDispose = noop;
const _WeakRef =
typeof WeakRef !== "undefined"
? WeakRef
: (function <T>(value: T) {
return { deref: () => value } satisfies Omit<
WeakRef<any>,
typeof Symbol.toStringTag
>;
} as any as typeof WeakRef);
const _WeakMap = typeof WeakMap !== "undefined" ? WeakMap : Map;
const _FinalizationRegistry =
typeof FinalizationRegistry !== "undefined"
? FinalizationRegistry
: (function <T>() {
return {
register: noop,
unregister: noop,
} satisfies Omit<FinalizationRegistry<T>, typeof Symbol.toStringTag>;
} as any as typeof FinalizationRegistry);
const finalizationBatchSize = 10024;
export class WeakCache<K extends object = any, V = any>
implements CommonCache<K, V>
{
private map = new _WeakMap<K, Node<K, V>>();
private registry: FinalizationRegistry<Node<K, V>>;
private newest: Node<K, V> | null = null;
private oldest: Node<K, V> | null = null;
private unfinalizedNodes: Set<UnfinalizedNode<K, V>> = new Set();
private finalizationScheduled = false;
public size = 0;
constructor(
private max = Infinity,
public dispose: (value: V, key?: K) => void = defaultDispose
) {
this.registry = new _FinalizationRegistry<Node<K, V>>(
this.deleteNode.bind(this)
);
}
public has(key: K): boolean {
return this.map.has(key);
}
public get(key: K): V | undefined {
const node = this.getNode(key);
return node && node.value;
}
private getNode(key: K): Node<K, V> | undefined {
const node = this.map.get(key);
if (node && node !== this.newest) {
const { older, newer } = node;
if (newer) {
newer.older = older;
}
if (older) {
older.newer = newer;
}
node.older = this.newest;
node.older!.newer = node;
node.newer = null;
this.newest = node;
if (node === this.oldest) {
this.oldest = newer;
}
}
return node;
}
public set(key: K, value: V): V {
let node = this.getNode(key);
if (node) {
return (node.value = value);
}
node = {
key,
value,
newer: null,
older: this.newest,
};
if (this.newest) {
this.newest.newer = node;
}
this.newest = node;
this.oldest = this.oldest || node;
this.scheduleFinalization(node);
this.map.set(key, node);
this.size++;
return node.value;
}
public clean() {
while (this.oldest && this.size > this.max) {
this.deleteNode(this.oldest);
}
}
private deleteNode(node: Node<K, V>) {
if (node === this.newest) {
this.newest = node.older;
}
if (node === this.oldest) {
this.oldest = node.newer;
}
if (node.newer) {
node.newer.older = node.older;
}
if (node.older) {
node.older.newer = node.newer;
}
this.size--;
const key = node.key || (node.keyRef && node.keyRef.deref());
this.dispose(node.value, key);
if (!node.keyRef) {
this.unfinalizedNodes.delete(node);
} else {
this.registry.unregister(node);
}
if (key) this.map.delete(key);
}
public delete(key: K): boolean {
const node = this.map.get(key);
if (node) {
this.deleteNode(node);
return true;
}
return false;
}
private scheduleFinalization(node: UnfinalizedNode<K, V>) {
this.unfinalizedNodes.add(node);
if (!this.finalizationScheduled) {
this.finalizationScheduled = true;
queueMicrotask(this.finalize);
}
}
private finalize = () => {
const iterator = this.unfinalizedNodes.values();
for (let i = 0; i < finalizationBatchSize; i++) {
const node = iterator.next().value;
if (!node) break;
this.unfinalizedNodes.delete(node);
const key = node.key;
delete (node as unknown as FullNode<K, V>).key;
(node as unknown as FullNode<K, V>).keyRef = new _WeakRef(key);
this.registry.register(key, node, node);
}
if (this.unfinalizedNodes.size > 0) {
queueMicrotask(this.finalize);
} else {
this.finalizationScheduled = false;
}
};
}

View File

@@ -0,0 +1,4 @@
# @wry/context
Manage contextual information needed by synchronous or asynchronous
tasks without explicitly passing objects around.

View File

@@ -0,0 +1,235 @@
'use strict';
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
var currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
var MISSING_VALUE = {};
var idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
var makeSlotClass = function () { return /** @class */ (function () {
function Slot() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
Slot.prototype.hasValue = function () {
for (var context_1 = currentContext; context_1; context_1 = context_1.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context_1.slots) {
var value = context_1.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context_1 !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
};
Slot.prototype.getValue = function () {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
};
Slot.prototype.withValue = function (value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
var _a;
var slots = (_a = {
__proto__: null
},
_a[this.id] = value,
_a);
var parent = currentContext;
currentContext = { parent: parent, slots: slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
};
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
Slot.bind = function (callback) {
var context = currentContext;
return function () {
var saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
};
// Immediately run a callback function without any captured context.
Slot.noContext = function (callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
var saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
};
return Slot;
}()); };
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
var globalKey = "@wry/context:Slot";
var host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(function () { return globalThis; }) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(function () { return global; }) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
var globalHost = host;
var Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
var bind = Slot.bind, noContext = Slot.noContext;
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
function asyncFromGen(genFn) {
return function () {
var gen = genFn.apply(this, arguments);
var boundNext = bind(gen.next);
var boundThrow = bind(gen.throw);
return new Promise(function (resolve, reject) {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
var next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
var invokeNext = function (value) { return invoke(boundNext, value); };
var invokeThrow = function (error) { return invoke(boundThrow, error); };
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
var wrappedFibers = [];
function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
var wrap = function (obj, method) {
var fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
exports.Slot = Slot;
exports.asyncFromGen = asyncFromGen;
exports.bind = bind;
exports.noContext = noContext;
exports.setTimeout = setTimeoutWithContext;
exports.wrapYieldingFiberMethods = wrapYieldingFiberMethods;
//# sourceMappingURL=bundle.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,235 @@
'use strict';
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
var currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
var MISSING_VALUE = {};
var idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
var makeSlotClass = function () { return /** @class */ (function () {
function Slot() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
Slot.prototype.hasValue = function () {
for (var context_1 = currentContext; context_1; context_1 = context_1.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context_1.slots) {
var value = context_1.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context_1 !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
};
Slot.prototype.getValue = function () {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
};
Slot.prototype.withValue = function (value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
var _a;
var slots = (_a = {
__proto__: null
},
_a[this.id] = value,
_a);
var parent = currentContext;
currentContext = { parent: parent, slots: slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
};
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
Slot.bind = function (callback) {
var context = currentContext;
return function () {
var saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
};
// Immediately run a callback function without any captured context.
Slot.noContext = function (callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
var saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
};
return Slot;
}()); };
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
var globalKey = "@wry/context:Slot";
var host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(function () { return globalThis; }) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(function () { return global; }) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
var globalHost = host;
var Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
var bind = Slot.bind, noContext = Slot.noContext;
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
function asyncFromGen(genFn) {
return function () {
var gen = genFn.apply(this, arguments);
var boundNext = bind(gen.next);
var boundThrow = bind(gen.throw);
return new Promise(function (resolve, reject) {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
var next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
var invokeNext = function (value) { return invoke(boundNext, value); };
var invokeThrow = function (error) { return invoke(boundThrow, error); };
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
var wrappedFibers = [];
function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
var wrap = function (obj, method) {
var fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
exports.Slot = Slot;
exports.asyncFromGen = asyncFromGen;
exports.bind = bind;
exports.noContext = noContext;
exports.setTimeout = setTimeoutWithContext;
exports.wrapYieldingFiberMethods = wrapYieldingFiberMethods;
//# sourceMappingURL=bundle.cjs.map

View File

@@ -0,0 +1,7 @@
import { Slot } from "./slot.js";
export { Slot };
export declare const bind: <TArgs extends any[], TResult, TThis = any>(callback: (this: TThis, ...args: TArgs) => TResult) => (this: TThis, ...args: TArgs) => TResult, noContext: <TResult, TArgs extends any[], TThis = any>(callback: (this: TThis, ...args: TArgs) => TResult, args?: TArgs | undefined, thisArg?: TThis | undefined) => TResult;
export { setTimeoutWithContext as setTimeout };
declare function setTimeoutWithContext(callback: () => any, delay: number): any;
export declare function asyncFromGen<TArgs extends any[], TYield = any, TReturn = any, TNext = any>(genFn: (...args: TArgs) => Generator<TYield, TReturn, TNext>): (...args: TArgs) => Promise<any>;
export declare function wrapYieldingFiberMethods<F extends Function>(Fiber: F): F;

View File

@@ -0,0 +1,64 @@
import { Slot } from "./slot.js";
export { Slot };
export const { bind, noContext } = Slot;
// Like global.setTimeout, except the callback runs with captured context.
export { setTimeoutWithContext as setTimeout };
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
export function asyncFromGen(genFn) {
return function () {
const gen = genFn.apply(this, arguments);
const boundNext = bind(gen.next);
const boundThrow = bind(gen.throw);
return new Promise((resolve, reject) => {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
const next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
const invokeNext = (value) => invoke(boundNext, value);
const invokeThrow = (error) => invoke(boundThrow, error);
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
const wrappedFibers = [];
export function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
const wrap = (obj, method) => {
const fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,12 @@
declare const makeSlotClass: () => {
new <TValue>(): {
readonly id: string;
hasValue(): boolean;
getValue(): TValue | undefined;
withValue<TResult, TArgs extends any[], TThis = any>(value: TValue, callback: (this: TThis, ...args: TArgs) => TResult, args?: TArgs | undefined, thisArg?: TThis | undefined): TResult;
};
bind<TArgs_1 extends any[], TResult_1, TThis_1 = any>(callback: (this: TThis_1, ...args: TArgs_1) => TResult_1): (this: TThis_1, ...args: TArgs_1) => TResult_1;
noContext<TResult_2, TArgs_2 extends any[], TThis_2 = any>(callback: (this: TThis_2, ...args: TArgs_2) => TResult_2, args?: TArgs_2 | undefined, thisArg?: TThis_2 | undefined): TResult_2;
};
export declare const Slot: ReturnType<typeof makeSlotClass>;
export {};

View File

@@ -0,0 +1,163 @@
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
let currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
const MISSING_VALUE = {};
let idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
const makeSlotClass = () => class Slot {
constructor() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
hasValue() {
for (let context = currentContext; context; context = context.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context.slots) {
const value = context.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
}
getValue() {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
}
withValue(value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
const slots = {
__proto__: null,
[this.id]: value,
};
const parent = currentContext;
currentContext = { parent, slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
}
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
static bind(callback) {
const context = currentContext;
return function () {
const saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
}
// Immediately run a callback function without any captured context.
static noContext(callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
const saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
}
};
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
const globalKey = "@wry/context:Slot";
const host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(() => globalThis) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(() => global) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
const globalHost = host;
export const Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
//# sourceMappingURL=slot.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"slot.js","sourceRoot":"","sources":["../src/slot.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,39 @@
{
"name": "@wry/context",
"version": "0.7.4",
"author": "Ben Newman <ben@eloper.dev>",
"description": "Manage contextual information needed by (a)synchronous tasks without explicitly passing objects around",
"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:before && npm run tsc && npm run rollup && npm run clean:after",
"clean:before": "rimraf lib",
"tsc": "npm run tsc:es5 && npm run tsc:esm",
"tsc:es5": "tsc -p tsconfig.es5.json",
"tsc:esm": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"clean:after": "rimraf lib/es5",
"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"
}
}

View File

@@ -0,0 +1 @@
export { default } from "../../shared/rollup.config.js";

View File

@@ -0,0 +1,87 @@
import { Slot } from "./slot.js";
export { Slot }
export const { bind, noContext } = Slot;
// Relying on the @types/node declaration of global.setTimeout can make
// things tricky for dowstream projects (see PR #7).
declare function setTimeout(
callback: (...args: any[]) => any,
ms?: number,
...args: any[]
): any;
// Like global.setTimeout, except the callback runs with captured context.
export { setTimeoutWithContext as setTimeout };
function setTimeoutWithContext(callback: () => any, delay: number) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
export function asyncFromGen<
TArgs extends any[],
TYield = any,
TReturn = any,
TNext = any,
>(
genFn: (...args: TArgs) => Generator<TYield, TReturn, TNext>
) {
return function (this: any) {
const gen = genFn.apply(this, arguments as any);
type Method = (
this: Generator<TYield, TReturn, TNext>,
arg: any,
) => IteratorResult<TYield, TReturn>;
const boundNext: Method = bind(gen.next);
const boundThrow: Method = bind(gen.throw!);
return new Promise((resolve, reject) => {
function invoke(method: Method, argument: any) {
try {
var result: any = method.call(gen, argument);
} catch (error) {
return reject(error);
}
const next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
} else {
next(result.value);
}
}
const invokeNext = (value?: any) => invoke(boundNext, value);
const invokeThrow = (error: any) => invoke(boundThrow, error);
invokeNext();
});
} as (...args: TArgs) => Promise<any>;
}
function isPromiseLike(value: any): value is PromiseLike<any> {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
const wrappedFibers: Function[] = [];
export function wrapYieldingFiberMethods<F extends Function>(Fiber: F): F {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
const wrap = (obj: any, method: string) => {
const fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments as any, this);
};
}
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}

View File

@@ -0,0 +1,183 @@
type Context = {
parent: Context | null;
slots: { [slotId: string]: any };
}
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
let currentContext: Context | null = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
const MISSING_VALUE: any = {};
let idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
const makeSlotClass = () => class Slot<TValue> {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
public readonly id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
public hasValue() {
for (let context = currentContext; context; context = context.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context.slots) {
const value = context.slots[this.id];
if (value === MISSING_VALUE) break;
if (context !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext!.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
}
public getValue(): TValue | undefined {
if (this.hasValue()) {
return currentContext!.slots[this.id] as TValue;
}
}
public withValue<TResult, TArgs extends any[], TThis = any>(
value: TValue,
callback: (this: TThis, ...args: TArgs) => TResult,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args?: TArgs,
thisArg?: TThis,
): TResult {
const slots = {
__proto__: null,
[this.id]: value,
};
const parent = currentContext;
currentContext = { parent, slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg!, args!);
} finally {
currentContext = parent;
}
}
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
static bind<TArgs extends any[], TResult, TThis = any>(
callback: (this: TThis, ...args: TArgs) => TResult,
) {
const context = currentContext;
return function (this: TThis) {
const saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments as any);
} finally {
currentContext = saved;
}
} as typeof callback;
}
// Immediately run a callback function without any captured context.
static noContext<TResult, TArgs extends any[], TThis = any>(
callback: (this: TThis, ...args: TArgs) => TResult,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args?: TArgs,
thisArg?: TThis,
) {
if (currentContext) {
const saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg!, args!);
} finally {
currentContext = saved;
}
} else {
return callback.apply(thisArg!, args!);
}
}
};
function maybe<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (ignored) {}
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
const globalKey = "@wry/context:Slot";
const host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(() => globalThis) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(() => global) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null) as typeof Array;
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
const globalHost: typeof host & {
[globalKey]?: typeof Slot;
} = host;
export const Slot: ReturnType<typeof makeSlotClass> =
globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
(Array as typeof globalHost)[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
} finally {
return Slot;
}
})(makeSlotClass());

View File

@@ -0,0 +1,478 @@
import * as assert from "assert";
import {
Slot,
bind,
noContext,
setTimeout,
asyncFromGen
} from "../index.js";
function repeat(s: string, times: number) {
let result = "";
while (times --> 0) result += s;
return result;
}
describe("Slot", function () {
it("is importable", function () {
assert.strictEqual(typeof Slot, "function");
});
it("has no value initially", function () {
const slot = new Slot;
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(typeof slot.getValue(), "undefined");
});
it("retains values set by withValue", function () {
const slot = new Slot<number>();
const results = slot.withValue(123, () => {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 123);
const results = [
slot.getValue(),
slot.withValue(456, () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
}),
slot.withValue(789, () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
}),
];
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 123);
return results;
});
assert.strictEqual(slot.hasValue(), false);
assert.deepEqual(results, [123, 456, 789]);
});
it("is not confused by other slots", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function inner() {
return repeat(
stringSlot.getValue()!,
numberSlot.getValue()!,
);
}
const oneWay = stringSlot.withValue("oyez", () => {
return numberSlot.withValue(3, inner);
});
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
const otherWay = numberSlot.withValue(3, () => {
return stringSlot.withValue("oyez", inner);
});
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
assert.strictEqual(oneWay, otherWay);
assert.strictEqual(oneWay, "oyezoyezoyez");
});
it("is a singleton", async function () {
const cjsSlotModule = await import("../slot.js");
assert.ok(new Slot<number>() instanceof cjsSlotModule.Slot);
assert.ok(new cjsSlotModule.Slot() instanceof Slot);
assert.strictEqual(cjsSlotModule.Slot, Slot);
const globalKey = "@wry/context:Slot";
assert.strictEqual((global as any)[globalKey], Slot);
assert.deepEqual(Object.keys(Array), []);
assert.strictEqual(
Object.keys(global).indexOf(globalKey),
-1,
);
});
it("can be subclassed", function () {
class NamedSlot extends Slot<number> {
constructor(public readonly name: string) {
super();
(this as any).id = name + ":" + this.id;
}
}
const ageSlot = new NamedSlot("age");
assert.strictEqual(ageSlot.hasValue(), false);
ageSlot.withValue(87, () => {
assert.strictEqual(ageSlot.hasValue(), true);
const age = ageSlot.getValue()!;
assert.strictEqual(age, 87);
assert.strictEqual(ageSlot.name, "age");
assert.ok(ageSlot.id.startsWith("age:slot:"));
});
class DefaultSlot<T> extends Slot<T> {
constructor(public readonly defaultValue: T) {
super();
}
hasValue() {
return true;
}
getValue() {
return super.hasValue() ? super.getValue() : this.defaultValue;
}
}
const defaultSlot = new DefaultSlot("default");
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "default");
const check = defaultSlot.withValue("real", function () {
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "real");
return bind(function () {
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "real");
});
});
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "default");
check();
});
});
describe("bind", function () {
it("is importable", function () {
assert.strictEqual(typeof bind, "function");
});
it("preserves multiple slots", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function neither() {
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
}
const checks = [bind(neither)];
stringSlot.withValue("asdf", () => {
function justStringAsdf() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "asdf");
assert.strictEqual(numberSlot.hasValue(), false);
}
checks.push(bind(justStringAsdf));
numberSlot.withValue(54321, () => {
checks.push(bind(function both() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "asdf");
assert.strictEqual(numberSlot.hasValue(), true);
assert.strictEqual(numberSlot.getValue(), 54321);
}));
});
stringSlot.withValue("oyez", () => {
checks.push(bind(function justStringOyez() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "oyez");
assert.strictEqual(numberSlot.hasValue(), false);
}));
numberSlot.withValue(12345, () => {
checks.push(bind(function bothAgain() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "oyez");
assert.strictEqual(numberSlot.hasValue(), true);
assert.strictEqual(numberSlot.getValue(), 12345);
}));
});
});
checks.push(bind(justStringAsdf));
});
checks.push(bind(neither));
checks.forEach(check => check());
});
it("does not permit rebinding", function () {
const slot = new Slot<number>();
const bound = slot.withValue(1, () => bind(function () {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 1);
return slot.getValue();
}));
assert.strictEqual(bound(), 1);
const rebound = slot.withValue(2, () => bind(bound));
assert.strictEqual(rebound(), 1);
assert.strictEqual(slot.hasValue(), false);
});
});
describe("noContext", function () {
it("is importable", function () {
assert.strictEqual(typeof noContext, "function");
});
it("severs context set by withValue", function () {
const slot = new Slot<string>();
const result = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return "inner";
});
});
assert.strictEqual(result, "inner");
});
it("severs bound context", function () {
const slot = new Slot<string>();
const bound = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return bind(function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return "inner";
});
});
});
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(bound(), "inner");
});
it("permits reestablishing inner context values", function () {
const slot = new Slot<string>();
const bound = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return bind(function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return slot.withValue("oyez", () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
});
});
});
});
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(bound(), "oyez");
});
it("permits passing arguments and this", function () {
const slot = new Slot<number>();
const self = {};
const notSelf = {};
const result = slot.withValue(1, function (a: number) {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 1);
assert.strictEqual(this, self);
return noContext(function (b: number) {
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(this, notSelf);
return slot.withValue(b, (aArg, bArg) => {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), b);
assert.strictEqual(this, notSelf);
assert.strictEqual(a, aArg);
assert.strictEqual(b, bArg);
return aArg * bArg;
}, [a, b], self);
}, [3], notSelf);
}, [2], self);
assert.strictEqual(result, 2 * 3);
});
it("works with Array-like (arguments) objects", function () {
function multiply(a: number, b: number) {
return noContext(function inner(a, b) {
return a * b;
}, arguments as any);
}
assert.strictEqual(multiply(3, 7) * 2, 42);
});
});
describe("setTimeout", function () {
it("is importable", function () {
assert.strictEqual(typeof setTimeout, "function");
});
it("binds its callback", function () {
const booleanSlot = new Slot<boolean>();
const objectSlot = new Slot<{ foo: number }>();
return new Promise<void>((resolve, reject) => {
booleanSlot.withValue(true, () => {
assert.strictEqual(booleanSlot.getValue(), true);
objectSlot.withValue({ foo: 42 }, () => {
setTimeout(function () {
try {
assert.strictEqual(booleanSlot.hasValue(), true);
assert.strictEqual(booleanSlot.getValue(), true);
assert.strictEqual(objectSlot.hasValue(), true);
assert.strictEqual(objectSlot.getValue()!.foo, 42);
resolve();
} catch (error) {
reject(error);
}
}, 10);
})
});
}).then(() => {
assert.strictEqual(booleanSlot.hasValue(), false);
assert.strictEqual(objectSlot.hasValue(), false);
});
});
});
describe("asyncFromGen", function () {
it("is importable", function () {
assert.strictEqual(typeof asyncFromGen, "function");
});
it("works like an async function", asyncFromGen(
function*(): Generator<number | Promise<number>, Promise<string>, number> {
let sum = 0;
const limit = yield new Promise(resolve => {
setTimeout(() => resolve(10), 10);
});
for (let i = 0; i < limit; ++i) {
sum += yield i + 1;
}
assert.strictEqual(sum, 55);
return Promise.resolve("ok");
},
));
it("properly handles exceptions", async function () {
const fn = asyncFromGen(function*(throwee?: object) {
const result = yield Promise.resolve("ok");
if (throwee) {
throw yield throwee;
}
return result;
});
const okPromise = fn();
const expected = {};
const koPromise = fn(expected);
assert.strictEqual(await okPromise, "ok");
try {
await koPromise;
throw new Error("not reached");
} catch (error) {
assert.strictEqual(error, expected);
}
try {
await fn(Promise.resolve("oyez"));
throw new Error("not reached");
} catch (thrown) {
assert.strictEqual(thrown, "oyez");
}
});
it("propagates contextual slot values across yields", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function checkNoValues() {
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
}
const inner = asyncFromGen(function*(
stringValue: string,
numberValue: number,
) {
function checkValues() {
assert.strictEqual(stringSlot.getValue(), stringValue);
assert.strictEqual(numberSlot.getValue(), numberValue);
}
checkValues();
yield new Promise<void>(resolve => setTimeout(function () {
checkValues();
resolve();
}, 10));
checkValues();
yield new Promise<void>(resolve => {
checkValues();
resolve();
});
checkValues();
yield Promise.resolve().then(checkNoValues);
checkValues();
return repeat(stringValue, numberValue);
});
const outer = asyncFromGen(function*() {
checkNoValues();
const oyezPromise = stringSlot.withValue("oyez", () => {
return numberSlot.withValue(3, () => inner("oyez", 3));
});
checkNoValues();
const hahaPromise = numberSlot.withValue(4, () => {
return stringSlot.withValue("ha", () => inner("ha", 4));
});
checkNoValues();
assert.strictEqual(yield oyezPromise, "oyezoyezoyez");
assert.strictEqual(yield hahaPromise, "hahahaha");
checkNoValues();
return Promise.all([oyezPromise, hahaPromise]);
});
return outer().then(results => {
checkNoValues();
assert.deepEqual(results, [
"oyezoyezoyez",
"hahahaha",
]);
});
});
it("allows Promise rejections to be caught", function () {
const fn = asyncFromGen(function*() {
try {
yield Promise.reject(new Error("expected"));
throw new Error("not reached");
} catch (error: any) {
assert.strictEqual(error?.message, "expected");
}
return "ok";
});
return fn().then(result => {
assert.strictEqual(result, "ok");
});
});
});

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Ben Newman
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.

View File

@@ -0,0 +1,4 @@
# @wry/equality
Structural equality checking for JavaScript values, with correct handling
of cyclic references, and minimal bundle size.

View File

@@ -0,0 +1,202 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const { toString, hasOwnProperty } = Object.prototype;
const fnToStr = Function.prototype.toString;
const previousComparisons = new Map();
/**
* Performs a deep equality check on two JavaScript values, tolerating cycles.
*/
function equal(a, b) {
try {
return check(a, b);
}
finally {
previousComparisons.clear();
}
}
function check(a, b) {
// If the two values are strictly equal, our job is easy.
if (a === b) {
return true;
}
// Object.prototype.toString returns a representation of the runtime type of
// the given value that is considerably more precise than typeof.
const aTag = toString.call(a);
const bTag = toString.call(b);
// If the runtime types of a and b are different, they could maybe be equal
// under some interpretation of equality, but for simplicity and performance
// we just return false instead.
if (aTag !== bTag) {
return false;
}
switch (aTag) {
case '[object Array]':
// Arrays are a lot like other objects, but we can cheaply compare their
// lengths as a short-cut before comparing their elements.
if (a.length !== b.length)
return false;
// Fall through to object case...
case '[object Object]': {
if (previouslyCompared(a, b))
return true;
const aKeys = definedKeys(a);
const bKeys = definedKeys(b);
// If `a` and `b` have a different number of enumerable keys, they
// must be different.
const keyCount = aKeys.length;
if (keyCount !== bKeys.length)
return false;
// Now make sure they have the same keys.
for (let k = 0; k < keyCount; ++k) {
if (!hasOwnProperty.call(b, aKeys[k])) {
return false;
}
}
// Finally, check deep equality of all child properties.
for (let k = 0; k < keyCount; ++k) {
const key = aKeys[k];
if (!check(a[key], b[key])) {
return false;
}
}
return true;
}
case '[object Error]':
return a.name === b.name && a.message === b.message;
case '[object Number]':
// Handle NaN, which is !== itself.
if (a !== a)
return b !== b;
// Fall through to shared +a === +b case...
case '[object Boolean]':
case '[object Date]':
return +a === +b;
case '[object RegExp]':
case '[object String]':
return a == `${b}`;
case '[object Map]':
case '[object Set]': {
if (a.size !== b.size)
return false;
if (previouslyCompared(a, b))
return true;
const aIterator = a.entries();
const isMap = aTag === '[object Map]';
while (true) {
const info = aIterator.next();
if (info.done)
break;
// If a instanceof Set, aValue === aKey.
const [aKey, aValue] = info.value;
// So this works the same way for both Set and Map.
if (!b.has(aKey)) {
return false;
}
// However, we care about deep equality of values only when dealing
// with Map structures.
if (isMap && !check(aValue, b.get(aKey))) {
return false;
}
}
return true;
}
case '[object Uint16Array]':
case '[object Uint8Array]': // Buffer, in Node.js.
case '[object Uint32Array]':
case '[object Int32Array]':
case '[object Int8Array]':
case '[object Int16Array]':
case '[object ArrayBuffer]':
// DataView doesn't need these conversions, but the equality check is
// otherwise the same.
a = new Uint8Array(a);
b = new Uint8Array(b);
// Fall through...
case '[object DataView]': {
let len = a.byteLength;
if (len === b.byteLength) {
while (len-- && a[len] === b[len]) {
// Keep looping as long as the bytes are equal.
}
}
return len === -1;
}
case '[object AsyncFunction]':
case '[object GeneratorFunction]':
case '[object AsyncGeneratorFunction]':
case '[object Function]': {
const aCode = fnToStr.call(a);
if (aCode !== fnToStr.call(b)) {
return false;
}
// We consider non-native functions equal if they have the same code
// (native functions require === because their code is censored).
// Note that this behavior is not entirely sound, since !== function
// objects with the same code can behave differently depending on
// their closure scope. However, any function can behave differently
// depending on the values of its input arguments (including this)
// and its calling context (including its closure scope), even
// though the function object is === to itself; and it is entirely
// possible for functions that are not === to behave exactly the
// same under all conceivable circumstances. Because none of these
// factors are statically decidable in JavaScript, JS function
// equality is not well-defined. This ambiguity allows us to
// consider the best possible heuristic among various imperfect
// options, and equating non-native functions that have the same
// code has enormous practical benefits, such as when comparing
// functions that are repeatedly passed as fresh function
// expressions within objects that are otherwise deeply equal. Since
// any function created from the same syntactic expression (in the
// same code location) will always stringify to the same code
// according to fnToStr.call, we can reasonably expect these
// repeatedly passed function expressions to have the same code, and
// thus behave "the same" (with all the caveats mentioned above),
// even though the runtime function objects are !== to one another.
return !endsWith(aCode, nativeCodeSuffix);
}
}
// Otherwise the values are not equal.
return false;
}
function definedKeys(obj) {
// Remember that the second argument to Array.prototype.filter will be
// used as `this` within the callback function.
return Object.keys(obj).filter(isDefinedKey, obj);
}
function isDefinedKey(key) {
return this[key] !== void 0;
}
const nativeCodeSuffix = "{ [native code] }";
function endsWith(full, suffix) {
const fromIndex = full.length - suffix.length;
return fromIndex >= 0 &&
full.indexOf(suffix, fromIndex) === fromIndex;
}
function previouslyCompared(a, b) {
// Though cyclic references can make an object graph appear infinite from the
// perspective of a depth-first traversal, the graph still contains a finite
// number of distinct object references. We use the previousComparisons cache
// to avoid comparing the same pair of object references more than once, which
// guarantees termination (even if we end up comparing every object in one
// graph to every object in the other graph, which is extremely unlikely),
// while still allowing weird isomorphic structures (like rings with different
// lengths) a chance to pass the equality test.
let bSet = previousComparisons.get(a);
if (bSet) {
// Return true here because we can be sure false will be returned somewhere
// else if the objects are not equivalent.
if (bSet.has(b))
return true;
}
else {
previousComparisons.set(a, bSet = new Set);
}
bSet.add(b);
return false;
}
exports.default = equal;
exports.equal = equal;
//# sourceMappingURL=bundle.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,202 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const { toString, hasOwnProperty } = Object.prototype;
const fnToStr = Function.prototype.toString;
const previousComparisons = new Map();
/**
* Performs a deep equality check on two JavaScript values, tolerating cycles.
*/
function equal(a, b) {
try {
return check(a, b);
}
finally {
previousComparisons.clear();
}
}
function check(a, b) {
// If the two values are strictly equal, our job is easy.
if (a === b) {
return true;
}
// Object.prototype.toString returns a representation of the runtime type of
// the given value that is considerably more precise than typeof.
const aTag = toString.call(a);
const bTag = toString.call(b);
// If the runtime types of a and b are different, they could maybe be equal
// under some interpretation of equality, but for simplicity and performance
// we just return false instead.
if (aTag !== bTag) {
return false;
}
switch (aTag) {
case '[object Array]':
// Arrays are a lot like other objects, but we can cheaply compare their
// lengths as a short-cut before comparing their elements.
if (a.length !== b.length)
return false;
// Fall through to object case...
case '[object Object]': {
if (previouslyCompared(a, b))
return true;
const aKeys = definedKeys(a);
const bKeys = definedKeys(b);
// If `a` and `b` have a different number of enumerable keys, they
// must be different.
const keyCount = aKeys.length;
if (keyCount !== bKeys.length)
return false;
// Now make sure they have the same keys.
for (let k = 0; k < keyCount; ++k) {
if (!hasOwnProperty.call(b, aKeys[k])) {
return false;
}
}
// Finally, check deep equality of all child properties.
for (let k = 0; k < keyCount; ++k) {
const key = aKeys[k];
if (!check(a[key], b[key])) {
return false;
}
}
return true;
}
case '[object Error]':
return a.name === b.name && a.message === b.message;
case '[object Number]':
// Handle NaN, which is !== itself.
if (a !== a)
return b !== b;
// Fall through to shared +a === +b case...
case '[object Boolean]':
case '[object Date]':
return +a === +b;
case '[object RegExp]':
case '[object String]':
return a == `${b}`;
case '[object Map]':
case '[object Set]': {
if (a.size !== b.size)
return false;
if (previouslyCompared(a, b))
return true;
const aIterator = a.entries();
const isMap = aTag === '[object Map]';
while (true) {
const info = aIterator.next();
if (info.done)
break;
// If a instanceof Set, aValue === aKey.
const [aKey, aValue] = info.value;
// So this works the same way for both Set and Map.
if (!b.has(aKey)) {
return false;
}
// However, we care about deep equality of values only when dealing
// with Map structures.
if (isMap && !check(aValue, b.get(aKey))) {
return false;
}
}
return true;
}
case '[object Uint16Array]':
case '[object Uint8Array]': // Buffer, in Node.js.
case '[object Uint32Array]':
case '[object Int32Array]':
case '[object Int8Array]':
case '[object Int16Array]':
case '[object ArrayBuffer]':
// DataView doesn't need these conversions, but the equality check is
// otherwise the same.
a = new Uint8Array(a);
b = new Uint8Array(b);
// Fall through...
case '[object DataView]': {
let len = a.byteLength;
if (len === b.byteLength) {
while (len-- && a[len] === b[len]) {
// Keep looping as long as the bytes are equal.
}
}
return len === -1;
}
case '[object AsyncFunction]':
case '[object GeneratorFunction]':
case '[object AsyncGeneratorFunction]':
case '[object Function]': {
const aCode = fnToStr.call(a);
if (aCode !== fnToStr.call(b)) {
return false;
}
// We consider non-native functions equal if they have the same code
// (native functions require === because their code is censored).
// Note that this behavior is not entirely sound, since !== function
// objects with the same code can behave differently depending on
// their closure scope. However, any function can behave differently
// depending on the values of its input arguments (including this)
// and its calling context (including its closure scope), even
// though the function object is === to itself; and it is entirely
// possible for functions that are not === to behave exactly the
// same under all conceivable circumstances. Because none of these
// factors are statically decidable in JavaScript, JS function
// equality is not well-defined. This ambiguity allows us to
// consider the best possible heuristic among various imperfect
// options, and equating non-native functions that have the same
// code has enormous practical benefits, such as when comparing
// functions that are repeatedly passed as fresh function
// expressions within objects that are otherwise deeply equal. Since
// any function created from the same syntactic expression (in the
// same code location) will always stringify to the same code
// according to fnToStr.call, we can reasonably expect these
// repeatedly passed function expressions to have the same code, and
// thus behave "the same" (with all the caveats mentioned above),
// even though the runtime function objects are !== to one another.
return !endsWith(aCode, nativeCodeSuffix);
}
}
// Otherwise the values are not equal.
return false;
}
function definedKeys(obj) {
// Remember that the second argument to Array.prototype.filter will be
// used as `this` within the callback function.
return Object.keys(obj).filter(isDefinedKey, obj);
}
function isDefinedKey(key) {
return this[key] !== void 0;
}
const nativeCodeSuffix = "{ [native code] }";
function endsWith(full, suffix) {
const fromIndex = full.length - suffix.length;
return fromIndex >= 0 &&
full.indexOf(suffix, fromIndex) === fromIndex;
}
function previouslyCompared(a, b) {
// Though cyclic references can make an object graph appear infinite from the
// perspective of a depth-first traversal, the graph still contains a finite
// number of distinct object references. We use the previousComparisons cache
// to avoid comparing the same pair of object references more than once, which
// guarantees termination (even if we end up comparing every object in one
// graph to every object in the other graph, which is extremely unlikely),
// while still allowing weird isomorphic structures (like rings with different
// lengths) a chance to pass the equality test.
let bSet = previousComparisons.get(a);
if (bSet) {
// Return true here because we can be sure false will be returned somewhere
// else if the objects are not equivalent.
if (bSet.has(b))
return true;
}
else {
previousComparisons.set(a, bSet = new Set);
}
bSet.add(b);
return false;
}
exports.default = equal;
exports.equal = equal;
//# sourceMappingURL=bundle.cjs.map

View File

@@ -0,0 +1,5 @@
/**
* Performs a deep equality check on two JavaScript values, tolerating cycles.
*/
export declare function equal(a: any, b: any): boolean;
export default equal;

View File

@@ -0,0 +1,197 @@
const { toString, hasOwnProperty } = Object.prototype;
const fnToStr = Function.prototype.toString;
const previousComparisons = new Map();
/**
* Performs a deep equality check on two JavaScript values, tolerating cycles.
*/
export function equal(a, b) {
try {
return check(a, b);
}
finally {
previousComparisons.clear();
}
}
// Allow default imports as well.
export default equal;
function check(a, b) {
// If the two values are strictly equal, our job is easy.
if (a === b) {
return true;
}
// Object.prototype.toString returns a representation of the runtime type of
// the given value that is considerably more precise than typeof.
const aTag = toString.call(a);
const bTag = toString.call(b);
// If the runtime types of a and b are different, they could maybe be equal
// under some interpretation of equality, but for simplicity and performance
// we just return false instead.
if (aTag !== bTag) {
return false;
}
switch (aTag) {
case '[object Array]':
// Arrays are a lot like other objects, but we can cheaply compare their
// lengths as a short-cut before comparing their elements.
if (a.length !== b.length)
return false;
// Fall through to object case...
case '[object Object]': {
if (previouslyCompared(a, b))
return true;
const aKeys = definedKeys(a);
const bKeys = definedKeys(b);
// If `a` and `b` have a different number of enumerable keys, they
// must be different.
const keyCount = aKeys.length;
if (keyCount !== bKeys.length)
return false;
// Now make sure they have the same keys.
for (let k = 0; k < keyCount; ++k) {
if (!hasOwnProperty.call(b, aKeys[k])) {
return false;
}
}
// Finally, check deep equality of all child properties.
for (let k = 0; k < keyCount; ++k) {
const key = aKeys[k];
if (!check(a[key], b[key])) {
return false;
}
}
return true;
}
case '[object Error]':
return a.name === b.name && a.message === b.message;
case '[object Number]':
// Handle NaN, which is !== itself.
if (a !== a)
return b !== b;
// Fall through to shared +a === +b case...
case '[object Boolean]':
case '[object Date]':
return +a === +b;
case '[object RegExp]':
case '[object String]':
return a == `${b}`;
case '[object Map]':
case '[object Set]': {
if (a.size !== b.size)
return false;
if (previouslyCompared(a, b))
return true;
const aIterator = a.entries();
const isMap = aTag === '[object Map]';
while (true) {
const info = aIterator.next();
if (info.done)
break;
// If a instanceof Set, aValue === aKey.
const [aKey, aValue] = info.value;
// So this works the same way for both Set and Map.
if (!b.has(aKey)) {
return false;
}
// However, we care about deep equality of values only when dealing
// with Map structures.
if (isMap && !check(aValue, b.get(aKey))) {
return false;
}
}
return true;
}
case '[object Uint16Array]':
case '[object Uint8Array]': // Buffer, in Node.js.
case '[object Uint32Array]':
case '[object Int32Array]':
case '[object Int8Array]':
case '[object Int16Array]':
case '[object ArrayBuffer]':
// DataView doesn't need these conversions, but the equality check is
// otherwise the same.
a = new Uint8Array(a);
b = new Uint8Array(b);
// Fall through...
case '[object DataView]': {
let len = a.byteLength;
if (len === b.byteLength) {
while (len-- && a[len] === b[len]) {
// Keep looping as long as the bytes are equal.
}
}
return len === -1;
}
case '[object AsyncFunction]':
case '[object GeneratorFunction]':
case '[object AsyncGeneratorFunction]':
case '[object Function]': {
const aCode = fnToStr.call(a);
if (aCode !== fnToStr.call(b)) {
return false;
}
// We consider non-native functions equal if they have the same code
// (native functions require === because their code is censored).
// Note that this behavior is not entirely sound, since !== function
// objects with the same code can behave differently depending on
// their closure scope. However, any function can behave differently
// depending on the values of its input arguments (including this)
// and its calling context (including its closure scope), even
// though the function object is === to itself; and it is entirely
// possible for functions that are not === to behave exactly the
// same under all conceivable circumstances. Because none of these
// factors are statically decidable in JavaScript, JS function
// equality is not well-defined. This ambiguity allows us to
// consider the best possible heuristic among various imperfect
// options, and equating non-native functions that have the same
// code has enormous practical benefits, such as when comparing
// functions that are repeatedly passed as fresh function
// expressions within objects that are otherwise deeply equal. Since
// any function created from the same syntactic expression (in the
// same code location) will always stringify to the same code
// according to fnToStr.call, we can reasonably expect these
// repeatedly passed function expressions to have the same code, and
// thus behave "the same" (with all the caveats mentioned above),
// even though the runtime function objects are !== to one another.
return !endsWith(aCode, nativeCodeSuffix);
}
}
// Otherwise the values are not equal.
return false;
}
function definedKeys(obj) {
// Remember that the second argument to Array.prototype.filter will be
// used as `this` within the callback function.
return Object.keys(obj).filter(isDefinedKey, obj);
}
function isDefinedKey(key) {
return this[key] !== void 0;
}
const nativeCodeSuffix = "{ [native code] }";
function endsWith(full, suffix) {
const fromIndex = full.length - suffix.length;
return fromIndex >= 0 &&
full.indexOf(suffix, fromIndex) === fromIndex;
}
function previouslyCompared(a, b) {
// Though cyclic references can make an object graph appear infinite from the
// perspective of a depth-first traversal, the graph still contains a finite
// number of distinct object references. We use the previousComparisons cache
// to avoid comparing the same pair of object references more than once, which
// guarantees termination (even if we end up comparing every object in one
// graph to every object in the other graph, which is extremely unlikely),
// while still allowing weird isomorphic structures (like rings with different
// lengths) a chance to pass the equality test.
let bSet = previousComparisons.get(a);
if (bSet) {
// Return true here because we can be sure false will be returned somewhere
// else if the objects are not equivalent.
if (bSet.has(b))
return true;
}
else {
previousComparisons.set(a, bSet = new Set);
}
bSet.add(b);
return false;
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC;AACtD,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;AAC5C,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,CAAM,EAAE,CAAM;IAClC,IAAI;QACF,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;KACpB;YAAS;QACR,mBAAmB,CAAC,KAAK,EAAE,CAAC;KAC7B;AACH,CAAC;AAED,iCAAiC;AACjC,eAAe,KAAK,CAAC;AAErB,SAAS,KAAK,CAAC,CAAM,EAAE,CAAM;IAC3B,yDAAyD;IACzD,IAAI,CAAC,KAAK,CAAC,EAAE;QACX,OAAO,IAAI,CAAC;KACb;IAED,4EAA4E;IAC5E,iEAAiE;IACjE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE9B,2EAA2E;IAC3E,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,IAAI,KAAK,IAAI,EAAE;QACjB,OAAO,KAAK,CAAC;KACd;IAED,QAAQ,IAAI,EAAE;QACZ,KAAK,gBAAgB;YACnB,wEAAwE;YACxE,0DAA0D;YAC1D,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;QACxC,iCAAiC;QACnC,KAAK,iBAAiB,CAAC,CAAC;YACtB,IAAI,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC;YAE1C,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAC7B,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAE7B,kEAAkE;YAClE,qBAAqB;YACrB,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC;YAC9B,IAAI,QAAQ,KAAK,KAAK,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAE5C,yCAAyC;YACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,EAAE,CAAC,EAAE;gBACjC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;oBACrC,OAAO,KAAK,CAAC;iBACd;aACF;YAED,wDAAwD;YACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,EAAE,CAAC,EAAE;gBACjC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACrB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE;oBAC1B,OAAO,KAAK,CAAC;iBACd;aACF;YAED,OAAO,IAAI,CAAC;SACb;QAED,KAAK,gBAAgB;YACnB,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC;QAEtD,KAAK,iBAAiB;YACpB,mCAAmC;YACnC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,2CAA2C;QAC7C,KAAK,kBAAkB,CAAC;QACxB,KAAK,eAAe;YAClB,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAEnB,KAAK,iBAAiB,CAAC;QACvB,KAAK,iBAAiB;YACpB,OAAO,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAErB,KAAK,cAAc,CAAC;QACpB,KAAK,cAAc,CAAC,CAAC;YACnB,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAC;YACpC,IAAI,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC;YAE1C,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,cAAc,CAAC;YAEtC,OAAO,IAAI,EAAE;gBACX,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC9B,IAAI,IAAI,CAAC,IAAI;oBAAE,MAAM;gBAErB,wCAAwC;gBACxC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;gBAElC,mDAAmD;gBACnD,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;oBAChB,OAAO,KAAK,CAAC;iBACd;gBAED,mEAAmE;gBACnE,uBAAuB;gBACvB,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE;oBACxC,OAAO,KAAK,CAAC;iBACd;aACF;YAED,OAAO,IAAI,CAAC;SACb;QAED,KAAK,sBAAsB,CAAC;QAC5B,KAAK,qBAAqB,CAAC,CAAC,sBAAsB;QAClD,KAAK,sBAAsB,CAAC;QAC5B,KAAK,qBAAqB,CAAC;QAC3B,KAAK,oBAAoB,CAAC;QAC1B,KAAK,qBAAqB,CAAC;QAC3B,KAAK,sBAAsB;YACzB,qEAAqE;YACrE,sBAAsB;YACtB,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QACtB,kBAAkB;QACpB,KAAK,mBAAmB,CAAC,CAAC;YACxB,IAAI,GAAG,GAAG,CAAC,CAAC,UAAU,CAAC;YACvB,IAAI,GAAG,KAAK,CAAC,CAAC,UAAU,EAAE;gBACxB,OAAO,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE;oBACjC,+CAA+C;iBAChD;aACF;YACD,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC;SACnB;QAED,KAAK,wBAAwB,CAAC;QAC9B,KAAK,4BAA4B,CAAC;QAClC,KAAK,iCAAiC,CAAC;QACvC,KAAK,mBAAmB,CAAC,CAAC;YACxB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;gBAC7B,OAAO,KAAK,CAAC;aACd;YAED,oEAAoE;YACpE,iEAAiE;YACjE,oEAAoE;YACpE,iEAAiE;YACjE,oEAAoE;YACpE,kEAAkE;YAClE,8DAA8D;YAC9D,kEAAkE;YAClE,gEAAgE;YAChE,kEAAkE;YAClE,8DAA8D;YAC9D,4DAA4D;YAC5D,+DAA+D;YAC/D,gEAAgE;YAChE,+DAA+D;YAC/D,yDAAyD;YACzD,oEAAoE;YACpE,kEAAkE;YAClE,6DAA6D;YAC7D,4DAA4D;YAC5D,oEAAoE;YACpE,iEAAiE;YACjE,mEAAmE;YACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;SAC3C;KACF;IAED,sCAAsC;IACtC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAyB,GAAY;IACvD,sEAAsE;IACtE,+CAA+C;IAC/C,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;AACpD,CAAC;AACD,SAAS,YAAY,CAEnB,GAAkB;IAElB,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAE7C,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC9C,OAAO,SAAS,IAAI,CAAC;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,SAAS,CAAC;AAClD,CAAC;AAED,SAAS,kBAAkB,CAAC,CAAS,EAAE,CAAS;IAC9C,6EAA6E;IAC7E,4EAA4E;IAC5E,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,0EAA0E;IAC1E,8EAA8E;IAC9E,+CAA+C;IAC/C,IAAI,IAAI,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,IAAI,EAAE;QACR,2EAA2E;QAC3E,0CAA0C;QAC1C,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;KAC9B;SAAM;QACL,mBAAmB,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;KAC5C;IACD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACZ,OAAO,KAAK,CAAC;AACf,CAAC"}

View File

@@ -0,0 +1,36 @@
{
"name": "@wry/equality",
"version": "0.5.7",
"author": "Ben Newman <ben@eloper.dev>",
"description": "Structural equality checking for JavaScript values",
"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:before && npm run tsc && npm run rollup",
"clean:before": "rimraf lib",
"tsc": "tsc",
"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"
}
}

View File

@@ -0,0 +1,21 @@
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.
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"
),
];

View File

@@ -0,0 +1,218 @@
const { toString, hasOwnProperty } = Object.prototype;
const fnToStr = Function.prototype.toString;
const previousComparisons = new Map<object, Set<object>>();
/**
* Performs a deep equality check on two JavaScript values, tolerating cycles.
*/
export function equal(a: any, b: any): boolean {
try {
return check(a, b);
} finally {
previousComparisons.clear();
}
}
// Allow default imports as well.
export default equal;
function check(a: any, b: any): boolean {
// If the two values are strictly equal, our job is easy.
if (a === b) {
return true;
}
// Object.prototype.toString returns a representation of the runtime type of
// the given value that is considerably more precise than typeof.
const aTag = toString.call(a);
const bTag = toString.call(b);
// If the runtime types of a and b are different, they could maybe be equal
// under some interpretation of equality, but for simplicity and performance
// we just return false instead.
if (aTag !== bTag) {
return false;
}
switch (aTag) {
case '[object Array]':
// Arrays are a lot like other objects, but we can cheaply compare their
// lengths as a short-cut before comparing their elements.
if (a.length !== b.length) return false;
// Fall through to object case...
case '[object Object]': {
if (previouslyCompared(a, b)) return true;
const aKeys = definedKeys(a);
const bKeys = definedKeys(b);
// If `a` and `b` have a different number of enumerable keys, they
// must be different.
const keyCount = aKeys.length;
if (keyCount !== bKeys.length) return false;
// Now make sure they have the same keys.
for (let k = 0; k < keyCount; ++k) {
if (!hasOwnProperty.call(b, aKeys[k])) {
return false;
}
}
// Finally, check deep equality of all child properties.
for (let k = 0; k < keyCount; ++k) {
const key = aKeys[k];
if (!check(a[key], b[key])) {
return false;
}
}
return true;
}
case '[object Error]':
return a.name === b.name && a.message === b.message;
case '[object Number]':
// Handle NaN, which is !== itself.
if (a !== a) return b !== b;
// Fall through to shared +a === +b case...
case '[object Boolean]':
case '[object Date]':
return +a === +b;
case '[object RegExp]':
case '[object String]':
return a == `${b}`;
case '[object Map]':
case '[object Set]': {
if (a.size !== b.size) return false;
if (previouslyCompared(a, b)) return true;
const aIterator = a.entries();
const isMap = aTag === '[object Map]';
while (true) {
const info = aIterator.next();
if (info.done) break;
// If a instanceof Set, aValue === aKey.
const [aKey, aValue] = info.value;
// So this works the same way for both Set and Map.
if (!b.has(aKey)) {
return false;
}
// However, we care about deep equality of values only when dealing
// with Map structures.
if (isMap && !check(aValue, b.get(aKey))) {
return false;
}
}
return true;
}
case '[object Uint16Array]':
case '[object Uint8Array]': // Buffer, in Node.js.
case '[object Uint32Array]':
case '[object Int32Array]':
case '[object Int8Array]':
case '[object Int16Array]':
case '[object ArrayBuffer]':
// DataView doesn't need these conversions, but the equality check is
// otherwise the same.
a = new Uint8Array(a);
b = new Uint8Array(b);
// Fall through...
case '[object DataView]': {
let len = a.byteLength;
if (len === b.byteLength) {
while (len-- && a[len] === b[len]) {
// Keep looping as long as the bytes are equal.
}
}
return len === -1;
}
case '[object AsyncFunction]':
case '[object GeneratorFunction]':
case '[object AsyncGeneratorFunction]':
case '[object Function]': {
const aCode = fnToStr.call(a);
if (aCode !== fnToStr.call(b)) {
return false;
}
// We consider non-native functions equal if they have the same code
// (native functions require === because their code is censored).
// Note that this behavior is not entirely sound, since !== function
// objects with the same code can behave differently depending on
// their closure scope. However, any function can behave differently
// depending on the values of its input arguments (including this)
// and its calling context (including its closure scope), even
// though the function object is === to itself; and it is entirely
// possible for functions that are not === to behave exactly the
// same under all conceivable circumstances. Because none of these
// factors are statically decidable in JavaScript, JS function
// equality is not well-defined. This ambiguity allows us to
// consider the best possible heuristic among various imperfect
// options, and equating non-native functions that have the same
// code has enormous practical benefits, such as when comparing
// functions that are repeatedly passed as fresh function
// expressions within objects that are otherwise deeply equal. Since
// any function created from the same syntactic expression (in the
// same code location) will always stringify to the same code
// according to fnToStr.call, we can reasonably expect these
// repeatedly passed function expressions to have the same code, and
// thus behave "the same" (with all the caveats mentioned above),
// even though the runtime function objects are !== to one another.
return !endsWith(aCode, nativeCodeSuffix);
}
}
// Otherwise the values are not equal.
return false;
}
function definedKeys<TObject extends object>(obj: TObject) {
// Remember that the second argument to Array.prototype.filter will be
// used as `this` within the callback function.
return Object.keys(obj).filter(isDefinedKey, obj);
}
function isDefinedKey<TObject extends object>(
this: TObject,
key: keyof TObject,
) {
return this[key] !== void 0;
}
const nativeCodeSuffix = "{ [native code] }";
function endsWith(full: string, suffix: string) {
const fromIndex = full.length - suffix.length;
return fromIndex >= 0 &&
full.indexOf(suffix, fromIndex) === fromIndex;
}
function previouslyCompared(a: object, b: object): boolean {
// Though cyclic references can make an object graph appear infinite from the
// perspective of a depth-first traversal, the graph still contains a finite
// number of distinct object references. We use the previousComparisons cache
// to avoid comparing the same pair of object references more than once, which
// guarantees termination (even if we end up comparing every object in one
// graph to every object in the other graph, which is extremely unlikely),
// while still allowing weird isomorphic structures (like rings with different
// lengths) a chance to pass the equality test.
let bSet = previousComparisons.get(a);
if (bSet) {
// Return true here because we can be sure false will be returned somewhere
// else if the objects are not equivalent.
if (bSet.has(b)) return true;
} else {
previousComparisons.set(a, bSet = new Set);
}
bSet.add(b);
return false;
}

View File

@@ -0,0 +1,411 @@
import * as assert from "assert";
import defaultEqual, { equal } from "../index.js";
function toStr(value: any) {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function assertEqual(a: any, b: any) {
assert.strictEqual(equal(a, b), true, `unexpectedly not equal(${toStr(a)}}, ${toStr(b)})`);
assert.strictEqual(equal(b, a), true, `unexpectedly not equal(${toStr(b)}, ${toStr(a)})`);
}
function assertNotEqual(a: any, b: any) {
assert.strictEqual(equal(a, b), false, `unexpectedly equal(${toStr(a)}, ${toStr(b)})`);
assert.strictEqual(equal(b, a), false, `unexpectedly equal(${toStr(b)}, ${toStr(a)})`);
}
describe("equality", function () {
it("should work with named and default imports", function () {
assert.strictEqual(defaultEqual, equal);
});
it("should work for primitive types", function () {
assertEqual(2 + 2, 4);
assertNotEqual(2 + 2, 5);
assertEqual("oyez", "oyez");
assertNotEqual("oyez", "onoz");
assertEqual(null, null);
assertEqual(void 0, void 1);
assertEqual(NaN, NaN);
assertNotEqual(void 0, null);
assertNotEqual(void 0, false);
assertNotEqual(false, null);
assertNotEqual(0, null);
assertNotEqual(0, false);
assertNotEqual(0, void 0);
assertEqual(123, new Number(123));
assertEqual(true, new Boolean(true));
assertEqual(false, new Boolean(false));
assertEqual("oyez", new String("oyez"));
});
it("should work for arrays", function () {
assertEqual([1, 2, 3], [1, 2, 3]);
assertEqual([1, [2], 3], [1, [2], 3]);
const a: any[] = [1];
a.push(a, 2);
const b: any[] = [1];
b.push(b, 2);
assertEqual(a, b);
assertEqual(
[1, /*hole*/, 3],
[1, /*hole*/, 3],
);
assertEqual(
[1, /*hole*/, 3],
[1, void 0, 3],
);
// Not equal because the arrays are a different length.
assertNotEqual(
[1, 2, /*hole*/,],
[1, 2],
);
});
it("should work for objects", function () {
assertEqual({
a: 1,
b: 2,
}, {
b: 2,
a: 1,
});
assertNotEqual({
a: 1,
b: 2,
c: 3,
}, {
b: 2,
a: 1,
});
const a: any = {};
a.self = a;
const b: any = {};
b.self = b;
assertEqual(a, b);
b.foo = 42;
assertNotEqual(a, b);
});
it("should consider undefined and missing object properties equivalent", function () {
assertEqual({
a: 1,
b: void 0,
c: 3,
}, {
a: 1,
c: 3,
});
assertEqual({
a: void 0,
b: void 0,
c: void 0,
}, {});
});
it("should work for Error objects", function () {
assertEqual(new Error("oyez"), new Error("oyez"));
assertNotEqual(new Error("oyez"), new Error("onoz"));
});
it("should work for Date objects", function () {
const now = new Date;
const alsoNow = new Date(+now);
assert.notStrictEqual(now, alsoNow);
assertEqual(now, alsoNow);
const later = new Date(+now + 10);
assertNotEqual(now, later);
});
it("should work for RegExp objects", function () {
assert.notStrictEqual(/xy/, /xy/);
assertEqual(/xy/img, /xy/mgi);
assertNotEqual(/xy/img, /x.y/img);
});
it("should work for Set objects", function () {
assertEqual(
new Set().add(1).add(2).add(3).add(2),
new Set().add(3).add(1).add(2).add(1),
);
const obj = {};
assertEqual(
new Set().add(1).add(obj).add(3).add(2),
new Set().add(3).add(obj).add(2).add(1),
);
assertNotEqual(
new Set(),
new Set().add(void 0),
);
});
it("should work for Map objects", function () {
assertEqual(
new Map().set(1, 2).set(2, 3),
new Map().set(2, 3).set(1, 2),
);
assertEqual(
new Map().set(1, 2).set(2, 3).set(1, 0),
new Map().set(2, 3).set(1, 2).set(1, 0),
);
assertNotEqual(
new Map().set(1, 2).set(2, 3).set(1, 0),
new Map().set(2, 3).set(1, 2).set(3, 4),
);
assertEqual(
new Map().set(1, new Set().add(2)),
new Map().set(1, new Set().add(2)),
);
assertNotEqual(
new Map().set(1, new Set().add(2)),
new Map().set(1, new Set().add(2).add(3)),
);
const a = new Map;
a.set(a, a);
const b = new Map;
b.set(a, b);
assertEqual(a, b);
a.set(1, 2);
b.set(1, 2);
assertEqual(a, b);
a.set(3, 4);
assertNotEqual(a, b);
});
it("should tolerate cycles", function () {
const a: any[] = [];
a.push(a);
const b: any[] = [];
b.push(b);
assertEqual(a, b);
assertEqual([a], b);
assertEqual(a, [b]);
assertEqual([a], [b]);
a.push(1);
b.push(1);
assertEqual(a, b);
assertEqual([a, 1], b);
assertEqual(a, [b, 1]);
const ring1 = { self: { self: { self: {} as any }}};
ring1.self.self.self.self = ring1;
const ring2 = { self: { self: {} as any }};
ring2.self.self.self = ring2;
assertEqual(ring1, ring2);
ring1.self.self.self.self = ring1.self;
assertEqual(ring1, ring2);
});
it("should not care about repeated references", function () {
const r = { foo: 42 };
assertEqual(
[r, r, r],
JSON.parse(JSON.stringify([r, r, r])),
);
});
it("should equate non-native functions with the same code", function () {
const fn = () => 1234;
assertEqual(fn, fn);
assertEqual(fn, () => 1234);
// These functions are behaviorally the same, but there's no way to
// decide that question statically.
assertNotEqual(
(a: number) => a + 1,
(b: number) => b + 1,
);
assertEqual(
{ before: 123, fn() { return 4 }, after: 321 },
{ after: 321, before: 123, fn() { return 4 } },
);
assertEqual(Object.assign, Object.assign);
// Since these slice methods are native functions, they happen to have
// exactly the same (censored) code, but we can test their equality by
// reference, since we can generally assume native functions are pure.
assertNotEqual(String.prototype.slice, Array.prototype.slice);
assertEqual(
Function.prototype.toString.call(String.prototype.slice),
Function.prototype.toString.call(Array.prototype.slice),
);
});
it("should equate async functions with the same code", function () {
const fn = async () => 1234;
assertEqual(fn, fn);
assertEqual(fn, async () => 1234);
// These functions are behaviorally the same, but there's no way to
// decide that question statically.
assertNotEqual(
async (a: number) => a + 1,
async (b: number) => b + 1,
);
assertEqual(
{ before: 123, async fn() { return 4 }, after: 321 },
{ after: 321, before: 123, async fn() { return 4 } },
);
});
it("should equate generator functions with the same code", function () {
const fn = function *(): Generator<number> { return yield 1234 };
assertEqual(fn, fn);
assertEqual(fn, function *(): Generator<number> { return yield 1234 });
// These functions are behaviorally the same, but there's no way to
// decide that question statically.
assertNotEqual(
function *(a: number): Generator<number> { return yield a + 1 },
function *(b: number): Generator<number> { return yield b + 1 },
);
assertEqual(
{ before: 123, *fn() { return 4 }, after: 321 },
{ after: 321, before: 123, *fn() { return 4 } },
);
});
it("should equate async generator functions with the same code", function () {
const fn = async function *(): AsyncGenerator<number> { return await (yield 1234) };
assertEqual(fn, fn);
assertEqual(fn, async function *(): AsyncGenerator<number> { return await (yield 1234) });
// These functions are behaviorally the same, but there's no way to
// decide that question statically.
assertNotEqual(
async function *(a: number): AsyncGenerator<number> { return yield a + 1 },
async function *(b: number): AsyncGenerator<number> { return yield b + 1 },
);
assertEqual(
{ before: 123, async *fn() { return 4 }, after: 321 },
{ after: 321, before: 123, async *fn() { return 4 } },
);
});
it('should work for Array Buffers And Typed Arrays', function () {
const hello = new Int8Array([1, 2, 3, 4, 5]);
const world = new Int8Array([1, 2, 3, 4, 5]);
const small = new Int8Array([1, 2, 3, 4])
assertEqual(new DataView(new ArrayBuffer(4)), new DataView(new ArrayBuffer(4)))
assertEqual(new Int16Array([42]), new Int16Array([42]));
assertEqual(new Int32Array(new ArrayBuffer(4)), new Int32Array(new ArrayBuffer(4)))
assertEqual(new ArrayBuffer(2), new ArrayBuffer(2))
assertNotEqual(new Int16Array([1, 2, 3]), new Int16Array([1, 2]));
assertNotEqual(new Int16Array([1, 2, 3]), new Uint16Array([1, 2, 3]))
assertNotEqual(new Int16Array([1, 2, 3]), new Int8Array([1, 2, 3]))
assertNotEqual(new Int32Array(8), new Uint32Array(8));
assertNotEqual(new Int32Array(new ArrayBuffer(8)), new Int32Array(Array.from({ length: 8 })));
assertNotEqual(new ArrayBuffer(1), new ArrayBuffer(2));
assertEqual(hello, world);
assertEqual(hello.buffer, world.buffer);
assertEqual(new DataView(hello.buffer), new DataView(world.buffer));
assertNotEqual(small, world)
assertNotEqual(small.buffer, world.buffer);
assertNotEqual(new DataView(small.buffer), new DataView(world.buffer));
});
it('should work with a kitchen sink', function () {
const foo = {
foo: 'value1',
bar: new Set([1, 2, 3]),
baz: /foo/i,
bat: {
hello: new Map([ ['hello', 'world'] ]),
world: {
aaa: new Map([
[{ foo: /bar/ }, 'sub sub value1'],
]),
bbb: [1, 2, { prop2:1, prop:2 }, 4, 5]
}
},
quz: new Set([{ a:1 , b:2 }]),
qut: new Date(2016, 2, 10),
qar: new Uint8Array([1, 2, 3, 4, 5]),
}
const bar = {
quz: new Set([{ a:1 , b:2 }]),
baz: /foo/i,
foo: 'value1',
bar: new Set([1, 2, 3]),
qar: new Uint8Array([1, 2, 3, 4, 5]),
qut: new Date('2016/03/10'),
bat: {
world: {
aaa: new Map([
[{ foo: /bar/ }, 'sub sub value1'],
]),
bbb: [1, 2, { prop2:1, prop:2 }, 4, 5]
},
hello: new Map([ ['hello', 'world'] ])
}
};
assertNotEqual(foo, bar)
});
describe("performance", function () {
const limit = 1e6;
this.timeout(20000);
function check(a: any, bEqual: any, bNotEqual: any) {
for (let i = 0; i < limit; ++i) {
assert.strictEqual(equal(a, bEqual), true);
assert.strictEqual(equal(bEqual, a), true);
assert.strictEqual(equal(a, bNotEqual), false);
assert.strictEqual(equal(bNotEqual, a), false);
}
}
it("should be fast for arrays", function () {
const a = [1, 2, 3];
check(a, a.slice(0), [1, 2, 4]);
});
it("should be fast for objects", function () {
const a = { a: 1, b: 2, c: 3 };
check(a, { ...a }, { a: 1, b: 3 });
});
it("should be fast for strings", function () {
check('foo', new String('foo'), 'bar');
});
it("should be fast for functions", function () {
check(() => 123, () => 123, () => 321);
});
});
});

View File

@@ -0,0 +1,4 @@
# @wry/trie
A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds
object keys weakly, yet can also hold non-object keys, unlike `WeakMap`.

View File

@@ -0,0 +1,89 @@
'use strict';
// A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds
// object keys weakly, yet can also hold non-object keys, unlike the
// native `WeakMap`.
// If no makeData function is supplied, the looked-up data will be an empty,
// null-prototype Object.
var defaultMakeData = function () { return Object.create(null); };
// Useful for processing arguments objects as well as arrays.
var _a = Array.prototype, forEach = _a.forEach, slice = _a.slice;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var Trie = /** @class */ (function () {
function Trie(weakness, makeData) {
if (weakness === void 0) { weakness = true; }
if (makeData === void 0) { makeData = defaultMakeData; }
this.weakness = weakness;
this.makeData = makeData;
}
Trie.prototype.lookup = function () {
return this.lookupArray(arguments);
};
Trie.prototype.lookupArray = function (array) {
var node = this;
forEach.call(array, function (key) { return node = node.getChildTrie(key); });
return hasOwnProperty.call(node, "data")
? node.data
: node.data = this.makeData(slice.call(array));
};
Trie.prototype.peek = function () {
return this.peekArray(arguments);
};
Trie.prototype.peekArray = function (array) {
var node = this;
for (var i = 0, len = array.length; node && i < len; ++i) {
var map = node.mapFor(array[i], false);
node = map && map.get(array[i]);
}
return node && node.data;
};
Trie.prototype.remove = function () {
return this.removeArray(arguments);
};
Trie.prototype.removeArray = function (array) {
var data;
if (array.length) {
var head = array[0];
var map = this.mapFor(head, false);
var child = map && map.get(head);
if (child) {
data = child.removeArray(slice.call(array, 1));
if (!child.data && !child.weak && !(child.strong && child.strong.size)) {
map.delete(head);
}
}
}
else {
data = this.data;
delete this.data;
}
return data;
};
Trie.prototype.getChildTrie = function (key) {
var map = this.mapFor(key, true);
var child = map.get(key);
if (!child)
map.set(key, child = new Trie(this.weakness, this.makeData));
return child;
};
Trie.prototype.mapFor = function (key, create) {
return this.weakness && isObjRef(key)
? this.weak || (create ? this.weak = new WeakMap : void 0)
: this.strong || (create ? this.strong = new Map : void 0);
};
return Trie;
}());
function isObjRef(value) {
switch (typeof value) {
case "object":
if (value === null)
break;
// Fall through to return true...
case "function":
return true;
}
return false;
}
exports.Trie = Trie;
//# sourceMappingURL=bundle.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,89 @@
'use strict';
// A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds
// object keys weakly, yet can also hold non-object keys, unlike the
// native `WeakMap`.
// If no makeData function is supplied, the looked-up data will be an empty,
// null-prototype Object.
var defaultMakeData = function () { return Object.create(null); };
// Useful for processing arguments objects as well as arrays.
var _a = Array.prototype, forEach = _a.forEach, slice = _a.slice;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var Trie = /** @class */ (function () {
function Trie(weakness, makeData) {
if (weakness === void 0) { weakness = true; }
if (makeData === void 0) { makeData = defaultMakeData; }
this.weakness = weakness;
this.makeData = makeData;
}
Trie.prototype.lookup = function () {
return this.lookupArray(arguments);
};
Trie.prototype.lookupArray = function (array) {
var node = this;
forEach.call(array, function (key) { return node = node.getChildTrie(key); });
return hasOwnProperty.call(node, "data")
? node.data
: node.data = this.makeData(slice.call(array));
};
Trie.prototype.peek = function () {
return this.peekArray(arguments);
};
Trie.prototype.peekArray = function (array) {
var node = this;
for (var i = 0, len = array.length; node && i < len; ++i) {
var map = node.mapFor(array[i], false);
node = map && map.get(array[i]);
}
return node && node.data;
};
Trie.prototype.remove = function () {
return this.removeArray(arguments);
};
Trie.prototype.removeArray = function (array) {
var data;
if (array.length) {
var head = array[0];
var map = this.mapFor(head, false);
var child = map && map.get(head);
if (child) {
data = child.removeArray(slice.call(array, 1));
if (!child.data && !child.weak && !(child.strong && child.strong.size)) {
map.delete(head);
}
}
}
else {
data = this.data;
delete this.data;
}
return data;
};
Trie.prototype.getChildTrie = function (key) {
var map = this.mapFor(key, true);
var child = map.get(key);
if (!child)
map.set(key, child = new Trie(this.weakness, this.makeData));
return child;
};
Trie.prototype.mapFor = function (key, create) {
return this.weakness && isObjRef(key)
? this.weak || (create ? this.weak = new WeakMap : void 0)
: this.strong || (create ? this.strong = new Map : void 0);
};
return Trie;
}());
function isObjRef(value) {
switch (typeof value) {
case "object":
if (value === null)
break;
// Fall through to return true...
case "function":
return true;
}
return false;
}
exports.Trie = Trie;
//# sourceMappingURL=bundle.cjs.map

View File

@@ -0,0 +1,16 @@
export declare class Trie<Data> {
private weakness;
private makeData;
private weak?;
private strong?;
private data?;
constructor(weakness?: boolean, makeData?: (array: any[]) => Data);
lookup<T extends any[]>(...array: T): Data;
lookupArray<T extends IArguments | any[]>(array: T): Data;
peek<T extends any[]>(...array: T): Data | undefined;
peekArray<T extends IArguments | any[]>(array: T): Data | undefined;
remove(...array: any[]): Data | undefined;
removeArray<T extends IArguments | any[]>(array: T): Data | undefined;
private getChildTrie;
private mapFor;
}

View File

@@ -0,0 +1,82 @@
// A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds
// object keys weakly, yet can also hold non-object keys, unlike the
// native `WeakMap`.
// If no makeData function is supplied, the looked-up data will be an empty,
// null-prototype Object.
const defaultMakeData = () => Object.create(null);
// Useful for processing arguments objects as well as arrays.
const { forEach, slice } = Array.prototype;
const { hasOwnProperty } = Object.prototype;
export class Trie {
constructor(weakness = true, makeData = defaultMakeData) {
this.weakness = weakness;
this.makeData = makeData;
}
lookup() {
return this.lookupArray(arguments);
}
lookupArray(array) {
let node = this;
forEach.call(array, key => node = node.getChildTrie(key));
return hasOwnProperty.call(node, "data")
? node.data
: node.data = this.makeData(slice.call(array));
}
peek() {
return this.peekArray(arguments);
}
peekArray(array) {
let node = this;
for (let i = 0, len = array.length; node && i < len; ++i) {
const map = node.mapFor(array[i], false);
node = map && map.get(array[i]);
}
return node && node.data;
}
remove() {
return this.removeArray(arguments);
}
removeArray(array) {
let data;
if (array.length) {
const head = array[0];
const map = this.mapFor(head, false);
const child = map && map.get(head);
if (child) {
data = child.removeArray(slice.call(array, 1));
if (!child.data && !child.weak && !(child.strong && child.strong.size)) {
map.delete(head);
}
}
}
else {
data = this.data;
delete this.data;
}
return data;
}
getChildTrie(key) {
const map = this.mapFor(key, true);
let child = map.get(key);
if (!child)
map.set(key, child = new Trie(this.weakness, this.makeData));
return child;
}
mapFor(key, create) {
return this.weakness && isObjRef(key)
? this.weak || (create ? this.weak = new WeakMap : void 0)
: this.strong || (create ? this.strong = new Map : void 0);
}
}
function isObjRef(value) {
switch (typeof value) {
case "object":
if (value === null)
break;
// Fall through to return true...
case "function":
return true;
}
return false;
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,45 @@
{
"name": "@wry/trie",
"version": "0.5.0",
"author": "Ben Newman <ben@eloper.dev>",
"description": "https://en.wikipedia.org/wiki/Trie",
"license": "MIT",
"type": "module",
"main": "lib/bundle.cjs",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [
"trie",
"prefix",
"weak",
"dictionary",
"lexicon"
],
"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:before && npm run tsc && npm run rollup && npm run clean:after",
"clean:before": "rimraf lib",
"tsc": "npm run tsc:es5 && npm run tsc:esm",
"tsc:es5": "tsc -p tsconfig.es5.json",
"tsc:esm": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"clean:after": "rimraf lib/es5",
"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"
}
}

View File

@@ -0,0 +1 @@
export { default } from "../../shared/rollup.config.js";

View File

@@ -0,0 +1,104 @@
// A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds
// object keys weakly, yet can also hold non-object keys, unlike the
// native `WeakMap`.
// If no makeData function is supplied, the looked-up data will be an empty,
// null-prototype Object.
const defaultMakeData = () => Object.create(null);
// Useful for processing arguments objects as well as arrays.
const { forEach, slice } = Array.prototype;
const { hasOwnProperty } = Object.prototype;
export class Trie<Data> {
// Since a `WeakMap` cannot hold primitive values as keys, we need a
// backup `Map` instance to hold primitive keys. Both `this._weakMap`
// and `this._strongMap` are lazily initialized.
private weak?: WeakMap<any, Trie<Data>>;
private strong?: Map<any, Trie<Data>>;
private data?: Data;
constructor(
private weakness = true,
private makeData: (array: any[]) => Data = defaultMakeData,
) {}
public lookup<T extends any[]>(...array: T): Data;
public lookup(): Data {
return this.lookupArray(arguments);
}
public lookupArray<T extends IArguments | any[]>(array: T): Data {
let node: Trie<Data> = this;
forEach.call(array, key => node = node.getChildTrie(key));
return hasOwnProperty.call(node, "data")
? node.data as Data
: node.data = this.makeData(slice.call(array));
}
public peek<T extends any[]>(...array: T): Data | undefined;
public peek(): Data | undefined {
return this.peekArray(arguments);
}
public peekArray<T extends IArguments | any[]>(array: T): Data | undefined {
let node: Trie<Data> | undefined = this;
for (let i = 0, len = array.length; node && i < len; ++i) {
const map = node.mapFor(array[i], false);
node = map && map.get(array[i]);
}
return node && node.data;
}
public remove(...array: any[]): Data | undefined;
public remove(): Data | undefined {
return this.removeArray(arguments);
}
public removeArray<T extends IArguments | any[]>(array: T): Data | undefined {
let data: Data | undefined;
if (array.length) {
const head = array[0];
const map = this.mapFor(head, false);
const child = map && map.get(head);
if (child) {
data = child.removeArray(slice.call(array, 1));
if (!child.data && !child.weak && !(child.strong && child.strong.size)) {
map.delete(head);
}
}
} else {
data = this.data;
delete this.data;
}
return data;
}
private getChildTrie(key: any) {
const map = this.mapFor(key, true)!;
let child = map.get(key);
if (!child) map.set(key, child = new Trie<Data>(this.weakness, this.makeData));
return child;
}
private mapFor(key: any, create: boolean): Trie<Data>["weak" | "strong"] | undefined {
return this.weakness && isObjRef(key)
? this.weak || (create ? this.weak = new WeakMap : void 0)
: this.strong || (create ? this.strong = new Map : void 0);
}
}
function isObjRef(value: any) {
switch (typeof value) {
case "object":
if (value === null) break;
// Fall through to return true...
case "function":
return true;
}
return false;
}

View File

@@ -0,0 +1,207 @@
import * as assert from "assert";
import { Trie } from "../index.js";
describe("Trie", function () {
it("can be imported", function () {
assert.strictEqual(typeof Trie, "function");
});
it("can hold objects weakly", function () {
const trie = new Trie<object>(true);
assert.strictEqual((trie as any).weakness, true);
const obj1 = {};
assert.strictEqual(
trie.lookup(obj1, 2, 3),
trie.lookup(obj1, 2, 3),
);
const obj2 = {};
assert.notStrictEqual(
trie.lookup(1, obj2),
trie.lookup(1, obj2, 3),
);
assert.strictEqual((trie as any).weak.has(obj1), true);
assert.strictEqual((trie as any).strong.has(obj1), false);
assert.strictEqual((trie as any).strong.get(1).weak.has(obj2), true);
assert.strictEqual((trie as any).strong.get(1).weak.get(obj2).strong.has(3), true);
});
it("can disable WeakMap", function () {
const trie = new Trie<object>(false);
assert.strictEqual((trie as any).weakness, false);
const obj1 = {};
assert.strictEqual(
trie.lookup(obj1, 2, 3),
trie.lookup(obj1, 2, 3),
);
const obj2 = {};
assert.notStrictEqual(
trie.lookup(1, obj2),
trie.lookup(1, obj2, 3),
);
assert.strictEqual(typeof (trie as any).weak, "undefined");
assert.strictEqual((trie as any).strong.has(obj1), true);
assert.strictEqual((trie as any).strong.has(1), true);
assert.strictEqual((trie as any).strong.get(1).strong.has(obj2), true);
assert.strictEqual((trie as any).strong.get(1).strong.get(obj2).strong.has(3), true);
});
it("can produce data types other than Object", function () {
const symbolTrie = new Trie(true, args => Symbol.for(args.join(".")));
const s123 = symbolTrie.lookup(1, 2, 3);
assert.strictEqual(s123.toString(), "Symbol(1.2.3)");
assert.strictEqual(s123, symbolTrie.lookup(1, 2, 3));
assert.strictEqual(s123, symbolTrie.lookupArray([1, 2, 3]));
const sNull = symbolTrie.lookup();
assert.strictEqual(sNull.toString(), "Symbol()");
const regExpTrie = new Trie(true, args => new RegExp("^(" + args.join("|") + ")$"));
const rXYZ = regExpTrie.lookup("x", "y", "z");
assert.strictEqual(rXYZ.test("w"), false);
assert.strictEqual(rXYZ.test("x"), true);
assert.strictEqual(rXYZ.test("y"), true);
assert.strictEqual(rXYZ.test("z"), true);
assert.strictEqual(String(rXYZ), "/^(x|y|z)$/");
class Data {
constructor(public readonly args: any[]) {}
}
const dataTrie = new Trie(true, args => new Data(args));
function checkData(...args: any[]) {
const data = dataTrie.lookupArray(args);
assert.strictEqual(data instanceof Data, true);
assert.notStrictEqual(data.args, args);
assert.deepStrictEqual(data.args, args);
assert.strictEqual(data, dataTrie.lookup(...args));
assert.strictEqual(data, dataTrie.lookupArray(arguments));
return data;
}
const datas = [
checkData(),
checkData(1),
checkData(1, 2),
checkData(2),
checkData(2, 3),
checkData(true, "a"),
checkData(/asdf/i, "b", function oyez() {}),
];
// Verify that all Data objects are distinct.
assert.strictEqual(new Set(datas).size, datas.length);
});
it("can peek at values", function () {
const trie = new Trie(true, (args) => args);
const obj = {};
assert.strictEqual(trie.peek(1, 2, 'x'), undefined);
assert.strictEqual(trie.peek(1, 2, obj), undefined);
assert.strictEqual(trie.peekArray([1, 2, 'x']), undefined);
assert.strictEqual(trie.peekArray([1, 2, obj]), undefined);
// peek/peekArray should not create anything on its own
assert.strictEqual(trie['weak'], undefined);
assert.strictEqual(trie['strong'], undefined);
assert.strictEqual(trie['data'], undefined);
const data1 = trie.lookup(1, 2, 'x');
const data2 = trie.lookup(1, 2, obj);
assert.strictEqual(trie.peek(1, 2, 'x'), data1);
assert.strictEqual(trie.peek(1, 2, obj), data2);
assert.strictEqual(trie.peekArray([1, 2, 'x']), data1);
assert.strictEqual(trie.peekArray([1, 2, obj]), data2);
});
describe("can remove values", function () {
it("will remove values", () => {
const trie = new Trie(true, (args) => args);
trie.lookup(1, 2, "x");
trie.remove(1, 2, "x");
assert.strictEqual(trie.peek(1, 2, "x"), undefined);
});
it("removing will return the value", () => {
const trie = new Trie(true, (args) => args);
const data = trie.lookup(1, 2, "x");
assert.strictEqual(trie.remove(1, 2, "x"), data);
});
it("will remove empty parent nodes", () => {
const trie = new Trie(true, (args) => args);
const data = trie.lookup(1, 2, "x");
assert.strictEqual(trie.peek(1, 2, "x"), data);
assert.equal(pathExistsInTrie(trie, 1, 2, "x"), true);
assert.strictEqual(trie.remove(1, 2, "x"), data);
assert.equal(pathExistsInTrie(trie, 1), false);
});
it("will not remove parent nodes with other children", () => {
const trie = new Trie(true, (args) => args);
trie.lookup(1, 2, "x");
const data = trie.lookup(1, 2);
trie.remove(1, 2, "x");
assert.strictEqual(trie.peek(1, 2, "x"), undefined);
assert.strictEqual(trie.peek(1, 2), data);
});
it("will remove data, not the full node, if a node still has children", () => {
const trie = new Trie(true, (args) => args);
trie.lookup(1, 2);
const data = trie.lookup(1, 2, "x");
trie.remove(1, 2);
assert.strictEqual(trie.peek(1, 2), undefined);
assert.strictEqual(trie.peek(1, 2, "x"), data);
});
it("will remove direct children", () => {
const trie = new Trie(true, (args) => args);
trie.lookup(1);
trie.remove(1);
assert.strictEqual(trie.peek(1), undefined);
});
it("will remove nodes from WeakMaps", () => {
const trie = new Trie(true, (args) => args);
const obj = {};
const data = trie.lookup(1, obj, "x");
assert.equal(pathExistsInTrie(trie, 1), true);
assert.strictEqual(trie.remove(1, obj, "x"), data);
assert.strictEqual(trie.peek(1, obj, "x"), undefined);
assert.equal(pathExistsInTrie(trie, 1, obj), false);
});
it("will not remove nodes if they contain an (even empty) WeakMap", () => {
const trie = new Trie(true, (args) => args);
const obj = {};
const data = trie.lookup(1, 2, "x");
trie.lookup(1, obj);
trie.remove(1, obj);
assert.strictEqual(trie.peek(1, 2, "x"), data);
assert.equal(pathExistsInTrie(trie, 1), true);
assert.equal(pathExistsInTrie(trie, 1, 2), true);
assert.strictEqual(trie.remove(1, 2, "x"), data);
assert.equal(pathExistsInTrie(trie, 1), true);
assert.equal(pathExistsInTrie(trie, 1, 2), false);
});
});
function pathExistsInTrie(trie: Trie<unknown>, ...path: any[]) {
return (
path.reduce((node: Trie<unknown> | undefined, key: any) => {
const map: Trie<unknown>["weak" | "strong"] =
// not the full implementation but enough for a test
trie["weakness"] && typeof key === "object"
? node?.["weak"]
: node?.["strong"];
return map?.get(key);
}, trie) !== undefined
);
}
});