@soba-so/react 0.0.13 → 0.0.15

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
@@ -32,7 +32,7 @@ export async function POST() {
32
32
  authorization: `Bearer ${process.env.SOBA_KEY}`,
33
33
  "content-type": "application/json",
34
34
  },
35
- body: JSON.stringify({ user: session.user.id }),
35
+ body: JSON.stringify({ user: sobaUserId(session.user.username) }),
36
36
  })
37
37
  return Response.json(await res.json())
38
38
  }
@@ -42,6 +42,12 @@ That is the whole integration. `user` is the same id you pass on a run, which is
42
42
  what makes the person whose machine this is and the person a run is attributed to
43
43
  the same row.
44
44
 
45
+ It also has to outlive your database. `session.user.id` is usually a key minted
46
+ at sign-up, so a reset or a move turns one person into two — holding the machine
47
+ under the old id, on metered compute under the new one. `sobaUserId`, from the
48
+ server-only subpath `@soba-so/sdk/identity`, derives a stable id from something
49
+ that survives: see [Identifying users](https://soba.so/docs/identity).
50
+
45
51
  The session lives ten minutes. A settings page left open for an hour is the
46
52
  ordinary case, so the provider refreshes it before it expires **and** heals a
47
53
  request that meets a dead one: a 401 mints a new session and retries once, for
@@ -106,11 +112,12 @@ Point the provider at a route that does it, and the hook will read the stream:
106
112
  ```ts
107
113
  // app/api/soba/run/route.ts
108
114
  import { Soba } from "@soba-so/sdk"
115
+ import { sobaUserId } from "@soba-so/sdk/identity"
109
116
 
110
117
  export async function POST(request: Request) {
111
118
  const session = await auth()
112
119
  const soba = new Soba({ apiKey: process.env.SOBA_KEY })
113
- const run = await soba.run({ user: session.user.id, ...(await request.json()) })
120
+ const run = await soba.run({ user: sobaUserId(session.user.username), ...(await request.json()) })
114
121
  return new Response(toSSE(run), { headers: { "content-type": "text/event-stream" } })
115
122
  }
116
123
  ```
@@ -0,0 +1,66 @@
1
+ /**
2
+ * THE OTHER HALF OF A RUN THAT DID NOT WORK.
3
+ *
4
+ * `<RunLimit>` and `<ConnectNudge>` answer the two failures a person can do
5
+ * something about — `payment_required` and `no_compute` — both decided BEFORE a
6
+ * run starts, where "connect something" or "upgrade" is a real button. This
7
+ * answers everything else: a supplier that refused, a key with no credit left,
8
+ * a model that is gone, a machine that disconnected mid-answer.
9
+ *
10
+ * It exists because that half was shipped as homework. `useRunLimit` returns
11
+ * null for anything unanswerable, which is correct and is also an instruction
12
+ * to the app to go and render something itself — so every surface in this repo
13
+ * wrote its own, four times, each one slightly different, and the one with the
14
+ * most machinery dropped the message on the floor for a month without anybody
15
+ * noticing. A `<p className="error">` is not a hard component to write. It is a
16
+ * very easy one to forget, and forgetting it looks exactly like the product
17
+ * doing nothing at all.
18
+ *
19
+ * THE PAIR IS THE POINT. Drop both in and they never both speak:
20
+ *
21
+ * <RunLimit error={failure} />
22
+ * <RunError error={failure} />
23
+ *
24
+ * The first draws when the failure is answerable and the second when it is not,
25
+ * so between them every failed run says something. That is the whole
26
+ * integration, and neither one needs the app to classify anything.
27
+ */
28
+ export interface RunErrorProps {
29
+ /**
30
+ * Whatever your catch block caught. `null` — the value every app holds before
31
+ * the first failure — renders nothing, as does any failure `<RunLimit>` is
32
+ * already answering.
33
+ *
34
+ * Required, unlike `RunLimit.error`, and deliberately: there is no state form
35
+ * of this. A limit can be foreseen from the plan before anybody presses
36
+ * anything; an error cannot be foreseen at all, and a component that drew one
37
+ * speculatively would be inventing a failure.
38
+ */
39
+ error: unknown;
40
+ /**
41
+ * Run it again. Omitted draws no button, which is right for a surface where
42
+ * retrying is not one press — a chat has its own regenerate and should not
43
+ * grow a second one.
44
+ */
45
+ onRetry?: () => void;
46
+ /** Dismissing. Without it the message stays until the next run replaces it. */
47
+ onDismiss?: () => void;
48
+ /**
49
+ * The line above the message. The default names no cause, because this
50
+ * component by definition does not know the cause — the one thing it is sure
51
+ * of is that the run did not finish.
52
+ */
53
+ title?: string;
54
+ className?: string;
55
+ }
56
+ /**
57
+ * WHAT TO SAY ABOUT A FAILURE, or null when it is not this component's to say.
58
+ *
59
+ * Exported for the same reason `useRunLimit` is: an app whose errors have their
60
+ * own shape should ask this and draw its own. Null means either nothing failed
61
+ * or `<RunLimit>` has it, and in both cases the right thing to render is
62
+ * nothing — so `{runErrorMessage(e) && <MyError …/>}` is a complete integration
63
+ * without the app ever naming a status code.
64
+ */
65
+ export declare function runErrorMessage(error: unknown): string | null;
66
+ export declare function RunError({ error, onRetry, onDismiss, title, className }: RunErrorProps): import("react").JSX.Element | null;
@@ -15,14 +15,17 @@ export interface SobaProviderProps {
15
15
  * const res = await fetch("https://www.soba.so/v1/end_users/session", {
16
16
  * method: "POST",
17
17
  * headers: { authorization: `Bearer ${process.env.SOBA_KEY}` },
18
- * body: JSON.stringify({ user: session.user.id }),
18
+ * body: JSON.stringify({ user: sobaUserId(session.user.username) }),
19
19
  * })
20
20
  * return Response.json(await res.json())
21
21
  * ```
22
22
  */
23
23
  sessionEndpoint?: string;
24
24
  /** The same id you pass as `user` on a run. It is what makes the person whose
25
- * machine this is and the person a run is attributed to the same row. */
25
+ * machine this is and the person a run is attributed to the same row — and
26
+ * it has to outlive your database, or a reset splits that person in two.
27
+ * `sobaUserId` from `@soba-so/sdk/identity` derives one; the reasoning is at
28
+ * https://soba.so/docs/identity. */
26
29
  user?: string;
27
30
  /** Your route that starts a run with `@soba-so/sdk` and forwards the stream. */
28
31
  runEndpoint?: string;
package/dist/context.d.ts CHANGED
@@ -20,9 +20,16 @@ export interface ComputeState {
20
20
  error: Error | null;
21
21
  refresh(): Promise<void>;
22
22
  }
23
- /** Why the flow was opened, carried into the panel's opening words. A person
24
- * whose laptop went to sleep should not be sold the idea from scratch. */
25
- export type ConnectReason = "payment_required" | "no_compute" | "manual";
23
+ /**
24
+ * Why the flow was opened, carried into the panel's opening words. A person
25
+ * whose laptop went to sleep should not be sold the idea from scratch.
26
+ *
27
+ * `pair` is the one that is not about words: it says the caller is ALREADY
28
+ * showing the steady state — the panel under `<ConnectButton />` — so the
29
+ * sheet must open on the chooser rather than repeating it. The other three
30
+ * leave where it opens to the panel.
31
+ */
32
+ export type ConnectReason = "payment_required" | "no_compute" | "manual" | "pair";
26
33
  export interface ConnectState {
27
34
  /** Your app's name, as the person connecting will see it. */
28
35
  app: {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { ConnectModal, type ConnectModalProps } from "./ConnectModal.js";
5
5
  export { ConnectNudge, type ConnectNudgeProps } from "./ConnectNudge.js";
6
6
  export { type ComputeState, type ConnectReason, type ConnectState, type PlanState, type RunRequest, type SobaContextValue, useSoba, } from "./context.js";
7
7
  export { collectText, parseEvent, readEvents } from "./events.js";
8
+ export { RunError, type RunErrorProps, runErrorMessage } from "./RunError.js";
8
9
  export { RunLimit, type RunLimitProps } from "./RunLimit.js";
9
10
  export { SobaBilling, type SobaBillingProps } from "./SobaBilling.js";
10
11
  export { SobaCheckout, type SobaCheckoutProps } from "./SobaCheckout.js";