diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts
index e821bb5..a5567e7 100644
--- a/packages/cli/src/init.ts
+++ b/packages/cli/src/init.ts
@@ -3,8 +3,8 @@ import fs from "fs-extra";
import path from "path";
import { renderFile } from "ejs";
-const DEFAULT_SHAPES_FOLDER = "./shapes";
-const DEFAULT_LDO_FOLDER = "./ldo";
+const DEFAULT_SHAPES_FOLDER = "./.shapes";
+const DEFAULT_LDO_FOLDER = "./.ldo";
const POTENTIAL_PARENT_DIRECTORIES = ["src", "lib", "bin"];
export interface InitOptions {
diff --git a/packages/demo-react/package.json b/packages/demo-react/package.json
index a804c6e..8bedb12 100644
--- a/packages/demo-react/package.json
+++ b/packages/demo-react/package.json
@@ -15,7 +15,7 @@
"build": "craco build",
"eject": "react-scripts eject",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
- "build:ldo": "ldo build --input src/shapes --output src/ldo"
+ "build:ldo": "ldo build --input src/.shapes --output src/.ldo"
},
"eslintConfig": {
"extends": [
diff --git a/packages/demo-react/src/ldo/solidProfile.context.ts b/packages/demo-react/src/.ldo/solidProfile.context.ts
similarity index 100%
rename from packages/demo-react/src/ldo/solidProfile.context.ts
rename to packages/demo-react/src/.ldo/solidProfile.context.ts
diff --git a/packages/demo-react/src/ldo/solidProfile.schema.ts b/packages/demo-react/src/.ldo/solidProfile.schema.ts
similarity index 100%
rename from packages/demo-react/src/ldo/solidProfile.schema.ts
rename to packages/demo-react/src/.ldo/solidProfile.schema.ts
diff --git a/packages/demo-react/src/ldo/solidProfile.shapeTypes.ts b/packages/demo-react/src/.ldo/solidProfile.shapeTypes.ts
similarity index 100%
rename from packages/demo-react/src/ldo/solidProfile.shapeTypes.ts
rename to packages/demo-react/src/.ldo/solidProfile.shapeTypes.ts
diff --git a/packages/demo-react/src/ldo/solidProfile.typings.ts b/packages/demo-react/src/.ldo/solidProfile.typings.ts
similarity index 100%
rename from packages/demo-react/src/ldo/solidProfile.typings.ts
rename to packages/demo-react/src/.ldo/solidProfile.typings.ts
diff --git a/packages/demo-react/src/shapes/solidProfile.shex b/packages/demo-react/src/.shapes/solidProfile.shex
similarity index 100%
rename from packages/demo-react/src/shapes/solidProfile.shex
rename to packages/demo-react/src/.shapes/solidProfile.shex
diff --git a/packages/demo-react/src/Header.tsx b/packages/demo-react/src/Header.tsx
new file mode 100644
index 0000000..4070f24
--- /dev/null
+++ b/packages/demo-react/src/Header.tsx
@@ -0,0 +1,44 @@
+import { useState } from "react";
+import type { FunctionComponent } from "react";
+import React from "react";
+import { useResource, useSolidAuth } from "@ldo/solid-react";
+
+const DEFAULT_ISSUER = "https://solidweb.me";
+
+export const LoggedInHeader: FunctionComponent<{ webId: string }> = ({
+ webId,
+}) => {
+ const webIdResource = useResource(webId);
+ const { logout } = useSolidAuth();
+ return (
+ <>
+
+ Logged in as {webId}. Welcome{" "}
+ {webIdResource.isReading() ? "LOADING NAME" : "Cool Dude"}
+
+
+ >
+ );
+};
+
+export const Header: FunctionComponent = () => {
+ const [issuer, setIssuer] = useState(DEFAULT_ISSUER);
+ const { login, signUp, session } = useSolidAuth();
+ return (
+
+ );
+};
diff --git a/packages/demo-react/src/Layout.tsx b/packages/demo-react/src/Layout.tsx
index fd811a4..10535ed 100644
--- a/packages/demo-react/src/Layout.tsx
+++ b/packages/demo-react/src/Layout.tsx
@@ -1,10 +1,11 @@
import { useSolidAuth } from "@ldo/solid-react";
-import React, { useState } from "react";
+import React from "react";
import type { FunctionComponent } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Dashboard } from "./dashboard/Dashboard";
import { BuildMainContainer } from "./dashboard/BuildMainContainer";
import { MediaPage } from "./media/MediaPage";
+import { Header } from "./Header";
const router = createBrowserRouter([
{
@@ -17,35 +18,14 @@ const router = createBrowserRouter([
},
]);
-const DEFAULT_ISSUER = "https://solidweb.me";
-
export const Layout: FunctionComponent = () => {
- const { login, logout, signUp, session, ranInitialAuthCheck } =
- useSolidAuth();
- const [issuer, setIssuer] = useState(DEFAULT_ISSUER);
+ const { session, ranInitialAuthCheck } = useSolidAuth();
if (!ranInitialAuthCheck) {
return
Loading
;
}
return (
-
+
{session.isLoggedIn ? : undefined}
diff --git a/packages/solid-react/src/useResource.ts b/packages/solid-react/src/useResource.ts
index 8a8d48a..53fde3c 100644
--- a/packages/solid-react/src/useResource.ts
+++ b/packages/solid-react/src/useResource.ts
@@ -9,12 +9,36 @@ import type {
import { useLdo } from "./SolidLdoProvider";
import { useForceReload } from "./util/useForceReload";
-export function useResource(uri: ContainerUri): Container;
-export function useResource(uri: LeafUri): Leaf;
-export function useResource(uri: string): Leaf | Container;
-export function useResource(uri: string): Leaf | Container {
+export interface UseResourceOptions {
+ suppressInitialRead?: boolean;
+ reloadOnMount?: boolean;
+}
+
+export function useResource(
+ uri: ContainerUri,
+ options?: UseResourceOptions,
+): Container;
+export function useResource(uri: LeafUri, options?: UseResourceOptions): Leaf;
+export function useResource(
+ uri: string,
+ options?: UseResourceOptions,
+): Leaf | Container;
+export function useResource(
+ uri: string,
+ options?: UseResourceOptions,
+): Leaf | Container {
const { getResource } = useLdo();
- const resource = useMemo(() => getResource(uri), [getResource, uri]);
+ const resource = useMemo(() => {
+ const resource = getResource(uri);
+ if (!options?.suppressInitialRead) {
+ if (options?.reloadOnMount) {
+ resource.read();
+ } else {
+ resource.readIfUnfetched();
+ }
+ }
+ return resource;
+ }, [getResource, uri]);
const pastResource = useRef();
const forceReload = useForceReload();
useEffect(() => {
@@ -23,6 +47,7 @@ export function useResource(uri: string): Leaf | Container {
}
pastResource.current = resource;
resource.on("update", forceReload);
+
return () => {
resource.off("update", forceReload);
};
diff --git a/packages/solid-react/src/useSubject.ts b/packages/solid-react/src/useSubject.ts
new file mode 100644
index 0000000..e69de29
diff --git a/packages/solid-react/src/util/TrackingProxyContext.ts b/packages/solid-react/src/util/TrackingProxyContext.ts
new file mode 100644
index 0000000..a27a130
--- /dev/null
+++ b/packages/solid-react/src/util/TrackingProxyContext.ts
@@ -0,0 +1,96 @@
+import type {
+ ArrayProxyTarget,
+ SubjectProxyTarget,
+ ProxyContextOptions,
+} from "@ldo/jsonld-dataset-proxy";
+import { ProxyContext } from "@ldo/jsonld-dataset-proxy";
+import type { SubscribableDataset } from "@ldo/subscribable-dataset";
+import { namedNode } from "@rdfjs/data-model";
+import type { Quad } from "@rdfjs/types";
+
+export interface TrackingProxyContextOptions extends ProxyContextOptions {
+ dataset: SubscribableDataset;
+}
+
+export class TrackingProxyContext extends ProxyContext {
+ private listener: () => void;
+ private subscribableDataset: SubscribableDataset;
+
+ constructor(options: TrackingProxyContextOptions, listener: () => void) {
+ super(options);
+ this.subscribableDataset = options.dataset;
+ this.listener = listener;
+ }
+
+ protected createSubjectHandler(): ProxyHandler {
+ const baseHandler = super.createSubjectHandler();
+ const oldGetFunction = baseHandler.get;
+ const newGetFunction: ProxyHandler["get"] = (
+ target: SubjectProxyTarget,
+ key: string | symbol,
+ receiver,
+ ) => {
+ const subject = target["@id"];
+ if (typeof key === "symbol") {
+ // Do Nothing
+ } else if (key === "@id") {
+ this.subscribableDataset.on([subject, null, null, null], this.listener);
+ } else if (!this.contextUtil.isArray(key)) {
+ const predicate = namedNode(this.contextUtil.keyToIri(key));
+ this.subscribableDataset.on(
+ [subject, predicate, null, null],
+ this.listener,
+ );
+ }
+ return oldGetFunction && oldGetFunction(target, key, receiver);
+ };
+ baseHandler.get = newGetFunction;
+ baseHandler.set = () => {
+ console.warn(
+ "You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
+ );
+ return true;
+ };
+ return baseHandler;
+ }
+
+ protected createArrayHandler(): ProxyHandler {
+ const baseHandler = super.createArrayHandler();
+ const oldGetFunction = baseHandler.get;
+ const newGetFunction: ProxyHandler["get"] = (
+ target: ArrayProxyTarget,
+ key: string | symbol,
+ receiver,
+ ) => {
+ if (qualifiedArrayMethods.has(key)) {
+ this.subscribableDataset.on(
+ [target[0][0], target[0][1], target[0][2], null],
+ this.listener,
+ );
+ }
+ return oldGetFunction && oldGetFunction(target, key, receiver);
+ };
+ baseHandler.get = newGetFunction;
+ return baseHandler;
+ }
+}
+
+const qualifiedArrayMethods = new Set([
+ "forEach",
+ "map",
+ "reduce",
+ Symbol.iterator,
+ "entries",
+ "every",
+ "filter",
+ "find",
+ "findIndex",
+ "findLast",
+ "findLastIndex",
+ "includes, indexOf",
+ "keys",
+ "lastIndexOf",
+ "reduceRight",
+ "some",
+ "values",
+]);
diff --git a/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts b/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts
index 6ce43e5..216eab1 100644
--- a/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts
+++ b/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts
@@ -42,6 +42,11 @@ export default class WrapperSubscribableDataset<
* The underlying event emitter
*/
private eventEmitter: EventEmitter;
+ /**
+ * Helps find all the events for a given listener
+ */
+ private listenerHashMap: Map, Set> =
+ new Map();
/**
*
@@ -502,7 +507,12 @@ export default class WrapperSubscribableDataset<
eventName: QuadMatch,
listener: nodeEventListener,
): this {
- this.eventEmitter.on(quadMatchToString(eventName), listener);
+ const eventString = quadMatchToString(eventName);
+ if (!this.listenerHashMap.has(listener)) {
+ this.listenerHashMap.set(listener, new Set());
+ }
+ this.listenerHashMap.get(listener)?.add(eventString);
+ this.eventEmitter.on(eventString, listener);
return this;
}
@@ -561,6 +571,19 @@ export default class WrapperSubscribableDataset<
return this;
}
+ /**
+ * Removes the specified listener from the listener array for the event named eventName.
+ */
+ removeListenerFromAllEvents(listener: nodeEventListener): this {
+ const eventStringSet = this.listenerHashMap.get(listener);
+ if (eventStringSet) {
+ eventStringSet.forEach((eventString) => {
+ this.eventEmitter.off(eventString, listener);
+ });
+ }
+ return this;
+ }
+
/**
* By default EventEmitters will print a warning if more than 10 listeners are added for a particular event. This is a useful default that helps finding memory leaks. The emitter.setMaxListeners() method allows the limit to be modified for this specific EventEmitter instance. The value can be set to Infinity (or 0) to indicate an unlimited number of listeners.
*/
diff --git a/packages/subscribable-dataset/src/types.ts b/packages/subscribable-dataset/src/types.ts
index b63ea59..fe7f49c 100644
--- a/packages/subscribable-dataset/src/types.ts
+++ b/packages/subscribable-dataset/src/types.ts
@@ -116,6 +116,11 @@ export interface SubscribableDataset
listener: nodeEventListener,
): this;
+ /**
+ * Removes the specified listener from all events
+ */
+ removeListenerFromAllEvents(listener: nodeEventListener): this;
+
/**
* By default EventEmitters will print a warning if more than 10 listeners are added for a particular event. This is a useful default that helps finding memory leaks. The emitter.setMaxListeners() method allows the limit to be modified for this specific EventEmitter instance. The value can be set to Infinity (or 0) to indicate an unlimited number of listeners.
*/
diff --git a/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts b/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts
index 2d2c20b..c8e91b4 100644
--- a/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts
+++ b/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts
@@ -336,6 +336,22 @@ describe("WrapperSubscribableDataset", () => {
expect(callbackFunc).toHaveBeenCalledTimes(0);
});
+ it("Unsubscribes from all events for a particular listener", () => {
+ const callbackFunc = jest.fn();
+ subscribableDatastet.on(
+ [namedNode("http://example.org/cartoons#Tom"), null, null, null],
+ callbackFunc,
+ );
+ subscribableDatastet.on(
+ [namedNode("http://example.org/cartoons#Licky"), null, null, null],
+ callbackFunc,
+ );
+ subscribableDatastet.removeListenerFromAllEvents(callbackFunc);
+ subscribableDatastet.add(tomColorQuad);
+ subscribableDatastet.add(lickyNameQuad);
+ expect(callbackFunc).toHaveBeenCalledTimes(0);
+ });
+
it("Runs 'once' without erroring", () => {
expect(
subscribableDatastet.once(