AI refactor

This commit is contained in:
Jon Staab
2026-06-16 09:22:26 -07:00
parent 28339976b9
commit ea9cc0bf26
34 changed files with 2601 additions and 227 deletions
+169 -37
View File
@@ -1,7 +1,57 @@
import {Maybe} from '@welshman/lib'
import {Repository, AdapterFactory, NetContext, WrapManager, DiffOptions, PullOptions, PushOptions, RequestOptions, PublishOptions, LoaderOptions, Tracker, Pool, push, pull, diff, publish, request, makeLoader} from '@welshman/net'
import {RelayStats} from './relayStats.js'
import {User} from './user.js'
import type {Readable, Unsubscriber} from "svelte/store"
import {isDVMKind, isEphemeralKind} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {
Pool,
Socket,
SocketEvent,
Tracker,
Repository,
WrapManager,
defaultSocketPolicies,
netContext,
isRelayEvent,
request,
publish,
diff,
pull,
push,
makeLoader,
} from "@welshman/net"
import type {
AdapterContext,
AdapterFactory,
SocketPolicy,
RelayMessage,
Loader,
LoaderOptions,
RequestOptions,
PublishOptions,
DiffOptions,
PullOptions,
PushOptions,
} from "@welshman/net"
import {
getEventsById,
deriveEventsById,
deriveEvents,
makeDeriveEvent,
getEventsByIdByUrl,
deriveEventsByIdByUrl,
getEventsByIdForUrl,
deriveEventsByIdForUrl,
deriveItemsByKey,
deriveIsDeleted,
} from "@welshman/store"
import type {
EventsByIdOptions,
EventOptions,
EventsByIdByUrlOptions,
EventsByIdForUrlOptions,
ItemsByKey,
ItemsByKeyOptions,
} from "@welshman/store"
import type {User} from "./user.js"
export type ClientOptions = {
user?: User
@@ -9,26 +59,82 @@ export type ClientOptions = {
socketPolicies?: SocketPolicy[]
}
export class Client {
/**
* The narrow seam that data modules (plugins) depend on instead of the concrete
* `Client`. Because plugins reference only `ClientContext`, the client never
* imports them and they never create a dependency cycle back to the client —
* the dependency is strictly one-way (plugin -> context).
*
* `Client implements ClientContext`, so the two stay in sync by construction.
*/
export interface ClientContext {
user?: User
pool: Pool
tracker: Tracker
repository: Repository
wrapManager: WrapManager
netContext: NetContext
relayStats: RelayStats
constructor(options: ClientOptions) {
// Net utilities, with this client's context baked in
request: (options: Omit<RequestOptions, "context">) => ReturnType<typeof request>
publish: (options: Omit<PublishOptions, "context">) => ReturnType<typeof publish>
diff: (options: Omit<DiffOptions, "context">) => ReturnType<typeof diff>
pull: (options: Omit<PullOptions, "context">) => ReturnType<typeof pull>
push: (options: Omit<PushOptions, "context">) => ReturnType<typeof push>
makeLoader: (options: Omit<LoaderOptions, "context">) => Loader
load: Loader
// Store utilities, with this client's repository/tracker baked in
getEventsById: (options: Omit<EventsByIdOptions, "repository">) => ReturnType<typeof getEventsById>
deriveEventsById: (
options: Omit<EventsByIdOptions, "repository">,
) => ReturnType<typeof deriveEventsById>
deriveEvents: (options: Omit<EventsByIdOptions, "repository">) => ReturnType<typeof deriveEvents>
makeDeriveEvent: (options: Omit<EventOptions, "repository">) => ReturnType<typeof makeDeriveEvent>
getEventsByIdByUrl: (
options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">,
) => ReturnType<typeof getEventsByIdByUrl>
deriveEventsByIdByUrl: (
options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">,
) => ReturnType<typeof deriveEventsByIdByUrl>
getEventsByIdForUrl: (
options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">,
) => ReturnType<typeof getEventsByIdForUrl>
deriveEventsByIdForUrl: (
options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">,
) => ReturnType<typeof deriveEventsByIdForUrl>
deriveItemsByKey: <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) => Readable<ItemsByKey<T>>
deriveIsDeleted: (event: TrustedEvent) => ReturnType<typeof deriveIsDeleted>
}
/**
* The core of an application instance. Owns the primitives a single identity
* needs (so data never bleeds across sessions) — a private repository, a socket
* pool, a tracker, a wrap manager — plus net/store helpers bound to them.
*
* Data modules are NOT fields on the client; they are composed separately (see
* `createApp`) and reach the client through the `ClientContext` seam.
*/
export class Client implements ClientContext {
user?: User
pool: Pool
tracker: Tracker
repository: Repository
wrapManager: WrapManager
netContext: AdapterContext
load: Loader
ingestCleanup: Unsubscriber
constructor(options: ClientOptions = {}) {
this.user = options.user
this.pool = new Pool({
makeSocket: (url: string) => {
const socketPolicies = options.socketPolicies ?? defaultSocketPolicies
let socketPolicies = options.socketPolicies ?? defaultSocketPolicies
if (this.user) {
socketPolicies = [...socketPolicies, this.user.makeSocketPolicyAuth()]
}
return makeSocket(url, socketPolicies)
return new Socket(url, socketPolicies)
},
})
this.tracker = new Tracker()
@@ -47,58 +153,84 @@ export class Client {
timeout: 3000,
threshold: 0.5,
})
// Ingest every event received on any socket into this client's repository.
// The net layer doesn't do this for us, and it's how all the repository-
// backed collections (and gift-wrap unwrapping) get populated.
this.ingestCleanup = this.pool.subscribe(socket => {
const onReceive = (message: RelayMessage) => this.ingest(message, socket.url)
socket.on(SocketEvent.Receive, onReceive)
return () => socket.off(SocketEvent.Receive, onReceive)
})
}
ingest = (message: RelayMessage, url: string) => {
if (!isRelayEvent(message)) return
const event = message[2]
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) return
if (!netContext.isEventValid(event, url)) return
this.tracker.track(event.id, url)
this.repository.publish(event)
}
cleanup() {
this.ingestCleanup()
this.pool.clear()
this.tracker.clear()
this.repository.clear()
this.wrapManager.clear()
this.relayStats.cleanup()
}
// Bind net utilities to client net context
// Net utilities, bound to this client's context
request = (options: RequestOptions) => request({...context: this.netContext, ...options})
request = (options: Omit<RequestOptions, "context">) =>
request({...options, context: this.netContext})
makeLoader = (options: LoaderOptions) => makeLoader({...context: this.netContext, ...options})
publish = (options: Omit<PublishOptions, "context">) =>
publish({...options, context: this.netContext})
publish = (options: PublishOptions) => publish({...context: this.netContext, ...options})
diff = (options: Omit<DiffOptions, "context">) => diff({...options, context: this.netContext})
diff = (options: DiffOptions) => diff({...context: this.netContext, ...options})
pull = (options: Omit<PullOptions, "context">) => pull({...options, context: this.netContext})
pull = (options: PullOptions) => pull({...context: this.netContext, ...options})
push = (options: Omit<PushOptions, "context">) => push({...options, context: this.netContext})
push = (options: PushOptions) => push({...context: this.netContext, ...options})
makeLoader = (options: Omit<LoaderOptions, "context">): Loader =>
makeLoader({...options, context: this.netContext})
// Bind store utilities to client stuff
// Store utilities, bound to this client's repository/tracker
getEventsById = (options: Omit<EventsByIdOptions 'repository'>) =>
getEventsById({repository: this.repository, ...options})
getEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
getEventsById({...options, repository: this.repository})
deriveEventsById = (options: Omit<EventsByIdOptions 'repository'>) =>
deriveEventsById({repository: this.repository, ...options})
deriveEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
deriveEventsById({...options, repository: this.repository})
deriveEvents = (options: Omit<EventsByIdOptions 'repository'>) =>
deriveEvents({repository: this.repository, ...options})
deriveEvents = (options: Omit<EventsByIdOptions, "repository">) =>
deriveEvents({...options, repository: this.repository})
makeDeriveEvent = (options: Omit<EventOptions, 'repository'>) =>
makeDeriveEvent({repository: this.repository, ...options})
makeDeriveEvent = (options: Omit<EventOptions, "repository">) =>
makeDeriveEvent({...options, repository: this.repository})
getEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, 'tracker' | 'repository'>) =>
getEventsByIdByUrl({tracker: this.tracker, repository: this.repository, ...options})
getEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
getEventsByIdByUrl({...options, tracker: this.tracker, repository: this.repository})
deriveEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, 'tracker' | 'repository'>) =>
deriveEventsByIdByUrl({tracker: this.tracker, repository: this.repository, ...options})
deriveEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
deriveEventsByIdByUrl({...options, tracker: this.tracker, repository: this.repository})
getEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, 'tracker' | 'repository'>) =>
getEventsByIdForUrl({tracker: this.tracker, repository: this.repository, ...options})
getEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
getEventsByIdForUrl({...options, tracker: this.tracker, repository: this.repository})
deriveEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, 'tracker' | 'repository'>)
deriveEventsByIdForUrl({tracker: this.tracker, repository: this.repository, ...options})
deriveEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
deriveEventsByIdForUrl({...options, tracker: this.tracker, repository: this.repository})
deriveItemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, 'repository'>) =>
deriveEventsByIdForUrl({repository: this.repository, ...options})
deriveItemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) =>
deriveItemsByKey<T>({...options, repository: this.repository})
deriveIsDeleted = (event: TrustedEvent) => deriveIsDeleted(this.repository, event)
}