Skip to main content

API reference

Everything on this page is exported from @powerhousedao/reactor-browser.

import {
GraphQLReactorClient,
GraphQLReactorProvider,
useSwitchboardClient,
type GraphQLReactorClientOptions,
type GraphQLReactorProviderProps,
type IReactorBrowserClient,
} from "@powerhousedao/reactor-browser/graphql-client";

For the quickstart and how the entry stays browser-bundlable, see Overview and quickstart.

IReactorBrowserClient

The contract the reactor-browser hooks depend on. It is declared as a Pick of the reactor's own IReactorClient, so the six member signatures cannot drift from the reactor's:

export interface IReactorBrowserClient extends Pick<
IReactorClient,
| "get"
| "subscribe"
| "execute"
| "getOperations"
| "create"
| "deleteDocument"
> {}

Two implementations satisfy it: the full in-browser reactor client, which gets it for free by implementing IReactorClient, and GraphQLReactorClient.

Everything outside those six members is deliberately excluded and not implemented here: drives, find, resolveIdOrSlug, relationships, executeAsync, executeBatch, jobs, createEmpty, rename, and the document-model module getters. For the full-reactor versions of those, see IReactorClient.

Methods

MethodBehaviour on the light client
get(identifier, view?, signal?)Reads by id or slug. A view.revision throws: the GraphQL read API has no point-in-time reads.
getOperations(identifier, view?, filter?, paging?, signal?)Paged. next() follows the returned cursor; the default page is { cursor: "0", limit: 100 }.
create(document, parentIdentifier?, signal?)Creates the document and emits Created.
execute(identifier, branch, actions, signal?)Reads the document, stamps and signs a single action, pushes it, emits Updated. Two round trips.
deleteDocument(identifier, propagate?, signal?)Deletes and emits Deleted carrying the identifier it was called with.
subscribe(search, callback, view?)Registers a change subscriber and returns its unsubscribe function. view is accepted and ignored.

execute returns the updated document with the operations this call appended, which is how useDispatch reports per-action errors: dispatchActions scans the returned operations for the action it sent and reads its error field.

GraphQLReactorClient

new GraphQLReactorClient(options: GraphQLReactorClientOptions);

Construct it directly only outside React. Under React, mount GraphQLReactorProvider and read the client from a hook.

GraphQLReactorClientOptions

OptionTypeDefaultNotes
urlstringrequiredThe Switchboard's GraphQL endpoint, e.g. http://localhost:4001/graphql.
tokenProvider() => Promise<string | undefined>ambientRenownTokenProviderResolves the bearer token per request.
subscriptionsUrlstringderived from urlThe websocket endpoint. http -> ws, https -> wss, /subscriptions appended.
realtimebooleantruefalse opens no websocket; the client then emits only its own changes.
graphqlClientReactorGraphQLClientbuilt from urlA pre-built SDK, mainly a test seam. It owns its own transport and headers, so it carries no auth middleware.

Methods beyond the interface

subgraph<TSdk>(name: string, getSdk: SubgraphSdkFactory<TSdk>): TSdk;

request<TResult = unknown, TVariables extends Variables = Variables>(
document: TypedGraphQLDocument<TResult, TVariables> | string,
variables?: TVariables,
options?: GraphQLRequestOptions,
): Promise<TResult>;

dispose(): void;

subgraph and request are covered in Subgraphs and attachments.

dispose() closes the realtime socket. The provider calls it on unmount. Registered subscribers are left in place, since they still receive the events the client produces itself.

It is not final: a subscribe after a dispose opens a new socket. React remounts a tree by running an effect's cleanup and then the effect again on the same client - StrictMode does it on every mount - and a terminal dispose would leave those pages with no realtime at all and nothing logged.

GraphQLReactorProvider

PropTypeDefaultWhat it does
urlstringrequiredThe Switchboard's GraphQL endpoint.
tokenProvider() => Promise<string | undefined>the logged-in Renown userResolves the bearer token, per request. Return undefined to send a request anonymously.
subscriptionsUrlstringderived from urlThe websocket endpoint.
realtimebooleantrueWhether server-pushed changes reach subscribers.
attachmentServicePHGlobal["attachmentService"]nonePublished into the slot the editors read.
packagesreadonly DocumentModelLib[]noneThe app's packages (real manifest, models, editors) — makes model and editor hooks work.
documentModelsreadonly DocumentModelModule[]noneSugar for hand-picked modules; wrapped into one synthetic package after packages.
childrenReactNoderequired

On mount the provider registers the window.ph event handlers once per page, publishes the client into window.ph.reactorClient and a DocumentCache built over it into window.ph.documentCache. On unmount it disposes the cache and the client, and leaves the slots populated, matching how Connect never unsets them.

The props are read once, in the useState initialiser. Changing url or tokenProvider afterwards does not rebuild the client; remount the provider if the endpoint has to change. attachmentService, packages and documentModels are the exceptions: each is published from its own effect, so passing them later does not throw away every open document.

packages takes real DocumentModelLibs, assembled from a generated package's subpath exports (my-models/document-models, my-models/editors, my-models/manifest) — never from the package root, whose entry also exports processorFactory and would drag processor (server-side) code into the browser bundle. They publish through a fixed StaticPackageManager into the same window.ph.vetraPackageManager slot Connect fills, which makes the document-model hooks AND the editor hooks work below the provider. Array order is precedence (same-type collisions and the fallback editor resolve to the earlier package). useDocumentModelModuleById(type, version?) resolves the LATEST version when version is omitted — the registry's own semantics — and pins an exact one when given. Passing no packages is equally supported: actions are plain data and the Switchboard runs the reducer either way. Editor discovery is not a rendering guarantee: document hooks work in a light app, drive APIs do not exist.

Hooks

HookReturnsUse it for
useReactorClient()IReactorBrowserClient | undefinedThe six shared members. Works on both implementations.
useSwitchboardClient()GraphQLReactorClient | undefinedsubgraph, request and dispose. undefined when a full reactor is mounted.

useSwitchboardClient is useReactorClient() narrowed to the light client, so a component reaches its extras without a cast, and a component running against a full reactor gets undefined rather than a runtime failure. The narrowing is by brand rather than by instanceof, so a client built by a second copy of the package - a bundled package carrying its own, or a hot-replaced module - is still recognised.

warning

useGraphQLReactorClient is a different, older hook. It reads the legacy window.ph.reactorGraphQLClient slot and has nothing to do with this client.

The document hooks (useDocument, useDocuments, useDispatch, useDocumentOperations and the rest) are unchanged. See React Hooks.

Signing

The signer is resolved from window.ph.renown at push time, so a client built before login signs after login with no rebuild.

SituationWhat happens
Logged-in Renown user, one actionThe action is stamped, then signed, then pushed.
Logged-in Renown user, several actionsThe batch is pushed unsigned and a warning is logged.
No userNothing is stamped or signed. The server computes the hashes itself.

Stamping writes two fields onto action.context:

  • prevOpHash = hashDocumentStateForScope(document, scope), the hash of the state the action applies to.
  • prevOpIndex = header.revision[scope] - 1, so an empty scope stamps -1.

Order matters: the signer reads context.prevOpHash off the action, so the client stamps before it signs.

Batch signing is not supported because the second action's baseline hash cannot be computed without running the reducer, and the light client deliberately ships no reducer.

Two things are worth knowing when you verify signatures:

  • The Switchboard signs an unsigned action with its own did:key, so a non-empty signatures list is not evidence of client-side signing. Check context.signer.user.address instead.
  • The server does not validate prevOpHash. A wrong hash is accepted, so conformance has to be asserted in tests rather than inferred from a write succeeding.

Reactivity

Every successful create, execute and deleteDocument emits a change event to matching subscribers. That is what makes a DocumentCache built on this client invalidate after a dispatch, with no websocket involved.

SearchFilter is { type?, parentId?, ids?, slugs? }. Each populated field is an AND condition tested by truthiness, so ids: [] matches nothing and {} matches everything. parentId is ignored: this client has no drive concept.

EventPayload
Created{ type, documents: [created] }
Updated{ type, documents: [updated] }
Deleted{ type, documents: [], context: { childId: identifier } }

Events are emitted after the write resolves and before it returns, so a rejected write emits nothing.

Realtime

The first subscribe opens one websocket for all subscribers and subscribes to documentChanges with no search argument. The server sends every change the connection may read, and this client applies each subscriber's filter on the way out.

The endpoint is <url> with http swapped for ws and /subscriptions appended - ws://localhost:4001/graphql/subscriptions for a Switchboard on http://localhost:4001/graphql. There is one socket server for the whole Switchboard, not one per subgraph.

Auth on the socket reuses the same tokenProvider, sent as the lowercase authorization connection param and re-resolved on every reconnect. A Switchboard with auth enabled refuses a connection that arrives without it, so an anonymous subscriber gets no realtime there.

A socket that cannot be opened or is refused is logged once and then ignored.

Id space

Change events carry resolved document ids, never the identifier a write was issued with. Read documents by id: a DocumentCache entry keyed by slug is never invalidated. The one exception is deleteDocument, which announces its identifier verbatim because the mutation returns a boolean and there is no document left to read an id off.

Auth

One tokenProvider, resolved per request, applied to the generated SDK, the hand-authored mutation, subgraph SDKs and the websocket connection params alike. The HTTP header is a lowercase authorization.

<GraphQLReactorProvider
url={switchboardUrl}
tokenProvider={async () => session?.accessToken}
>

The default provider returns the logged-in Renown user's bearer token, and undefined when nobody is logged in. Returning undefined or an empty string sends the request with no authorization header at all. A provider that rejects fails the request rather than silently downgrading to anonymous.

Limitations

Known and deliberate. None of these is a bug report.

LimitationDetail
No offline or local-first writingNo IndexedDB, no outbox, no optimistic state, no reconnect flush. A dispatch that fails is a failed dispatch.
execute costs two round tripsOne read for the signing and sinceRevision baseline, one mutation.
No batch signingMore than one action in a batch is pushed unsigned with a warning.
No point-in-time readsA view.revision throws in get and getOperations.
Slug-keyed cache entries are never invalidatedChange events carry resolved ids. Read by id.
Concurrent dispatches can leave the cache staleA forced refetch that arrives while another is in flight returns the pending one instead of chaining. The next server event repairs it.
A mixed-scope batch over-fetchesThe returned operations are windowed from the lowest head revision across the scopes the batch targets.
No drives, no find, no jobsNot on the interface. create(document, parentIdentifier?) is the only parenting there is.
Realtime degrades to local-onlyAn unreachable or refused socket is logged once and ignored. The failed socket is closed, so the next subscriber retries; nothing subscribes on sign-in of its own accord.
No document-model or editor modulesThe provider populates neither window.ph.vetraPackageManager nor window.ph.reactorClientModule, so useVetraPackages, useDocumentModelModules, useEditorModules and useEditorModuleById return empty or undefined, and the exported loadFile, addDocument, addFileWithProgress and copyNode helpers throw or no-op. Render editor components the app imports directly.
The attachment service is a wiring seamThe prop reaches useAttachmentService below the provider and nothing more. No attachment has been read over this client end to end.
Own writes are announced twiceOnce locally, once by the server push, so the cache refetches twice. Harmless.
Import the /graphql-client subpathThe package root reaches @powerhousedao/reactor and does not bundle for the browser. See Bundling.