Skip to content

Commit

Permalink
* Use prefix key instead of nesting when saving sigchains
Browse files Browse the repository at this point in the history
* Import LocalDBService into SigChainService
* Improve async handling
  • Loading branch information
adrastaea committed Nov 27, 2024
1 parent b63039d commit 35ff923
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 95 deletions.
67 changes: 33 additions & 34 deletions packages/backend/src/nest/auth/sigchain.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,57 +33,56 @@ describe('SigChainManager', () => {
await module.close()
})

it('should throw an error when trying to get an active chain without setting one', () => {
it('should throw an error when trying to get an active chain without setting one', async () => {
expect(() => sigChainManager.getActiveChain()).toThrowError()
})
it('should throw an error when trying to set an active chain that does not exist', () => {
it('should throw an error when trying to set an active chain that does not exist', async () => {
expect(() => sigChainManager.setActiveChain('nonexistent')).toThrowError()
})
it('should add a new chain and it not be active if not set to be', () => {
const sigChain = sigChainManager.createChain('test', 'user', false)
it('should add a new chain and it not be active if not set to be', async () => {
const sigChain = await sigChainManager.createChain('test', 'user', false)
expect(() => sigChainManager.getActiveChain()).toThrowError()
sigChainManager.setActiveChain('test')
expect(sigChainManager.getActiveChain()).toBe(sigChain)
})
it('should add a new chain and it be active if set to be', () => {
const sigChain = sigChainManager.createChain('test2', 'user2', true)
it('should add a new chain and it be active if set to be', async () => {
const sigChain = await sigChainManager.createChain('test2', 'user2', true)
expect(sigChainManager.getActiveChain()).toBe(sigChain)
const prevSigChain = sigChainManager.getChainByTeamName('test')
const prevSigChain = sigChainManager.getChain('test')
expect(prevSigChain).toBeDefined()
expect(prevSigChain).not.toBe(sigChain)
})
it('should delete nonactive chain without changing active chain', () => {
sigChainManager.deleteChain('test')
expect(() => sigChainManager.getChainByTeamName('test')).toThrowError()
it('should delete nonactive chain without changing active chain', async () => {
sigChainManager.setActiveChain('test2')
await sigChainManager.deleteChain('test', false)
expect(() => sigChainManager.getChain('test')).toThrowError()
expect(sigChainManager.getActiveChain()).toBeDefined()
})
it('should delete active chain and set active chain to undefined', () => {
sigChainManager.deleteChain('test2')
it('should delete active chain and set active chain to undefined', async () => {
await sigChainManager.deleteChain('test2', false)
expect(sigChainManager.getActiveChain).toThrowError()
})
it('should save and load a chain', async () => {
const sigChain = sigChainManager.createChain('test', 'user', true)
await localDbService.setSigChain(sigChain)
const serializedChain = sigChain.save()
const retrievedChain = await localDbService.getSigChain('test')
expect(retrievedChain).toBeDefined()
expect(retrievedChain?.context.user.userName).toBe('user')
sigChainManager.deleteChain('test')
const loadedSigChain = sigChainManager.rehydrateSigChain(
retrievedChain!.serializedTeam,
retrievedChain!.context,
retrievedChain!.teamKeyRing,
false
)
expect(loadedSigChain).toBeDefined()
expect(loadedSigChain.context.user.userName).toBe('user')
expect(loadedSigChain.team.teamName).toBe('test')
})
it('should save and load sigchain using nestjs service', async () => {
const sigChain = sigChainManager.createChain('test3', 'user', true)
sigChainManager.saveChain(sigChain.team.teamName)
sigChainManager.deleteChain(sigChain.team.teamName)
const loadedSigChain = await sigChainManager.loadChain('test3', false)
const TEAM_NAME = 'test3'
const sigChain = await sigChainManager.createChain(TEAM_NAME, 'user', true)
await sigChainManager.saveChain(TEAM_NAME)
await sigChainManager.deleteChain(TEAM_NAME, false)
const loadedSigChain = await sigChainManager.loadChain(TEAM_NAME, true)
expect(loadedSigChain).toBeDefined()
expect(sigChainManager.getActiveChain()).toBe(loadedSigChain)
})
it('should not allow duplicate chains to be added', async () => {
await sigChainManager.createChain('test4', 'user4', false)
await expect(sigChainManager.createChain('test4', 'user4', false)).rejects.toThrowError()
})
it('should handle concurrent chain operations correctly', async () => {
const TEAM_NAME1 = 'test6'
const TEAM_NAME2 = 'test7'
await Promise.all([
sigChainManager.createChain(TEAM_NAME1, 'user1', true),
sigChainManager.createChain(TEAM_NAME2, 'user2', false),
])
expect(sigChainManager.getChain(TEAM_NAME1)).toBeDefined()
expect(sigChainManager.getChain(TEAM_NAME2)).toBeDefined()
})
})
119 changes: 85 additions & 34 deletions packages/backend/src/nest/auth/sigchain.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
import { Injectable, OnModuleInit } from '@nestjs/common'
import { SigChain } from './sigchain'
import { Keyring, LocalUserContext } from '3rd-party/auth/packages/auth/dist'
import { LocalDbService } from '../local-db/local-db.service'
import { createLogger } from '../common/logger'

@Injectable()
export class SigChainService implements OnModuleInit {
private readonly logger = new Logger(SigChainService.name)
public activeChainTeamName: string | undefined
private readonly logger = createLogger(SigChainService.name)
private chains: Map<string, SigChain> = new Map()
private activeChainTeamName: string | undefined
private static _instance: SigChainService | undefined
private readonly localDbService: LocalDbService

constructor(private readonly localDbService: LocalDbService) {}

onModuleInit() {
if (SigChainService._instance) {
Expand All @@ -22,17 +24,42 @@ export class SigChainService implements OnModuleInit {
if (!this.activeChainTeamName) {
throw new Error('No active chain found!')
}
return this.getChainByTeamName(this.activeChainTeamName)
return this.getChain(this.activeChainTeamName)
}

setActiveChain(teamName: string): SigChain {
/**
* Gets a chain by team name
* @param teamName Name of the team to get the chain for
* @returns The chain for the team
* @throws Error if the chain doesn't exist
*/
getChain(teamName: string): SigChain {
if (!this.chains.has(teamName)) {
throw new Error(`No chain found for team ${teamName}`)
}
return this.chains.get(teamName)!
}

static get instance(): SigChainService {
if (!SigChainService._instance) {
throw new Error("SigChainManagerService hasn't been initialized yet! Run init() before accessing")
}
return SigChainService._instance
}

setActiveChain(teamName: string): void {
if (!this.chains.has(teamName)) {
throw new Error(`No chain found for team ${teamName}, can't set to active!`)
}
this.activeChainTeamName = teamName
return this.getActiveChain()
}

/**
* Adds a chain to the service
* @param chain SigChain to add
* @param setActive Whether to set the chain as active
* @returns Whether the chain was set as active
*/
addChain(chain: SigChain, setActive: boolean): boolean {
if (this.chains.has(chain.team.teamName)) {
throw new Error(`Chain for team ${chain.team.teamName} already exists`)
Expand All @@ -45,17 +72,29 @@ export class SigChainService implements OnModuleInit {
return false
}

deleteChain(teamName: string): void {
if (!this.chains.has(teamName)) {
throw new Error(`No chain found for team ${teamName} to delete!`)
/**
* Deletes a chain from the service
* @param teamName Name of the team to delete
* @param fromDisk Whether to delete the chain from disk as well
*/
async deleteChain(teamName: string, fromDisk: boolean): Promise<void> {
if (fromDisk) {
this.localDbService.deleteSigChain(teamName)
}
this.chains.delete(teamName)
if (this.activeChainTeamName === teamName) {
this.activeChainTeamName = undefined
}
}

createChain(teamName: string, username: string, setActive: boolean): SigChain {
/**
* Creates a new chain and adds it to the service
* @param teamName Name of the team to create
* @param username Name of the user to create
* @param setActive Whether to set the chain as active
* @returns The created chain
*/
async createChain(teamName: string, username: string, setActive: boolean): Promise<SigChain> {
if (this.chains.has(teamName)) {
throw new Error(`Chain for team ${teamName} already exists`)
}
Expand All @@ -64,44 +103,56 @@ export class SigChainService implements OnModuleInit {
return sigChain
}

rehydrateSigChain(
/**
* Deserializes a chain and adds it to the service
* @param serializedTeam Serialized chain to deserialize
* @param context User context to use for the chain
* @param teamKeyRing Keyring to use for the chain
* @param setActive Whether to set the chain as active
* @returns The SigChain instance created from the serialized chain
*/
private async deserialize(
serializedTeam: Uint8Array,
context: LocalUserContext,
teamKeyRing: Keyring,
setActive: boolean
): SigChain {
): Promise<SigChain> {
this.logger.info('Deserializing chain')
const sigChain = SigChain.load(serializedTeam, context, teamKeyRing)
this.addChain(sigChain, setActive)
return sigChain
}

async loadChain(teamName: string, setActive: boolean): Promise<SigChain> {
const chain = await this.localDbService.getSigChain(teamName)
if (this.localDbService.getStatus() !== 'open') {
throw new Error('LocalDB not open!')
}
return this.rehydrateSigChain(chain!.serializedTeam, chain!.context, chain!.teamKeyRing, setActive)
}
/* LevelDB methods */

saveChain(teamName: string): void {
/**
* Loads a chain from disk and adds it to the service
* @param teamName Name of the team to load
* @param setActive Whether to set the chain as active
* @returns The SigChain instance loaded from disk
* @throws Error if the chain doesn't exist
*/
async loadChain(teamName: string, setActive: boolean): Promise<SigChain> {
if (this.localDbService.getStatus() !== 'open') {
throw new Error('LocalDB not open!')
this.localDbService.open()
}
const chain = this.getChainByTeamName(teamName)
this.localDbService.setSigChain(chain)
}

getChainByTeamName(teamName: string): SigChain {
if (!this.chains.has(teamName)) {
throw new Error(`No chain found for team ${teamName}!`)
this.logger.info(`Loading chain for team ${teamName}`)
const chain = await this.localDbService.getSigChain(teamName)
if (!chain) {
throw new Error(`Chain for team ${teamName} not found`)
}
return this.chains.get(teamName)!
return await this.deserialize(chain.serializedTeam, chain.context, chain.teamKeyRing, setActive)
}

static get instance(): SigChainService {
if (!SigChainService._instance) {
throw new Error("SigChainManagerService hasn't been initialized yet! Run init() before accessing")
/**
* Saves a chain to disk
* @param teamName Name of the team to save
*/
async saveChain(teamName: string): Promise<void> {
if (this.localDbService.getStatus() !== 'open') {
this.localDbService.open()
}
return SigChainService._instance
const chain = await this.getChain(teamName)
await this.localDbService.setSigChain(chain)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ describe('ConnectionsManagerService', () => {
registrationService = await module.resolve(RegistrationService)
sigChainService = await module.resolve(SigChainService)

// initialize sigchain on local db
sigChainService.createChain(community.name!, userIdentity.nickname, false)
localDbService.setSigChain(sigChainService.getChainByTeamName(community.name!))
sigChainService.saveChain(community.name!)
sigChainService.deleteChain(community.name!, false)

lazyModuleLoader = await module.resolve(LazyModuleLoader)
const { Libp2pModule: Module } = await import('../libp2p/libp2p.module')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,18 +229,10 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
}

if (community.name) {
const sigChainBlob = await this.localDbService.getSigChain(community.name)
if (sigChainBlob) {
try {
this.sigChainService.rehydrateSigChain(
sigChainBlob.serializedTeam,
sigChainBlob.context,
sigChainBlob.teamKeyRing,
true
)
} catch (e) {
this.logger.error('Failed to load sigchain', e)
}
try {
this.sigChainService.loadChain(community.name, true)
} catch (e) {
this.logger.error('Failed to load sigchain', e)
}
}

Expand Down
31 changes: 18 additions & 13 deletions packages/backend/src/nest/local-db/local-db.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,32 +164,37 @@ export class LocalDbService {
}

public async setSigChain(sigChain: SigChain) {
// TODO: can this be made more efficient by using a prefix instead of loading all sigchains?
this.logger.info('Setting sigchain', sigChain.team.teamName)
const teamName = sigChain.team.teamName
let sigChains = await this.get(LocalDBKeys.SIGCHAINS)
if (!sigChains) {
sigChains = {}
}
const key = `${LocalDBKeys.SIGCHAINS}${teamName}`
const serializedSigChain: SigChainSaveData = {
serializedTeam: Buffer.from(sigChain.save()).toString('base64'),
context: sigChain.context,
teamKeyRing: sigChain.team.teamKeyring(),
}
sigChains[teamName] = serializedSigChain
await this.put(LocalDBKeys.SIGCHAINS, sigChains)
this.logger.info('Saving sigchain', teamName)
await this.put(key, serializedSigChain)
}

public async getSigChain(teamName: string): Promise<SerializedSigChain | undefined> {
const sigChains = await this.get(LocalDBKeys.SIGCHAINS)
this.logger.info('Getting sigchain', teamName)
const sigChainBlob = sigChains?.[teamName]
const key = `${LocalDBKeys.SIGCHAINS}${teamName}`
this.logger.info('Getting sigchain', teamName, key)
const sigChainBlob = await this.get(key)
if (sigChainBlob) {
// convert serializedTeam from base64 to buffer to Uint8Array
const serializedTeamBuffer = Buffer.from(sigChainBlob.serializedTeam, 'base64')
return {
serializedTeam: Buffer.from(sigChainBlob.serializedTeam, 'base64'),
serializedTeam: new Uint8Array(serializedTeamBuffer),
context: sigChainBlob.context,
teamKeyRing: sigChainBlob.teamKeyRing,
}
} as SerializedSigChain
} else {
this.logger.error('Sigchain not found', teamName)
return undefined
}
}

public async deleteSigChain(teamName: string) {
const key = `${LocalDBKeys.SIGCHAINS}${teamName}`
await this.delete(key)
}
}
2 changes: 1 addition & 1 deletion packages/backend/src/nest/local-db/local-db.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export enum LocalDBKeys {
// exists in the Community object.
OWNER_ORBIT_DB_IDENTITY = 'ownerOrbitDbIdentity',

SIGCHAINS = 'sigchains',
SIGCHAINS = 'sigchains:',
USER_CONTEXTS = 'userContexts',
KEYRINGS = 'keyrings',
}
Expand Down

0 comments on commit 35ff923

Please sign in to comment.