Events
The SDK's event bus: how to listen for saves, conflicts, EAS syncs, address changes and file changes, every event name with its payload, and which events reach other tabs.
The SDK announces some changes on an in-memory event bus, eventEmitter. Listen to it to refresh your own caches or UI when a property is saved, an EAS sync finishes, the user's addresses change, or files are written. @seedprotocol/react uses the same events to keep its hooks current.
import { eventEmitter, EAS_SEED_DATA_SYNCED_TO_DB_EVENT } from '@seedprotocol/sdk'Most apps don't need the bus: Item, ItemProperty and the React hooks already update when data changes. Reach for it when you keep data outside those, such as a search index or a cache of query results.
eventEmitter
A small event emitter, one per page (or per Node process).
eventEmitter.on(event: string | symbol, listener: (...args: any[]) => void): typeof eventEmitter
eventEmitter.once(event: string | symbol, listener: (...args: any[]) => void): typeof eventEmitter
eventEmitter.off(event: string | symbol, listener: (...args: any[]) => void): typeof eventEmitter
eventEmitter.emit(event: string | symbol, ...args: any[]): boolean
eventEmitter.removeAllListeners(event?: string | symbol): typeof eventEmitter| Method | Description |
|---|---|
on(event, listener) | Adds a listener. Adding the same function twice for one event registers it once. Returns the emitter, not an unsubscribe function. addListener is an alias |
once(event, listener) | Adds a listener that's removed after its first call. off with the same function doesn't remove it, because once registers a wrapper |
off(event, listener) | Removes a listener. Pass the same function you added. removeListener is an alias |
emit(event, ...args) | Calls every listener for event synchronously, in the order added. Returns false when there were none |
removeAllListeners(event?) | Removes every listener for event, or for all events when called with no argument |
There's no subscribe or unsubscribe method. To stop listening, keep a reference to your listener and pass it to off.
Example
import { eventEmitter, EAS_SEED_DATA_SYNCED_TO_DB_EVENT } from '@seedprotocol/sdk'
const onSynced = () => {
void refreshSearchIndex()
}
eventEmitter.on(EAS_SEED_DATA_SYNCED_TO_DB_EVENT, onSynced)
// later
eventEmitter.off(EAS_SEED_DATA_SYNCED_TO_DB_EVENT, onSynced)In React, add the listener in a useEffect and remove it in the cleanup function.
emit runs each listener in its own try/catch. A listener that throws is logged as [seed] listener for "<event>" threw:, and the other listeners and the SDK code that emitted the event carry on. Listeners run synchronously, so start async work without awaiting it inside the listener, as above.
Don't call removeAllListeners() without an event name. @seedprotocol/react registers its own listeners on the same emitter, and removing them stops its hooks from updating after syncs, address changes and file writes.
Events the SDK emits
| Event | Constant | Payload | When | Other tabs |
|---|---|---|---|---|
itemProperty.saved | none | { seedLocalId, seedUid } | ItemProperty#save() resolved successfully. seedUid is undefined for drafts | No |
itemProperty.conflict | none | PropertyConflict | A local edit was kept over a different value that EAS sync wrote after it. Emitted in the tab that made the edit or ran the sync. See ItemProperty: Conflicts | No |
easSeedDataSyncedToDb | EAS_SEED_DATA_SYNCED_TO_DB_EVENT | none | An EAS sync finished writing items. On Node it re-reads the loaded items first; in the browser they've already read sync's rows through the change feed | Yes |
addresses.persisted | ADDRESSES_PERSISTED_EVENT | { owned: string[], watched: string[] } | client.setAddresses() or client.init({ addresses }) saved addresses that differ, ignoring case, order and duplicates, from the ones this tab last announced. client.stop() resets that, so the next save after init is announced | Yes |
localCopies.removed | LOCAL_COPIES_REMOVED_EVENT | { addresses: string[], removedSeedLocalIds: string[], removedSeedUids: string[] } | client.removeLocalCopiesForAddresses() deleted local copies of on-chain items | Yes |
file-saved | none | string: the file's path | An Image, File or Html save wrote a file in this tab | As fs.files.changed |
fs.files.changed | FILES_CHANGED_EVENT | string[]: file paths | Files changed outside this tab's file system cache: written by a download or image-resize worker, or written, truncated or removed by another tab | Yes |
model:ambiguous | MODEL_AMBIGUOUS_EVENT | ModelAmbiguousEventPayload (below) | Background work, such as an EAS sync, skipped an item because its model name exists in more than one schema | No |
An EAS sync with no owned or watched addresses, or where none of your models has a schema on EAS, ends early and doesn't emit easSeedDataSyncedToDb.
ModelAmbiguousEventPayload:
| Field | Type | Description |
|---|---|---|
modelName | string | The ambiguous model name |
schemaNames | string[] | The schemas that define it |
seedLocalId, seedUid | string | undefined | The item that was skipped |
context | string | What was running, such as 'easSync.metadata' |
The payload types AddressesPersistedPayload, LocalCopiesRemovedPayload and ModelAmbiguousEventPayload are exported. parseAddressesPersistedPayload(value) turns an unknown payload into { owned, watched }, with empty arrays for anything missing.
itemProperty.saved comes from save() only. Assigning post.title = 'x' saves the value without emitting it, so don't rely on this event to see every edit. For changes to stored data, use BaseDb.liveQuery or, in the browser, the change feed.
Across tabs
The bus belongs to one tab. When several tabs share a database, the SDK forwards events that mean "the shared data changed" to the other tabs over a BroadcastChannel:
easSeedDataSyncedToDb,addresses.persistedandlocalCopies.removedare emitted in the tab where they happen and again in every other tab. Before re-emittinglocalCopies.removed, the receiving tab drops the removed items from its cache. Its loaded items have already read a sync's rows through the change feed.file-savedstays in its tab. Separately, every file a tab writes, truncates or removes through its file system (property files,ItemStoragefiles, schema files, downloads) is announced to the other tabs. They refresh their file cache and emitfs.files.changedwith the paths, batched over 25 ms (at most 500 paths per message).- When a download or image-resize worker writes files,
fs.files.changedis emitted in this tab and in every other tab.
Every other event stays in the tab that emitted it. A tab never forwards an event it received from another tab. Nothing is forwarded in Node, or when client.init was called with multiTab: 'off'. See Multi-tab.
The SDK doesn't listen for any event on the bus, so emitting one does nothing. Outside NODE_ENV=production (in a browser build, only when the bundler replaces process.env.NODE_ENV with another value; with no process and no replacement the warning stays off), emitting syncDbWithEas or fs.downloadAll.request, which older SDKs handled, with no listener logs a console warning once per event: [seed] Nothing handles the "syncDbWithEas" event any more, so emitting it does nothing: call client.syncFromEas() instead. To sync from code, call await client.syncFromEas(): it returns when the sync is done, throws on failure and runs in any tab. See Client and configuration.
Related
- Multi-tab: leader tabs and what's shared between tabs
- Reactivity model: how the SDK and React hooks stay current
- Client and configuration:
setAddresses,syncFromEasandremoveLocalCopiesForAddresses - ItemProperty: when
itemProperty.savedfires - Querying:
liveQueryfor watching stored data
Querying
How to read the local database: Item.all and find, Drizzle queries and transactions through BaseDb, live queries, the browser change feed, and helpers for published data.
Node entry point
What @seedprotocol/sdk/node exports: the Property builders, the schema import helpers, and the Node file manager and database.