Created
August 20, 2023 04:28
-
-
Save jymchng/341c6899ec9a822ef57721275a21a00f to your computer and use it in GitHub Desktop.
Typescript Secret/SyncSecret
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { describe, expect, test } from '@jest/globals'; | |
import { Secret } from './secret'; | |
describe('Secret<T> Class', () => { | |
// Test Case 1: Creating a Secret with an initial secret value | |
it('should create a Secret with an initial secret value', () => { | |
const secret = new Secret<string>('mySecret'); | |
expect(secret.toString()).toBe('Secret<string>'); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
}); | |
// Test Case 2: Creating a Secret with a function to generate the secret | |
it('should create a Secret with a function to generate the secret', () => { | |
const secret = new Secret<number>(undefined, () => 42); | |
expect(secret.toString()).toBe('Secret<number>'); | |
expect(secret.exposeSecret()).toBe(42); | |
}); | |
// Test Case 3: Attempting to create a Secret without specifying secret or function | |
it('should throw an error when neither secret nor function is specified', () => { | |
expect(() => new Secret<number>()).toThrowError( | |
'Either `secret` or `func` must be specified', | |
); | |
}); | |
// Test Case 4: Attempting to create a Secret with both secret and function | |
it('should throw an error when both secret and function are specified', () => { | |
expect(() => new Secret<number>(69, () => 42)).toThrowError( | |
'Only `secret` or `func` should be specified', | |
); | |
}); | |
// Test Case 5: Creating a Secret with a maximum expose count | |
it('should limit the number of times the secret can be exposed', () => { | |
const secret = new Secret<string>('mySecret', undefined, 2); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
expect(secret.exposeSecret()).toBeUndefined(); // Exceeds max expose count | |
}); | |
// Test Case 6: Getting the max expose count | |
it('should return the maximum expose count', () => { | |
const secret = new Secret<number>(undefined, () => 42, 3); | |
expect(secret.max_expose_count).toBe(3); | |
}); | |
// Test Case 7: Getting the expose count | |
it('should return the current expose count', () => { | |
const secret = new Secret<number>(undefined, () => 42, 3); | |
secret.exposeSecret(); | |
secret.exposeSecret(); | |
expect(secret.expose_count).toBe(2); | |
}); | |
// Test Case 8: Exposing the secret after reaching max expose count | |
it('should not expose the secret after reaching max expose count', () => { | |
const secret = new Secret<number>(undefined, () => 42, 2); | |
secret.exposeSecret(); | |
secret.exposeSecret(); | |
expect(secret.expose_count).toBe(2); | |
expect(secret.exposeSecret()).toBeUndefined(); | |
}); | |
// Test Case 9: Creating a Secret with no maximum expose count | |
it('should not limit the number of times the secret can be exposed if max expose count is not specified', () => { | |
const secret = new Secret<string>('mySecret'); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
}); | |
// Test Case 10: Getting the string representation of the Secret | |
it('should return a string representation of the Secret', () => { | |
const secret = new Secret<number>(undefined, () => 42, 2); | |
expect(secret.toString()).toBe('Secret<number>'); | |
}); | |
// Test Case 11: Creating a Secret with a maximum expose count of 0 | |
it('should not expose the secret when max expose count is 0', () => { | |
expect(() => { | |
const secret = new Secret<number>(undefined, () => 42, 0); | |
}).toThrowError( | |
'`max_expose_count` when specified must be a positive integer', | |
); | |
}); | |
// Test Case 12: Getting the max expose count when not specified | |
it('should return -1 as the max expose count when not specified', () => { | |
const secret = new Secret<number>(undefined, () => 42); | |
expect(secret.max_expose_count).toBe(-1); | |
}); | |
// Test Case 13: Creating a Secret with negative max expose count | |
it('should throw an error when max expose count is negative', () => { | |
expect(() => new Secret<number>(undefined, () => 42, -1)).toThrowError( | |
'`max_expose_count` when specified must be a positive integer', | |
); | |
}); | |
// Test Case 14: Creating a Secret with both secret and function when function returns undefined | |
it('should throw an error when both secret and function are specified and function returns undefined', () => { | |
expect(() => new Secret<number>(undefined, () => undefined)).toThrowError( | |
'Function to generate the secret returned undefined', | |
); | |
}); | |
// Test Case 15: Attempting to expose the secret beyond max expose count | |
it('should not expose the secret beyond the specified max expose count', () => { | |
const secret = new Secret<number>(undefined, () => 42, 2); | |
secret.exposeSecret(); | |
secret.exposeSecret(); | |
expect(secret.exposeSecret()).toBeUndefined(); | |
}); | |
// Test Case 16: Getting the expose count when not specified | |
it('should return 0 as the expose count when not specified', () => { | |
const secret = new Secret<number>(undefined, () => 42); | |
expect(secret.expose_count).toBe(0); | |
}); | |
// Test Case 17: Creating a Secret with a function that throws an error | |
it('should throw an error when the function to generate the secret throws an error', () => { | |
expect( | |
() => | |
new Secret<number>(undefined, () => { | |
throw new Error('Function error'); | |
}), | |
).toThrowError('Function error'); | |
}); | |
// Test Case 18: Creating a Secret with an initial secret and max expose count | |
it('should limit exposure of the initial secret based on the max expose count', () => { | |
const secret = new Secret<string>('mySecret', undefined, 2); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
expect(secret.exposeSecret()).toBe('mySecret'); | |
expect(secret.exposeSecret()).toBeUndefined(); | |
}); | |
// Test Case 19: Creating a Secret with a function that generates undefined | |
it('should throw when the function generates undefined', () => { | |
expect(() => { | |
const secret = new Secret<number>(undefined, () => undefined, 2); | |
}).toThrow('Function to generate the secret returned undefined'); | |
}); | |
// Test Case 20: Creating a Secret with a negative max expose count | |
it('should throw an error when the max expose count is set to a negative number', () => { | |
expect(() => new Secret<number>(36, () => 69, -2)).toThrowError( | |
'Only `secret` or `func` should be specified', | |
); | |
}); | |
// Test Case 21: Creating a Secret with an initial secret and negative max expose count | |
it('should throw an error when the max expose count is set to a negative number', () => { | |
expect(() => new Secret<number>(69, undefined, -2)).toThrowError( | |
'`max_expose_count` when specified must be a positive integer', | |
); | |
}); | |
// Test Case 22: Creating a Secret with undefined as the initial secret | |
it('should throw an error when the initial secret is undefined', () => { | |
expect(() => new Secret<number>(undefined)).toThrowError( | |
'Either `secret` or `func` must be specified', | |
); | |
}); | |
// Test Case 23: Attempting to create a Secret with both initial secret and function returning undefined | |
it('should throw an error when both initial secret and function return undefined', () => { | |
expect(() => new Secret<number>(undefined, () => undefined)).toThrowError( | |
'Function to generate the secret returned undefined', | |
); | |
}); | |
// Test Case 24: Creating a Secret with both initial secret and function returning a value | |
it('should throw an error when both initial secret and function return a value', () => { | |
expect(() => new Secret<number>(69, () => 42)).toThrowError( | |
'Only `secret` or `func` should be specified', | |
); | |
}); | |
// Test Case 25: Creating a Secret with a function that throws an error | |
it('should throw an error when the function to generate the secret throws an error', () => { | |
expect( | |
() => | |
new Secret<number>(undefined, () => { | |
throw new Error('Function error'); | |
}), | |
).toThrowError('Function error'); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
let function_str: string = 'function' as const; | |
// https://stackoverflow.com/questions/40081332/what-does-the-is-keyword-do-in-typescript | |
function isFunction(maybeFunc: Function | unknown): maybeFunc is Function { | |
return typeof maybeFunc === 'function'; | |
} | |
/** | |
* The `Secret<T>` class represents a container for managing a secret value of type `T`. | |
* It allows limited exposure of the secret, optionally generated by a function, based on a maximum expose count. | |
* | |
* @template T - The type of the secret value. | |
*/ | |
class Secret<T> { | |
#inner_secret: T; | |
#max_expose_count: number = -1; | |
#expose_count: number = 0; | |
/** | |
* Creates a new `Secret` instance. | |
* | |
* @param {T | undefined} secret - The initial secret value (optional). | |
* @param {(...args: any[]) => T | undefined} func - A function to generate the secret value (optional). | |
* @param {number} max_expose_count - The maximum number of times the secret can be exposed (optional). | |
* @param {any[]} func_args - Additional arguments to pass to the function when generating the secret. | |
* | |
* @throws {Error} Either `secret` or `func` must be specified. | |
* @throws {Error} Only `secret` or `func` should be specified, not both. | |
*/ | |
constructor( | |
secret?: T, | |
func?: (...args: any[]) => T, | |
max_expose_count?: number, | |
...func_args: any[] | |
) { | |
if (!secret && !func) { | |
throw new Error('Either `secret` or `func` must be specified'); | |
} | |
if (secret && func) { | |
throw new Error('Only `secret` or `func` should be specified'); | |
} | |
if (isFunction(func)) { | |
let value = func(...func_args); | |
if (value == undefined || value == null) { | |
throw new Error('Function to generate the secret returned undefined'); | |
} | |
this.#inner_secret = func(...func_args); | |
} else if (secret) { | |
this.#inner_secret = secret; | |
} else { | |
throw new Error('Unreachable'); | |
} | |
if (max_expose_count || max_expose_count == 0) { | |
if (max_expose_count <= 0) { | |
throw new Error( | |
'`max_expose_count` when specified must be a positive integer', | |
); | |
} | |
this.#max_expose_count = max_expose_count; | |
} | |
} | |
/** | |
* Returns a string representation of the `Secret` instance. | |
* | |
* @returns {string} A string in the format "Secret<T>" indicating the type of the inner secret. | |
*/ | |
toString(): string { | |
return `Secret<${typeof this.#inner_secret}>`; | |
} | |
/** | |
* Exposes the secret value, subject to the maximum expose count. | |
* | |
* @returns {T | undefined} The secret value if it can be exposed, or `undefined` if the maximum expose count is reached. | |
*/ | |
exposeSecret(): T | undefined { | |
if (this.#max_expose_count > 0) { | |
if (this.#expose_count == this.#max_expose_count) { | |
return; | |
} else { | |
this.#expose_count++; | |
return this.#inner_secret; | |
} | |
} | |
this.#expose_count++; | |
return this.#inner_secret; | |
} | |
/** | |
* Gets the maximum allowed expose count for the secret. | |
* | |
* @returns {number} The maximum expose count. | |
*/ | |
get max_expose_count(): number { | |
return this.#max_expose_count; | |
} | |
/** | |
* Gets the current expose count for the secret. | |
* | |
* @returns {number} The current expose count. | |
*/ | |
get expose_count(): number { | |
return this.#expose_count; | |
} | |
} | |
export { Secret, isFunction }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SyncSecret } from './SyncSecret'; | |
describe('SyncSecret<T> Concurrency Tests', () => { | |
// Test Case 1: Concurrently expose the secret up to the max expose count | |
it('should allow concurrent exposures up to the max expose count', async () => { | |
const maxExposureCount = 3; | |
const secret = new SyncSecret<string>( | |
'mySecret', | |
undefined, | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Ensure that all exposures are allowed and return the secret | |
const results = await Promise.all(exposePromises); | |
expect(results).toEqual(['mySecret', 'mySecret', 'mySecret']); | |
}); | |
// Test Case 2: Concurrently expose the secret beyond the max expose count | |
it('should block further exposures after reaching the max expose count', async () => { | |
const maxExposureCount = 5; | |
const secret = new SyncSecret<string>( | |
'mySecret', | |
undefined, | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount + 3; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Ensure that only the first two exposures return the secret, the third returns undefined | |
const results = await Promise.all(exposePromises); | |
expect(results).toEqual([ | |
'mySecret', | |
'mySecret', | |
'mySecret', | |
'mySecret', | |
'mySecret', | |
undefined, | |
, | |
, | |
]); | |
}); | |
// Test Case 3: Concurrently expose the secret with multiple threads | |
it('should handle concurrent exposures from multiple threads', async () => { | |
const maxExposureCount = 4; | |
const secret = new SyncSecret<string>( | |
'mySecret', | |
undefined, | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Simulate multiple threads by running exposures in parallel | |
const results = await Promise.all([...exposePromises]); | |
// Ensure that the exposures are still limited to the max expose count | |
expect(results).toEqual(['mySecret', 'mySecret', 'mySecret', 'mySecret']); | |
}); | |
// Test Case 4: Concurrently expose the secret with a function that takes time to execute | |
it('should handle concurrent exposures with a slow function', async () => { | |
const maxExposureCount = 3; | |
const secret = new SyncSecret<string>( | |
undefined, | |
() => 'mySecret', | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Ensure that exposures are still limited to the max expose count | |
const results = await Promise.all(exposePromises); | |
expect(results).toEqual(['mySecret', 'mySecret', 'mySecret']); | |
}); | |
// Test Case 5: Concurrently expose the secret with max expose count | |
it('should handle concurrent exposures with max expose count and a slow function', async () => { | |
const maxExposureCount = 5000; | |
const secret = new SyncSecret<string>( | |
undefined, | |
() => 'mySecret', | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
const expectedResults = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
expectedResults.push('mySecret'); | |
} | |
for (let i = 0; i < 1000; i++) { | |
expectedResults.push(undefined); | |
} | |
for (let i = 0; i < maxExposureCount + 1000; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Ensure that only the first two exposures return the secret, the third returns undefined | |
const results = await Promise.all(exposePromises); | |
expect(results).toEqual(expectedResults); | |
}); | |
// Test Case 6: Concurrently expose the secret with a very large max expose count | |
it('should handle concurrent exposures with a large max expose count', async () => { | |
const maxExposureCount = 1000; | |
const secret = new SyncSecret<string>( | |
'mySecret', | |
undefined, | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount + 1000; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Ensure that only the first 1000 exposures return the secret, the rest return undefined | |
const results = await Promise.all(exposePromises); | |
expect(results.slice(0, maxExposureCount)).toEqual( | |
Array(maxExposureCount).fill('mySecret'), | |
); | |
expect(results.slice(maxExposureCount)).toEqual( | |
Array(1000).fill(undefined), | |
); | |
}); | |
// Test Case 7: Concurrently expose the secret with multiple threads and a large max expose count | |
it('should handle concurrent exposures from multiple threads with a large max expose count', async () => { | |
const maxExposureCount = 1000; | |
const secret = new SyncSecret<string>( | |
'mySecret', | |
undefined, | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Simulate multiple threads by running exposures in parallel | |
const results: Array<string> = await Promise.all([...exposePromises, ...exposePromises]); | |
// Ensure that the exposures are still limited to the max expose count | |
expect(results.slice(0, maxExposureCount)).toEqual( | |
Array(maxExposureCount).fill('mySecret'), | |
); | |
expect(results.slice(maxExposureCount)).toEqual( | |
Array(maxExposureCount).fill('mySecret'), | |
); | |
}); | |
// Test Case 8: Expose the secret with a function that takes time, ensuring the order is maintained | |
it('should maintain the order of exposures with a slow function', async () => { | |
const maxExposureCount = 1000; | |
// A slow function that takes some time to generate the secret | |
const slowFunc = (index: number) => { | |
return new Promise<string>((resolve) => { | |
setTimeout(() => resolve(`mySecret${index}`), 100); | |
}); | |
}; | |
const exposePromises = []; | |
const expectedResults = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
const secret = new SyncSecret<string>( | |
undefined, | |
() => `mySecret${i}`, | |
maxExposureCount, | |
); | |
exposePromises.push(secret.exposeSecret()); | |
} | |
for (let i = 0; i < maxExposureCount; i++) { | |
expectedResults.push(`mySecret${i}`); | |
} | |
// Ensure that exposures are in the correct order | |
const results = await Promise.all(exposePromises); | |
expect(results).toEqual(expectedResults); | |
}); | |
// Test Case 9: Concurrently expose the secret with multiple threads, a large max expose count, and a slow function | |
it('should handle concurrent exposures from multiple threads with a large max expose count and a slow function', async () => { | |
const maxExposureCount = 1000; | |
const secret = new SyncSecret<string>( | |
undefined, | |
() => 'mySecret', | |
maxExposureCount, | |
); | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Simulate multiple threads by running exposures in parallel | |
const results = await Promise.all([...exposePromises, ...exposePromises]); | |
// Ensure that the exposures are still limited to the max expose count | |
expect(results.slice(0, maxExposureCount)).toEqual( | |
Array(maxExposureCount).fill('mySecret'), | |
); | |
expect(results.slice(maxExposureCount)).toEqual( | |
Array(maxExposureCount).fill('mySecret'), | |
); | |
}); | |
// Test Case 10: Concurrently expose the secret with multiple threads, a large max expose count, and a slow function | |
it('should maintain order when handling concurrent exposures from multiple threads with a large max expose count and a slow function', async () => { | |
const maxExposureCount = 1000; | |
const exposePromises = []; | |
for (let i = 0; i < maxExposureCount; i++) { | |
const secret = new SyncSecret<string>( | |
undefined, | |
() => `mySecret${i}`, | |
maxExposureCount, | |
); | |
exposePromises.push(secret.exposeSecret()); | |
} | |
// Simulate multiple threads by running exposures in parallel | |
const results = await Promise.all([...exposePromises]); | |
// Ensure that the exposures maintain the correct order | |
const expectedResults = Array(maxExposureCount) | |
.fill(0) | |
.map((_, index) => `mySecret${index}`); | |
expect(results).toEqual(expectedResults); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isFunction } from './secret'; | |
export class SyncSecret<T> { | |
#inner_secret: T; | |
#max_expose_count: number = -1; | |
static sab = new SharedArrayBuffer(1024); | |
#expose_count: Uint32Array = new Uint32Array(SyncSecret.sab); | |
/** | |
* Create a new `SyncSecret` instance. | |
* | |
* @param {T} secret - The initial secret value. | |
* @param {(...args: any[]) => T} func - A function to generate the secret value. | |
* @param {number} max_expose_count - The maximum number of times the secret can be exposed. | |
* @param {...any[]} func_args - Arguments to pass to the function when generating the secret. | |
* | |
* @throws {Error} Throws an error if neither `secret` nor `func` is specified. | |
* @throws {Error} Throws an error if both `secret` and `func` are specified. | |
* @throws {Error} Throws an error if the function returns `undefined` or `null`. | |
* @throws {Error} Throws an error if `max_expose_count` is not a positive integer. | |
*/ | |
constructor( | |
secret?: T, | |
func?: (...args: any[]) => T, | |
max_expose_count?: number, | |
...func_args: any[] | |
) { | |
if (!secret && !func) { | |
throw new Error('Either `secret` or `func` must be specified'); | |
} | |
if (secret && func) { | |
throw new Error('Only `secret` or `func` should be specified'); | |
} | |
if (isFunction(func)) { | |
let value = func(...func_args); | |
if (value == undefined || value == null) { | |
throw new Error('Function to generate the secret returned undefined'); | |
} | |
this.#inner_secret = func(...func_args); | |
} else if (secret) { | |
this.#inner_secret = secret; | |
} else { | |
throw new Error('Unreachable'); | |
} | |
if (max_expose_count || max_expose_count == 0) { | |
if (max_expose_count <= 0) { | |
throw new Error( | |
'`max_expose_count` when specified must be a positive integer', | |
); | |
} | |
this.#max_expose_count = max_expose_count; | |
} | |
this.#expose_count[0] = 0; | |
} | |
/** | |
* Expose the secret value. | |
* | |
* @returns {Promise<T>} A promise that resolves to the secret value. | |
* Returns `undefined` if the max expose count is reached. | |
*/ | |
async exposeSecret(): Promise<T> { | |
if (this.#max_expose_count > 0) { | |
let old_expose_count = Atomics.load(this.#expose_count, 0); | |
if (old_expose_count == this.#max_expose_count) { | |
return; | |
} else { | |
Atomics.compareExchange( | |
this.#expose_count, | |
0, | |
old_expose_count, | |
old_expose_count + 1, | |
); | |
return this.#inner_secret; | |
} | |
} | |
Atomics.add(this.#expose_count, 0, 1); | |
return this.#inner_secret; | |
} | |
/** | |
* Returns a string representation of the `Secret` instance. | |
* | |
* @returns {string} A string in the format "Secret<T>" indicating the type of the inner secret. | |
*/ | |
toString(): string { | |
return `Secret<${typeof this.#inner_secret}>`; | |
} | |
/** | |
* Gets the maximum allowed expose count for the secret. | |
* | |
* @returns {number} The maximum expose count. | |
*/ | |
get max_expose_count(): number { | |
return this.#max_expose_count; | |
} | |
/** | |
* Gets the current expose count for the secret. | |
* | |
* @returns {number} The current expose count. | |
*/ | |
get expose_count(): number { | |
return Atomics.load(this.#expose_count, 0); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment