Files
Pete Miller 3e5b3073de createInterfaceApi - a browser proxy store (#29601)
* createInterfaceApi - a browser proxy store

A way to generate a state store browser proxy from any remote API, designed for mojom (or extension) APIs

The intention is to create something requiring minimal configuration and remove often repeated patterns to do with WebUI <-> Browser communication and
- storing and caching fetch results across UI components
- event listening to data updates and upadting local cache
- passing through actions to UI components
- mocking data for unit tests and storybook

This is not intended to replace all local state management. The intention is to manage only the data that is owned by a remote (e.g. the browser) and present it to the UI. Any local-only state should still be managed by the relevant framework (i.e. React, Lit, or Svelte) or a separate state manager.
However, this does add another relevant utility:

Used to send an instance of an api to a tree of React components. However, since it accepts any function it can also conveniently be used to store global state for that tree, using React hooks (`useState`, `useMemo`) or derived state from values retrieved from the API.
React Context is not that performant, as the whole tree will re-render when any pieces of state change, so this should be used sparingly. Instead, hooks for each individual endpoint should be used via the API instance which is passed down via Context. The API instance itself won't change and cause re-renders, but the individual hooks will cause subscriptions to be made to each piece of data.

A helper to generate endpoints from an interface. Prevents having to re-declare the mojom function signature as Typescript can deduce it from the generated mojom JS.

A helper to mark an interface function as an 'event'. Prevents having to re-declare the mojom function signature as Typescript can deduce it from the generated mojom JS.

See `components/common/api/readme.md` for examples.
2026-02-08 15:54:40 -08:00

85 lines
2.7 KiB
TypeScript

// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { ArgsOfMethod, MethodKeys } from './api_utils'
import { EventDef, event } from './create_interface_api'
export function partialReceiver<
Interface extends {},
Receiver,
const PartialInterface extends Partial<Interface>,
>(
Type: new () => Interface,
Receiver: new (handler: Interface) => Receiver,
handler: PartialInterface,
) {
const observer = Object.create(Type.prototype) as Interface
Object.assign(observer, handler)
return new Receiver(observer)
}
export function eventsFor<
Interface extends {},
const PartialInterface extends Partial<Interface>,
>(
Type: new () => Interface,
/**
* Partial interface that contains only the methods that are to be observed
*/
handler: PartialInterface,
/**
* Function that provides the constructed receiver to the caller so that it
* can be bound.
*/
observerHandler: (observer: Interface) => void,
) {
const observer = Object.create(Type.prototype) as Interface
Object.assign(observer, handler)
type ValidKeys = Extract<keyof PartialInterface, MethodKeys<Interface>>
// TODO: only bubble up events that are asked for. Some events may just want to be
// handled in the API definition so that all events are handled in the same place.
type Events = {
[K in keyof PartialInterface]: EventDef<
[],
PartialInterface[K] extends (...args: any) => any
? Parameters<PartialInterface[K]>
: never
>
}
// Create 'events' that can be used by createInterfaceAPI
const events = {} as Events
for (const key of Object.keys(handler) as ValidKeys[]) {
const originalHandler = (observer as any)[key]
let apiEventEmitter: (args: ArgsOfMethod<Interface, typeof key>) => void
const newHandler = (...args: ArgsOfMethod<Interface, typeof key>) => {
// Call the original handler with the arguments
;(originalHandler as Function).call(observer, ...args)
// Emit the event with the arguments
if (apiEventEmitter) {
;(apiEventEmitter! as any)(...(args as unknown as []))
} else {
console.warn('event emitter was not provided for event', key)
}
}
// Original is new
;(observer as any)[key] = newHandler
// Handle each event and fire to the API emitter
;(events as any)[key] = event((emitter) => {
// register new handler for the event, so we can fire on the API
apiEventEmitter = emitter
})
}
// Allow the caller to bind the receiver
observerHandler(observer)
return events
}