Make remote and log resource ownership explicit

This commit is contained in:
vorotamoroz committed 2026-10-29 14:56:06 +00:00
1 parent db95f420b9
commit 28bc6e87a1
17 files changed
+987 -71

No files matched your search

+7 -10
View File
@@ -9,6 +9,7 @@ import {
import { REMOTE_RESOURCE_KINDS, type JoinedDevicesSnapshot } from "@vrtmrz/livesync-commonlib/replication";
import type { ObsidianNoticeGroups } from "@/modules/services/ObsidianNoticeGroups";
import JoinedDevicesDialog from "@/modules/services/LiveSyncUI/dialogues/JoinedDevicesDialog.svelte";
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource";
import {
getCouchDBReplicatorConfigurationIdentity,
getObjectStorageReplicatorConfigurationIdentity,
@@ -119,9 +120,9 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi
settings
);
if (!resource) throw new Error("Joined devices are unavailable");
try {
return withOwnedRemoteResource(resource, async (ownedResource) => {
assertCurrent();
const snapshot = await resource.read();
const snapshot = await ownedResource.read();
assertCurrent();
notify(snapshot, id, identity!);
return joinedDevicesView(snapshot, {
@@ -130,9 +131,7 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi
pluginVersion: services.API.getPluginVersion(),
appVersion: services.API.getAppVersion(),
});
} finally {
await resource.dispose();
}
});
},
forget: async (id) => {
if (id === (await ownId())) throw new Error("This device cannot be forgotten");
@@ -141,14 +140,12 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi
settings
);
if (!resource) throw new Error("Joined devices are unavailable");
try {
return withOwnedRemoteResource(resource, async (ownedResource) => {
assertCurrent();
const result = await resource.forget(id);
const result = await ownedResource.forget(id);
assertCurrent();
return result;
} finally {
await resource.dispose();
}
});
},
cancel: () => controller.abort(),
};
@@ -110,6 +110,67 @@ describe("joined devices service feature", () => {
expect(await pending).toEqual({ status: "handled", value: "closed" });
await expect(options.refresh()).rejects.toThrow();
});
it("propagates a failed refresh after releasing its resource", async () => {
const data = fixture();
const failure = new Error("joined devices read failed");
data.read.mockRejectedValueOnce(failure);
const { options, pending } = await open(data);
await expect(options.refresh()).rejects.toBe(failure);
expect(data.dispose).toHaveBeenCalledOnce();
data.close();
await pending;
});
it.each(["refresh", "forget"] as const)(
"waits for resource disposal after %s",
async (operationName) => {
const data = fixture();
let finishDisposal!: () => void;
data.dispose.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
finishDisposal = resolve;
})
);
const { options, pending } = await open(data);
const operation = operationName === "refresh" ? options.refresh() : options.forget("other");
let settled = false;
void operation.then(
() => {
settled = true;
},
() => {
settled = true;
}
);
await vi.waitFor(() => expect(data.dispose).toHaveBeenCalledOnce());
expect(settled).toBe(false);
finishDisposal();
await expect(operation).resolves.toBeDefined();
expect(settled).toBe(true);
data.close();
await pending;
}
);
it.each(["refresh", "forget"] as const)(
"uses disposal failure precedence when %s also fails",
async (operationName) => {
const data = fixture();
const operationFailure = new Error("joined devices operation failed");
const disposalFailure = new Error("joined devices disposal failed");
if (operationName === "refresh") data.read.mockRejectedValueOnce(operationFailure);
else data.forget.mockRejectedValueOnce(operationFailure);
data.dispose.mockRejectedValueOnce(disposalFailure);
const { options, pending } = await open(data);
const operation = operationName === "refresh" ? options.refresh() : options.forget("other");
await expect(operation).rejects.toBe(disposalFailure);
expect(data.dispose).toHaveBeenCalledOnce();
data.close();
await pending;
}
);
it("prevents self removal and fences an old dialogue after a remote switch", async () => {
const data = fixture();
const { options, pending } = await open(data);