Push and subscriptions
useDocumentWatch()
Section titled “useDocumentWatch()”function
declare function useDocumentWatch(document: DocumentOrSnapshot): DocumentWatchSnapshot;Subscribes the calling component to other people’s writes to the rows of a query document.
Takes the document, which for a view is the value beside it: useInjection() returns a snapshot and a snapshot knows the document it was taken of, so the query and its parameters are named in one place and this is not it:
const bars = useInjection(Q_Bar)const live = useDocumentWatch(bars)
{ live.stale && <button onClick={ () => bars.update({}) }>Reload</button> }The document is left alone – no values travel with a change notification, so there is nothing to apply. What this returns says that a row on screen has changed and which fields of it did; the view decides what that is worth.
For rows the user is editing this is the wrong half: a working set holds a draft for them and can say what a change means, so an editing view passes {watch: true} to useWorkingSet() instead. Doing both for the same rows asks to be told twice and refetches the document the drafts stand on.
Rules of hooks apply.
Parameters
document |
query document to watch, or the snapshot a view holds of one |
Returns what has changed under the document since it was last read
watchDocument()
Section titled “watchDocument()”function
declare function watchDocument(document: QueryDocument<any>, options?: WatchOptions): DocumentWatch;Watches the rows of a query document for other people’s writes, and reports them.
Nothing is applied to the document and nothing can be: the message carries a mask and no values, so what is knowable is that a row on screen is no longer what the database holds. What an application does with that runs from nothing at all, through a notice the user acts on, to running the query again – and none of those is a decision a framework is in a position to make, so this reports and stops.
const live = useDocumentWatch(bars)...{ live.stale && <button onClick={ () => bars.update({}) }>Reload</button> }Running the query again is update({}), which the document has already. What arrived is forgotten when
the document changes, that being either the refetch or a page turn, and either way rows that are fresh.
Parameters
document |
the document to watch |
options |
how to start it, see WatchOptions |
Returns the store, which has to be closed when the view holding it goes
watchWorkingSet()
Section titled “watchWorkingSet()”function
declare function watchWorkingSet(workingSet: WorkingSet): () => void;Tells the given working set about the writes other people land on the rows it is holding, as they land.
The rows are being edited, so a change to one means something on its own: the field takes the status it would have taken at save time, and the user sees it while they are still typing rather than afterwards. No values arrive, which the working set already has a shape for – a field known to have changed and not known to what reads as the value the row was read with, and carries the same status a conflict does.
The version is deliberately left where it stands. Adopting the one that just landed would quietly remove the guard the next save runs into, and the marks would be the only warning the user ever got; leaving it keeps the merge as the authority and makes this the early notice it is meant to be.
A row this working set holds is its case and not a document’s, even where the document it was registered from is on screen: refetching that document would swap the row objects the drafts stand on. A view that edits watches its working set – useWorkingSet({watch: true}) – and a view that only displays watches its document.
One subscription per working set, however many callers ask for it: a working set is read by as many components as care to render it, and what arrives is applied to the set rather than handed to a caller, so a second registration would only deliver every message twice. The last caller to stop watching is the one that closes it.
Parameters
workingSet |
the working set to keep current |
Returns a function that stops watching
WatchOptions
Section titled “WatchOptions”interface
interface WatchOptionsHow a watch is to be started.
WatchOptions.open
Section titled “WatchOptions.open”open?: boolean;Whether the watch is watching when it is handed over. Default true, which is what a caller outside React wants: it asked for a watch and gets one that is live.
Opening registers a subscription, which is a side effect and so belongs in an effect rather than in a render – React calls a useState() initializer twice under StrictMode and keeps one of the two results, so a watch that opened where it was created would leave a second, live one that nothing holds and nothing can ever close. useDocumentWatch() therefore constructs the watch closed and opens it from its effect.
DocumentWatch
Section titled “DocumentWatch”interface
interface DocumentWatchA document’s view of what other people are writing: a store like the document itself.
DocumentWatch.subscribe
Section titled “DocumentWatch.subscribe”subscribe: (fn: () => void) => () => void;DocumentWatch.getSnapshot
Section titled “DocumentWatch.getSnapshot”getSnapshot: () => DocumentWatchSnapshot;DocumentWatch.open
Section titled “DocumentWatch.open”open: () => void;starts watching, and does nothing to a watch that is watching already
DocumentWatch.close
Section titled “DocumentWatch.close”close: () => void;stops watching, reversibly: open() puts the subscription back
DocumentWatchSnapshot
Section titled “DocumentWatchSnapshot”interface
interface DocumentWatchSnapshotWhat has happened under a document since it was last read.
DocumentWatchSnapshot.stale
Section titled “DocumentWatchSnapshot.stale”stale: boolean;true once somebody else’s write touched a row this document is showing, in a field it selected.
Whether that is worth telling the user about, refetching for, or ignoring is the application’s, and this is the whole of what the framework knows.
DocumentWatchSnapshot.remoteChanged
Section titled “DocumentWatchSnapshot.remoteChanged”remoteChanged: RemoteChangedRow[];Which rows are out of date and in which of their fields: one entry per row, however many times it changed, in the order the rows were first heard about.
RemoteChangedRow
Section titled “RemoteChangedRow”interface
interface RemoteChangedRowOne row somebody else changed under us, and the fields of it they touched.
RemoteChangedRow.type
Section titled “RemoteChangedRow.type”type: string;RemoteChangedRow.id
Section titled “RemoteChangedRow.id”id: string;RemoteChangedRow.fields
Section titled “RemoteChangedRow.fields”fields: string[];the fields the change touched that this client has a name for, in bit order. In a document’s record this is every field the row has changed in, the messages about it having been folded together.
ENTITY_VERSION
Section titled “ENTITY_VERSION”constant
declare const ENTITY_VERSION = "EntityVersion";The channel every merge publishes to, named as EntityVersionPublisher registers it.
EntityVersionMessage
Section titled “EntityVersionMessage”interface
interface EntityVersionMessageOne recorded change to one row, as it arrives: which row changed, which fields it touched, and who did it.
The EntityVersion record the merge writes, and nothing more – no field values travel, which is what makes the message the same size whatever changed and is why what a subscriber can do with one is either mark it or go and read the row.
EntityVersionMessage.id
Section titled “EntityVersionMessage.id”id: string;the version the row stands at now
EntityVersionMessage.entityType
Section titled “EntityVersionMessage.entityType”entityType: string;EntityVersionMessage.entityId
Section titled “EntityVersionMessage.entityId”entityId: string;EntityVersionMessage.prev
Section titled “EntityVersionMessage.prev”prev: string | null;the version the change was made against, null for a row that had none
EntityVersionMessage.fieldMask
Section titled “EntityVersionMessage.fieldMask”fieldMask: string | null;the fields that changed, as a decimal string: 128 bits is past what a number holds exactly
EntityVersionMessage.fieldSet
Section titled “EntityVersionMessage.fieldSet”fieldSet: string;id of the field set the mask was written against
EntityVersionMessage.ownerId
Section titled “EntityVersionMessage.ownerId”ownerId: string | null;who made the change
EntityVersionMessage.created
Section titled “EntityVersionMessage.created”created: string;when, ISO-8601
subscribeToTopic()
Section titled “subscribeToTopic()”function
declare function subscribeToTopic<T>(topic: string, handler: TopicHandler<T>, condition?: FilterExpression | null): () => void;Subscribes to a channel, connecting if this is the first subscription.
The returned function unsubscribes. A subscription lives until it does: a dropped connection does not end one, because this module re-issues every registered subscription on reconnect – an application subscribes once and keeps receiving messages across a server restart without doing anything about it.
Parameters
topic |
name of the channel, as the server registered it |
handler |
called with the payload of every message matching the condition |
condition |
FilterDSL condition every message on the channel is evaluated against before it is delivered here, or null to receive everything the channel carries. Field paths are resolved against the channel’s payload, and a to-many hop in one reads positionally (bazes.0.name), unlike the same DSL against the database. |
Returns a function that unsubscribes
PubSubConnection
Section titled “PubSubConnection”constant
declare const PubSubConnection: { subscribe(fn: () => void): () => void; getSnapshot(): PubSubConnectionSnapshot;};The connection state as a store, in the subscribe/getSnapshot shape QueryDocument and WorkingSet already use – so a view renders an offline indicator with useSyncExternalStore and nothing else.
getSnapshot() returns the same object for as long as nothing changes, which is what useSyncExternalStore’s own change detection needs.
PubSubConnectionSnapshot
Section titled “PubSubConnectionSnapshot”interface
interface PubSubConnectionSnapshotSnapshot for a pub sub connection.
PubSubConnectionSnapshot.status
Section titled “PubSubConnectionSnapshot.status”status: PubSubStatus;“reconnecting” does not distinguish a server that is down from a session that has expired, and it cannot: a browser reports a refused websocket handshake to script without a status code, so a 401 and a dead port arrive here as the same event. A page whose session is gone therefore sits in “reconnecting” until something on the ordinary HTTP path – the next query – gets the 401 that can be read, and the application does what it does about that. Worth knowing before this status is put in front of a user as “connection lost”.
PubSubConnectionSnapshot.attempts
Section titled “PubSubConnectionSnapshot.attempts”attempts: number;Failed connection attempts since the last successful one, which is what the current backoff window is derived from. Zero while connected.
PubSubStatus
Section titled “PubSubStatus”type
type PubSubStatus = "idle" | "connecting" | "connected" | "reconnecting";What the connection is doing, as far as an application has any use for knowing.
“idle” covers both “nothing has subscribed yet” and “the last socket closed and nothing wants another one”, because to a view waiting for messages those are the same situation: none are coming.
TopicHandler
Section titled “TopicHandler”type
type TopicHandler<T> = (payload: T) => void;Called with the payload of every message published to the topic that the subscription’s condition matched.