parent
0fa45f82de
commit
21d631c406
@ -0,0 +1,158 @@ |
|||||||
|
import type { LeafUri } from "./uriTypes"; |
||||||
|
import type { PresentLeaf } from "./resource/abstract/leaf/PresentLeaf"; |
||||||
|
import type { DataLeaf } from "./resource/abstract/leaf/DataLeaf"; |
||||||
|
import type { ResourceError } from "./resource/error/ResourceError"; |
||||||
|
import type { BinaryLeaf } from "./resource/abstract/leaf/BinaryLeaf"; |
||||||
|
import type { DatasetChanges } from "@ldo/rdf-utils"; |
||||||
|
import type { AbsentLeaf } from "./resource/abstract/leaf/AbsentLeaf"; |
||||||
|
|
||||||
|
export interface WaitingProcess<Args extends unknown[], Return> { |
||||||
|
name: string; |
||||||
|
args: Args; |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
awaitingResolutions: ((returnValue: Return) => void)[]; |
||||||
|
awaitingRejections: ((err: unknown) => void)[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface WaitingProcessOptions<Args extends unknown[], Return> { |
||||||
|
name: string; |
||||||
|
args: Args; |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
modifyLastProcess: ( |
||||||
|
lastProcess: WaitingProcess<unknown[], unknown>, |
||||||
|
args: Args, |
||||||
|
) => boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export abstract class LeafRequestBatcher { |
||||||
|
private lastRequestTimestampMap: Record<string, number> = {}; |
||||||
|
private isWaiting: boolean = false; |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private processQueue: WaitingProcess<any[], any>[] = []; |
||||||
|
private shouldBatchAllRequests: boolean; |
||||||
|
private batchMilis: number; |
||||||
|
|
||||||
|
private triggerOrWaitProcess() { |
||||||
|
const processName = this.shouldBatchAllRequests |
||||||
|
? "any" |
||||||
|
: this.processQueue[0].name; |
||||||
|
|
||||||
|
// Set last request timestamp if not available
|
||||||
|
if (!this.lastRequestTimestampMap[processName]) { |
||||||
|
this.lastRequestTimestampMap[processName] = Date.UTC(0, 0, 0, 0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
const lastRequestTimestamp = this.lastRequestTimestampMap[processName]; |
||||||
|
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; |
||||||
|
|
||||||
|
const triggerProcess = async () => { |
||||||
|
this.isWaiting = false; |
||||||
|
this.lastRequestTimestampMap[processName] = Date.now(); |
||||||
|
this.lastRequestTimestampMap["any"] = Date.now(); |
||||||
|
const processToTrigger = this.processQueue.shift(); |
||||||
|
if (processToTrigger) { |
||||||
|
try { |
||||||
|
const returnValue = await processToTrigger.perform( |
||||||
|
processToTrigger.args, |
||||||
|
); |
||||||
|
processToTrigger.awaitingResolutions.forEach((callback) => { |
||||||
|
callback(returnValue); |
||||||
|
}); |
||||||
|
} catch (err) { |
||||||
|
processToTrigger.awaitingRejections.forEach((callback) => { |
||||||
|
callback(err); |
||||||
|
}); |
||||||
|
} |
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (timeSinceLastTrigger < this.batchMilis && !this.isWaiting) { |
||||||
|
this.isWaiting = true; |
||||||
|
setTimeout(triggerProcess, this.batchMilis - timeSinceLastTrigger); |
||||||
|
} else { |
||||||
|
triggerProcess(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected async queueProcess<ReturnType>( |
||||||
|
options: WaitingProcessOptions<unknown[], ReturnType>, |
||||||
|
): Promise<ReturnType> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const lastProcessInQueue = |
||||||
|
this.processQueue[this.processQueue.length - 1]; |
||||||
|
if (lastProcessInQueue) { |
||||||
|
const didModifyLast = lastProcessInQueue |
||||||
|
? options.modifyLastProcess(lastProcessInQueue, options.args) |
||||||
|
: false; |
||||||
|
if (didModifyLast) { |
||||||
|
lastProcessInQueue.awaitingResolutions.push(resolve); |
||||||
|
lastProcessInQueue.awaitingRejections.push(reject); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
this.processQueue.push({ |
||||||
|
name: options.name, |
||||||
|
args: options.args, |
||||||
|
perform: options.perform, |
||||||
|
awaitingResolutions: [resolve], |
||||||
|
awaitingRejections: [reject], |
||||||
|
}); |
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
|
||||||
|
// const READ_KEY = "read";
|
||||||
|
// const lastProcessInQueue =
|
||||||
|
// this.processQueue[this.processQueue.length - 1];
|
||||||
|
// if (lastProcessInQueue?.name === READ_KEY) {
|
||||||
|
// lastProcessInQueue.awaitingResolutions.push(resolve);
|
||||||
|
// lastProcessInQueue.awaitingRejections.push(reject);
|
||||||
|
// } else {
|
||||||
|
// const readProcess: WaitingProcess<[], PresentLeaf | ResourceError> = {
|
||||||
|
// name: READ_KEY,
|
||||||
|
// args: [],
|
||||||
|
// perform: this.performRead,
|
||||||
|
// awaitingResolutions: [resolve],
|
||||||
|
// awaitingRejections: [reject],
|
||||||
|
// };
|
||||||
|
// this.processQueue.push(readProcess);
|
||||||
|
// this.triggerOrWaitProcess();
|
||||||
|
// }
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// All intance variables
|
||||||
|
uri: LeafUri; |
||||||
|
|
||||||
|
// Read Methods
|
||||||
|
read(): Promise<PresentLeaf | ResourceError> { |
||||||
|
const READ_KEY = "read"; |
||||||
|
return this.queueProcess({ |
||||||
|
name: READ_KEY, |
||||||
|
args: [], |
||||||
|
perform: this.performRead, |
||||||
|
modifyLastProcess: (last) => { |
||||||
|
return last.name === READ_KEY; |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private performRead(): Promise<PresentLeaf | ResourceError> { |
||||||
|
console.log("Reading"); |
||||||
|
throw new Error("Doing Read"); |
||||||
|
} |
||||||
|
|
||||||
|
// Create Methods
|
||||||
|
abstract createData(overwrite?: boolean): Promise<DataLeaf | ResourceError>; |
||||||
|
|
||||||
|
abstract upload( |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<BinaryLeaf | ResourceError>; |
||||||
|
|
||||||
|
abstract updateData( |
||||||
|
changes: DatasetChanges, |
||||||
|
): Promise<DataLeaf | ResourceError>; |
||||||
|
|
||||||
|
abstract delete(): Promise<AbsentLeaf | ResourceError>; |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||||
|
export interface WaitingProcess<Args extends any[], Return> { |
||||||
|
name: string; |
||||||
|
args: Args; |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
awaitingResolutions: ((returnValue: Return) => void)[]; |
||||||
|
awaitingRejections: ((err: any) => void)[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface WaitingProcessOptions<Args extends any[], Return> { |
||||||
|
name: string; |
||||||
|
args: Args; |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
modifyLastProcess: ( |
||||||
|
lastProcess: WaitingProcess<any[], any>, |
||||||
|
args: Args, |
||||||
|
) => boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export class RequestBatcher { |
||||||
|
private lastRequestTimestampMap: Record<string, number> = {}; |
||||||
|
private isWaiting: boolean = false; |
||||||
|
private processQueue: WaitingProcess<any[], any>[] = []; |
||||||
|
public shouldBatchAllRequests: boolean; |
||||||
|
public batchMillis: number; |
||||||
|
|
||||||
|
constructor( |
||||||
|
options?: Partial<{ |
||||||
|
shouldBatchAllRequests: boolean; |
||||||
|
batchMillis: number; |
||||||
|
}>, |
||||||
|
) { |
||||||
|
this.shouldBatchAllRequests = options?.shouldBatchAllRequests || false; |
||||||
|
this.batchMillis = options?.batchMillis || 1000; |
||||||
|
} |
||||||
|
|
||||||
|
private triggerOrWaitProcess() { |
||||||
|
if (!this.processQueue[0]) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const processName = this.shouldBatchAllRequests |
||||||
|
? "any" |
||||||
|
: this.processQueue[0].name; |
||||||
|
|
||||||
|
// Set last request timestamp if not available
|
||||||
|
if (!this.lastRequestTimestampMap[processName]) { |
||||||
|
this.lastRequestTimestampMap[processName] = Date.UTC(0, 0, 0, 0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
const lastRequestTimestamp = this.lastRequestTimestampMap[processName]; |
||||||
|
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; |
||||||
|
|
||||||
|
const triggerProcess = async () => { |
||||||
|
this.isWaiting = false; |
||||||
|
this.lastRequestTimestampMap[processName] = Date.now(); |
||||||
|
this.lastRequestTimestampMap["any"] = Date.now(); |
||||||
|
const processToTrigger = this.processQueue.shift(); |
||||||
|
if (processToTrigger) { |
||||||
|
try { |
||||||
|
const returnValue = await processToTrigger.perform( |
||||||
|
...processToTrigger.args, |
||||||
|
); |
||||||
|
processToTrigger.awaitingResolutions.forEach((callback) => { |
||||||
|
callback(returnValue); |
||||||
|
}); |
||||||
|
} catch (err) { |
||||||
|
processToTrigger.awaitingRejections.forEach((callback) => { |
||||||
|
callback(err); |
||||||
|
}); |
||||||
|
} |
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (timeSinceLastTrigger < this.batchMillis && !this.isWaiting) { |
||||||
|
this.isWaiting = true; |
||||||
|
setTimeout(triggerProcess, this.batchMillis - timeSinceLastTrigger); |
||||||
|
} else { |
||||||
|
triggerProcess(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async queueProcess<Args extends any[], ReturnType>( |
||||||
|
options: WaitingProcessOptions<Args, ReturnType>, |
||||||
|
): Promise<ReturnType> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const lastProcessInQueue = |
||||||
|
this.processQueue[this.processQueue.length - 1]; |
||||||
|
if (lastProcessInQueue) { |
||||||
|
const didModifyLast = lastProcessInQueue |
||||||
|
? options.modifyLastProcess(lastProcessInQueue, options.args) |
||||||
|
: false; |
||||||
|
if (didModifyLast) { |
||||||
|
lastProcessInQueue.awaitingResolutions.push(resolve); |
||||||
|
lastProcessInQueue.awaitingRejections.push(reject); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
const waitingProcess: WaitingProcess<Args, ReturnType> = { |
||||||
|
name: options.name, |
||||||
|
args: options.args, |
||||||
|
perform: options.perform, |
||||||
|
awaitingResolutions: [resolve], |
||||||
|
awaitingRejections: [reject], |
||||||
|
}; |
||||||
|
// HACK: Ugly cast
|
||||||
|
this.processQueue.push( |
||||||
|
waitingProcess as unknown as WaitingProcess<any[], any>, |
||||||
|
); |
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -1,45 +0,0 @@ |
|||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import type { FetchableDocument } from "./FetchableDocument"; |
|
||||||
|
|
||||||
export interface DocumentGetterOptions { |
|
||||||
autoLoad?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export abstract class DocumentStore< |
|
||||||
DocumentType extends FetchableDocument, |
|
||||||
Initializer, |
|
||||||
> { |
|
||||||
protected documentMap: Map<Initializer, DocumentType>; |
|
||||||
protected context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
constructor(context: SolidLdoDatasetContext) { |
|
||||||
this.documentMap = new Map(); |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
get( |
|
||||||
initializerInput: Initializer, |
|
||||||
options?: DocumentGetterOptions, |
|
||||||
): DocumentType { |
|
||||||
const initializer = this.normalizeInitializer(initializerInput); |
|
||||||
const document = this.documentMap.get(initializer); |
|
||||||
if (document) { |
|
||||||
if (options?.autoLoad) { |
|
||||||
document.reload(); |
|
||||||
} |
|
||||||
return document; |
|
||||||
} |
|
||||||
const newDocument = this.create(initializer, options); |
|
||||||
this.documentMap.set(initializer, newDocument); |
|
||||||
return newDocument; |
|
||||||
} |
|
||||||
|
|
||||||
protected abstract create( |
|
||||||
initializer: Initializer, |
|
||||||
options?: DocumentGetterOptions, |
|
||||||
): DocumentType; |
|
||||||
|
|
||||||
protected normalizeInitializer(initializer: Initializer): Initializer { |
|
||||||
return initializer; |
|
||||||
} |
|
||||||
} |
|
@ -1,118 +0,0 @@ |
|||||||
import EventEmitter from "events"; |
|
||||||
import type { DocumentError } from "./errors/DocumentError"; |
|
||||||
import type { DocumentGetterOptions } from "./DocumentStore"; |
|
||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import type TypedEventEmitter from "typed-emitter"; |
|
||||||
|
|
||||||
export type FetchableDocumentEventEmitter = TypedEventEmitter<{ |
|
||||||
stateUpdate: () => void; |
|
||||||
}>; |
|
||||||
|
|
||||||
export abstract class FetchableDocument extends (EventEmitter as new () => FetchableDocumentEventEmitter) { |
|
||||||
protected _isLoading: boolean; |
|
||||||
protected _isWriting: boolean; |
|
||||||
protected _didInitialFetch: boolean; |
|
||||||
protected _error?: DocumentError; |
|
||||||
protected context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
constructor( |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
super(); |
|
||||||
this._isLoading = false; |
|
||||||
this._isWriting = false; |
|
||||||
this._didInitialFetch = false; |
|
||||||
this.context = context; |
|
||||||
// Trigger load if autoload is true
|
|
||||||
if (documentGetterOptions?.autoLoad) { |
|
||||||
this._isLoading = true; |
|
||||||
this.read(); |
|
||||||
} |
|
||||||
} |
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Getters |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
get isLoading() { |
|
||||||
return this._isLoading; |
|
||||||
} |
|
||||||
|
|
||||||
get didInitialFetch() { |
|
||||||
return this._didInitialFetch; |
|
||||||
} |
|
||||||
|
|
||||||
get isLoadingInitial() { |
|
||||||
return this._isLoading && !this._didInitialFetch; |
|
||||||
} |
|
||||||
|
|
||||||
get isReloading() { |
|
||||||
return this._isLoading && this._didInitialFetch; |
|
||||||
} |
|
||||||
|
|
||||||
get error() { |
|
||||||
return this._error; |
|
||||||
} |
|
||||||
|
|
||||||
get isWriting() { |
|
||||||
return this._isWriting; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
async read() { |
|
||||||
this._isLoading = true; |
|
||||||
this.emitStateUpdate(); |
|
||||||
const documentError = await this.fetchDocument(); |
|
||||||
this._isLoading = false; |
|
||||||
this._didInitialFetch = true; |
|
||||||
if (documentError) { |
|
||||||
this.setError(documentError); |
|
||||||
} |
|
||||||
this.emitStateUpdate(); |
|
||||||
} |
|
||||||
|
|
||||||
async reload() { |
|
||||||
return this.read(); |
|
||||||
} |
|
||||||
|
|
||||||
protected abstract fetchDocument(): Promise<DocumentError | undefined>; |
|
||||||
|
|
||||||
protected beginWrite() { |
|
||||||
this._isWriting = true; |
|
||||||
this.emitStateUpdate(); |
|
||||||
} |
|
||||||
|
|
||||||
protected endWrite(error?: DocumentError) { |
|
||||||
if (error) { |
|
||||||
this.setError(error); |
|
||||||
} |
|
||||||
this._isWriting = false; |
|
||||||
this.emitStateUpdate(); |
|
||||||
} |
|
||||||
|
|
||||||
setError(error: DocumentError) { |
|
||||||
this._error = error; |
|
||||||
this.emitStateUpdate(); |
|
||||||
this.context.documentEventEmitter.emit("documentError", error); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Emitter Information |
|
||||||
*/ |
|
||||||
protected emitStateUpdate() { |
|
||||||
this.emit("stateUpdate"); |
|
||||||
} |
|
||||||
|
|
||||||
onStateUpdate(callback: () => void) { |
|
||||||
this.on("stateUpdate", callback); |
|
||||||
} |
|
||||||
|
|
||||||
offStateUpdate(callback: () => void) { |
|
||||||
this.off("stateUpdate", callback); |
|
||||||
} |
|
||||||
} |
|
@ -1,73 +0,0 @@ |
|||||||
import type { AccessModes as IAccessModes } from "@inrupt/solid-client"; |
|
||||||
import { universalAccess } from "@inrupt/solid-client"; |
|
||||||
import { FetchableDocument } from "../FetchableDocument"; |
|
||||||
import type { Resource } from "../resource/Resource"; |
|
||||||
import { DocumentError } from "../errors/DocumentError"; |
|
||||||
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext"; |
|
||||||
import type { DocumentGetterOptions } from "../DocumentStore"; |
|
||||||
|
|
||||||
export type AccessModes = IAccessModes; |
|
||||||
|
|
||||||
export class AccessRules extends FetchableDocument { |
|
||||||
readonly resource: Resource; |
|
||||||
private _publicAccess: IAccessModes | null; |
|
||||||
private _agentAccess: Record<string, IAccessModes> | null; |
|
||||||
|
|
||||||
constructor( |
|
||||||
resource: Resource, |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
super(context, documentGetterOptions); |
|
||||||
this._publicAccess = null; |
|
||||||
this._agentAccess = null; |
|
||||||
this.resource = resource; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Getters |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
get publicAccess() { |
|
||||||
return this._publicAccess; |
|
||||||
} |
|
||||||
|
|
||||||
get agentAccess() { |
|
||||||
return this._agentAccess; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
protected async fetchDocument() { |
|
||||||
try { |
|
||||||
const [publicAccess, agentAccess] = await Promise.all([ |
|
||||||
universalAccess.getPublicAccess(this.resource.uri, { |
|
||||||
fetch: this.context.fetch, |
|
||||||
}), |
|
||||||
universalAccess.getAgentAccessAll(this.resource.uri, { |
|
||||||
fetch: this.context.fetch, |
|
||||||
}), |
|
||||||
]); |
|
||||||
this._publicAccess = publicAccess || { |
|
||||||
read: false, |
|
||||||
write: false, |
|
||||||
append: false, |
|
||||||
controlRead: false, |
|
||||||
controlWrite: false, |
|
||||||
}; |
|
||||||
this._agentAccess = agentAccess || {}; |
|
||||||
return undefined; |
|
||||||
} catch (err: unknown) { |
|
||||||
if (typeof err === "object" && (err as Error).message) { |
|
||||||
this.setError(new DocumentError(this, 500, (err as Error).message)); |
|
||||||
} |
|
||||||
this.setError( |
|
||||||
new DocumentError(this, 500, "Error Fetching Access Rules"), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
import type { DocumentGetterOptions } from "../DocumentStore"; |
|
||||||
import { DocumentStore } from "../DocumentStore"; |
|
||||||
import type { Resource } from "../resource/Resource"; |
|
||||||
import { AccessRules } from "./AccessRules"; |
|
||||||
|
|
||||||
export class AccessRulesStore extends DocumentStore<AccessRules, Resource> { |
|
||||||
protected create( |
|
||||||
initializer: Resource, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
return new AccessRules(initializer, this.context, documentGetterOptions); |
|
||||||
} |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
import type { FetchableDocument } from "../FetchableDocument"; |
|
||||||
|
|
||||||
export class DocumentError extends Error { |
|
||||||
public readonly document: FetchableDocument; |
|
||||||
public readonly status: number; |
|
||||||
|
|
||||||
constructor(document: FetchableDocument, status: number, message: string) { |
|
||||||
super(message); |
|
||||||
this.document = document; |
|
||||||
this.status = status; |
|
||||||
} |
|
||||||
} |
|
@ -1,75 +0,0 @@ |
|||||||
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext"; |
|
||||||
import type { DocumentGetterOptions } from "../DocumentStore"; |
|
||||||
import { FetchableDocument } from "../FetchableDocument"; |
|
||||||
import { DocumentError } from "../errors/DocumentError"; |
|
||||||
import type { ContainerResource } from "./dataResource/containerResource/ContainerResource"; |
|
||||||
|
|
||||||
export abstract class Resource extends FetchableDocument { |
|
||||||
public readonly uri: string; |
|
||||||
|
|
||||||
constructor( |
|
||||||
uri: string, |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
super(context, documentGetterOptions); |
|
||||||
this.uri = uri; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Getters |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
get accessRules() { |
|
||||||
return this.context.accessRulesStore.get(this); |
|
||||||
} |
|
||||||
|
|
||||||
get parentContainer(): ContainerResource | undefined { |
|
||||||
return this.context.containerResourceStore.getContainerForResouce(this); |
|
||||||
} |
|
||||||
|
|
||||||
get ["@id"]() { |
|
||||||
return this.uri; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
async delete() { |
|
||||||
this.beginWrite(); |
|
||||||
const response = await this.context.fetch(this.uri, { |
|
||||||
method: "DELETE", |
|
||||||
}); |
|
||||||
if (response.status >= 200 && response.status < 300) { |
|
||||||
this.endWrite(); |
|
||||||
this.parentContainer?.removeContainedResources(this); |
|
||||||
return; |
|
||||||
} |
|
||||||
this.endWrite( |
|
||||||
new DocumentError(this, response.status, `Could not delete ${this.uri}`), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
async checkExists() { |
|
||||||
const response = await this.context.fetch(this.uri, { |
|
||||||
method: "OPTIONS", |
|
||||||
}); |
|
||||||
return response.status === 404; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Static Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
/** |
|
||||||
* Takes in a URL and will normalize it to the document it's fetching |
|
||||||
*/ |
|
||||||
static normalizeUri(uri: string): string { |
|
||||||
const [strippedHashUri] = uri.split("#"); |
|
||||||
return strippedHashUri; |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
import type { DocumentError } from "../../errors/DocumentError"; |
|
||||||
import { Resource } from "../Resource"; |
|
||||||
|
|
||||||
export class BinaryResource extends Resource { |
|
||||||
fetchDocument(): Promise<DocumentError | undefined> { |
|
||||||
throw new Error("Method not implemented."); |
|
||||||
} |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
import type { DocumentGetterOptions } from "../../DocumentStore"; |
|
||||||
import { DocumentStore } from "../../DocumentStore"; |
|
||||||
import { BinaryResource } from "./BinaryResource"; |
|
||||||
|
|
||||||
export class BinaryResourceStore extends DocumentStore<BinaryResource, string> { |
|
||||||
protected create( |
|
||||||
initializer: string, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
return new BinaryResource(initializer, this.context, documentGetterOptions); |
|
||||||
} |
|
||||||
|
|
||||||
protected normalizeInitializer(initializer: string): string { |
|
||||||
return BinaryResource.normalizeUri(initializer); |
|
||||||
} |
|
||||||
} |
|
@ -1,102 +0,0 @@ |
|||||||
import { parseRdf } from "@ldo/ldo"; |
|
||||||
import { Resource } from "../Resource"; |
|
||||||
import { DocumentError } from "../../errors/DocumentError"; |
|
||||||
import { namedNode, quad as createQuad } from "@rdfjs/data-model"; |
|
||||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
|
||||||
import { changesToSparqlUpdate } from "@ldo/rdf-utils"; |
|
||||||
import type { Quad } from "@rdfjs/types"; |
|
||||||
|
|
||||||
export class DataResource extends Resource { |
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
async create() { |
|
||||||
// TODO
|
|
||||||
} |
|
||||||
|
|
||||||
protected async fetchDocument(): Promise<DocumentError | undefined> { |
|
||||||
// Fetch the document using auth fetch
|
|
||||||
const response = await this.context.fetch(this.uri, { |
|
||||||
headers: { |
|
||||||
accept: "text/turtle", |
|
||||||
}, |
|
||||||
}); |
|
||||||
// Handle Error
|
|
||||||
if (response.status !== 200) { |
|
||||||
// TODO: Handle edge cases
|
|
||||||
return new DocumentError( |
|
||||||
this, |
|
||||||
response.status, |
|
||||||
`Error fetching resource ${this.uri}`, |
|
||||||
); |
|
||||||
} |
|
||||||
// Parse the incoming turtle into a dataset
|
|
||||||
const rawTurtle = await response.text(); |
|
||||||
let loadedDataset; |
|
||||||
try { |
|
||||||
loadedDataset = await parseRdf(rawTurtle, { |
|
||||||
baseIRI: this.uri, |
|
||||||
}); |
|
||||||
} catch (err) { |
|
||||||
if (typeof err === "object" && (err as Error).message) { |
|
||||||
return new DocumentError(this, 500, (err as Error).message); |
|
||||||
} |
|
||||||
return new DocumentError( |
|
||||||
this, |
|
||||||
500, |
|
||||||
"Server returned poorly formatted Turtle", |
|
||||||
); |
|
||||||
} |
|
||||||
// Start transaction
|
|
||||||
const transactionalDataset = |
|
||||||
this.context.solidLdoDataset.startTransaction(); |
|
||||||
const graphNode = namedNode(this.uri); |
|
||||||
// Destroy all triples that were once a part of this resouce
|
|
||||||
loadedDataset.deleteMatches(undefined, undefined, undefined, graphNode); |
|
||||||
// Add the triples from the fetched item
|
|
||||||
loadedDataset.forEach((quad) => { |
|
||||||
transactionalDataset.add( |
|
||||||
createQuad(quad.subject, quad.predicate, quad.object, graphNode), |
|
||||||
); |
|
||||||
}); |
|
||||||
transactionalDataset.commit(); |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
|
|
||||||
async update( |
|
||||||
changes: DatasetChanges<Quad>, |
|
||||||
): Promise<DocumentError | undefined> { |
|
||||||
this.beginWrite(); |
|
||||||
// Convert changes to transactional Dataset
|
|
||||||
const transactionalDataset = |
|
||||||
this.context.solidLdoDataset.startTransaction(); |
|
||||||
changes.added?.forEach((quad) => transactionalDataset.add(quad)); |
|
||||||
changes.removed?.forEach((quad) => transactionalDataset.delete(quad)); |
|
||||||
// Commit data optimistically
|
|
||||||
transactionalDataset.commit(); |
|
||||||
// Make request
|
|
||||||
const sparqlUpdate = await changesToSparqlUpdate(changes); |
|
||||||
const response = await this.context.fetch(this.uri, { |
|
||||||
method: "PATCH", |
|
||||||
body: sparqlUpdate, |
|
||||||
headers: { |
|
||||||
"Content-Type": "application/sparql-update", |
|
||||||
}, |
|
||||||
}); |
|
||||||
if (response.status < 200 || response.status > 299) { |
|
||||||
// Handle Error by rollback
|
|
||||||
transactionalDataset.rollback(); |
|
||||||
this.endWrite( |
|
||||||
new DocumentError( |
|
||||||
this, |
|
||||||
response.status, |
|
||||||
`Problem writing to ${this.uri}`, |
|
||||||
), |
|
||||||
); |
|
||||||
return; |
|
||||||
} |
|
||||||
this.endWrite(); |
|
||||||
} |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
import type { DocumentGetterOptions } from "../../DocumentStore"; |
|
||||||
import { DocumentStore } from "../../DocumentStore"; |
|
||||||
import { DataResource } from "./DataResource"; |
|
||||||
|
|
||||||
export class DataResourceStore extends DocumentStore<DataResource, string> { |
|
||||||
protected create( |
|
||||||
initializer: string, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
return new DataResource(initializer, this.context, documentGetterOptions); |
|
||||||
} |
|
||||||
|
|
||||||
protected normalizeInitializer(initializer: string): string { |
|
||||||
return DataResource.normalizeUri(initializer); |
|
||||||
} |
|
||||||
} |
|
@ -1,80 +0,0 @@ |
|||||||
import { ContainerShapeType } from "../../../../ldo/solid.shapeTypes"; |
|
||||||
import type { Resource } from "../../Resource"; |
|
||||||
import { DataResource } from "../DataResource"; |
|
||||||
|
|
||||||
export class ContainerResource extends DataResource { |
|
||||||
private _contains: Set<Resource> = new Set(); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Getters |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
get contains() { |
|
||||||
return Array.from(this._contains); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* Methods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
protected async fetchDocument() { |
|
||||||
const error = await super.fetchDocument(); |
|
||||||
if (error) { |
|
||||||
return error; |
|
||||||
} |
|
||||||
// Update the contains
|
|
||||||
const container = this.context.solidLdoDataset |
|
||||||
.usingType(ContainerShapeType) |
|
||||||
.fromSubject(this.uri); |
|
||||||
const resourcesToAdd: Resource[] = []; |
|
||||||
container.contains?.forEach((resourceData) => { |
|
||||||
if (resourceData["@id"]) { |
|
||||||
if (resourceData.type?.some((type) => type["@id"] === "Container")) { |
|
||||||
resourcesToAdd.push( |
|
||||||
this.context.containerResourceStore.get(resourceData["@id"]), |
|
||||||
); |
|
||||||
} else { |
|
||||||
if (resourceData["@id"].endsWith(".ttl")) { |
|
||||||
resourcesToAdd.push( |
|
||||||
this.context.dataResourceStore.get(resourceData["@id"]), |
|
||||||
); |
|
||||||
} else { |
|
||||||
resourcesToAdd.push( |
|
||||||
this.context.binaryResourceStore.get(resourceData["@id"]), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
this.addContainedResources(...resourcesToAdd); |
|
||||||
} |
|
||||||
|
|
||||||
public addContainedResources(...resources: Resource[]) { |
|
||||||
let someResourceUpdated = false; |
|
||||||
resources.forEach((resource) => { |
|
||||||
if (!this._contains.has(resource)) { |
|
||||||
someResourceUpdated = true; |
|
||||||
this._contains.add(resource); |
|
||||||
this.parentContainer?.addContainedResources(this); |
|
||||||
} |
|
||||||
}); |
|
||||||
if (someResourceUpdated) { |
|
||||||
this.emitStateUpdate(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public removeContainedResources(...resources: Resource[]) { |
|
||||||
let someResourceUpdated = false; |
|
||||||
resources.forEach((resource) => { |
|
||||||
if (this._contains.has(resource)) { |
|
||||||
someResourceUpdated = true; |
|
||||||
this._contains.delete(resource); |
|
||||||
} |
|
||||||
}); |
|
||||||
if (someResourceUpdated) { |
|
||||||
this.emitStateUpdate(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,49 +0,0 @@ |
|||||||
import type { DocumentGetterOptions } from "../../../DocumentStore"; |
|
||||||
import { DocumentStore } from "../../../DocumentStore"; |
|
||||||
import type { Resource } from "../../Resource"; |
|
||||||
import { ContainerResource } from "./ContainerResource"; |
|
||||||
|
|
||||||
export class ContainerResourceStore extends DocumentStore< |
|
||||||
ContainerResource, |
|
||||||
string |
|
||||||
> { |
|
||||||
protected create( |
|
||||||
initializer: string, |
|
||||||
documentGetterOptions?: DocumentGetterOptions, |
|
||||||
) { |
|
||||||
return new ContainerResource( |
|
||||||
initializer, |
|
||||||
this.context, |
|
||||||
documentGetterOptions, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
protected normalizeInitializer(initializer: string) { |
|
||||||
return ContainerResource.normalizeUri(initializer); |
|
||||||
} |
|
||||||
|
|
||||||
getContainerForResouce(resource: Resource) { |
|
||||||
const parentUri = ContainerResourceStore.getParentUri(resource.uri); |
|
||||||
return parentUri ? this.get(parentUri) : undefined; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns the parent container URI |
|
||||||
*/ |
|
||||||
static getParentUri(uri: string) { |
|
||||||
const urlObject = new URL(uri); |
|
||||||
const pathItems = urlObject.pathname.split("/"); |
|
||||||
if ( |
|
||||||
pathItems.length < 2 || |
|
||||||
(pathItems.length === 2 && pathItems[1].length === 0) |
|
||||||
) { |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
if (pathItems[pathItems.length - 1] === "") { |
|
||||||
pathItems.pop(); |
|
||||||
} |
|
||||||
pathItems.pop(); |
|
||||||
urlObject.pathname = `${pathItems.join("/")}/`; |
|
||||||
return urlObject.toString(); |
|
||||||
} |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
import { Mixin } from "ts-mixer"; |
|
||||||
|
|
||||||
class Foo { |
|
||||||
protected makeFoo() { |
|
||||||
return "foo"; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
class Bar { |
|
||||||
protected makeBar() { |
|
||||||
return "bar"; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
class FooBar extends Mixin(Foo, Bar) { |
|
||||||
public makeFooBar() { |
|
||||||
return this.makeFoo() + this.makeBar(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const fooBar = new FooBar(); |
|
@ -0,0 +1,78 @@ |
|||||||
|
import type { WaitingProcess } from "../src/LeafRequestBatcher"; |
||||||
|
import { RequestBatcher } from "../src/RequestBatcher"; |
||||||
|
|
||||||
|
describe("RequestBatcher", () => { |
||||||
|
type ReadWaitingProcess = WaitingProcess<[string], string>; |
||||||
|
|
||||||
|
it("Batches a request", async () => { |
||||||
|
const requestBatcher = new RequestBatcher({ batchMillis: 1000 }); |
||||||
|
const perform = async (input: string): Promise<string> => `Hello ${input}`; |
||||||
|
const perform1 = jest.fn(perform); |
||||||
|
const perform2 = jest.fn(perform); |
||||||
|
const perform3 = jest.fn(perform); |
||||||
|
const perform4 = jest.fn(perform); |
||||||
|
|
||||||
|
const modifyLastProcess = (last, input: [string]) => { |
||||||
|
if (last.name === "read") { |
||||||
|
(last as ReadWaitingProcess).args[0] += input; |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
}; |
||||||
|
|
||||||
|
let return1: string = ""; |
||||||
|
let return2: string = ""; |
||||||
|
let return3: string = ""; |
||||||
|
let return4: string = ""; |
||||||
|
|
||||||
|
await Promise.all([ |
||||||
|
requestBatcher |
||||||
|
.queueProcess<[string], string>({ |
||||||
|
name: "read", |
||||||
|
args: ["a"], |
||||||
|
perform: perform1, |
||||||
|
modifyLastProcess, |
||||||
|
}) |
||||||
|
.then((val) => (return1 = val)), |
||||||
|
requestBatcher |
||||||
|
.queueProcess<[string], string>({ |
||||||
|
name: "read", |
||||||
|
args: ["b"], |
||||||
|
perform: perform2, |
||||||
|
modifyLastProcess, |
||||||
|
}) |
||||||
|
.then((val) => (return2 = val)), |
||||||
|
, |
||||||
|
requestBatcher |
||||||
|
.queueProcess<[string], string>({ |
||||||
|
name: "read", |
||||||
|
args: ["c"], |
||||||
|
perform: perform3, |
||||||
|
modifyLastProcess, |
||||||
|
}) |
||||||
|
.then((val) => (return3 = val)), |
||||||
|
, |
||||||
|
requestBatcher |
||||||
|
.queueProcess<[string], string>({ |
||||||
|
name: "read", |
||||||
|
args: ["d"], |
||||||
|
perform: perform4, |
||||||
|
modifyLastProcess, |
||||||
|
}) |
||||||
|
.then((val) => (return4 = val)), |
||||||
|
, |
||||||
|
]); |
||||||
|
|
||||||
|
expect(return1).toBe("Hello a"); |
||||||
|
expect(return2).toBe("Hello bcd"); |
||||||
|
expect(return3).toBe("Hello bcd"); |
||||||
|
expect(return4).toBe("Hello bcd"); |
||||||
|
|
||||||
|
expect(perform1).toHaveBeenCalledTimes(1); |
||||||
|
expect(perform1).toHaveBeenCalledWith("a"); |
||||||
|
expect(perform2).toHaveBeenCalledTimes(1); |
||||||
|
expect(perform2).toHaveBeenCalledWith("bcd"); |
||||||
|
expect(perform3).toHaveBeenCalledTimes(0); |
||||||
|
expect(perform4).toHaveBeenCalledTimes(0); |
||||||
|
}); |
||||||
|
}); |
@ -1,5 +0,0 @@ |
|||||||
describe("Trivial", () => { |
|
||||||
it("Trivial", () => { |
|
||||||
expect(true).toBe(true); |
|
||||||
}); |
|
||||||
}); |
|
Loading…
Reference in new issue