Completed Batch Handler

main
jaxoncreed 2 years ago
parent 0fa45f82de
commit 21d631c406
  1. 158
      packages/solid/src/LeafRequestBatcher.ts
  2. 113
      packages/solid/src/RequestBatcher.ts
  3. 45
      packages/solid/src/document/DocumentStore.ts
  4. 118
      packages/solid/src/document/FetchableDocument.ts
  5. 73
      packages/solid/src/document/accessRules/AccessRules.ts
  6. 13
      packages/solid/src/document/accessRules/AccessRulesStore.ts
  7. 12
      packages/solid/src/document/errors/DocumentError.ts
  8. 75
      packages/solid/src/document/resource/Resource.ts
  9. 8
      packages/solid/src/document/resource/binaryResource/BinaryResource.ts
  10. 16
      packages/solid/src/document/resource/binaryResource/BinaryResourceStore.ts
  11. 102
      packages/solid/src/document/resource/dataResource/DataResource.ts
  12. 16
      packages/solid/src/document/resource/dataResource/DataResourceStore.ts
  13. 80
      packages/solid/src/document/resource/dataResource/containerResource/ContainerResource.ts
  14. 49
      packages/solid/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts
  15. 21
      packages/solid/src/test.ts
  16. 78
      packages/solid/test/RequestBatcher.test.ts
  17. 5
      packages/solid/test/trivial.test.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<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…
Cancel
Save