Compare commits

...

7 Commits

Author SHA1 Message Date
Sosthene
6625771830 Merge branch 'Fix_db' 2025-09-03 15:12:31 +02:00
Sosthene
e320cfa193 Pair device right away to prevent errors on prd update operations 2025-09-03 15:11:07 +02:00
Sosthene
80dc42bbe6 Replace blobs with buffers 2025-09-03 15:10:42 +02:00
Sosthene
6569686634 Don't ignore falsish data (empty string, 0...) 2025-09-03 15:09:53 +02:00
Sosthene
77e3dfc29c [bug] fix broken update of processes 2025-09-03 15:09:35 +02:00
Sosthene
a2ae855c10 Fix broken db operation for raw bytes 2025-09-03 15:08:54 +02:00
Sosthene
c3455ac888 [bug] fix parseKey 2025-09-02 17:14:17 +02:00
3 changed files with 69 additions and 62 deletions

View File

@ -60,19 +60,27 @@ export default class Database {
} }
private parseKey(fullKey: string): { storeName: string; key: string } | null { private parseKey(fullKey: string): { storeName: string; key: string } | null {
const parts = fullKey.split(':', 2); const colonIndex = fullKey.indexOf(':');
if (parts.length !== 2) return null; if (colonIndex === -1) return null;
return { storeName: parts[0], key: parts[1] };
const storeName = fullKey.substring(0, colonIndex);
const key = fullKey.substring(colonIndex + 1);
return { storeName, key };
} }
/** /**
* Get a single object from a store * Get a single object from a store
* O(log n) operation - only reads specific key * O(log n) operation - only reads specific key
*/ */
public async getObject(storeName: string, key: string): Promise<any | null> { public async getObject(storeName: string, key: string, isBuffer: boolean = false): Promise<any | null> {
try { try {
const fullKey = this.getKey(storeName, key); const fullKey = this.getKey(storeName, key);
return await this.db.get(fullKey); if (isBuffer) {
return await this.db.get(fullKey, { valueEncoding: 'buffer' });
} else {
return await this.db.get(fullKey);
}
} catch (error) { } catch (error) {
if ((error as any).code === 'LEVEL_NOT_FOUND') { if ((error as any).code === 'LEVEL_NOT_FOUND') {
return null; return null;
@ -85,12 +93,16 @@ export default class Database {
* Add or update an object in a store * Add or update an object in a store
* O(log n) operation - only writes specific key-value pair * O(log n) operation - only writes specific key-value pair
*/ */
public async addObject(operation: DatabaseObject): Promise<void> { public async addObject(operation: DatabaseObject, isBuffer: boolean = false): Promise<void> {
const { storeName, object, key } = operation; const { storeName, object, key } = operation;
if (key) { if (key) {
const fullKey = this.getKey(storeName, key); const fullKey = this.getKey(storeName, key);
await this.db.put(fullKey, object); if (isBuffer) {
await this.db.put(fullKey, object, { valueEncoding: 'buffer' });
} else {
await this.db.put(fullKey, object);
}
} else { } else {
// Auto-generate key if none provided // Auto-generate key if none provided
const autoKey = Date.now().toString() + Math.random().toString(36).substr(2, 9); const autoKey = Date.now().toString() + Math.random().toString(36).substr(2, 9);

View File

@ -80,56 +80,49 @@ export class Service {
const existing = await this.getProcess(processId); const existing = await this.getProcess(processId);
if (existing) { if (existing) {
// Look for state id we don't know yet // Look for state id we don't know yet
let new_states = []; let newStates: string[] = [];
let roles = []; let newRoles: Record<string, RoleDefinition>[] = [];
for (const state of process.states) { for (const state of process.states) {
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; } if (!state || !state.state_id) { continue; } // shouldn't happen
if (!this.lookForStateId(existing, state.state_id)) { if (state.state_id === EMPTY32BYTES) {
// We check that the tip is the same we have, if not we update
const existingTip = existing.states[existing.states.length - 1].commited_in;
if (existingTip !== state.commited_in) {
console.log('Found new tip for process', processId);
existing.states.pop(); // We discard the last state
existing.states.push(state);
// We know that's the last state, so we just trigger the update
toSave[processId] = existing;
}
} else if (!this.lookForStateId(existing, state.state_id)) {
// We don't want to overwrite what we already have for existing processes
// We may end up overwriting the keys for example
// So the process we're going to save needs to merge new states with what we already have
const existingLastState = existing.states.pop();
existing.states.push(state);
existing.states.push(existingLastState);
toSave[processId] = existing; // We mark it for update
if (this.rolesContainsUs(state.roles)) { if (this.rolesContainsUs(state.roles)) {
new_states.push(state.state_id); newStates.push(state.state_id);
roles.push(state.roles); newRoles.push(state.roles);
} }
} }
} }
if (new_states.length != 0) { if (newStates.length != 0) {
// We request the new states await this.requestDataFromPeers(processId, newStates, newRoles);
await this.requestDataFromPeers(processId, new_states, roles);
toSave[processId] = process;
} }
// Just to be sure check if that's a pairing process
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) {
// This is a pairing process
try {
const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses'] as unknown as number[]);
// Are we part of it?
if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) {
// We save the process to db
await this.saveProcessToDb(processId, process as Process);
// We update the device
await this.updateDevice();
}
} catch (e) {
console.error('Failed to check for pairing process:', e);
}
}
// Otherwise we're probably just in the initial loading at page initialization // Otherwise we're probably just in the initial loading at page initialization
// We may learn an update for this process
// TODO maybe actually check if what the relay is sending us contains more information than what we have
// relay should always have more info than us, but we never know
// For now let's keep it simple and let the worker do the job
} else { } else {
// We add it to db // We add it to db
console.log(`Saving ${processId} to db`);
toSave[processId] = process; toSave[processId] = process;
} }
} }
await this.batchSaveProcessesToDb(toSave); if (toSave && Object.keys(toSave).length > 0) {
console.log('batch saving processes to db', toSave);
await this.batchSaveProcessesToDb(toSave);
}
} }
}, 500) }, 500)
} catch (e) { } catch (e) {
@ -716,7 +709,7 @@ export class Service {
for (const attribute of Object.keys(lastState.public_data)) { for (const attribute of Object.keys(lastState.public_data)) {
try { try {
const value = this.decodeValue(lastState.public_data[attribute]); const value = this.decodeValue(lastState.public_data[attribute]);
if (value) { if (value !== null && value !== undefined) {
processData[attribute] = value; processData[attribute] = value;
} }
} catch (e) { } catch (e) {
@ -992,31 +985,31 @@ export class Service {
} }
// Blob and data storage methods // Blob and data storage methods
async saveBlobToDb(hash: string, data: Blob) { async saveBufferToDb(hash: string, data: Buffer) {
const db = await Database.getInstance(); const db = await Database.getInstance();
try { try {
await db.addObject({ await db.addObject({
storeName: 'data', storeName: 'data',
object: data, object: data,
key: hash, key: hash,
}); }, true);
} catch (e) { } catch (e) {
console.error(`Failed to save data to db: ${e}`); console.error(`Failed to save data to db: ${e}`);
} }
} }
async getBlobFromDb(hash: string): Promise<Blob | null> { async getBufferFromDb(hash: string): Promise<Buffer | null> {
const db = await Database.getInstance(); const db = await Database.getInstance();
try { try {
return await db.getObject('data', hash); return await db.getObject('data', hash, true);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
async saveDataToStorage(hash: string, data: Blob, ttl: number | null) { async saveDataToStorage(hash: string, data: Buffer, ttl: number | null) {
console.log('💾 Saving data to storage:', hash); console.log('💾 Saving data to storage:', hash);
// TODO: Implement actual storage service console.debug('TODO');
// const storages = [STORAGEURL]; // const storages = [STORAGEURL];
// try { // try {
// await storeData(storages, hash, data, ttl); // await storeData(storages, hash, data, ttl);
@ -1057,6 +1050,10 @@ export class Service {
return uint8Array; return uint8Array;
} }
hexToBuffer(hexString: string): Buffer {
return Buffer.from(this.hexToUInt8Array(hexString));
}
public async handleApiReturn(apiReturn: ApiReturn) { public async handleApiReturn(apiReturn: ApiReturn) {
// Check for errors in the returned objects // Check for errors in the returned objects
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.error) { if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.error) {
@ -1121,9 +1118,9 @@ export class Service {
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) { for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
const blob = this.hexToBlob(cipher); const buffer = this.hexToBuffer(cipher);
try { try {
await this.saveBlobToDb(hash, blob); await this.saveBufferToDb(hash, buffer);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -1144,9 +1141,9 @@ export class Service {
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
for (const hash of apiReturn.push_to_storage) { for (const hash of apiReturn.push_to_storage) {
const blob = await this.getBlobFromDb(hash); const buffer = await this.getBufferFromDb(hash);
if (blob) { if (buffer) {
await this.saveDataToStorage(hash, blob, null); await this.saveDataToStorage(hash, buffer, null);
} else { } else {
console.error('Failed to get data from db'); console.error('Failed to get data from db');
} }
@ -1247,11 +1244,10 @@ export class Service {
} }
if (hash && key) { if (hash && key) {
const blob = await this.getBlobFromDb(hash); const buffer = await this.getBufferFromDb(hash);
if (blob) { if (buffer) {
// Decrypt the data // Decrypt the data
const buf = await blob.arrayBuffer(); const cipher = new Uint8Array(buffer);
const cipher = new Uint8Array(buf);
const keyUIntArray = this.hexToUInt8Array(key); const keyUIntArray = this.hexToUInt8Array(key);

View File

@ -366,15 +366,14 @@ export class Server {
if (!processId || !stateId) { if (!processId || !stateId) {
throw new Error('Failed to get process id or state id'); throw new Error('Failed to get process id or state id');
} }
// now pair the device before continuing
service.pairDevice(processId, [service.getDeviceAddress()]);
await service.handleApiReturn(pairingResult); await service.handleApiReturn(pairingResult);
const udpateResult = await service.createPrdUpdate(processId, stateId); const udpateResult = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(udpateResult); await service.handleApiReturn(udpateResult);
const approveResult = await service.approveChange(processId, stateId); const approveResult = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveResult); await service.handleApiReturn(approveResult);
// now pair the device
service.pairDevice(processId, [service.getDeviceAddress()]);
// Update the device in the database // Update the device in the database
const device = service.dumpDeviceFromMemory(); const device = service.dumpDeviceFromMemory();
if (device) { if (device) {