diff --git a/packages/solid/src/requester/requests/deleteResource.ts b/packages/solid/src/requester/requests/deleteResource.ts index d3de47e..8b5baaa 100644 --- a/packages/solid/src/requester/requests/deleteResource.ts +++ b/packages/solid/src/requester/requests/deleteResource.ts @@ -62,15 +62,7 @@ export async function deleteResource( // if it hasn't been deleted when you're unauthenticated. 404 happens when // the document never existed if (response.status === 205 || response.status === 404) { - if (options?.dataset) { - options.dataset.deleteMatches( - undefined, - undefined, - undefined, - namedNode(uri), - ); - deleteResourceRdfFromContainer(uri, options.dataset); - } + updateDatasetOnSuccessfulDelete(uri, response.status === 205, options); return { isError: false, type: "deleteSuccess", @@ -83,3 +75,22 @@ export async function deleteResource( return UnexpectedResourceError.fromThrown(uri, err); } } + +/** + * TODO + */ +export function updateDatasetOnSuccessfulDelete( + uri: string, + resourceExisted: boolean, + options?: DatasetRequestOptions, +): void { + if (options?.dataset) { + options.dataset.deleteMatches( + undefined, + undefined, + undefined, + namedNode(uri), + ); + deleteResourceRdfFromContainer(uri, options.dataset); + } +} diff --git a/packages/solid/src/requester/requests/readResource.ts b/packages/solid/src/requester/requests/readResource.ts index a0a961f..4b9a7bc 100644 --- a/packages/solid/src/requester/requests/readResource.ts +++ b/packages/solid/src/requester/requests/readResource.ts @@ -20,6 +20,7 @@ import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; import { guaranteeFetch } from "../../util/guaranteeFetch"; import { UnexpectedResourceError } from "../results/error/ErrorResult"; import { checkHeadersForRootContainer } from "./checkRootContainer"; +import { namedNode } from "@rdfjs/data-model"; /** * All possible return values for reading a leaf @@ -103,6 +104,16 @@ export async function readResource( headers: { accept: "text/turtle, */*" }, }); if (response.status === 404) { + // Clear existing data if present + if (options?.dataset) { + options.dataset.deleteMatches( + undefined, + undefined, + undefined, + namedNode(uri), + ); + } + return { isError: false, type: "absentReadSuccess", diff --git a/packages/solid/src/resource/Leaf.ts b/packages/solid/src/resource/Leaf.ts index 1490a0b..7272f15 100644 --- a/packages/solid/src/resource/Leaf.ts +++ b/packages/solid/src/resource/Leaf.ts @@ -362,7 +362,8 @@ export class Leaf extends Resource { * A helper method updates this leaf's internal state upon delete success * @param result - the result of the delete success */ - protected updateWithDeleteSuccess(_result: DeleteSuccess) { + public updateWithDeleteSuccess(result: DeleteSuccess) { + super.updateWithDeleteSuccess(result); this.binaryData = undefined; } diff --git a/packages/solid/src/resource/Resource.ts b/packages/solid/src/resource/Resource.ts index ef36e9d..881c065 100644 --- a/packages/solid/src/resource/Resource.ts +++ b/packages/solid/src/resource/Resource.ts @@ -15,7 +15,10 @@ import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import { getParentUri } from "../util/rdfUtils"; import type { RequesterResult } from "../requester/results/RequesterResult"; -import type { DeleteResult } from "../requester/requests/deleteResource"; +import { + updateDatasetOnSuccessfulDelete, + type DeleteResult, +} from "../requester/requests/deleteResource"; import type { ReadSuccess } from "../requester/results/success/ReadSuccess"; import { isReadSuccess } from "../requester/results/success/ReadSuccess"; import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; @@ -427,7 +430,7 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ * A helper method updates this resource's internal state upon delete success * @param result - the result of the delete success */ - protected updateWithDeleteSuccess(_result: DeleteSuccess) { + public updateWithDeleteSuccess(_result: DeleteSuccess) { this.absent = true; this.didInitialFetch = true; } @@ -743,9 +746,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ * TODO */ protected async onNotification(message: NotificationMessage): Promise { + const objectResource = this.context.solidLdoDataset.getResource( + message.object, + ); switch (message.type) { case "Update": - await this.read(); + case "Add": + await objectResource.read(); + return; + case "Delete": + case "Remove": + // Delete the resource without have to make an additional read request + updateDatasetOnSuccessfulDelete(message.object, true, { + dataset: this.context.solidLdoDataset, + }); + objectResource.updateWithDeleteSuccess({ + type: "deleteSuccess", + isError: false, + uri: message.object, + resourceExisted: true, + }); + return; } } diff --git a/packages/solid/src/resource/notifications/NotificationMessage.ts b/packages/solid/src/resource/notifications/NotificationMessage.ts index 34a8042..7e78153 100644 --- a/packages/solid/src/resource/notifications/NotificationMessage.ts +++ b/packages/solid/src/resource/notifications/NotificationMessage.ts @@ -1,7 +1,7 @@ export interface NotificationMessage { "@context": string | string[]; id: string; - type: "Update"; + type: "Update" | "Delete" | "Remove" | "Add"; object: string; published: string; } diff --git a/packages/solid/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index 1c94822..1b03fac 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -2022,15 +2022,18 @@ describe("Integration", () => { * =========================================================================== */ describe("Notification Subscriptions", () => { - it("Notification is propogated when a resource is updated", async () => { - const spidermanNode = namedNode("http://example.org/#spiderman"); - const foafNameNode = namedNode("http://xmlns.com/foaf/0.1/name"); + const spidermanNode = namedNode("http://example.org/#spiderman"); + const foafNameNode = namedNode("http://xmlns.com/foaf/0.1/name"); + it("handles notification when a resource is updated", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.read(); const spidermanCallback = jest.fn(); - solidLdoDataset.addListener([spidermanNode, null, null, null], jest.fn()); + solidLdoDataset.addListener( + [spidermanNode, null, null, null], + spidermanCallback, + ); const subscriptionResult = await resource.subscribeToNotifications(); expect(subscriptionResult.type).toBe("subscribeToNotificationSuccess"); @@ -2053,29 +2056,140 @@ describe("Integration", () => { ).toBe(1); expect(spidermanCallback).toHaveBeenCalledTimes(1); - // // Notification is not propogated after unsubscribe - // spidermanCallback.mockClear(); - // const unsubscribeResponse = await resource.unsubscribeFromNotifications(); - // expect(unsubscribeResponse.type).toBe( - // "unsubscribeFromNotificationSuccess", - // ); - // await authFetch(SAMPLE_DATA_URI, { - // method: "PATCH", - // body: 'INSERT DATA { "Miles Morales" . }', - // headers: { - // "Content-Type": "application/sparql-update", - // }, - // }); - // await wait(50); - - // expect(spidermanCallback).not.toHaveBeenCalled(); - // expect( - // solidLdoDataset.match( - // spidermanNode, - // foafNameNode, - // literal("Miles Morales"), - // ).size, - // ).toBe(0); + // Notification is not propogated after unsubscribe + spidermanCallback.mockClear(); + const unsubscribeResponse = await resource.unsubscribeFromNotifications(); + expect(unsubscribeResponse.type).toBe( + "unsubscribeFromNotificationSuccess", + ); + await authFetch(SAMPLE_DATA_URI, { + method: "PATCH", + body: 'INSERT DATA { "Miles Morales" . }', + headers: { + "Content-Type": "application/sparql-update", + }, + }); + await wait(50); + + expect(spidermanCallback).not.toHaveBeenCalled(); + expect( + solidLdoDataset.match( + spidermanNode, + foafNameNode, + literal("Miles Morales"), + ).size, + ).toBe(0); + + await resource.unsubscribeFromNotifications(); + }); + + it("handles notification when subscribed to a child that is deleted", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); + await resource.read(); + + const spidermanCallback = jest.fn(); + solidLdoDataset.addListener( + [spidermanNode, null, null, null], + spidermanCallback, + ); + + const containerCallback = jest.fn(); + solidLdoDataset.addListener( + [namedNode(TEST_CONTAINER_URI), null, null, null], + containerCallback, + ); + + const subscriptionResult = await resource.subscribeToNotifications(); + expect(subscriptionResult.type).toBe("subscribeToNotificationSuccess"); + + await authFetch(SAMPLE_DATA_URI, { + method: "DELETE", + }); + await wait(1000); + + expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(0); + expect( + testContainer.children().some((child) => child.uri === SAMPLE_DATA_URI), + ).toBe(false); + expect(spidermanCallback).toHaveBeenCalledTimes(1); + expect(containerCallback).toHaveBeenCalledTimes(1); + + await resource.unsubscribeFromNotifications(); + }); + + it("handles notification when subscribed to a parent with a deleted child", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); + await resource.read(); + + const spidermanCallback = jest.fn(); + solidLdoDataset.addListener( + [spidermanNode, null, null, null], + spidermanCallback, + ); + + const containerCallback = jest.fn(); + solidLdoDataset.addListener( + [namedNode(TEST_CONTAINER_URI), null, null, null], + containerCallback, + ); + + const subscriptionResult = await testContainer.subscribeToNotifications(); + expect(subscriptionResult.type).toBe("subscribeToNotificationSuccess"); + + await authFetch(SAMPLE_DATA_URI, { + method: "DELETE", + }); + await wait(1000); + + expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(0); + expect( + testContainer.children().some((child) => child.uri === SAMPLE_DATA_URI), + ).toBe(false); + expect(spidermanCallback).toHaveBeenCalledTimes(1); + expect(containerCallback).toHaveBeenCalledTimes(1); + + await testContainer.unsubscribeFromNotifications(); + }); + + it("handles notification when subscribed to a parent with an added child", async () => { + const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); + const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); + await resource.read(); + + const spidermanCallback = jest.fn(); + solidLdoDataset.addListener( + [spidermanNode, null, null, null], + spidermanCallback, + ); + + const containerCallback = jest.fn(); + solidLdoDataset.addListener( + [namedNode(TEST_CONTAINER_URI), null, null, null], + containerCallback, + ); + + const subscriptionResult = await testContainer.subscribeToNotifications(); + expect(subscriptionResult.type).toBe("subscribeToNotificationSuccess"); + + await authFetch(TEST_CONTAINER_URI, { + method: "POST", + headers: { "content-type": "text/turtle", slug: "sample2.ttl" }, + body: SPIDER_MAN_TTL, + }); + await wait(1000); + + expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(4); + expect( + testContainer + .children() + .some((child) => child.uri === SAMPLE2_DATA_URI), + ).toBe(true); + expect(spidermanCallback).toHaveBeenCalledTimes(1); + expect(containerCallback).toHaveBeenCalledTimes(1); + + await testContainer.unsubscribeFromNotifications(); }); }); });