Subscribable dataset can now remove all events for a particular listener

main
Ailin Luca 2 years ago
parent 3791babfa2
commit 00b1c6ef6f
  1. 4
      packages/cli/src/init.ts
  2. 2
      packages/demo-react/package.json
  3. 0
      packages/demo-react/src/.ldo/solidProfile.context.ts
  4. 0
      packages/demo-react/src/.ldo/solidProfile.schema.ts
  5. 0
      packages/demo-react/src/.ldo/solidProfile.shapeTypes.ts
  6. 0
      packages/demo-react/src/.ldo/solidProfile.typings.ts
  7. 0
      packages/demo-react/src/.shapes/solidProfile.shex
  8. 44
      packages/demo-react/src/Header.tsx
  9. 28
      packages/demo-react/src/Layout.tsx
  10. 35
      packages/solid-react/src/useResource.ts
  11. 0
      packages/solid-react/src/useSubject.ts
  12. 96
      packages/solid-react/src/util/TrackingProxyContext.ts
  13. 25
      packages/subscribable-dataset/src/WrapperSubscribableDataset.ts
  14. 5
      packages/subscribable-dataset/src/types.ts
  15. 16
      packages/subscribable-dataset/test/WrapperSubscribableDataset.test.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 {

@ -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": [

@ -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 (
<>
<span>
Logged in as {webId}. Welcome{" "}
{webIdResource.isReading() ? "LOADING NAME" : "Cool Dude"}
</span>
<button onClick={logout}>Log Out</button>
</>
);
};
export const Header: FunctionComponent = () => {
const [issuer, setIssuer] = useState(DEFAULT_ISSUER);
const { login, signUp, session } = useSolidAuth();
return (
<header style={{ display: "flex" }}>
{session.webId ? (
<LoggedInHeader webId={session.webId} />
) : (
<>
<input
type="text"
value={issuer}
onChange={(e) => setIssuer(e.target.value)}
/>
<button onClick={() => login(issuer)}>Log In</button>
<button onClick={() => signUp(issuer)}>Sign Up</button>
</>
)}
</header>
);
};

@ -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 <p>Loading</p>;
}
return (
<div>
<header style={{ display: "flex" }}>
{session.isLoggedIn ? (
<>
<span>Logged in as {session.webId}</span>
<button onClick={logout}>Log Out</button>
</>
) : (
<>
<input
type="text"
value={issuer}
onChange={(e) => setIssuer(e.target.value)}
/>
<button onClick={() => login(issuer)}>Log In</button>
<button onClick={() => signUp(issuer)}>Sign Up</button>
</>
)}
</header>
<Header />
<hr />
{session.isLoggedIn ? <RouterProvider router={router} /> : undefined}
</div>

@ -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<Resource | undefined>();
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);
};

@ -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<Quad>;
}
export class TrackingProxyContext extends ProxyContext {
private listener: () => void;
private subscribableDataset: SubscribableDataset<Quad>;
constructor(options: TrackingProxyContextOptions, listener: () => void) {
super(options);
this.subscribableDataset = options.dataset;
this.listener = listener;
}
protected createSubjectHandler(): ProxyHandler<SubjectProxyTarget> {
const baseHandler = super.createSubjectHandler();
const oldGetFunction = baseHandler.get;
const newGetFunction: ProxyHandler<SubjectProxyTarget>["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<ArrayProxyTarget> {
const baseHandler = super.createArrayHandler();
const oldGetFunction = baseHandler.get;
const newGetFunction: ProxyHandler<ArrayProxyTarget>["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",
]);

@ -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<nodeEventListener<InAndOutQuad>, Set<string>> =
new Map();
/**
*
@ -502,7 +507,12 @@ export default class WrapperSubscribableDataset<
eventName: QuadMatch,
listener: nodeEventListener<InAndOutQuad>,
): 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<InAndOutQuad>): 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.
*/

@ -116,6 +116,11 @@ export interface SubscribableDataset<InAndOutQuad extends BaseQuad = BaseQuad>
listener: nodeEventListener<InAndOutQuad>,
): this;
/**
* Removes the specified listener from all events
*/
removeListenerFromAllEvents(listener: nodeEventListener<InAndOutQuad>): 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.
*/

@ -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(

Loading…
Cancel
Save