Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

libtuple

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

libtuple - npm Package Compare versions

Comparing version
0.0.7
to
0.0.8
+38
Dict.mjs
import Tuple from './Tuple.mjs';
import { size, keys } from './Tuple.mjs';
const base = Object.create(null);
base.toString = Object.prototype.toString;
base[Symbol.toStringTag] = 'Dict';
base[Symbol.iterator] = function() {
let index = 0;
return { next: () => {
const iteration = index++;
if(this[size] < index)
{
return { done: true };
}
return {value: [this[keys][iteration], this[this[keys][iteration]]], done: false };
}};
};
export default function Dict(obj = {})
{
if(new.target)
{
throw new Error('"Dict" is not a constructor. Create a Record by invoking the function directly.');
}
if(!obj || typeof obj !== 'object')
{
throw new Error('Parameter must be an object.');
}
const keys = Object.keys(obj);
const values = Object.values(obj);
const tagged = Tuple.bind({args: obj, base, length: keys.length, keys});
return tagged('dict', Tuple(...keys), Tuple(...values));
}
import Tuple from './Tuple.mjs';
import { _index, size } from './Tuple.mjs';
const base = Object.create(null);
base.toString = Object.prototype.toString;
base[Symbol.toStringTag] = 'Group';
base.toJSON = function() {
return [...this];
};
base[Symbol.iterator] = function() {
let index = 0;
return { next: () => {
const iteration = index++;
if(this[size] < index)
{
return { done: true };
}
return { value: this[iteration], done: false };
}};
};
export default function Group(...args)
{
if(new.target)
{
throw new Error('"Group" is not a constructor. Create a Group by invoking the function directly.');
}
const tuples = args.map(a => Tuple(a)).sort((a, b) => a[_index] < b[_index] ? -1 : 1);
const tagged = Tuple.bind({args: tuples.map(t => t[0]), base});
return tagged(...tuples, 'group');
}
export { default as Tuple } from './Tuple.mjs';
export { default as Group } from './Group.mjs';
export { default as Record } from './Record.mjs';
export { default as Dict } from './Dict.mjs';
export { default as Schema } from './Schema.mjs';
import Tuple from './Tuple.mjs';
import { size, keys } from './Tuple.mjs';
const base = Object.create(null);
base.toString = Object.prototype.toString;
base[Symbol.toStringTag] = 'Record';
base[Symbol.iterator] = function() {
let index = 0;
return { next: () => {
const iteration = index++;
if(this[size] < index)
{
return { done: true };
}
return {value: [this[keys][iteration], this[this[keys][iteration]]], done: false };
}};
};
export default function Record(obj = {})
{
if(new.target)
{
throw new Error('"Record" is not a constructor. Create a Record by invoking the function directly.');
}
if(!obj || typeof obj !== 'object')
{
throw new Error('Parameter must be an object.');
}
const entries = Object.fromEntries(Object.entries(obj).sort((a, b) => a[0] < b[0] ? -1 : 1));
const keys = Object.keys(entries);
const values = Object.values(entries);
const tagged = Tuple.bind({args: obj, base, length: keys.length, keys});
return tagged(Tuple(...keys), Tuple(...values), 'record');
}
import Tuple from './Tuple.mjs';
import Group from './Group.mjs';
import Record from './Record.mjs';
import Dict from './Dict.mjs';
/**
* @callback SchemaMapper
*/
const Schema = {
/**
* Map one or more values to a Tuple.
* Will append additional properties as unmapped values if more values are provided than appear in the schema.
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
tuple(...schema)
{
return (args = [], path = '') => {
return Tuple(...args.map(
(arg, index) => schema[index] ? schema[index](arg, `${path || 'root'}[${index}]`): arg
));
}
},
/**
* Map one or more values to a Group.
* Will append additional properties as unmapped values if more values are provided than appear in the schema.
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
group(...schema)
{
return (args = [], path = '') => {
return Group(...args.map(
(arg, index) => schema[index] ? schema[index](arg, `${path || 'root'}[${index}]`): arg
));
}
},
/**
* Map an object to a Record.
* Will append additional properties as unmapped values if more values are provided than appear in the schema.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
record(schema = {})
{
return (arg, path = '') => {
const entries = Object.entries(schema);
return Record(Object.assign(
{},
arg,
Object.fromEntries(
entries.map(([key, schema]) => [key, schema(arg[key], `${path || 'root'}[${key}]`)])
)
));
}
},
/**
* Map an object to a Record.
* Will append additional properties as unmapped values if more values are provided than appear in the schema.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
dict(schema = {})
{
return (arg, path = '') => {
const entries = Object.entries(arg);
return Dict(Object.fromEntries(
entries.map(([key, value]) => [key, schema[key] ? schema[key](value, `${path || 'root'}[${key}]`) : value])
));
}
},
/**
* Map n values to a Tuple.
* Will append each value in the input to the Tuple using the same mapper.
* @param {SchemaMapper} schema - A SchemaMapper
*/
nTuple(schema)
{
return (args, path) => {
if(!Array.isArray(args))
{
args = [args]
}
return Tuple(...args.map((arg, index) => schema ? schema(arg, `${path || 'root'}[${index}]`) : arg));
}
},
/**
* Map n values to a Group.
* Will append each value in the input to the Group using the same mapper.
* @param {SchemaMapper} schema - A list of SchemaMappers
*/
nGroup(schema)
{
return (args, path = '') => {
if(!Array.isArray(args))
{
args = [args]
}
return Group(...args.map((arg, index) => schema ? schema(arg, `${path || 'root'}[${index}]`) : arg));
}
},
/**
* Map n keys to a Record.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
nRecord(schema)
{
return (arg, path = '') => {
const entries = Object.entries(arg);
return Record(Object.fromEntries(
entries.map(([key, value]) => [key, schema[key] ? schema[key](value, `${path || 'root'}[${key}]`) : value])
));
}
},
/**
* Map n keys to a Dict.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
nDict(schema)
{
/**
* @type SchemaMapper
*/
return (arg, path = '') => {
const entries = Object.entries(arg);
return Dict(Object.fromEntries(
entries.map(([key, value]) => [key, schema[key] ? schema[key](value, `${path || 'root'}[${key}]`) : value])
));
}
},
/**
* Strictly map values to a Tuple.
* @throws {TypeError} Will throw if the number of values does not match the number of SchemaMappers
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
sTuple(...schema)
{
return (args, path = '') => {
if(schema.length !== args.length)
{
throw new TypeError(`Expected ${schema.length} elements, got ${args.length} elements at ${path || 'root'}.`);
}
return Tuple(...args.map(
(arg, index) => schema[index] ? schema[index](arg, `${path || 'root'}[${index}]`): arg
));
}
},
/**
* Strictly map values to a Group.
* @throws {TypeError} Will throw if the number of values does not match the number of SchemaMappers
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
sGroup(...schema)
{
return (args, path = '') => {
if(schema.length !== args.length)
{
throw new TypeError(`Expected ${schema.length} elements, got ${args.length} elements at ${path || 'root'}.`);
}
return Group(...args.map(
(arg, index) => schema[index] ? schema[index](arg, `${path || 'root'}[${index}]`): arg
));
}
},
/**
* Strictly map an object to a Record.
* @throws {TypeError} Will throw if the properties provided do not match the keys of the SchemaMappers.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
sRecord(schema = {})
{
return (arg, path = '') => {
const entries = Object.entries(arg);
const schemaLength = Object.keys(schema).length;
if(schemaLength > entries.length)
{
throw new TypeError(`Expected ${schemaLength} elements, got ${entries.length} elements at ${path || 'root'}.`);
}
return Record(Object.fromEntries(
entries.map(([key, value]) => {
if(!(key in schema))
{
throw new TypeError(`Extra key "${key}" not found in schema at ` + (path ? `${path}[${key}]` : key));
}
return [key, schema[key](value, path ? `${path}[${key}]` : key)]
})
));
}
},
/**
* Strictly map an object to a Dict.
* @throws {TypeError} Will throw if the properties provided do not match the keys of the SchemaMappers.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
sDict(schema = {})
{
return (arg, path = '') => {
const entries = Object.entries(arg);
const schemaLength = Object.keys(schema).length;
if(schemaLength > entries.length)
{
throw new TypeError(`Expected ${schemaLength} elements, got ${entries.length} elements at ${path || 'root'}.`);
}
return Dict(Object.fromEntries(
entries.map(([key, value]) => {
if(!(key in schema))
{
throw new TypeError(`Extra key "${key}" not found in schema at ` + (path ? `${path}[${key}]` : key));
}
return [key, schema[key](value, path ? `${path}[${key}]` : key)]
})
));
}
},
/**
* Exclusively map values to a Tuple.
* Will drop any keys not present in the schema.
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
xTuple(...schema)
{
return (args, path = '') => {
if(schema.length > args.length)
{
throw new TypeError(`Expected ${schema.length} elements, got ${args.length} elements at ${path || 'root'}.`);
}
return Tuple(...schema.map(
(schema, index) => schema(args[index], `${path || 'root'}[${index}]`)
));
}
},
/**
* Exclusively map values to a Group.
* Will drop any keys not present in the schema.
* @param {...SchemaMapper} schema A list of SchemaMappers
*/
xGroup(...schema)
{
return (args, path = '') => {
if(schema.length > args.length)
{
throw new TypeError(`Expected ${schema.length} elements, got ${args.length} elements at ${path || 'root'}.`);
}
return Group(...schema.map(
(schema, index) => schema(args[index], `${path || 'root'}[${index}]`)
));
}
},
/**
* Exclusively map an object to a Record.
* Will drop any keys not present in the schema.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
xRecord(schema)
{
return (arg, path = '') => {
const schemaEntries = Object.entries(schema);
return Record(Object.fromEntries(
schemaEntries.map(([key, schema]) => [key, schema(arg[key], `${path || 'root'}[${key}]`)])
));
}
},
/**
* Exclusively map an object to a Dict.
* Will drop any keys not present in the schema.
* @param {Object.<string, SchemaMapper>} schema - An Object holding SchemaMappers
*/
xDict(schema)
{
return (arg, path = '') => {
const schemaKeys = Object.keys(schema);
return Dict(Object.fromEntries(
schemaKeys.map((key) => [key, schema[key](arg[key], `${path || 'root'}[${key}]`)])
));
}
},
/**
* Validate a boolean
* @param {Object} options
* @param {function(any):any} options.map Transform the value after its been validated.
*/
boolean(options = {})
{
return (value, path = '') => {
if(typeof value !== 'boolean')
{
throw new TypeError(`Expected boolean, got ${typeof value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate a number
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
number(options = {})
{
return (value, path = '') => {
if(typeof value !== 'number')
{
throw new TypeError(`Expected number, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if('max' in options && options.max < value)
{
throw new TypeError(`Expected max ${options.max}, got ${value} at ${path || 'root'}`);
}
if('min' in options && options.min > value)
{
throw new TypeError(`Expected min ${options.min}, got ${value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate an integer
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
integer(options = {})
{
const _options = {...options};
const checks = [Number.isInteger];
if(options.check)
{
checks.push(options.check);
}
_options.check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.number(_options);
},
/**
* Validate a float
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
float(options = {})
{
const _options = {...options};
const checks = [Number.isFinite];
if(options.check)
{
checks.push(options.check);
}
_options.check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.number(_options);
},
/**
* Validate a NaN
* @param {Object} options
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
NaN(options = {})
{
const _options = {...options};
const checks = [Number.isNaN];
if(options.check)
{
checks.push(options.check);
}
_options.check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.number(_options);
},
/**
* Validate an infinite value
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
infinity(options = {})
{
const checks = [n => !Number.isFinite(n) && !Number.isNaN(n)];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.number({...options, check});
},
/**
* Validate a bigint
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
bigint(options = {})
{
return (value, path = '') => {
if(typeof value !== 'bigint')
{
throw new TypeError(`Expected bigint, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if('max' in options && options.max < value)
{
throw new TypeError(`Expected max ${options.max}, got ${value} at ${path || 'root'}`);
}
if('min' in options && options.min > value)
{
throw new TypeError(`Expected min ${options.min}, got ${value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate a string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
string(options = {})
{
return (value, path = '') => {
if(typeof value !== 'string')
{
throw new TypeError(`Expected string, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if('max' in options && options.max < value.length)
{
throw new TypeError(`Expected max length ${options.max}, got "${value}" at ${path || 'root'}`);
}
if('min' in options && options.min > value.length)
{
throw new TypeError(`Expected min length ${options.min}, got "${value}" at ${path || 'root'}`);
}
if('prefix' in options && options.prefix !== value.substr(0, options.prefix.length))
{
throw new TypeError(`Expected prefix ${options.prefix}, got "${value}" at ${path || 'root'}`);
}
if('suffix' in options && options.suffix !== value.substr(value.length - options.suffix.length))
{
throw new TypeError(`Expected suffix ${options.suffix}, got "${value}" at ${path || 'root'}`);
}
if('infix' in options && value.indexOf(options.infix) === -1)
{
throw new TypeError(`Expected infix ${options.infix}, got "${value}" at ${path || 'root'}`);
}
if(options.match && !value.match(options.match))
{
throw new TypeError(`Expected string to match ${options.match}, got "${value}" at ${path || 'root'}`);
}
if(options.noMatch && value.noMatch(options.noMatch))
{
throw new TypeError(`Expected string NOT to match ${options.noMatch}, got "${value}" at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate a numeric string
* @param {Object} options
* @param {number} options.max Max value
* @param {number} options.min Min value
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
numericString(options = {})
{
return (value, path = '') => {
if(isNaN(value) || value === null || value != Number(value))
{
throw new TypeError(`Expected numeric, got "${value}" at ${path || 'root'}`);
}
if('max' in options && options.max < Number(value))
{
throw new TypeError(`Expected max ${options.max}, got "${value}" at ${path || 'root'}`);
}
if('min' in options && options.min > Number(value))
{
throw new TypeError(`Expected min ${options.min}, got "${value}" at ${path || 'root'}`);
}
const {min, max, ...newOptions} = options;
return Schema.string(newOptions)(value);
};
},
/**
* Validate a date string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
dateString(options = {})
{
return (value, path = '') => {
if(isNaN(Date.parse(value)))
{
throw new TypeError(`Expected dateString, got "${value}" at ${path || 'root'}`);
}
if('max' in options && options.max < value)
{
throw new TypeError(`Expected max ${options.max}, got "${value}" at ${path || 'root'}`);
}
if('min' in options && options.min > value)
{
throw new TypeError(`Expected min ${options.min}, got "${value}" at ${path || 'root'}`);
}
const {min, max, ...newOptions} = options;
return Schema.string(newOptions)(value);
};
},
uuidString(options = {})
{
const checks = [ value => String(value).match(/^[a-z,0-9]{8}-[a-z,0-9]{4}-[a-z,0-9]{4}-[a-z,0-9]{4}-[a-z,0-9]{12}$/i) ];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
/**
* Validate a url string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
urlString(options = {})
{
const checks = [ URL.canParse ? value => URL.canParse(value) : value => { try { new URL(value); return true; } catch { return false; } } ];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
/**
* Validate a regex string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
emailString(options = {})
{
const checks = [ value => {
const atPos = value.indexOf('@');
const atPos2 = value.indexOf('@', atPos + 1);
const dotPos = value.indexOf('.', atPos);
return (atPos > 0) && (atPos2 === -1) && (dotPos - atPos > 0) && (value.length - dotPos > 2);
} ];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
regexString(options = {})
{
const checks = [ value => { try { RegExp(value); return !!value; } catch { return false; } } ];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
/**
* Validate a base64 string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
base64String(options = {})
{
const checks = [ value => value !== '' && value.trim() !== '' && btoa(atob(value)) === value];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
/**
* Validate a JSON string
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {Regex} options.match Throw a TypeError if this does NOT match
* @param {Regex} options.noMatch Throw a TypeError if this DOES match
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
jsonString(options = {})
{
const checks = [ value => { try { JSON.parse(value); return true; } catch { return false; } } ];
if(options.check)
{
checks.push(options.check);
}
const check = v => checks.map(c => c(v)).reduce((a, b) => a && b, true);
return Schema.string({...options, check});
},
/**
* Validate an array
* @param {Object} options
* @param {number} options.max Max length
* @param {number} options.min Min length
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
* @param {function(any):each} options.each Transform each value in the array, after its been validated..
*/
array(options = {})
{
return (value, path = '') => {
if(!Array.isArray(value))
{
throw new TypeError(`Expected Array, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if('max' in options && options.max < value.length)
{
throw new TypeError(`Expected max length ${options.max}, got "${value.length}" at ${path || 'root'}`);
}
if('min' in options && options.min > value.length)
{
throw new TypeError(`Expected min length ${options.min}, got "${value.length}" at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
if(options.each)
{
value = value.map(options.each);
}
return value;
};
},
/**
* Validate an object
* @param {Object} options
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):class} options.class Throw a TypeError if the class does not match.
* @param {function(any):any} options.map Transform the object after its been validated.
* @param {function(any):each} options.each Transform each entry in the object, after its been validated.
*/
object(options = {})
{
return (value, path = '') => {
if(typeof value !== 'object')
{
throw new TypeError(`Expected object, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if(options.class && !(value instanceof options.class))
{
throw new TypeError(`Expected object of type ${options.class.name}, got Object of type ${value.constructor ? value.constructor.name : 'null'} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
if(options.each)
{
value = Object.fromEntries(Object.entries(value).map(options.each));
}
return value;
};
},
date(options = {})
{
return (value, path = '') => {
if(!(value instanceof Date))
{
throw new TypeError(`Expected Date, got "${value}" at ${path || 'root'}`);
}
if('max' in options && options.max < value)
{
throw new TypeError(`Expected max ${options.max}, got "${value}" at ${path || 'root'}`);
}
if('min' in options && options.min > value)
{
throw new TypeError(`Expected min ${options.min}, got "${value}" at ${path || 'root'}`);
}
return Schema.object({...options, class: Date})(value);
};
},
/**
* Validate a function
* @param {Object} options
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
function(options = {})
{
return (value, path = '') => {
if(typeof value !== 'function')
{
throw new TypeError(`Expected function, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate a symbol
* @param {Object} options
* @param {function(any):boolean} options.check Throw a TypeError if this returns false.
* @param {function(any):any} options.map Transform the value after its been validated.
*/
symbol(options = {})
{
return (value, path = '') => {
if(typeof value !== 'symbol')
{
throw new TypeError(`Expected symbol, got ${typeof value} at ${path || 'root'}`);
}
if(options.check && !options.check(value))
{
throw new TypeError(`Validation failed! got ${value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate a null
* @param {Object} options
* @param {function(any):any} options.map Transform the value after its been validated.
*/
null(options = {})
{
return (value, path = '') => {
if(value !== null)
{
throw new TypeError(`Expected null, got ${typeof value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Validate an undefined
* @param {Object} options
* @param {function(any):any} options.map Transform the value after its been validated.
*/
undefined(options = {})
{
return (value, path = '') => {
if(value !== undefined)
{
throw new TypeError(`Expected undefined, got ${typeof value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Return the value
* @param {Object} options
* @param {function(any):any} options.map Transform the value.
*/
value(options = {})
{
return (value, path = '') => {
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Match the value to a set of literals with strict-equals comparison.
* @param {...any} literals Value must be strictly equal to one of these.
* @param {function(any):any} options.map Transform the value after its been validated.
* @returns
*/
oneOf(literals = [], options = {})
{
return (value, path = '') => {
if(!literals.includes(value))
{
throw new TypeError(`Expected oneOf ${values.join(', ')}, got ${value} at ${path || 'root'}`);
}
if(options.map)
{
value = options.map(value);
}
return value;
};
},
/**
* Drop the value (always maps to `undefined`)
*/
drop()
{
return () => undefined;
},
/**
* Map the value with the first matching SchemaMapper
* @param {...function(options):value} mappers
*/
or(...mappers)
{
return (value, path) => {
const errors = [];
for(const mapper of mappers)
{
try
{
return mapper(value, path);
}
catch(error)
{
errors.push(error);
}
}
const multi = new Error(errors.map(e => e.message).join(', '));
multi.errors = errors;
throw multi;
};
},
/**
* Repeat a SchemaMapper n times
* @param {number} n The number of times to repeat.
* @param {SchemaMapper} schema The SchemaMapper to repeat.
*/
repeat(n, schema)
{
return Array(n).fill(schema);
},
/**
* Safely parse a Schema into an immutable structure.
* Returns NaN on error. This is helpful because `NaN !== NaN`, and its falsey.
* @param {SchemaMapper} schema The Schema to parse by.
* @param {values} any The values to parse.
* @returns {object|NaN}
*/
parse(schema, values)
{
try
{
return schema(values);
}
catch(error)
{
console.error(error);
return NaN;
}
}
};
Object.freeze(Schema);
export default Schema;
+12
-6
{
"name": "libtuple",
"version": "0.0.7",
"version": "0.0.8",
"author": "Sean Morris",
"description": "Memory-efficient tuple implementation.",
"repository": "https://github.com/seanmorris/libtuple",
"main": "Tuple.mjs",
"main": "index.mjs",
"keywords":["tuples"],
"scripts": {
"test": "node --test test.mjs"
"test": "node --test --expose-gc test/test.mjs test/schema.test.mjs"
},
"license": "Apache 2.0",
"files": [
"Dict.mjs",
"Group.mjs",
"LICENSE",
"NOTICE",
"README.md",
"Record.mjs",
"Schema.mjs",
"Tuple.mjs",
"README.md",
"LICENSE",
"NOTICE"
"index.mjs",
"package.json"
]
}
+578
-37
# libtuple
*Memory-efficient tuple implementation in 2.5kB*
[![Test](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml/badge.svg)](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml) *Memory-efficient immutables in 13.5kB*
### Install with NPM
### Install & Use
`libtuple` is now ESM compliant!
#### npm:
You can install libtuple via `npm`:
```bash

@@ -11,22 +17,91 @@ $ npm install libtuple

## Usage
## Tuples are...
`import` Tuple from 'libtuple';
*(Groups, Records, and Dicts are just specialized Tuples)*
### Immutable
Tuples are immutable. Any attempt to modify them will not throw an error, but will silently fail, leaving the original values unchanged.
```javascript
import Tuple from 'libtuple';
````
const tuple = Tuple('a', 'b', 'c');
Pass a list of values to the `Tuple()` function:
tuple[0] = 'NEW VALUE'; // This will not change the tuple, it will still be 'a'
console.log( tuple[0] ); // 'a'
```
### Composable
Tuples can be members of other tuples. This works as expected:
```javascript
const tuple123 = Tuple(1, 2, 3);
````
console.log( Tuple(Tuple(1, 2), Tuple(3, 4)) === Tuple(Tuple(1, 2), Tuple(3, 4)) );
// true
```
This value will be strictly equivalent to any tuple generated with the same values:
### Iterable & Spreadable
Tuples and Groups can be looped over just like Arrays:
```javascript
const tuple = Tuple(1, 2, 3);
for(const value of tuple) {
console.log(value)
}
```
Records, and Dicts can also be iterated just like normal objects:
```javascript
const record = Record({a: 1, b: 2, c: 3});
for(const [key, value] of Object.entries(record)) {
console.log(key, value);
}
```
Tuples & Groups can be spread just like arrays:
```javascript
const tuple = Tuple(1, 2, 3);
console.log([...tuple]); // [1, 2, 3]
```
Similarly, Records & Dicts can be spread into objects:
```javascript
const record = Record({a: 1, b: 2, c: 3});
console.log({...record}); // {a: 1, b: 2, c: 3}
```
#### Usage
Simply import the functions from `libtuple`:
```javascript
import { Tuple, Group, Record, Dict } from 'libtuple';
```
You can also import them via URL imports, or [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import): *(npm not required)*
```javascript
import { Tuple, Group, Record, Dict } from 'https://cdn.jsdelivr.net/npm/libtuple@0.0.7-alpha-4/index.mjs';
```
```javascript
const { Tuple, Group, Record, Dict } = await import('https://cdn.jsdelivr.net/npm/libtuple/index.mjs');
```
### Tuple()
Pass a list of values to the `Tuple()` function. This value will be strictly equivalent to any tuple generated with the same values:
```javascript
const tuple123 = Tuple(1, 2, 3);
tuple123 === Tuple(1, 2, 3); // true
````
const tuple321 = Tuple(3, 2, 1);
tuple123 === tuple321; // false
```
This is true for tuples with objects as well:

@@ -42,58 +117,515 @@

Watch out for the following however, object references can be tricky. In this example, each `[]` represents its own, unique object, so the following returns false:
### Group()
A `Group()` is similar to a `Tuple()`, except they're not ordered:
```javascript
Tuple( [] ) === Tuple( [] ); // FALSE!!!
Group(3, 2, 1) === Group(1, 2, 3); // true
```
Use the same ***object reference*** to get the same tuple:
### Record()
A `Record()` works the same way, but works with keys & values, and is **not** ordered.
```javascript
const a = [];
const [a, b, c] = [1, 2, 3];
Record({a, b, c}) === Record({c, b, a}); // true
```
Tuple( a ) === Tuple( a ); // true :)
### Dict()
A `Dict()` is like an ordered `Record()`:
```javascript
const [a, b, c] = [1, 2, 3];
Dict({a, b, c}) === Dict({a, b, c}); // true
Dict({a, b, c}) === Dict({c, b, a}); // false
```
## Tuples are...
### Schema
### Composable
A `Schema` allows you to define a complex structure for your immutables. It is defined by one or more SchemaMappers, which take a value and will either return it, or throw an error:
Tuples can be members of of other tuples. This works as expected:
```javascript
import { Schema as s } from 'libtuple';
const boolSchema = s.boolean();
boolSchema(true); // returns true
boolSchema(false); // returns false
boolSchema(123); // throws an error
```
You can create schemas for Tuples, Groups, Records, and Dicts:
```javascript
console.log( Tuple(Tuple(1, 2), Tuple(3, 4)) === Tuple(Tuple(1, 2), Tuple(3, 4)) );
// true
import { Schema as s } from 'libtuple';
const pointSchema = s.tuple(
s.number(),
s.number(),
);
const pointTuple = pointSchema([5, 10]);
console.log(pointTuple);
const userSchema = s.record({
id: s.number(),
email: s.string(),
});
const userRecord = userSchema({id: 1, email: 'fake@example.com'});
console.log(userRecord);
```
### Frozen
#### Schema.parse(schema, value)
You cannot add, remove or modify any property on a tuple.
`Schema.parse()` will return the parsed value, or NaN on error, since `NaN` is falsey, and `NaN !== NaN`.
```javascript
const tuple = Tuple('a', 'b', 'c');
import { Schema as s } from 'libtuple';
tuple[0] = 'NEW VALUE';
const boolSchema = s.boolean();
console.log( tuple[0] ); // 'a'
s.parse(boolSchema, true); // returns true
s.parse(boolSchema, false); // returns false
s.parse(boolSchema, 123); // returns NaN
```
### SchemaMappers
*Expand the sections below to see SchemaMapper documentation.*
<details>
<summary>Schema Mappers for Values</summary>
#### Schema.value(options)
* options.map - Callback to transform the value after its been validated.
* options.check - Throw a TypeError if this returns false.
#### Schema.drop()
Drop the value (always maps to `undefined`)
#### Schema.boolean(options)
* options.map - Callback to transform the value after its been validated.
#### Schema.number(options)
* options.min - Min value
* options.max - Max value
* options.map - Callback to transform the value after its been validated.
* options.check - Throw a TypeError if this returns false.
#### Schema.bigint(options)
* options.min - Min value
* options.max - Max value
* options.map - Callback to transform the value after its been validated.
* options.check - Throw a TypeError if this returns false.
#### Schema.string(options)
* options.min - Min length
* options.max - Max length
* options.map - Callback to transform the value after its been validated.
* options.match - Throw a TypeError if this does NOT match
* options.noMatch - Throw a TypeError if this DOES match
* options.check - Throw a TypeError if this returns false.
#### Schema.array(options)
* options.min - Min length
* options.max - Max length
* options.map - Callback to transform the value after its been validated.
* options.each - Callback to transform each element.
* options.check - Throw a TypeError if this returns false.
#### Schema.object(options)
* options.class - Throw a TypeError if the class does not match.
* options.map - Callback to transform the value after its been validated.
* options.each - Callback to transform each element.
* options.check - Throw a TypeError if this returns false.
#### Schema.function(options)
* options.map - Callback to transform the value after its been validated.
* options.check - Throw a TypeError if this returns false.
#### Schema.symbol(options)
* options.map - Callback to transform the value after its been validated.
* options.check - Throw a TypeError if this returns false.
#### Schema.null(options)
* options.map - Callback to transform the value after its been validated.
#### Schema.undefined(options)
* options.map - Callback to transform the value after its been validated.
---
</details>
<details>
<summary>Schema Mappers for Convenience</summary>
#### Convenience methods for numbers
The following methods will call `s.number` with additional constraints added:
* s.integer
* s.float
* s.NaN
* s.infinity
#### Convenience methods for strings
The following methods will call `s.string` with additional constraints added:
* s.numericString
```javascript
// options.min & options.max are overridden for numeric comparison.
const positive = s.numericString({map: Number, min: Number.EPSILON});
const negative = s.numericString({map: Number, max: -Number.EPSILON});
negative('-100'); // -100
positive('100'); // 100
negative('5'); // ERROR
positive('-5'); // ERROR
```
* s.dateString
```javascript
// options.min & options.max are overridden for comparison with Date objects.
const after1994 = s.dateString({min: new Date('01/01/1995')});
after1994('07/04/1995'); // '01/01/1996'
after1994('07/04/1989'); // ERROR
```
* s.uuidString
```javascript
const uuidSchema = s.uuidString();
uuidSchema('0ff5d941-f46a-4f4a-aec8-1d1ec117e2a3'); // '0ff5d941-f46a-4f4a-aec8-1d1ec117e2a3'
uuidSchema('0ff5d941'); // ERROR
```
* s.urlString
```javascript
const urlSchema = s.urlString();
urlSchema('https://example.com'); // 'https://example.com'
urlSchema('not a url'); // ERROR
```
* s.emailString
```javascript
const emailSchema = s.emailString();
emailSchema('person@example.com'); // 'https://example.com'
emailSchema('not an email'); // ERROR
```
* s.regexString
```javascript
const regexSchema = s.regexString();
regexSchema('.+?'); // 'https://example.com'
regexSchema('+++'); // ERROR
```
* s.base64String
```javascript
const base64Schema = s.base64String();
base64Schema('RXhhbXBsZSBzdHJpbmc='); // 'RXhhbXBsZSBzdHJpbmc=';
base64Schema('notbase64'); // ERROR;
```
* s.jsonString
```javascript
const jsonSchema = s.jsonString();
jsonSchema('[0, 1, 2]'); // '[0, 1, 2]';
jsonSchema('not json'); // ERROR;
```
</details>
<details>
<summary>Special Schema Mappers</summary>
#### Schema.or(...schemaMappers)
Map the value with the first matching SchemaMapper
```javascript
import { Schema as s } from 'libtuple';
const dateSchema = s.or(
s.string({match: /\d\d \w+ \d\d\d\d \d\d:\d\d:\d\d \w+?/, map: s => new Date(s)}),
s.object({class: Date})
);
console.log( dateSchema('04 Apr 1995 00:12:00 GMT') );
console.log( dateSchema(new Date) );
```
### Not Iterable
#### Schema.repeat(r, schemaMapper)
You can access properties like `[0]`, `[1]`, and `.length` on a tuple, but they are not arrays. You can get equivalent array values quite easily with `Array.from()`:
Repeat a SchemaMapper r times
```javascript
const tuple = Tuple('a', 'b', 'c');
import { Schema as s } from 'libtuple';
console.log( tuple[1] ) // 'b'
const pointSchema = s.tuple(s.repeat(2, s.number()));
Array.from(tuple).map(t => console.log(t));
// 'a'
// 'b'
// 'c'
const point = pointSchema([5, 10]);
```
#### Schema.oneOf(literals = [], options = {})
Match the value to a set of literals with strict-equals comparison.
```javascript
import { Schema as s } from 'libtuple';
const schema = s.oneOf(['something', 1234]);
s.parse(schema, 1234); // 1234
s.parse(schema, 'something'); // 'something'
s.parse(schema, 'not on list'); // ERROR!
```
---
</details>
<details>
<summary>Schema Mappers for Tuples, Groups, Records and Dicts</summary>
#### Schema.tuple(...values)
Map one or more values to a Tuple.
```javascript
import { Schema as s } from 'libtuple';
const pointSchema = s.tuple(s.number(), s.number());
const point = pointSchema([5, 10]);
```
#### Schema.group(...values)
Map one or more values to a Group.
#### Schema.record(properties)
Map one or more properties to a Record.
```javascript
import { Schema as s } from 'libtuple';
const companySchema = s.sRecord({
name: s.string(),
phone: s.string(),
address: s.string(),
});
const company = companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
});
console.log({company});
```
#### Schema.dict(properties)
Map one or more values to a Dict.
```javascript
import { Schema as s } from 'libtuple';
const companySchema = s.sDict({
name: s.string(),
phone: s.string(),
address: s.string(),
});
const company = companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
});
console.log({company});
```
#### Schema.nTuple(...values)
Map n values to a Tuple. Will append each value in the input to the Tuple using the same mapper.
```javascript
import { Schema as s } from 'libtuple';
const vectorSchema = s.nTuple(s.number());
const vec2 = vectorSchema([5, 10]);
const vec3 = vectorSchema([5, 10, 11]);
const vec4 = vectorSchema([5, 10, 11, 17]);
console.log({vec2, vec3, vec4});
```
#### Schema.nGroup(...values)
Map n values to a Group. Will append each value in the input to the Group using the same mapper.
#### Schema.nRecord(properties)
Map n properties to a Record. Will append additional properties without mapping or validation, if present.
```javascript
import { Schema as s } from 'libtuple';
const companySchema = s.nRecord({
name: s.string(),
phone: s.string(),
address: s.string(),
});
const company = companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
openHours: '9AM-7PM',
slogan: 'We do business.',
});
```
#### Schema.nDict(properties)
Map n properties to a Dict. Will append additional properties without mapping or validation, if present.
#### Schema.sTuple(...values)
Strictly map values to a Tuple. Will throw an error if the number of values does not match.
```javascript
import { Schema as s } from 'libtuple';
const pointSchema = s.sTuple(s.number(), s.number());
const pointA = pointSchema([5, 10]);
const pointB = pointSchema([5, 10, 1]); // ERROR!
```
#### Schema.sGroup(...values)
Strictly map values to a Group. Will throw an error if the number of values does not match.
#### Schema.sRecord(properties)
Strictly map values to a Record. Will throw an error if the number of values does not match.
```javascript
import { Schema as s } from 'libtuple';
const companySchema = s.nRecord({
name: s.string(),
phone: s.string(),
address: s.string(),
});
const company = companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
});
// ERROR!
companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
openHours: '9AM-7PM',
slogan: 'We do business.',
});
```
#### Schema.sDict(properties)
Strictly map values to a Dict. Will throw an error if the number of values does not match.
#### Schema.xTuple(...values)
Exclusively map values to a Tuple. Will drop any keys not present in the schema.
```javascript
import { Schema as s } from './index.mjs';
const pointSchema = s.xTuple(s.number(), s.number());
const pointA = pointSchema([5, 10]); // [5, 10]
const pointB = pointSchema([5, 10, 1]); // Also [5, 10]
console.log(pointB[0]); // 5
console.log(pointB[1]); // 10
console.log(pointB[2]); // undefined
```
#### Schema.xGroup(...values)
Exclusively map values to a Group. Will drop any keys not present in the schema.
#### Schema.xRecord(properties)
Exclusively map values to a Record. Will drop any keys not present in the schema.
```javascript
const companySchema = s.xRecord({
name: s.string(),
phone: s.string(),
address: s.string(),
});
const company = companySchema({
name: 'Acme Corporation',
phone: '+1-000-555-1234',
address: '123 Fake St, Anytown, USA',
openHours: '9AM-7PM',
slogan: 'We do business.',
});
console.log({company});
```
#### Schema.xDict(properties)
Exclusively map values to a Dict. Will drop any keys not present in the schema.
---
</details>
## Gotchas
In JavaScript, object comparisons are based on reference, not on the actual content of the objects. This means that even if two objects have the same properties and values, they are considered different if they do not reference the same memory location.
For example, the following comparison returns false because each {} creates a new, unique object:
```javascript
Tuple( {} ) === Tuple( {} ); // FALSE!!!
```
Each {} is a different object in memory, so the tuples containing them are not strictly equal. This is an important behavior to understand when working with tuples that contain objects.
To get the same tuple, you need to use the exact same object reference:
```javascript
const a = {};
Tuple( a ) === Tuple( a ); // true :)
```
## How It Works
A *tuple* is a type represented by a sequence of values. Unlike arrays, where `[1,2] !== [1,2]`, since, although they hold the same values, the actual object references are different. Tuples give you `Tuple(1,2) === Tuple(1,2)`.
A *tuple* is a type represented by a sequence of values. Unlike arrays, where `[1, 2] !== [1, 2]` (as they hold different object references), tuples provide a mechanism where `Tuple(1, 2) === Tuple(1, 2)`. This ensures that tuples with the same values are always strictly equal, simplifying equality checks and enhancing memory efficiency.

@@ -108,7 +640,7 @@ For a sequence of primitives, this is trivial. Simply run `JSON.stringify` on the list of values and you've got a unique scalar that you can compare against others, and the object-reference problem is gone. Once you add objects to the mix, however, things can get complicated.

Organizing the hierarchy with the scalar prefixes *after* the objects allows us to exploit the `WeakMap`'s garbage collection behavior. Once the object keys are GC'ed, so are the entries of the `WeakMap`. Holding a key here does not prevent objects from being GC'ed, so the branches of the internal tuple tree only stay in-memory as long as the objects they're comprised of.
Organizing the hierarchy with the scalar prefixes *after* the objects allows us to exploit the `WeakMap`'s garbage collection behavior. Once the object keys are GC'ed, so are the entries of the `WeakMap`. Holding a key here does not prevent objects from being GC'ed, so the branches of the internal tuple tree only stay in-memory as long as the objects they contain are in use.
## Limitations
* `Symbol`s cannot participate in `tuples`.
* Registered `Symbol`s cannot be used in `Tuples`. (i.e. created with `Symbol.for()`; [more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#shared_symbols_in_the_global_symbol_registry)) [⚠️ Node v19 & Earlier](https://github.com/nodejs/node/issues/49135)

@@ -118,1 +650,10 @@ ## Testing

Run `npm run test` or `node --test test.mjs` in the terminal.
# 🍻 Licensed under the Apache License, Version 2.0
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+78
-22

@@ -1,13 +0,41 @@

const refTree = new WeakMap;
const scalarMap = new Map;
const refTree = new WeakMap;
const scalarMap = new Map;
const terminator = Object.create(null);
const baseTuple = Object.create(null);
baseTuple[Symbol.toStringTag] = 'Tuple';
baseTuple.toString = Object.prototype.toString;
export const size = Symbol('size');
export const _index = Symbol('index');
export const keys = Symbol('keys');
const base = Object.create(null);
base.toString = Object.prototype.toString;
base[Symbol.toStringTag] = 'Tuple';
base.toJSON = function() {
return [...this];
}
base[Symbol.iterator] = function() {
let index = 0;
return { next: () => {
const iteration = index++;
if(this[size] < index)
{
return { done: true };
}
return { value: this[iteration], done: false };
}};
};
let index = 0;
Object.freeze(terminator);
Object.freeze(baseTuple);
Object.freeze(base);
const registry = new FinalizationRegistry(held => scalarMap.delete(held));
const registry = new FinalizationRegistry(held => {
if(scalarMap.has(held) && scalarMap.get(held).deref() !== undefined)
{
// Preventing race condition #1 outlined here:
// https://github.com/seanmorris/libtuple/issues/2
return;
}
scalarMap.delete(held);
});

@@ -30,9 +58,9 @@ export default function Tuple(...args)

const type = typeof arg;
const canMap = arg !== null && (type === 'object' || type === 'function');
const canMap = arg !== null && (type === 'object' || type === 'symbol' || type === 'function');
prefix = null;
if(type === 'symbol')
if(type === 'symbol' && Symbol.keyFor(arg))
{
throw new Error('Symbols cannot participate in Tuples.');
throw new Error('Registered symbols (`Symbol.for(...)`) cannot participate in Tuples.');
}

@@ -88,12 +116,28 @@

{
part = JSON.stringify(part.map(p => `${typeof p}::${p}`))
part = JSON.stringify(part.map(p => `${typeof p}::${Object.is(p, -0) ? '-0' : p}`));
if(!maps)
{
const result = Object.create(baseTuple);
Object.assign(result, {length:args.length, ...args});
const a = (this ? this.args : args);
const result = Object.create(this ? this.base : base);
const length = Array.isArray(a) ? a.length : Object.keys(a).length;
Object.assign(result, a);
Object.defineProperties(result, {
length: {value: length},
[size]: {value: length},
[keys]: {value: this && this.keys},
[_index]: {value: index++}
});
Object.freeze(result);
if(!scalarMap.has(part))
// Preventing race condition #2 outlined here:
// https://github.com/seanmorris/libtuple/issues/2
if(scalarMap.has(part) && scalarMap.get(part).deref() !== undefined)
{
return scalarMap.get(part).deref();
}
else
{
registry.register(result, part);

@@ -103,6 +147,2 @@ scalarMap.set(part, new WeakRef(result));

}
else
{
return scalarMap.get(part).deref();
}
}

@@ -112,4 +152,12 @@

{
const result = Object.create(baseTuple);
Object.assign(result, {length:args.length, ...args});
const a = (this ? this.args : args);
const result = Object.create(this ? this.base : base);
const length = Array.isArray(a) ? a.length : Object.keys(a).length;
Object.assign(result, a);
Object.defineProperties(result, {
length: {value: length},
[size]: {value: length},
[keys]: {value: this && this.keys},
[_index]: {value: index++}
});
Object.freeze(result);

@@ -125,4 +173,12 @@

{
const result = Object.create(baseTuple);
Object.assign(result, {length:args.length, ...args});
const a = (this ? this.args : args);
const result = Object.create(this ? this.base : base);
const length = Array.isArray(a) ? a.length : Object.keys(a).length;
Object.assign(result, a);
Object.defineProperties(result, {
length: {value: length},
[size]: {value: length},
[keys]: {value: this && this.keys},
[_index]: {value: index++}
});
Object.freeze(result);

@@ -129,0 +185,0 @@