Skip to content

Instantly share code, notes, and snippets.

@jymchng
Created August 20, 2023 04:28
Show Gist options
  • Save jymchng/341c6899ec9a822ef57721275a21a00f to your computer and use it in GitHub Desktop.
Save jymchng/341c6899ec9a822ef57721275a21a00f to your computer and use it in GitHub Desktop.
Typescript Secret/SyncSecret
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');
});
});
'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 };
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);
});
});
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