Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: improve unique #810

Merged
merged 6 commits into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
56 changes: 32 additions & 24 deletions src/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import * as uniqueExec from './utils/unique';
* Module to generate unique entries.
*/
export class Unique {
// maximum time unique.exec will attempt to run before aborting
/**
* Maximum time `unique.exec` will attempt to run before aborting.
*
* @deprecated Use options instead.
*/
maxTime = 10;

// maximum retries unique.exec will recurse before aborting ( max loop depth )
/**
* Maximum retries `unique.exec` will recurse before aborting (max loop depth).
*
* @deprecated Use options instead.
*/
maxRetries = 10;

// time the script started
// startTime: number = 0;

constructor() {
// Bind `this` so namespaced is working correctly
for (const name of Object.getOwnPropertyNames(Unique.prototype)) {
Expand All @@ -31,38 +36,41 @@ export class Unique {
* @template Method The type of the method to execute.
* @param method The method used to generate the values.
* @param args The arguments used to call the method.
* @param opts The optional options used to configure this method.
* @param opts.startTime The time this execution stared. This will be ignored/overwritten.
* @param opts.maxTime The time this method may take before throwing an error.
* @param opts.maxRetries The total number of attempts to try before throwing an error.
* @param opts.currentIterations The current attempt. This will be ignored/overwritten.
* @param opts.exclude The value or values that should be excluded/skipped.
* @param opts.compare The function used to determine whether a value was already returned.
* @param options The optional options used to configure this method.
* @param options.startTime The time this execution stared. This will be ignored/overwritten. Defaults to `new Date().getTime()`.
* @param options.maxTime The time in milliseconds this method may take before throwing an error. Defaults to `50`.
* @param options.maxRetries The total number of attempts to try before throwing an error. Defaults to `50`.
* @param options.currentIterations The current attempt. This will be ignored/overwritten. Defaults to `0`.
* @param options.exclude The value or values that should be excluded/skipped. Defaults to `[]`.
* @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key.
*
* @example
* faker.unique(faker.name.firstName) // 'Corbin'
*/
unique<Method extends (...parameters) => RecordKey>(
method: Method,
args?: Parameters<Method>,
opts?: {
options: {
startTime?: number;
maxTime?: number;
maxRetries?: number;
currentIterations?: number;
exclude?: RecordKey | RecordKey[];
compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1;
}
} = {}
): ReturnType<Method> {
opts = opts || {};
opts.startTime = new Date().getTime();
if (typeof opts.maxTime !== 'number') {
opts.maxTime = this.maxTime;
}
if (typeof opts.maxRetries !== 'number') {
opts.maxRetries = this.maxRetries;
}
opts.currentIterations = 0;
return uniqueExec.exec(method, args, opts);
const {
startTime = new Date().getTime(),
maxTime = this.maxTime,
maxRetries = this.maxRetries,
currentIterations = 0,
} = options;
return uniqueExec.exec(method, args, {
...options,
startTime,
maxTime,
maxRetries,
currentIterations,
});
}
}
167 changes: 90 additions & 77 deletions src/utils/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@ import { FakerError } from '../errors/faker-error';

export type RecordKey = string | number | symbol;

// global results store
// currently uniqueness is global to entire faker instance
// this means that faker should currently *never* return duplicate values across all API methods when using `Faker.unique`
// it's possible in the future that some users may want to scope found per function call instead of faker instance
const found: Record<RecordKey, RecordKey> = {};

// global exclude list of results
// defaults to nothing excluded
const exclude: RecordKey[] = [];

// current iteration or retries of unique.exec ( current loop depth )
/**
* Global store of unique values.
* Uniqueness for entire faker instance.
* This means that faker should *never* return duplicate values across all API methods when using `Faker.unique` without passing `options.store`.
*/
const GLOBAL_UNIQUE_STORE: Record<RecordKey, RecordKey> = {};

/**
* Global exclude list of results.
* Defaults to nothing excluded.
*/
const GLOBAL_UNIQUE_EXCLUDE: RecordKey[] = [];

/**
* Current iteration or retries of `unique.exec` (current loop depth).
*/
const currentIterations = 0;

// uniqueness compare function
// default behavior is to check value as key against object hash
/**
* Uniqueness compare function.
* Default behavior is to check value as key against object hash.
*
* @param obj The object to check.
* @param key The key to check.
*/
function defaultCompare(
obj: Record<RecordKey, RecordKey>,
key: RecordKey
Expand All @@ -27,100 +37,103 @@ function defaultCompare(
return 0;
}

// common error handler for messages
function errorMessage(
now: number,
code: string,
opts: { startTime: number }
): never {
console.error('error', code);
/**
* Logs the given code as an error and throws it.
* Also logs a message for helping the user.
*
* @param startTime The time the execution started.
* @param now The current time.
* @param code The error code.
*
* @throws The given error code with additional text.
*/
function errorMessage(startTime: number, now: number, code: string): never {
console.error('Error', code);
console.log(
'found',
Object.keys(found).length,
'unique entries before throwing error. \nretried:',
currentIterations,
'\ntotal time:',
now - opts.startTime,
'ms'
`Found ${
Object.keys(GLOBAL_UNIQUE_STORE).length
} unique entries before throwing error.
retried: ${currentIterations}
total time: ${now - startTime}ms`
);
throw new FakerError(
code +
' for uniqueness check \n\nMay not be able to generate any more unique values with current settings. \nTry adjusting maxTime or maxRetries parameters for faker.unique()'
`${code} for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.unique().`
);
}

/**
* Generates a unique result using the results of the given method.
* Used unique entries will be stored internally and filtered from subsequent calls.
*
* @template Method The type of the method to execute.
* @param method The method used to generate the values.
* @param args The arguments used to call the method.
* @param options The optional options used to configure this method.
* @param options.startTime The time this execution stared. This will be ignored/overwritten. Defaults to `new Date().getTime()`.
* @param options.maxTime The time in milliseconds this method may take before throwing an error. Defaults to `50`.
* @param options.maxRetries The total number of attempts to try before throwing an error. Defaults to `50`.
* @param options.currentIterations The current attempt. This will be ignored/overwritten. Defaults to `0`.
* @param options.exclude The value or values that should be excluded/skipped. Defaults to `[]`.
* @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key.
*/
export function exec<Method extends (...parameters) => RecordKey>(
method: Method,
args: Parameters<Method>,
opts: {
options: {
startTime?: number;
maxTime?: number;
maxRetries?: number;
currentIterations?: number;
exclude?: RecordKey | RecordKey[];
compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1;
currentIterations?: number;
startTime?: number;
}
} = {}
): ReturnType<Method> {
const now = new Date().getTime();

opts = opts || {};
opts.maxTime = opts.maxTime || 3;
opts.maxRetries = opts.maxRetries || 50;
opts.exclude = opts.exclude || exclude;
opts.compare = opts.compare || defaultCompare;

if (typeof opts.currentIterations !== 'number') {
opts.currentIterations = 0;
}

if (opts.startTime == null) {
opts.startTime = new Date().getTime();
const {
startTime = new Date().getTime(),
maxTime = 50,
maxRetries = 50,
compare = defaultCompare,
} = options;
let { exclude = GLOBAL_UNIQUE_EXCLUDE } = options;
options.currentIterations = options.currentIterations ?? 0;

// Support single exclude argument as string
if (!Array.isArray(exclude)) {
exclude = [exclude];
}

const startTime = opts.startTime;

// support single exclude argument as string
if (!Array.isArray(opts.exclude)) {
opts.exclude = [opts.exclude];
}

if (opts.currentIterations > 0) {
// console.log('iterating', currentIterations)
}
// if (options.currentIterations > 0) {
// console.log('iterating', options.currentIterations)
// }

// console.log(now - startTime)
if (now - startTime >= opts.maxTime) {
return errorMessage(
now,
`Exceeded maxTime: ${opts.maxTime}`,
// @ts-expect-error: we know that opts.startTime is defined
opts
);
if (now - startTime >= maxTime) {
return errorMessage(startTime, now, `Exceeded maxTime: ${maxTime}`);
}

if (opts.currentIterations >= opts.maxRetries) {
return errorMessage(
now,
`Exceeded maxRetries: ${opts.maxRetries}`,
// @ts-expect-error: we know that opts.startTime is defined
opts
);
if (options.currentIterations >= maxRetries) {
return errorMessage(startTime, now, `Exceeded maxRetries: ${maxRetries}`);
}

// execute the provided method to find a potential satisfied value
// Execute the provided method to find a potential satisfied value.
const result: ReturnType<Method> = method.apply(this, args);

// if the result has not been previously found, add it to the found array and return the value as it's unique
// If the result has not been previously found, add it to the found array and return the value as it's unique.
if (
opts.compare(found, result) === -1 &&
opts.exclude.indexOf(result) === -1
compare(GLOBAL_UNIQUE_STORE, result) === -1 &&
exclude.indexOf(result) === -1
) {
found[result] = result;
opts.currentIterations = 0;
GLOBAL_UNIQUE_STORE[result] = result;
options.currentIterations = 0;
return result;
} else {
// console.log('conflict', result);
opts.currentIterations++;
return exec(method, args, opts);
options.currentIterations++;
return exec(method, args, options);
}
}
Loading