Skip to content
Merged
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
123 changes: 80 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import addStyle from "roamjs-components/dom/addStyle";
import { OnloadArgs } from "roamjs-components/types";
import runExtension from "roamjs-components/util/runExtension";
import { createBlock, createPage } from "roamjs-components/writes";

Expand All @@ -19,7 +20,10 @@ type StickyNoteMeta = {
};

type ReactLike = {
createElement: (component: unknown, props: Record<string, unknown>) => unknown;
createElement: (
component: unknown,
props: Record<string, unknown>,
) => unknown;
};

type ReactDomLike = {
Expand Down Expand Up @@ -60,33 +64,49 @@ const randomRotation = (): number =>

const normalizeLayout = (layout: StickyNoteLayout): StickyNoteLayout => ({
...layout,
rotation: Number.isFinite(layout.rotation) ? layout.rotation : randomRotation(),
rotation: Number.isFinite(layout.rotation)
? layout.rotation
: randomRotation(),
});

const getLayouts = (): StickyNoteLayouts => {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) {
const getLayouts = ({
extensionSettings,
}: {
extensionSettings: OnloadArgs["extensionAPI"]["settings"];
}): StickyNoteLayouts => {
const raw = extensionSettings.get(STORAGE_KEY);
if (typeof raw !== "string" || !raw) {
return {};
}
try {
const parsed = JSON.parse(raw) as StickyNoteLayouts;
return Object.fromEntries(
Object.entries(parsed).map(([uid, layout]) => [uid, normalizeLayout(layout)])
Object.entries(parsed).map(([uid, layout]) => [
uid,
normalizeLayout(layout),
]),
);
} catch {
return {};
}
};

const setLayouts = (layouts: StickyNoteLayouts): void => {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts));
const setLayouts = ({
extensionSettings,
layouts,
}: {
extensionSettings: OnloadArgs["extensionAPI"]["settings"];
layouts: StickyNoteLayouts;
}): void => {
extensionSettings.set(STORAGE_KEY, JSON.stringify(layouts));
};
Comment on lines +100 to 102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unhandled Promise from async extensionSettings.set in setLayouts

The setLayouts function calls extensionSettings.set() without awaiting or catching the returned Promise, causing silent failures and unhandled promise rejections.

Root Cause

The old code used synchronous localStorage.setItem which would throw synchronously on failure (e.g. quota exceeded), but the new code at src/index.ts:101 calls extensionSettings.set(STORAGE_KEY, JSON.stringify(layouts)) which returns Promise<void> (per the type definition at node_modules/roamjs-components/types/native.d.ts:393: set: (k: string, v: unknown) => Promise<void>). Since the return type of setLayouts is void, this Promise is silently discarded.

This means:

  • If extensionSettings.set rejects, the error is completely lost—no logging, no user feedback.
  • This produces unhandled promise rejections in the runtime, which may be reported as errors in the console or could crash the extension in strict environments.
  • Every callsite that invokes persistLayouts() (lines 451, 482, 513, 558, 781, 805, 830) is affected since persistLayouts at line 751 delegates directly to setLayouts.

Impact: Layout changes (drag, resize, minimize, delete) may silently fail to persist without any indication to the user, and unhandled rejections are produced.

Suggested change
}): void => {
extensionSettings.set(STORAGE_KEY, JSON.stringify(layouts));
};
}): void => {
void extensionSettings.set(STORAGE_KEY, JSON.stringify(layouts)).catch((err) => {
console.error("[sticky-note] Failed to persist layouts", err);
});
};
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const getPageUid = (): string | null => {
const result = window.roamAlphaAPI.q(
`[:find ?uid :where [?p :node/title "${PAGE_TITLE}"] [?p :block/uid ?uid]]`
) as [string][];
return result.length ? result[0][0] : null;
const page = window.roamAlphaAPI.pull("[:block/uid]", [
":node/title",
PAGE_TITLE,
]) as { ":block/uid"?: string } | null;
return page?.[":block/uid"] || null;
};

const ensurePageUid = async (): Promise<string> => {
Expand All @@ -104,11 +124,9 @@ const fetchStickyNoteUids = (): string[] => {
[?p :node/title "${PAGE_TITLE}"]
[?p :block/children ?c]
[?c :block/uid ?uid]
[?c :block/order ?order]]`
[?c :block/order ?order]]`,
) as [string, number][];
return result
.sort((a, b) => a[1] - b[1])
.map((entry) => entry[0]);
return result.sort((a, b) => a[1] - b[1]).map((entry) => entry[0]);
};

const fetchBlockText = (uid: string): string => {
Expand All @@ -118,12 +136,14 @@ const fetchBlockText = (uid: string): string => {
:where
[?b :block/uid ?uid]
[(get-else $ ?b :block/string "") ?text]]`,
uid
uid,
) as [string][];
return result.length ? result[0][0] : "";
};

const ensureStickyNoteMeta = async (noteUid: string): Promise<StickyNoteMeta> => {
const ensureStickyNoteMeta = async (
noteUid: string,
): Promise<StickyNoteMeta> => {
const noteText = fetchBlockText(noteUid).trim();

if (noteText) {
Expand Down Expand Up @@ -176,7 +196,7 @@ const mountRoamBlock = ({
const defaultLayout = (
index: number,
viewportWidth: number,
viewportHeight: number
viewportHeight: number,
): StickyNoteLayout => {
const width = 240;
const height = 220;
Expand All @@ -193,10 +213,7 @@ const defaultLayout = (
};
};

const applyLayout = (
note: HTMLElement,
layout: StickyNoteLayout
): void => {
const applyLayout = (note: HTMLElement, layout: StickyNoteLayout): void => {
note.style.left = `${layout.x}px`;
note.style.top = `${layout.y}px`;
note.style.width = `${layout.width}px`;
Expand All @@ -208,7 +225,7 @@ const applyLayout = (
const mutateLayout = (
layouts: StickyNoteLayouts,
uid: string,
next: Partial<StickyNoteLayout>
next: Partial<StickyNoteLayout>,
): void => {
const current = layouts[uid];
layouts[uid] = {
Expand All @@ -225,7 +242,7 @@ const getStickyRenderedIdFromUid = ({
root?: ParentNode;
}): string | null => {
const el = root.querySelector(
`.roamjs-sticky-note__embedded-root [id^="block-input-"][id$="-${uid}"]`
`.roamjs-sticky-note__embedded-root [id^="block-input-"][id$="-${uid}"]`,
) as HTMLElement | null;
return el?.id || null;
};
Expand Down Expand Up @@ -296,13 +313,17 @@ const createStickyNoteElement = ({
uid,
layout,
layouts,
persistLayouts,
readLayoutsSetting,
meta,
resizeObservers,
blockUnmounts,
}: {
uid: string;
layout: StickyNoteLayout;
layouts: StickyNoteLayouts;
persistLayouts: () => void;
readLayoutsSetting: () => unknown;
meta: StickyNoteMeta;
resizeObservers: Set<ResizeObserver>;
blockUnmounts: Set<() => void>;
Expand Down Expand Up @@ -341,10 +362,11 @@ const createStickyNoteElement = ({

const minimizeButton = document.createElement("button");
minimizeButton.type = "button";
minimizeButton.className = "bp3-button bp3-minimal roamjs-sticky-note__button";
minimizeButton.className =
"bp3-button bp3-minimal roamjs-sticky-note__button";
minimizeButton.setAttribute(
"aria-label",
layout.minimized ? "Expand sticky note" : "Minimize sticky note"
layout.minimized ? "Expand sticky note" : "Minimize sticky note",
);
minimizeButton.textContent = layout.minimized ? "▢" : "–";

Expand All @@ -371,7 +393,7 @@ const createStickyNoteElement = ({
const unmountBlock = mountRoamBlock({ uid, el: blockContainer, open: true });
const hideEmbeddedRootTitle = (): void => {
const rootMain = blockContainer.querySelector(
".rm-level-0 > .rm-block-main, .rm-block-main"
".rm-level-0 > .rm-block-main, .rm-block-main",
) as HTMLElement | null;
if (rootMain) {
rootMain.style.display = "none";
Expand Down Expand Up @@ -426,7 +448,7 @@ const createStickyNoteElement = ({
}
resizePersistTimeout = window.setTimeout(() => {
resizePersistTimeout = null;
setLayouts(layouts);
persistLayouts();
}, 250);
};

Expand All @@ -439,18 +461,25 @@ const createStickyNoteElement = ({
note.style.left = `${x}px`;
note.style.top = `${y}px`;
mutateLayout(layouts, uid, { x, y });
scheduleLayoutPersistence();
};

const onPointerUp = (): void => {
const stopDragging = (): void => {
if (!isDragging) {
return;
}
isDragging = false;
note.classList.remove(NOTE_DRAGGING_CLASS);
document.body.style.userSelect = previousBodyUserSelect;
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
setLayouts(layouts);
document.removeEventListener("pointerup", stopDragging);
document.removeEventListener("pointercancel", stopDragging);
window.removeEventListener("blur", stopDragging);
if (resizePersistTimeout) {
window.clearTimeout(resizePersistTimeout);
resizePersistTimeout = null;
}
persistLayouts();
};

const onPointerDown = (event: PointerEvent): void => {
Expand All @@ -465,7 +494,9 @@ const createStickyNoteElement = ({
previousBodyUserSelect = document.body.style.userSelect;
document.body.style.userSelect = "none";
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
document.addEventListener("pointerup", stopDragging);
document.addEventListener("pointercancel", stopDragging);
window.addEventListener("blur", stopDragging);
};

header.addEventListener("pointerdown", onPointerDown);
Expand All @@ -476,10 +507,10 @@ const createStickyNoteElement = ({
minimizeButton.textContent = nextMinimized ? "▢" : "–";
minimizeButton.setAttribute(
"aria-label",
nextMinimized ? "Expand sticky note" : "Minimize sticky note"
nextMinimized ? "Expand sticky note" : "Minimize sticky note",
);
mutateLayout(layouts, uid, { minimized: nextMinimized });
setLayouts(layouts);
persistLayouts();
window.requestAnimationFrame(() => {
const activeElement = document.activeElement as HTMLElement | null;
activeElement?.blur();
Expand Down Expand Up @@ -524,7 +555,7 @@ const createStickyNoteElement = ({
blockUnmounts.delete(cleanupEmbeddedBlock);
note.remove();
delete layouts[uid];
setLayouts(layouts);
persistLayouts();
});

return note;
Expand Down Expand Up @@ -701,7 +732,7 @@ export default runExtension(async ({ extensionAPI }) => {
display: none;
}
`,
"roamjs-sticky-note-style"
"roamjs-sticky-note-style",
);

const container = document.createElement("div");
Expand All @@ -715,7 +746,10 @@ export default runExtension(async ({ extensionAPI }) => {
container.style.zIndex = "19";
document.body.append(container);

const layouts = getLayouts();
const extensionSettings = extensionAPI.settings;
const layouts = getLayouts({ extensionSettings });
const persistLayouts = (): void => setLayouts({ extensionSettings, layouts });
const readLayoutsSetting = (): unknown => extensionSettings.get(STORAGE_KEY);
const resizeObservers = new Set<ResizeObserver>();
const blockUnmounts = new Set<() => void>();
await ensurePageUid();
Expand All @@ -731,6 +765,8 @@ export default runExtension(async ({ extensionAPI }) => {
uid,
layout,
layouts,
persistLayouts,
readLayoutsSetting,
meta,
resizeObservers,
blockUnmounts,
Expand All @@ -742,7 +778,7 @@ export default runExtension(async ({ extensionAPI }) => {
delete layouts[key];
}
}
setLayouts(layouts);
persistLayouts();

const createStickyNote = async (): Promise<void> => {
const pageUid = await ensurePageUid();
Expand All @@ -763,14 +799,16 @@ export default runExtension(async ({ extensionAPI }) => {
const layout = defaultLayout(
Object.keys(layouts).length,
window.innerWidth,
window.innerHeight
window.innerHeight,
);
layouts[uid] = layout;
setLayouts(layouts);
persistLayouts();
const note = createStickyNoteElement({
uid,
layout,
layouts,
persistLayouts,
readLayoutsSetting,
meta: { titleUid: uid, titleText: "Sticky Note" },
resizeObservers,
blockUnmounts,
Expand All @@ -789,9 +827,8 @@ export default runExtension(async ({ extensionAPI }) => {
});
}
delete layouts[uid];
setLayouts(layouts);
persistLayouts();
}
console.error("[sticky-note] Failed to create sticky note", error);
throw error;
}
};
Expand Down