@cosmoops/connect-sdk 0.33.0 → 0.34.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.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
  import { Ban, AlertTriangle, Building2, Clock, CreditCard, ExternalLink, User, X } from "lucide-react";
5
5
  import { Button, Card } from "@heroui/react";
6
6
  import { blockingBlockers } from "../../helpers.js";
@@ -36,7 +36,19 @@ function formatDate(iso) {
36
36
  */
37
37
  export function useConnectAccessGate() {
38
38
  const { session, isLoading: sessionLoading } = useConnect();
39
- const { data, isLoading: entitlementsLoading } = useConnectEntitlements();
39
+ const { data, isLoading: entitlementsLoading, refetch } = useConnectEntitlements();
40
+ return { ...decideAccess(session, sessionLoading, data, entitlementsLoading), refresh: refetch };
41
+ }
42
+ /**
43
+ * The priority ordering, with the reads lifted out.
44
+ *
45
+ * Split from the hook so the twelve verdicts stay one `return` each and none
46
+ * of them has to carry `refresh` - a field that is the same function in every
47
+ * branch and says nothing about which branch it is. Pure, like
48
+ * `rbac/policy.ts` and `evaluateSignInRisk`, and for the same reason: a
49
+ * decision spread across its inputs cannot be tested.
50
+ */
51
+ function decideAccess(session, sessionLoading, data, entitlementsLoading) {
40
52
  if (sessionLoading) {
41
53
  return { state: "loading", blocker: null, paymentRetry: null };
42
54
  }
@@ -147,6 +159,49 @@ export function useConnectAccessGate() {
147
159
  */
148
160
  export function ConnectAccessGate({ children, fallback, loading, className }) {
149
161
  const gate = useConnectAccessGate();
162
+ const isBlocked = gate.state !== "loading" &&
163
+ gate.state !== "signed_out" &&
164
+ gate.state !== "clear" &&
165
+ gate.state !== "payment_retry";
166
+ /**
167
+ * A blocked gate re-checks when the tab comes back.
168
+ *
169
+ * Every blocker this component draws is resolved **somewhere else** - on
170
+ * Connect, in another tab, by an admin, on a phone - and none of that
171
+ * reaches this tab. The resolve button used to open Connect in a new tab,
172
+ * so the overwhelmingly common path was: subscribe over there, switch back,
173
+ * and find the same "Choose a plan to continue" card, with a reload the
174
+ * only way out. It read as the purchase not having worked.
175
+ *
176
+ * The button is a same-tab navigation now (see `GateCard`), which handles
177
+ * the common case on its own. This covers the rest: a second tab, a
178
+ * colleague with the admin rights, a payment settled on a phone.
179
+ *
180
+ * Only while blocked, deliberately. A `clear` gate re-reading entitlements
181
+ * on every tab focus would put a request on the critical path of
182
+ * alt-tabbing back into an application that is working fine, for every
183
+ * workspace, all day. Nothing is gained by it: the gate has nothing to
184
+ * show, and losing access mid-session is what the server-side check before
185
+ * each action is for.
186
+ */
187
+ useEffect(() => {
188
+ if (!isBlocked || typeof document === "undefined")
189
+ return;
190
+ const recheck = () => {
191
+ if (document.visibilityState === "visible")
192
+ gate.refresh();
193
+ };
194
+ document.addEventListener("visibilitychange", recheck);
195
+ window.addEventListener("focus", recheck);
196
+ return () => {
197
+ document.removeEventListener("visibilitychange", recheck);
198
+ window.removeEventListener("focus", recheck);
199
+ };
200
+ // `gate.refresh` is stable per render of the provider and re-subscribing
201
+ // on it would churn the listeners; `isBlocked` is the only thing that
202
+ // should add or remove them.
203
+ // eslint-disable-next-line react-hooks/exhaustive-deps
204
+ }, [isBlocked]);
150
205
  // `children`, unless the host explicitly asked for a placeholder. See the
151
206
  // doc comment - this is the branch that decides whether a partner's page
152
207
  // can stream.
@@ -215,9 +270,27 @@ function AccessGateFallback({ state, blocker, className, }) {
215
270
  * so "see plans" landing back on the same blocked screen never gets anyone
216
271
  * to a choice they can act on. Sending them to Connect's own subscribe page
217
272
  * always resolves.
273
+ *
274
+ * ### Same tab, and it carries the way back
275
+ *
276
+ * It used to `window.open(resolveUrl, "_blank")`. A new tab is the wrong
277
+ * shape for a step you have to finish before you can continue: the customer
278
+ * subscribes over there, is left on Connect's billing page with nothing
279
+ * pointing home, and the tab they started in is still showing this card. The
280
+ * most common ending was two open tabs and the belief that the payment had
281
+ * not worked.
282
+ *
283
+ * So: a same-tab navigation carrying `?next=` back to the page they are
284
+ * standing on, which Connect honours after a subscribe once the origin
285
+ * matches one registered for this application's client
286
+ * (`resolveApplicationReturnUrl`). An unregistered origin costs the return
287
+ * trip and nothing else - they still reach Connect, still resolve the
288
+ * blocker, and `ConnectAccessGate`'s focus re-check catches them on the way
289
+ * back. Partners should register their app's origin; the fallback is a
290
+ * degraded path, not a broken one.
218
291
  */
219
292
  function GateCard({ icon: Icon, iconClassName, title, message, actionLabel, resolveUrl, className, }) {
220
- return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), _jsx(Card.Title, { children: title })] }), _jsx(Card.Description, { children: message })] }), resolveUrl && (_jsx(Card.Footer, { children: _jsxs(Button, { variant: "primary", onPress: () => window.open(resolveUrl, "_blank", "noopener,noreferrer"), children: [actionLabel ?? "Resolve on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }) }))] }) }) }));
293
+ return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), _jsx(Card.Title, { children: title })] }), _jsx(Card.Description, { children: message })] }), resolveUrl && (_jsx(Card.Footer, { children: _jsxs(Button, { variant: "primary", onPress: () => window.location.assign(withReturnTo(resolveUrl)), children: [actionLabel ?? "Resolve on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }) }))] }) }) }));
221
294
  }
222
295
  /**
223
296
  * The skippable payment-due warning. Dismissal is scoped to this specific
@@ -236,3 +309,26 @@ function PaymentRetryBanner({ blocker, className, }) {
236
309
  window.localStorage.setItem(dismissKey, "1");
237
310
  }, children: _jsx(X, { className: "size-4" }) })] })] }) }));
238
311
  }
312
+ /**
313
+ * `resolveUrl` with `?next=` pointing at the page the customer is standing
314
+ * on, so Connect can hand them back once the blocker is gone.
315
+ *
316
+ * Returns the URL untouched if anything is off - no `window`, an unparseable
317
+ * `resolveUrl`, a `next` already set by the server. Getting back is a
318
+ * courtesy on top of resolving the blocker, and it must never be the reason
319
+ * somebody cannot reach the screen that resolves it.
320
+ */
321
+ function withReturnTo(resolveUrl) {
322
+ if (typeof window === "undefined")
323
+ return resolveUrl;
324
+ try {
325
+ const url = new URL(resolveUrl);
326
+ if (url.searchParams.has("next"))
327
+ return resolveUrl;
328
+ url.searchParams.set("next", window.location.href);
329
+ return url.toString();
330
+ }
331
+ catch {
332
+ return resolveUrl;
333
+ }
334
+ }
@@ -483,7 +483,7 @@ export function useConnectSessions() {
483
483
  */
484
484
  export function useConnectResource(path, options) {
485
485
  const requiresSession = options?.requiresSession ?? true;
486
- const { fetchResource, session, isLoading: sessionLoading } = useConnect();
486
+ const { fetchResource, invalidateResource, session, isLoading: sessionLoading } = useConnect();
487
487
  // Stamped with the `path` it belongs to, so switching paths reports loading
488
488
  // without a synchronous `setIsLoading(true)` - and, more usefully, so a
489
489
  // resolved response for a previous path can never be read as this one's.
@@ -523,6 +523,9 @@ export function useConnectResource(path, options) {
523
523
  // should render its signed-out state, not skeletons forever.
524
524
  isLoading: !signedOut && !loaded,
525
525
  error: loaded?.error ?? null,
526
- refetch: () => setVersion((v) => v + 1),
526
+ refetch: () => {
527
+ invalidateResource(path);
528
+ setVersion((v) => v + 1);
529
+ },
527
530
  };
528
531
  }
@@ -161,12 +161,14 @@ export function buildCancelUrl({ appUrl, subscriptionId }) {
161
161
  * prefix that has never served this page. Building the other one is what
162
162
  * sent every first-time subscriber from a partner's pricing page to a 404.
163
163
  */
164
- export function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, }) {
164
+ export function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, next, }) {
165
165
  const path = subscriptionId
166
166
  ? `/billing/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan/${encodeURIComponent(planId)}`
167
167
  : `/apps/${encodeURIComponent(applicationId)}/subscribe/${encodeURIComponent(planId)}`;
168
168
  const url = new URL(path, appUrl);
169
169
  if (coupon?.trim())
170
170
  url.searchParams.set("coupon", coupon.trim());
171
+ if (next?.trim())
172
+ url.searchParams.set("next", next.trim());
171
173
  return url.toString();
172
174
  }
@@ -24,3 +24,46 @@ export function blockingBlockers(readiness) {
24
24
  export function normalizePlanFeatures(features) {
25
25
  return (features ?? []).map((feature) => typeof feature === "string" ? { key: feature, name: feature, description: "" } : feature);
26
26
  }
27
+ /**
28
+ * What a key the plan carries no answer for reads as - see
29
+ * `SubscribedPlan.limits`, which is sparse.
30
+ *
31
+ * `"none"` rather than `"unlimited"`: a plan that was never asked about a
32
+ * limit has not sold it, and defaulting the other way would hand an unbounded
33
+ * allowance to every tier that predates the question.
34
+ */
35
+ export const NO_PLAN_LIMIT = { model: "none", value: null };
36
+ /**
37
+ * One plan's answer for one limit key, with the sparse-map default applied.
38
+ *
39
+ * Indexing `plan.limits` directly is what makes a missing key `undefined`,
40
+ * which then reads as unlimited through the first `?.` somebody writes. This
41
+ * returns a `PlanLimit` either way.
42
+ */
43
+ export function planLimitFor(limits, key) {
44
+ return limits?.[key] ?? NO_PLAN_LIMIT;
45
+ }
46
+ /**
47
+ * Whether `additional` more of the resource fit under this limit, given how
48
+ * many the subscriber already has. The comparison, written once.
49
+ *
50
+ * Every way of writing it at a call site is wrong in a different direction:
51
+ * `count < limit.value` is off by one as soon as you add more than one row,
52
+ * `if (!limit.value)` refuses the unlimited tier, and
53
+ * `limit.value ?? Infinity` grants unlimited to a plan that allows none. This
54
+ * is the same implementation Connect's own server uses to validate a plan.
55
+ *
56
+ * ```ts
57
+ * const limit = await connect.entitlements.getLimit(workspaceId, "sites");
58
+ * if (!planLimitAllows(limit, await countSites(workspaceId))) {
59
+ * throw new Error("Your plan's site limit is reached.");
60
+ * }
61
+ * ```
62
+ */
63
+ export function planLimitAllows(limit, currentCount, additional = 1) {
64
+ if (limit.model === "unlimited")
65
+ return true;
66
+ if (limit.model === "none")
67
+ return false;
68
+ return currentCount + additional <= (limit.value ?? 0);
69
+ }
package/dist/esm/index.js CHANGED
@@ -15,5 +15,6 @@
15
15
  */
16
16
  export { ConnectError, isConnectError } from "./errors.js";
17
17
  export { blockingBlockers, normalizePlanFeatures } from "./helpers.js";
18
+ export { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "./helpers.js";
18
19
  export { arrayRemove, arrayUnion, deleteField, documentExists, increment, serverTimestamp, storeIndexFor, storeIndexSignature, unchangedSince, } from "./store.js";
19
20
  export { describeQueryRejection, storeIndexCovers } from "./store-schema.js";
@@ -35,11 +35,18 @@ import { ConnectError } from "../errors.js";
35
35
  * | --- | --- | --- |
36
36
  * | `CONNECT_API_KEY` | yes | server-side credential |
37
37
  * | `CONNECT_CLIENT_ID` | yes | OAuth client id |
38
- * | `CONNECT_APP_URL` | no | non-production Connect instance |
39
- * | `CONNECT_SESSION_COOKIE` | no | session cookie name |
38
+ * | `CONNECT_APP_URL` | no | non-production Connect instance; unset means production |
39
+ * | `CONNECT_SESSION_COOKIE` | no | session cookie name; unset means `connect_session` |
40
+ *
41
+ * `CONNECT_APP_URL` is server-side only and does not reach the browser:
42
+ * `ConnectProvider` defaults `appUrl` to the production origin independently,
43
+ * so an app pointed at another instance must pass that prop as well or its
44
+ * portal links and its browser-built authorize URL still go to production.
40
45
  *
41
46
  * Register `{your origin}/api/connect/callback` as a redirect URI in the
42
- * application's Connect client section. That is the only step this cannot do for you.
47
+ * application's Connect client section. That is the only step this cannot do
48
+ * for you, and Connect matches it exactly - every origin you run from needs
49
+ * its own entry, localhost included.
43
50
  *
44
51
  * No `next.config.ts` changes needed either, including `transpilePackages`:
45
52
  * `dist/` is already ES2022 with `"use client"` hoisted to the top of every
@@ -175,4 +175,4 @@ export class ConnectClient {
175
175
  }
176
176
  }
177
177
  /** Kept in step with `package.json` by `sdk.test.ts`. */
178
- export const SDK_VERSION = "0.32.0";
178
+ export const SDK_VERSION = "0.34.0";
@@ -14,6 +14,10 @@ export class BillingResource {
14
14
  * independently of the instance it talks to. Without the coercion a partner
15
15
  * pointed at a Connect that has not upgraded yet renders a feature list of
16
16
  * blanks - which is exactly what happened. See `normalizePlanFeatures`.
17
+ *
18
+ * `limits` is defaulted for the same reason, one version later: an instance
19
+ * that predates limits sends no such field, and a pricing grid mapping over
20
+ * `plan.limits` would throw rather than simply show no limit rows.
17
21
  */
18
22
  async listPlans() {
19
23
  const response = await request(this.transport, { method: "GET", path: "/plans" });
@@ -21,6 +25,7 @@ export class BillingResource {
21
25
  plans: (response.plans ?? []).map((plan) => ({
22
26
  ...plan,
23
27
  features: normalizePlanFeatures(plan.features),
28
+ limits: plan.limits ?? [],
24
29
  })),
25
30
  };
26
31
  }
@@ -1,4 +1,5 @@
1
1
  import { pathSegment, request } from "../http.js";
2
+ import { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "../../helpers.js";
2
3
  /**
3
4
  * The two checks worth making on every request: may this workspace be here at
4
5
  * all, and what is it paying for.
@@ -42,6 +43,52 @@ export class EntitlementsResource {
42
43
  const result = await this.getSubscription(workspaceId);
43
44
  return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
44
45
  }
46
+ /**
47
+ * How much of a metered resource this workspace may have - the numeric
48
+ * counterpart of `hasFeature`, for the resources your application counts
49
+ * itself (sites, projects, pipelines).
50
+ *
51
+ * **This is the effective ceiling, not the plan's own number.** Connect has
52
+ * already folded in whatever the add-ons on the subscription raise it by, so
53
+ * "Starter includes 1 site, they bought two 5-site packs" arrives here as
54
+ * `{ model: "limited", value: 11 }`. You never join the plan against the
55
+ * add-on list yourself, and a customer who buys another pack needs no change
56
+ * on your side.
57
+ *
58
+ * Returns `{ model: "none" }` rather than throwing when there is no active
59
+ * subscription, when the plan carries no answer for this key, and when the
60
+ * Connect instance predates limits entirely - the same reasoning
61
+ * `hasFeature` gives for answering `false`: "not entitled" is the same answer
62
+ * either way, and a limit check should not need a try/catch around it.
63
+ *
64
+ * Connect does not enforce this. It cannot see what you are counting, so it
65
+ * hands you the number and your code refuses the next row - `canAdd` is that
66
+ * comparison if you already have the count.
67
+ */
68
+ async getLimit(workspaceId, key) {
69
+ const result = await this.getSubscription(workspaceId);
70
+ if (!result.hasActiveSubscription)
71
+ return NO_PLAN_LIMIT;
72
+ return planLimitFor(result.plan?.limits, key);
73
+ }
74
+ /**
75
+ * Whether this workspace may add `additional` more of a limited resource,
76
+ * given how many it already has.
77
+ *
78
+ * You supply the count, because only your application can produce it. This
79
+ * is `getLimit` plus `planLimitAllows`, which exists so the comparison is
80
+ * not rewritten per call site - see that helper for the three ways of
81
+ * getting it wrong.
82
+ *
83
+ * ```ts
84
+ * if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
85
+ * return refuse("Your plan's site limit is reached.");
86
+ * }
87
+ * ```
88
+ */
89
+ async canAdd(workspaceId, key, currentCount, additional = 1) {
90
+ return planLimitAllows(await this.getLimit(workspaceId, key), currentCount, additional);
91
+ }
45
92
  /**
46
93
  * One page of the workspaces subscribed to your application, newest first.
47
94
  *
@@ -23,6 +23,15 @@ export type ConnectAccessGateState = "loading"
23
23
  | "clear";
24
24
  export interface ConnectAccessGateResult {
25
25
  state: ConnectAccessGateState;
26
+ /**
27
+ * Re-reads entitlements, evicting the cached answer first.
28
+ *
29
+ * Exposed because a blocker is almost always resolved **somewhere this tab
30
+ * cannot see** - on Connect, in another tab or on a phone - and nothing
31
+ * about that reaches this one. The gate wires it to the tab regaining
32
+ * focus; call it yourself if your app learns of a change some other way.
33
+ */
34
+ refresh(): void;
26
35
  /** The blocker driving a hard-block state (`suspended`/`no_subscription`/`onboarding_pending`/`blocked`). `null` otherwise. */
27
36
  blocker: ReadinessBlocker | null;
28
37
  /** The `payment_retry_pending` blocker, only when `state === "payment_retry"`. Carries `graceEndsAt`. */
@@ -196,7 +196,21 @@ export interface ConnectProviderProps {
196
196
  * not a Connect URL - it is your backend that exchanges the code.
197
197
  */
198
198
  redirectUri?: string;
199
- /** Override for non-production Connect instances. */
199
+ /**
200
+ * Override for non-production Connect instances. Defaults to
201
+ * `https://connect.cosmoops.com`.
202
+ *
203
+ * The twin of `CONNECT_APP_URL`, and *not* derived from it - that variable
204
+ * is read on your server and never reaches the browser. This value is where
205
+ * the user menu's Profile, Billing and Invoices destinations open, where
206
+ * `ConnectAppsGrid` sends an unsubscribed application, where checkout
207
+ * opens, and - if you pass `redirectUri` and so build the authorize URL
208
+ * here rather than in your own `/signin` route - where sign-in goes. Point
209
+ * the server somewhere else and leave this alone and the two halves
210
+ * disagree, so pass both or neither. Your root layout is a Server
211
+ * Component, so `appUrl={process.env.CONNECT_APP_URL}` is enough and needs
212
+ * no browser-visible variable.
213
+ */
200
214
  appUrl?: string;
201
215
  /**
202
216
  * Replace the `fetch` used to reach your bridge. The browser-side twin of
@@ -327,11 +341,20 @@ export declare function useConnectEntitlements(): {
327
341
  isLoading: boolean;
328
342
  error: string | null;
329
343
  /**
330
- * Forces a fresh read - the provider's cache clears the mutated path's
331
- * entry on a successful `mutateResource` call, but nothing re-triggers
332
- * this hook's own effect on its own (none of its dependencies change from
333
- * a cache write nobody here observes). Call this right after a
334
- * `mutateResource` you made against the same `path` completes.
344
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
345
+ * read.
346
+ *
347
+ * It used only to re-trigger the effect, on the assumption that the caller
348
+ * had just evicted the entry itself through `mutateResource`. That made it
349
+ * useless for the other reason to refetch: **something changed somewhere
350
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
351
+ * promises with no expiry, so a `refetch()` without the eviction re-read
352
+ * the same settled promise and reported the same stale answer forever -
353
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
354
+ * whose workspace had subscribed in a different one.
355
+ *
356
+ * Still safe after a `mutateResource` against the same path: evicting an
357
+ * entry that is already gone is a no-op.
335
358
  */
336
359
  refetch(): void;
337
360
  };
@@ -347,11 +370,20 @@ export declare function useConnectAddonUsage(): {
347
370
  isLoading: boolean;
348
371
  error: string | null;
349
372
  /**
350
- * Forces a fresh read - the provider's cache clears the mutated path's
351
- * entry on a successful `mutateResource` call, but nothing re-triggers
352
- * this hook's own effect on its own (none of its dependencies change from
353
- * a cache write nobody here observes). Call this right after a
354
- * `mutateResource` you made against the same `path` completes.
373
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
374
+ * read.
375
+ *
376
+ * It used only to re-trigger the effect, on the assumption that the caller
377
+ * had just evicted the entry itself through `mutateResource`. That made it
378
+ * useless for the other reason to refetch: **something changed somewhere
379
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
380
+ * promises with no expiry, so a `refetch()` without the eviction re-read
381
+ * the same settled promise and reported the same stale answer forever -
382
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
383
+ * whose workspace had subscribed in a different one.
384
+ *
385
+ * Still safe after a `mutateResource` against the same path: evicting an
386
+ * entry that is already gone is a no-op.
355
387
  */
356
388
  refetch(): void;
357
389
  };
@@ -361,11 +393,20 @@ export declare function useConnectPlans(): {
361
393
  isLoading: boolean;
362
394
  error: string | null;
363
395
  /**
364
- * Forces a fresh read - the provider's cache clears the mutated path's
365
- * entry on a successful `mutateResource` call, but nothing re-triggers
366
- * this hook's own effect on its own (none of its dependencies change from
367
- * a cache write nobody here observes). Call this right after a
368
- * `mutateResource` you made against the same `path` completes.
396
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
397
+ * read.
398
+ *
399
+ * It used only to re-trigger the effect, on the assumption that the caller
400
+ * had just evicted the entry itself through `mutateResource`. That made it
401
+ * useless for the other reason to refetch: **something changed somewhere
402
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
403
+ * promises with no expiry, so a `refetch()` without the eviction re-read
404
+ * the same settled promise and reported the same stale answer forever -
405
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
406
+ * whose workspace had subscribed in a different one.
407
+ *
408
+ * Still safe after a `mutateResource` against the same path: evicting an
409
+ * entry that is already gone is a no-op.
369
410
  */
370
411
  refetch(): void;
371
412
  };
@@ -384,11 +425,20 @@ export declare function useConnectCompanyProfile(): {
384
425
  isLoading: boolean;
385
426
  error: string | null;
386
427
  /**
387
- * Forces a fresh read - the provider's cache clears the mutated path's
388
- * entry on a successful `mutateResource` call, but nothing re-triggers
389
- * this hook's own effect on its own (none of its dependencies change from
390
- * a cache write nobody here observes). Call this right after a
391
- * `mutateResource` you made against the same `path` completes.
428
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
429
+ * read.
430
+ *
431
+ * It used only to re-trigger the effect, on the assumption that the caller
432
+ * had just evicted the entry itself through `mutateResource`. That made it
433
+ * useless for the other reason to refetch: **something changed somewhere
434
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
435
+ * promises with no expiry, so a `refetch()` without the eviction re-read
436
+ * the same settled promise and reported the same stale answer forever -
437
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
438
+ * whose workspace had subscribed in a different one.
439
+ *
440
+ * Still safe after a `mutateResource` against the same path: evicting an
441
+ * entry that is already gone is a no-op.
392
442
  */
393
443
  refetch(): void;
394
444
  };
@@ -400,11 +450,20 @@ export declare function useConnectApps(): {
400
450
  isLoading: boolean;
401
451
  error: string | null;
402
452
  /**
403
- * Forces a fresh read - the provider's cache clears the mutated path's
404
- * entry on a successful `mutateResource` call, but nothing re-triggers
405
- * this hook's own effect on its own (none of its dependencies change from
406
- * a cache write nobody here observes). Call this right after a
407
- * `mutateResource` you made against the same `path` completes.
453
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
454
+ * read.
455
+ *
456
+ * It used only to re-trigger the effect, on the assumption that the caller
457
+ * had just evicted the entry itself through `mutateResource`. That made it
458
+ * useless for the other reason to refetch: **something changed somewhere
459
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
460
+ * promises with no expiry, so a `refetch()` without the eviction re-read
461
+ * the same settled promise and reported the same stale answer forever -
462
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
463
+ * whose workspace had subscribed in a different one.
464
+ *
465
+ * Still safe after a `mutateResource` against the same path: evicting an
466
+ * entry that is already gone is a no-op.
408
467
  */
409
468
  refetch(): void;
410
469
  };
@@ -423,11 +482,20 @@ export declare function useConnectWorkspaces(): {
423
482
  isLoading: boolean;
424
483
  error: string | null;
425
484
  /**
426
- * Forces a fresh read - the provider's cache clears the mutated path's
427
- * entry on a successful `mutateResource` call, but nothing re-triggers
428
- * this hook's own effect on its own (none of its dependencies change from
429
- * a cache write nobody here observes). Call this right after a
430
- * `mutateResource` you made against the same `path` completes.
485
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
486
+ * read.
487
+ *
488
+ * It used only to re-trigger the effect, on the assumption that the caller
489
+ * had just evicted the entry itself through `mutateResource`. That made it
490
+ * useless for the other reason to refetch: **something changed somewhere
491
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
492
+ * promises with no expiry, so a `refetch()` without the eviction re-read
493
+ * the same settled promise and reported the same stale answer forever -
494
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
495
+ * whose workspace had subscribed in a different one.
496
+ *
497
+ * Still safe after a `mutateResource` against the same path: evicting an
498
+ * entry that is already gone is a no-op.
431
499
  */
432
500
  refetch(): void;
433
501
  };
@@ -442,11 +510,20 @@ export declare function useConnectWorkspaceMembers(): {
442
510
  isLoading: boolean;
443
511
  error: string | null;
444
512
  /**
445
- * Forces a fresh read - the provider's cache clears the mutated path's
446
- * entry on a successful `mutateResource` call, but nothing re-triggers
447
- * this hook's own effect on its own (none of its dependencies change from
448
- * a cache write nobody here observes). Call this right after a
449
- * `mutateResource` you made against the same `path` completes.
513
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
514
+ * read.
515
+ *
516
+ * It used only to re-trigger the effect, on the assumption that the caller
517
+ * had just evicted the entry itself through `mutateResource`. That made it
518
+ * useless for the other reason to refetch: **something changed somewhere
519
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
520
+ * promises with no expiry, so a `refetch()` without the eviction re-read
521
+ * the same settled promise and reported the same stale answer forever -
522
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
523
+ * whose workspace had subscribed in a different one.
524
+ *
525
+ * Still safe after a `mutateResource` against the same path: evicting an
526
+ * entry that is already gone is a no-op.
450
527
  */
451
528
  refetch(): void;
452
529
  };
@@ -456,11 +533,20 @@ export declare function useConnectIntegrations(): {
456
533
  isLoading: boolean;
457
534
  error: string | null;
458
535
  /**
459
- * Forces a fresh read - the provider's cache clears the mutated path's
460
- * entry on a successful `mutateResource` call, but nothing re-triggers
461
- * this hook's own effect on its own (none of its dependencies change from
462
- * a cache write nobody here observes). Call this right after a
463
- * `mutateResource` you made against the same `path` completes.
536
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
537
+ * read.
538
+ *
539
+ * It used only to re-trigger the effect, on the assumption that the caller
540
+ * had just evicted the entry itself through `mutateResource`. That made it
541
+ * useless for the other reason to refetch: **something changed somewhere
542
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
543
+ * promises with no expiry, so a `refetch()` without the eviction re-read
544
+ * the same settled promise and reported the same stale answer forever -
545
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
546
+ * whose workspace had subscribed in a different one.
547
+ *
548
+ * Still safe after a `mutateResource` against the same path: evicting an
549
+ * entry that is already gone is a no-op.
464
550
  */
465
551
  refetch(): void;
466
552
  };
@@ -476,11 +562,20 @@ export declare function useConnectSessions(): {
476
562
  isLoading: boolean;
477
563
  error: string | null;
478
564
  /**
479
- * Forces a fresh read - the provider's cache clears the mutated path's
480
- * entry on a successful `mutateResource` call, but nothing re-triggers
481
- * this hook's own effect on its own (none of its dependencies change from
482
- * a cache write nobody here observes). Call this right after a
483
- * `mutateResource` you made against the same `path` completes.
565
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
566
+ * read.
567
+ *
568
+ * It used only to re-trigger the effect, on the assumption that the caller
569
+ * had just evicted the entry itself through `mutateResource`. That made it
570
+ * useless for the other reason to refetch: **something changed somewhere
571
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
572
+ * promises with no expiry, so a `refetch()` without the eviction re-read
573
+ * the same settled promise and reported the same stale answer forever -
574
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
575
+ * whose workspace had subscribed in a different one.
576
+ *
577
+ * Still safe after a `mutateResource` against the same path: evicting an
578
+ * entry that is already gone is a no-op.
484
579
  */
485
580
  refetch(): void;
486
581
  };
@@ -512,11 +607,20 @@ export declare function useConnectResource<T>(path: string, options?: Readonly<U
512
607
  isLoading: boolean;
513
608
  error: string | null;
514
609
  /**
515
- * Forces a fresh read - the provider's cache clears the mutated path's
516
- * entry on a successful `mutateResource` call, but nothing re-triggers
517
- * this hook's own effect on its own (none of its dependencies change from
518
- * a cache write nobody here observes). Call this right after a
519
- * `mutateResource` you made against the same `path` completes.
610
+ * Forces a fresh read - evicts this path's cache entry and re-runs the
611
+ * read.
612
+ *
613
+ * It used only to re-trigger the effect, on the assumption that the caller
614
+ * had just evicted the entry itself through `mutateResource`. That made it
615
+ * useless for the other reason to refetch: **something changed somewhere
616
+ * this tab cannot see.** The provider's cache is a `Map` of resolved
617
+ * promises with no expiry, so a `refetch()` without the eviction re-read
618
+ * the same settled promise and reported the same stale answer forever -
619
+ * which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
620
+ * whose workspace had subscribed in a different one.
621
+ *
622
+ * Still safe after a `mutateResource` against the same path: evicting an
623
+ * entry that is already gone is a no-op.
520
624
  */
521
625
  refetch(): void;
522
626
  };