feat: Support async default values in getDefault

This commit is contained in:
Joachim Van Herwegen 2022-10-05 11:13:11 +02:00
parent c73ef50e48
commit a1e916b73a
17 changed files with 93 additions and 43 deletions

View File

@ -15,6 +15,7 @@ import { InternalServerError } from '../util/errors/InternalServerError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { IdentifierMap } from '../util/map/IdentifierMap'; import { IdentifierMap } from '../util/map/IdentifierMap';
import { getDefault } from '../util/map/MapUtil';
import { readableToQuads } from '../util/StreamUtil'; import { readableToQuads } from '../util/StreamUtil';
import { ACL } from '../util/Vocabularies'; import { ACL } from '../util/Vocabularies';
import { getAccessControlledResources } from './AcpUtil'; import { getAccessControlledResources } from './AcpUtil';
@ -78,12 +79,8 @@ export class AcpReader extends PermissionReader {
// Extract all the policies relevant for the target // Extract all the policies relevant for the target
const identifiers = this.getAncestorIdentifiers(target); const identifiers = this.getAncestorIdentifiers(target);
for (const identifier of identifiers) { for (const identifier of identifiers) {
let acrs = resourceCache.get(identifier); const acrs = await getDefault(resourceCache, identifier, async(): Promise<IAccessControlledResource[]> =>
if (!acrs) { [ ...getAccessControlledResources(await this.readAcrData(identifier)) ]);
const data = await this.readAcrData(identifier);
acrs = [ ...getAccessControlledResources(data) ];
resourceCache.set(identifier, acrs);
}
const size = policies.length; const size = policies.length;
policies.push(...this.getEffectivePolicies(target, acrs)); policies.push(...this.getEffectivePolicies(target, acrs));
this.logger.debug(`Found ${policies.length - size} policies relevant for ${target.path} in ${identifier.path}`); this.logger.debug(`Found ${policies.length - size} policies relevant for ${target.path} in ${identifier.path}`);

View File

@ -1,6 +1,7 @@
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import { concat } from '../util/IterableUtil'; import { concat } from '../util/IterableUtil';
import { IdentifierMap, IdentifierSetMultiMap } from '../util/map/IdentifierMap'; import { IdentifierMap, IdentifierSetMultiMap } from '../util/map/IdentifierMap';
import { getDefault } from '../util/map/MapUtil';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/PathUtil'; import { ensureTrailingSlash, trimTrailingSlashes } from '../util/PathUtil';
import type { PermissionReaderInput } from './PermissionReader'; import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader'; import { PermissionReader } from './PermissionReader';
@ -44,11 +45,7 @@ export class PathBasedReader extends PermissionReader {
for (const [ identifier, modes ] of accessMap) { for (const [ identifier, modes ] of accessMap) {
const reader = this.findReader(identifier.path); const reader = this.findReader(identifier.path);
if (reader) { if (reader) {
let matches = result.get(reader); const matches = getDefault(result, reader, (): AccessMap => new IdentifierSetMultiMap());
if (!matches) {
matches = new IdentifierSetMultiMap();
result.set(reader, matches);
}
matches.set(identifier, modes); matches.set(identifier, modes);
} }
} }

View File

@ -27,7 +27,7 @@ export class UnionPermissionReader extends UnionHandler<PermissionReader> {
private mergePermissionMaps(permissionMap: PermissionMap, result: PermissionMap): void { private mergePermissionMaps(permissionMap: PermissionMap, result: PermissionMap): void {
for (const [ identifier, permissionSet ] of permissionMap) { for (const [ identifier, permissionSet ] of permissionMap) {
for (const [ credential, permission ] of Object.entries(permissionSet) as [keyof PermissionSet, Permission][]) { for (const [ credential, permission ] of Object.entries(permissionSet) as [keyof PermissionSet, Permission][]) {
const resultSet = getDefault(result, identifier, {}); const resultSet = getDefault(result, identifier, (): PermissionSet => ({}));
resultSet[credential] = this.mergePermissions(permission, resultSet[credential]); resultSet[credential] = this.mergePermissions(permission, resultSet[credential]);
} }
} }

View File

@ -1,10 +1,10 @@
import type { ValuePreferences } from '../../http/representation/RepresentationPreferences'; import type { ValuePreferences } from '../../http/representation/RepresentationPreferences';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { PromiseOrValue } from '../../util/PromiseUtil';
import { getConversionTarget, getTypeWeight, preferencesToString } from './ConversionUtil'; import { getConversionTarget, getTypeWeight, preferencesToString } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter';
type PromiseOrValue<T> = T | Promise<T>;
type ValuePreferencesArg = type ValuePreferencesArg =
PromiseOrValue<string> | PromiseOrValue<string> |
PromiseOrValue<string[]> | PromiseOrValue<string[]> |

View File

@ -1,5 +1,29 @@
import { types } from 'util';
import { createAggregateError } from './errors/HttpErrorUtil'; import { createAggregateError } from './errors/HttpErrorUtil';
export type PromiseOrValue<T> = T | Promise<T>;
/**
* Verifies if the given value is a Promise or not.
* @param object - Object to check.
*/
export function isPromise<T>(object: PromiseOrValue<T>): object is Promise<T> {
return types.isPromise(object);
}
/**
* Calls `callback` with the resolved value of `object`.
* In case `object` is a Promise, the result will also be a Promise,
* otherwise the result will be sync.
*/
export function resolvePromiseOrValue<TIn, TOut>(object: PromiseOrValue<TIn>, callback: (val: TIn) => TOut):
PromiseOrValue<TOut> {
if (isPromise(object)) {
return object.then((val): TOut => callback(val));
}
return callback(object);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
function noop(): void {} function noop(): void {}

View File

@ -10,6 +10,7 @@ import { isHttpRequest } from '../server/HttpRequest';
import { InternalServerError } from './errors/InternalServerError'; import { InternalServerError } from './errors/InternalServerError';
import type { Guarded } from './GuardedStream'; import type { Guarded } from './GuardedStream';
import { guardStream } from './GuardedStream'; import { guardStream } from './GuardedStream';
import type { PromiseOrValue } from './PromiseUtil';
export const endOfStream = promisify(eos); export const endOfStream = promisify(eos);
@ -119,12 +120,12 @@ export interface AsyncTransformOptions<T = any> extends DuplexOptions {
/** /**
* Transforms data from the source by calling the `push` method * Transforms data from the source by calling the `push` method
*/ */
transform?: (this: Transform, data: T, encoding: string) => any | Promise<any>; transform?: (this: Transform, data: T, encoding: string) => PromiseOrValue<any>;
/** /**
* Performs any final actions after the source has ended * Performs any final actions after the source has ended
*/ */
flush?: (this: Transform) => any | Promise<any>; flush?: (this: Transform) => PromiseOrValue<any>;
} }
/** /**

View File

@ -1,4 +1,5 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker'; import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker'; import type { ResourceLocker } from './ResourceLocker';
@ -12,11 +13,11 @@ export class EqualReadWriteLocker implements ReadWriteLocker {
this.locker = locker; this.locker = locker;
} }
public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
return this.withLock(identifier, whileLocked); return this.withLock(identifier, whileLocked);
} }
public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
return this.withLock(identifier, whileLocked); return this.withLock(identifier, whileLocked);
} }
@ -26,7 +27,7 @@ export class EqualReadWriteLocker implements ReadWriteLocker {
* @param identifier - Identifier of resource that needs to be locked. * @param identifier - Identifier of resource that needs to be locked.
* @param whileLocked - Function to resolve while the resource is locked. * @param whileLocked - Function to resolve while the resource is locked.
*/ */
private async withLock<T>(identifier: ResourceIdentifier, whileLocked: () => T | Promise<T>): Promise<T> { private async withLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
await this.locker.acquire(identifier); await this.locker.acquire(identifier);
try { try {
return await whileLocked(); return await whileLocked();

View File

@ -1,4 +1,5 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker'; import type { ReadWriteLocker } from './ReadWriteLocker';
/** /**
@ -14,7 +15,7 @@ export interface ExpiringReadWriteLocker extends ReadWriteLocker {
* @param whileLocked - A function to execute while the resource is locked. * @param whileLocked - A function to execute while the resource is locked.
* Receives a callback as input parameter to maintain the lock. * Receives a callback as input parameter to maintain the lock.
*/ */
withReadLock: <T>(identifier: ResourceIdentifier, whileLocked: (maintainLock: () => void) => T | Promise<T>) withReadLock: <T>(identifier: ResourceIdentifier, whileLocked: (maintainLock: () => void) => PromiseOrValue<T>)
=> Promise<T>; => Promise<T>;
/** /**
@ -26,6 +27,6 @@ export interface ExpiringReadWriteLocker extends ReadWriteLocker {
* @param whileLocked - A function to execute while the resource is locked. * @param whileLocked - A function to execute while the resource is locked.
* Receives a callback as input parameter to maintain the lock. * Receives a callback as input parameter to maintain the lock.
*/ */
withWriteLock: <T>(identifier: ResourceIdentifier, whileLocked: (maintainLock: () => void) => T | Promise<T>) withWriteLock: <T>(identifier: ResourceIdentifier, whileLocked: (maintainLock: () => void) => PromiseOrValue<T>)
=> Promise<T>; => Promise<T>;
} }

View File

@ -2,6 +2,7 @@ import type { ResourceIdentifier } from '../../http/representation/ResourceIdent
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage'; import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
import { ForbiddenHttpError } from '../errors/ForbiddenHttpError'; import { ForbiddenHttpError } from '../errors/ForbiddenHttpError';
import { InternalServerError } from '../errors/InternalServerError'; import { InternalServerError } from '../errors/InternalServerError';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker'; import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker'; import type { ResourceLocker } from './ResourceLocker';
@ -43,7 +44,7 @@ export class GreedyReadWriteLocker implements ReadWriteLocker {
this.suffixes = suffixes; this.suffixes = suffixes;
} }
public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
await this.preReadSetup(identifier); await this.preReadSetup(identifier);
try { try {
return await whileLocked(); return await whileLocked();
@ -52,7 +53,7 @@ export class GreedyReadWriteLocker implements ReadWriteLocker {
} }
} }
public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
if (identifier.path.endsWith(`.${this.suffixes.count}`)) { if (identifier.path.endsWith(`.${this.suffixes.count}`)) {
throw new ForbiddenHttpError('This resource is used for internal purposes.'); throw new ForbiddenHttpError('This resource is used for internal purposes.');
} }
@ -117,7 +118,7 @@ export class GreedyReadWriteLocker implements ReadWriteLocker {
/** /**
* Safely runs an action on the count. * Safely runs an action on the count.
*/ */
private async withInternalReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): private async withInternalReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
Promise<T> { Promise<T> {
const read = this.getReadLockKey(identifier); const read = this.getReadLockKey(identifier);
await this.locker.acquire(read); await this.locker.acquire(read);

View File

@ -1,4 +1,5 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import type { PromiseOrValue } from '../PromiseUtil';
/** /**
* Allows the locking of resources which is needed for non-atomic {@link ResourceStore}s. * Allows the locking of resources which is needed for non-atomic {@link ResourceStore}s.
@ -14,7 +15,7 @@ export interface ReadWriteLocker {
* *
* @returns A promise resolving when the lock is released. * @returns A promise resolving when the lock is released.
*/ */
withReadLock: <T>(identifier: ResourceIdentifier, whileLocked: () => T | Promise<T>) => Promise<T>; withReadLock: <T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>) => Promise<T>;
/** /**
* Run the given function while the resource is locked. * Run the given function while the resource is locked.
@ -26,5 +27,5 @@ export interface ReadWriteLocker {
* *
* @returns A promise resolving when the lock is released. * @returns A promise resolving when the lock is released.
*/ */
withWriteLock: <T>(identifier: ResourceIdentifier, whileLocked: () => T | Promise<T>) => Promise<T>; withWriteLock: <T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>) => Promise<T>;
} }

View File

@ -5,6 +5,7 @@ import type { Initializable } from '../../init/Initializable';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import type { AttemptSettings } from '../LockUtils'; import type { AttemptSettings } from '../LockUtils';
import { retryFunction } from '../LockUtils'; import { retryFunction } from '../LockUtils';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker'; import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker'; import type { ResourceLocker } from './ResourceLocker';
import type { RedisResourceLock, RedisReadWriteLock, RedisAnswer } from './scripts/RedisLuaScripts'; import type { RedisResourceLock, RedisReadWriteLock, RedisAnswer } from './scripts/RedisLuaScripts';
@ -127,7 +128,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
}; };
} }
public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
const key = this.getReadWriteKey(identifier); const key = this.getReadWriteKey(identifier);
await retryFunction( await retryFunction(
this.swallowFalse(this.redisRw.acquireReadLock.bind(this.redisRw, key)), this.swallowFalse(this.redisRw.acquireReadLock.bind(this.redisRw, key)),
@ -143,7 +144,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
} }
} }
public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)): Promise<T> { public async withWriteLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>): Promise<T> {
const key = this.getReadWriteKey(identifier); const key = this.getReadWriteKey(identifier);
await retryFunction( await retryFunction(
this.swallowFalse(this.redisRw.acquireWriteLock.bind(this.redisRw, key)), this.swallowFalse(this.redisRw.acquireWriteLock.bind(this.redisRw, key)),

View File

@ -1,5 +1,6 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ExpiringReadWriteLocker } from './ExpiringReadWriteLocker'; import type { ExpiringReadWriteLocker } from './ExpiringReadWriteLocker';
/** /**
@ -20,14 +21,14 @@ export class VoidLocker implements ExpiringReadWriteLocker {
public async withReadLock<T>( public async withReadLock<T>(
identifier: ResourceIdentifier, identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>, whileLocked: (maintainLock: () => void) => PromiseOrValue<T>,
): Promise<T> { ): Promise<T> {
return whileLocked(noop); return whileLocked(noop);
} }
public async withWriteLock<T>( public async withWriteLock<T>(
identifier: ResourceIdentifier, identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>, whileLocked: (maintainLock: () => void) => PromiseOrValue<T>,
): Promise<T> { ): Promise<T> {
return whileLocked(noop); return whileLocked(noop);
} }

View File

@ -1,6 +1,7 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../errors/InternalServerError'; import { InternalServerError } from '../errors/InternalServerError';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ExpiringReadWriteLocker } from './ExpiringReadWriteLocker'; import type { ExpiringReadWriteLocker } from './ExpiringReadWriteLocker';
import type { ReadWriteLocker } from './ReadWriteLocker'; import type { ReadWriteLocker } from './ReadWriteLocker';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@ -24,12 +25,12 @@ export class WrappedExpiringReadWriteLocker implements ExpiringReadWriteLocker {
} }
public async withReadLock<T>(identifier: ResourceIdentifier, public async withReadLock<T>(identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>): Promise<T> { whileLocked: (maintainLock: () => void) => PromiseOrValue<T>): Promise<T> {
return this.locker.withReadLock(identifier, async(): Promise<T> => this.expiringPromise(identifier, whileLocked)); return this.locker.withReadLock(identifier, async(): Promise<T> => this.expiringPromise(identifier, whileLocked));
} }
public async withWriteLock<T>(identifier: ResourceIdentifier, public async withWriteLock<T>(identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>): Promise<T> { whileLocked: (maintainLock: () => void) => PromiseOrValue<T>): Promise<T> {
return this.locker.withWriteLock(identifier, async(): Promise<T> => this.expiringPromise(identifier, whileLocked)); return this.locker.withWriteLock(identifier, async(): Promise<T> => this.expiringPromise(identifier, whileLocked));
} }
@ -39,7 +40,7 @@ export class WrappedExpiringReadWriteLocker implements ExpiringReadWriteLocker {
* it receives. The ResourceIdentifier is only used for logging. * it receives. The ResourceIdentifier is only used for logging.
*/ */
private async expiringPromise<T>(identifier: ResourceIdentifier, private async expiringPromise<T>(identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>): Promise<T> { whileLocked: (maintainLock: () => void) => PromiseOrValue<T>): Promise<T> {
let timer: Timeout; let timer: Timeout;
let createTimeout: () => Timeout; let createTimeout: () => Timeout;

View File

@ -1,3 +1,5 @@
import { resolvePromiseOrValue } from '../PromiseUtil';
import type { PromiseOrValue } from '../PromiseUtil';
import type { SetMultiMap } from './SetMultiMap'; import type { SetMultiMap } from './SetMultiMap';
export type MapKey<T> = T extends Map<infer TKey, any> ? TKey : never; export type MapKey<T> = T extends Map<infer TKey, any> ? TKey : never;
@ -42,18 +44,33 @@ export function modify<T extends SetMultiMap<any, any>>(map: T, options: ModifyO
/** /**
* Finds the result of calling `map.get(key)`. * Finds the result of calling `map.get(key)`.
* If there is no result, it instead returns the default value. * If there is no result, it instead returns the result of the default function.
* The Map will also be updated to assign that default value to the given key. * The Map will also be updated to assign that default value to the given key.
* *
* @param map - Map to use. * @param map - Map to use.
* @param key - Key to find the value for. * @param key - Key to find the value for.
* @param defaultValue - Value to insert and return if no result was found. * @param defaultFn - Function to generate default value to insert and return if no result was found.
*/ */
export function getDefault<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, defaultValue: TValue): TValue { export function getDefault<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, defaultFn: () => TValue): TValue;
/**
* Finds the result of calling `map.get(key)`.
* If there is no result, it instead returns the result of the default function.
* The Map will also be updated to assign the resolved default value to the given key.
*
* @param map - Map to use.
* @param key - Key to find the value for.
* @param defaultFn - Function to generate default value to insert and return if no result was found.
*/
export function getDefault<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, defaultFn: () => Promise<TValue>):
Promise<TValue>;
export function getDefault<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, defaultFn: () => PromiseOrValue<TValue>):
PromiseOrValue<TValue> {
const value = map.get(key); const value = map.get(key);
if (value) { if (value) {
return value; return value;
} }
map.set(key, defaultValue); return resolvePromiseOrValue<TValue, TValue>(defaultFn(), (val): TValue => {
return defaultValue; map.set(key, val);
return val;
});
} }

View File

@ -6,6 +6,7 @@ import type { ResourceIdentifier } from '../../../src/http/representation/Resour
import { LockingResourceStore } from '../../../src/storage/LockingResourceStore'; import { LockingResourceStore } from '../../../src/storage/LockingResourceStore';
import type { ResourceStore } from '../../../src/storage/ResourceStore'; import type { ResourceStore } from '../../../src/storage/ResourceStore';
import type { ExpiringReadWriteLocker } from '../../../src/util/locking/ExpiringReadWriteLocker'; import type { ExpiringReadWriteLocker } from '../../../src/util/locking/ExpiringReadWriteLocker';
import type { PromiseOrValue } from '../../../src/util/PromiseUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil'; import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { flushPromises } from '../../util/Util'; import { flushPromises } from '../../util/Util';
@ -47,7 +48,7 @@ describe('A LockingResourceStore', (): void => {
locker = { locker = {
withReadLock: jest.fn(async <T>(id: ResourceIdentifier, withReadLock: jest.fn(async <T>(id: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>): Promise<T> => { whileLocked: (maintainLock: () => void) => PromiseOrValue<T>): Promise<T> => {
order.push('lock read'); order.push('lock read');
try { try {
// Allows simulating a timeout event // Allows simulating a timeout event
@ -61,7 +62,7 @@ describe('A LockingResourceStore', (): void => {
} }
}), }),
withWriteLock: jest.fn(async <T>(identifier: ResourceIdentifier, withWriteLock: jest.fn(async <T>(identifier: ResourceIdentifier,
whileLocked: (maintainLock: () => void) => T | Promise<T>): Promise<T> => { whileLocked: (maintainLock: () => void) => PromiseOrValue<T>): Promise<T> => {
order.push('lock write'); order.push('lock write');
try { try {
return await whileLocked(emptyFn); return await whileLocked(emptyFn);

View File

@ -1,6 +1,7 @@
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import type { ReadWriteLocker } from '../../../../src/util/locking/ReadWriteLocker'; import type { ReadWriteLocker } from '../../../../src/util/locking/ReadWriteLocker';
import { WrappedExpiringReadWriteLocker } from '../../../../src/util/locking/WrappedExpiringReadWriteLocker'; import { WrappedExpiringReadWriteLocker } from '../../../../src/util/locking/WrappedExpiringReadWriteLocker';
import type { PromiseOrValue } from '../../../../src/util/PromiseUtil';
jest.useFakeTimers(); jest.useFakeTimers();
@ -14,9 +15,9 @@ describe('A WrappedExpiringReadWriteLocker', (): void => {
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
wrappedLocker = { wrappedLocker = {
withReadLock: jest.fn(async<T>(id: ResourceIdentifier, whileLocked: () => T | Promise<T>): withReadLock: jest.fn(async<T>(id: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
Promise<T> => whileLocked()), Promise<T> => whileLocked()),
withWriteLock: jest.fn(async<T>(id: ResourceIdentifier, whileLocked: () => T | Promise<T>): withWriteLock: jest.fn(async<T>(id: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
Promise<T> => whileLocked()), Promise<T> => whileLocked()),
}; };

View File

@ -44,12 +44,17 @@ describe('MapUtil', (): void => {
describe('#getDefault', (): void => { describe('#getDefault', (): void => {
it('returns the value it finds in the Map for the given key.', async(): Promise<void> => { it('returns the value it finds in the Map for the given key.', async(): Promise<void> => {
const map = new Map([[ key1, 123 ]]); const map = new Map([[ key1, 123 ]]);
expect(getDefault(map, key1, 999)).toBe(123); expect(getDefault(map, key1, (): number => 999)).toBe(123);
}); });
it('returns the default value if it finds no value for the given key.', async(): Promise<void> => { it('returns the default value if it finds no value for the given key.', async(): Promise<void> => {
const map = new Map([[ key1, 123 ]]); const map = new Map([[ key1, 123 ]]);
expect(getDefault(map, key2, 999)).toBe(999); expect(getDefault(map, key2, (): number => 999)).toBe(999);
});
it('can handle async default functions.', async(): Promise<void> => {
const map = new Map([[ key1, 123 ]]);
await expect(getDefault(map, key2, async(): Promise<number> => 999)).resolves.toBe(999);
}); });
}); });
}); });