bitwarden-estensione-browser/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts

117 lines
3.5 KiB
TypeScript

import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../browser/browser-api";
import { SyncedItemMetadata } from "./sync-item-metadata";
export class SessionSyncer {
subscription: Subscription;
id = Utils.newGuid();
// ignore initial values
private ignoreNUpdates = 0;
constructor(
private subject: Subject<any>,
private memoryStorageService: AbstractMemoryStorageService,
private metaData: SyncedItemMetadata
) {
if (!(subject instanceof Subject)) {
throw new Error("subject must inherit from Subject");
}
if (metaData.initializer == null) {
throw new Error("initializer must be provided");
}
}
async init() {
switch (this.subject.constructor) {
case ReplaySubject:
// ignore all updates currently in the buffer
this.ignoreNUpdates = (this.subject as any)._buffer.length;
break;
case BehaviorSubject:
this.ignoreNUpdates = 1;
break;
default:
break;
}
await this.observe();
// must be synchronous
const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey);
if (hasInSessionMemory) {
await this.updateFromMemory();
}
this.listenForUpdates();
}
private async observe() {
const stream = this.subject.pipe(skip(this.ignoreNUpdates));
this.ignoreNUpdates = 0;
// This may be a memory leak.
// There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary
// contexts. If so, this is handled by destruction of the context.
this.subscription = stream
.pipe(
concatMap(async (next) => {
if (this.ignoreNUpdates > 0) {
this.ignoreNUpdates -= 1;
return;
}
await this.updateSession(next);
})
)
.subscribe();
}
private listenForUpdates() {
// This is an unawaited promise, but it will be executed asynchronously in the background.
BrowserApi.messageListener(
this.updateMessageCommand,
async (message) => await this.updateFromMessage(message)
);
}
async updateFromMessage(message: any) {
if (message.command != this.updateMessageCommand || message.id === this.id) {
return;
}
await this.update(message.serializedValue);
}
async updateFromMemory() {
const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey);
await this.update(value);
}
async update(serializedValue: any) {
const unBuiltValue = JSON.parse(serializedValue);
if (BrowserApi.manifestVersion !== 3 && BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
}
const builder = SyncedItemMetadata.builder(this.metaData);
const value = builder(unBuiltValue);
this.ignoreNUpdates = 1;
this.subject.next(value);
}
private async updateSession(value: any) {
const serializedValue = JSON.stringify(value);
if (BrowserApi.manifestVersion === 3 || BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
}
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue });
}
private get updateMessageCommand() {
return `${this.metaData.sessionKey}_update`;
}
}