Skip to content

allows memoization of circular structures #23

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
npm-debug.log
.DS_Store
coverage/
.vscode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newline at EOF. Reasoning

1 change: 1 addition & 0 deletions benchmark/cache/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use stict'
const ora = require('ora')
const Table = require('cli-table2')
const debug = require('logdown')()
Expand Down
1 change: 1 addition & 0 deletions benchmark/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use stict'
const ora = require('ora')
const Table = require('cli-table2')
const debug = require('logdown')()
Expand Down
1 change: 1 addition & 0 deletions benchmark/recursive-solo.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use stict'
const ora = require('ora')
const logger = require('logdown')()
const Table = require('cli-table2')
Expand Down
1 change: 1 addition & 0 deletions benchmark/strategy/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use stict'
const ora = require('ora')
const Table = require('cli-table2')
const debug = require('logdown')()
Expand Down
11 changes: 7 additions & 4 deletions benchmark/strategy/partial-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ function strategy (fn, options) {
return cache.get(cacheKey)
}

function variadic (fn, cache, serializer, ...args) {
function variadic (fn, cache, serializer) {
for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
args[_key - 3] = arguments[_key];
}
var cacheKey = serializer(args)

if (!cache.has(cacheKey)) {
Expand All @@ -34,9 +37,9 @@ function strategy (fn, options) {
return cache.get(cacheKey)
}

var memoized = fn.length === 1
? monadic
: variadic
var memoized = fn.length === 1 ?
monadic :
variadic

memoized = memoized.bind(this, fn, options.cache.create(), options.serializer)
memoized.label = 'strategy: Partial application, cache: ' + options.cache.label + ', serializer: ' + options.serializer.label
Expand Down
16 changes: 16 additions & 0 deletions src/delme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var memoize = require('../src')

var circular = {
a: 'foo'
}
circular.b = circular

function circularFunction (a) {
return a.a
}

var memoizedCircularFunction = memoize(circularFunction)

// Assertions
var one = memoizedCircularFunction(circular)
var two = memoizedCircularFunction(circular)
109 changes: 85 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict'
const util = require('util')
//
// Main
//
Expand Down Expand Up @@ -57,7 +59,11 @@ function strategyDefault (fn, options) {
return cache.get(cacheKey)
}

function variadic (fn, cache, serializer, ...args) {
function variadic (fn, cache, serializerrgs) {
for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
args[_key - 3] = arguments[_key];
}

var cacheKey = serializer(args)

if (!cache.has(cacheKey)) {
Expand All @@ -69,9 +75,9 @@ function strategyDefault (fn, options) {
return cache.get(cacheKey)
}

var memoized = fn.length === 1
? monadic
: variadic
var memoized = fn.length === 1 ?
monadic :
variadic

memoized = memoized.bind(
this,
Expand All @@ -89,36 +95,91 @@ function strategyDefault (fn, options) {
// Serializer
//

function serializerDefault (...args) {
return JSON.stringify(args)
function customReplacer(args) {
var cache = [];
const replacer = function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Circular reference found, discard key
return
}
// Store value in our collection
cache.push(value)
}
}
cache = null // Enable garbage collection
return replacer
}

//
// Cache
// Serializer
//

class ObjectWithoutPrototypeCache {
constructor () {
this.cache = Object.create(null)
function serializerDefault () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key]
}

has (key) {
return (key in this.cache)
try {
// try the fastest way first.
return JSON.stringify(args)
} catch (error) {
if (error instanceof TypeError &&
error.message === 'Converting circular structure to JSON') {
// if node
if (util && util.inspect) {
return JSON.stringify(util.inspect(args))
} else {
return JSON.stringify(args, customReplacer)
}
} else {
throw error // let others bubble up
}
}
}

get (key) {
return this.cache[key]
}
//
// Cache
//

set (key, value) {
this.cache[key] = value
}
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var ObjectWithoutPrototypeCache = function () {
function ObjectWithoutPrototypeCache() {
_classCallCheck(this, ObjectWithoutPrototypeCache);

delete (key) {
delete this.cache[key]
this.cache = Object.create(null);
}
}

const cacheDefault = {
create: () => new ObjectWithoutPrototypeCache()
}
_createClass(ObjectWithoutPrototypeCache, [{
key: "has",
value: function has(key) {
return key in this.cache;
}
}, {
key: "get",
value: function get(key) {
return this.cache[key];
}
}, {
key: "set",
value: function set(key, value) {
this.cache[key] = value;
}
}, {
key: "delete",
value: function _delete(key) {
delete this.cache[key];
}
}]);

return ObjectWithoutPrototypeCache;
}();

var cacheDefault = {
create: function create() {
return new ObjectWithoutPrototypeCache();
}
};
18 changes: 18 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,21 @@ test('inject custom serializer', function () {

expect(serializerMethodExecutionCount).toBe(2)
})

test('memoize circular JSON', function () {
var circular = {
a: 'foo'
}
circular.b = circular

function circularFunction(a) {
return a.a
}

var memoizedCircularFunction = memoize(circularFunction)

// Assertions

expect(memoizedCircularFunction(circular)).toBe("foo")
expect(memoizedCircularFunction(circular)).toBe("foo")
})