Get for leaf is complete

main
jaxoncreed 2 years ago
parent 21d631c406
commit fa141fed5d
  1. 60474
      package-lock.json
  2. 5
      packages/solid/package.json
  3. 0
      packages/solid/src/.ldo/solid.context.ts
  4. 0
      packages/solid/src/.ldo/solid.schema.ts
  5. 0
      packages/solid/src/.ldo/solid.shapeTypes.ts
  6. 0
      packages/solid/src/.ldo/solid.typings.ts
  7. 0
      packages/solid/src/.shapes/solid.shex
  8. 158
      packages/solid/src/LeafRequestBatcher.ts
  9. 9
      packages/solid/src/ProcessManager.ts
  10. 14
      packages/solid/src/SolidLdoDatasetContext.ts
  11. 125
      packages/solid/src/requester/LeafRequester.ts
  12. 9
      packages/solid/src/requester/requesterResults/AbsentResult.ts
  13. 16
      packages/solid/src/requester/requesterResults/BinaryResult.ts
  14. 15
      packages/solid/src/requester/requesterResults/DataResult.ts
  15. 33
      packages/solid/src/requester/requesterResults/ErrorResult.ts
  16. 50
      packages/solid/src/requester/requesterResults/HttpErrorResult.ts
  17. 7
      packages/solid/src/requester/requesterResults/RequesterResult.ts
  18. 32
      packages/solid/src/util/RequestBatcher.ts
  19. 48
      packages/solid/test/LeafRequester.test.ts
  20. 15
      packages/solid/test/RequestBatcher.test.ts
  21. 106
      packages/solid/test/solidServer.helper.ts

60474
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -10,7 +10,7 @@
"test": "jest --coverage",
"test:watch": "jest --watch",
"prepublishOnly": "npm run test && npm run build",
"build:ldo": "ldo build --input src/shapes --output src/ldo",
"build:ldo": "ldo build --input src/.shapes --output src/.ldo",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern"
},
"repository": {
@ -24,10 +24,13 @@
},
"homepage": "https://github.com/o-development/devtool-boilerplate#readme",
"devDependencies": {
"@inrupt/solid-client-authn-core": "^1.17.1",
"@ldo/cli": "^0.0.0",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid/community-server": "^6.0.2",
"@types/jest": "^29.0.3",
"jest-rdf": "^1.8.0",
"ts-jest": "^29.0.2",
"ts-node": "^10.9.1",
"typed-emitter": "^2.1.0"

@ -1,158 +0,0 @@
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>;
}

@ -1,9 +0,0 @@
interface loadingStatus;
export class LoadingManager {
private loadingStatus: Record<string, Array<[key: string, promise: Promise<unknown>]>>
public registerProcess(): void {
}
}

@ -1,15 +1,15 @@
import type TypedEmitter from "typed-emitter";
// import type TypedEmitter from "typed-emitter";
import type { SolidLdoDataset } from "./SolidLdoDataset";
import type { DocumentError } from "./document/errors/DocumentError";
// import type { DocumentError } from "./document/errors/DocumentError";
export type OnDocumentErrorCallback = (error: DocumentError) => void;
// export type OnDocumentErrorCallback = (error: DocumentError) => void;
export type DocumentEventEmitter = TypedEmitter<{
documentError: OnDocumentErrorCallback;
}>;
// export type DocumentEventEmitter = TypedEmitter<{
// documentError: OnDocumentErrorCallback;
// }>;
export interface SolidLdoDatasetContext {
solidLdoDataset: SolidLdoDataset;
documentEventEmitter: DocumentEventEmitter;
// documentEventEmitter: DocumentEventEmitter;
fetch: typeof fetch;
}

@ -0,0 +1,125 @@
import type { LeafUri } from "../uriTypes";
import { RequestBatcher } from "../util/RequestBatcher";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import { AbsentResult } from "./requesterResults/AbsentResult";
import {
DataResult,
TurtleFormattingError,
} from "./requesterResults/DataResult";
import { BinaryResult } from "./requesterResults/BinaryResult";
import {
HttpErrorResult,
ServerHttpError,
UnauthenticatedHttpError,
UnexpectedHttpError,
} from "./requesterResults/HttpErrorResult";
import { UnexpectedError } from "./requesterResults/ErrorResult";
import { parseRdf } from "@ldo/ldo";
import { namedNode } from "@rdfjs/data-model";
export type ReadResult =
| AbsentResult
| DataResult
| BinaryResult
| ServerHttpError
| UnauthenticatedHttpError
| UnexpectedHttpError
| UnexpectedError
| TurtleFormattingError;
export class LeafRequester {
private requestBatcher = new RequestBatcher();
// All intance variables
readonly uri: LeafUri;
private context: SolidLdoDatasetContext;
constructor(uri: LeafUri, context: SolidLdoDatasetContext) {
this.uri = uri;
this.context = context;
}
// Read Methods
read(): Promise<ReadResult> {
const READ_KEY = "read";
return this.requestBatcher.queueProcess({
name: READ_KEY,
args: [],
perform: this.performRead.bind(this),
modifyQueue: (queue, isLoading) => {
if (queue.length === 0) {
return isLoading[READ_KEY];
} else {
return queue[queue.length - 1].name === READ_KEY;
}
},
});
}
private async performRead(): Promise<ReadResult> {
try {
// Fetch options to determine the document type
const response = await this.context.fetch(this.uri);
if (AbsentResult.is(response)) {
return new AbsentResult(this.uri);
}
if (ServerHttpError.is(response)) {
return new ServerHttpError(this.uri, response);
}
if (UnauthenticatedHttpError.is(response)) {
return new UnauthenticatedHttpError(this.uri, response);
}
if (HttpErrorResult.isnt(response)) {
return new UnexpectedHttpError(this.uri, response);
}
if (DataResult.is(response)) {
// Parse Turtle
const rawTurtle = await response.text();
let loadedDataset;
try {
loadedDataset = await parseRdf(rawTurtle, {
baseIRI: this.uri,
});
} catch (err) {
return new TurtleFormattingError(
this.uri,
err instanceof Error ? err.message : "Failed to parse rdf",
);
}
// 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
transactionalDataset.addAll(loadedDataset);
transactionalDataset.commit();
return new DataResult(this.uri);
} else {
// Load Blob
const blob = await response.blob();
return new BinaryResult(this.uri, blob);
}
} catch (err) {
return UnexpectedError.fromThrown(this.uri, err);
}
}
// // Create Methods
// abstract createDataResource(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,9 @@
import { RequesterResult } from "./RequesterResult";
export class AbsentResult extends RequesterResult {
type = "absent" as const;
static is(response: Response): boolean {
return response.status === 404;
}
}

@ -0,0 +1,16 @@
import { RequesterResult } from "./RequesterResult";
export class BinaryResult extends RequesterResult {
type = "binary" as const;
readonly blob: Blob;
constructor(uri: string, blob: Blob) {
super(uri);
this.blob = blob;
}
static is(response: Response): boolean {
const contentType = response.headers.get("content-type");
return !contentType || contentType !== "text/turtle";
}
}

@ -0,0 +1,15 @@
import { ErrorResult } from "./ErrorResult";
import { RequesterResult } from "./RequesterResult";
export class DataResult extends RequesterResult {
type = "data" as const;
static is(response: Response): boolean {
const contentType = response.headers.get("content-type");
return !!contentType && contentType === "text/turtle";
}
}
export class TurtleFormattingError extends ErrorResult {
errorType = "turtleFormatting";
}

@ -0,0 +1,33 @@
export abstract class ErrorResult extends Error {
readonly type = "error" as const;
readonly uri: string;
abstract readonly errorType: string;
constructor(uri: string, message?: string) {
super(message || "An error unkown error was encountered during a request.");
this.uri = uri;
}
}
export class UnexpectedError extends ErrorResult {
error: Error;
readonly errorType = "unexpected";
constructor(uri: string, error: Error) {
super(uri, error.message);
this.error = error;
}
static fromThrown(uri: string, err: unknown) {
if (err instanceof Error) {
return new UnexpectedError(uri, err);
} else if (typeof err === "string") {
return new UnexpectedError(uri, new Error(err));
} else {
return new UnexpectedError(
uri,
new Error(`Error of type ${typeof err} thrown: ${err}`),
);
}
}
}

@ -0,0 +1,50 @@
import { ErrorResult } from "./ErrorResult";
export abstract class HttpErrorResult extends ErrorResult {
public readonly status: number;
public readonly headers: Headers;
public readonly response: Response;
constructor(uri: string, response: Response, message?: string) {
super(
uri,
message ||
`Request for ${uri} returned ${response.status} (${response.statusText}).`,
);
this.status = response.status;
this.headers = response.headers;
this.response = response;
}
async getBodyForDebug(): Promise<string> {
if (this.response.bodyUsed) {
return `Could not get body for ${this.uri} that yeilded status ${this.status}. The body stream has already been consumed.`;
}
return await this.response.text();
}
static isnt(response: Response) {
return response.status < 200 || response.status >= 300;
}
}
export class UnexpectedHttpError extends HttpErrorResult {
errorType = "unexpectedHttp" as const;
}
export class UnauthenticatedHttpError extends HttpErrorResult {
status: 401;
errorType = "unauthenticated" as const;
static is(response: Response) {
return response.status === 404;
}
}
export class ServerHttpError extends HttpErrorResult {
errorType = "server" as const;
static is(response: Response) {
return response.status >= 500 && response.status < 600;
}
}

@ -0,0 +1,7 @@
export abstract class RequesterResult {
readonly uri: string;
abstract readonly type: string;
constructor(uri: string) {
this.uri = uri;
}
}

@ -11,14 +11,23 @@ export interface WaitingProcessOptions<Args extends any[], Return> {
name: string;
args: Args;
perform: (...args: Args) => Promise<Return>;
modifyLastProcess: (
lastProcess: WaitingProcess<any[], any>,
/**
*
* @param processQueue The current process queue
* @param isLoading The current is loading
* @param args provided args
* @returns true if the process queue has been modified and a new process should not be added to the queue
*/
modifyQueue: (
processQueue: WaitingProcess<any[], any>[],
isLoading: Record<string, boolean>,
args: Args,
) => boolean;
}
export class RequestBatcher {
private lastRequestTimestampMap: Record<string, number> = {};
private isLoading: Record<string, boolean> = {};
private isWaiting: boolean = false;
private processQueue: WaitingProcess<any[], any>[] = [];
public shouldBatchAllRequests: boolean;
@ -68,7 +77,21 @@ export class RequestBatcher {
callback(err);
});
}
this.triggerOrWaitProcess();
// Reset loading
if (
!this.processQueue.some(
(process) => process.name === processToTrigger.name,
)
) {
this.isLoading[processToTrigger.name] = false;
}
if (this.processQueue.length > 0) {
this.triggerOrWaitProcess();
} else {
this.isLoading["any"] = false;
}
}
};
@ -88,7 +111,7 @@ export class RequestBatcher {
this.processQueue[this.processQueue.length - 1];
if (lastProcessInQueue) {
const didModifyLast = lastProcessInQueue
? options.modifyLastProcess(lastProcessInQueue, options.args)
? options.modifyQueue(this.processQueue, this.isLoading, options.args)
: false;
if (didModifyLast) {
lastProcessInQueue.awaitingResolutions.push(resolve);
@ -107,6 +130,7 @@ export class RequestBatcher {
this.processQueue.push(
waitingProcess as unknown as WaitingProcess<any[], any>,
);
this.isLoading[waitingProcess.name] = true;
this.triggerOrWaitProcess();
});
}

@ -0,0 +1,48 @@
import type { App } from "@solid/community-server";
import { LeafRequester } from "../src/requester/LeafRequester";
import crossFetch from "cross-fetch";
import {
createApp,
getSecret,
refreshToken,
type ISecretData,
type ITokenData,
getAuthenticatedFetch,
} from "./solidServer.helper";
import { buildAuthenticatedFetch } from "@inrupt/solid-client-authn-core";
describe("Leaf Requester", () => {
let app: App;
let authFetch: typeof fetch;
beforeAll(async () => {
// Start up the server
// app = await createApp();
// await app.start();
authFetch = await getAuthenticatedFetch();
});
it("special request", async () => {
const response = await authFetch(
"https://solidweb.me/jackson/everything_public/anonexistentfile.json",
{
method: "PUT",
headers: { "content-type": "application/json+ld" },
body: JSON.stringify({ some: "test" }),
},
);
console.log("STATUS:", response.status);
console.log("HEADERS:", response.headers);
console.log("BODY:", await response.text());
});
it("reads", async () => {
const leafRequester = new LeafRequester(
"https://solidweb.me/jackson/everything-public/someotherfile.json",
{ fetch: authFetch },
);
await leafRequester.read();
});
});

@ -1,5 +1,5 @@
import type { WaitingProcess } from "../src/LeafRequestBatcher";
import { RequestBatcher } from "../src/RequestBatcher";
import type { WaitingProcess } from "../src/util/RequestBatcher";
import { RequestBatcher } from "../src/util/RequestBatcher";
describe("RequestBatcher", () => {
type ReadWaitingProcess = WaitingProcess<[string], string>;
@ -12,7 +12,8 @@ describe("RequestBatcher", () => {
const perform3 = jest.fn(perform);
const perform4 = jest.fn(perform);
const modifyLastProcess = (last, input: [string]) => {
const modifyQueue = (queue, isLoading, input: [string]) => {
const last = queue[queue.length - 1];
if (last.name === "read") {
(last as ReadWaitingProcess).args[0] += input;
return true;
@ -31,7 +32,7 @@ describe("RequestBatcher", () => {
name: "read",
args: ["a"],
perform: perform1,
modifyLastProcess,
modifyQueue,
})
.then((val) => (return1 = val)),
requestBatcher
@ -39,7 +40,7 @@ describe("RequestBatcher", () => {
name: "read",
args: ["b"],
perform: perform2,
modifyLastProcess,
modifyQueue,
})
.then((val) => (return2 = val)),
,
@ -48,7 +49,7 @@ describe("RequestBatcher", () => {
name: "read",
args: ["c"],
perform: perform3,
modifyLastProcess,
modifyQueue,
})
.then((val) => (return3 = val)),
,
@ -57,7 +58,7 @@ describe("RequestBatcher", () => {
name: "read",
args: ["d"],
perform: perform4,
modifyLastProcess,
modifyQueue,
})
.then((val) => (return4 = val)),
,

@ -0,0 +1,106 @@
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
import * as path from "path";
import type { KeyPair } from "@inrupt/solid-client-authn-core";
import {
buildAuthenticatedFetch,
createDpopHeader,
generateDpopKeyPair,
} from "@inrupt/solid-client-authn-core";
import { AppRunner, resolveModulePath } from "@solid/community-server";
import "jest-rdf";
import fetch from "cross-fetch";
const config = [
{
},
];
const SERVER_DOMAIN = "https://solidweb.me";
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
export function createApp() {
return new AppRunner().create(
{
mainModulePath: resolveModulePath(""),
typeChecking: false,
},
resolveModulePath("config/default.json"),
{},
{
port: 3_001,
loggingLevel: "off",
seededPodConfigJson: path.join(
__dirname,
"configs",
"solid-css-seed.json",
),
},
);
}
export interface ISecretData {
id: string;
secret: string;
}
// From https://communitysolidserver.github.io/CommunitySolidServer/5.x/usage/client-credentials/
export async function getSecret(): Promise<ISecretData> {
const result = await fetch(`${SERVER_DOMAIN}/idp/credentials/`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
email: config[0].email,
password: config[0].password,
name: config[0].podName,
}),
});
const json = await result.json();
return json;
}
export interface ITokenData {
accessToken: string;
dpopKey: KeyPair;
}
// From https://communitysolidserver.github.io/CommunitySolidServer/5.x/usage/client-credentials/
export async function refreshToken({
id,
secret,
}: ISecretData): Promise<ITokenData> {
const dpopKey = await generateDpopKeyPair();
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`;
const tokenUrl = `${SERVER_DOMAIN}/.oidc/token`;
const accessToken = await fetch(tokenUrl, {
method: "POST",
headers: {
// The header needs to be in base64 encoding.
authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
"content-type": "application/x-www-form-urlencoded",
dpop: await createDpopHeader(tokenUrl, "POST", dpopKey),
},
body: "grant_type=client_credentials&scope=webid",
})
.then((res) => res.json())
.then((res) => res.access_token);
return { accessToken, dpopKey };
}
export async function getAuthenticatedFetch() {
// Generate secret
const secret = await getSecret();
// Get token
const token = await refreshToken(secret);
// Build authenticated fetch
const authFetch = await buildAuthenticatedFetch(fetch, token.accessToken, {
dpopKey: token.dpopKey,
});
return authFetch;
}
Loading…
Cancel
Save