Add account activity tracking to account service

This commit is contained in:
Matt Gibson 2024-03-28 08:58:26 -04:00
parent e7678de6e7
commit 403b2c8c0c
No known key found for this signature in database
GPG Key ID: 963EE038B0581878
3 changed files with 77 additions and 0 deletions

View File

@ -30,6 +30,7 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
export abstract class AccountService {
accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
accountActivity$: Observable<Record<UserId, Date>>;
/**
* Updates the `accounts$` observable with the new account data.
* @param userId
@ -64,6 +65,12 @@ export abstract class AccountService {
* @param userId
*/
abstract clean(userId: UserId): Promise<void>;
/**
* Updates the given user's last activity time.
* @param userId
* @param lastActivity
*/
abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>;
}
export abstract class InternalAccountService extends AccountService {

View File

@ -12,6 +12,7 @@ import { AccountInfo, accountInfoEqual } from "../abstractions/account.service";
import {
ACCOUNT_ACCOUNTS,
ACCOUNT_ACTIVE_ACCOUNT_ID,
ACCOUNT_ACTIVITY,
AccountServiceImplementation,
} from "./account.service";
@ -215,6 +216,16 @@ describe("accountService", () => {
},
});
});
it("removes account activity of the given user", async () => {
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
state.stateSubject.next({ [userId]: new Date() });
await sut.clean(userId);
const activityState = await firstValueFrom(state.state$);
expect(activityState).toEqual({});
});
});
describe("switchAccount", () => {
@ -235,4 +246,30 @@ describe("accountService", () => {
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
});
});
describe("setAccountActivity", () => {
it("should update the account activity", async () => {
await sut.setAccountActivity(userId, new Date());
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
expect(state.nextMock).toHaveBeenCalledWith({ [userId]: expect.any(Date) });
});
it("should not update if the activity is the same", async () => {
const date = new Date();
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
state.stateSubject.next({ [userId]: date });
await sut.setAccountActivity(userId, date);
expect(state.nextMock).not.toHaveBeenCalled();
});
it("should update accountActivity$ with the new activity", async () => {
await sut.setAccountActivity(userId, new Date());
const currentState = await firstValueFrom(sut.accountActivity$);
expect(currentState[userId]).toBeInstanceOf(Date);
});
});
});

View File

@ -26,6 +26,11 @@ export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_DISK, "activeAccountId", {
deserializer: (id: UserId) => id,
});
export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK, "activity", {
deserializer: (activity) => new Date(activity),
});
const loggedOutInfo: AccountInfo = {
email: "",
emailVerified: false,
@ -40,6 +45,7 @@ export class AccountServiceImplementation implements InternalAccountService {
accounts$;
activeAccount$;
accountActivity$;
constructor(
private messagingService: MessagingService,
@ -58,6 +64,7 @@ export class AccountServiceImplementation implements InternalAccountService {
distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)),
shareReplay({ bufferSize: 1, refCount: false }),
);
this.accountActivity$ = this.globalStateProvider.get(ACCOUNT_ACTIVITY).state$;
}
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
@ -82,6 +89,7 @@ export class AccountServiceImplementation implements InternalAccountService {
async clean(userId: UserId) {
await this.setAccountInfo(userId, loggedOutInfo);
await this.removeAccountActivity(userId);
}
async switchAccount(userId: UserId): Promise<void> {
@ -107,6 +115,19 @@ export class AccountServiceImplementation implements InternalAccountService {
);
}
async setAccountActivity(userId: UserId, date: Date): Promise<void> {
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
(activity) => {
activity ??= {};
activity[userId] = date;
return activity;
},
{
shouldUpdate: (activity) => activity?.[userId] != date,
},
);
}
// TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow
async delete(): Promise<void> {
try {
@ -139,4 +160,16 @@ export class AccountServiceImplementation implements InternalAccountService {
},
);
}
private async removeAccountActivity(userId: UserId): Promise<void> {
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
(activity) => {
delete activity?.[userId];
return activity;
},
{
shouldUpdate: (activity) => activity?.[userId] != null,
},
);
}
}