commit 625d6b6bbc40d1f82a80b95abed2a1e7a506830f
parent bfde4170fe757a22725264ba8b9a9eda0003e215
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:51 +0200
wallet-core: isolate migration faults from notifications
Diffstat:
3 files changed, 32 insertions(+), 14 deletions(-)
diff --git a/packages/taler-wallet-core/src/db-converter.ts b/packages/taler-wallet-core/src/db-converter.ts
@@ -32,6 +32,7 @@
*/
import {
+ DatabaseMaintenanceProgressNotification,
Logger,
NotificationType,
sha256,
@@ -518,6 +519,16 @@ export interface DbConversionReport {
totalRecords: number;
}
+/** Optional hooks for observing or deliberately interrupting a conversion. */
+export interface DbConversionOptions {
+ /**
+ * Called after a progress notification has been delivered to the source
+ * handle. Throwing aborts the conversion, which lets callers inject a
+ * controlled interruption without relying on host notification callbacks.
+ */
+ onProgress?: (notification: DatabaseMaintenanceProgressNotification) => void;
+}
+
/** Small enough to bound retained records while amortising transaction setup. */
export const DB_CONVERSION_BATCH_SIZE = 128;
@@ -643,6 +654,7 @@ async function digestStore(
export async function convertWalletDb(
src: WalletDbHandle,
dst: WalletDbHandle,
+ options: DbConversionOptions = {},
): Promise<DbConversionReport> {
const copied: Record<string, number> = {};
@@ -668,7 +680,7 @@ export async function convertWalletDb(
step?: CopyStep,
processedRecords?: number,
): void => {
- src.emitNotification({
+ const notification: DatabaseMaintenanceProgressNotification = {
type: NotificationType.DatabaseMaintenanceProgress,
operation: "indexeddb-to-native-migration",
phase,
@@ -677,7 +689,9 @@ export async function convertWalletDb(
...(step ? { step: step.name } : {}),
...(processedRecords !== undefined ? { processedRecords } : {}),
totalRecords,
- });
+ };
+ src.emitNotification(notification);
+ options.onProgress?.(notification);
};
const makeRecordProgress = (phase: "copy" | "verify") => {
diff --git a/packages/taler-wallet-core/src/db-native-migration.test.ts b/packages/taler-wallet-core/src/db-native-migration.test.ts
@@ -348,19 +348,21 @@ test("native migration: an interrupted attempt restarts after reopening", async
const progress: WalletNotification[] = [];
first.handle.setNotificationSink((n) => {
progress.push(n);
- if (
- !interruptionInjected &&
- n.type === NotificationType.DatabaseMaintenanceProgress &&
- n.operation === "indexeddb-to-native-migration" &&
- n.phase === "copy" &&
- (n.processedRecords ?? 0) >= DB_CONVERSION_PROGRESS_RECORDS
- ) {
- interruptionInjected = true;
- throw Error("simulated migration interruption");
- }
});
await assert.rejects(
- () => migrateWalletDbToNative(first.db, first.handle),
+ () =>
+ migrateWalletDbToNative(first.db, first.handle, {
+ onProgress(n) {
+ if (
+ !interruptionInjected &&
+ n.phase === "copy" &&
+ (n.processedRecords ?? 0) >= DB_CONVERSION_PROGRESS_RECORDS
+ ) {
+ interruptionInjected = true;
+ throw Error("simulated migration interruption");
+ }
+ },
+ }),
/simulated migration interruption/,
);
assert.ok(
diff --git a/packages/taler-wallet-core/src/db-native-migration.ts b/packages/taler-wallet-core/src/db-native-migration.ts
@@ -56,6 +56,7 @@ import type { Sqlite3Database } from "@gnu-taler/idb-bridge";
import {
convertWalletDb,
DB_CONVERSION_STEP_COUNT,
+ DbConversionOptions,
DbConversionReport,
} from "./db-converter.js";
import {
@@ -313,6 +314,7 @@ export interface NativeMigrationResult {
export async function migrateWalletDbToNative(
db: Sqlite3Database,
src: WalletDbHandle,
+ conversionOptions: DbConversionOptions = {},
): Promise<NativeMigrationResult> {
// Read this before native initialization upgrades the schema. A running
// marker written by versions before cleanup_safe existed is trustworthy:
@@ -382,7 +384,7 @@ export async function migrateWalletDbToNative(
logger.info("migrating the wallet database to the native schema");
// Verifies its own copy record by record and throws on any difference, so
// reaching the next statement means the native tables hold the wallet.
- const report = await convertWalletDb(src, dst);
+ const report = await convertWalletDb(src, dst, conversionOptions);
await ndb.lock.run(async () => {
const violations = await (