mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
139 lines
5.5 KiB
TypeScript
139 lines
5.5 KiB
TypeScript
import type { Readable } from 'stream';
|
|
import type { Patch } from '../ldp/http/Patch';
|
|
import type { Representation } from '../ldp/representation/Representation';
|
|
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
|
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
|
import { getLoggerFor } from '../logging/LogUtil';
|
|
import type { Guarded } from '../util/GuardedStream';
|
|
import type { ExpiringLock } from '../util/locking/ExpiringLock';
|
|
import type { ExpiringResourceLocker } from '../util/locking/ExpiringResourceLocker';
|
|
import type { AtomicResourceStore } from './AtomicResourceStore';
|
|
import type { Conditions } from './Conditions';
|
|
import type { ResourceStore } from './ResourceStore';
|
|
|
|
/**
|
|
* Store that for every call acquires a lock before executing it on the requested resource,
|
|
* and releases it afterwards.
|
|
*/
|
|
export class LockingResourceStore implements AtomicResourceStore {
|
|
protected readonly logger = getLoggerFor(this);
|
|
|
|
private readonly source: ResourceStore;
|
|
private readonly locks: ExpiringResourceLocker;
|
|
|
|
public constructor(source: ResourceStore, locks: ExpiringResourceLocker) {
|
|
this.source = source;
|
|
this.locks = locks;
|
|
}
|
|
|
|
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
|
|
conditions?: Conditions): Promise<Representation> {
|
|
return this.lockedRepresentationRun(identifier,
|
|
async(): Promise<Representation> => this.source.getRepresentation(identifier, preferences, conditions));
|
|
}
|
|
|
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
|
conditions?: Conditions): Promise<ResourceIdentifier> {
|
|
return this.lockedRun(container,
|
|
async(): Promise<ResourceIdentifier> => this.source.addResource(container, representation, conditions));
|
|
}
|
|
|
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
|
conditions?: Conditions): Promise<void> {
|
|
return this.lockedRun(identifier,
|
|
async(): Promise<void> => this.source.setRepresentation(identifier, representation, conditions));
|
|
}
|
|
|
|
public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
|
|
return this.lockedRun(identifier, async(): Promise<void> => this.source.deleteResource(identifier, conditions));
|
|
}
|
|
|
|
public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
|
|
return this.lockedRun(identifier,
|
|
async(): Promise<void> => this.source.modifyResource(identifier, patch, conditions));
|
|
}
|
|
|
|
/**
|
|
* Acquires a lock for the identifier and releases it when the function is executed.
|
|
* @param identifier - Identifier that should be locked.
|
|
* @param func - Function to be executed.
|
|
*/
|
|
protected async lockedRun<T>(identifier: ResourceIdentifier, func: () => Promise<T>): Promise<T> {
|
|
const lock = await this.locks.acquire(identifier);
|
|
try {
|
|
return await func();
|
|
} finally {
|
|
await lock.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Acquires a lock for the identifier that should return a representation with Readable data and releases it when the
|
|
* Readable is read, closed or results in an error.
|
|
* When using this function, it is required to close the Readable stream when you are ready.
|
|
*
|
|
* @param identifier - Identifier that should be locked.
|
|
* @param func - Function to be executed.
|
|
*/
|
|
protected async lockedRepresentationRun(identifier: ResourceIdentifier, func: () => Promise<Representation>):
|
|
Promise<Representation> {
|
|
const lock = await this.locks.acquire(identifier);
|
|
let representation;
|
|
try {
|
|
// Make the resource time out to ensure that the lock is always released eventually.
|
|
representation = await func();
|
|
return this.createExpiringRepresentation(representation, lock);
|
|
} finally {
|
|
// If the representation contains a valid Readable, wait for it to be consumed.
|
|
const data = representation?.data;
|
|
if (!data) {
|
|
await lock.release();
|
|
} else {
|
|
// When an error occurs, destroy the readable so the lock is released safely.
|
|
data.on('error', (): void => data.destroy());
|
|
|
|
// An `end` and/or `close` event signals that the readable has been consumed.
|
|
new Promise((resolve): void => {
|
|
data.on('end', resolve);
|
|
data.on('close', resolve);
|
|
}).then((): any => lock.release(), null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wraps a representation to make it time out when nothing is read for a certain amount of time.
|
|
*
|
|
* @param source - The representation to wrap
|
|
* @param lock - The lock for the corresponding identifier.
|
|
*/
|
|
protected createExpiringRepresentation(source: Representation, lock: ExpiringLock): Representation {
|
|
return Object.create(source, {
|
|
data: { value: this.createExpiringReadable(source.data, lock) },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wraps a readable to make it time out when nothing is read for a certain amount of time.
|
|
*
|
|
* @param source - The readable to wrap
|
|
* @param lock - The lock for the corresponding identifier.
|
|
*/
|
|
protected createExpiringReadable(source: Guarded<Readable>, lock: ExpiringLock): Readable {
|
|
// Destroy the source when a timeout occurs.
|
|
lock.on('expired', (): void => {
|
|
source.destroy(new Error(`Stream reading timout exceeded`));
|
|
});
|
|
|
|
// Spy on the source to renew the lock upon reading.
|
|
return Object.create(source, {
|
|
read: {
|
|
value(size: number): any {
|
|
lock.renew();
|
|
return source.read(size);
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|