From ada595549a5c4c6c853756d598846b180941c6da Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 10 Dec 2024 21:45:25 -0800 Subject: [PATCH] [Refactor] extract implementations to `side-channel-weakmap`, `side-channel-map`, `side-channel-list` --- .eslintrc | 1 + index.d.ts | 17 ++--- index.js | 169 +++++--------------------------------------------- list.d.ts | 14 ----- package.json | 13 ++-- test/index.js | 131 ++++++++++++++++++++++---------------- 6 files changed, 106 insertions(+), 239 deletions(-) delete mode 100644 list.d.ts diff --git a/.eslintrc b/.eslintrc index 93978e7..9b13ad8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "extends": "@ljharb", "rules": { + "id-length": 0, "max-lines-per-function": 0, "multiline-comment-style": 1, "new-cap": [2, { "capIsNewExceptions": ["GetIntrinsic"] }], diff --git a/index.d.ts b/index.d.ts index 4d0a3cb..18c6317 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,13 +1,14 @@ +import getSideChannelList from 'side-channel-list'; +import getSideChannelMap from 'side-channel-map'; +import getSideChannelWeakMap from 'side-channel-weakmap'; + declare namespace getSideChannel { - type Channel = { - assert: (key: K) => void; - has: (key: K) => boolean; - get: (key: K) => V | undefined; - set: (key: K, value: V) => void; - delete: (key: K) => boolean; - } + type Channel = + | getSideChannelList.Channel + | ReturnType, false>> + | ReturnType, false>>; } -declare function getSideChannel(): getSideChannel.Channel; +declare function getSideChannel(): getSideChannel.Channel; export = getSideChannel; diff --git a/index.js b/index.js index fb546d2..a8a9b05 100644 --- a/index.js +++ b/index.js @@ -1,107 +1,18 @@ 'use strict'; -var GetIntrinsic = require('get-intrinsic'); -var callBound = require('call-bind/callBound'); -var inspect = require('object-inspect'); - var $TypeError = require('es-errors/type'); -var $WeakMap = GetIntrinsic('%WeakMap%', true); -var $Map = GetIntrinsic('%Map%', true); - -/** @template T @typedef { any>(this: ThisParameterType, ...args: Parameters) => ReturnType} CallBind */ - -/** @type {CallBind<(this: WeakMap, key: K) => V>} */ -var $weakMapGet = callBound('WeakMap.prototype.get', true); -/** @type {CallBind} */ -var $weakMapSet = callBound('WeakMap.prototype.set', true); -/** @type {CallBind} */ -var $weakMapHas = callBound('WeakMap.prototype.has', true); -/** @type {CallBind} */ -var $weakMapDelete = callBound('WeakMap.prototype.delete', true); -/** @type {CallBind} */ -var $mapGet = callBound('Map.prototype.get', true); -/** @type {CallBind} */ -var $mapSet = callBound('Map.prototype.set', true); -/** @type {CallBind} */ -var $mapHas = callBound('Map.prototype.has', true); -/** @type {CallBind} */ -var $mapDelete = callBound('Map.prototype.delete', true); - -/* -* This function traverses the list returning the node corresponding to the given key. -* -* That node is also moved to the head of the list, so that if it's accessed again we don't need to traverse the whole list. -* By doing so, all the recently used nodes can be accessed relatively quickly. -*/ -/** @type {import('./list.d.ts').listGetNode} */ -// eslint-disable-next-line consistent-return -var listGetNode = function (list, key, isDelete) { - /** @type {typeof list | NonNullable<(typeof list)['next']>} */ - var prev = list; - /** @type {(typeof list)['next']} */ - var curr; - // eslint-disable-next-line eqeqeq - for (; (curr = prev.next) != null; prev = curr) { - if (curr.key === key) { - prev.next = curr.next; - if (!isDelete) { - // eslint-disable-next-line no-extra-parens - curr.next = /** @type {NonNullable} */ (list.next); - list.next = curr; // eslint-disable-line no-param-reassign - } - return curr; - } - } -}; +var inspect = require('object-inspect'); +var getSideChannelList = require('side-channel-list'); +var getSideChannelMap = require('side-channel-map'); +var getSideChannelWeakMap = require('side-channel-weakmap'); -/** @type {import('./list.d.ts').listGet} */ -// eslint-disable-next-line consistent-return -var listGet = function (objects, key) { - if (objects) { - var node = listGetNode(objects, key); - return node && node.value; - } -}; -/** @type {import('./list.d.ts').listSet} */ -var listSet = function (objects, key, value) { - if (objects) { - var node = listGetNode(objects, key); - if (node) { - node.value = value; - } else { - // Prepend the new node to the beginning of the list - objects.next = /** @type {import('./list.d.ts').ListNode} */ ({ // eslint-disable-line no-param-reassign, no-extra-parens - key: key, - next: objects.next, - value: value - }); - } - } -}; -/** @type {import('./list.d.ts').listHas} */ -var listHas = function (objects, key) { - if (!objects) { - return false; - } - return !!listGetNode(objects, key); -}; -/** @type {import('./list.d.ts').listDelete} */ -// eslint-disable-next-line consistent-return -var listDelete = function (objects, key) { - if (objects) { - return listGetNode(objects, key, true); - } -}; +var makeChannel = getSideChannelWeakMap || getSideChannelMap || getSideChannelList; /** @type {import('.')} */ module.exports = function getSideChannel() { /** @typedef {ReturnType} Channel */ - /** @typedef {Parameters[0]} K */ - /** @typedef {Parameters[1]} V */ - /** @type {WeakMap | undefined} */ var $wm; - /** @type {Map | undefined} */ var $m; - /** @type {import('./list.d.ts').RootNode | undefined} */ var $o; + /** @type {Channel | undefined} */ var $channelData; /** @type {Channel} */ var channel = { @@ -111,72 +22,20 @@ module.exports = function getSideChannel() { } }, 'delete': function (key) { - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if ($wm) { - return $weakMapDelete($wm, key); - } - } else if ($Map) { - if ($m) { - return $mapDelete($m, key); - } - } else { - if ($o) { // eslint-disable-line no-lonely-if - var root = $o && $o.next; - var deletedNode = listDelete($o, key); - if (deletedNode && root && root === deletedNode) { - $o = void undefined; - } - return !!deletedNode; - } - } - return false; + return !!$channelData && $channelData['delete'](key); }, - get: function (key) { // eslint-disable-line consistent-return - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if ($wm) { - return $weakMapGet($wm, key); - } - } else if ($Map) { - if ($m) { - return $mapGet($m, key); - } - } else { - return listGet($o, key); - } + get: function (key) { + return $channelData && $channelData.get(key); }, has: function (key) { - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if ($wm) { - return $weakMapHas($wm, key); - } - } else if ($Map) { - if ($m) { - return $mapHas($m, key); - } - } else { - return listHas($o, key); - } - return false; + return !!$channelData && $channelData.has(key); }, set: function (key, value) { - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if (!$wm) { - $wm = new $WeakMap(); - } - $weakMapSet($wm, key, value); - } else if ($Map) { - if (!$m) { - $m = new $Map(); - } - $mapSet($m, key, value); - } else { - if (!$o) { - // Initialize the linked list as an empty node, so that we don't have to special-case handling of the first node: we can always refer to it as (previous node).next, instead of something like (list).head - $o = { next: void undefined }; - } - // eslint-disable-next-line no-extra-parens - listSet(/** @type {NonNullable} */ ($o), key, value); + if (!$channelData) { + $channelData = makeChannel(); } + + $channelData.set(key, value); } }; // @ts-expect-error TODO: figure out why this is erroring diff --git a/list.d.ts b/list.d.ts deleted file mode 100644 index cc8f649..0000000 --- a/list.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -type ListNode = { - key: K; - next: undefined | ListNode; - value: T; -}; -type RootNode = { - next: undefined | ListNode; -}; - -export function listGetNode(list: RootNode, key: ListNode['key'], isDelete?: boolean): ListNode | undefined; -export function listGet(objects: undefined | RootNode, key: ListNode['key']): T | undefined; -export function listSet(objects: undefined | RootNode, key: ListNode['key'], value: T): void; -export function listHas(objects: undefined | RootNode, key: ListNode['key']): boolean; -export function listDelete(objects: undefined | RootNode, key: ListNode['key']): ListNode | undefined; diff --git a/package.json b/package.json index 975e5ff..41f59b9 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Store information about any JS value in a side channel. Uses WeakMap if available.", "main": "index.js", "exports": { - "./package.json": "./package.json", - ".": "./index.js" + ".": "./index.js", + "./package.json": "./package.json" }, "types": "./index.d.ts", "scripts": { @@ -43,17 +43,16 @@ }, "homepage": "https://github.com/ljharb/side-channel#readme", "dependencies": { - "call-bind": "^1.0.8", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.1", "@ljharb/eslint-config": "^21.1.1", "@ljharb/tsconfig": "^0.2.2", - "@types/call-bind": "^1.0.5", - "@types/get-intrinsic": "^1.2.3", "@types/object-inspect": "^1.13.0", "@types/tape": "^5.6.5", "auto-changelog": "^2.5.0", diff --git a/test/index.js b/test/index.js index 8da3200..bd1e7c2 100644 --- a/test/index.js +++ b/test/index.js @@ -4,80 +4,101 @@ var test = require('tape'); var getSideChannel = require('../'); -test('export', function (t) { - t.equal(typeof getSideChannel, 'function', 'is a function'); - t.equal(getSideChannel.length, 0, 'takes no arguments'); +test('getSideChannel', function (t) { + t.test('export', function (st) { + st.equal(typeof getSideChannel, 'function', 'is a function'); - var channel = getSideChannel(); - t.ok(channel, 'is truthy'); - t.equal(typeof channel, 'object', 'is an object'); + st.equal(getSideChannel.length, 0, 'takes no arguments'); - t.end(); -}); + var channel = getSideChannel(); + st.ok(channel, 'is truthy'); + st.equal(typeof channel, 'object', 'is an object'); + st.end(); + }); -test('assert', function (t) { - var channel = getSideChannel(); - t['throws']( - function () { channel.assert({}); }, - TypeError, - 'nonexistent value throws' - ); + t.test('assert', function (st) { + var channel = getSideChannel(); + st['throws']( + function () { channel.assert({}); }, + TypeError, + 'nonexistent value throws' + ); - var o = {}; - channel.set(o, 'data'); - t.doesNotThrow(function () { channel.assert(o); }, 'existent value noops'); + var o = {}; + channel.set(o, 'data'); + st.doesNotThrow(function () { channel.assert(o); }, 'existent value noops'); - t.end(); -}); + st.end(); + }); -test('has', function (t) { - var channel = getSideChannel(); - /** @type {unknown[]} */ var o = []; + t.test('has', function (st) { + var channel = getSideChannel(); + /** @type {unknown[]} */ var o = []; - t.equal(channel.has(o), false, 'nonexistent value yields false'); + st.equal(channel.has(o), false, 'nonexistent value yields false'); - channel.set(o, 'foo'); - t.equal(channel.has(o), true, 'existent value yields true'); + channel.set(o, 'foo'); + st.equal(channel.has(o), true, 'existent value yields true'); - t.equal(channel.has('abc'), false, 'non object value non existent yields false'); + st.equal(channel.has('abc'), false, 'non object value non existent yields false'); - channel.set('abc', 'foo'); - t.equal(channel.has('abc'), true, 'non object value that exists yields true'); + channel.set('abc', 'foo'); + st.equal(channel.has('abc'), true, 'non object value that exists yields true'); - t.end(); -}); + st.end(); + }); -test('get', function (t) { - var channel = getSideChannel(); - var o = {}; - t.equal(channel.get(o), undefined, 'nonexistent value yields undefined'); + t.test('get', function (st) { + var channel = getSideChannel(); + var o = {}; + st.equal(channel.get(o), undefined, 'nonexistent value yields undefined'); - var data = {}; - channel.set(o, data); - t.equal(channel.get(o), data, '"get" yields data set by "set"'); + var data = {}; + channel.set(o, data); + st.equal(channel.get(o), data, '"get" yields data set by "set"'); - t.end(); -}); + st.end(); + }); + + t.test('set', function (st) { + var channel = getSideChannel(); + var o = function () {}; + st.equal(channel.get(o), undefined, 'value not set'); + + channel.set(o, 42); + st.equal(channel.get(o), 42, 'value was set'); + + channel.set(o, Infinity); + st.equal(channel.get(o), Infinity, 'value was set again'); + + var o2 = {}; + channel.set(o2, 17); + st.equal(channel.get(o), Infinity, 'o is not modified'); + st.equal(channel.get(o2), 17, 'o2 is set'); + + channel.set(o, 14); + st.equal(channel.get(o), 14, 'o is modified'); + st.equal(channel.get(o2), 17, 'o2 is not modified'); + + st.end(); + }); + + t.test('delete', function (st) { + var channel = getSideChannel(); + var o = {}; + st.equal(channel['delete']({}), false, 'nonexistent value yields false'); -test('set', function (t) { - var channel = getSideChannel(); - var o = function () {}; - t.equal(channel.get(o), undefined, 'value not set'); + channel.set(o, 42); + st.equal(channel.has(o), true, 'value is set'); - channel.set(o, 42); - t.equal(channel.get(o), 42, 'value was set'); + st.equal(channel['delete']({}), false, 'nonexistent value still yields false'); - channel.set(o, Infinity); - t.equal(channel.get(o), Infinity, 'value was set again'); + st.equal(channel['delete'](o), true, 'deleted value yields true'); - var o2 = {}; - channel.set(o2, 17); - t.equal(channel.get(o), Infinity, 'o is not modified'); - t.equal(channel.get(o2), 17, 'o2 is set'); + st.equal(channel.has(o), false, 'value is no longer set'); - channel.set(o, 14); - t.equal(channel.get(o), 14, 'o is modified'); - t.equal(channel.get(o2), 17, 'o2 is not modified'); + st.end(); + }); t.end(); });