web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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:
Mclient/package.json | 10++++++++++
Mclient/src/idb/config.ts | 16++++++++++++++++
Aclient/src/idb/index.ts | 2++
Mclient/src/idb/store.ts | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Aclient/src/sql/constants.ts | 1+
Mclient/src/sql/index.ts | 1+
Mclient/src/sql/web.ts | 26++++++++++++++++++++++++--
Mgeocoder/package.json | 5+++++
Ageocoder/src/constants.ts | 1+
Mgeocoder/src/geocoder.ts | 1+
Mgeocoder/src/utils.ts | 2+-
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 = {