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

feat: resettable unique store #800

Merged
merged 6 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions src/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class Unique {
* @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.
* @param options.store The store of unique entries. Defaults to a global store.
*
* @example
* faker.unique(faker.name.firstName) // 'Corbin'
Expand All @@ -123,6 +124,7 @@ export class Unique {
currentIterations?: number;
exclude?: RecordKey | RecordKey[];
compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1;
store?: Record<RecordKey, RecordKey>;
} = {}
): ReturnType<Method> {
const { maxTime = this._maxTime, maxRetries = this._maxRetries } = options;
Expand Down
47 changes: 30 additions & 17 deletions src/utils/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type RecordKey = string | number | symbol;

/**
* Global store of unique values.
* This means that faker should *never* return duplicate values across all API methods when using `Faker.unique`.
* 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> = {};

Expand All @@ -14,11 +14,6 @@ const GLOBAL_UNIQUE_STORE: Record<RecordKey, RecordKey> = {};
*/
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.
Expand All @@ -43,15 +38,21 @@ function defaultCompare(
* @param startTime The time the execution started.
* @param now The current time.
* @param code The error code.
* @param store The store of unique entries.
* @param currentIterations Current iteration or retries of `unique.exec` (current loop depth).
*
* @throws The given error code with additional text.
*/
function errorMessage(startTime: number, now: number, code: string): never {
function errorMessage(
startTime: number,
now: number,
code: string,
store: Record<RecordKey, RecordKey>,
currentIterations: number
): never {
console.error('Error', code);
console.log(
`Found ${
Object.keys(GLOBAL_UNIQUE_STORE).length
} unique entries before throwing error.
`Found ${Object.keys(store).length} unique entries before throwing error.
retried: ${currentIterations}
total time: ${now - startTime}ms`
);
Expand All @@ -77,6 +78,7 @@ Try adjusting maxTime or maxRetries parameters for faker.unique().`
* @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.
* @param options.store The store of unique entries. Defaults to `GLOBAL_UNIQUE_STORE`.
*/
export function exec<Method extends (...parameters) => RecordKey>(
method: Method,
Expand All @@ -88,6 +90,7 @@ export function exec<Method extends (...parameters) => RecordKey>(
currentIterations?: number;
exclude?: RecordKey | RecordKey[];
compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1;
store?: Record<RecordKey, RecordKey>;
} = {}
): ReturnType<Method> {
const now = new Date().getTime();
Expand All @@ -97,6 +100,7 @@ export function exec<Method extends (...parameters) => RecordKey>(
maxTime = 50,
maxRetries = 50,
compare = defaultCompare,
store = GLOBAL_UNIQUE_STORE,
} = options;
let { exclude = GLOBAL_UNIQUE_EXCLUDE } = options;
options.currentIterations = options.currentIterations ?? 0;
Expand All @@ -112,22 +116,31 @@ export function exec<Method extends (...parameters) => RecordKey>(

// console.log(now - startTime)
if (now - startTime >= maxTime) {
return errorMessage(startTime, now, `Exceeded maxTime: ${maxTime}`);
return errorMessage(
startTime,
now,
`Exceeded maxTime: ${maxTime}`,
store,
options.currentIterations
);
}

if (options.currentIterations >= maxRetries) {
return errorMessage(startTime, now, `Exceeded maxRetries: ${maxRetries}`);
return errorMessage(
startTime,
now,
`Exceeded maxRetries: ${maxRetries}`,
store,
options.currentIterations
);
}

// 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 (
compare(GLOBAL_UNIQUE_STORE, result) === -1 &&
exclude.indexOf(result) === -1
) {
GLOBAL_UNIQUE_STORE[result] = result;
if (compare(store, result) === -1 && exclude.indexOf(result) === -1) {
store[result] = result;
options.currentIterations = 0;
return result;
} else {
Expand Down
16 changes: 16 additions & 0 deletions test/unique.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,20 @@ Try adjusting maxTime or maxRetries parameters for faker.unique().`)
expect(options.exclude).toBe(exclude);
expect(options.compare).toBe(compare);
});

it('should be possible to pass a user-specific store', () => {
const store = {};

const method = () => 'with conflict: 0';

expect(faker.unique(method, [], { store })).toBe('with conflict: 0');
expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' });

expect(() => faker.unique(method, [], { store })).toThrow();

delete store['with conflict: 0'];

expect(faker.unique(method, [], { store })).toBe('with conflict: 0');
expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' });
});
});