@soba-so/react 0.0.9 → 0.0.11
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/dist/ComputeStatus.d.ts +21 -0
- package/dist/ConnectButton.d.ts +10 -2
- package/dist/ConnectModal.d.ts +9 -1
- package/dist/ConnectNudge.d.ts +120 -0
- package/dist/RunLimit.d.ts +55 -0
- package/dist/SobaBilling.d.ts +41 -0
- package/dist/SobaCurrentPlan.d.ts +29 -0
- package/dist/SobaPlans.d.ts +56 -1
- package/dist/SobaProvider.d.ts +9 -0
- package/dist/context.d.ts +46 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.mjs +1942 -122
- package/dist/styles.css +1937 -117
- package/dist/types.d.ts +16 -1
- package/dist/useCurrentPlan.d.ts +98 -0
- package/dist/useOpenSobaConnect.d.ts +32 -0
- package/dist/usePlans.d.ts +115 -0
- package/dist/useRunLimit.d.ts +140 -0
- package/package.json +9 -8
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and the code disagree about what a thing is called.
|
|
7
7
|
*/
|
|
8
8
|
export type MachineState = "online" | "busy" | "offline";
|
|
9
|
-
export type CostClass = "user-hardware" | "user-subscription" | "open-weights" | "frontier";
|
|
9
|
+
export type CostClass = "user-hardware" | "user-subscription" | "user-api-key" | "open-weights" | "frontier";
|
|
10
10
|
export interface Runtime {
|
|
11
11
|
id: string;
|
|
12
12
|
display_name: string;
|
|
@@ -97,6 +97,9 @@ export interface Plan {
|
|
|
97
97
|
interval: string;
|
|
98
98
|
unit: string;
|
|
99
99
|
included_units: number | null;
|
|
100
|
+
/** What happens past the allowance: `stop` or `meter`. The difference
|
|
101
|
+
* between a wall and a bill, and not guessable from the price. */
|
|
102
|
+
overage: string;
|
|
100
103
|
/** Dollars per unit past the allowance. */
|
|
101
104
|
overage_per_unit: number;
|
|
102
105
|
trial_days: number;
|
|
@@ -106,6 +109,18 @@ export interface Plan {
|
|
|
106
109
|
route_allow: string[] | null;
|
|
107
110
|
/** Whether it can be taken without a payment rail. */
|
|
108
111
|
selectable: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* WHAT CONNECTING THEIR OWN COMPUTE EARNS THEM.
|
|
114
|
+
*
|
|
115
|
+
* `none` | `unlimited` | `included` | `credit`. The one dimension no other
|
|
116
|
+
* pricing component has a column for, and the reason a free plan on this
|
|
117
|
+
* product can be worth choosing.
|
|
118
|
+
*/
|
|
119
|
+
connected_reward: string;
|
|
120
|
+
/** The larger allowance an `included` reward grants. Null otherwise. */
|
|
121
|
+
connected_included_units: number | null;
|
|
122
|
+
/** Dollars back per interval on a `credit` reward. Zero otherwise. */
|
|
123
|
+
connected_credit: number;
|
|
109
124
|
}
|
|
110
125
|
export interface CurrentPlan {
|
|
111
126
|
plan_id: string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CurrentPlan, Plan } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE ALLOWANCE, WORKED OUT ONCE, FOR WHOEVER IS DRAWING IT.
|
|
4
|
+
*
|
|
5
|
+
* `<SobaCurrentPlan />` is one rendering of this. The arithmetic is the part
|
|
6
|
+
* apps get wrong, and it is worth saying exactly why: it lives across two
|
|
7
|
+
* objects and four fields that disagree about what zero means.
|
|
8
|
+
*
|
|
9
|
+
* - `included_units: null` is unlimited. `0` is not. A falsy check reads them
|
|
10
|
+
* identically, which is how an unlimited plan starts advertising no
|
|
11
|
+
* allowance at all.
|
|
12
|
+
* - Past the allowance is a BILL on `meter` and a WALL on `stop`, and nothing
|
|
13
|
+
* in the price says which.
|
|
14
|
+
* - `connected_reward: "included"` RAISES the ceiling to
|
|
15
|
+
* `connected_included_units` while a machine is attached, awake or not,
|
|
16
|
+
* because a larger allowance is a promise about the month.
|
|
17
|
+
* - `connected_reward: "unlimited"` raises nothing. It takes the runs their own
|
|
18
|
+
* AI SERVES out of the count, so the ceiling stays the plan's and the number
|
|
19
|
+
* simply stops rising for those runs.
|
|
20
|
+
* - `credit` touches neither: it settles at renewal.
|
|
21
|
+
*
|
|
22
|
+
* Those last three mirror `allowanceFor` in `@soba-platform/core`, which is the
|
|
23
|
+
* rule the gateway actually enforces — so a card drawn from this and the run it
|
|
24
|
+
* describes cannot disagree about what is left.
|
|
25
|
+
*/
|
|
26
|
+
export type AllowanceState =
|
|
27
|
+
/** No plan is assigned, so there is no ceiling of this kind at all. */
|
|
28
|
+
"none"
|
|
29
|
+
/** A trial clock is running, which outranks every other thing to say. */
|
|
30
|
+
| "trial"
|
|
31
|
+
/** No ceiling: an unlimited plan. */
|
|
32
|
+
| "unlimited"
|
|
33
|
+
/** Inside the allowance. */
|
|
34
|
+
| "within"
|
|
35
|
+
/** Past it, and being billed. A bill is not a wall. */
|
|
36
|
+
| "metered"
|
|
37
|
+
/** Past it, and stopped until it resets. */
|
|
38
|
+
| "spent";
|
|
39
|
+
export interface CurrentPlanReward {
|
|
40
|
+
line: string;
|
|
41
|
+
/** The reward is live right now, because something is attached. */
|
|
42
|
+
inForce: boolean;
|
|
43
|
+
/** Not a reward at all: this plan cannot run until something is connected,
|
|
44
|
+
* and nothing is. The state `connected_reward` cannot express. */
|
|
45
|
+
required: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Something is attached right now, whatever it is doing.
|
|
48
|
+
*
|
|
49
|
+
* Not the same question as `inForce`: a `credit` plan with a laptop paired
|
|
50
|
+
* has a machine and no reward running yet, and a card that reads only the
|
|
51
|
+
* reward would draw that account as though it owned no computer. This is
|
|
52
|
+
* what decides whether the control beside the line ASKS for a machine or
|
|
53
|
+
* NAMES the one there is.
|
|
54
|
+
*/
|
|
55
|
+
connected: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether there is anything to press about it.
|
|
58
|
+
*
|
|
59
|
+
* True only while connecting would CHANGE something: a cap this plan would
|
|
60
|
+
* lift, or a requirement it has not met, with nothing attached. False once
|
|
61
|
+
* something is — repeating an offer to somebody who took it is the screen
|
|
62
|
+
* telling them it did not work — and false for a credit, which settles at
|
|
63
|
+
* renewal and cannot be acted on today.
|
|
64
|
+
*/
|
|
65
|
+
actionable: boolean;
|
|
66
|
+
/** Raise the connect flow. The same one flow the provider mounts, so a
|
|
67
|
+
* billing page with this and a pricing grid cannot put two on screen. */
|
|
68
|
+
open(): void;
|
|
69
|
+
}
|
|
70
|
+
export interface CurrentPlanView {
|
|
71
|
+
plan: Plan | null;
|
|
72
|
+
assignment: CurrentPlan | null;
|
|
73
|
+
loading: boolean;
|
|
74
|
+
error: Error | null;
|
|
75
|
+
state: AllowanceState;
|
|
76
|
+
/** Whole days, rounded up, because half a day left is a day left to whoever
|
|
77
|
+
* is reading — and zero days left while the trial runs is a lie. */
|
|
78
|
+
trialDaysLeft: number | null;
|
|
79
|
+
/** The ceiling that actually applies, raised where an `included` reward is in
|
|
80
|
+
* force. Null is unlimited. */
|
|
81
|
+
ceiling: number | null;
|
|
82
|
+
/** True when `ceiling` is larger than the plan's own because of a machine. */
|
|
83
|
+
raised: boolean;
|
|
84
|
+
/** Runs their own AI serves are not counted against the ceiling at all. */
|
|
85
|
+
uncounted: boolean;
|
|
86
|
+
used: number;
|
|
87
|
+
/** Against `ceiling`, floored at zero. Null when unlimited. */
|
|
88
|
+
remaining: number | null;
|
|
89
|
+
/** 0–100 against `ceiling`, or null when there is nothing to be a share of. */
|
|
90
|
+
percent: number | null;
|
|
91
|
+
unlimited: boolean;
|
|
92
|
+
periodEnd: Date | null;
|
|
93
|
+
reward: CurrentPlanReward | null;
|
|
94
|
+
/** One sentence, already chosen from the state above. The thing a billing
|
|
95
|
+
* page has to get right and the reason this hook exists. */
|
|
96
|
+
summary: string;
|
|
97
|
+
}
|
|
98
|
+
export declare function useCurrentPlan(): CurrentPlanView;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Launch } from "@soba-so/connect-ui";
|
|
2
|
+
/**
|
|
3
|
+
* THE PRESS THAT PUTS SOBA CONNECT IN FRONT OF SOMEBODY, and the one honest
|
|
4
|
+
* thing a page can say about it afterwards.
|
|
5
|
+
*
|
|
6
|
+
* Two surfaces need this and they draw it differently — the nudge above a
|
|
7
|
+
* composer puts the line under its detail, the wall puts it under its actions —
|
|
8
|
+
* so this is the behaviour without the markup, the same split `useRunLimit` and
|
|
9
|
+
* `<RunLimit />` already have.
|
|
10
|
+
*
|
|
11
|
+
* WHY THERE IS A NOTE AT ALL. No browser will tell a page whether a scheme
|
|
12
|
+
* handler exists; the only observable consequence of one is that something else
|
|
13
|
+
* takes the foreground, which is what `showSobaConnect` watches for. So a press
|
|
14
|
+
* can be followed by nothing happening, and a button that can do nothing
|
|
15
|
+
* without saying so is a button people press twice and then give up on.
|
|
16
|
+
*
|
|
17
|
+
* AND WHY IT SAYS SO LITTLE. `absent` means less here than it does in the
|
|
18
|
+
* connect flow: the machine that went quiet is very often NOT the computer this
|
|
19
|
+
* browser is on — a phone, a work laptop, the desktop in another room — and on
|
|
20
|
+
* that one there is nothing to open and nothing wrong. So the note reports what
|
|
21
|
+
* was seen and names the likeliest reason, and never concludes that the app is
|
|
22
|
+
* missing. `unknown` says nothing: the tab was already in the background, so
|
|
23
|
+
* the signal means nothing and inventing a sentence from it would be worse than
|
|
24
|
+
* the silence.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useOpenSobaConnect(): {
|
|
27
|
+
open: () => void;
|
|
28
|
+
/** What the attempt looked like, or null before one. */
|
|
29
|
+
launch: Launch | null;
|
|
30
|
+
/** The one line worth showing, or null when there is nothing honest to say. */
|
|
31
|
+
note: string | null;
|
|
32
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Plan } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE PRICE LIST AS A DECISION, NOT AS MARKUP.
|
|
4
|
+
*
|
|
5
|
+
* `<SobaPlans />` is one rendering of this, and it is the rendering that will
|
|
6
|
+
* never match the cards already on a customer's page. Everything that is
|
|
7
|
+
* genuinely OURS about a pricing surface lives here instead — and it is more
|
|
8
|
+
* than it looks, which is why every app that drew its own got at least one of
|
|
9
|
+
* these wrong:
|
|
10
|
+
*
|
|
11
|
+
* - **Free is selected, priced is checked out.** `select` answers 402 for
|
|
12
|
+
* anything with a price, so calling the wrong one is an error either way.
|
|
13
|
+
* - **The connect row has two reasons.** A plan that REWARDS connecting, and a
|
|
14
|
+
* plan that REQUIRES it — the second carries `connected_reward: "none"` and
|
|
15
|
+
* is invisible to anyone reading the reward alone, which is how the one card
|
|
16
|
+
* where a machine is mandatory ends up with no way to attach one.
|
|
17
|
+
* - **A credit is never a button.** It is settled at renewal against the share
|
|
18
|
+
* of the period their own compute served; there is nothing to press today.
|
|
19
|
+
* - **Which control is loud.** On a plan that is already theirs with nothing
|
|
20
|
+
* connected, the connect button is the only thing on the card that does
|
|
21
|
+
* anything, so it is the primary one and the plan's own control is not.
|
|
22
|
+
* - **Who pays for the compute.** The classes the user already owns cost the
|
|
23
|
+
* app nothing at the moment of a run, and that difference is the product.
|
|
24
|
+
*
|
|
25
|
+
* An app rendering its own pricing page reads this and draws it. Ours draws it
|
|
26
|
+
* too, from the same values, so the two cannot drift.
|
|
27
|
+
*/
|
|
28
|
+
export interface PlanPrice {
|
|
29
|
+
/** Already formatted: `$19`, or `Free`. */
|
|
30
|
+
amount: string;
|
|
31
|
+
/** `month`, or null on a free plan — where an app usually writes "forever". */
|
|
32
|
+
interval: string | null;
|
|
33
|
+
free: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface PlanCompute {
|
|
36
|
+
costClass: string;
|
|
37
|
+
/** What the class is called on a pricing page, not in the router. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** The user already pays for this one, so a run on it costs the app nothing.
|
|
40
|
+
* The whole argument of the product, as a boolean. */
|
|
41
|
+
owned: boolean;
|
|
42
|
+
/** The runtimes that satisfy it, for a caller drawing vendor marks. Empty for
|
|
43
|
+
* the classes we buy, which is an answer rather than a gap: a vendor mark
|
|
44
|
+
* there would imply they could bring something they cannot. */
|
|
45
|
+
runtimes: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface PlanConnect {
|
|
48
|
+
/** `Connect your own` for a reward, `To use this` for a requirement. */
|
|
49
|
+
key: string;
|
|
50
|
+
line: string;
|
|
51
|
+
/** Whether this plan rewards connecting (rather than requiring it). */
|
|
52
|
+
reward: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether there is anything to press, which is narrower than whether the line
|
|
55
|
+
* is worth reading: only on the plan they are ON, only while nothing is
|
|
56
|
+
* connected, and never for a credit. Connecting changes the allowance of the
|
|
57
|
+
* plan somebody holds, so the offer means nothing from a card they have not
|
|
58
|
+
* taken.
|
|
59
|
+
*
|
|
60
|
+
* WHEN THIS IS TRUE, THIS IS THE CARD'S CONTROL. The plan is already theirs,
|
|
61
|
+
* so `cta` below is a disabled "Your plan" — a button-shaped thing that does
|
|
62
|
+
* nothing — and drawing both put a live control above a dead one. The dead
|
|
63
|
+
* one is the one to drop.
|
|
64
|
+
*/
|
|
65
|
+
actionable: boolean;
|
|
66
|
+
/** Raise the connect flow, with the reason carried into its opening words. */
|
|
67
|
+
open(): void;
|
|
68
|
+
}
|
|
69
|
+
export interface PlanCta {
|
|
70
|
+
/** `Choose`, `Subscribe for $19/month`, `Your plan`, `Switching…`. */
|
|
71
|
+
label: string;
|
|
72
|
+
disabled: boolean;
|
|
73
|
+
busy: boolean;
|
|
74
|
+
/** Take this plan: granted outright when free, Stripe when priced. Rejects
|
|
75
|
+
* with whatever the endpoint said; `failed` below carries the last message
|
|
76
|
+
* for a caller that would rather read than catch. */
|
|
77
|
+
take(): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export interface PlanOffer {
|
|
80
|
+
plan: Plan;
|
|
81
|
+
current: boolean;
|
|
82
|
+
featured: boolean;
|
|
83
|
+
price: PlanPrice;
|
|
84
|
+
/** `2,000 runs`, or `Unlimited runs`. The figure a person compares on. */
|
|
85
|
+
allowance: string;
|
|
86
|
+
/** What follows from it: `Included each month · then $0.02 a run · 14-day
|
|
87
|
+
* trial`. A consequence of the figure rather than a second fact beside it. */
|
|
88
|
+
allowanceNote: string;
|
|
89
|
+
/** Null `route_allow` is any compute, an empty one is none, and both are
|
|
90
|
+
* sentences rather than lists. Null here means `compute` below is the list. */
|
|
91
|
+
computeLabel: string | null;
|
|
92
|
+
compute: PlanCompute[];
|
|
93
|
+
connect: PlanConnect | null;
|
|
94
|
+
cta: PlanCta;
|
|
95
|
+
}
|
|
96
|
+
export interface PlansOptions {
|
|
97
|
+
/** The plan the app is pushing, by id. Never derived: nothing on a plan says
|
|
98
|
+
* "recommended", and guessing from the price would be a component making a
|
|
99
|
+
* business decision for the app. Yields to `current`. */
|
|
100
|
+
featured?: string;
|
|
101
|
+
/** Handle a priced plan yourself instead of using Soba's checkout. */
|
|
102
|
+
onSelect?: (plan: Plan) => void;
|
|
103
|
+
successUrl?: string;
|
|
104
|
+
cancelUrl?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface PlansView {
|
|
107
|
+
offers: PlanOffer[];
|
|
108
|
+
/** The plan they are on, already resolved out of `offers`. */
|
|
109
|
+
current: PlanOffer | null;
|
|
110
|
+
loading: boolean;
|
|
111
|
+
error: Error | null;
|
|
112
|
+
/** What the last `take()` came back with. Cleared by the next attempt. */
|
|
113
|
+
failed: string | null;
|
|
114
|
+
}
|
|
115
|
+
export declare function usePlans(options?: PlansOptions): PlansView;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Plan } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE TWO FAILURES THE CONNECT UI ANSWERS, AND WHAT TO OFFER FOR EACH.
|
|
4
|
+
*
|
|
5
|
+
* `docs/errors.md` names exactly two run failures a person can do something
|
|
6
|
+
* about: `402 payment_required`, the plan's allowance is spent, and
|
|
7
|
+
* `409 no_compute`, nothing this run may use is connected and awake. Everything
|
|
8
|
+
* else is the app's bug, our outage, or a ceiling that is not an error.
|
|
9
|
+
*
|
|
10
|
+
* Until this hook, every app integrating Soba wrote the same dead end. Both
|
|
11
|
+
* example apps did it the same way — read `error.message`, throw the `code`
|
|
12
|
+
* away, render a red paragraph, and point at a settings page in another route.
|
|
13
|
+
* The information to do better was already on the wire; nothing turned it into
|
|
14
|
+
* an offer.
|
|
15
|
+
*
|
|
16
|
+
* SO THIS IS THE WHOLE TRIGGER LAYER, and it is deliberately headless. The
|
|
17
|
+
* moment of need lands inside somebody else's product, in the middle of their
|
|
18
|
+
* chat or their composer, and no stylesheet we ship will match the cards
|
|
19
|
+
* already on that screen. An app that wants ours renders `<RunLimit />`; an app
|
|
20
|
+
* with its own card grammar calls this and draws it, and both open the same
|
|
21
|
+
* modal, because the flow behind it is nine phases long and involves a
|
|
22
|
+
* download. A card is a trigger. The modal is the flow.
|
|
23
|
+
*/
|
|
24
|
+
/** The failure codes worth answering. Anything else returns null. */
|
|
25
|
+
export type LimitReason = "payment_required" | "no_compute";
|
|
26
|
+
export interface Offer {
|
|
27
|
+
reason: LimitReason;
|
|
28
|
+
/** A heading, in the app's own voice rather than the API's. */
|
|
29
|
+
title: string;
|
|
30
|
+
/** One line under it. Already accounts for what is connected. */
|
|
31
|
+
body: string;
|
|
32
|
+
/**
|
|
33
|
+
* What connecting earns, from the plan itself, or null when the plan offers
|
|
34
|
+
* nothing for it. Null is the signal NOT to offer connecting as a way out:
|
|
35
|
+
* an app whose plan carries no reward would be asking for a laptop and giving
|
|
36
|
+
* nothing back, and a person who connects one and sees no change has been
|
|
37
|
+
* told something untrue by a component.
|
|
38
|
+
*/
|
|
39
|
+
connectOffer: string | null;
|
|
40
|
+
/** The cheapest plan that would lift this, when one is for sale. */
|
|
41
|
+
upgradeTo: Plan | null;
|
|
42
|
+
/** Open the connect flow, with the reason carried into its opening copy. */
|
|
43
|
+
connect(): void;
|
|
44
|
+
/** Take `upgradeTo`: granted outright when free, Stripe otherwise. */
|
|
45
|
+
upgrade(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface RunLimitOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Answer from the plan's own state when nothing has failed yet, rather than
|
|
50
|
+
* only reporting what did.
|
|
51
|
+
*
|
|
52
|
+
* Defaults to true only when the hook is called with no arguments at all.
|
|
53
|
+
* Pass it explicitly with an error to get both halves — what is already in
|
|
54
|
+
* the way now, and what refused the run once one has been tried — which is
|
|
55
|
+
* the shape a chat wants and the wrong shape for a button.
|
|
56
|
+
*/
|
|
57
|
+
predict?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a Soba error code out of whatever your route handed back.
|
|
61
|
+
*
|
|
62
|
+
* Accepts the parsed body (`{ error: { code } }`, the OpenAI-compatible
|
|
63
|
+
* envelope every Soba failure uses), a `Response`, or an `Error` carrying
|
|
64
|
+
* either. Exported because the alternative an app reaches for is matching on
|
|
65
|
+
* the message text, which changes.
|
|
66
|
+
*/
|
|
67
|
+
export declare function sobaErrorCode(input: unknown): string | null;
|
|
68
|
+
/**
|
|
69
|
+
* WHAT, IF ANYTHING, IS IN THE WAY. Null when nothing is.
|
|
70
|
+
*
|
|
71
|
+
* Two ways to ask, one answer shape:
|
|
72
|
+
*
|
|
73
|
+
* **After a run failed** — pass whatever your catch block caught. Null for
|
|
74
|
+
* anything that is not one of the two answerable failures, so
|
|
75
|
+
* `{offer && <Card …/>}` is the whole integration and an unrelated error still
|
|
76
|
+
* renders however your app renders errors.
|
|
77
|
+
*
|
|
78
|
+
* **Before one is tried** — call it with NO ARGUMENTS, or pass
|
|
79
|
+
* `{ predict: true }` alongside the error to get both. It answers from what is
|
|
80
|
+
* already known: a plan that can only reach compute the user owns, with nothing
|
|
81
|
+
* connected, has nowhere to send a run, and saying so before somebody types a
|
|
82
|
+
* paragraph is strictly better than saying it after. This is the form a chat
|
|
83
|
+
* wants, because the card belongs in the stream above the composer rather than
|
|
84
|
+
* in the wreckage of a request.
|
|
85
|
+
*
|
|
86
|
+
* AN EMPTY ERROR SLOT IS NOT AN INVITATION TO PREDICT, and the distinction is
|
|
87
|
+
* the whole reason this takes an options object. `useState<unknown>(null)` is
|
|
88
|
+
* how every app in this repo holds the last failure, so `useRunLimit(error)`
|
|
89
|
+
* arrives as `null` on the very first render — and reading that as "no error
|
|
90
|
+
* was passed" put a card about a refused run on screen before anybody had
|
|
91
|
+
* pressed the button. `examples/inbox` opened on one. Only the ABSENCE of the
|
|
92
|
+
* argument asks for the state form now; a slot holding nothing means nothing
|
|
93
|
+
* has failed yet, which is the plainer reading of both.
|
|
94
|
+
*
|
|
95
|
+
* The state form is deliberately CONSERVATIVE. It reports only what the plan
|
|
96
|
+
* itself declares — its `route_allow` and its `overage` — and never reimplements
|
|
97
|
+
* the dispatch rule. Predicting a refusal is how a browser starts disagreeing
|
|
98
|
+
* with the server about whose turn it is to say no.
|
|
99
|
+
*/
|
|
100
|
+
export declare function useRunLimit(...args: [error?: unknown, options?: RunLimitOptions]): Offer | null;
|
|
101
|
+
/**
|
|
102
|
+
* THE REFUSAL THAT IS ALREADY CERTAIN, from facts the plan states about itself.
|
|
103
|
+
*
|
|
104
|
+
* Only two things qualify, and both are the plan's own declarations rather than
|
|
105
|
+
* a guess at what dispatch will decide:
|
|
106
|
+
*
|
|
107
|
+
* - **Nowhere to run.** `route_allow` names nothing but classes the user owns,
|
|
108
|
+
* and nothing they own can serve right now. The app has told us it will not
|
|
109
|
+
* buy this run; there is no machine to take it. That is a `409` waiting to
|
|
110
|
+
* happen, and it is the plan's own ceiling saying so.
|
|
111
|
+
*
|
|
112
|
+
* `online`, NOT `connected`, and the difference is the whole of the bug this
|
|
113
|
+
* fixed. A paired machine that has gone quiet — a shut lid, a dropped
|
|
114
|
+
* connection, a worker still retrying — is `connected` and cannot take a
|
|
115
|
+
* thing, so an app whose plan can only reach it said nothing at all until a
|
|
116
|
+
* run came back 409. `online` is false for BOTH of them: nothing paired, and
|
|
117
|
+
* nothing paired that is awake. It is the same fact dispatch checks first
|
|
118
|
+
* (`candidateMachines` is scoped to `state = online`), read from the same
|
|
119
|
+
* place, which is why this is not the browser inventing a rule.
|
|
120
|
+
* - **Nothing left.** The allowance is spent and `overage` is `stop`. `meter`
|
|
121
|
+
* is excluded on purpose: past the allowance it is a bill, not a wall, and
|
|
122
|
+
* blocking somebody who agreed to pay for overage would be this component
|
|
123
|
+
* inventing a policy.
|
|
124
|
+
*
|
|
125
|
+
* Anything subtler — a model no supplier carries, a cost class an environment
|
|
126
|
+
* excludes — is left to the server, which is the only party that knows.
|
|
127
|
+
*
|
|
128
|
+
* EXPORTED FOR ITS OWN TEST AND NOT FROM `index.ts`, so it is reachable by
|
|
129
|
+
* `packages/react/test` and not by anybody's app. This is the whole decision
|
|
130
|
+
* the predicting form makes, and it is two comparisons with four ways to be
|
|
131
|
+
* subtly wrong; the alternative is a React harness to assert a sentence.
|
|
132
|
+
*/
|
|
133
|
+
export declare function foreseen(plan: {
|
|
134
|
+
plan: Plan | null;
|
|
135
|
+
remaining: number | null;
|
|
136
|
+
}, compute: {
|
|
137
|
+
connected: boolean;
|
|
138
|
+
online: boolean;
|
|
139
|
+
loading: boolean;
|
|
140
|
+
}): LimitReason | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soba-so/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Connect the AI your users already pay for, from your own React app.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://soba.so/docs/components",
|
|
@@ -19,13 +19,18 @@
|
|
|
19
19
|
"!dist/**/*.map",
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "node ../../scripts/build-react.mjs",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
25
|
+
"prepublishOnly": "node ../../scripts/build-react.mjs"
|
|
26
|
+
},
|
|
22
27
|
"peerDependencies": {
|
|
23
28
|
"react": ">=18"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
31
|
+
"@soba-so/connect-ui": "workspace:*",
|
|
26
32
|
"@types/react": "^19.0.0",
|
|
27
|
-
"typescript": "^7.0.2"
|
|
28
|
-
"@soba-so/connect-ui": "0.1.0"
|
|
33
|
+
"typescript": "^7.0.2"
|
|
29
34
|
},
|
|
30
35
|
"engines": {
|
|
31
36
|
"node": ">=22"
|
|
@@ -39,9 +44,5 @@
|
|
|
39
44
|
],
|
|
40
45
|
"publishConfig": {
|
|
41
46
|
"access": "public"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "node ../../scripts/build-react.mjs",
|
|
45
|
-
"typecheck": "tsc -p tsconfig.json"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|