Defined Interface for Connected

main
Jackson Morgan 6 months ago
parent 3c571a6098
commit f11fcbda49
  1. 279
      package-lock.json
  2. 2
      packages/connected-nextgraph/package.json
  3. 2
      packages/connected-nextgraph/src/index.ts
  4. 75
      packages/connected-nextgraph/src/resources/NextGraphResource.ts
  5. 0
      packages/connected-nextgraph/src/util/isNextGraphUri.ts
  6. 3
      packages/connected-solid/src/test.ts
  7. 67
      packages/connected/src/ConnectedLdoDataset.ts
  8. 3
      packages/connected/src/ConnectedPlugin.ts
  9. 64
      packages/connected/src/InvalidIdentifierResource.ts
  10. 29
      packages/connected/src/Resource.ts
  11. 12
      packages/connected/src/index.ts
  12. 10
      packages/connected/src/notifications/NotificationMessage.ts
  13. 144
      packages/connected/src/notifications/NotificationSubscription.ts
  14. 30
      packages/connected/src/notifications/results/NotificationErrors.ts
  15. 7
      packages/connected/src/results/ConnectedResult.ts
  16. 7
      packages/connected/src/results/ResourceResult.ts
  17. 139
      packages/connected/src/results/error/ErrorResult.ts
  18. 16
      packages/connected/src/results/error/InvalidUriError.ts
  19. 19
      packages/connected/src/results/success/CheckRootContainerSuccess.ts
  20. 13
      packages/connected/src/results/success/CreateSuccess.ts
  21. 14
      packages/connected/src/results/success/DeleteSuccess.ts
  22. 71
      packages/connected/src/results/success/ReadSuccess.ts
  23. 51
      packages/connected/src/results/success/SuccessResult.ts
  24. 11
      packages/connected/src/results/success/Unfetched.ts
  25. 24
      packages/connected/src/results/success/UpdateSuccess.ts

279
package-lock.json generated

@ -4604,6 +4604,18 @@
"resolved": "packages/cli",
"link": true
},
"node_modules/@ldo/connected": {
"resolved": "packages/connected",
"link": true
},
"node_modules/@ldo/connected-nextgraph": {
"resolved": "packages/connected-nextgraph",
"link": true
},
"node_modules/@ldo/connected-solid": {
"resolved": "packages/connected-solid",
"link": true
},
"node_modules/@ldo/dataset": {
"resolved": "packages/dataset",
"link": true
@ -20562,35 +20574,11 @@
"ldo": "dist/index.js"
}
},
"packages/cli/node_modules/@ldo/dataset": {
"version": "0.0.1-alpha.24",
"resolved": "https://registry.npmjs.org/@ldo/dataset/-/dataset-0.0.1-alpha.24.tgz",
"integrity": "sha512-Jlh6DjvLN4gOOT8SYCKQaHZB8taeHvoriQRbF3s5CtYFf8Ne20job+02cToE1kVcBCinBrjZ/qnZ777wje+Z7A==",
"license": "MIT",
"dependencies": {
"@ldo/rdf-utils": "^0.0.1-alpha.24",
"@rdfjs/dataset": "^1.1.0",
"buffer": "^6.0.3",
"readable-stream": "^4.2.0"
}
},
"packages/cli/node_modules/@ldo/jsonld-dataset-proxy": {
"version": "0.0.1-alpha.29",
"resolved": "https://registry.npmjs.org/@ldo/jsonld-dataset-proxy/-/jsonld-dataset-proxy-0.0.1-alpha.29.tgz",
"integrity": "sha512-6r1tn/t82aJ6vjxEGyNSJTzQJgz1Z4Pu875o4EolXXJZCu7TiXBdsW6HyAuloqiIV5b6A2b+M6G0mOuObndooA==",
"license": "MIT",
"dependencies": {
"@ldo/rdf-utils": "^0.0.1-alpha.24",
"@ldo/subscribable-dataset": "^0.0.1-alpha.24",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/dataset": "^1.1.0",
"jsonld2graphobject": "^0.0.4"
}
},
"packages/cli/node_modules/@ldo/schema-converter-shex/node_modules/jsonld2graphobject": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/jsonld2graphobject/-/jsonld2graphobject-0.0.5.tgz",
"integrity": "sha512-5BqfXOq96+OBjjiJNG8gQH66pYt6hW88z2SJxdvFJo4XNoVMvqAcUz+JSm/KEWS5NLRnebApEzFrYP3HUiUmYw==",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@rdfjs/types": "^1.0.1",
@ -20599,16 +20587,6 @@
"uuid": "^8.3.2"
}
},
"packages/cli/node_modules/@ldo/subscribable-dataset": {
"version": "0.0.1-alpha.24",
"resolved": "https://registry.npmjs.org/@ldo/subscribable-dataset/-/subscribable-dataset-0.0.1-alpha.24.tgz",
"integrity": "sha512-grQ0/pzdx4euBOTxMHqQqebOYBqrBbNS9Jk8sYFR4u/dEg8e6nIGz0E4beI83dHp/hT8fT18gs/gV4UxZzmphQ==",
"license": "MIT",
"dependencies": {
"@ldo/dataset": "^0.0.1-alpha.24",
"@ldo/rdf-utils": "^0.0.1-alpha.24"
}
},
"packages/cli/node_modules/@types/fs-extra": {
"version": "9.0.13",
"dev": true,
@ -20617,30 +20595,6 @@
"@types/node": "*"
}
},
"packages/cli/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"packages/cli/node_modules/fs-extra": {
"version": "10.1.0",
"license": "MIT",
@ -20653,22 +20607,6 @@
"node": ">=12"
}
},
"packages/cli/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"packages/cli/node_modules/ts-jest": {
"version": "27.1.5",
"dev": true,
@ -20736,13 +20674,194 @@
"node": ">=4.2.0"
}
},
"packages/cli/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"packages/connected": {
"name": "@ldo/connected",
"version": "1.0.0-alpha.1",
"license": "MIT",
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/rdf-utils": "^1.0.0-alpha.1",
"@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@ldo/cli": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid-notifications/types": "^0.1.2",
"@solid/community-server": "^7.1.3",
"@types/jest": "^27.0.3",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"jest-rdf": "^1.8.0",
"ts-jest": "^27.1.2",
"ts-node": "^10.9.1",
"typed-emitter": "^2.1.0",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1"
}
},
"packages/connected-nextgraph": {
"name": "@ldo/connected-nextgraph",
"version": "1.0.0-alpha.1",
"license": "MIT",
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/rdf-utils": "^1.0.0-alpha.1",
"@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@ldo/cli": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid-notifications/types": "^0.1.2",
"@solid/community-server": "^7.1.3",
"@types/jest": "^27.0.3",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"jest-rdf": "^1.8.0",
"ts-jest": "^27.1.2",
"ts-node": "^10.9.1",
"typed-emitter": "^2.1.0",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1"
}
},
"packages/connected-nextgraph/node_modules/ts-jest": {
"version": "27.1.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
"integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"bin": {
"uuid": "dist/bin/uuid"
"ts-jest": "cli.js"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@types/jest": "^27.0.0",
"babel-jest": ">=27.0.0 <28",
"jest": "^27.0.0",
"typescript": ">=3.8 <5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/jest": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"packages/connected-nextgraph/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"packages/connected-solid": {
"name": "@ldo/connected-solid",
"version": "1.0.0-alpha.1",
"license": "MIT",
"dependencies": {
"@ldo/connected": "^1.0.0-alpha.1",
"@ldo/connected-nextgraph": "^1.0.0-alpha.1"
},
"devDependencies": {}
},
"packages/connected/node_modules/ts-jest": {
"version": "27.1.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
"integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@types/jest": "^27.0.0",
"babel-jest": ">=27.0.0 <28",
"jest": "^27.0.0",
"typescript": ">=3.8 <5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/jest": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"packages/connected/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"packages/dataset": {

@ -1,5 +1,5 @@
{
"name": "@ldo/connected-solid",
"name": "@ldo/connected-nextgraph",
"version": "1.0.0-alpha.1",
"description": "A plugin for @ldo/connected to work with the Solid ecosystem.",
"main": "dist/index.js",

@ -3,4 +3,4 @@ export * from "./NextGraphConnectedPlugin";
export * from "./resources/NextGraphResource";
export * from "./util/isSolidUri";
export * from "./util/isNextGraphUri";

@ -1,3 +1,74 @@
import type { Resource } from "@ldo/connected";
import {
Unfetched,
type ConnectedResult,
type Resource,
type ResourceResult,
type SubscriptionCallbacks,
type ResourceEventEmitter,
} from "@ldo/connected";
import type { NextGraphUri } from "../types";
import EventEmitter from "events";
export class NextGraphResource implements Resource {}
export class NextGraphResource
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource<NextGraphUri>
{
public readonly uri: NextGraphUri;
public readonly type = "NextGraphResource" as const;
public status: ConnectedResult;
constructor(uri: NextGraphUri) {
super();
this.uri = uri;
this.status = new Unfetched(this);
}
isLoading(): boolean {
throw new Error("Method not implemented.");
}
isFetched(): boolean {
throw new Error("Method not implemented.");
}
isUnfetched(): boolean {
throw new Error("Method not implemented.");
}
isDoingInitialFetch(): boolean {
throw new Error("Method not implemented.");
}
isPresent(): boolean {
throw new Error("Method not implemented.");
}
isAbsent(): boolean {
throw new Error("Method not implemented.");
}
isSubscribedToNotifications(): boolean {
throw new Error("Method not implemented.");
}
read(): Promise<ResourceResult<this>> {
throw new Error("Method not implemented.");
}
readIfAbsent(): Promise<ResourceResult<this>> {
throw new Error("Method not implemented.");
}
subscribeToNotifications(callbacks?: SubscriptionCallbacks): Promise<string> {
throw new Error("Method not implemented.");
}
unsubscribeFromNotifications(subscriptionId: string): Promise<void> {
throw new Error("Method not implemented.");
}
unsubscribeFromAllNotifications(): Promise<void> {
throw new Error("Method not implemented.");
}
}

@ -16,4 +16,5 @@ const containerResource = dataset.getResource("https://example.com/container/");
const leafResource = dataset.getResource(
"https://example.com/container/index.ttl",
);
const nextGraphResource = dataset.getResource("did:ng:cool", "solid");
const nextGraphResource = dataset.getResource("did:ng:cool");

@ -2,13 +2,29 @@ import { LdoDataset } from "@ldo/ldo";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset";
import { InvalidIdentifierResource } from "./InvalidIdentifierResource";
type ReturnTypeFromArgs<T, Arg> = T extends (arg: Arg) => infer R ? R : never;
type ResourceTypes<Plugins extends ConnectedPlugin[]> =
| ReturnType<Plugins[number]["getResource"]>
| InvalidIdentifierResource;
type GetResourceReturn<
Plugin extends ConnectedPlugin,
UriType extends string,
> = UriType extends Parameters<Plugin["getResource"]>[0]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ResourceTypes<[Plugin]>;
export class ConnectedLdoDataset<
Plugins extends ConnectedPlugin[],
> extends LdoDataset {
private plugins: Plugins;
/**
* @internal
*
* A mapping between a resource URI and a Solid resource
*/
protected resourceMap: Map<string, ResourceTypes<Plugins>>;
constructor(
plugins: Plugins,
@ -18,18 +34,55 @@ export class ConnectedLdoDataset<
) {
super(datasetFactory, transactionDatasetFactory, initialDataset);
this.plugins = plugins;
this.resourceMap = new Map();
}
/**
* Retireves a representation of a Resource at the given URI. This resource
* represents the current state of the resource: whether it is currently
* fetched or in the process of fetching as well as some information about it.
*
* @param uri - the URI of the resource
* @param pluginName - optionally, force this function to choose a specific
* plugin to use rather than perform content negotiation.
*
* @returns a Resource
*
* @example
* ```typescript
* const profileDocument = solidLdoDataset
* .getResource("https://example.com/profile");
* ```
*/
getResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
_uri: UriType,
_pluginName?: Name,
): UriType extends Parameters<Plugin["getResource"]>[0]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> {
throw new Error("Not Implemented");
>(uri: UriType, pluginName?: Name): GetResourceReturn<Plugin, UriType> {
// Check for which plugins this uri is valid
const validPlugins = this.plugins
.filter((plugin) => plugin.isUriValid(uri))
.filter((plugin) => (pluginName ? pluginName === plugin.name : true));
if (validPlugins.length === 0) {
return new InvalidIdentifierResource(uri) as GetResourceReturn<
Plugin,
UriType
>;
} else if (validPlugins.length > 1) {
// TODO: LDO is currently not architected to have an ID valid in multiple
// protocols. This will need to be refactored if this is ever the case.
throw new Error(
"LDO Connect does not currently support two plugins with overlappng uris",
);
}
const plugin = validPlugins[0];
const normalizedUri = plugin.normalizeUri?.(uri) ?? uri;
let resource = this.resourceMap.get(normalizedUri);
if (!resource) {
resource = plugin.getResource(uri) as ResourceTypes<Plugins>;
this.resourceMap.set(normalizedUri, resource);
}
return resource as GetResourceReturn<Plugin, UriType>;
}
}

@ -3,4 +3,7 @@ import type { Resource } from "./Resource";
export interface ConnectedPlugin {
name: string;
getResource(uri: string): Resource;
createResource(): Promise<Resource>;
isUriValid(uri: string): boolean;
normalizeUri?: (uri: string) => string;
}

@ -0,0 +1,64 @@
import EventEmitter from "events";
import type { Resource, ResourceEventEmitter } from "./Resource";
import { InvalidUriError } from "./results/error/InvalidUriError";
import type { SubscriptionCallbacks } from "./notifications/NotificationSubscription";
export class InvalidIdentifierResource
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource
{
public readonly uri: string;
public readonly type = "InvalidIdentifierResouce" as const;
public status: InvalidUriError<this>;
constructor(uri: string) {
super();
this.uri = uri;
this.status = new InvalidUriError(this);
}
isLoading(): boolean {
return false;
}
isFetched(): boolean {
return false;
}
isUnfetched(): boolean {
return true;
}
isDoingInitialFetch(): boolean {
return false;
}
isPresent(): boolean {
return false;
}
isAbsent(): boolean {
return true;
}
isSubscribedToNotifications(): boolean {
return false;
}
async read(): Promise<InvalidUriError<this>> {
return this.status;
}
async readIfAbsent(): Promise<InvalidUriError<this>> {
return this.status;
}
async createAndOverwrite(): Promise<InvalidUriError<this>> {
return this.status;
}
async createIfAbsent(): Promise<InvalidUriError<this>> {
return this.status;
}
async subscribeToNotifications(
_callbacks?: SubscriptionCallbacks,
): Promise<string> {
throw new Error("Cannot subscribe to an invalid resource.");
}
async unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {
// Do Nothing
}
async unsubscribeFromAllNotifications(): Promise<void> {
// Do Nothing
}
}

@ -1 +1,28 @@
export interface Resource {}
import type TypedEmitter from "typed-emitter";
import type { ConnectedResult } from "./results/ConnectedResult";
import type { ResourceResult } from "./results/ResourceResult";
import type { SubscriptionCallbacks } from "./notifications/NotificationSubscription";
export type ResourceEventEmitter = TypedEmitter<{
update: () => void;
notification: () => void;
}>;
export interface Resource<UriType extends string = string>
extends ResourceEventEmitter {
readonly uri: UriType;
readonly type: string;
status: ConnectedResult;
isLoading(): boolean;
isFetched(): boolean;
isUnfetched(): boolean;
isDoingInitialFetch(): boolean;
isPresent(): boolean;
isAbsent(): boolean;
isSubscribedToNotifications(): boolean;
read(): Promise<ResourceResult<this>>;
readIfAbsent(): Promise<ResourceResult<this>>;
subscribeToNotifications(callbacks?: SubscriptionCallbacks): Promise<string>;
unsubscribeFromNotifications(subscriptionId: string): Promise<void>;
unsubscribeFromAllNotifications(): Promise<void>;
}

@ -1,5 +1,13 @@
import { ConnectedLdoDataset } from "./ConnectedLdoDataset";
export * from "./ConnectedLdoDataset";
export * from "./ConnectedPlugin";
export * from "./Resource";
export * from "./InvalidIdentifierResource";
export * from "./notifications/NotificationMessage";
export * from "./notifications/NotificationSubscription";
export * from "./results/ConnectedResult";
export * from "./results/ResourceResult";
export * from "./results/error/ErrorResult";
export * from "./results/success/SuccessResult";
export * from "./results/success/Unfetched";

@ -0,0 +1,10 @@
/**
* A message sent from the Pod as a notification
*/
export interface NotificationMessage {
"@context": string | string[];
id: string;
type: "Update" | "Delete" | "Remove" | "Add";
object: string;
published: string;
}

@ -0,0 +1,144 @@
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext";
import type { Resource } from "../Resource";
import type { NotificationMessage } from "./NotificationMessage";
import type { NotificationCallbackError } from "./results/NotificationErrors";
import { v4 } from "uuid";
export interface SubscriptionCallbacks {
onNotification?: (message: NotificationMessage) => void;
// TODO: make notification errors more specific
onNotificationError?: (error: Error) => void;
}
/**
* @internal
* Abstract class for notification subscription methods.
*/
export abstract class NotificationSubscription {
protected resource: Resource;
protected parentSubscription: (message: NotificationMessage) => void;
protected context: SolidLdoDatasetContext;
protected subscriptions: Record<string, SubscriptionCallbacks> = {};
private isOpen: boolean = false;
constructor(
resource: Resource,
parentSubscription: (message: NotificationMessage) => void,
context: SolidLdoDatasetContext,
) {
this.resource = resource;
this.parentSubscription = parentSubscription;
this.context = context;
}
public isSubscribedToNotifications(): boolean {
return this.isOpen;
}
/**
* ===========================================================================
* PUBLIC
* ===========================================================================
*/
/**
* @internal
* subscribeToNotifications
*/
async subscribeToNotifications(
subscriptionCallbacks?: SubscriptionCallbacks,
): Promise<string> {
const subscriptionId = v4();
this.subscriptions[subscriptionId] = subscriptionCallbacks ?? {};
if (!this.isOpen) {
await this.open();
this.setIsOpen(true);
}
return subscriptionId;
}
/**
* @internal
* unsubscribeFromNotification
*/
async unsubscribeFromNotification(subscriptionId: string): Promise<void> {
if (
!!this.subscriptions[subscriptionId] &&
Object.keys(this.subscriptions).length === 1
) {
await this.close();
this.setIsOpen(false);
}
delete this.subscriptions[subscriptionId];
}
/**
* @internal
* unsubscribeFromAllNotifications
*/
async unsubscribeFromAllNotifications(): Promise<void> {
await Promise.all(
Object.keys(this.subscriptions).map((id) =>
this.unsubscribeFromNotification(id),
),
);
}
/**
* ===========================================================================
* HELPERS
* ===========================================================================
*/
/**
* @internal
* Opens the subscription
*/
protected abstract open(): Promise<void>;
/**
* @internal
* Closes the subscription
*/
protected abstract close(): Promise<void>;
/**
* ===========================================================================
* CALLBACKS
* ===========================================================================
*/
/**
* @internal
* onNotification
*/
protected onNotification(message: NotificationMessage): void {
this.parentSubscription(message);
Object.values(this.subscriptions).forEach(({ onNotification }) => {
onNotification?.(message);
});
}
/**
* @internal
* onNotificationError
*/
protected onNotificationError(message: NotificationCallbackError): void {
Object.values(this.subscriptions).forEach(({ onNotificationError }) => {
onNotificationError?.(message);
});
if (message.type === "disconnectedNotAttemptingReconnectError") {
this.setIsOpen(false);
}
}
/**
* @internal
* setIsOpen
*/
protected setIsOpen(status: boolean) {
const shouldUpdate = status !== this.isOpen;
this.isOpen = status;
if (shouldUpdate) this.resource.emit("update");
}
}

@ -0,0 +1,30 @@
import type { UnexpectedResourceError } from "../../../requester/results/error/ErrorResult";
import { ResourceError } from "../../../requester/results/error/ErrorResult";
export type NotificationCallbackError =
| DisconnectedAttemptingReconnectError
| DisconnectedNotAttemptingReconnectError
| UnsupportedNotificationError
| UnexpectedResourceError;
/**
* Indicates that the requested method for receiving notifications is not
* supported by this Pod.
*/
export class UnsupportedNotificationError extends ResourceError {
readonly type = "unsupportedNotificationError" as const;
}
/**
* Indicates that the socket has disconnected and is attempting to reconnect.
*/
export class DisconnectedAttemptingReconnectError extends ResourceError {
readonly type = "disconnectedAttemptingReconnectError" as const;
}
/**
* Indicates that the socket has disconnected and is attempting to reconnect.
*/
export class DisconnectedNotAttemptingReconnectError extends ResourceError {
readonly type = "disconnectedNotAttemptingReconnectError" as const;
}

@ -0,0 +1,7 @@
/**
* A type returned by all request functions
*/
export interface ConnectedResult {
readonly type: string;
readonly isError: boolean;
}

@ -0,0 +1,7 @@
import type { Resource } from "../Resource";
import type { ResourceError } from "./error/ErrorResult";
import type { ResourceSuccess } from "./success/SuccessResult";
export type ResourceResult<ResourceType extends Resource> =
| ResourceSuccess<ResourceType>
| ResourceError<ResourceType>;

@ -0,0 +1,139 @@
import type { Resource } from "../../Resource";
import type { ConnectedResult } from "../ConnectedResult";
/**
* A result indicating that the request failed in some kind of way
*/
export abstract class ErrorResult extends Error implements ConnectedResult {
/**
* Indicates the specific type of error
*/
abstract readonly type: string;
/**
* Always true
*/
readonly isError = true as const;
/**
* @param message - a custom message for the error
*/
constructor(message?: string) {
super(message || "An unkown error was encountered.");
}
}
/**
* An error for a specific resource
*/
export abstract class ResourceError<
ResourceType extends Resource,
> extends ErrorResult {
/**
* The URI of the resource
*/
readonly uri: ResourceType["uri"];
/**
* The resource that failed
*/
readonly resource: ResourceType;
/**
* @param uri - The URI of the resource
* @param message - A custom message for the error
*/
constructor(resource: ResourceType, message?: string) {
super(message || `An unkown error for ${resource.uri}`);
this.uri = resource.uri;
this.resource = resource;
}
}
/**
* An error that aggregates many errors
*/
export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult {
readonly type = "aggregateError" as const;
/**
* A list of all errors returned
*/
readonly errors: ErrorType[];
/**
* @param errors - List of all errors returned
* @param message - A custom message for the error
*/
constructor(
errors: (ErrorType | AggregateError<ErrorType>)[],
message?: string,
) {
const allErrors: ErrorType[] = [];
errors.forEach((error) => {
if (error instanceof AggregateError) {
error.errors.forEach((subError) => {
allErrors.push(subError);
});
} else {
allErrors.push(error);
}
});
super(
message ||
`Encountered multiple errors:${allErrors.reduce(
(agg, cur) => `${agg}\n${cur}`,
"",
)}`,
);
this.errors = allErrors;
}
}
/**
* Represents some error that isn't handled under other errors. This is usually
* returned when something threw an error that LDO did not expect.
*/
export class UnexpectedResourceError<
ResourceType extends Resource,
> extends ResourceError<ResourceType> {
readonly type = "unexpectedResourceError" as const;
/**
* The error that was thrown
*/
error: Error;
/**
* @param uri - URI of the resource
* @param error - The error that was thrown
*/
constructor(resource: ResourceType, error: Error) {
super(resource, error.message);
this.error = error;
}
/**
* @internal
*
* Creates an UnexpectedResourceError from a thrown error
* @param uri - The URI of the resource
* @param err - The thrown error
* @returns an UnexpectedResourceError
*/
static fromThrown<ResourceType extends Resource>(
resource: ResourceType,
err: unknown,
): UnexpectedResourceError<ResourceType> {
if (err instanceof Error) {
return new UnexpectedResourceError(resource, err);
} else if (typeof err === "string") {
return new UnexpectedResourceError(resource, new Error(err));
} else {
return new UnexpectedResourceError(
resource,
new Error(`Error of type ${typeof err} thrown: ${err}`),
);
}
}
}

@ -0,0 +1,16 @@
import type { Resource } from "../../Resource";
import { ResourceError } from "./ErrorResult";
/**
* An InvalidUriError is returned when a URI was provided that is not a valid
* URI.
*/
export class InvalidUriError<
ResourceType extends Resource,
> extends ResourceError<ResourceType> {
readonly type = "invalidUriError" as const;
constructor(resource: ResourceType, message?: string) {
super(resource, message || `${resource.uri} is an invalid uri.`);
}
}

@ -0,0 +1,19 @@
import type { Container } from "../../../resource/Container";
import type { ResourceSuccess, SuccessResult } from "./SuccessResult";
/**
* Indicates that the request to check if a resource is the root container was
* a success.
*/
export interface CheckRootContainerSuccess extends ResourceSuccess {
type: "checkRootContainerSuccess";
/**
* True if this resoure is the root container
*/
isRootContainer: boolean;
}
export interface GetStorageContainerFromWebIdSuccess extends SuccessResult {
type: "getStorageContainerFromWebIdSuccess";
storageContainers: Container[];
}

@ -0,0 +1,13 @@
import type { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that the request to create the resource was a success.
*/
export interface CreateSuccess extends ResourceSuccess {
type: "createSuccess";
/**
* True if there was a resource that existed before at the given URI that was
* overwritten
*/
didOverwrite: boolean;
}

@ -0,0 +1,14 @@
import type { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that the request to delete a resource was a success.
*/
export interface DeleteSuccess extends ResourceSuccess {
type: "deleteSuccess";
/**
* True if there was a resource at the provided URI that was deleted. False if
* a resource didn't exist.
*/
resourceExisted: boolean;
}

@ -0,0 +1,71 @@
import type { ResourceSuccess, SuccessResult } from "./SuccessResult";
/**
* Indicates that the request to read a resource was a success
*/
export interface ReadSuccess extends ResourceSuccess {
/**
* True if the resource was recalled from local memory rather than a recent
* request
*/
recalledFromMemory: boolean;
}
/**
* Indicates that the read request was successful and that the resource
* retrieved was a binary resource.
*/
export interface BinaryReadSuccess extends ReadSuccess {
type: "binaryReadSuccess";
/**
* The raw data for the binary resource
*/
blob: Blob;
/**
* The mime type of the binary resource
*/
mimeType: string;
}
/**
* Indicates that the read request was successful and that the resource
* retrieved was a data (RDF) resource.
*/
export interface DataReadSuccess extends ReadSuccess {
type: "dataReadSuccess";
}
/**
* Indicates that the read request was successful and that the resource
* retrieved was a container resource.
*/
export interface ContainerReadSuccess extends ReadSuccess {
type: "containerReadSuccess";
/**
* True if this container is a root container
*/
isRootContainer: boolean;
}
/**
* Indicates that the read request was successful, but no resource exists at
* the provided URI.
*/
export interface AbsentReadSuccess extends ReadSuccess {
type: "absentReadSuccess";
}
/**
* A helper function that checks to see if a result is a ReadSuccess result
*
* @param result - the result to check
* @returns true if the result is a ReadSuccessResult result
*/
export function isReadSuccess(result: SuccessResult): result is ReadSuccess {
return (
result.type === "binaryReadSuccess" ||
result.type === "dataReadSuccess" ||
result.type === "absentReadSuccess" ||
result.type === "containerReadSuccess"
);
}

@ -0,0 +1,51 @@
import type { Resource } from "../../Resource";
import type { ConnectedResult } from "../ConnectedResult";
/**
* Indicates that some action taken by LDO was a success
*/
export abstract class SuccessResult implements ConnectedResult {
abstract readonly type: string;
readonly isError = false as const;
}
/**
* Indicates that a request to a resource was aa success
*/
export abstract class ResourceSuccess<
ResourceType extends Resource,
> extends SuccessResult {
/**
* The URI of the resource
*/
uri: ResourceType["uri"];
/**
* The resource that was successful
*/
resource: Resource;
constructor(resource: ResourceType) {
super();
this.uri = resource.uri;
this.resource = resource;
}
}
/**
* A grouping of multiple successes as a result of an action
*/
export class AggregateSuccess<
SuccessType extends SuccessResult,
> extends SuccessResult {
type = "aggregateSuccess" as const;
/**
* An array of all successesses
*/
results: SuccessType[];
constructor(results: SuccessType[]) {
super();
this.results = results;
}
}

@ -0,0 +1,11 @@
import type { Resource } from "../../Resource";
import { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that a specific resource is unfetched
*/
export class Unfetched<
ResourceType extends Resource,
> extends ResourceSuccess<ResourceType> {
readonly type = "unfetched" as const;
}

@ -0,0 +1,24 @@
import type { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that an update request to a resource was successful
*/
export interface UpdateSuccess extends ResourceSuccess {
type: "updateSuccess";
}
/**
* Indicates that an update request to the default graph was successful. This
* data was not written to a Pod. It was only written locally.
*/
export interface UpdateDefaultGraphSuccess extends ResourceSuccess {
type: "updateDefaultGraphSuccess";
}
/**
* Indicates that LDO ignored an invalid update (usually because a container
* attempted an update)
*/
export interface IgnoredInvalidUpdateSuccess extends ResourceSuccess {
type: "ignoredInvalidUpdateSuccess";
}
Loading…
Cancel
Save