Publishing and EAS sync
React hooks for publishing an item (usePublishItem) and for watching the SDK's EAS sync runs (useEasSyncProcesses and related hooks).
usePublishItem wraps item.publish(), and the useEasSyncProcesses* hooks watch the SDK's EAS sync runs. Publishing itself is done by the separate @seedprotocol/publish package, on the OP Sepolia testnet: item.publish(), and so usePublishItem, works only once that package's initPublish() has run.
import {
usePublishItem,
useEasSyncProcesses,
useEasSyncProcessesState,
useEasSyncProcessesNonActiveCount,
useEasSyncProcessById,
} from '@seedprotocol/react'
import type { EasSyncProcessRecord } from '@seedprotocol/react'usePublishItem
Calls item.publish(options) and reports whether the publish is running and how it failed.
function usePublishItem(): {
publishItem: (item: Item<any> | undefined, options?: ItemPublishOptions) => void
isLoading: boolean
error: Error | null
resetError: () => void
}The return type isn't exported. ItemPublishOptions is exported from @seedprotocol/sdk.
item.publish() hands the item to the publish executor that @seedprotocol/publish registers in initPublish() (or <PublishProvider config>). That executor runs a full publish, Arweave uploads and then EAS attestations, with the publish wallet. item.publish() lists what it rejects with before the publish starts, and item.publish() and publishItem how a publish fails.
Returns
| Field | Type | Description |
|---|---|---|
publishItem | (item: Item<any> | undefined, options?: ItemPublishOptions) => void | Starts item.publish(options) and returns at once. Does nothing for undefined. With @seedprotocol/publish, options are its CreatePublishOptions (signers, publishMode, tags, onPublished, maxUploadBytes, confirmUpload). |
isLoading | boolean | true while the item's state machine is in its publishing state, which lasts until the publish has finished or failed. It stays false when item.publish() rejects before the publish starts. |
error | Error | null | What item.publish() rejected with. Cleared when a new publish starts. |
resetError | () => void | Sets error back to null. |
error keeps the class and code of what item.publish() rejected with, so match with instanceof or code, not the message. A ManagedAccountPublishError with useModularExecutor arrives as itself, and so do UploadTooLargeError and UploadNeedsConfirmError from @seedprotocol/publish, with the upload figures in estimate. When the user agrees to the size in an UploadNeedsConfirmError, call publishItem(item, { confirmUpload: true }). Any other publish that fails after it started rejects with an Error whose message names the step, Publish of <id> failed at <step>: …, and whose cause is that step's own error, such as a PublishNotAuthorizedError at authorizing.
publishItem returns nothing to await. Follow isLoading and error, or call item.publish() yourself and await it. For the step-by-step status of a running publish, use publish's useItemPublishStatus.
Unless options has a dataItemSigner, the publish signs its data items with the publish wallet's data item signer, and ConnectButton registers one in every setup: the in-app wallet with useModularExecutor, and otherwise the smart wallet's admin EOA, or a plain in-app or injected wallet's own key. A wallet you register with setPublishWallet uses its dataItemSigner, or its signer when it isn't kind: 'smart'; a smart account with neither falls back to the config's dataItemSigner, and without that an item with Html, Image or File values fails at creatingArweaveDataItems. See Who signs.
publishItem takes the Item class, while useItem returns IItem. Cast with item as Item<any>.
Example
import type { Item } from '@seedprotocol/sdk'
import { useItem, usePublishItem } from '@seedprotocol/react'
function PublishButton({ seedLocalId }: { seedLocalId: string }) {
const { item } = useItem({ modelName: 'Post', seedLocalId })
const { publishItem, isLoading, error, resetError } = usePublishItem()
return (
<>
<button
disabled={!item || isLoading}
onClick={() => {
resetError()
publishItem(item as Item<any> | undefined)
}}
>
{isLoading ? 'Publishing…' : 'Publish'}
</button>
{error && <p role="alert">{error.message}</p>}
</>
)
}EAS sync processes
The SDK pulls attestations from EAS into the local database in sync runs. A run starts when you call client.syncFromEas(), after the user's addresses change (when the client is configured to sync on address change), at startup once the client has found its models' schemas on EAS, and when an automatic sync that failed is retried after the device comes back online. Requests that arrive during a run are merged into one next run. Each run writes a row to the eas_sync_processes table when it starts and updates it when it finishes. The hooks below watch that table, so you can show sync progress, a history, or a "last sync failed" banner.
Rows live in the local database, so they survive reloads and are shared by every tab of the app.
The hooks can mount before the client is ready, as children of SeedClientGate do. Until then useEasSyncProcesses, useEasSyncProcessesNonActiveCount and both fields of useEasSyncProcessesState are undefined, and useEasSyncProcessById with an id reports isLoading: true. Syncing across devices has an example.
EasSyncProcessRecord
One row of eas_sync_processes. The type is the SDK's EasSyncProcessType.
| Field | Type | Description |
|---|---|---|
id | number | Auto-incrementing row ID. |
status | string | 'in_progress', 'completed' or 'failed'. A row left in_progress by a closed tab stays so until the next run starts, which marks it failed with Interrupted: the sync stopped before it finished (tab closed?). |
startedAt | number | When the run started (ms since epoch). |
completedAt | number | null | When it finished. |
requestPayload | string | JSON: what asked for the run (source: client_api, address_change, models_init or retry; older rows can also hold event_bus, which the SDK no longer writes), the correlation IDs of the requests it serves, and its options. |
errorMessage | string | null | The failure message, cut to 500 characters. |
errorDetails | string | null | The failure's stack trace, cut to 2,000 characters. |
persistedSnapshot | string | JSON: the run's last recorded phase. |
createdAt | number | null | Row creation time. |
updatedAt | number | null | Last update time. |
useEasSyncProcesses
Every sync run, newest first.
function useEasSyncProcesses(): EasSyncProcessRecord[] | undefinedReturns the rows as a bare array, or undefined until the first result. It re-renders whenever a row is added or changes.
import { useEasSyncProcesses } from '@seedprotocol/react'
function SyncHistory() {
const runs = useEasSyncProcesses()
if (!runs) return null
return (
<ul>
{runs.slice(0, 5).map((run) => (
<li key={run.id}>
{new Date(run.startedAt).toLocaleString()}: {run.status}
{run.errorMessage ? ` (${run.errorMessage})` : ''}
</li>
))}
</ul>
)
}useEasSyncProcessesState
The same rows plus the number of runs that aren't in progress, from one subscription. Use it when a component needs both.
function useEasSyncProcessesState(): {
records: EasSyncProcessRecord[] | undefined
nonActiveCount: number | undefined
}| Field | Type | Description |
|---|---|---|
records | EasSyncProcessRecord[] | undefined | As useEasSyncProcesses. |
nonActiveCount | number | undefined | Rows whose status isn't 'in_progress' (completed plus failed). undefined until the first result. |
import { useEasSyncProcessesState } from '@seedprotocol/react'
function SyncBadge() {
const { records } = useEasSyncProcessesState()
const running = records?.some((r) => r.status === 'in_progress') ?? false
const lastFailed = records?.[0]?.status === 'failed'
if (running) return <span>Syncing…</span>
if (lastFailed) return <span role="alert">Last sync failed</span>
return null
}useEasSyncProcessesNonActiveCount
The nonActiveCount of useEasSyncProcessesState on its own, for a "clear history" button's label, for example.
function useEasSyncProcessesNonActiveCount(): number | undefinedIt still subscribes to every row, so it re-renders whenever any row changes.
useEasSyncProcessById
One sync run by its id.
function useEasSyncProcessById(id: number | undefined): {
record: EasSyncProcessRecord | null
isLoading: boolean
}| Field | Type | Description |
|---|---|---|
record | EasSyncProcessRecord | null | The row, or null while loading or when no row has that id. |
isLoading | boolean | true until the first result, including while the client isn't ready. false when id is undefined. |
When id is undefined, the hook runs no query and returns { record: null, isLoading: false }, so check id before showing "not found".
import { useEasSyncProcessById } from '@seedprotocol/react'
function SyncRunDetail({ id }: { id?: number }) {
const { record, isLoading } = useEasSyncProcessById(id)
if (id === undefined) return <p>Select a sync run</p>
if (isLoading) return <p>Loading…</p>
if (!record) return <p>No sync run with ID {id}</p>
return (
<dl>
<dt>Status</dt>
<dd>{record.status}</dd>
{record.errorMessage && (
<>
<dt>Error</dt>
<dd>{record.errorMessage}</dd>
</>
)}
</dl>
)
}Related
- Publish overview:
initPublish, wallets and the publish flow - Publish components and hooks: React UI for publishing
- Publishing and revoking
- Syncing across devices: how EAS sync brings published data back
- Ownership and drafts: owned and watched addresses
- Client and configuration:
client.syncFromEas()andclient.setAddresses() - Items:
useItemswithincludeEas
useLiveQuery
Run a reactive SQL or Drizzle query against the app's local database and re-render whenever its rows change.
Images and media
Components and hooks for showing Image properties stored on the device, and for resolving raw media strings from feeds into images, video, audio, file links, HTML and JSON.