wallet-storage-client
Version:
Client only Wallet Storage
394 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletStorageManager = void 0;
const index_client_1 = require("../index.client");
/**
* The `SignerStorage` 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, `SignerStorage` 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. `SignerStorage` manages wait-blocking write requests
* for these services.
*/
class WalletStorageManager {
constructor(identityKey, active, backups) {
this.stores = [];
this._userIdentityKeyToId = {};
this._readerCount = 0;
this._writerCount = 0;
/**
* if true, allow only a single writer to proceed at a time.
* queue the blocked requests so they get executed in order when released.
*/
this._isSingleWriter = true;
/**
* if true, allow no new reader or writers to proceed.
* queue the blocked requests so they get executed in order when released.
*/
this._syncLocked = false;
/**
* if true, allow no new reader or writers or sync to proceed.
* queue the blocked requests so they get executed in order when released.
*/
this._storageProviderLocked = false;
this.stores = [];
if (active)
this.stores.push(active);
if (backups)
this.stores = this.stores.concat(backups);
this._authId = { identityKey };
}
isStorageProvider() { return false; }
async getUserId() {
let userId = this._authId.userId;
if (!userId) {
if (!this.isAvailable())
await this.makeAvailable();
const { user, isNew } = await this.getActive().findOrInsertUser(this._authId.identityKey);
if (!user)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('identityKey', 'exist on storage.');
userId = user.userId;
this._authId.userId = userId;
this._authId.isActive = user.activeStorage === undefined || user.activeStorage === this.getSettings().storageIdentityKey;
}
return userId;
}
async getAuth(mustBeActive) {
if (!this._authId.userId)
this._authId.userId = await this.getUserId();
if (mustBeActive && !this._authId.isActive)
throw new index_client_1.sdk.WERR_NOT_ACTIVE();
return this._authId;
}
getActive() {
if (this.stores.length < 1)
throw new index_client_1.sdk.WERR_INVALID_OPERATION('An active WalletStorageProvider must be added to this WalletStorageManager');
return this.stores[0];
}
async getActiveForWriter() {
while (this._storageProviderLocked || this._syncLocked || this._isSingleWriter && this._writerCount > 0 || this._readerCount > 0) {
await (0, index_client_1.wait)(100);
}
this._writerCount++;
return this.getActive();
}
async getActiveForReader() {
while (this._storageProviderLocked || this._syncLocked || this._isSingleWriter && this._writerCount > 0) {
await (0, index_client_1.wait)(100);
}
this._readerCount++;
return this.getActive();
}
async getActiveForSync() {
// Wait for a current sync task to complete...
while (this._syncLocked) {
await (0, index_client_1.wait)(100);
}
// Set syncLocked which prevents any new storageProvider, readers or writers...
this._syncLocked = true;
// Wait for any current storageProvider, readers and writers to complete
while (this._storageProviderLocked || this._readerCount > 0 || this._writerCount > 0) {
await (0, index_client_1.wait)(100);
}
// Allow the sync to proceed on the active store.
return this.getActive();
}
async getActiveForStorageProvider() {
// Wait for a current storageProvider call to complete...
while (this._storageProviderLocked) {
await (0, index_client_1.wait)(100);
}
// Set storageProviderLocked which prevents any new sync, readers or writers...
this._storageProviderLocked = true;
// Wait for any current sync, readers and writers to complete
while (this._syncLocked || this._readerCount > 0 || this._writerCount > 0) {
await (0, index_client_1.wait)(100);
}
// We can finally confirm that active storage is still able to support `StorageProvider`
if (!this.getActive().isStorageProvider())
throw new index_client_1.sdk.WERR_INVALID_OPERATION('Active "WalletStorageProvider" does not support "StorageProvider" interface.');
// Allow the sync to proceed on the active store.
return this.getActive();
}
async runAsWriter(writer) {
try {
const active = await this.getActiveForWriter();
const r = await writer(active);
return r;
}
finally {
this._writerCount--;
}
}
async runAsReader(reader) {
try {
const active = await this.getActiveForReader();
const r = await reader(active);
return r;
}
finally {
this._readerCount--;
}
}
/**
*
* @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._syncLocked = false;
}
}
async runAsStorageProvider(sync) {
try {
const active = await this.getActiveForStorageProvider();
const r = await sync(active);
return r;
}
finally {
this._storageProviderLocked = false;
}
}
/**
*
* @returns true if the active `WalletStorageProvider` also implements `StorageProvider`
*/
isActiveStorageProvider() {
return this.getActive().isStorageProvider();
}
isAvailable() {
return this.getActive().isAvailable();
}
async addWalletStorageProvider(provider) {
await provider.makeAvailable();
if (this._services)
provider.setServices(this._services);
this.stores.push(provider);
}
setServices(v) {
this._services = v;
for (const store of this.stores)
store.setServices(v);
}
getServices() {
if (!this._services)
throw new index_client_1.sdk.WERR_INVALID_OPERATION('Must setServices first.');
return this._services;
}
getSettings() { return this.getActive().getSettings(); }
async makeAvailable() {
return await this.runAsWriter(async (writer) => {
writer.makeAvailable();
return writer.getSettings();
});
}
async migrate(storageName, storageIdentityKey) {
return await this.runAsWriter(async (writer) => {
return writer.migrate(storageName, storageIdentityKey);
});
}
async destroy() {
return await this.runAsWriter(async (writer) => {
for (const store of this.stores)
await store.destroy();
});
}
async findOrInsertUser(identityKey) {
const auth = await this.getAuth();
if (identityKey != auth.identityKey)
throw new index_client_1.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 index_client_1.sdk.WERR_INTERNAL('userId may not change for given identityKey');
this._authId.userId = r.user.userId;
return r;
});
}
async abortAction(args) {
index_client_1.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) {
index_client_1.sdk.validateInternalizeActionArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.internalizeAction(auth, args);
});
}
async relinquishCertificate(args) {
index_client_1.sdk.validateRelinquishCertificateArgs(args);
return await this.runAsWriter(async (writer) => {
const auth = await this.getAuth(true);
return await writer.relinquishCertificate(auth, args);
});
}
async relinquishOutput(args) {
index_client_1.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) {
const auth = await this.getAuth();
if (identityKey !== auth.identityKey)
throw new index_client_1.sdk.WERR_UNAUTHORIZED();
const readerSettings = await reader.makeAvailable();
return await this.runAsSync(async (sync) => {
const writer = sync;
const writerSettings = this.getSettings();
let log = '';
let inserts = 0, updates = 0;
for (;;) {
const ss = await index_client_1.entity.SyncState.fromStorage(writer, identityKey, readerSettings);
const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey);
const chunk = await reader.getSyncChunk(args);
const r = await writer.processSyncChunk(args, chunk);
inserts += r.inserts;
updates += r.updates;
//log += `${r.maxUpdated_at} inserted ${r.inserts} updated ${r.updates}\n`
if (r.done)
break;
}
//console.log(log)
console.log(`sync complete: ${inserts} inserts, ${updates} updates`);
});
}
async updateBackups(activeSync) {
const auth = await this.getAuth();
return await this.runAsSync(async (sync) => {
for (const backup of this.stores.slice(1)) {
await this.syncToWriter(auth, backup, sync);
}
}, activeSync);
}
async syncToWriter(auth, writer, activeSync) {
const identityKey = auth.identityKey;
const writerSettings = await writer.makeAvailable();
return await this.runAsSync(async (sync) => {
const reader = sync;
const readerSettings = this.getSettings();
let log = '';
let inserts = 0, updates = 0;
for (;;) {
const ss = await index_client_1.entity.SyncState.fromStorage(writer, identityKey, readerSettings);
const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey);
const chunk = await reader.getSyncChunk(args);
const r = await writer.processSyncChunk(args, chunk);
inserts += r.inserts;
updates += r.updates;
log += `${r.maxUpdated_at} inserted ${r.inserts} updated ${r.updates}\n`;
if (r.done)
break;
}
//console.log(log)
//console.log(`sync complete: ${inserts} inserts, ${updates} updates`)
return { inserts, updates };
}, activeSync);
}
/**
* Updates backups and switches to new active storage provider from among current backup providers.
*
* @param storageIdentityKey of current backup storage provider that is to become the new active provider.
*/
async setActive(storageIdentityKey) {
const newActiveIndex = this.stores.findIndex(s => s.getSettings().storageIdentityKey === storageIdentityKey);
if (newActiveIndex < 0)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('storageIdentityKey', `registered with this "WalletStorageManager" as a backup data store.`);
if (newActiveIndex === 0)
/** Setting the current active as the new active is a permitted no-op. */
return;
const auth = await this.getAuth();
const newActive = this.stores[newActiveIndex];
const newActiveStorageIdentityKey = (await newActive.makeAvailable()).storageIdentityKey;
return await this.runAsSync(async (sync) => {
await sync.setActive(auth, newActiveStorageIdentityKey);
await this.updateBackups(sync);
// swap stores...
const oldActive = this.stores[0];
this.stores[0] = this.stores[newActiveIndex];
this.stores[newActiveIndex] = oldActive;
this._authId = { ...this._authId, userId: undefined };
});
}
}
exports.WalletStorageManager = WalletStorageManager;
//# sourceMappingURL=WalletStorageManager.js.map