@usearete/react 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,43 +7,74 @@ Built on top of [`@usearete/sdk`](https://www.npmjs.com/package/@usearete/sdk),
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- npm install @usearete/react react zustand
10
+ npm install @usearete/react @usearete/sdk zod
11
+ ```
12
+
13
+ `@usearete/react` installs `zustand` as a normal dependency, so do not install it separately. React remains an application dependency. Generated consumers install the core SDK and Zod because generated stack files import their types and schemas directly.
14
+
15
+ ### Hooks linting
16
+
17
+ Arete's fluent `.use()`, `.useOne()`, and `.useMutation()` calls are real React
18
+ hooks, but the standard Hooks rule cannot identify deeply nested member calls.
19
+ The package ships `@usearete/react/eslint-plugin.cjs` with an
20
+ `arete/fluent-hooks` rule for flat ESLint configurations. The ORE template also
21
+ configures the equivalent bundled rule for ESLint 8. Use it together with
22
+ `react-hooks/rules-of-hooks`, not instead of the standard rule.
23
+
24
+ ```js
25
+ // eslint.config.js
26
+ import areteHooks from '@usearete/react/eslint-plugin.cjs';
27
+
28
+ export default [{
29
+ plugins: { arete: areteHooks },
30
+ rules: { 'arete/fluent-hooks': 'error' },
31
+ }];
11
32
  ```
12
33
 
13
34
  > Not using React? Use [`@usearete/sdk`](../core/README.md) directly.
14
35
 
15
36
  ## Quick Start
16
37
 
38
+ Keep the generated stack explicit at the provider and consumer boundaries. Equivalent calls share one provider-managed client and store:
39
+
17
40
  ```tsx
18
41
  import { AreteProvider, useArete } from '@usearete/react';
19
42
  import { ORE_STREAM_STACK } from './generated/ore-stack';
20
43
 
44
+ const publishableKey = import.meta.env.VITE_ARETE_PUBLISHABLE_KEY;
45
+ if (!publishableKey) throw new Error('VITE_ARETE_PUBLISHABLE_KEY is required');
46
+
21
47
  function Dashboard() {
22
- const { views, isLoading, error } = useArete(ORE_STREAM_STACK, {
23
- url: 'ws://localhost:8877',
24
- httpUrl: 'http://localhost:8877',
48
+ const arete = useArete(ORE_STREAM_STACK);
49
+ const board = arete.views.OreBoard.state.use({
50
+ address: arete.addresses.board(),
25
51
  });
52
+ const roundId = board.data?.state.roundId;
53
+ const round = arete.views.OreRound.state.use(
54
+ roundId == null ? undefined : { roundId },
55
+ );
26
56
 
27
- const { data: latestRound } = views.OreRound.latest.useOne();
28
-
29
- if (isLoading) {
30
- return <div>Connecting...</div>;
57
+ if (arete.status === 'error') {
58
+ return <button onClick={() => void arete.retry()}>Reconnect</button>;
31
59
  }
32
-
33
- if (error) {
34
- return <div>{error.message}</div>;
60
+ if (board.status !== 'ready' || round.status !== 'ready') {
61
+ return <p>Connecting...</p>;
35
62
  }
36
63
 
37
- return <pre>{JSON.stringify(latestRound, null, 2)}</pre>;
64
+ return (
65
+ <div>
66
+ {arete.socketIssue && <p>{arete.socketIssue.message}</p>}
67
+ <p>Round {round.data?.id.roundId?.toString() ?? 'unavailable'}</p>
68
+ </div>
69
+ );
38
70
  }
39
71
 
40
72
  export default function App() {
41
73
  return (
42
74
  <AreteProvider
43
- autoConnect={true}
44
- auth={{
45
- // publishableKey: 'hspk_...',
46
- }}
75
+ autoConnect
76
+ stack={ORE_STREAM_STACK}
77
+ auth={{ publishableKey }}
47
78
  >
48
79
  <Dashboard />
49
80
  </AreteProvider>
@@ -51,102 +82,307 @@ export default function App() {
51
82
  }
52
83
  ```
53
84
 
85
+ The hosted ORE stack requires this publishable key for authentication and rate limiting. Read-only browser viewing does not require a wallet, but it is not unauthenticated.
86
+
87
+ Single-stack applications can remove the repeated constant without ambient types by binding the React API once:
88
+
89
+ ```tsx
90
+ import { createAreteReact } from '@usearete/react';
91
+ import { ORE_STREAM_STACK } from './generated/ore-stack';
92
+
93
+ export const {
94
+ Provider: OreProvider,
95
+ useArete: useOre,
96
+ } = createAreteReact(ORE_STREAM_STACK);
97
+ ```
98
+
99
+ `useOre()` remains explicit about the application dependency. Multi-stack apps can keep using `AreteProvider` and `useArete(stack)` directly.
100
+
101
+ Provider-default `useArete()` calls remain available. For full type inference, register the stack type once (e.g. in `src/arete.d.ts`):
102
+
103
+ ```ts
104
+ import type { OreStreamStack } from './generated/ore-stack';
105
+
106
+ declare module '@usearete/react' {
107
+ interface AreteDefaultStackRegistry {
108
+ defaultStack: OreStreamStack;
109
+ }
110
+ }
111
+ ```
112
+
113
+ Multi-stack apps can keep passing the stack explicitly: `useArete(stack, options)` always wins over the provider default.
114
+
54
115
  ## Provider
55
116
 
56
117
  `AreteProvider` manages connected clients for descendant hooks.
57
118
 
58
119
  Supported props:
59
120
 
60
- - `autoConnect`
121
+ - `stack` — default stack for argument-less `useArete()` calls
122
+ - `stackOptions` — default client lookup options (`url`, `httpUrl`, `transport`, `transactions`, `programs`) for the provider stack, including explicit `useArete(providerStack)` calls
123
+ - `autoConnect` — defaults to `true`; controls only whether the provider starts the initial connection
124
+ - `autoReconnect` — defaults to `true`; controls only recovery after an established connection is lost
61
125
  - `wallet`
62
126
  - `auth`
63
127
  - `fetch`
64
- - `validateFrames`
128
+ - `validateFrames` — set `false` to suppress rejected-frame warnings; generated schemas still normalize and validate entities
129
+ - `onFrameValidationError` — structured callback for generated-schema rejections
65
130
  - `reconnectIntervals`
66
131
  - `maxReconnectAttempts`
67
132
  - `maxEntriesPerView`
68
133
  - `flushIntervalMs`
69
134
 
70
- These are provider-wide defaults. Endpoint overrides stay on the hook call.
135
+ These are provider-wide defaults. Per-call overrides stay on the hook call and always win. Connection settings are captured when a shared client is created; changing provider props does not mutate that client. `wallet` updates are synchronized in place. For other changed settings, call `retry()` to replace the shared client using the latest provider configuration.
71
136
 
72
- ## `useArete(stack, options?)`
137
+ The lifecycle flags are intentionally independent. `autoConnect={false}` leaves initial connection ownership to the application. It does not disable automatic recovery for a connection the application establishes, but `canRetry` remains false because provider-owned replacement attempts would also stay disconnected; use the client lifecycle directly. `autoReconnect={false}` does not suppress the initial connection; it disables only automatic recovery after that connection is lost.
73
138
 
74
- `useArete` returns the connected React surface for a stack:
139
+ ## `useArete(stack?, options?)`
140
+
141
+ `useArete` returns the connected React surface for a stack. With no arguments it resolves the provider's `stack`/`stackOptions`. Passing the same stack explicitly keeps the provider's `stackOptions`; per-call options override them, while another explicit stack is isolated from the provider stack's options. It throws a descriptive error when no stack is passed and no default is configured.
75
142
 
76
143
  - `views`
77
144
  - `programs`
78
145
  - `queries`
146
+ - `read` — callable stack reads with `.use(...args, options?)` React hooks
79
147
  - `chain`
80
148
  - `client`
81
- - `connectionState`
149
+ - `reads` — deprecated alias of `read`
150
+ - `status` — preferred connection state for UI; includes the initial client-creation window
151
+ - `connectionState` — raw state from the connected client
82
152
  - `isConnected`
83
153
  - `isLoading`
154
+ - `canRetry` — true when `retry()` can start a new shared connection attempt
84
155
  - `error`
156
+ - `socketIssue` — the latest structured server WebSocket issue, if any
157
+ - `retry()` — replaces the failed shared client; every matching `useArete` consumer observes the connecting window and adopts the same replacement
85
158
 
86
159
  Supported hook options:
87
160
 
88
161
  - `url`
89
162
  - `httpUrl`
90
163
  - `transport`
164
+ - `transactions` — explicit transaction transport when execution uses a different service from `httpUrl`
91
165
  - `programs`
92
166
 
167
+ All options are optional. By default the client connects to the endpoints embedded in the generated stack definition (`stack.endpoints`), so `useArete(STACK)` with no options is the common case.
168
+
93
169
  Notes:
94
170
 
95
171
  - `transport: 'http'` disables streaming view subscriptions, but HTTP-backed surfaces like `queries`, `chain`, and program reads still work.
96
- - If you pass attached `programs`, keep that object stable with a module constant or `useMemo` so React does not create a fresh client on every render.
172
+ - Attached `programs` are keyed by value (program name and id): passing a fresh object literal each render still resolves to the same client.
173
+ - Repeated `useArete` calls with the same stack object and options share one provider-managed client and store. It is safe for sibling components to call the hook independently.
97
174
 
98
175
  ## Views
99
176
 
100
- View hooks return a `ViewHookResult<T>` object:
177
+ View hooks return a discriminated `ViewHookResult<T>`. `status` and `isEmpty`
178
+ narrow the associated data and error fields:
101
179
 
102
180
  ```ts
103
- type ViewHookResult<T> = {
104
- data: T | undefined;
105
- isLoading: boolean;
106
- error?: Error;
107
- refresh: () => void;
108
- };
181
+ if (round.status === 'error') {
182
+ console.error(round.error); // Error
183
+ } else if (round.status === 'ready' && !round.isEmpty) {
184
+ console.log(round.data.id.roundId); // entity, not undefined
185
+ }
186
+ ```
187
+
188
+ `status` distinguishes "no client yet" (`connecting`) from "subscribed, waiting for the first frame" (`subscribing`). `isPending` covers both states, `isReady` marks a usable ready result, and `isEmpty` distinguishes a ready hook with no entity or list entries from loading. Errors may retain previously committed data, but `isReady` becomes false. `isLoading` remains for compatibility.
189
+
190
+ Use the connection `status` together with `socketIssue` and `retry()` for recovery UI:
191
+
192
+ ```tsx
193
+ const arete = useArete(ORE_STREAM_STACK);
194
+
195
+ if (arete.canRetry) {
196
+ return <button onClick={() => void arete.retry()}>Reconnect</button>;
197
+ }
198
+
199
+ return <ConnectionBadge status={arete.status} issue={arete.socketIssue} />;
200
+ ```
201
+
202
+ The view hook objects themselves are refresh targets — they refresh that view's *active* subscriptions without you holding a hook result, and are a no-op when nothing is subscribed:
203
+
204
+ ```ts
205
+ await arete.views.OreRound.state.refresh({ roundId }); // one keyed subscription
206
+ await arete.views.OreRound.state.refresh(); // every active subscription of the view
207
+ await arete.views.OreRound.latest.refresh(); // list views
208
+
209
+ // Handy in mutation reconciliation — the component doesn't need its own
210
+ // subscriptions just to refresh them:
211
+ claim.submit(args, {
212
+ reconcile: { refresh: [arete.views.OreBoard.state, arete.views.OreMiner.state, preview] },
213
+ });
214
+ ```
215
+
216
+ When several resources feed one status line, aggregate them with `summarizeStatuses` instead of hand-rolling loading/error lists:
217
+
218
+ ```tsx
219
+ const streams = summarizeStatuses({ Board: board, Round: round, Miner: authority && miner });
220
+ // streams.loading → names still loading, e.g. ['Board']
221
+ // streams.refreshing → names resynchronizing committed data
222
+ // streams.errors → 'Name: message' strings
223
+ // streams.isLoading / streams.isRefreshing / streams.hasError
109
224
  ```
110
225
 
226
+ Falsy entries (`authority && miner`) are skipped.
227
+
111
228
  List views support:
112
229
 
113
230
  - `.use()`
114
231
  - `.use({ take: 1 })`
115
232
  - `.useOne()`
116
233
 
234
+ Put list query configuration (`take`, `skip`, `key`, `partition`, `filters`, `after`, `withSnapshot`, `snapshotLimit`, `schema`, and `onSchemaValidationError`) in the first params object. For compatibility, fields also accepted by view options are honored from the second argument when absent from params; params win when both are supplied. Keep `enabled` and `initialData` in the second argument. State hooks accept one entity as `initialData`, list hooks accept an entity array, and `useOne` accepts one entity.
235
+
236
+ `onSchemaValidationError(diagnostic)` observes entities rejected by a caller-supplied schema. The diagnostic is `{ view, key?, entity, error }`. Accepted entities and existing filtering behavior are unchanged.
237
+
238
+ Protocol v2 gives each parameter set independent ordered membership. Different windows and filters on one view can coexist, while equivalent normalized queries share one reference-counted wire subscription:
239
+
240
+ ```tsx
241
+ const firstPage = views.OreRound.latest.use({ take: 10 });
242
+ const secondPage = views.OreRound.latest.use({ take: 10, skip: 10 });
243
+
244
+ if (firstPage.isLoading) return <p>Loading...</p>;
245
+
246
+ return (
247
+ <>
248
+ {firstPage.isRefreshing && <p>Refreshing...</p>}
249
+ <RoundList rounds={firstPage.data ?? []} />
250
+ <RoundList rounds={secondPage.data ?? []} />
251
+ </>
252
+ );
253
+ ```
254
+
255
+ During reconnect, hooks preserve their committed data and set `isRefreshing`. A completed authoritative snapshot then atomically replaces membership for that exact query, including an empty result.
256
+
257
+ `await result.refresh()` resolves after the refreshed subscription's next complete snapshot is committed. Registration, send, and subscription failures reject, clear `isRefreshing`, and appear in `result.error`. A view refresh with no active matching subscription remains a no-op.
258
+
117
259
  State views support:
118
260
 
119
- - `.use(key)`
261
+ - `.use(key)` — subscribe to one keyed entity using the generated key type (for example `{ roundId: bigint }`)
262
+ - `.use(undefined)` — disabled: no subscription, `{ data: undefined, isLoading: false }`. Useful while a wallet is not connected: `views.OreMiner.state.use(authority ? { authority } : undefined)`.
263
+
264
+ Keyed hooks only expose data for the key you passed: when the key changes, `data` becomes `undefined` (and `isLoading` true) until the new key's snapshot arrives. Components never need to re-verify returned data against their inputs.
265
+
266
+ `initialData` is trusted as immediately usable ready data. The first live store update or completed empty snapshot replaces it; an empty list settles as `[]` and an absent state entity settles as `undefined`. Do not seed transaction-authorizing data unless the application is willing to trust that seed.
267
+
268
+ > **BigInt props in development:** entities contain bigints, and React ≤19.2's dev-mode performance track calls `JSON.stringify` on changed props — it crashes on bigint *arrays and objects* (scalar bigints are fine), wedging the app after the first live update. Prefer stack-provided UI fields when they exist (e.g. `deployedPerSquareUi` next to `deployedPerSquare`), use `stringifyBigints(value)` for a typed structural bigint→string conversion of the rest (raw account snapshots are the common case), or upgrade to React 19.3+ where this is fixed.
269
+
270
+ ## Reads
271
+
272
+ Stacks can expose one-shot read functions (e.g. RPC-backed previews). Each one gets a React hook:
273
+
274
+ ```tsx
275
+ const preview = arete.read.solClaimPreview.use(authority);
276
+ const quote = arete.read.quoteManualDeployment.use(
277
+ input,
278
+ { debounceMs: 300 },
279
+ );
280
+ ```
281
+
282
+ - Arguments form the cache key — the read re-runs when they change, and the previous arguments' data is never exposed while the fresh result is loading.
283
+ - The hook is disabled while any required argument is `null`/`undefined`, so no `enabled` flag or non-null assertions are needed.
284
+ - `.use(...args, { debounceMs, initialData })` adds React-only behavior with options last. Debounce applies to automatic argument changes; `refresh()` always runs immediately. When options follow an omitted optional read argument, pass `undefined` in that argument's position.
285
+ - `status` is `'disabled' | 'connecting' | 'loading' | 'ready' | 'refreshing' | 'error'`. The existing loading and refreshing booleans remain available.
286
+ - `isPending`, `isReady`, and `isEmpty` cover the common UI branches without comparing status strings.
287
+ - `preview.refresh()` re-runs the read on demand, typically after a mutation (see below).
288
+
289
+ The same functions remain callable for imperative access: `await arete.read.solClaimPreview(authority)`. The old `arete.reads` namespace remains as a deprecated alias.
120
290
 
121
291
  ## Programs, Queries, and Chain
122
292
 
123
293
  `useArete` mirrors the connected core client surface:
124
294
 
125
295
  - `programs.<program>.raw.<instruction>` for raw typed instructions
126
- - `programs.<program>.plan` / `programs.<program>.instructions` for semantic instructions
296
+ - `programs.<program>.instructions`, `.transactions`, and `.flows` for semantic operations
127
297
  - `programs.<program>.accounts` and `programs.<program>.queries` for HTTP reads
128
298
  - `queries` for stack-level HTTP queries
129
299
  - `chain` for chain helpers
130
300
 
131
301
  Raw instruction hooks preserve `.execute`, `.build`, and `.useMutation()`.
132
302
 
133
- Semantic instruction hooks preserve:
303
+ Semantic operation hooks preserve:
134
304
 
135
305
  - `.execute`
136
- - `.send`
137
- - `.resolve`
138
- - `.plan`
139
- - `.build`
140
- - `.stage`
306
+ - `.prepare`
141
307
  - `.useMutation()`
142
308
 
309
+ Mutation hooks expose `mutate(args, options)` for event handlers. It records
310
+ errors in hook state and does not return a promise. Use the equivalent rejecting
311
+ `mutateAsync(args, options)` or `submit(args, options)` when the caller must
312
+ await or compose the result.
313
+
314
+ Program and read hooks are safe to call while the client is still connecting: mutations throw "Arete client is not connected" if submitted early, and reads stay disabled.
315
+
316
+ ## Safe amount parsing
317
+
318
+ `safeToRawAmount(input, decimals)` is re-exported by `@usearete/react` for form validation. Unlike `toRawAmount`, it reports invalid input without throwing:
319
+
320
+ ```tsx
321
+ import { safeToRawAmount } from '@usearete/react';
322
+
323
+ const parsed = safeToRawAmount({ ui: amountText }, 9);
324
+ if (!parsed.success) {
325
+ setAmountError(String(parsed.error));
326
+ return;
327
+ }
328
+
329
+ const rawAmount = parsed.data;
330
+ ```
331
+
332
+ The return type is `{ success: true, data } | { success: false, error }`.
333
+
334
+ ## Default reconciliation
335
+
336
+ Mutations from generated program hooks **reconcile against the stream by default**: after the transaction confirms on-chain, the hook waits until the stack has processed the highest confirmed receipt slot before settling (30s timeout). HTTP-only mutations and results without a receipt slot skip this step. The processed-slot watermark does not promise that a particular entity changed; use `refresh` for the views or one-shot reads that must be re-read.
337
+
338
+ Per-submit options:
339
+
340
+ ```tsx
341
+ // Opt out entirely:
342
+ deploy.submit(args, { reconcile: false });
343
+
344
+ // Keep the default, but also refresh views and one-shot reads afterwards.
345
+ // `refresh` accepts view hook objects, view/read hook results, or plain
346
+ // callbacks:
347
+ claim.submit(args, {
348
+ reconcile: { refresh: [arete.views.OreBoard.state, miner, preview] },
349
+ });
350
+
351
+ // Custom timeout:
352
+ deploy.submit(args, { reconcile: { timeoutMs: 10_000 } });
353
+
354
+ // Full control — a function replaces the default:
355
+ deploy.submit(args, { reconcile: async (context) => { /* ... */ } });
356
+ ```
357
+
358
+ The mutation `phase` field is the discriminated status to branch on for busy labels (`'preparing'`, `'awaiting-wallet'`, `'submitted'`, `'confirmed'`, `'reconciling'`, `'reconciled'`, plus the failure outcomes); `status` remains `'pending'` until the mutation settles. Generated operation hooks publish each completed receipt and signature during `submitted`, before the final operation receipt resolves. `onConfirmed` runs immediately after chain confirmation; `onSuccess` runs only after successful reconciliation (or immediately when reconciliation is disabled/skipped). Reconciliation failure, including a refreshed snapshot timeout, is exposed through `reconciliationError`, does not call `onSuccess`, and does not reject the confirmed `submit()` result. The confirmed transaction remains landed. `retryReconciliation()` repeats only post-confirmation watermark/refresh work using the saved receipts and targets; it never rebuilds, signs, or submits. Convenience booleans (`isPreparing`, `isAwaitingWallet`, `isReconciling`, `canRetryReconciliation`) remain available.
359
+
360
+ ## Migration notes (0.3 → next)
361
+
362
+ - `AreteProvider` accepts a default `stack` and `stackOptions`; `useArete()` may then be called with no arguments. Register the stack type via `AreteDefaultStackRegistry` for full inference.
363
+ - View hook results expose a `status` field (`'disabled' | 'connecting' | 'subscribing' | 'ready' | 'error'`).
364
+ - View hook objects (`arete.views.X.state` / `.list`) expose `refresh(key?)` and can be passed directly in `reconcile: { refresh: [...] }`.
365
+ - New `summarizeStatuses(namedResults)` status aggregator; `summarizeViews` remains as a deprecated alias.
366
+ - New `stringifyBigints(value)` display helper (also re-exported from `@usearete/sdk`).
367
+ - `UseAreteResult`'s second type parameter defaults to `undefined`.
368
+ - The duplicate low-level `useView` and `useEntity` hooks were removed. Use generated `arete.views.<Entity>.<view>.use(...)` hooks.
369
+
370
+ ## Migration notes (0.2 → 0.3)
371
+
372
+ - **Generated-hook mutations reconcile by default.** `submit()` now resolves after stream reconciliation instead of right after confirmation. Timeouts settle as `confirmed-unreconciled` rather than failing. Pass `reconcile: false` to restore the old behavior.
373
+ - **State view keys are required and generated.** Pass the generated key object, or explicitly pass `undefined`/`null` to disable the hook. Zero-argument state subscriptions are no longer supported, and malformed keys throw the shared core serializer error.
374
+ - **Disabled view hooks report `isLoading: false`.** Previously `enabled: false` still started in a loading state.
375
+ - New additive `UseMutationResult` fields: `isPreparing`, `isAwaitingWallet`, `isReconciling`.
376
+ - New mutation callback: `onConfirmed`, for work that must run after chain confirmation even if stream reconciliation later fails.
377
+ - `useArete` now exposes `socketIssue` and deduplicated `retry()`.
378
+ - Stack reads expose imperative calls and hooks together under `arete.read.<name>`; `arete.reads` remains a deprecated alias.
379
+ - Repeated `useArete` calls now share a provider-managed client when stack and option identities match; remove component-local connection ownership workarounds.
380
+
143
381
  ## Low-Level Hooks
144
382
 
145
383
  The React SDK also exports:
146
384
 
147
385
  - `useConnectionState`
148
- - `useView`
149
- - `useEntity`
150
386
  - `useAreteContext`
151
387
 
152
388
  These accept the same client lookup overrides when you need to target a non-default client.