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 type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { IdentifierMap } from '../util/map/IdentifierMap';
import { getDefault } from '../util/map/MapUtil';
import { readableToQuads } from '../util/StreamUtil';
import { ACL } from '../util/Vocabularies';
import { getAccessControlledResources } from './AcpUtil';
@ -78,12 +79,8 @@ export class AcpReader extends PermissionReader {
// Extract all the policies relevant for the target
const identifiers = this.getAncestorIdentifiers(target);
for (const identifier of identifiers) {
let acrs = resourceCache.get(identifier);
if (!acrs) {
const data = await this.readAcrData(identifier);
acrs = [ ...getAccessControlledResources(data) ];
resourceCache.set(identifier, acrs);
}
const acrs = await getDefault(resourceCache, identifier, async(): Promise<IAccessControlledResource[]> =>
[ ...getAccessControlledResources(await this.readAcrData(identifier)) ]);
const size = policies.length;
policies.push(...this.getEffectivePolicies(target, acrs));
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 { concat } from '../util/IterableUtil';
import { IdentifierMap, IdentifierSetMultiMap } from '../util/map/IdentifierMap';
import { getDefault } from '../util/map/MapUtil';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/PathUtil';
import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';
@ -44,11 +45,7 @@ export class PathBasedReader extends PermissionReader {
for (const [ identifier, modes ] of accessMap) {
const reader = this.findReader(identifier.path);
if (reader) {
let matches = result.get(reader);
if (!matches) {
matches = new IdentifierSetMultiMap();
result.set(reader, matches);
}
const matches = getDefault(result, reader, (): AccessMap => new IdentifierSetMultiMap());
matches.set(identifier, modes);
}
}

View File

@ -27,7 +27,7 @@ export class UnionPermissionReader extends UnionHandler<PermissionReader> {
private mergePermissionMaps(permissionMap: PermissionMap, result: PermissionMap): void {
for (const [ identifier, permissionSet ] of permissionMap) {
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]);
}
}

View File

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

View File

@ -1,5 +1,29 @@
import { types } from 'util';
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
function noop(): void {}

View File

@ -10,6 +10,7 @@ import { isHttpRequest } from '../server/HttpRequest';
import { InternalServerError } from './errors/InternalServerError';
import type { Guarded } from './GuardedStream';
import { guardStream } from './GuardedStream';
import type { PromiseOrValue } from './PromiseUtil';
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
*/
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
*/
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 { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker';
@ -12,11 +13,11 @@ export class EqualReadWriteLocker implements ReadWriteLocker {
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);
}
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);
}
@ -26,7 +27,7 @@ export class EqualReadWriteLocker implements ReadWriteLocker {
* @param identifier - Identifier of resource that needs to be 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);
try {
return await whileLocked();

View File

@ -1,4 +1,5 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import type { PromiseOrValue } from '../PromiseUtil';
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.
* 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>;
/**
@ -26,6 +27,6 @@ export interface ExpiringReadWriteLocker extends ReadWriteLocker {
* @param whileLocked - A function to execute while the resource is locked.
* 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>;
}

View File

@ -2,6 +2,7 @@ import type { ResourceIdentifier } from '../../http/representation/ResourceIdent
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
import { ForbiddenHttpError } from '../errors/ForbiddenHttpError';
import { InternalServerError } from '../errors/InternalServerError';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker';
@ -43,7 +44,7 @@ export class GreedyReadWriteLocker implements ReadWriteLocker {
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);
try {
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}`)) {
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.
*/
private async withInternalReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => (Promise<T> | T)):
private async withInternalReadLock<T>(identifier: ResourceIdentifier, whileLocked: () => PromiseOrValue<T>):
Promise<T> {
const read = this.getReadLockKey(identifier);
await this.locker.acquire(read);

View File

@ -1,4 +1,5 @@
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.
@ -14,7 +15,7 @@ export interface ReadWriteLocker {
*
* @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.
@ -26,5 +27,5 @@ export interface ReadWriteLocker {
*
* @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 type { AttemptSettings } from '../LockUtils';
import { retryFunction } from '../LockUtils';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ReadWriteLocker } from './ReadWriteLocker';
import type { ResourceLocker } from './ResourceLocker';
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);
await retryFunction(
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);
await retryFunction(
this.swallowFalse(this.redisRw.acquireWriteLock.bind(this.redisRw, key)),

View File

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

View File

@ -1,6 +1,7 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../errors/InternalServerError';
import type { PromiseOrValue } from '../PromiseUtil';
import type { ExpiringReadWriteLocker } from './ExpiringReadWriteLocker';
import type { ReadWriteLocker } from './ReadWriteLocker';
import Timeout = NodeJS.Timeout;
@ -24,12 +25,12 @@ export class WrappedExpiringReadWriteLocker implements ExpiringReadWriteLocker {
}
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));
}
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));
}
@ -39,7 +40,7 @@ export class WrappedExpiringReadWriteLocker implements ExpiringReadWriteLocker {
* it receives. The ResourceIdentifier is only used for logging.
*/
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 createTimeout: () => Timeout;

View File

@ -1,3 +1,5 @@
import { resolvePromiseOrValue } from '../PromiseUtil';
import type { PromiseOrValue } from '../PromiseUtil';
import type { SetMultiMap } from './SetMultiMap';
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)`.
* 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.
*
* @param map - Map to use.
* @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);
if (value) {
return value;
}
map.set(key, defaultValue);
return defaultValue;
return resolvePromiseOrValue<TValue, TValue>(defaultFn(), (val): TValue => {
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 type { ResourceStore } from '../../../src/storage/ResourceStore';
import type { ExpiringReadWriteLocker } from '../../../src/util/locking/ExpiringReadWriteLocker';
import type { PromiseOrValue } from '../../../src/util/PromiseUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { flushPromises } from '../../util/Util';
@ -47,7 +48,7 @@ describe('A LockingResourceStore', (): void => {
locker = {
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');
try {
// Allows simulating a timeout event
@ -61,7 +62,7 @@ describe('A LockingResourceStore', (): void => {
}
}),
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');
try {
return await whileLocked(emptyFn);

View File

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

View File

@ -44,12 +44,17 @@ describe('MapUtil', (): void => {
describe('#getDefault', (): void => {
it('returns the value it finds in the Map for the given key.', async(): Promise<void> => {
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> => {
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);
});
});
});