Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion packages/extension-chrome/src/lib/apply-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export async function applyRemoteChanges(
}

if (existingNode != null) {
// Already in the local tree — assume in sync; next reconcile fixes drift.
// Remote bookmark already in the local tree — propagate title/url
// changes so users on Device A see edits from Device B within the
// 5-minute poll window (issue #1).
await applyRemoteEdit(existingNode, bm.url, bm.title);
continue;
}

Expand All @@ -63,6 +66,34 @@ export async function applyRemoteChanges(
}
}

async function applyRemoteEdit(
nodeId: string,
remoteUrl: string,
remoteTitle: string,
): Promise<void> {
let current: chrome.bookmarks.BookmarkTreeNode | undefined;
try {
const found = await chrome.bookmarks.get(nodeId);
current = found[0];
} catch {
// Node may have been deleted locally between mapping and apply; skip.
return;
}
if (current == null) return;

const changes: { title?: string; url?: string } = {};
if (current.title !== remoteTitle) changes.title = remoteTitle;
if (current.url !== remoteUrl) changes.url = remoteUrl;
if (Object.keys(changes).length === 0) return;

// Suppress both the old URL (in case it's being changed away) and the new
// URL — the resulting onChanged echo will report the new URL.
if (current.url != null && current.url.length > 0) suppress(current.url);
suppress(remoteUrl);

await chrome.bookmarks.update(nodeId, changes);
}

async function ensureFolderPath(
folder: string,
bookmarksBarId: string,
Expand Down
63 changes: 63 additions & 0 deletions packages/extension-chrome/test/apply-remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,69 @@ describe("applyRemoteChanges", () => {
expect(isSuppressed("https://example.com/new")).toBe(true);
});

it("propagates a remote title change to the local node (issue #1)", async () => {
const bm = bookmark({
id: "u1",
url: "https://example.com/",
title: "New title from another device",
});
const idMap = await IdMap.load();
idMap.set(asUlid("u1"), asNodeId("node-1"));

// Current local node has the old title
(chrome.bookmarks.get as any).mockResolvedValueOnce([
{ id: "node-1", parentId: BAR, title: "Old title", url: "https://example.com/" },
]);

await applyRemoteChanges(file([bm]), idMap, BAR, OTHER);

expect(chrome.bookmarks.update).toHaveBeenCalledWith("node-1", {
title: "New title from another device",
});
// The URL is unchanged, but we still suppress to avoid an onChanged echo
expect(isSuppressed("https://example.com/")).toBe(true);
});

it("propagates a remote URL change to the local node (issue #1)", async () => {
const bm = bookmark({
id: "u1",
url: "https://example.com/new-path",
title: "Same title",
});
const idMap = await IdMap.load();
idMap.set(asUlid("u1"), asNodeId("node-1"));

(chrome.bookmarks.get as any).mockResolvedValueOnce([
{ id: "node-1", parentId: BAR, title: "Same title", url: "https://example.com/old-path" },
]);

await applyRemoteChanges(file([bm]), idMap, BAR, OTHER);

expect(chrome.bookmarks.update).toHaveBeenCalledWith("node-1", {
url: "https://example.com/new-path",
});
// Suppress both old and new URL — the onChanged echo will fire with the new URL
expect(isSuppressed("https://example.com/new-path")).toBe(true);
});

it("skips the update call when remote matches local (no spurious onChanged)", async () => {
const bm = bookmark({
id: "u1",
url: "https://example.com/",
title: "Same",
});
const idMap = await IdMap.load();
idMap.set(asUlid("u1"), asNodeId("node-1"));

(chrome.bookmarks.get as any).mockResolvedValueOnce([
{ id: "node-1", parentId: BAR, title: "Same", url: "https://example.com/" },
]);

await applyRemoteChanges(file([bm]), idMap, BAR, OTHER);

expect(chrome.bookmarks.update).not.toHaveBeenCalled();
});

it("does not create a bookmark already mapped", async () => {
const bm = bookmark({ id: "u1", url: "https://example.com/" });
const idMap = await IdMap.load();
Expand Down
1 change: 1 addition & 0 deletions packages/extension-chrome/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const chromeStub = {
update: vi.fn(async () => ({} as chrome.bookmarks.BookmarkTreeNode)),
move: vi.fn(async () => ({} as chrome.bookmarks.BookmarkTreeNode)),
remove: vi.fn(async () => {}),
get: vi.fn(async () => [] as chrome.bookmarks.BookmarkTreeNode[]),
getTree: vi.fn(async () => [] as chrome.bookmarks.BookmarkTreeNode[]),
getSubTree: vi.fn(async () => [] as chrome.bookmarks.BookmarkTreeNode[]),
onCreated: { addListener: vi.fn(), removeListener: vi.fn() },
Expand Down
Loading