Added useSubscribeToNotifications

main
Jackson Morgan 8 months ago
parent b42ed2dbf3
commit 0f35c7c7a8
  1. 52
      packages/solid-react/src/useSubscribeToResource.ts
  2. 116
      packages/solid-react/test/Integration.test.tsx
  3. 20
      packages/solid-type-index/src/react/useTypeIndex.ts
  4. 35
      packages/solid-type-index/src/react/util/useSubscribeToUris.ts
  5. 2
      packages/solid/src/resource/notifications/NotificationSubscription.ts

@ -0,0 +1,52 @@
import { useLdo } from "./SolidLdoProvider";
import { useEffect, useRef } from "react";
export function useSubscribeToResource(...uris: string[]): void {
const { dataset } = useLdo();
const currentlySubscribed = useRef<Record<string, string>>({});
useEffect(() => {
const resources = uris.map((uri) => dataset.getResource(uri));
const previousSubscriptions = { ...currentlySubscribed.current };
Promise.all<void>(
resources.map(async (resource) => {
if (!previousSubscriptions[resource.uri]) {
// Prevent multiple triggers from created subscriptions while waiting
// for connection
currentlySubscribed.current[resource.uri] = "AWAITING";
// Read and subscribe
await resource.readIfUnfetched();
currentlySubscribed.current[resource.uri] =
await resource.subscribeToNotifications();
} else {
delete previousSubscriptions[resource.uri];
}
}),
).then(async () => {
// Unsubscribe from all remaining previous subscriptions
await Promise.all(
Object.entries(previousSubscriptions).map(
async ([resourceUri, subscriptionId]) => {
// Unsubscribe
delete currentlySubscribed.current[resourceUri];
const resource = dataset.getResource(resourceUri);
await resource.unsubscribeFromNotifications(subscriptionId);
},
),
);
});
}, [uris]);
// Cleanup Subscriptions
useEffect(() => {
return () => {
Promise.all(
Object.entries(currentlySubscribed.current).map(
async ([resourceUri, subscriptionId]) => {
const resource = dataset.getResource(resourceUri);
await resource.unsubscribeFromNotifications(subscriptionId);
},
),
);
};
}, []);
}

@ -16,6 +16,7 @@ import type { PostSh } from "./.ldo/post.typings";
import { useSubject } from "../src/useSubject";
import { useMatchSubject } from "../src/useMatchSubject";
import { useMatchObject } from "../src/useMatchObject";
import { useSubscribeToResource } from "../src/useSubscribeToResource";
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
@ -519,4 +520,119 @@ describe("Integration Tests", () => {
expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2");
});
});
/**
* ===========================================================================
* useSubscribeToResource
* ===========================================================================
*/
describe("useSubscribeToResource", () => {
it("handles useSubscribeToResource", async () => {
const NotificationTest: FunctionComponent = () => {
const [subscribedUris, setSubScribedUris] = useState<string[]>([
SAMPLE_DATA_URI,
]);
useSubscribeToResource(...subscribedUris);
const resource1 = useResource(SAMPLE_DATA_URI);
const resource2 = useResource(SAMPLE_BINARY_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
const addPublisher = useCallback(async () => {
await fetch(SAMPLE_DATA_URI, {
method: "PATCH",
body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> <http://schema.org/publisher> <https://example.com/Publisher3> . }`,
headers: {
"Content-Type": "application/sparql-update",
},
});
}, []);
if (resource1.isLoading() || resource2.isLoading())
return <p>Loading</p>;
return (
<div>
<p role="resource1">
{resource1.isSubscribedToNotifications().toString()}
</p>
<p role="resource2">
{resource2.isSubscribedToNotifications().toString()}
</p>
<ul role="list">
{post.publisher.map((publisher) => {
return <li key={publisher["@id"]}>{publisher["@id"]}</li>;
})}
</ul>
<button onClick={addPublisher}>Add Publisher</button>
<button
onClick={() =>
setSubScribedUris([SAMPLE_DATA_URI, SAMPLE_BINARY_URI])
}
>
Subscribe More
</button>
<button onClick={() => setSubScribedUris([SAMPLE_BINARY_URI])}>
Subscribe Less
</button>
</div>
);
};
const { unmount } = render(
<UnauthenticatedSolidLdoProvider>
<NotificationTest />
</UnauthenticatedSolidLdoProvider>,
);
const preResource1P = await screen.findByRole("resource1");
expect(preResource1P.innerHTML).toBe("false");
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
const list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("https://example.com/Publisher1");
expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2");
const resource1P = await screen.findByRole("resource1");
expect(resource1P.innerHTML).toBe("true");
const resource2P = await screen.findByRole("resource2");
expect(resource2P.innerHTML).toBe("false");
// Click button to add a publisher
await fireEvent.click(screen.getByText("Add Publisher"));
await screen.findByText("https://example.com/Publisher3");
// Verify the new publisher is in the list
const updatedList = await screen.findByRole("list");
expect(updatedList.children[2].innerHTML).toBe(
"https://example.com/Publisher3",
);
await fireEvent.click(screen.getByText("Subscribe More"));
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
const resource1PUpdated = await screen.findByRole("resource1");
expect(resource1PUpdated.innerHTML).toBe("true");
const resource2PUpdated = await screen.findByRole("resource2");
expect(resource2PUpdated.innerHTML).toBe("true");
await fireEvent.click(screen.getByText("Subscribe Less"));
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
const resource1PUpdatedAgain = await screen.findByRole("resource1");
expect(resource1PUpdatedAgain.innerHTML).toBe("false");
const resource2PUpdatedAgain = await screen.findByRole("resource2");
expect(resource2PUpdatedAgain.innerHTML).toBe("true");
unmount();
});
});
});

@ -1,12 +1,11 @@
import type { LeafUri } from "@ldo/solid";
import { useTypeIndexProfile } from "./useTypeIndexProfile";
import { useEffect, useMemo } from "react";
import { useLdo } from "@ldo/solid-react";
import { useMemo } from "react";
import { useSubscribeToUris } from "./util/useSubscribeToUris";
export function useTypeIndex(classUri: string): Promise<LeafUri[]> {
const { dataset } = useLdo();
const profile = useTypeIndexProfile();
const typeIndexUris: string[] = useMemo(() => {
const uris: string[] = [];
profile?.privateTypeIndex?.forEach((indexNode) => {
@ -15,17 +14,10 @@ export function useTypeIndex(classUri: string): Promise<LeafUri[]> {
profile?.publicTypeIndex?.forEach((indexNode) => {
uris.push(indexNode["@id"]);
});
return uris;
}, [profile]);
useEffect(() => {
const resources = typeIndexUris.map((uri) => dataset.getResource(uri));
resources.forEach((resource) => {
resource.readIfUnfetched();
resource.subscribeToNotifications();
});
useSubscribeToUris(typeIndexUris);
return () => {
resources.forEach((resource) => resource.unsubscribeFromNotifications());
}
}, [typeIndexUris]);
}

@ -0,0 +1,35 @@
import { useLdo } from "@ldo/solid-react";
import { useEffect, useRef } from "react";
export function useSubscribeToUris(uris: string[]) {
const { dataset } = useLdo();
const currentlySubscribed = useRef<Record<string, string>>({});
useEffect(() => {
const resources = uris.map((uri) => dataset.getResource(uri));
const previousSubscriptions = { ...currentlySubscribed.current };
Promise.all<void>(
resources.map(async (resource) => {
if (!previousSubscriptions[resource.uri]) {
// Read and subscribe
await resource.readIfUnfetched();
currentlySubscribed.current[resource.uri] =
await resource.subscribeToNotifications();
} else {
delete previousSubscriptions[resource.uri];
}
}),
).then(async () => {
// Unsubscribe from all remaining previous subscriptions
await Promise.all(
Object.entries(previousSubscriptions).map(
async ([resourceUri, subscriptionId]) => {
// Unsubscribe
delete currentlySubscribed.current[resourceUri];
const resource = dataset.getResource(resourceUri);
await resource.unsubscribeFromNotifications(subscriptionId);
},
),
);
});
}, [uris]);
}

@ -137,7 +137,7 @@ export abstract class NotificationSubscription {
* setIsOpen
*/
protected setIsOpen(status: boolean) {
const shouldUpdate = status === this.isOpen;
const shouldUpdate = status !== this.isOpen;
this.isOpen = status;
if (shouldUpdate) this.resource.emit("update");
}

Loading…
Cancel
Save