commit cbb512e3c48682400f9e16539b1d1701dd630bfa
parent db239fc4efac2d1911de429466fe3442cf66b737
Author: triesap <triesap@radroots.dev>
Date: Sat, 27 Dec 2025 20:48:40 +0000
client: export idb/sql constants and harden bootstrap
- Add package exports for idb entrypoint and sql/constants module
- Centralize radroots IDB configs/stores and dedupe bootstrap promises
- Add structured debug logging for IDB upgrades and SQL wasm resolution
- Expose geocoder constants, update default DB path, and guard failed fetch
Diffstat:
11 files changed, 124 insertions(+), 26 deletions(-)
diff --git a/client/package.json b/client/package.json
@@ -40,6 +40,11 @@
"import": "./dist/esm/geolocation/index.js",
"require": "./dist/cjs/geolocation/index.js"
},
+ "./idb": {
+ "types": "./dist/types/idb/index.d.ts",
+ "import": "./dist/esm/idb/index.js",
+ "require": "./dist/cjs/idb/index.js"
+ },
"./keystore": {
"types": "./dist/types/keystore/index.d.ts",
"import": "./dist/esm/keystore/index.js",
@@ -60,6 +65,11 @@
"import": "./dist/esm/sql/index.js",
"require": "./dist/cjs/sql/index.js"
},
+ "./sql/constants": {
+ "types": "./dist/types/sql/constants.d.ts",
+ "import": "./dist/esm/sql/constants.js",
+ "require": "./dist/cjs/sql/constants.js"
+ },
"./tangle": {
"types": "./dist/types/tangle/index.d.ts",
"import": "./dist/esm/tangle/index.js",
diff --git a/client/src/idb/config.ts b/client/src/idb/config.ts
@@ -45,3 +45,19 @@ export const IDB_CONFIG_TANGLE: IdbClientConfig = {
database: RADROOTS_IDB_DATABASE,
store: IDB_STORE_TANGLE
};
+
+export const RADROOTS_IDB_CONFIGS: IdbClientConfig[] = [
+ IDB_CONFIG_DATASTORE,
+ IDB_CONFIG_KEYSTORE,
+ IDB_CONFIG_KEYSTORE_NOSTR,
+ IDB_CONFIG_CRYPTO_REGISTRY,
+ IDB_CONFIG_CIPHER_AES_GCM,
+ IDB_CONFIG_CIPHER_SQL,
+ IDB_CONFIG_TANGLE
+];
+
+export const RADROOTS_IDB_STORES: string[] = [
+ ...RADROOTS_IDB_CONFIGS.map((config) => config.store),
+ `${IDB_STORE_KEYSTORE}${IDB_STORE_CIPHER_SUFFIX}`,
+ `${IDB_STORE_KEYSTORE_NOSTR}${IDB_STORE_CIPHER_SUFFIX}`
+];
diff --git a/client/src/idb/index.ts b/client/src/idb/index.ts
@@ -0,0 +1,2 @@
+export * from "./config.js";
+export * from "./store.js";
diff --git a/client/src/idb/store.ts b/client/src/idb/store.ts
@@ -1,26 +1,11 @@
-import {
- IDB_STORE_CIPHER_AES_GCM,
- IDB_STORE_CIPHER_SQL,
- IDB_STORE_CIPHER_SUFFIX,
- IDB_STORE_CRYPTO_REGISTRY,
- IDB_STORE_DATASTORE,
- IDB_STORE_KEYSTORE,
- IDB_STORE_KEYSTORE_NOSTR,
- IDB_STORE_TANGLE,
- RADROOTS_IDB_DATABASE
-} from "./config.js";
+import { RADROOTS_IDB_DATABASE, RADROOTS_IDB_STORES } from "./config.js";
-const RADROOTS_IDB_STORES = [
- IDB_STORE_DATASTORE,
- IDB_STORE_KEYSTORE,
- IDB_STORE_KEYSTORE_NOSTR,
- IDB_STORE_CRYPTO_REGISTRY,
- IDB_STORE_CIPHER_AES_GCM,
- IDB_STORE_CIPHER_SQL,
- IDB_STORE_TANGLE,
- `${IDB_STORE_KEYSTORE}${IDB_STORE_CIPHER_SUFFIX}`,
- `${IDB_STORE_KEYSTORE_NOSTR}${IDB_STORE_CIPHER_SUFFIX}`
-];
+const log_idb = (label: string, payload?: Record<string, unknown>): void => {
+ console.log(`[idb] ${label}`, payload ?? {});
+};
+
+const RADROOTS_IDB_STORE_SET = new Set(RADROOTS_IDB_STORES);
+const IDB_BOOTSTRAP_PROMISES = new Map<string, Promise<void>>();
const idb_missing_stores = (db: IDBDatabase, stores: string[]): string[] =>
stores.filter((store) => !db.objectStoreNames.contains(store));
@@ -40,9 +25,19 @@ const idb_database_exists = async (database: string): Promise<boolean> => {
const idb_open = (database: string, version?: number, stores?: string[]): Promise<IDBDatabase> =>
new Promise((resolve, reject) => {
const request = indexedDB.open(database, version);
+ request.onblocked = () => {
+ log_idb(`open_blocked`, { database, version, stores });
+ };
request.onupgradeneeded = () => {
if (!stores || stores.length === 0) return;
const db = request.result;
+ const existing_stores = Array.from(db.objectStoreNames);
+ log_idb(`open_upgrade`, {
+ database,
+ version,
+ stores,
+ existing_stores
+ });
for (const store of stores) {
if (!db.objectStoreNames.contains(store)) db.createObjectStore(store);
}
@@ -57,20 +52,43 @@ const idb_open = (database: string, version?: number, stores?: string[]): Promis
const idb_store_ensure_all = async (database: string, stores: string[]): Promise<void> => {
if (stores.length === 0) return;
const target_stores = Array.from(new Set(stores));
+ log_idb(`ensure_start`, { database, target_stores });
let attempt = 0;
while (attempt < 5) {
attempt++;
const db = await idb_open(database);
const missing = idb_missing_stores(db, target_stores);
const version = db.version;
+ const existing_stores = Array.from(db.objectStoreNames);
+ log_idb(`ensure_check`, {
+ database,
+ attempt,
+ version,
+ existing_stores,
+ missing
+ });
if (missing.length === 0) {
db.close();
return;
}
db.close();
try {
+ log_idb(`ensure_upgrade`, {
+ database,
+ attempt,
+ next_version: version + 1,
+ missing
+ });
const upgraded = await idb_open(database, version + 1, missing);
const still_missing = idb_missing_stores(upgraded, target_stores);
+ const upgraded_stores = Array.from(upgraded.objectStoreNames);
+ log_idb(`ensure_upgraded`, {
+ database,
+ attempt,
+ version: upgraded.version,
+ upgraded_stores,
+ still_missing
+ });
upgraded.close();
if (still_missing.length === 0) return;
} catch (e) {
@@ -80,8 +98,29 @@ const idb_store_ensure_all = async (database: string, stores: string[]): Promise
}
};
+const idb_bootstrap_key = (database: string, stores: string[]): string =>
+ `${database}:${stores.join("|")}`;
+
+const idb_store_bootstrap_ready = async (database: string, stores: string[]): Promise<void> => {
+ const key = idb_bootstrap_key(database, stores);
+ const pending = IDB_BOOTSTRAP_PROMISES.get(key);
+ if (pending) return await pending;
+ const promise = idb_store_ensure_all(database, stores);
+ IDB_BOOTSTRAP_PROMISES.set(key, promise);
+ try {
+ await promise;
+ } catch (e) {
+ IDB_BOOTSTRAP_PROMISES.delete(key);
+ throw e;
+ }
+};
+
export const idb_store_ensure = async (database: string, store: string): Promise<void> => {
if (typeof indexedDB === "undefined") return;
+ if (database === RADROOTS_IDB_DATABASE) {
+ await idb_store_bootstrap(database);
+ if (RADROOTS_IDB_STORE_SET.has(store)) return;
+ }
await idb_store_ensure_all(database, [store]);
};
@@ -89,7 +128,7 @@ export const idb_store_bootstrap = async (database: string, stores?: string[]):
if (typeof indexedDB === "undefined") return;
const target_stores = stores ?? (database === RADROOTS_IDB_DATABASE ? RADROOTS_IDB_STORES : []);
if (target_stores.length === 0) return;
- await idb_store_ensure_all(database, target_stores);
+ await idb_store_bootstrap_ready(database, target_stores);
};
export const idb_store_exists = async (database: string, store: string): Promise<boolean> => {
diff --git a/client/src/sql/constants.ts b/client/src/sql/constants.ts
@@ -0,0 +1 @@
+export const DEFAULT_SQL_WASM_PATH = "/assets/sql-wasm.wasm";
diff --git a/client/src/sql/index.ts b/client/src/sql/index.ts
@@ -1,3 +1,4 @@
export * from "./error.js";
export * from "./types.js";
export * from "./web.js";
+export * from "./constants.js";
diff --git a/client/src/sql/web.ts b/client/src/sql/web.ts
@@ -10,10 +10,10 @@ import { WebEncryptedStore } from "../idb/encrypted_store.js";
import { idb_value_as_bytes } from "../idb/value.js";
import { is_error } from "../utils/resolve.js";
import { cl_sql_error } from "./error.js";
+import { DEFAULT_SQL_WASM_PATH } from "./constants.js";
import type { IClientSqlEncryptedStore, IWebSqlEngine, SqlJsExecOutcome, SqlJsParams, SqlJsResultRow, WebSqlEngineConfig } from "./types.js";
const DEFAULT_SQL_CIPHER_CONFIG: IdbClientConfig = IDB_CONFIG_CIPHER_SQL;
-const DEFAULT_SQL_WASM_PATH = "/assets/sql-wasm.wasm";
const resolve_or_throw = <T>(value: ResolveError<T>): T => {
if (is_error(value)) throw new Error(value.err);
return value;
@@ -100,8 +100,30 @@ export class WebSqlEngine implements IWebSqlEngine {
}
static async create(config: WebSqlEngineConfig): Promise<WebSqlEngine> {
+ const locate_wasm = (wasm_file: string): string => {
+ const resolved = resolve_wasm_path(
+ config.sql_wasm_path,
+ wasm_file,
+ DEFAULT_SQL_WASM_PATH
+ );
+ console.log(`[sql] wasm_resolve`, {
+ wasm_file,
+ resolved,
+ sql_wasm_path: config.sql_wasm_path,
+ store_key: config.store_key,
+ idb_database: config.idb_config.database,
+ idb_store: config.idb_config.store
+ });
+ return resolved;
+ };
+ console.log(`[sql] init`, {
+ sql_wasm_path: config.sql_wasm_path,
+ store_key: config.store_key,
+ idb_database: config.idb_config.database,
+ idb_store: config.idb_config.store
+ });
const sql = await init_sql_js({
- locateFile: wasm_file => resolve_wasm_path(config.sql_wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH)
+ locateFile: locate_wasm
});
const store = new WebSqlEngineEncryptedStore(config);
const existing = await store.load();
diff --git a/geocoder/package.json b/geocoder/package.json
@@ -12,6 +12,11 @@
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
+ },
+ "./constants": {
+ "types": "./dist/types/constants.d.ts",
+ "import": "./dist/esm/constants.js",
+ "require": "./dist/cjs/constants.js"
}
},
"scripts": {
diff --git a/geocoder/src/constants.ts b/geocoder/src/constants.ts
@@ -0,0 +1 @@
+export { DEFAULT_GEOCODER_DATABASE_FILE, DEFAULT_GEOCODER_DATABASE_PATH } from "./utils.js";
diff --git a/geocoder/src/geocoder.ts b/geocoder/src/geocoder.ts
@@ -33,6 +33,7 @@ export class Geocoder implements IGeocoder {
locateFile: wasm_file => resolve_wasm_path(connect_config.wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH)
});
const database_res = await fetch(database_path);
+ if (!database_res.ok) return err_msg(`*`);
const database_buffer = await database_res.arrayBuffer();
this._db = new sql.Database(new Uint8Array(database_buffer));
return true;
diff --git a/geocoder/src/utils.ts b/geocoder/src/utils.ts
@@ -3,7 +3,7 @@ import { parse_route_path, resolve_route_path } from "@radroots/utils";
import type { GeocoderReverseResult, IGeocoderCountryListResult } from "./types.js";
export const DEFAULT_GEOCODER_DATABASE_FILE = `geonames.db`;
-export const DEFAULT_GEOCODER_DATABASE_PATH = `/geonames/geonames.db`;
+export const DEFAULT_GEOCODER_DATABASE_PATH = `/assets/geonames.db`;
const GEOCODER_DATABASE_FILE_EXTS = [`.db`];
export type GeocoderDatabaseRoute = {