@nebulr-group/bridge-svelte 0.8.2 → 0.9.0-beta.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/dist/client/BridgeBootstrap.d.ts +41 -0
- package/dist/client/BridgeBootstrap.js +28 -2
- package/dist/client/BridgeBootstrap.svelte +17 -1
- package/dist/client/BridgeBootstrap.svelte.d.ts +2 -0
- package/dist/client/resolve-config.d.ts +43 -0
- package/dist/client/resolve-config.js +112 -0
- package/dist/core/bridge-runtime.js +92 -1
- package/package.json +1 -1
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
import type { RouteGuardConfig } from '../auth/route-guard.js';
|
|
2
2
|
import { waitForBridge as _waitForBridge } from '../core/bridge-instance.js';
|
|
3
3
|
import type { BridgeConfig } from '../shared/types/config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for `bridgeBootstrap()`: any `BridgeConfig` field plus the route
|
|
6
|
+
* rules. Every field is optional.
|
|
7
|
+
*
|
|
8
|
+
* Precedence: an option you pass explicitly wins over the environment
|
|
9
|
+
* (`VITE_BRIDGE_APP_ID`, `VITE_BRIDGE_API_BASE_URL`, `VITE_BRIDGE_HOSTED_URL`,
|
|
10
|
+
* `VITE_BRIDGE_DEBUG`), and the environment wins over the built-in default.
|
|
11
|
+
*/
|
|
12
|
+
export type BridgeBootstrapOptions = Partial<BridgeConfig> & Partial<RouteGuardConfig>;
|
|
13
|
+
/** What the `load` returned by `bridgeBootstrap()` hands to your layout. */
|
|
14
|
+
export interface BridgeBootstrapData {
|
|
15
|
+
/** The effective config, after options, environment and defaults. */
|
|
16
|
+
config: BridgeConfig;
|
|
17
|
+
/** The route rules Bridge is enforcing. */
|
|
18
|
+
routeConfig: RouteGuardConfig;
|
|
19
|
+
}
|
|
20
|
+
/** The SvelteKit `load` function `bridgeBootstrap()` returns. */
|
|
21
|
+
export type BridgeBootstrapLoad = (event: {
|
|
22
|
+
url: URL;
|
|
23
|
+
fetch: typeof globalThis.fetch;
|
|
24
|
+
}) => Promise<BridgeBootstrapData>;
|
|
25
|
+
/**
|
|
26
|
+
* Start Bridge from your root layout. Returns the layout's `load` function.
|
|
27
|
+
*
|
|
28
|
+
* The app id and addresses come from `VITE_BRIDGE_APP_ID` (and
|
|
29
|
+
* `VITE_BRIDGE_API_BASE_URL` for a stage or local app); anything passed here
|
|
30
|
+
* wins over the environment. With no app id anywhere it throws rather than
|
|
31
|
+
* guessing.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // src/routes/+layout.ts
|
|
35
|
+
* import { bridgeBootstrap } from '@nebulr-group/bridge-svelte';
|
|
36
|
+
* export const ssr = false;
|
|
37
|
+
* export const load = bridgeBootstrap({ rules: [{ match: '/', public: true }] });
|
|
38
|
+
*/
|
|
39
|
+
export declare function bridgeBootstrap(options?: BridgeBootstrapOptions): BridgeBootstrapLoad;
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated Use `export const load = bridgeBootstrap({ rules })` — it reads
|
|
42
|
+
* the app id and addresses from the `VITE_BRIDGE_*` variables. This positional
|
|
43
|
+
* form keeps working unchanged and reads no environment.
|
|
44
|
+
*/
|
|
4
45
|
export declare function bridgeBootstrap(url: URL, config: BridgeConfig | string, routeConfig?: RouteGuardConfig, kitFetch?: typeof globalThis.fetch): Promise<{
|
|
5
46
|
flagsReady: Promise<void>;
|
|
6
47
|
}>;
|
|
@@ -8,6 +8,7 @@ import { installBridgeAuthFetch } from '../core/bridge-runtime.js';
|
|
|
8
8
|
import { useBridge, sanitizeReturnTo, stashReturnTo, takeReturnTo, withReturnTo, } from '@nebulr-group/bridge-auth-core';
|
|
9
9
|
import { logger } from '../shared/logger.js';
|
|
10
10
|
import { bridgeConfig, getConfig, getRouteGuardConfig } from './stores/config.store.js';
|
|
11
|
+
import { resolveBridgeConfig } from './resolve-config.js';
|
|
11
12
|
// TBP-653 — `bridgeBootstrap` used to short-circuit on every call after the
|
|
12
13
|
// first completed one, and the route guard lived below that return. SvelteKit
|
|
13
14
|
// re-runs the root layout load for every navigation (it reads `url`), so the
|
|
@@ -29,7 +30,32 @@ const _configuredPromise = new Promise((resolve) => {
|
|
|
29
30
|
// Child loads can start before the root layout load has run; this only has to
|
|
30
31
|
// cover that ordering, not a missing bootstrap.
|
|
31
32
|
const CONFIGURE_TIMEOUT_MS = 10_000;
|
|
32
|
-
export
|
|
33
|
+
export function bridgeBootstrap(urlOrOptions, config, routeConfig, kitFetch) {
|
|
34
|
+
if (urlOrOptions instanceof URL) {
|
|
35
|
+
if (config === undefined) {
|
|
36
|
+
throw new Error('[bridge] bridgeBootstrap(url, config) was called without a config.');
|
|
37
|
+
}
|
|
38
|
+
return runBootstrap(urlOrOptions, config, routeConfig, kitFetch);
|
|
39
|
+
}
|
|
40
|
+
return createBootstrapLoad(urlOrOptions ?? {});
|
|
41
|
+
}
|
|
42
|
+
function createBootstrapLoad(options) {
|
|
43
|
+
const { rules, defaultAccess, returnTo, ...configOptions } = options;
|
|
44
|
+
const routeConfig = {
|
|
45
|
+
rules: rules ?? [],
|
|
46
|
+
defaultAccess: defaultAccess ?? 'protected',
|
|
47
|
+
...(returnTo ? { returnTo } : {}),
|
|
48
|
+
};
|
|
49
|
+
// Resolved on the first call, not at import: a missing app id must surface
|
|
50
|
+
// as a load error the developer sees, and the environment is only final then.
|
|
51
|
+
let resolved = null;
|
|
52
|
+
return async ({ url, fetch }) => {
|
|
53
|
+
resolved ??= resolveBridgeConfig(configOptions);
|
|
54
|
+
await runBootstrap(url, resolved, routeConfig, fetch);
|
|
55
|
+
return { config: getConfig(), routeConfig };
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function runBootstrap(url, config, routeConfig = { rules: [], defaultAccess: 'protected' }, kitFetch) {
|
|
33
59
|
// Until one call has completed, a call may be the one that lands on a
|
|
34
60
|
// callback URL or needs the no-flash paywall redirect. Afterwards those are
|
|
35
61
|
// owned by <BridgeBootstrap> (reactive paywall) — same split as before.
|
|
@@ -103,7 +129,7 @@ async function waitForConfigured() {
|
|
|
103
129
|
let timer;
|
|
104
130
|
const timeout = new Promise((_, reject) => {
|
|
105
131
|
timer = setTimeout(() => reject(new Error('[bridge] assertAuthorized() ran but bridgeBootstrap() never configured the SDK. ' +
|
|
106
|
-
'
|
|
132
|
+
'Add `export const load = bridgeBootstrap({ rules })` to your root +layout.ts.')), CONFIGURE_TIMEOUT_MS);
|
|
107
133
|
});
|
|
108
134
|
try {
|
|
109
135
|
await Promise.race([_configuredPromise, timeout]);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { beforeNavigate, goto } from '$app/navigation';
|
|
3
3
|
import { page } from '$app/stores';
|
|
4
|
-
import { onMount, onDestroy } from 'svelte';
|
|
4
|
+
import { onMount, onDestroy, type Snippet } from 'svelte';
|
|
5
5
|
import { createRouteGuard, routeRulesReferenceFlag } from '../auth/route-guard.js';
|
|
6
6
|
import { stashReturnTo, withReturnTo } from '@nebulr-group/bridge-auth-core';
|
|
7
7
|
import {
|
|
8
8
|
getBridgeAuth,
|
|
9
|
+
bridgeReadyStore,
|
|
9
10
|
isAuthenticated,
|
|
10
11
|
subscriptionStore,
|
|
11
12
|
loadSubscription,
|
|
@@ -37,14 +38,24 @@
|
|
|
37
38
|
// Props: optional `runtime` overrides for advanced/debug use (websocketFactory,
|
|
38
39
|
// reconnect overrides, etc.); `onBootstrapComplete` callback fires after the
|
|
39
40
|
// runtime + any auto-detected capabilities (flags) have attached.
|
|
41
|
+
//
|
|
42
|
+
// TBP-695 — the shell owns readiness. Wrap the app in <BridgeBootstrap> and
|
|
43
|
+
// `children` render only once Bridge is ready: the root `load`
|
|
44
|
+
// (bridgeBootstrap) has finished AND the runtime + capabilities attached
|
|
45
|
+
// below. The developer writes no ready flag. Self-closing use (no children)
|
|
46
|
+
// still works for apps that gate on `onBootstrapComplete` themselves.
|
|
40
47
|
let {
|
|
41
48
|
runtime,
|
|
42
49
|
onBootstrapComplete,
|
|
50
|
+
children,
|
|
43
51
|
}: {
|
|
44
52
|
runtime?: StartBridgeRuntimeOptions;
|
|
45
53
|
onBootstrapComplete?: () => void;
|
|
54
|
+
children?: Snippet;
|
|
46
55
|
} = $props();
|
|
47
56
|
|
|
57
|
+
let runtimeAttached = $state(false);
|
|
58
|
+
|
|
48
59
|
// Phase 4 (TBP-288/320) — expose the unified bridge surface via Svelte
|
|
49
60
|
// context so descendants can call `useBridge()`.
|
|
50
61
|
setBridgeContext(bridgeSurface);
|
|
@@ -210,6 +221,7 @@
|
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
// Auth-core manages auto-refresh internally — no startAutoRefresh() needed
|
|
224
|
+
runtimeAttached = true;
|
|
213
225
|
if (onBootstrapComplete) onBootstrapComplete();
|
|
214
226
|
});
|
|
215
227
|
|
|
@@ -238,3 +250,7 @@
|
|
|
238
250
|
</script>
|
|
239
251
|
|
|
240
252
|
<RealtimeDevBadge enabled={devBadgeEnabled} />
|
|
253
|
+
|
|
254
|
+
{#if runtimeAttached && $bridgeReadyStore}
|
|
255
|
+
{@render children?.()}
|
|
256
|
+
{/if}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
1
2
|
import { type StartBridgeRuntimeOptions } from '../core/bridge-runtime.js';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
runtime?: StartBridgeRuntimeOptions;
|
|
4
5
|
onBootstrapComplete?: () => void;
|
|
6
|
+
children?: Snippet;
|
|
5
7
|
};
|
|
6
8
|
declare const BridgeBootstrap: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
9
|
type BridgeBootstrap = ReturnType<typeof BridgeBootstrap>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BridgeConfig } from '../shared/types/config.js';
|
|
2
|
+
/** Where Bridge's production API lives — the default when no address is set. */
|
|
3
|
+
export declare const PRODUCTION_API_BASE_URL = "https://api.thebridge.dev";
|
|
4
|
+
/** The standard Vite variables Bridge reads, already mapped to config fields. */
|
|
5
|
+
export interface BridgeEnv {
|
|
6
|
+
appId?: string;
|
|
7
|
+
apiBaseUrl?: string;
|
|
8
|
+
hostedUrl?: string;
|
|
9
|
+
debug?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the `VITE_BRIDGE_*` variables from the consuming app's build.
|
|
13
|
+
*
|
|
14
|
+
* Every access is a literal `import.meta.env.VITE_…` property read on purpose:
|
|
15
|
+
* that is the form Vite statically replaces, including in a library consumed
|
|
16
|
+
* from node_modules. A dynamic key (`env[name]`) would not be replaced.
|
|
17
|
+
* The try/catch covers a non-Vite bundler, where `import.meta.env` is undefined.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readBridgeEnv(): BridgeEnv;
|
|
20
|
+
/**
|
|
21
|
+
* The hosted pages for an API address on Bridge's own domains: `api` becomes
|
|
22
|
+
* `auth`, so `api-stage.thebridge.dev` pairs with `auth-stage.thebridge.dev`.
|
|
23
|
+
*
|
|
24
|
+
* Without this, a stage app that sets only its API address (the documented
|
|
25
|
+
* shape) still sent sign-in to production's hosted pages, where its app id does
|
|
26
|
+
* not exist — the same wrong-environment failure as the API address, one hop
|
|
27
|
+
* later. Any other host (localhost, self-hosted) cannot be derived.
|
|
28
|
+
*/
|
|
29
|
+
export declare function hostedUrlFor(apiBaseUrl: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Build the effective config: an option passed explicitly wins over the
|
|
32
|
+
* environment, and the environment wins over the built-in default.
|
|
33
|
+
*
|
|
34
|
+
* The hosted-pages address follows the API address on Bridge's own domains
|
|
35
|
+
* (see `hostedUrlFor`), so one variable is enough for stage.
|
|
36
|
+
*
|
|
37
|
+
* Refuses to guess: with no app id anywhere it throws, naming the variable to
|
|
38
|
+
* set. An app id with no API address runs against production — that is the
|
|
39
|
+
* documented shape of a production app (`VITE_BRIDGE_APP_ID` alone) — and in a
|
|
40
|
+
* development build it says so once, naming `VITE_BRIDGE_API_BASE_URL`, because
|
|
41
|
+
* a stage or local id against production is the mistake this exists to catch.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveBridgeConfig(options?: Partial<BridgeConfig>, env?: BridgeEnv, dev?: boolean): BridgeConfig;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// TBP-695 — Bridge starts from one line with no arguments.
|
|
2
|
+
//
|
|
3
|
+
// The SDK used to read no environment at all: every app re-typed the same four
|
|
4
|
+
// `import.meta.env.VITE_BRIDGE_*` lines into its own +layout.ts, and the one it
|
|
5
|
+
// most often left out was the API address. A stage or local app id without it
|
|
6
|
+
// silently talked to PRODUCTION, where that app does not exist. Reading the
|
|
7
|
+
// standard variables here removes the boilerplate AND the trap; the no-guess
|
|
8
|
+
// rule below is what keeps the second half true.
|
|
9
|
+
import { logger } from '../shared/logger.js';
|
|
10
|
+
/** Where Bridge's production API lives — the default when no address is set. */
|
|
11
|
+
export const PRODUCTION_API_BASE_URL = 'https://api.thebridge.dev';
|
|
12
|
+
/**
|
|
13
|
+
* Read the `VITE_BRIDGE_*` variables from the consuming app's build.
|
|
14
|
+
*
|
|
15
|
+
* Every access is a literal `import.meta.env.VITE_…` property read on purpose:
|
|
16
|
+
* that is the form Vite statically replaces, including in a library consumed
|
|
17
|
+
* from node_modules. A dynamic key (`env[name]`) would not be replaced.
|
|
18
|
+
* The try/catch covers a non-Vite bundler, where `import.meta.env` is undefined.
|
|
19
|
+
*/
|
|
20
|
+
export function readBridgeEnv() {
|
|
21
|
+
try {
|
|
22
|
+
return {
|
|
23
|
+
appId: import.meta.env.VITE_BRIDGE_APP_ID,
|
|
24
|
+
apiBaseUrl: import.meta.env.VITE_BRIDGE_API_BASE_URL,
|
|
25
|
+
hostedUrl: import.meta.env.VITE_BRIDGE_HOSTED_URL,
|
|
26
|
+
debug: import.meta.env.VITE_BRIDGE_DEBUG,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The hosted pages for an API address on Bridge's own domains: `api` becomes
|
|
35
|
+
* `auth`, so `api-stage.thebridge.dev` pairs with `auth-stage.thebridge.dev`.
|
|
36
|
+
*
|
|
37
|
+
* Without this, a stage app that sets only its API address (the documented
|
|
38
|
+
* shape) still sent sign-in to production's hosted pages, where its app id does
|
|
39
|
+
* not exist — the same wrong-environment failure as the API address, one hop
|
|
40
|
+
* later. Any other host (localhost, self-hosted) cannot be derived.
|
|
41
|
+
*/
|
|
42
|
+
export function hostedUrlFor(apiBaseUrl) {
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(apiBaseUrl);
|
|
45
|
+
const match = /^api(-[a-z0-9-]+)?\.thebridge\.dev$/.exec(url.hostname);
|
|
46
|
+
return match ? `https://auth${match[1] ?? ''}.thebridge.dev` : undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isDevBuild() {
|
|
53
|
+
try {
|
|
54
|
+
return import.meta.env.DEV === true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// An empty variable means "not set". Vite loads `KEY=` as '' and the demo's
|
|
61
|
+
// tracked .env files use exactly that to stop a key falling through to a
|
|
62
|
+
// developer's .env.local — so '' must never count as a value.
|
|
63
|
+
function present(value) {
|
|
64
|
+
if (typeof value !== 'string')
|
|
65
|
+
return undefined;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return trimmed === '' ? undefined : trimmed;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the effective config: an option passed explicitly wins over the
|
|
71
|
+
* environment, and the environment wins over the built-in default.
|
|
72
|
+
*
|
|
73
|
+
* The hosted-pages address follows the API address on Bridge's own domains
|
|
74
|
+
* (see `hostedUrlFor`), so one variable is enough for stage.
|
|
75
|
+
*
|
|
76
|
+
* Refuses to guess: with no app id anywhere it throws, naming the variable to
|
|
77
|
+
* set. An app id with no API address runs against production — that is the
|
|
78
|
+
* documented shape of a production app (`VITE_BRIDGE_APP_ID` alone) — and in a
|
|
79
|
+
* development build it says so once, naming `VITE_BRIDGE_API_BASE_URL`, because
|
|
80
|
+
* a stage or local id against production is the mistake this exists to catch.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveBridgeConfig(options = {}, env = readBridgeEnv(), dev = isDevBuild()) {
|
|
83
|
+
const appId = present(options.appId) ?? present(env.appId);
|
|
84
|
+
if (!appId) {
|
|
85
|
+
throw new Error('[bridge] No Bridge app id was found. Set VITE_BRIDGE_APP_ID in your .env ' +
|
|
86
|
+
'(plus VITE_BRIDGE_API_BASE_URL for a stage or local app), ' +
|
|
87
|
+
'or pass { appId } to bridgeBootstrap().');
|
|
88
|
+
}
|
|
89
|
+
const apiBaseUrl = present(options.apiBaseUrl) ?? present(env.apiBaseUrl);
|
|
90
|
+
const hostedUrl = present(options.hostedUrl) ?? present(env.hostedUrl) ?? (apiBaseUrl ? hostedUrlFor(apiBaseUrl) : undefined);
|
|
91
|
+
const debug = options.debug ?? (present(env.debug) === undefined ? undefined : env.debug === 'true');
|
|
92
|
+
if (!apiBaseUrl && dev) {
|
|
93
|
+
logger.warn(`[bridge] VITE_BRIDGE_API_BASE_URL is not set, so app ${appId} is using production ` +
|
|
94
|
+
`(${PRODUCTION_API_BASE_URL}). Set it if this is a stage or local app.`);
|
|
95
|
+
}
|
|
96
|
+
if (apiBaseUrl && !hostedUrl && dev) {
|
|
97
|
+
logger.warn(`[bridge] VITE_BRIDGE_HOSTED_URL is not set and cannot be derived from ${apiBaseUrl}, ` +
|
|
98
|
+
`so sign-in pages will open on production. Set it to this environment's hosted pages.`);
|
|
99
|
+
}
|
|
100
|
+
const resolved = { ...options, appId };
|
|
101
|
+
if (apiBaseUrl)
|
|
102
|
+
resolved.apiBaseUrl = apiBaseUrl;
|
|
103
|
+
else
|
|
104
|
+
delete resolved.apiBaseUrl;
|
|
105
|
+
if (hostedUrl)
|
|
106
|
+
resolved.hostedUrl = hostedUrl;
|
|
107
|
+
else
|
|
108
|
+
delete resolved.hostedUrl;
|
|
109
|
+
if (debug !== undefined)
|
|
110
|
+
resolved.debug = debug;
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
@@ -263,6 +263,91 @@ export function startBridgeRuntime(options = {}) {
|
|
|
263
263
|
// Best-effort: the next reconnect or push repairs it.
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
|
+
// TBP-686 — the same repair, for the one live payload the session snapshot
|
|
267
|
+
// does not carry: `quota.updated`.
|
|
268
|
+
//
|
|
269
|
+
// `GET /session/init` has no quota slice (nothing in bridge-api's
|
|
270
|
+
// session-snapshot types mentions it), and auth-core's QuotaStore fills
|
|
271
|
+
// itself from exactly two places: a one-shot lazy `GET /usage/quota/:metric`
|
|
272
|
+
// on the FIRST read of a metric, and live `quota.updated` pushes. It never
|
|
273
|
+
// re-reads — `ensureHydrated()` returns the cached snapshot on every
|
|
274
|
+
// subsequent call. So a push lost across a socket swap leaves `used` frozen
|
|
275
|
+
// at whatever it was when the metric was first read, for the rest of the
|
|
276
|
+
// session, with no error anywhere: the ingest returns 201, the server
|
|
277
|
+
// computes correctly and AppSync accepts the publish.
|
|
278
|
+
//
|
|
279
|
+
// That is what `metered-display` and `metered-plan-switch` hit on stage: a
|
|
280
|
+
// token refresh reauthorized mid-test, and the server published `used`
|
|
281
|
+
// 5-160ms before the replacement subscription was acknowledged.
|
|
282
|
+
//
|
|
283
|
+
// The ordering guarantee is the one stated above for the session snapshot,
|
|
284
|
+
// and it is what makes this deterministic rather than lucky: setOnOpen fires
|
|
285
|
+
// from markOpen(), which AppSync's `subscribe_success` drives — so anything
|
|
286
|
+
// published before this point is already durable and the REST read sees it,
|
|
287
|
+
// and anything published after arrives on the live socket.
|
|
288
|
+
//
|
|
289
|
+
// Scope: only metrics the store has already hydrated. A page that never
|
|
290
|
+
// asked about a metric has nothing stale to repair, so this costs one GET
|
|
291
|
+
// per watched metric per open and nothing at all for apps that do not use
|
|
292
|
+
// quotas.
|
|
293
|
+
const catchUpQuotaSnapshots = async () => {
|
|
294
|
+
if (_realtime !== rt)
|
|
295
|
+
return;
|
|
296
|
+
const token = _currentAuthToken;
|
|
297
|
+
const doFetch = _originalFetch ?? globalThis.fetch;
|
|
298
|
+
if (!token || typeof doFetch !== 'function')
|
|
299
|
+
return;
|
|
300
|
+
// One try over BOTH the lookup and the read: `requestCatchUp` fires this
|
|
301
|
+
// without a `.catch()`, so anything thrown out here becomes an unhandled
|
|
302
|
+
// rejection in the host app rather than a swallowed best-effort repair.
|
|
303
|
+
let quotas;
|
|
304
|
+
let metrics = [];
|
|
305
|
+
try {
|
|
306
|
+
quotas = useBridge().quotas;
|
|
307
|
+
metrics = [...quotas.getAll().keys()];
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return; // useBridge() not constructed, or no quota surface — nothing to repair.
|
|
311
|
+
}
|
|
312
|
+
if (metrics.length === 0)
|
|
313
|
+
return;
|
|
314
|
+
let appId = config.appId;
|
|
315
|
+
try {
|
|
316
|
+
appId = getBridgeAuth().getApiContext().appId ?? appId;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// BridgeAuth not constructed — config.appId it is.
|
|
320
|
+
}
|
|
321
|
+
const base = (config.apiBaseUrl ?? 'https://api.thebridge.dev').replace(/\/+$/, '');
|
|
322
|
+
await Promise.all(metrics.map(async (metric) => {
|
|
323
|
+
// The value this repair is replacing. If a live push lands while the
|
|
324
|
+
// GET is in flight, the push is NEWER than the answer coming back and
|
|
325
|
+
// must win — otherwise the repair itself would reintroduce a stale
|
|
326
|
+
// `used`, which is the very bug it exists to fix.
|
|
327
|
+
const before = quotas.getAll().get(metric);
|
|
328
|
+
try {
|
|
329
|
+
const res = await doFetch(`${base}/usage/quota/${encodeURIComponent(metric)}`, {
|
|
330
|
+
method: 'GET',
|
|
331
|
+
headers: { Authorization: `Bearer ${token}`, 'x-app-id': appId ?? '' },
|
|
332
|
+
});
|
|
333
|
+
if (!res.ok)
|
|
334
|
+
return;
|
|
335
|
+
const data = (await res.json());
|
|
336
|
+
// Stopped, or the session changed while this was in flight: the
|
|
337
|
+
// answer describes a workspace we no longer have.
|
|
338
|
+
if (_realtime !== rt || _currentAuthToken !== token)
|
|
339
|
+
return;
|
|
340
|
+
if (quotas.getAll().get(metric) !== before)
|
|
341
|
+
return; // a push won the race
|
|
342
|
+
// `null` is a real answer — "no quota configured for this metric" —
|
|
343
|
+
// and applyInitialSnapshot drops the cached entry for it.
|
|
344
|
+
quotas.applyInitialSnapshot(metric, data ?? null);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Best-effort, per metric: one failure must not skip the others.
|
|
348
|
+
}
|
|
349
|
+
}));
|
|
350
|
+
};
|
|
266
351
|
const requestCatchUp = () => {
|
|
267
352
|
if (!_currentAuthToken)
|
|
268
353
|
return; // signed out: no tenant or user scope to fetch
|
|
@@ -271,7 +356,13 @@ export function startBridgeRuntime(options = {}) {
|
|
|
271
356
|
return;
|
|
272
357
|
}
|
|
273
358
|
_catchUpInFlight = true;
|
|
274
|
-
|
|
359
|
+
// Both repairs ride the one in-flight/queued gate, so an open storm still
|
|
360
|
+
// costs one round of requests, and neither can starve the other.
|
|
361
|
+
void Promise.all([catchUpSessionSnapshot(), catchUpQuotaSnapshots()]).catch(() => {
|
|
362
|
+
// Both halves are already best-effort internally; this is the backstop
|
|
363
|
+
// that keeps a repair failure from surfacing as an unhandled rejection
|
|
364
|
+
// in the consuming app. `.finally` below still drains the queue.
|
|
365
|
+
}).finally(() => {
|
|
275
366
|
_catchUpInFlight = false;
|
|
276
367
|
if (_catchUpQueued) {
|
|
277
368
|
_catchUpQueued = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulr-group/bridge-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-beta.0",
|
|
4
4
|
"description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
|
|
5
5
|
"author": "Iman Pouya",
|
|
6
6
|
"license": "MIT",
|