@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
660 lines • 27.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletStorageManager = void 0;
const entities_1 = require("../storage/schema/entities");
const sdk = __importStar(require("../sdk"));
class ManagedStorage {
constructor(storage) {
this.storage = storage;
this.isStorageProvider = storage.isStorageProvider();
this.isAvailable = false;
}
}
/**
* The `WalletStorageManager` class delivers authentication checking storage access to the wallet.
*
* If manages multiple `StorageBase` derived storage services: one actice, the rest as backups.
*
* Of the storage services, one is 'active' at any one time.
* On startup, and whenever triggered by the wallet, `WalletStorageManager` runs a syncrhonization sequence:
*
* 1. While synchronizing, all other access to storage is blocked waiting.
* 2. The active service is confirmed, potentially triggering a resolution process if there is disagreement.
* 3. Changes are pushed from the active storage service to each inactive, backup service.
*
* Some storage services do not support multiple writers. `WalletStorageManager` manages wait-blocking write requests
* for these services.
*/
class WalletStorageManager {
/**
* Creates a new WalletStorageManager with the given identityKey and optional active and backup storage providers.
*
* @param identityKey The identity key of the user for whom this wallet is being managed.
* @param active An optional active storage provider. If not provided, no active storage will be set.
* @param backups An optional array of backup storage providers. If not provided, no backups will be set.
*/
constructor(identityKey, active, backups) {
/**
* All configured stores including current active, backups, and conflicting actives.
*/
this._stores = [];
/**
* True if makeAvailable has been run and access to managed stores (active) is allowed
*/
this._isAvailable = false;
this.readerLocks = [];
this.writerLocks = [];
this.syncLocks = [];
this.spLocks = [];
const stores = [...(backups || [])];
if (active)
stores.unshift(active);
this._stores = stores.map(s => new ManagedStorage(s));
this._authId = { identityKey };
}
isStorageProvider() {
return false;
}
isAvailable() {
return this._isAvailable;
}
/**
* The active storage is "enabled" only if its `storageIdentityKey` matches the user's currently selected `activeStorage`,
* and only if there are no stores with conflicting `activeStorage` selections.
*
* A wallet may be created without including the user's currently selected active storage. This allows readonly access to their wallet data.
*
* In addition, if there are conflicting `activeStorage` selections among backup storage providers then the active remains disabled.
*/
get isActiveEnabled() {
return (this._active !== undefined &&
this._active.settings.storageIdentityKey === this._active.user.activeStorage &&
this._conflictingActives !== undefined &&
this._conflictingActives.length === 0);
}
/**
* @returns true if at least one WalletStorageProvider has been added.
*/
canMakeAvailable() {
return this._stores.length > 0;
}
/**
* This async function must be called after construction and before
* any other async function can proceed.
*
* Runs through `_stores` validating all properties and partitioning across `_active`, `_backups`, `_conflictingActives`.
*
* @throws WERR_INVALID_PARAMETER if canMakeAvailable returns false.
*
* @returns {TableSettings} from the active storage.
*/
async makeAvailable() {
var _a;
if (this._isAvailable)
return this._active.settings;
this._active = undefined;
this._backups = [];
this._conflictingActives = [];
if (this._stores.length < 1)
throw new sdk.WERR_INVALID_PARAMETER('active', 'valid. Must add active storage provider to wallet.');
// Initial backups. conflictingActives will be removed.
const backups = [];
let i = -1;
for (const store of this._stores) {
i++;
if (!store.isAvailable || !store.settings || !store.user) {
// Validate all ManagedStorage properties.
store.settings = await store.storage.makeAvailable();
const r = await store.storage.findOrInsertUser(this._authId.identityKey);
store.user = r.user;
store.isAvailable = true;
}
if (!this._active)
// _stores[0] becomes the default active store. It may be replaced if it is not the user's "enabled" activeStorage and that store is found among the remainder (backups).
this._active = store;
else {
const ua = store.user.activeStorage;
const si = store.settings.storageIdentityKey;
if (ua === si && !this.isActiveEnabled) {
// This store's user record selects it as an enabled active storage...
// swap the current not-enabled active for this storeage.
backups.push(this._active);
this._active = store;
}
else {
// This store is a backup: Its user record selects some other storage as active.
backups.push(store);
}
}
}
// Review backups, partition out conflicting actives.
const si = (_a = this._active.settings) === null || _a === void 0 ? void 0 : _a.storageIdentityKey;
for (const store of backups) {
if (store.user.activeStorage !== si)
this._conflictingActives.push(store);
else
this._backups.push(store);
}
this._isAvailable = true;
this._authId.userId = this._active.user.userId;
this._authId.isActive = this.isActiveEnabled;
return this._active.settings;
}
verifyActive() {
if (!this._active || !this._isAvailable)
throw new sdk.WERR_INVALID_OPERATION('An active WalletStorageProvider must be added to this WalletStorageManager and makeAvailable must be called.');
return this._active;
}
async getAuth(mustBeActive) {
if (!this.isAvailable())
await this.makeAvailable();
if (mustBeActive && !this._authId.isActive)
throw new sdk.WERR_NOT_ACTIVE();
return this._authId;
}
async getUserId() {
return (await this.getAuth()).userId;
}
getActive() {
return this.verifyActive().storage;
}
getActiveSettings() {
return this.verifyActive().settings;
}
getActiveUser() {
return this.verifyActive().user;
}
getActiveStore() {
return this.verifyActive().settings.storageIdentityKey;
}
getActiveStoreName() {
return this.verifyActive().settings.storageName;
}
getBackupStores() {
this.verifyActive();
return this._backups.map(b => b.settings.storageIdentityKey);
}
getConflictingStores() {
this.verifyActive();
return this._conflictingActives.map(b => b.settings.storageIdentityKey);
}
getAllStores() {
this.verifyActive();
return this._stores.map(b => b.settings.storageIdentityKey);
}
async getActiveLock(lockQueue) {
if (!this.isAvailable())
await this.makeAvailable();
let resolveNewLock = () => { };
const newLock = new Promise(resolve => {
resolveNewLock = resolve;
lockQueue.push(resolve);
});
if (lockQueue.length === 1) {
resolveNewLock();
}
await newLock;
}
releaseActiveLock(queue) {
queue.shift(); // Remove the current lock from the queue
if (queue.length > 0) {
queue[0]();
}
}
async getActiveForReader() {
await this.getActiveLock(this.readerLocks);
return this.getActive();
}
releaseActiveForReader() {
this.releaseActiveLock(this.readerLocks);
}
async getActiveForWriter() {
await this.getActiveLock(this.readerLocks);
await this.getActiveLock(this.writerLocks);
return this.getActive();
}
releaseActiveForWriter() {
this.releaseActiveLock(this.writerLocks);
this.releaseActiveLock(this.readerLocks);
}
async getActiveForSync() {
await this.getActiveLock(this.readerLocks);
await this.getActiveLock(this.writerLocks);
await this.getActiveLock(this.syncLocks);
return this.getActive();
}
releaseActiveForSync() {
this.releaseActiveLock(this.syncLocks);
this.releaseActiveLock(this.writerLocks);
this.releaseActiveLock(this.readerLocks);
}
async getActiveForStorageProvider() {
await this.getActiveLock(this.readerLocks);
await this.getActiveLock(this.writerLocks);
await this.getActiveLock(this.syncLocks);
await this.getActiveLock(this.spLocks);
const active = this.getActive();
// We can finally confirm that active storage is still able to support `StorageProvider`
if (!active.isStorageProvider())
throw new sdk.WERR_INVALID_OPERATION('Active "WalletStorageProvider" does not support "StorageProvider" interface.');
// Allow the sync to proceed on the active store.
return active;
}
releaseActiveForStorageProvider() {
this.releaseActiveLock(this.spLocks);
this.releaseActiveLock(this.syncLocks);
this.releaseActiveLock(this.writerLocks);
this.releaseActiveLock(this.readerLocks);
}
async runAsWriter(writer) {
try {
const active = await this.getActiveForWriter();
const r = await writer(active);
return r;
}
finally {
this.releaseActiveForWriter();
}
}
async runAsReader(reader) {
try {
const active = await this.getActiveForReader();
const r = await reader(active);
return r;
}
finally {
this.releaseActiveForReader();
}
}
/**
*
* @param sync the function to run with sync access lock
* @param activeSync from chained sync functions, active storage already held under sync access lock.
* @returns
*/
async runAsSync(sync, activeSync) {
try {
const active = activeSync || (await this.getActiveForSync());
const r = await sync(active);
return r;
}
finally {
if (!activeSync)
this.releaseActiveForSync();
}
}
async runAsStorageProvider(sync) {
try {
const active = await this.getActiveForStorageProvider();
const r = await sync(active);
return r;
}
finally {
this.releaseActiveForStorageProvider();
}
}
/**
*
* @returns true if the active `WalletStorageProvider` also implements `StorageProvider`
*/
isActiveStorageProvider() {
return this.getActive().isStorageProvider();
}
async addWalletStorageProvider(provider) {
await provider.makeAvailable();
if (this._services)
provider.setServices(this._services);
this._stores.push(new ManagedStorage(provider));
this._isAvailable = false;
await this.makeAvailable();
}
setServices(v) {
this._services = v;
for (const store of this._stores)
store.storage.setServices(v);
}
getServices() {
if (!this._services)
throw new sdk.WERR_INVALID_OPERATION('Must setServices first.');
return this._services;
}
getSettings() {
return this.getActive().getSettings();
}
async migrate(storageName, storageIdentityKey) {
return await this.runAsWriter(async (writer) => {
return writer.migrate(storageName, storageIdentityKey);
});
}
async destroy() {
if (this._stores.length < 1)
return;
return await this.runAsWriter(async (writer) => {
for (const store of this._stores)
await store.storage.destroy();
});
}
async findOrInsertUser(identityKey) {
const auth = await this.getAuth();
if (identityKey != auth.identityKey)
throw new sdk.WERR_UNAUTHORIZED();
return await this.runAsWriter(async (writer) => {
const r = await writer.findOrInsertUser(identityKey);
if (auth.userId && auth.userId !== r.user.userId)
throw new sdk.WERR_INTERNAL('userId may not change for given identityKey');
this._authId.userId = r.user.userId;
return r;
});
}
async abortAction(args) {
sdk.validateAbortActionArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.abortAction(auth, args);
});
}
async createAction(vargs) {
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.createAction(auth, vargs);
});
}
async internalizeAction(args) {
sdk.validateInternalizeActionArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.internalizeAction(auth, args);
});
}
async relinquishCertificate(args) {
sdk.validateRelinquishCertificateArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.relinquishCertificate(auth, args);
});
}
async relinquishOutput(args) {
sdk.validateRelinquishOutputArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.relinquishOutput(auth, args);
});
}
async processAction(args) {
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.processAction(auth, args);
});
}
async insertCertificate(certificate) {
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.insertCertificateAuth(auth, certificate);
});
}
async listActions(vargs) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.listActions(auth, vargs);
});
}
async listCertificates(args) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.listCertificates(auth, args);
});
}
async listOutputs(vargs) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.listOutputs(auth, vargs);
});
}
async findCertificates(args) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.findCertificatesAuth(auth, args);
});
}
async findOutputBaskets(args) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.findOutputBasketsAuth(auth, args);
});
}
async findOutputs(args) {
const auth = await this.getAuth();
return await this.runAsReader(async (reader) => {
return await reader.findOutputsAuth(auth, args);
});
}
async findProvenTxReqs(args) {
return await this.runAsReader(async (reader) => {
return await reader.findProvenTxReqs(args);
});
}
async syncFromReader(identityKey, reader, activeSync, log = '') {
const auth = await this.getAuth();
if (identityKey !== auth.identityKey)
throw new sdk.WERR_UNAUTHORIZED();
const readerSettings = await reader.makeAvailable();
let inserts = 0, updates = 0;
log = await this.runAsSync(async (sync) => {
const writer = sync;
const writerSettings = this.getSettings();
log += `syncFromReader from ${readerSettings.storageName} to ${writerSettings.storageName}\n`;
let i = -1;
for (;;) {
i++;
const ss = await entities_1.EntitySyncState.fromStorage(writer, identityKey, readerSettings);
const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey);
const chunk = await reader.getSyncChunk(args);
if (chunk.user) {
// Merging state from a reader cannot update activeStorage
chunk.user.activeStorage = this._active.user.activeStorage;
}
const r = await writer.processSyncChunk(args, chunk);
inserts += r.inserts;
updates += r.updates;
log += `chunk ${i} inserted ${r.inserts} updated ${r.updates} ${r.maxUpdated_at}\n`;
if (r.done)
break;
}
log += `syncFromReader complete: ${inserts} inserts, ${updates} updates\n`;
return log;
}, activeSync);
return { inserts, updates, log };
}
async syncToWriter(auth, writer, activeSync, log = '', progLog) {
progLog || (progLog = s => s);
const identityKey = auth.identityKey;
const writerSettings = await writer.makeAvailable();
let inserts = 0, updates = 0;
log = await this.runAsSync(async (sync) => {
const reader = sync;
const readerSettings = reader.getSettings();
log += progLog(`syncToWriter from ${readerSettings.storageName} to ${writerSettings.storageName}\n`);
let i = -1;
for (;;) {
i++;
const ss = await entities_1.EntitySyncState.fromStorage(writer, identityKey, readerSettings);
const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey);
const chunk = await reader.getSyncChunk(args);
log += entities_1.EntitySyncState.syncChunkSummary(chunk);
const r = await writer.processSyncChunk(args, chunk);
inserts += r.inserts;
updates += r.updates;
log += progLog(`chunk ${i} inserted ${r.inserts} updated ${r.updates} ${r.maxUpdated_at}\n`);
if (r.done)
break;
}
log += progLog(`syncToWriter complete: ${inserts} inserts, ${updates} updates\n`);
return log;
}, activeSync);
return { inserts, updates, log };
}
async updateBackups(activeSync, progLog) {
progLog || (progLog = s => s);
const auth = await this.getAuth(true);
return await this.runAsSync(async (sync) => {
let log = progLog(`BACKUP CURRENT ACTIVE TO ${this._backups.length} STORES\n`);
for (const backup of this._backups) {
const stwr = await this.syncToWriter(auth, backup.storage, sync, undefined, progLog);
log += stwr.log;
}
return log;
}, activeSync);
}
/**
* Updates backups and switches to new active storage provider from among current backup providers.
*
* Also resolves conflicting actives.
*
* @param storageIdentityKey of current backup storage provider that is to become the new active provider.
*/
async setActive(storageIdentityKey, progLog) {
progLog || (progLog = s => s);
if (!this.isAvailable())
await this.makeAvailable();
// Confirm a valid storageIdentityKey: must match one of the _stores.
const newActiveIndex = this._stores.findIndex(s => s.settings.storageIdentityKey === storageIdentityKey);
if (newActiveIndex < 0)
throw new sdk.WERR_INVALID_PARAMETER('storageIdentityKey', `registered with this "WalletStorageManager". ${storageIdentityKey} does not match any managed store.`);
const identityKey = (await this.getAuth()).identityKey;
const newActive = this._stores[newActiveIndex];
let log = progLog(`setActive to ${newActive.settings.storageName}`);
if (storageIdentityKey === this.getActiveStore() && this.isActiveEnabled)
/** Setting the current active as the new active is a permitted no-op. */
return log + progLog(` unchanged\n`);
log += progLog('\n');
log += await this.runAsSync(async (sync) => {
let log = '';
if (this._conflictingActives.length > 0) {
// Merge state from conflicting actives into `newActive`.
// Handle case where new active is current active to resolve conflicts.
// And where new active is one of the current conflict actives.
this._conflictingActives.push(this._active);
// Remove the new active from conflicting actives and
// set new active as the conflicting active that matches the target `storageIdentityKey`
this._conflictingActives = this._conflictingActives.filter(ca => {
const isNewActive = ca.settings.storageIdentityKey === storageIdentityKey;
return !isNewActive;
});
// Merge state from conflicting actives into `newActive`.
for (const conflict of this._conflictingActives) {
log += progLog('MERGING STATE FROM CONFLICTING ACTIVES:\n');
const sfr = await this.syncToWriter({ identityKey, userId: newActive.user.userId, isActive: false }, newActive.storage, conflict.storage, undefined, progLog);
log += sfr.log;
}
log += progLog('PROPAGATE MERGED ACTIVE STATE TO NON-ACTIVES\n');
}
else {
log += progLog('BACKUP CURRENT ACTIVE STATE THEN SET NEW ACTIVE\n');
}
// If there were conflicting actives,
// Push state merged from all merged actives into newActive to all stores other than the now single active.
// Otherwise,
// Push state from current active to all other stores.
const backupSource = this._conflictingActives.length > 0 ? newActive : this._active;
// Update the backupSource's user record with the new activeStorage
// which will propagate to all other stores in the following backup loop.
await backupSource.storage.setActive({ identityKey, userId: backupSource.user.userId }, storageIdentityKey);
for (const store of this._stores) {
// Update cached user.activeStorage of all stores
store.user.activeStorage = storageIdentityKey;
if (store.settings.storageIdentityKey !== backupSource.settings.storageIdentityKey) {
// If this store is not the backupSource store push state from backupSource to this store.
const stwr = await this.syncToWriter({ identityKey, userId: store.user.userId, isActive: false }, store.storage, backupSource.storage, undefined, progLog);
log += stwr.log;
}
}
this._isAvailable = false;
await this.makeAvailable();
return log;
});
return log;
}
getStoreEndpointURL(store) {
if (store.storage.constructor.name === 'StorageClient')
return store.storage.endpointUrl;
return undefined;
}
getStores() {
const stores = [];
if (this._active) {
stores.push({
isActive: true,
isEnabled: this.isActiveEnabled,
isBackup: false,
isConflicting: false,
userId: this._active.user.userId,
storageIdentityKey: this._active.settings.storageIdentityKey,
storageName: this._active.settings.storageName,
storageClass: this._active.storage.constructor.name,
endpointURL: this.getStoreEndpointURL(this._active)
});
}
for (const store of this._conflictingActives || []) {
stores.push({
isActive: true,
isEnabled: false,
isBackup: false,
isConflicting: true,
userId: store.user.userId,
storageIdentityKey: store.settings.storageIdentityKey,
storageName: store.settings.storageName,
storageClass: store.storage.constructor.name,
endpointURL: this.getStoreEndpointURL(store)
});
}
for (const store of this._backups || []) {
stores.push({
isActive: false,
isEnabled: false,
isBackup: true,
isConflicting: false,
userId: store.user.userId,
storageIdentityKey: store.settings.storageIdentityKey,
storageName: store.settings.storageName,
storageClass: store.storage.constructor.name,
endpointURL: this.getStoreEndpointURL(store)
});
}
return stores;
}
}
exports.WalletStorageManager = WalletStorageManager;
//# sourceMappingURL=WalletStorageManager.js.map