Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/roam/src/components/settings/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ const FeatureFlagsTab = (): React.ReactElement => {

<FeatureFlagPanel
title="Use new settings store"
description="When enabled, accessor getters read from block props instead of the old system. Surfaces dual-write gaps during development."
description="Enabled by default. Disable temporarily to read from legacy settings while rollback support remains available."
featureKey="Use new settings store"
/>

Expand Down
8 changes: 6 additions & 2 deletions apps/roam/src/components/settings/utils/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
migrateGraphLevel,
migratePersonalSettings,
} from "./migrateLegacyToBlockProps";
import { migratePropsStoreDefault } from "./migratePropsStoreDefault";
import { getTopLevelBlockPropsConfig } from "~/components/settings/utils/zodSchema";
import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE } from "./zodSchema";
import toFlexRegex from "roamjs-components/util/toFlexRegex";
Expand Down Expand Up @@ -376,8 +377,11 @@ export const initSchema = async (): Promise<InitSchemaResult> => {
refreshConfigTree();
}

await migrateGraphLevel(blockUids);
await migratePersonalSettings(blockUids);
const graphSettingsMigrated = await migrateGraphLevel(blockUids);
const personalSettingsMigrated = await migratePersonalSettings(blockUids);
if (graphSettingsMigrated && personalSettingsMigrated) {
await migratePropsStoreDefault(blockUids);
Comment thread
mdroidian marked this conversation as resolved.
Comment thread
mdroidian marked this conversation as resolved.
Comment on lines +382 to +383

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate the shared flag on each user's migration

In a shared graph, once the first user reaches this call, Use new settings store becomes true in the graph-wide Feature Flags block, but every collaborator has a different personal block and migration marker. On another user's next load, bulkReadSettings() runs before initSchema() and therefore reads defaults from that user's not-yet-migrated personal block; for example, a legacy Disable product diagnostics opt-out becomes false and initPostHog() is called before this migration can repair the block. Unlike the earlier validation case, this occurs even when the second user's legacy data is valid, because this conditional cannot make an already-enabled graph flag wait for that user; the read gate needs per-user migration readiness.

Useful? React with 👍 / 👎.

}
(window as unknown as Record<string, unknown>).dgDualReadLog =
logDualReadComparison;
return { blockUids, nodePageUids: {} };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import getBlockProps from "~/utils/getBlockProps";
import type { json } from "~/utils/getBlockProps";
import setBlockProps from "~/utils/setBlockProps";
import { setBlockPropsAsync } from "~/utils/setBlockProps";
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import { createBlock } from "roamjs-components/writes";
import { getSetting, setSetting } from "~/utils/extensionSettings";
Expand All @@ -26,8 +26,8 @@ import type { z } from "zod";
import { invalidateDiscourseNodeTypeCaches } from "~/utils/discourseNodeTypeCache";

const LOG_PREFIX = "[DG BlockProps Migration]";
const GRAPH_MIGRATION_MARKER = "Block props migrated";
const PERSONAL_MIGRATION_MARKER = "dg-personal-settings-migrated";
const GRAPH_MIGRATION_MARKER = "Block props migrated v2";
const PERSONAL_MIGRATION_MARKER = "dg-personal-settings-migrated-v2";
const MAX_ERROR_CONTEXT_LENGTH = 5000;

const hasGraphMigrationMarker = (blockMap: Record<string, string>): boolean =>
Expand Down Expand Up @@ -59,7 +59,7 @@ const shouldWrite = (
return JSON.stringify(parsedLegacy) !== JSON.stringify(currentProps);
};

const migrateSection = ({
const migrateSection = async ({
label,
blockUid,
schema,
Expand All @@ -71,17 +71,11 @@ const migrateSection = ({
schema: z.ZodTypeAny;
legacyData: Record<string, unknown>;
onWrite?: () => void;
}): boolean => {
}): Promise<boolean> => {
const currentProps = getBlockProps(blockUid);

const parseResult = schema.safeParse(legacyData);
if (!parseResult.success) {
if (isPropsValid(schema, currentProps)) {
console.log(
`${LOG_PREFIX} ${label}: legacy malformed but props already valid, skipping`,
);
return true;
}
console.warn(`${LOG_PREFIX} ${label}: Zod validation failed, skipping`, {
error: parseResult.error.message,
});
Expand All @@ -105,10 +99,26 @@ const migrateSection = ({
return true;
}

setBlockProps(blockUid, parsedLegacy, false);
onWrite?.();
console.log(`${LOG_PREFIX} ${label}: migrated`);
return true;
try {
await setBlockPropsAsync(blockUid, parsedLegacy, false);
onWrite?.();
console.log(`${LOG_PREFIX} ${label}: migrated`);
return true;
} catch (error) {
console.warn(`${LOG_PREFIX} ${label}: write failed, skipping`, error);
internalError({
error,
type: "DG Block Props Migration",
context: {
label,
blockUid,
legacyData: serializeErrorContext(legacyData),
currentProps: serializeErrorContext(currentProps),
},
sendEmail: false,
});
return false;
}
};

const migrateDiscourseNodes = async (): Promise<boolean> => {
Expand Down Expand Up @@ -155,13 +165,13 @@ const migrateDiscourseNodes = async (): Promise<boolean> => {
}

if (
!migrateSection({
!(await migrateSection({
label: `Discourse Node (${nodeText})`,
blockUid: nodePageUid,
schema: DiscourseNodeSchema,
legacyData,
onWrite: invalidateDiscourseNodeTypeCaches,
})
}))
) {
allOk = false;
}
Expand All @@ -172,7 +182,7 @@ const migrateDiscourseNodes = async (): Promise<boolean> => {

export const migrateGraphLevel = async (
blockUids: Record<string, string>,
): Promise<void> => {
): Promise<boolean> => {
const pageUid = getPageUidByPageTitle(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE);
if (!pageUid) {
internalError({
Expand All @@ -181,12 +191,12 @@ export const migrateGraphLevel = async (
context: { scope: "graph" },
sendEmail: false,
});
return;
return false;
}

if (hasGraphMigrationMarker(blockUids)) {
console.log(`${LOG_PREFIX} graph-level: skipped (already migrated)`);
return;
return true;
}

let failures = 0;
Expand Down Expand Up @@ -215,12 +225,12 @@ export const migrateGraphLevel = async (
}
}
if (
!migrateSection({
!(await migrateSection({
label: "Feature Flags",
blockUid: featureFlagUid,
schema: FeatureFlagsSchema,
legacyData: mergedFlags,
})
}))
) {
failures++;
}
Expand All @@ -241,12 +251,12 @@ export const migrateGraphLevel = async (
} else {
const legacyGlobal = readAllLegacyGlobalSettings();
if (
!migrateSection({
!(await migrateSection({
label: "Global",
blockUid: globalUid,
schema: GlobalSettingsSchema,
legacyData: legacyGlobal,
})
}))
) {
failures++;
}
Expand All @@ -263,25 +273,28 @@ export const migrateGraphLevel = async (
node: { text: GRAPH_MIGRATION_MARKER },
});
console.log(`${LOG_PREFIX} graph-level: completed`);
return true;
} catch (e) {
console.warn(
`${LOG_PREFIX} graph-level: data migrated but marker write failed (will retry next load)`,
e,
);
return false;
}
} else {
console.warn(
`${LOG_PREFIX} graph-level: ${failures} section(s) failed, marker not created (will retry next load)`,
);
}

console.warn(
`${LOG_PREFIX} graph-level: ${failures} section(s) failed, marker not created (will retry next load)`,
);
return false;
};

export const migratePersonalSettings = async (
blockUids: Record<string, string>,
): Promise<void> => {
): Promise<boolean> => {
if (getSetting<boolean>(PERSONAL_MIGRATION_MARKER, false)) {
console.log(`${LOG_PREFIX} personal: skipped (already migrated)`);
return;
return true;
}

const personalKey = getPersonalSettingsKey();
Expand All @@ -300,11 +313,11 @@ export const migratePersonalSettings = async (
},
sendEmail: false,
});
return;
return false;
}

const legacyPersonal = readAllLegacyPersonalSettings();
const ok = migrateSection({
const ok = await migrateSection({
label: "Personal",
blockUid: personalUid,
schema: PersonalSettingsSchema,
Expand All @@ -315,15 +328,18 @@ export const migratePersonalSettings = async (
try {
await setSetting(PERSONAL_MIGRATION_MARKER, true);
console.log(`${LOG_PREFIX} personal: completed`);
return true;
} catch (e) {
console.warn(
`${LOG_PREFIX} personal: data migrated but marker write failed (will retry next load)`,
e,
);
return false;
}
} else {
console.warn(
`${LOG_PREFIX} personal: failed, marker not created (will retry next load)`,
);
}

console.warn(
`${LOG_PREFIX} personal: failed, marker not created (will retry next load)`,
);
return false;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import getBlockProps from "~/utils/getBlockProps";
import { setBlockPropsAsync } from "~/utils/setBlockProps";
import internalError from "~/utils/internalError";
import { invalidateDiscourseNodeTypeCaches } from "~/utils/discourseNodeTypeCache";
import { FEATURE_FLAG_KEYS } from "./settingKeys";
import { STATIC_TOP_LEVEL_ENTRIES } from "./zodSchema";

const LOG_PREFIX = "[DG Props Default Migration]";
export const PROPS_STORE_DEFAULT_MIGRATION_KEY =
"Props settings default migrated";

export const migratePropsStoreDefault = async (
blockUids: Record<string, string>,
): Promise<void> => {
const featureFlagsUid = blockUids[STATIC_TOP_LEVEL_ENTRIES.featureFlags.key];

if (!featureFlagsUid) {
internalError({
error: "Cannot enable props-based settings by default",
type: "DG Props Default Migration",
context: { featureFlagsUid },
sendEmail: false,
});
return;
}

try {
const featureFlags = getBlockProps(featureFlagsUid);
if (featureFlags[PROPS_STORE_DEFAULT_MIGRATION_KEY] === true) {
console.log(`${LOG_PREFIX} skipped (already migrated)`);
return;
}

const propsStoreAlreadyEnabled =
featureFlags[FEATURE_FLAG_KEYS.useNewSettingsStore] === true;
await setBlockPropsAsync(
featureFlagsUid,
{
[PROPS_STORE_DEFAULT_MIGRATION_KEY]: true,
...(propsStoreAlreadyEnabled
? {}
: { [FEATURE_FLAG_KEYS.useNewSettingsStore]: true }),
},
false,
);

if (!propsStoreAlreadyEnabled) {
invalidateDiscourseNodeTypeCaches();
}
console.log(`${LOG_PREFIX} completed`);
} catch (error) {
internalError({
error,
type: "DG Props Default Migration",
context: { featureFlagsUid },
sendEmail: false,
});
}
};
Loading