import { AbstractService, AppGroupManagedApplication, DeinitSource, DeinitCallback, DeviceInterface, DeinitMode, InternalEventBus, InternalEventBusInterface, RawStorageKey, } from '@standardnotes/services' import { UuidGenerator } from '@standardnotes/utils' import { AppGroupCallback } from './AppGroupCallback' import { ApplicationGroupEvent, ApplicationGroupEventData } from './ApplicationGroupEvent' import { DescriptorRecord } from './DescriptorRecord' import { ApplicationDescriptor } from './ApplicationDescriptor' export class SNApplicationGroup extends AbstractService< ApplicationGroupEvent, | ApplicationGroupEventData[ApplicationGroupEvent.PrimaryApplicationSet] | ApplicationGroupEventData[ApplicationGroupEvent.DeviceWillRestart] | ApplicationGroupEventData[ApplicationGroupEvent.DescriptorsDataChanged] > { public primaryApplication!: AppGroupManagedApplication private descriptorRecord!: DescriptorRecord callback!: AppGroupCallback constructor(public device: D, internalEventBus?: InternalEventBusInterface) { if (internalEventBus === undefined) { internalEventBus = new InternalEventBus() } super(internalEventBus) } override deinit() { super.deinit() this.device.deinit() ;(this.device as unknown) = undefined ;(this.callback as unknown) = undefined ;(this.primaryApplication as unknown) = undefined ;(this.onApplicationDeinit as unknown) = undefined } public async initialize(callback: AppGroupCallback): Promise { if (this.device.isDeviceDestroyed()) { throw 'Attempting to initialize new application while device is destroyed.' } this.callback = callback this.descriptorRecord = (await this.device.getJsonParsedRawStorageValue( RawStorageKey.DescriptorRecord, )) as DescriptorRecord if (!this.descriptorRecord) { await this.createNewDescriptorRecord() } let primaryDescriptor = this.findPrimaryDescriptor() if (!primaryDescriptor) { console.error('No primary application descriptor found. Ensure migrations have been run.') primaryDescriptor = this.getDescriptors()[0] this.setDescriptorAsPrimary(primaryDescriptor) await this.persistDescriptors() } const application = await this.buildApplication(primaryDescriptor) this.primaryApplication = application await this.notifyEvent(ApplicationGroupEvent.PrimaryApplicationSet, { application: application }) } private async createNewDescriptorRecord() { /** * The identifier 'standardnotes' is used because this was the * database name of Standard Notes web/desktop * */ const identifier = 'standardnotes' const descriptorRecord: DescriptorRecord = { [identifier]: { identifier: identifier, label: 'Main Workspace', primary: true, }, } void this.device.setRawStorageValue(RawStorageKey.DescriptorRecord, JSON.stringify(descriptorRecord)) this.descriptorRecord = descriptorRecord await this.persistDescriptors() } public getDescriptors() { return Object.values(this.descriptorRecord) } private findPrimaryDescriptor() { for (const descriptor of this.getDescriptors()) { if (descriptor.primary) { return descriptor } } return undefined } async signOutAllWorkspaces() { await this.primaryApplication.user.signOut(false, DeinitSource.SignOutAll) } onApplicationDeinit: DeinitCallback = ( application: AppGroupManagedApplication, mode: DeinitMode, source: DeinitSource, ) => { if (this.primaryApplication === application) { ;(this.primaryApplication as unknown) = undefined } const performSyncronously = async () => { if (source === DeinitSource.SignOut) { void this.removeDescriptor(this.descriptorForApplication(application)) } const descriptors = this.getDescriptors() if (descriptors.length === 0 || source === DeinitSource.SignOutAll) { const identifiers = descriptors.map((d) => d.identifier) this.descriptorRecord = {} const { killsApplication } = await this.device.clearAllDataFromDevice(identifiers) if (killsApplication) { return } } const device = this.device void this.notifyEvent(ApplicationGroupEvent.DeviceWillRestart, { source, mode }) this.deinit() if (mode === DeinitMode.Hard) { device.performHardReset() } else { device.performSoftReset() } } void performSyncronously() } public setDescriptorAsPrimary(primaryDescriptor: ApplicationDescriptor) { for (const descriptor of this.getDescriptors()) { descriptor.primary = descriptor === primaryDescriptor } } private async persistDescriptors() { await this.device.setRawStorageValue(RawStorageKey.DescriptorRecord, JSON.stringify(this.descriptorRecord)) void this.notifyEvent(ApplicationGroupEvent.DescriptorsDataChanged, { descriptors: this.descriptorRecord }) } public renameDescriptor(descriptor: ApplicationDescriptor, label: string) { descriptor.label = label void this.persistDescriptors() } public removeDescriptor(descriptor: ApplicationDescriptor) { delete this.descriptorRecord[descriptor.identifier] const descriptors = this.getDescriptors() if (descriptor.primary && descriptors.length > 0) { this.setDescriptorAsPrimary(descriptors[0]) } return this.persistDescriptors() } public removeAllDescriptors() { this.descriptorRecord = {} return this.persistDescriptors() } private descriptorForApplication(application: AppGroupManagedApplication) { return this.descriptorRecord[application.identifier] } private createNewApplicationDescriptor(label?: string) { const identifier = UuidGenerator.GenerateUuid() const index = this.getDescriptors().length + 1 const descriptor: ApplicationDescriptor = { identifier: identifier, label: label || `Workspace ${index}`, primary: false, } return descriptor } private async createNewPrimaryDescriptor(label?: string): Promise { const descriptor = this.createNewApplicationDescriptor(label) this.descriptorRecord[descriptor.identifier] = descriptor this.setDescriptorAsPrimary(descriptor) await this.persistDescriptors() } public async unloadCurrentAndCreateNewDescriptor(label?: string): Promise { await this.createNewPrimaryDescriptor(label) if (this.primaryApplication) { this.primaryApplication.deinit(this.primaryApplication.getDeinitMode(), DeinitSource.SwitchWorkspace) } } public async unloadCurrentAndActivateDescriptor(descriptor: ApplicationDescriptor) { this.setDescriptorAsPrimary(descriptor) await this.persistDescriptors() if (this.primaryApplication) { this.primaryApplication.deinit(this.primaryApplication.getDeinitMode(), DeinitSource.SwitchWorkspace) } } private async buildApplication(descriptor: ApplicationDescriptor) { const application = await this.callback.applicationCreator(descriptor, this.device) application.setOnDeinit(this.onApplicationDeinit) return application } }