Skip to content

Commit 67f9604

Browse files
authored
ENG-1188: prod: pull watchers for prop based settings (#682)
* pull watchers for prop settings * address review * address review * address devin review from accessors pr
1 parent a35a194 commit 67f9604

4 files changed

Lines changed: 293 additions & 6 deletions

File tree

apps/roam/src/components/settings/utils/accessors.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,18 +470,19 @@ export const setDiscourseNodeSetting = (
470470
}
471471

472472
let pageUid = nodeType;
473-
const blockProps = getBlockPropsByUid(pageUid, []);
473+
let blockProps = getBlockPropsByUid(pageUid, []);
474474

475475
if (!blockProps || Object.keys(blockProps).length === 0) {
476476
const lookedUpUid = getPageUidByPageTitle(
477477
`${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`,
478478
);
479479
if (lookedUpUid) {
480480
pageUid = lookedUpUid;
481+
blockProps = getBlockPropsByUid(pageUid, []);
481482
}
482483
}
483484

484-
if (!pageUid) {
485+
if (!blockProps || Object.keys(blockProps).length === 0) {
485486
internalError({
486487
error: `setDiscourseNodeSetting - could not find page for: ${nodeType}`,
487488
type: "DG Accessor",
@@ -534,4 +535,4 @@ export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => {
534535
}
535536

536537
return nodes;
537-
};
538+
};

apps/roam/src/components/settings/utils/init.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,12 @@ const initSingleDiscourseNode = async (
128128

129129
const initDiscourseNodePages = async (): Promise<Record<string, string>> => {
130130
if (hasNonDefaultNodes()) {
131-
return {};
131+
const existingNodes = getAllDiscourseNodes();
132+
const nodePageUids: Record<string, string> = {};
133+
for (const node of existingNodes) {
134+
nodePageUids[node.text] = node.type;
135+
}
136+
return nodePageUids;
132137
}
133138

134139
const results = await Promise.all(
@@ -153,6 +158,5 @@ export type InitSchemaResult = {
153158
export const initSchema = async (): Promise<InitSchemaResult> => {
154159
const blockUids = await initSettingsPageBlocks();
155160
const nodePageUids = await initDiscourseNodePages();
156-
157161
return { blockUids, nodePageUids };
158162
};
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import { type json, normalizeProps } from "~/utils/getBlockProps";
2+
import type { AddPullWatch, PullBlock } from "roamjs-components/types";
3+
import {
4+
TOP_LEVEL_BLOCK_PROP_KEYS,
5+
getPersonalSettingsKey,
6+
FeatureFlagsSchema,
7+
GlobalSettingsSchema,
8+
PersonalSettingsSchema,
9+
DiscourseNodeSchema,
10+
type FeatureFlags,
11+
type GlobalSettings,
12+
type PersonalSettings,
13+
type DiscourseNodeSettings,
14+
} from "./zodSchema";
15+
16+
type PullWatchCallback = Parameters<AddPullWatch>[2];
17+
18+
// Need assertions to bridge type defs between the (roamjs-components) and json type (getBlockProps.ts)
19+
const getNormalizedProps = (data: PullBlock | null): Record<string, json> => {
20+
return normalizeProps((data?.[":block/props"] || {}) as json) as Record<
21+
string,
22+
json
23+
>;
24+
};
25+
26+
const hasPropChanged = (
27+
before: PullBlock | null,
28+
after: PullBlock | null,
29+
key?: string,
30+
): boolean => {
31+
const beforeProps = getNormalizedProps(before);
32+
const afterProps = getNormalizedProps(after);
33+
34+
if (key) {
35+
return JSON.stringify(beforeProps[key]) !== JSON.stringify(afterProps[key]);
36+
}
37+
38+
return JSON.stringify(beforeProps) !== JSON.stringify(afterProps);
39+
};
40+
41+
const createCleanupFn = (watches: Parameters<AddPullWatch>[]): (() => void) => {
42+
return () => {
43+
watches.forEach(([pattern, entityId, callback]) => {
44+
window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback);
45+
});
46+
};
47+
};
48+
49+
const createSettingsWatchCallback = <T>(
50+
schema: { safeParse: (data: unknown) => { success: boolean; data?: T } },
51+
onSettingsChange: (context: {
52+
newSettings: T;
53+
oldSettings: T | null;
54+
before: PullBlock | null;
55+
after: PullBlock | null;
56+
}) => void,
57+
): PullWatchCallback => {
58+
return (before, after) => {
59+
const beforeProps = getNormalizedProps(before);
60+
const afterProps = getNormalizedProps(after);
61+
const beforeResult = schema.safeParse(beforeProps);
62+
const afterResult = schema.safeParse(afterProps);
63+
64+
if (!afterResult.success) return;
65+
66+
const oldSettings = beforeResult.success
67+
? (beforeResult.data ?? null)
68+
: null;
69+
const newSettings = afterResult.data as T;
70+
71+
onSettingsChange({ newSettings, oldSettings, before, after });
72+
};
73+
};
74+
75+
const addPullWatch = (
76+
watches: Parameters<AddPullWatch>[],
77+
blockUid: string,
78+
callback: PullWatchCallback,
79+
): void => {
80+
const pattern = "[:block/props]";
81+
const entityId = `[:block/uid "${blockUid}"]`;
82+
83+
window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback);
84+
watches.push([pattern, entityId, callback]);
85+
};
86+
87+
export const featureFlagHandlers: Partial<
88+
Record<
89+
keyof FeatureFlags,
90+
(newValue: boolean, oldValue: boolean, allFlags: FeatureFlags) => void
91+
>
92+
> = {
93+
// Add handlers as needed:
94+
// "Enable Left Sidebar": (newValue) => { ... },
95+
// "Suggestive Mode Enabled": (newValue) => { ... },
96+
// "Reified Relation Triples": (newValue) => { ... },
97+
};
98+
99+
type GlobalSettingsHandlers = {
100+
[K in keyof GlobalSettings]?: (
101+
newValue: GlobalSettings[K],
102+
oldValue: GlobalSettings[K],
103+
allSettings: GlobalSettings,
104+
) => void;
105+
};
106+
107+
export const globalSettingsHandlers: GlobalSettingsHandlers = {
108+
// Add handlers as needed:
109+
// "Trigger": (newValue) => { ... },
110+
// "Canvas Page Format": (newValue) => { ... },
111+
// "Left Sidebar": (newValue) => { ... },
112+
// "Export": (newValue) => { ... },
113+
// "Suggestive Mode": (newValue) => { ... },
114+
};
115+
116+
type PersonalSettingsHandlers = {
117+
[K in keyof PersonalSettings]?: (
118+
newValue: PersonalSettings[K],
119+
oldValue: PersonalSettings[K],
120+
allSettings: PersonalSettings,
121+
) => void;
122+
};
123+
124+
export const personalSettingsHandlers: PersonalSettingsHandlers = {
125+
// "Left Sidebar" stub for testing with stubSetLeftSidebarPersonalSections() in accessors.ts
126+
/* eslint-disable @typescript-eslint/naming-convention */
127+
"Left Sidebar": (newValue, oldValue) => {
128+
const oldSections = Object.keys(oldValue || {});
129+
const newSections = Object.keys(newValue || {});
130+
131+
if (newSections.length === 0 && oldSections.length === 0) return;
132+
133+
console.group("👤 [PullWatch] Personal Settings Changed: Left Sidebar");
134+
console.log("Old value:", JSON.stringify(oldValue, null, 2));
135+
console.log("New value:", JSON.stringify(newValue, null, 2));
136+
137+
const addedSections = newSections.filter((s) => !oldSections.includes(s));
138+
const removedSections = oldSections.filter((s) => !newSections.includes(s));
139+
140+
if (addedSections.length > 0) {
141+
console.log(" → Sections added:", addedSections);
142+
}
143+
if (removedSections.length > 0) {
144+
console.log(" → Sections removed:", removedSections);
145+
}
146+
console.groupEnd();
147+
},
148+
/* eslint-enable @typescript-eslint/naming-convention */
149+
};
150+
151+
export const discourseNodeHandlers: Array<
152+
(
153+
nodeType: string,
154+
newSettings: DiscourseNodeSettings,
155+
oldSettings: DiscourseNodeSettings | null,
156+
) => void
157+
> = [
158+
// Add handlers as needed:
159+
// (nodeType, newSettings, oldSettings) => { ... },
160+
];
161+
162+
export const setupPullWatchOnSettingsPage = (
163+
blockUids: Record<string, string>,
164+
): (() => void) => {
165+
const watches: Parameters<AddPullWatch>[] = [];
166+
167+
const featureFlagsBlockUid =
168+
blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags];
169+
const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global];
170+
const personalSettingsKey = getPersonalSettingsKey();
171+
const personalSettingsBlockUid = blockUids[personalSettingsKey];
172+
173+
if (featureFlagsBlockUid && Object.keys(featureFlagHandlers).length > 0) {
174+
addPullWatch(
175+
watches,
176+
featureFlagsBlockUid,
177+
createSettingsWatchCallback(
178+
FeatureFlagsSchema,
179+
({ newSettings, oldSettings, before, after }) => {
180+
for (const [key, handler] of Object.entries(featureFlagHandlers)) {
181+
const typedKey = key as keyof FeatureFlags;
182+
if (hasPropChanged(before, after, key) && handler) {
183+
handler(
184+
newSettings[typedKey],
185+
oldSettings?.[typedKey] ?? false,
186+
newSettings,
187+
);
188+
}
189+
}
190+
},
191+
),
192+
);
193+
}
194+
195+
if (
196+
globalSettingsBlockUid &&
197+
Object.keys(globalSettingsHandlers).length > 0
198+
) {
199+
addPullWatch(
200+
watches,
201+
globalSettingsBlockUid,
202+
createSettingsWatchCallback(
203+
GlobalSettingsSchema,
204+
({ newSettings, oldSettings, before, after }) => {
205+
for (const [key, handler] of Object.entries(globalSettingsHandlers)) {
206+
const typedKey = key as keyof GlobalSettings;
207+
if (hasPropChanged(before, after, key) && handler) {
208+
// Object.entries loses key-handler correlation, but data is Zod-validated
209+
(
210+
handler as (
211+
newValue: unknown,
212+
oldValue: unknown,
213+
allSettings: GlobalSettings,
214+
) => void
215+
)(newSettings[typedKey], oldSettings?.[typedKey], newSettings);
216+
}
217+
}
218+
},
219+
),
220+
);
221+
}
222+
223+
if (
224+
personalSettingsBlockUid &&
225+
Object.keys(personalSettingsHandlers).length > 0
226+
) {
227+
addPullWatch(
228+
watches,
229+
personalSettingsBlockUid,
230+
createSettingsWatchCallback(
231+
PersonalSettingsSchema,
232+
({ newSettings, oldSettings, before, after }) => {
233+
for (const [key, handler] of Object.entries(
234+
personalSettingsHandlers,
235+
)) {
236+
const typedKey = key as keyof PersonalSettings;
237+
if (hasPropChanged(before, after, key) && handler) {
238+
// Object.entries loses key-handler correlation, but data is Zod-validated
239+
(
240+
handler as (
241+
newValue: unknown,
242+
oldValue: unknown,
243+
allSettings: PersonalSettings,
244+
) => void
245+
)(newSettings[typedKey], oldSettings?.[typedKey], newSettings);
246+
}
247+
}
248+
},
249+
),
250+
);
251+
}
252+
253+
return createCleanupFn(watches);
254+
};
255+
256+
export const setupPullWatchDiscourseNodes = (
257+
nodePageUids: Record<string, string>,
258+
): (() => void) => {
259+
const watches: Parameters<AddPullWatch>[] = [];
260+
261+
if (discourseNodeHandlers.length === 0) {
262+
return () => {};
263+
}
264+
265+
Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => {
266+
addPullWatch(
267+
watches,
268+
pageUid,
269+
createSettingsWatchCallback(
270+
DiscourseNodeSchema,
271+
({ newSettings, oldSettings }) => {
272+
for (const handler of discourseNodeHandlers) {
273+
handler(nodeType, newSettings, oldSettings);
274+
}
275+
},
276+
),
277+
);
278+
});
279+
280+
return createCleanupFn(watches);
281+
};
282+
283+
export { getNormalizedProps, hasPropChanged };

apps/roam/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {
4141
STREAMLINE_STYLING_KEY,
4242
DISALLOW_DIAGNOSTICS,
4343
} from "./data/userSettings";
44-
import { initSchema } from "./components/settings/utils/init";
4544

4645
export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*";
4746

0 commit comments

Comments
 (0)