Skip to content
Closed
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
6 changes: 6 additions & 0 deletions apps/ade-cli/src/services/sync/deviceRegistryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,12 @@ export function createDeviceRegistryService(args: DeviceRegistryServiceArgs) {
});
args.db.run("delete from sync_cluster_state");
args.db.run("delete from devices");
// Local DELETEs are CRR changesets; discard them so connectToBrain /
// disconnectFromBrain cannot broadcast mass device tombstones to the brain
// or paired peers.
if (args.db.sync.isAvailable?.()) {
args.db.sync.discardUnpublishedChangesForTables(["devices", "sync_cluster_state"]);
}
};

const forgetDevice = (deviceId: string): void => {
Expand Down
5 changes: 5 additions & 0 deletions apps/ade-cli/src/services/sync/syncPeerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ export function createSyncPeerService(args: SyncPeerServiceArgs) {
sendLocalChanges();
},

acknowledgeLocalDbVersion(): void {
pendingOutboundChangeset = null;
outboundLocalDbVersion = args.db.sync.getDbVersion();
},

async executeRemoteCommand(action: SyncRemoteCommandAction | (string & {}), commandArgs: Record<string, unknown>): Promise<unknown> {
if (!ws || ws.readyState !== WebSocket.OPEN) {
throw new Error("Not connected to a host device.");
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/services/sync/syncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ export function createSyncService(args: SyncServiceArgs) {
}
await stopHostIfRunning();
deviceRegistryService.clearClusterRegistryForViewerJoin();
syncPeerService.acknowledgeLocalDbVersion();
writeSavedDraft(draft);
syncPeerService.setSavedDraft(draft);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function createInMemoryAdeDb(): { db: AdeDb; raw: Database } {
touchedTables: [],
rebuiltFts: false,
}),
discardUnpublishedChangesForTables: () => {},
},
flushNow: () => undefined,
close: () => raw.close(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function createInMemoryAdeDb(): AdeDb {
getDbVersion: () => 0,
exportChangesSince: () => [],
applyChanges: () => ({ appliedCount: 0, dbVersion: 0, touchedTables: [], rebuiltFts: false }),
discardUnpublishedChangesForTables: () => {},
},
flushNow: () => {},
close: () => {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ function makeMockDb() {
run: vi.fn(),
getJson: vi.fn(() => null),
setJson: vi.fn(),
sync: { getSiteId: vi.fn(), getDbVersion: vi.fn(), exportChangesSince: vi.fn(), applyChanges: vi.fn() },
sync: {
getSiteId: vi.fn(),
getDbVersion: vi.fn(),
exportChangesSince: vi.fn(),
applyChanges: vi.fn(),
discardUnpublishedChangesForTables: vi.fn(),
},
flushNow: vi.fn(),
close: vi.fn(),
} as any;
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main/services/prs/prService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ function makeMockDb() {
run: vi.fn(),
getJson: vi.fn(() => null),
setJson: vi.fn(),
sync: { getSiteId: vi.fn(), getDbVersion: vi.fn(), exportChangesSince: vi.fn(), applyChanges: vi.fn() },
sync: {
getSiteId: vi.fn(),
getDbVersion: vi.fn(),
exportChangesSince: vi.fn(),
applyChanges: vi.fn(),
discardUnpublishedChangesForTables: vi.fn(),
},
flushNow: vi.fn(),
close: vi.fn(),
} as any;
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/services/state/kvDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export type AdeDbSyncApi = {
getDbVersion: () => number;
exportChangesSince: (version: number) => CrsqlChangeRow[];
applyChanges: (changes: CrsqlChangeRow[]) => ApplyRemoteChangesResult;
/**
* Drop unpublished local-site rows from crsql_changes for the given tables.
* Used when a viewer clears local registry state that must not be relayed to
* the brain or paired peers.
*/
discardUnpublishedChangesForTables: (tableNames: string[]) => void;
};

/**
Expand Down Expand Up @@ -3006,6 +3012,19 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise<AdeDb> {
rebuiltFts: false,
};
},
discardUnpublishedChangesForTables: (tableNames: string[]) => {
if (!crsqliteLoaded || tableNames.length === 0) return;
runStatement(db, "begin");
try {
for (const tableName of tableNames) {
runStatement(db, "delete from crsql_changes where [table] = ?", [tableName]);
}
runStatement(db, "commit");
} catch (err) {
runStatement(db, "rollback");
throw err;
}
},
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { DEFAULT_NOTIFICATION_PREFERENCES, normalizeNotificationPreferences } from "../../../shared/types/sync";
import { openKvDb } from "../state/kvDb";
import { isCrsqliteAvailable } from "../state/crsqliteExtension";
import { nowIso } from "../shared/utils";
import { createDeviceRegistryService } from "./deviceRegistryService";

function createLogger() {
Expand Down Expand Up @@ -154,6 +156,47 @@ describe("deviceRegistryService", () => {
dbB.close();
});

it("does not leave device-registry DELETE changesets after viewer join clear", async () => {
if (!isCrsqliteAvailable()) return;

const projectRoot = makeProjectRoot("ade-device-registry-viewer-clear-");
const dbPath = path.join(projectRoot, ".ade", "ade.db");
const db = await openKvDb(dbPath, createLogger() as any);
const registry = createDeviceRegistryService({
db,
logger: createLogger() as any,
projectRoot,
});

const local = registry.ensureLocalDevice();
registry.upsertPeerMetadata(
{
deviceId: "peer-phone",
deviceName: "Phone",
platform: "iOS",
deviceType: "phone",
siteId: "site-phone",
dbVersion: 0,
capabilities: [],
},
{ lastSeenAt: nowIso() },
);
expect(registry.listDevices().length).toBeGreaterThan(1);

const versionBeforeClear = db.sync.getDbVersion();
registry.clearClusterRegistryForViewerJoin();
expect(registry.listDevices()).toHaveLength(0);

const deviceChanges = db.sync.exportChangesSince(versionBeforeClear).filter((change) => change.table === "devices");
expect(deviceChanges).toHaveLength(0);

const recreated = registry.ensureLocalDevice();
expect(recreated.deviceId).toBe(local.deviceId);
expect(registry.listDevices()).toHaveLength(1);

db.close();
});

it("persists notification preferences in device metadata across registry restarts", async () => {
const projectRoot = makeProjectRoot("ade-device-registry-prefs-");
const dbPath = path.join(projectRoot, ".ade", "ade.db");
Expand Down
Loading