system-worker progress
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Kieran 2024-01-09 09:28:48 +00:00
parent 24e145a0a0
commit bbdfb43834
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
10 changed files with 124 additions and 53 deletions

View File

@ -4,7 +4,7 @@ import { FormattedMessage } from "react-intl";
import { injectIntl } from "react-intl"; import { injectIntl } from "react-intl";
import messages from "@/Components/messages"; import messages from "@/Components/messages";
import { ProfileLoader } from "@/system"; import { System } from "@/system";
import { LoginStore } from "@/Utils/Login"; import { LoginStore } from "@/Utils/Login";
import AccountName from "./AccountName"; import AccountName from "./AccountName";
@ -285,8 +285,8 @@ class IrisAccount extends Component<Props> {
componentDidMount() { componentDidMount() {
const session = LoginStore.snapshot(); const session = LoginStore.snapshot();
const myPub = session.publicKey; const myPub = session.publicKey;
ProfileLoader.Cache.hook(() => { System.ProfileLoader.Cache.hook(() => {
const profile = ProfileLoader.Cache.getFromCache(myPub); const profile = System.ProfileLoader.Cache.getFromCache(myPub);
const irisToActive = profile && profile.nip05 && profile.nip05.endsWith("@iris.to"); const irisToActive = profile && profile.nip05 && profile.nip05.endsWith("@iris.to");
this.setState({ profile, irisToActive }); this.setState({ profile, irisToActive });
if (profile && !irisToActive) { if (profile && !irisToActive) {

View File

@ -28,7 +28,7 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
const q = system.Query(NoopStore, sub); const q = system.Query(NoopStore, sub);
let t: ReturnType<typeof setTimeout> | undefined; let t: ReturnType<typeof setTimeout> | undefined;
let tBuf: Array<TaggedNostrEvent> = []; let tBuf: Array<TaggedNostrEvent> = [];
q.feed.on("event", evs => { q.on("event", evs => {
if (!t) { if (!t) {
tBuf = [...evs]; tBuf = [...evs];
t = setTimeout(() => { t = setTimeout(() => {
@ -41,9 +41,8 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
}); });
q.uncancel(); q.uncancel();
return () => { return () => {
q.feed.off("event"); q.off("event");
q.cancel(); q.cancel();
q.sendClose();
}; };
} }
}, [sub]); }, [sub]);

View File

@ -34,6 +34,7 @@ export default defineConfig({
assetsInclude: ["**/*.md", "**/*.wasm"], assetsInclude: ["**/*.md", "**/*.wasm"],
build: { build: {
outDir: "build", outDir: "build",
commonjsOptions: { transformMixedEsModules: true },
}, },
clearScreen: false, clearScreen: false,
publicDir: appConfig.get("publicDir"), publicDir: appConfig.get("publicDir"),
@ -51,4 +52,7 @@ export default defineConfig({
globals: true, globals: true,
environment: "jsdom", environment: "jsdom",
}, },
worker: {
format: "es",
},
}); });

View File

@ -14,10 +14,10 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
const subscribe = (onChanged: () => void) => { const subscribe = (onChanged: () => void) => {
if (rb) { if (rb) {
const q = system.Query<TStore>(type, rb); const q = system.Query<TStore>(type, rb);
q.feed.on("event", onChanged); q.on("event", onChanged);
q.uncancel(); q.uncancel();
return () => { return () => {
q.feed.off("event", onChanged); q.off("event", onChanged);
q.cancel(); q.cancel();
}; };
} }
@ -28,7 +28,7 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
const getState = (): StoreSnapshot<TSnapshot> => { const getState = (): StoreSnapshot<TSnapshot> => {
const q = system.GetQuery(rb?.id ?? ""); const q = system.GetQuery(rb?.id ?? "");
if (q) { if (q) {
return unwrap(q).feed?.snapshot as StoreSnapshot<TSnapshot>; return q.snapshot as StoreSnapshot<TSnapshot>;
} }
return EmptySnapshot as StoreSnapshot<TSnapshot>; return EmptySnapshot as StoreSnapshot<TSnapshot>;
}; };

View File

@ -1,7 +1,6 @@
import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection";
import { RequestBuilder } from "./request-builder"; import { RequestBuilder } from "./request-builder";
import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { NoteStore, NoteStoreSnapshotData, StoreSnapshot } from "./note-collection";
import { Query } from "./query";
import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr"; import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr";
import { ProfileLoaderService } from "./profile-cache"; import { ProfileLoaderService } from "./profile-cache";
import { RelayCache } from "./outbox-model"; import { RelayCache } from "./outbox-model";
@ -43,7 +42,15 @@ export * from "./cache/user-relays";
export * from "./cache/user-metadata"; export * from "./cache/user-metadata";
export * from "./cache/relay-metric"; export * from "./cache/relay-metric";
export * from "./worker"; export * from "./worker/system-worker";
export interface QueryLike {
on: (event: "event", fn?: (evs: Array<TaggedNostrEvent>) => void) => void;
off: (event: "event", fn?: (evs: Array<TaggedNostrEvent>) => void) => void;
cancel: () => void;
uncancel: () => void;
get snapshot(): StoreSnapshot<NoteStoreSnapshotData>;
}
export interface SystemInterface { export interface SystemInterface {
/** /**
@ -65,14 +72,14 @@ export interface SystemInterface {
* Get an active query by ID * Get an active query by ID
* @param id Query ID * @param id Query ID
*/ */
GetQuery(id: string): Query | undefined; GetQuery(id: string): QueryLike | undefined;
/** /**
* Open a new query to relays * Open a new query to relays
* @param type Store type * @param type Store type
* @param req Request to send to relays * @param req Request to send to relays
*/ */
Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): Query; Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): QueryLike;
/** /**
* Fetch data from nostr relays asynchronously * Fetch data from nostr relays asynchronously

View File

@ -20,6 +20,7 @@ import {
UsersRelays, UsersRelays,
SnortSystemDb, SnortSystemDb,
EventExt, EventExt,
QueryLike,
} from "."; } from ".";
import { EventsCache } from "./cache/events"; import { EventsCache } from "./cache/events";
import { RelayCache, RelayMetadataLoader } from "./outbox-model"; import { RelayCache, RelayMetadataLoader } from "./outbox-model";
@ -224,16 +225,16 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
this.#pool.disconnect(address); this.#pool.disconnect(address);
} }
GetQuery(id: string): Query | undefined { GetQuery(id: string): QueryLike | undefined {
return this.#queryManager.get(id); return this.#queryManager.get(id) as QueryLike;
} }
Fetch(req: RequestBuilder, cb?: (evs: ReadonlyArray<TaggedNostrEvent>) => void) { Fetch(req: RequestBuilder, cb?: (evs: ReadonlyArray<TaggedNostrEvent>) => void) {
return this.#queryManager.fetch(req, cb); return this.#queryManager.fetch(req, cb);
} }
Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): Query { Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): QueryLike {
return this.#queryManager.query(type, req); return this.#queryManager.query(type, req) as QueryLike;
} }
async #sendQuery(q: Query, qSend: BuiltRawReqFilter) { async #sendQuery(q: Query, qSend: BuiltRawReqFilter) {

View File

@ -116,6 +116,7 @@ export interface TraceReport {
interface QueryEvents { interface QueryEvents {
trace: (report: TraceReport) => void; trace: (report: TraceReport) => void;
event: (evs: ReadonlyArray<TaggedNostrEvent>) => void;
} }
/** /**
@ -172,6 +173,8 @@ export class Query extends EventEmitter<QueryEvents> implements QueryBase {
this.#leaveOpen = leaveOpen ?? false; this.#leaveOpen = leaveOpen ?? false;
this.#timeout = timeout ?? 5_000; this.#timeout = timeout ?? 5_000;
this.#checkTraces(); this.#checkTraces();
this.feed.on("event", evs => this.emit("event", evs));
} }
isOpen() { isOpen() {
@ -193,6 +196,10 @@ export class Query extends EventEmitter<QueryEvents> implements QueryBase {
return this.#feed; return this.#feed;
} }
get snapshot() {
return this.#feed.snapshot;
}
handleEvent(sub: string, e: TaggedNostrEvent) { handleEvent(sub: string, e: TaggedNostrEvent) {
for (const t of this.#tracing) { for (const t of this.#tracing) {
if (t.id === sub || sub === "*") { if (t.id === sub || sub === "*") {

View File

@ -4,6 +4,8 @@ export const enum WorkerCommand {
Init, Init,
ConnectRelay, ConnectRelay,
DisconnectRelay, DisconnectRelay,
Query,
QueryResult,
} }
export interface WorkerMessage<T> { export interface WorkerMessage<T> {

View File

@ -1,9 +1,11 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import { NostrSystem, NostrsystemProps } from "../nostr-system"; import { NostrSystem } from "../nostr-system";
import { WorkerMessage, WorkerCommand } from "."; import { WorkerMessage, WorkerCommand } from ".";
let system: NostrSystem | undefined; const system = new NostrSystem({
checkSigs: true,
});
function reply<T>(id: string, type: WorkerCommand, data: T) { function reply<T>(id: string, type: WorkerCommand, data: T) {
globalThis.postMessage({ globalThis.postMessage({
@ -18,31 +20,19 @@ function okReply(id: string, message?: string) {
function errorReply(id: string, message: string) { function errorReply(id: string, message: string) {
reply<string>(id, WorkerCommand.ErrorResponse, message); reply<string>(id, WorkerCommand.ErrorResponse, message);
} }
function checkInitialized() {
if (system === undefined) {
throw new Error("System not initialized");
}
}
globalThis.onmessage = async ev => { globalThis.onmessage = async ev => {
console.debug(ev);
const data = ev.data as { id: string; type: WorkerCommand }; const data = ev.data as { id: string; type: WorkerCommand };
try { try {
switch (data.type) { switch (data.type) {
case WorkerCommand.Init: { case WorkerCommand.Init: {
const cmd = ev.data as WorkerMessage<NostrsystemProps>;
if (system === undefined) {
system = new NostrSystem(cmd.data);
await system.Init(); await system.Init();
okReply(data.id); okReply(data.id);
} else {
errorReply(data.id, "System is already initialized");
}
break; break;
} }
case WorkerCommand.ConnectRelay: { case WorkerCommand.ConnectRelay: {
checkInitialized();
const cmd = ev.data as WorkerMessage<[string, { read: boolean; write: boolean }]>; const cmd = ev.data as WorkerMessage<[string, { read: boolean; write: boolean }]>;
await system?.ConnectToRelay(cmd.data[0], cmd.data[1]); await system.ConnectToRelay(cmd.data[0], cmd.data[1]);
okReply(data.id, "Connected"); okReply(data.id, "Connected");
break; break;
} }

View File

@ -12,51 +12,110 @@ import {
RequestBuilder, RequestBuilder,
SystemInterface, SystemInterface,
TaggedNostrEvent, TaggedNostrEvent,
CachedMetadata,
DefaultOptimizer,
RelayMetadataLoader,
RelayMetricCache,
RelayMetrics,
UserProfileCache,
UserRelaysCache,
UsersRelays,
QueryLike,
} from ".."; } from "..";
import { NostrSystemEvents, NostrsystemProps } from "../nostr-system"; import { NostrSystemEvents, NostrsystemProps } from "../nostr-system";
import { Query } from "../query";
import { WorkerCommand, WorkerMessage } from "."; import { WorkerCommand, WorkerMessage } from ".";
import { FeedCache } from "@snort/shared";
import { EventsCache } from "../cache/events";
import { RelayMetricHandler } from "../relay-metric-handler";
import debug from "debug";
export class SystemWorker extends EventEmitter<NostrSystemEvents> implements SystemInterface { export class SystemWorker extends EventEmitter<NostrSystemEvents> implements SystemInterface {
#log = debug("SystemWorker");
#worker: Worker; #worker: Worker;
#commandQueue: Map<string, (v: unknown) => void> = new Map(); #commandQueue: Map<string, (v: unknown) => void> = new Map();
checkSigs: boolean; #relayCache: FeedCache<UsersRelays>;
#profileCache: FeedCache<CachedMetadata>;
#relayMetricsCache: FeedCache<RelayMetrics>;
#profileLoader: ProfileLoaderService;
#relayMetrics: RelayMetricHandler;
#eventsCache: FeedCache<NostrEvent>;
#relayLoader: RelayMetadataLoader;
get checkSigs() {
return true;
}
set checkSigs(v: boolean) {
// not used
}
constructor(scriptPath: string, props: NostrsystemProps) { constructor(scriptPath: string, props: NostrsystemProps) {
super(); super();
this.checkSigs = props.checkSigs ?? false;
this.#relayCache = props.relayCache ?? new UserRelaysCache(props.db?.userRelays);
this.#profileCache = props.profileCache ?? new UserProfileCache(props.db?.users);
this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache(props.db?.relayMetrics);
this.#eventsCache = props.eventsCache ?? new EventsCache(props.db?.events);
this.#profileLoader = new ProfileLoaderService(this, this.#profileCache);
this.#relayMetrics = new RelayMetricHandler(this.#relayMetricsCache);
this.#relayLoader = new RelayMetadataLoader(this, this.#relayCache);
this.#worker = new Worker(scriptPath, { this.#worker = new Worker(scriptPath, {
name: "SystemWorker", name: "SystemWorker",
type: "module",
}); });
this.#worker.onmessage = async e => {
const cmd = e.data as { id: string; type: WorkerCommand; data?: unknown };
if (cmd.type === WorkerCommand.OkResponse) {
const q = this.#commandQueue.get(cmd.id);
q?.(cmd.data);
this.#commandQueue.delete(cmd.id);
}
};
} }
get Sockets(): ConnectionStateSnapshot[] { get Sockets(): ConnectionStateSnapshot[] {
throw new Error("Method not implemented."); return [];
} }
async Init() { async Init() {
await this.#workerRpc<void, string>(WorkerCommand.Init, undefined); await this.#workerRpc(WorkerCommand.Init);
} }
GetQuery(id: string): Query | undefined { GetQuery(id: string): QueryLike | undefined {
return undefined; return undefined;
} }
Query<T extends NoteStore>(type: new () => T, req: RequestBuilder): Query { Query<T extends NoteStore>(type: new () => T, req: RequestBuilder): QueryLike {
throw new Error("Method not implemented."); const chan = this.#workerRpc<[RequestBuilder], { id: string; port: MessagePort }>(WorkerCommand.Query, [req]);
return {
on: (_: "event", cb) => {
chan.then(c => {
c.port.onmessage = e => {
cb?.(e.data as Array<TaggedNostrEvent>);
};
});
},
off: (_: "event", cb) => {
chan.then(c => {
c.port.close();
});
},
cancel: () => {},
uncancel: () => {},
} as QueryLike;
} }
Fetch(req: RequestBuilder, cb?: ((evs: TaggedNostrEvent[]) => void) | undefined): Promise<TaggedNostrEvent[]> { Fetch(req: RequestBuilder, cb?: ((evs: TaggedNostrEvent[]) => void) | undefined): Promise<TaggedNostrEvent[]> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
ConnectToRelay(address: string, options: RelaySettings): Promise<void> { async ConnectToRelay(address: string, options: RelaySettings) {
throw new Error("Method not implemented."); await this.#workerRpc(WorkerCommand.ConnectRelay, [address, options, false]);
} }
DisconnectRelay(address: string): void { DisconnectRelay(address: string): void {
throw new Error("Method not implemented."); this.#workerRpc(WorkerCommand.DisconnectRelay, address);
} }
HandleEvent(ev: TaggedNostrEvent): void { HandleEvent(ev: TaggedNostrEvent): void {
@ -72,24 +131,26 @@ export class SystemWorker extends EventEmitter<NostrSystemEvents> implements Sys
} }
get ProfileLoader(): ProfileLoaderService { get ProfileLoader(): ProfileLoaderService {
throw new Error("Method not implemented."); return this.#profileLoader;
} }
get RelayCache(): RelayCache { get RelayCache(): RelayCache {
throw new Error("Method not implemented."); return this.#relayCache;
} }
get Optimizer(): Optimizer { get Optimizer(): Optimizer {
throw new Error("Method not implemented."); return DefaultOptimizer;
} }
#workerRpc<T, R>(type: WorkerCommand, data: T, timeout = 5_000) { #workerRpc<T, R>(type: WorkerCommand, data?: T, timeout = 5_000) {
const id = uuid(); const id = uuid();
this.#worker.postMessage({ const msg = {
id, id,
type, type,
data, data,
} as WorkerMessage<T>); } as WorkerMessage<T>;
this.#log(msg);
this.#worker.postMessage(msg);
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
let t: ReturnType<typeof setTimeout>; let t: ReturnType<typeof setTimeout>;
this.#commandQueue.set(id, v => { this.#commandQueue.set(id, v => {