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