Skip to content

Commit

Permalink
refactor: improve unique (#810)
Browse files Browse the repository at this point in the history
Co-authored-by: ST-DDT <ST-DDT@gmx.de>
  • Loading branch information
Shinigami92 and ST-DDT authored Apr 10, 2022
1 parent a712442 commit 612fc38
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 109 deletions.
51 changes: 27 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,36 @@ 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 This parameter does nothing.
* @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 This parameter does nothing.
* @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 { maxTime = this.maxTime, maxRetries = this.maxRetries } = options;
return uniqueExec.exec(method, args, {
...options,
startTime: new Date().getTime(),
maxTime,
maxRetries,
currentIterations: 0,
});
}
}
173 changes: 96 additions & 77 deletions src/utils/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@ 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.
* This means that faker should *never* return duplicate values across all API methods when using `Faker.unique`.
*/
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 +36,110 @@ 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. 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. 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,
startTime,
maxTime,
maxRetries,
compare,
exclude,
});
}
}
Loading

0 comments on commit 612fc38

Please sign in to comment.