From 21d631c40613294b9f85fdd3f04fcd7b901e7a49 Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Thu, 7 Sep 2023 23:03:12 -0400 Subject: [PATCH] Completed Batch Handler --- packages/solid/src/LeafRequestBatcher.ts | 158 ++++++++++++++++++ packages/solid/src/RequestBatcher.ts | 113 +++++++++++++ packages/solid/src/document/DocumentStore.ts | 45 ----- .../solid/src/document/FetchableDocument.ts | 118 ------------- .../src/document/accessRules/AccessRules.ts | 73 -------- .../document/accessRules/AccessRulesStore.ts | 13 -- .../src/document/errors/DocumentError.ts | 12 -- .../solid/src/document/resource/Resource.ts | 75 --------- .../resource/binaryResource/BinaryResource.ts | 8 - .../binaryResource/BinaryResourceStore.ts | 16 -- .../resource/dataResource/DataResource.ts | 102 ----------- .../dataResource/DataResourceStore.ts | 16 -- .../containerResource/ContainerResource.ts | 80 --------- .../ContainerResourceStore.ts | 49 ------ packages/solid/src/test.ts | 21 --- packages/solid/test/RequestBatcher.test.ts | 78 +++++++++ packages/solid/test/trivial.test.ts | 5 - 17 files changed, 349 insertions(+), 633 deletions(-) create mode 100644 packages/solid/src/LeafRequestBatcher.ts create mode 100644 packages/solid/src/RequestBatcher.ts delete mode 100644 packages/solid/src/document/DocumentStore.ts delete mode 100644 packages/solid/src/document/FetchableDocument.ts delete mode 100644 packages/solid/src/document/accessRules/AccessRules.ts delete mode 100644 packages/solid/src/document/accessRules/AccessRulesStore.ts delete mode 100644 packages/solid/src/document/errors/DocumentError.ts delete mode 100644 packages/solid/src/document/resource/Resource.ts delete mode 100644 packages/solid/src/document/resource/binaryResource/BinaryResource.ts delete mode 100644 packages/solid/src/document/resource/binaryResource/BinaryResourceStore.ts delete mode 100644 packages/solid/src/document/resource/dataResource/DataResource.ts delete mode 100644 packages/solid/src/document/resource/dataResource/DataResourceStore.ts delete mode 100644 packages/solid/src/document/resource/dataResource/containerResource/ContainerResource.ts delete mode 100644 packages/solid/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts delete mode 100644 packages/solid/src/test.ts create mode 100644 packages/solid/test/RequestBatcher.test.ts delete mode 100644 packages/solid/test/trivial.test.ts diff --git a/packages/solid/src/LeafRequestBatcher.ts b/packages/solid/src/LeafRequestBatcher.ts new file mode 100644 index 0000000..3dd142e --- /dev/null +++ b/packages/solid/src/LeafRequestBatcher.ts @@ -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 { + name: string; + args: Args; + perform: (...args: Args) => Promise; + awaitingResolutions: ((returnValue: Return) => void)[]; + awaitingRejections: ((err: unknown) => void)[]; +} + +export interface WaitingProcessOptions { + name: string; + args: Args; + perform: (...args: Args) => Promise; + modifyLastProcess: ( + lastProcess: WaitingProcess, + args: Args, + ) => boolean; +} + +export abstract class LeafRequestBatcher { + private lastRequestTimestampMap: Record = {}; + private isWaiting: boolean = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private processQueue: WaitingProcess[] = []; + 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( + options: WaitingProcessOptions, + ): Promise { + 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 { + const READ_KEY = "read"; + return this.queueProcess({ + name: READ_KEY, + args: [], + perform: this.performRead, + modifyLastProcess: (last) => { + return last.name === READ_KEY; + }, + }); + } + + private performRead(): Promise { + console.log("Reading"); + throw new Error("Doing Read"); + } + + // Create Methods + abstract createData(overwrite?: boolean): Promise; + + abstract upload( + blob: Blob, + mimeType: string, + overwrite?: boolean, + ): Promise; + + abstract updateData( + changes: DatasetChanges, + ): Promise; + + abstract delete(): Promise; +} diff --git a/packages/solid/src/RequestBatcher.ts b/packages/solid/src/RequestBatcher.ts new file mode 100644 index 0000000..c3d8171 --- /dev/null +++ b/packages/solid/src/RequestBatcher.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface WaitingProcess { + name: string; + args: Args; + perform: (...args: Args) => Promise; + awaitingResolutions: ((returnValue: Return) => void)[]; + awaitingRejections: ((err: any) => void)[]; +} + +export interface WaitingProcessOptions { + name: string; + args: Args; + perform: (...args: Args) => Promise; + modifyLastProcess: ( + lastProcess: WaitingProcess, + args: Args, + ) => boolean; +} + +export class RequestBatcher { + private lastRequestTimestampMap: Record = {}; + private isWaiting: boolean = false; + private processQueue: WaitingProcess[] = []; + 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( + options: WaitingProcessOptions, + ): Promise { + 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 = { + name: options.name, + args: options.args, + perform: options.perform, + awaitingResolutions: [resolve], + awaitingRejections: [reject], + }; + // HACK: Ugly cast + this.processQueue.push( + waitingProcess as unknown as WaitingProcess, + ); + this.triggerOrWaitProcess(); + }); + } +} diff --git a/packages/solid/src/document/DocumentStore.ts b/packages/solid/src/document/DocumentStore.ts deleted file mode 100644 index bace8f3..0000000 --- a/packages/solid/src/document/DocumentStore.ts +++ /dev/null @@ -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; - 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; - } -} diff --git a/packages/solid/src/document/FetchableDocument.ts b/packages/solid/src/document/FetchableDocument.ts deleted file mode 100644 index 036a503..0000000 --- a/packages/solid/src/document/FetchableDocument.ts +++ /dev/null @@ -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; - - 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); - } -} diff --git a/packages/solid/src/document/accessRules/AccessRules.ts b/packages/solid/src/document/accessRules/AccessRules.ts deleted file mode 100644 index ad486a2..0000000 --- a/packages/solid/src/document/accessRules/AccessRules.ts +++ /dev/null @@ -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 | 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"), - ); - } - } -} diff --git a/packages/solid/src/document/accessRules/AccessRulesStore.ts b/packages/solid/src/document/accessRules/AccessRulesStore.ts deleted file mode 100644 index c184e4a..0000000 --- a/packages/solid/src/document/accessRules/AccessRulesStore.ts +++ /dev/null @@ -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 { - protected create( - initializer: Resource, - documentGetterOptions?: DocumentGetterOptions, - ) { - return new AccessRules(initializer, this.context, documentGetterOptions); - } -} diff --git a/packages/solid/src/document/errors/DocumentError.ts b/packages/solid/src/document/errors/DocumentError.ts deleted file mode 100644 index 8114ef2..0000000 --- a/packages/solid/src/document/errors/DocumentError.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/solid/src/document/resource/Resource.ts b/packages/solid/src/document/resource/Resource.ts deleted file mode 100644 index 3f25287..0000000 --- a/packages/solid/src/document/resource/Resource.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/solid/src/document/resource/binaryResource/BinaryResource.ts b/packages/solid/src/document/resource/binaryResource/BinaryResource.ts deleted file mode 100644 index e23b812..0000000 --- a/packages/solid/src/document/resource/binaryResource/BinaryResource.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DocumentError } from "../../errors/DocumentError"; -import { Resource } from "../Resource"; - -export class BinaryResource extends Resource { - fetchDocument(): Promise { - throw new Error("Method not implemented."); - } -} diff --git a/packages/solid/src/document/resource/binaryResource/BinaryResourceStore.ts b/packages/solid/src/document/resource/binaryResource/BinaryResourceStore.ts deleted file mode 100644 index 0321a2d..0000000 --- a/packages/solid/src/document/resource/binaryResource/BinaryResourceStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DocumentGetterOptions } from "../../DocumentStore"; -import { DocumentStore } from "../../DocumentStore"; -import { BinaryResource } from "./BinaryResource"; - -export class BinaryResourceStore extends DocumentStore { - protected create( - initializer: string, - documentGetterOptions?: DocumentGetterOptions, - ) { - return new BinaryResource(initializer, this.context, documentGetterOptions); - } - - protected normalizeInitializer(initializer: string): string { - return BinaryResource.normalizeUri(initializer); - } -} diff --git a/packages/solid/src/document/resource/dataResource/DataResource.ts b/packages/solid/src/document/resource/dataResource/DataResource.ts deleted file mode 100644 index aa9f7e3..0000000 --- a/packages/solid/src/document/resource/dataResource/DataResource.ts +++ /dev/null @@ -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 { - // 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, - ): Promise { - 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(); - } -} diff --git a/packages/solid/src/document/resource/dataResource/DataResourceStore.ts b/packages/solid/src/document/resource/dataResource/DataResourceStore.ts deleted file mode 100644 index fb30450..0000000 --- a/packages/solid/src/document/resource/dataResource/DataResourceStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DocumentGetterOptions } from "../../DocumentStore"; -import { DocumentStore } from "../../DocumentStore"; -import { DataResource } from "./DataResource"; - -export class DataResourceStore extends DocumentStore { - protected create( - initializer: string, - documentGetterOptions?: DocumentGetterOptions, - ) { - return new DataResource(initializer, this.context, documentGetterOptions); - } - - protected normalizeInitializer(initializer: string): string { - return DataResource.normalizeUri(initializer); - } -} diff --git a/packages/solid/src/document/resource/dataResource/containerResource/ContainerResource.ts b/packages/solid/src/document/resource/dataResource/containerResource/ContainerResource.ts deleted file mode 100644 index 9b285a6..0000000 --- a/packages/solid/src/document/resource/dataResource/containerResource/ContainerResource.ts +++ /dev/null @@ -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 = 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(); - } - } -} diff --git a/packages/solid/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts b/packages/solid/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts deleted file mode 100644 index 3ec158f..0000000 --- a/packages/solid/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts +++ /dev/null @@ -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(); - } -} diff --git a/packages/solid/src/test.ts b/packages/solid/src/test.ts deleted file mode 100644 index e05b58f..0000000 --- a/packages/solid/src/test.ts +++ /dev/null @@ -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(); diff --git a/packages/solid/test/RequestBatcher.test.ts b/packages/solid/test/RequestBatcher.test.ts new file mode 100644 index 0000000..a2a7de0 --- /dev/null +++ b/packages/solid/test/RequestBatcher.test.ts @@ -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 => `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); + }); +}); diff --git a/packages/solid/test/trivial.test.ts b/packages/solid/test/trivial.test.ts deleted file mode 100644 index 53b8992..0000000 --- a/packages/solid/test/trivial.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe("Trivial", () => { - it("Trivial", () => { - expect(true).toBe(true); - }); -});