accounts
Version:
Tempo Accounts SDK
217 lines (187 loc) • 6.71 kB
text/typescript
import { Provider as ox_Provider, RpcRequest } from 'ox'
import type * as Dialog from '../Dialog.js'
import type * as Remote from '../Remote.js'
import type * as core_Store from '../Store.js'
/** Request stored by the host request coordinator. */
type StoredRequest = {
/** Active account captured from the provider that enqueued the request. */
account: { address: string } | undefined
/** Chain ID captured from the provider that enqueued the request. */
chainId: number
/** Rejects the provider request promise. */
reject: (error: Error) => void
/** JSON-RPC request sent to the remote. */
request: RpcRequest.RpcRequest
/** Resolves the provider request promise. */
resolve: (result: unknown) => void
/** Request is waiting for a remote response. */
status: 'pending'
/** Whether this pending request has been synced to the remote UI. */
synced: boolean
}
/** Host-scoped remote request context. */
type Context = {
/** Active provider attachments. */
attachments: Set<symbol>
/** Active dialog transport. */
dialog: Dialog.Instance | undefined
/** JSON-RPC request ID allocator. */
ids: ReturnType<typeof RpcRequest.createStore>
/** In-memory pending request queue. */
requestQueue: StoredRequest[]
/** Whether pending work has been synced since the last empty queue. */
synced: boolean
}
/** Provider attachment options for a host request coordinator. */
export type AttachOptions = {
/** Dialog implementation used to communicate with the remote app. */
dialog: Dialog.Dialog
/** Remote host URL. */
host: string
/** Provider store used by the dialog transport for account sync and referrer setup. */
store: core_Store.Store
/** Visual theme overrides applied to the embed. */
theme?: Dialog.Theme | undefined
}
/** Remote request enqueue options. */
export type RequestOptions = {
/** Active account captured from the provider enqueueing the request. */
account: { address: string } | undefined
/** Active chain ID captured from the provider enqueueing the request. */
chainId: number
/** Provider RPC request input. */
request: unknown
}
const contexts = new Map<string, Context>()
/** Attaches a provider to the host request coordinator and starts the transport driver. */
export function attach(host: string, options: AttachOptions): () => void {
const context = get(host)
const attachment = Symbol()
context.attachments.add(attachment)
const next = options.dialog({
host: options.host,
onReject: (ids) => reject(host, ids),
onResponse: (response) => respond(host, response),
store: options.store,
theme: options.theme,
})
const changed = Boolean(context.dialog && context.dialog !== next)
if (context.dialog && context.dialog !== next) context.dialog.destroy()
context.dialog = next
if (changed) for (const request of context.requestQueue) request.synced = false
sync(host)
return () => {
context.attachments.delete(attachment)
release(host)
}
}
/** Enqueues an RPC request in the host request coordinator and waits for completion. */
export function request(host: string, options: RequestOptions): Promise<unknown> {
const context = get(host)
const request = context.ids.prepare(options.request as never)
return new Promise((resolve, reject) => {
context.requestQueue.push({
account: options.account,
chainId: options.chainId,
reject,
request,
resolve,
status: 'pending',
synced: false,
})
sync(host)
})
}
/** Clears a host-scoped request coordinator. */
export function reset(host: string): void {
const context = contexts.get(key(host))
context?.dialog?.destroy()
contexts.delete(key(host))
}
/** Converts a host URL into a request coordinator key. */
function key(host: string): string {
return new URL(host).host
}
/** Returns the request context shared by remotes on the same host. */
function get(host: string): Context {
const key_ = key(host)
const current = contexts.get(key_)
if (current) return current
const context = {
attachments: new Set<symbol>(),
dialog: undefined,
ids: RpcRequest.createStore(),
requestQueue: [],
synced: false,
}
contexts.set(key_, context)
return context
}
/** Releases idle transport resources. */
function release(host: string) {
const context = get(host)
if (context.attachments.size > 0) return
if (context.requestQueue.length > 0) return
context.dialog?.destroy()
context.dialog = undefined
context.synced = false
}
/** Marks displayed pending requests as rejected. */
function reject(host: string, ids: readonly number[]) {
if (ids.length === 0) return
const context = get(host)
const ids_ = new Set(ids)
const rejected = context.requestQueue.filter((queued) => ids_.has(queued.request.id))
if (rejected.length === 0) return
context.requestQueue = context.requestQueue.filter((queued) => !ids_.has(queued.request.id))
for (const request of rejected) request.reject(new ox_Provider.UserRejectedRequestError())
sync(host, { force: true })
}
/** Resolves or rejects a pending request with an RPC response from the remote UI. */
function respond(
host: string,
response: { id: number; result?: unknown; error?: { code: number; message: string } | undefined },
) {
const context = get(host)
const queued = context.requestQueue.find((queued) => queued.request.id === response.id)
if (!queued) return
context.requestQueue = context.requestQueue.filter((queued) => queued.request.id !== response.id)
if (response.error) queued.reject(ox_Provider.parseError(response.error))
else queued.resolve(response.result)
sync(host, { force: true })
}
/** Syncs newly pending host requests to the remote dialog. */
function sync(host: string, options: sync.Options = {}) {
const context = get(host)
const dialog = context.dialog
if (!dialog) return
const pending = context.requestQueue
if (pending.length === 0) {
if (context.synced) dialog.close()
context.synced = false
release(host)
return
}
const request = pending[0]!
if (!options.force && request.synced) return
request.synced = true
context.synced = true
dialog.syncRequests({
account: request.account,
chainId: request.chainId,
requests: [toRequest(request)],
})
}
declare namespace sync {
type Options = {
/** Whether to send the current pending queue even when all requests were previously synced. */
force?: boolean | undefined
}
}
/** Removes host-only metadata before syncing requests to the remote UI. */
function toRequest(request: StoredRequest): Remote.Request {
return {
request: request.request,
status: request.status,
}
}