WIP: Circular dependency but basic pattern implemented for profile get

- Get from Cache > Get from Database > Get from Relays
This commit is contained in:
SondreB 2023-01-07 17:39:21 +01:00
parent 42029048a4
commit 6df1dac494
No known key found for this signature in database
GPG Key ID: D6CC44C75005FDBF
15 changed files with 224 additions and 65 deletions

15
package-lock.json generated
View File

@ -33,6 +33,7 @@
"qrcode": "^1.5.1",
"rxjs": "~7.8.0",
"sanitize-html": "^2.8.1",
"ts-cacheable": "^1.0.9",
"tslib": "^2.4.1",
"uuid": "^9.0.0",
"zone.js": "~0.12.0"
@ -11961,6 +11962,14 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-cacheable": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/ts-cacheable/-/ts-cacheable-1.0.9.tgz",
"integrity": "sha512-Wqbuh086IO2268XGb+a2ZodBVkSQAdbo5UkJJ7/B1csiskMIpip5rC7jfEv3UGZ/ylgN1ij7El0rUcqikws3zg==",
"peerDependencies": {
"rxjs": "^6.6.0 || ^7.4.0"
}
},
"node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
@ -21657,6 +21666,12 @@
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true
},
"ts-cacheable": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/ts-cacheable/-/ts-cacheable-1.0.9.tgz",
"integrity": "sha512-Wqbuh086IO2268XGb+a2ZodBVkSQAdbo5UkJJ7/B1csiskMIpip5rC7jfEv3UGZ/ylgN1ij7El0rUcqikws3zg==",
"requires": {}
},
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",

View File

@ -37,6 +37,7 @@
"qrcode": "^1.5.1",
"rxjs": "~7.8.0",
"sanitize-html": "^2.8.1",
"ts-cacheable": "^1.0.9",
"tslib": "^2.4.1",
"uuid": "^9.0.0",
"zone.js": "~0.12.0"

View File

@ -45,5 +45,5 @@
<button mat-stroked-button (click)="publishFollowList()">Publish Follow List</button>
</p>
<p class="dimmed">Circles is how you organize people you follow. Different circles can have different rules applied and circles is an important way to make the experience more enjoyable.</p>
<small>Circles is how you organize people you follow. Different circles can have different rules applied and circles is an important way to make the experience more enjoyable.</small>
</div>

View File

@ -28,9 +28,11 @@ export class CirclesComponent {
following: NostrProfileDocument[] = [];
searchTerm: any;
circles: Circle[] = [];
constructor(
public appState: ApplicationState,
private circlesService: CirclesService,
public circlesService: CirclesService,
private storage: StorageService,
private profile: ProfileService,
public dialog: MatDialog,
@ -48,8 +50,6 @@ export class CirclesComponent {
this.utilities.unsubscribe(this.subscriptions);
}
circles: Circle[] = [];
async load() {
this.loading = true;
this.circles = await this.circlesService.list();
@ -57,13 +57,13 @@ export class CirclesComponent {
this.loading = false;
}
async deleteCircle(id: string) {
async deleteCircle(id: number) {
const pubKeys = this.getFollowingInCircle(id).map((f) => f.pubkey);
await this.circlesService.deleteCircle(id);
for (var i = 0; i < pubKeys.length; i++) {
await this.profile.setCircle(pubKeys[i], '');
await this.profile.setCircle(pubKeys[i], undefined);
}
await this.load();
@ -134,9 +134,9 @@ export class CirclesComponent {
});
}
getFollowingInCircle(id: string) {
if (id == null || id == '') {
return this.following.filter((f) => f.circle == null || f.circle == '');
getFollowingInCircle(id?: number) {
if (id == null) {
return this.following.filter((f) => f.circle == null || f.circle == null);
} else {
return this.following.filter((f) => f.circle == id);
}
@ -228,6 +228,11 @@ export class CirclesComponent {
},
];
this.circlesService.circles$.subscribe((circles) => {
circles.unshift(CirclesService.DEFAULT);
this.circles = circles;
});
await this.load();
}
}

View File

@ -80,7 +80,7 @@ export class FeedPublicComponent {
relay?: Relay;
initialLoad = true;
async follow(pubkey: string, circle?: string) {
async follow(pubkey: string, circle?: number) {
await this.profile.follow(pubkey, circle);
}
@ -91,7 +91,8 @@ export class FeedPublicComponent {
const fiveMinutesAgo = moment().subtract(5, 'minutes').unix();
this.sub = relay.sub([{ kinds: [1], since: fiveMinutesAgo }], {});
// Get the last 100 items.
this.sub = relay.sub([{ kinds: [1], limit: 100 }], {});
this.events = [];

View File

@ -27,6 +27,8 @@
<button (click)="subscribeEvents2()">Click me3</button>
<button (click)="downloadProfiles2()">Click me4</button>
<p>You can import your existing followers:</p>
<button class="follow-button" mat-flat-button color="primary" (click)="import(this.appState.getPublicKey())">Import your following list</button>
<br /><br />

View File

@ -169,6 +169,44 @@ export class HomeComponent {
}, 250);
}
downloadProfiles2() {
const array = [
'00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700',
'17e2889fba01021d048a13fd0ba108ad31c38326295460c21e69c43fa8fbe515',
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245',
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
'65594f279a789982b55c02a38c92a99b986f891d2814c5f553d1bbfe3e23853d',
'82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2',
'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98',
'd987084c48390a290f5d2a34603ae64f55137d9b4affced8c0eae030eb222a25',
'edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da',
];
const observable = this.profileService.getProfile2(array[0]).subscribe((profile) => {
console.log('GOT CACHED PROFILE:', profile);
});
// const observable = this.profileService.getProfile2(array[0]).dataService.downloadNewestProfiles(array).subscribe((profile) => {
// console.log('PROFILE RECEIVED:', profile);
// let doc = profile as NostrEventDocument;
// const index = array.findIndex((a) => a == doc.pubkey);
// if (index > -1) {
// array.splice(index, 1);
// }
// if (array.length === 0) {
// console.log('FOUND ALL!!!!');
// }
// });
setInterval(() => {
console.log('observable.closed:', observable.closed);
}, 250);
}
subscribeEvents() {
const observable = this.dataService.subscribeLatestEvents([1], [], 100).subscribe((event) => {
console.log('EVENT RECEIVED:', event);

View File

@ -0,0 +1,84 @@
import { Observable, Subject, of, tap, throwError } from 'rxjs';
interface CacheContent {
expiry: number;
value: any;
}
export class CacheService {
private cache: Map<string, CacheContent> = new Map<string, CacheContent>();
private inFlightObservables: Map<string, Subject<any>> = new Map<string, Subject<any>>();
readonly DEFAULT_MAX_AGE: number = 300000;
get(key: string, fallback?: Observable<any>, maxAge?: number): Observable<any> | Subject<any> {
if (this.hasValidCachedValue(key)) {
console.log(`%cGetting from cache ${key}`, 'color: green');
return of(this.cache.get(key)!.value);
}
if (!maxAge) {
maxAge = this.DEFAULT_MAX_AGE;
}
if (this.inFlightObservables.has(key)) {
return this.inFlightObservables.get(key)!;
} else if (fallback && fallback instanceof Observable) {
this.inFlightObservables.set(key, new Subject());
console.log(`%c Calling api for ${key}`, 'color: purple');
return fallback.pipe(
tap({
next: (val) => {
// on next 11, etc.
console.log('on next', val);
this.set(key, val, maxAge);
},
error: (error) => {
console.log('on error', error.message);
this.inFlightObservables.delete(key);
throwError(() => error);
},
complete: () => console.log('on complete'),
})
);
} else {
return throwError(() => 'Requested key is not available in Cache');
}
}
set(key: string, value: any, maxAge: number = this.DEFAULT_MAX_AGE): void {
this.cache.set(key, { value: value, expiry: Date.now() + maxAge });
this.notifyInFlightObservers(key, value);
}
has(key: string): boolean {
return this.cache.has(key);
}
private notifyInFlightObservers(key: string, value: any): void {
if (this.inFlightObservables.has(key)) {
const inFlight = this.inFlightObservables.get(key)!;
const observersCount = inFlight.observers.length;
if (observersCount) {
console.log(`%cNotifying ${inFlight.observers.length} flight subscribers for ${key}`, 'color: blue');
inFlight.next(value);
}
inFlight.complete();
this.inFlightObservables.delete(key);
}
}
private hasValidCachedValue(key: string): boolean {
if (this.cache.has(key)) {
if (this.cache.get(key)!.expiry < Date.now()) {
this.cache.delete(key);
return false;
}
return true;
} else {
return false;
}
}
}

View File

@ -2,14 +2,17 @@ import { Injectable } from '@angular/core';
import { Circle, NostrEventDocument, NostrNoteDocument, NostrProfile, NostrProfileDocument } from './interfaces';
import { StorageService } from './storage.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { liveQuery } from 'dexie';
import { DatabaseService } from './database.service';
@Injectable({
providedIn: 'root',
})
export class CirclesService {
static DEFAULT: Circle = { id: '', name: 'Following', color: '#e91e63', style: '1', public: true, created: Math.floor(Date.now() / 1000) };
static DEFAULT: Circle = { name: 'Following', color: '#e91e63', style: '1', public: true, created: Math.floor(Date.now() / 1000) };
private table;
private table2;
// Just a basic observable that triggers whenever any profile has changed.
#circlesChangedSubject: BehaviorSubject<void> = new BehaviorSubject<void>(undefined);
@ -22,8 +25,11 @@ export class CirclesService {
this.#circlesChangedSubject.next(undefined);
}
constructor(private storage: StorageService) {
circles$ = liveQuery(() => this.listCircles());
constructor(private storage: StorageService, private db: DatabaseService) {
this.table = this.storage.table<Circle>('circles');
this.table2 = this.db.circles;
}
async #filter(predicate: (value: Circle, key: string) => boolean): Promise<Circle[]> {
@ -34,7 +40,6 @@ export class CirclesService {
for await (const [key, value] of iterator) {
if (predicate(value, key)) {
value.id = key;
items.push(value);
}
}
@ -42,6 +47,10 @@ export class CirclesService {
return items;
}
async listCircles() {
return await this.db.circles.toArray();
}
async list() {
return this.#filter((value, key) => true);
}
@ -61,50 +70,38 @@ export class CirclesService {
}
}
async getCircle(id?: string) {
async getCircle(id?: number) {
if (!id) {
return CirclesService.DEFAULT;
}
const circle = await this.#get<Circle>(id);
let circle = await this.table2.get(id);
// const circle = await this.#get<Circle>(id);
if (!circle) {
return CirclesService.DEFAULT;
circle = CirclesService.DEFAULT;
circle.id = id;
}
circle.id = id;
return circle;
}
/** Circles are upserts, we replace the existing circles and only keep latest. */
async putCircle(document: Circle | any) {
const id = document.id;
// Remove the id from the document before we persist.
// delete document.id;
document.created = Math.floor(Date.now() / 1000);
console.log(document);
await this.table.put(id, document);
this.#changed();
await this.table2.put(document);
// this.#changed();
}
async deleteCircle(id: string) {
await this.table.del(id);
this.#changed();
async deleteCircle(id: number) {
await this.table2.delete(id);
// this.#changed();
}
/** Wipes all circles. */
async wipe() {
for await (const [key, value] of this.table.iterator({})) {
await this.table.del(key);
}
this.table2.clear();
this.#changed();
// this.#changed();
}
}

View File

@ -25,22 +25,22 @@ export class DataService {
constructor(
private appState: ApplicationState,
private storage: StorageService,
private profileService: ProfileService,
// private profileService: ProfileService,
private feedService: FeedService,
private validator: DataValidation,
private eventService: EventService,
private relayService: RelayService
) {
// Whenever the profile service needs to get a profile from the network, this event is triggered.
this.profileService.profileRequested$.subscribe(async (pubkey) => {
if (!pubkey) {
return;
}
// this.profileService.profileRequested$.subscribe(async (pubkey) => {
// if (!pubkey) {
// return;
// }
console.log('PROFILE REQUESTED:', pubkey);
// console.log('PROFILE REQUESTED:', pubkey);
await this.downloadProfile(pubkey);
});
// await this.downloadProfile(pubkey);
// });
}
async initialize() {
@ -59,9 +59,9 @@ export class DataService {
// If at startup we don't have the logged on user profile, queue it up for retreival.
// When requesting the profile, it will be auto-requested from relays.
setTimeout(async () => {
await this.profileService.getProfile(this.appState.getPublicKey());
}, 2000);
// setTimeout(async () => {
// await this.profileService.getProfile(this.appState.getPublicKey());
// }, 2000);
}
// async downloadProfiles() {
@ -287,7 +287,7 @@ export class DataService {
profile.created_at = prossedEvent.created_at;
// Persist the profile.
await this.profileService.updateProfile(prossedEvent.pubkey, profile);
// await this.profileService.updateProfile(prossedEvent.pubkey, profile);
// TODO: Add NIP-05 and nostr.directory verification.
// const displayName = encodeURIComponent(profile.name);

View File

@ -1,11 +1,6 @@
import { Injectable } from '@angular/core';
import Dexie, { Table } from 'dexie';
import { NostrNoteDocument, NostrProfileDocument } from './interfaces';
export interface CircleItem {
id: string;
name: string;
}
import { Circle, NostrNoteDocument, NostrProfileDocument } from './interfaces';
export interface RelayItem {
id?: number;
@ -31,7 +26,7 @@ export class DatabaseService extends Dexie {
events!: Table<EventItem, string>;
notes!: Table<NostrNoteDocument, string>;
profiles!: Table<NostrProfileDocument, string>;
circles!: Table<CircleItem, number>;
circles!: Table<Circle, number>;
constructor() {
super('blockcore');

View File

@ -1,7 +1,7 @@
import { Event, Relay, Sub } from 'nostr-tools';
export interface Circle {
id: string;
id?: number;
name: string;
color: string;
style: string;
@ -116,7 +116,7 @@ export interface NostrProfileDocument extends NostrProfile {
/** Indicates if a user is muted and not displayed in the home feed and notification is shown on replies. */
mute?: boolean;
circle?: string;
circle?: number;
/** List of domains where the user has been verified, e.g. "@nostr.directory", "@domain.com" */
verifications: string[];

View File

@ -7,6 +7,9 @@ import { ApplicationState } from './applicationstate.service';
import { Utilities } from './utilities.service';
import { DatabaseService } from './database.service';
import { liveQuery } from 'dexie';
import { Cacheable } from 'ts-cacheable';
import { CacheService } from './cache.service';
import { DataService } from './data.service';
@Injectable({
providedIn: 'root',
@ -16,6 +19,8 @@ export class ProfileService {
initialized = false;
cache = new CacheService();
// #profile: NostrProfileDocument;
/** TODO: Destroy this array when there are zero subscribers left. */
@ -92,7 +97,7 @@ export class ProfileService {
this.#profilesChangedSubject.next(undefined);
}
constructor(private db: DatabaseService, private storage: StorageService, private appState: ApplicationState, private utilities: Utilities) {
constructor(private db: DatabaseService, private storage: StorageService, private dataService: DataService, private appState: ApplicationState, private utilities: Utilities) {
this.table = this.storage.table<NostrProfileDocument>('profile');
}
@ -104,6 +109,22 @@ export class ProfileService {
this.#profileRequested.next(pubkey);
}
#getProfile(pubkey: string) {
return new Observable((observer) => {
this.table.get(pubkey).then((profile) => {
if (profile) {
observer.next(profile);
}
return this.dataService.downloadNewestProfiles([pubkey]);
});
});
}
getProfile2(pubkey: string) {
return this.cache.get(pubkey, this.#getProfile(pubkey));
}
// profileDownloadQueue: string[] = [];
/** Will attempt to get the profile from local storage, if not available will attempt to get from relays. */
@ -196,7 +217,7 @@ export class ProfileService {
return this.filter((value, key) => value.block == true);
}
async #setFollow(pubkey: string, circle?: string, follow?: boolean, existingProfile?: NostrProfileDocument) {
async #setFollow(pubkey: string, circle?: number, follow?: boolean, existingProfile?: NostrProfileDocument) {
let profile = await this.getProfile(pubkey);
const now = Math.floor(Date.now() / 1000);
@ -253,11 +274,11 @@ export class ProfileService {
}
}
async follow(pubkey: string, circle?: string, existingProfile?: NostrProfileDocument) {
async follow(pubkey: string, circle?: number, existingProfile?: NostrProfileDocument) {
return this.#setFollow(pubkey, circle, true, existingProfile);
}
async setCircle(pubkey: string, circle?: string) {
async setCircle(pubkey: string, circle?: number) {
return this.updateProfileValue(pubkey, (p) => {
p.circle = circle;
return p;

View File

@ -46,7 +46,7 @@ export class EventActionsComponent {
await this.notesService.deleteNote(this.event.id);
}
async follow(circle?: string) {
async follow(circle?: number) {
console.log('FOLLOW:', this.profile);
if (!this.profile) {

View File

@ -46,7 +46,7 @@ export class ProfileActionsComponent {
await this.notesService.deleteNote(this.event.id);
}
async follow(circle?: string) {
async follow(circle?: number) {
console.log('FOLLOW:', this.profile);
if (!this.profile) {