@base44-preview/sdk 0.8.43-pr.259.3f82534 → 0.8.43-pr.260.d28232f
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/actor.d.ts +3 -13
- package/dist/actor.js +3 -4
- package/dist/client.js +28 -13
- package/dist/client.types.d.ts +12 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +0 -1
- package/dist/modules/actors.d.ts +34 -9
- package/dist/modules/actors.js +129 -338
- package/dist/modules/actors.types.d.ts +22 -29
- package/dist/modules/analytics.js +14 -3
- package/dist/modules/auth.js +4 -7
- package/dist/modules/auth.types.d.ts +0 -2
- package/dist/modules/connectors.js +58 -0
- package/dist/modules/connectors.types.d.ts +122 -0
- package/dist/utils/axios-client.js +7 -6
- package/package.json +1 -1
- package/dist/modules/actors.error.d.ts +0 -14
- package/dist/modules/actors.error.js +0 -15
package/dist/actor.d.ts
CHANGED
|
@@ -9,13 +9,6 @@
|
|
|
9
9
|
* Cloudflare Durable Object implementation — this file provides types only.
|
|
10
10
|
*/
|
|
11
11
|
import type { Base44Client } from "./client";
|
|
12
|
-
export type ActorConnectionIdentity = Readonly<{
|
|
13
|
-
type: "authenticated";
|
|
14
|
-
userId: string;
|
|
15
|
-
}> | Readonly<{
|
|
16
|
-
type: "anonymous";
|
|
17
|
-
anonymousId: string;
|
|
18
|
-
}>;
|
|
19
12
|
/**
|
|
20
13
|
* A single client connection. `Send` is the message type this connection accepts
|
|
21
14
|
* via {@link send} — the actor's *outgoing* (server→client) messages.
|
|
@@ -25,8 +18,6 @@ export interface Conn<Send = unknown> {
|
|
|
25
18
|
* receives from `subscribe()`. Identifies a distinct client, so multiple
|
|
26
19
|
* tabs are separate connections. */
|
|
27
20
|
id: string;
|
|
28
|
-
/** Identity verified by the Actor Worker. Legacy Actors may not provide it. */
|
|
29
|
-
identity?: ActorConnectionIdentity;
|
|
30
21
|
send(data: Send): void;
|
|
31
22
|
reject(code: number, reason: string): void;
|
|
32
23
|
}
|
|
@@ -95,10 +86,9 @@ export declare abstract class Actor<Incoming = unknown, Outgoing = unknown> {
|
|
|
95
86
|
protected get storage(): Storage;
|
|
96
87
|
/**
|
|
97
88
|
* Anonymous Base44 client scoped to this actor instance — no user or service
|
|
98
|
-
* auth, so entity access is RLS-gated (same as a logged-out visitor).
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* the editor does not apply here.
|
|
89
|
+
* auth, so entity access is RLS-gated (same as a logged-out visitor). Always
|
|
90
|
+
* operates on production data: an actor runs server-side with no per-connection
|
|
91
|
+
* identity, so a Test DB preview selected in the editor does not apply here.
|
|
102
92
|
* Example: `const rows = await this.client.entities.Score.list();`
|
|
103
93
|
*/
|
|
104
94
|
protected get client(): Base44Client;
|
package/dist/actor.js
CHANGED
|
@@ -74,10 +74,9 @@ export class Actor {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Anonymous Base44 client scoped to this actor instance — no user or service
|
|
77
|
-
* auth, so entity access is RLS-gated (same as a logged-out visitor).
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* the editor does not apply here.
|
|
77
|
+
* auth, so entity access is RLS-gated (same as a logged-out visitor). Always
|
|
78
|
+
* operates on production data: an actor runs server-side with no per-connection
|
|
79
|
+
* identity, so a Test DB preview selected in the editor does not apply here.
|
|
81
80
|
* Example: `const rows = await this.client.entities.Score.list();`
|
|
82
81
|
*/
|
|
83
82
|
get client() {
|
package/dist/client.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createAppLogsModule } from "./modules/app-logs.js";
|
|
|
12
12
|
import { createUsersModule } from "./modules/users.js";
|
|
13
13
|
import { RoomsSocket } from "./utils/socket-utils.js";
|
|
14
14
|
import { createAnalyticsModule } from "./modules/analytics.js";
|
|
15
|
-
import { createActorsModule, resolveActorsHost } from "./modules/actors.js";
|
|
15
|
+
import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
|
|
16
16
|
/**
|
|
17
17
|
* Creates a Base44 client.
|
|
18
18
|
*
|
|
@@ -94,13 +94,6 @@ export function createClient(config) {
|
|
|
94
94
|
interceptResponses: false,
|
|
95
95
|
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
96
96
|
});
|
|
97
|
-
// Actors keeps raw responses for 409 routing and owns error reporting so
|
|
98
|
-
// response-validation failures use the same onError path as request failures.
|
|
99
|
-
const actorConnectionClient = createAxiosClient({
|
|
100
|
-
baseURL: `${serverUrl}/api`,
|
|
101
|
-
headers,
|
|
102
|
-
interceptResponses: false,
|
|
103
|
-
});
|
|
104
97
|
const serviceRoleHeaders = {
|
|
105
98
|
...headers,
|
|
106
99
|
...(token ? { "on-behalf-of": `Bearer ${token}` } : {}),
|
|
@@ -117,6 +110,15 @@ export function createClient(config) {
|
|
|
117
110
|
token: serviceToken,
|
|
118
111
|
interceptResponses: false,
|
|
119
112
|
});
|
|
113
|
+
// Dedicated client for actor connection-token mints: no onError (a legacy
|
|
114
|
+
// actor answers every mint with an expected 409 before the proxy fallback,
|
|
115
|
+
// which must not reach the app's error handler — the actors module forwards
|
|
116
|
+
// genuine failures itself via onMintError) and no constructor token
|
|
117
|
+
// (auth is per-request so a login/logout is picked up on every reconnect).
|
|
118
|
+
const actorsAxiosClient = createAxiosClient({
|
|
119
|
+
baseURL: `${serverUrl}/api`,
|
|
120
|
+
headers,
|
|
121
|
+
});
|
|
120
122
|
const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
|
|
121
123
|
appBaseUrl: normalizedAppBaseUrl,
|
|
122
124
|
serverUrl,
|
|
@@ -134,13 +136,26 @@ export function createClient(config) {
|
|
|
134
136
|
}
|
|
135
137
|
const actorsModule = createActorsModule({
|
|
136
138
|
appId,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// serverUrl is often relative/empty in same-origin apps, while the legacy
|
|
140
|
-
// WebSocket fallback needs an absolute host.
|
|
139
|
+
// serverUrl is often relative/empty (same-origin app); the proxy-fallback
|
|
140
|
+
// URL needs an absolute host, so fall back to the page origin.
|
|
141
141
|
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
|
|
142
142
|
functionsVersion,
|
|
143
|
-
getAuthToken: () =>
|
|
143
|
+
getAuthToken: () => token || getAccessToken(),
|
|
144
|
+
mintConnectionToken: async (actorName, room, connectionId) => {
|
|
145
|
+
const authToken = token || getAccessToken();
|
|
146
|
+
return await actorsAxiosClient.post(`/apps/${appId}/actors/${encodeURIComponent(actorName)}/connection-token`, { room, connection_id: connectionId }, {
|
|
147
|
+
headers: {
|
|
148
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
149
|
+
// The mint endpoint resolves draft vs published from this header;
|
|
150
|
+
// only the functions axios clients send it by default.
|
|
151
|
+
...(functionsVersion
|
|
152
|
+
? { "Base44-Functions-Version": functionsVersion }
|
|
153
|
+
: {}),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
transport: options === null || options === void 0 ? void 0 : options.actorsTransport,
|
|
158
|
+
onMintError: options === null || options === void 0 ? void 0 : options.onError,
|
|
144
159
|
});
|
|
145
160
|
const userModules = {
|
|
146
161
|
entities: createEntitiesModule({
|
package/dist/client.types.d.ts
CHANGED
|
@@ -15,10 +15,20 @@ import type { ActorsModule } from "./modules/actors.types.js";
|
|
|
15
15
|
export interface CreateClientOptions {
|
|
16
16
|
/**
|
|
17
17
|
* Optional error handler that will be called whenever an API error occurs.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Also receives {@link ActorsModule | actors} connection failures. Errors
|
|
20
|
+
* are usually {@linkcode Base44Error} instances — check `error.status`.
|
|
20
21
|
*/
|
|
21
22
|
onError?: (error: Error) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Forces the actors transport. `"auto"` (default) connects directly to the
|
|
25
|
+
* actor and falls back to the platform proxy when the app's actors don't
|
|
26
|
+
* support direct connections; `"proxy"` always uses the platform proxy
|
|
27
|
+
* (ops rollback — no connection-token calls); `"direct"` disables the
|
|
28
|
+
* fallback (validation environments).
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
actorsTransport?: "auto" | "proxy" | "direct";
|
|
22
32
|
}
|
|
23
33
|
/**
|
|
24
34
|
* Configuration for creating a Base44 client.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,9 +12,8 @@ export type { AgentsModule, AgentName, AgentNameRegistry, AgentConversation, Age
|
|
|
12
12
|
export type { AiGatewayModule, AiGatewayConnection, } from "./modules/ai-gateway.types.js";
|
|
13
13
|
export type { AppLogsModule } from "./modules/app-logs.types.js";
|
|
14
14
|
export type { ActorsModule, ActorClient, ActorRef, Connection, ActorSubscription, ActorConnectOptions, ActorNameRegistry, ActorRegistry, } from "./modules/actors.types.js";
|
|
15
|
-
export { ActorConnectionError } from "./modules/actors.error.js";
|
|
16
15
|
export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js";
|
|
17
|
-
export { Actor, type
|
|
18
|
-
export type { ConnectorsModule, UserConnectorsModule, } from "./modules/connectors.types.js";
|
|
16
|
+
export { Actor, type Conn } from "./actor.js";
|
|
17
|
+
export type { ConnectorsModule, UserConnectorsModule, ConnectorApiRequest, ConnectorApiResponse, ConnectorApiResponsePhase, } from "./modules/connectors.types.js";
|
|
19
18
|
export type { CustomIntegrationsModule, CustomIntegrationCallParams, CustomIntegrationCallResponse, } from "./modules/custom-integrations.types.js";
|
|
20
19
|
export type { GetAccessTokenOptions, SaveAccessTokenOptions, RemoveAccessTokenOptions, GetLoginUrlOptions, } from "./utils/auth-utils.types.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,4 @@ import { Base44Error } from "./utils/axios-client.js";
|
|
|
3
3
|
import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, } from "./utils/auth-utils.js";
|
|
4
4
|
export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
|
|
5
5
|
export * from "./types.js";
|
|
6
|
-
export { ActorConnectionError } from "./modules/actors.error.js";
|
|
7
6
|
export { Actor } from "./actor.js";
|
package/dist/modules/actors.d.ts
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
import { type AxiosInstance } from "axios";
|
|
2
|
-
import { ActorConnectionError } from "./actors.error.js";
|
|
3
1
|
import type { ActorRef } from "./actors.types.js";
|
|
2
|
+
/** Credentials minted by the platform for one direct actor connection. */
|
|
3
|
+
export interface ActorConnectionCredentials {
|
|
4
|
+
/** Direct actor endpoint, already carrying `?_pk=<connectionId>`. */
|
|
5
|
+
websocket_url: string;
|
|
6
|
+
/** Short-lived JWT bound to (app, actor, room, connectionId); appended to
|
|
7
|
+
* the URL as `token=` since browsers can't set WebSocket headers. */
|
|
8
|
+
token: string;
|
|
9
|
+
}
|
|
4
10
|
interface ActorsConfig {
|
|
5
11
|
appId: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
/** Current user access token, if authenticated. Rides the WS query on the
|
|
13
|
+
* proxy-fallback path so the platform proxy can authenticate the connection;
|
|
14
|
+
* anonymous connects omit it. */
|
|
9
15
|
getAuthToken(): string | null | undefined;
|
|
10
16
|
/** Same semantics as function calls: editors with a non-prod version get the
|
|
11
17
|
* draft actor script; everyone else gets the published one. */
|
|
12
18
|
functionsVersion?: string;
|
|
13
|
-
/** Absolute host
|
|
19
|
+
/** Absolute host for the proxy-fallback URL (scheme is swapped to wss, ws
|
|
20
|
+
* for localhost). Resolved by {@link resolveActorsHost}. */
|
|
14
21
|
host: string;
|
|
22
|
+
/** Mints a direct-connect credential for one (actor, room, connection).
|
|
23
|
+
* Called per connection attempt: the token's expiry is checked at upgrade,
|
|
24
|
+
* so every reconnect needs a fresh one. */
|
|
25
|
+
mintConnectionToken(actorName: string, room: string, connectionId: string): Promise<ActorConnectionCredentials>;
|
|
26
|
+
/** @internal Ops escape hatch: "proxy" never mints (legacy path only),
|
|
27
|
+
* "direct" never falls back. Default "auto". */
|
|
28
|
+
transport?: "auto" | "proxy" | "direct";
|
|
29
|
+
/** Called when a mint fails for a reason other than the expected
|
|
30
|
+
* direct→proxy fallback (which recovers by itself). Wired to the client's
|
|
31
|
+
* `options.onError`. */
|
|
32
|
+
onMintError?: (error: Error) => void;
|
|
15
33
|
}
|
|
16
34
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
35
|
+
* The legacy platform-proxy URL, byte-for-byte what PartySocket built before
|
|
36
|
+
* the direct path existed: same scheme swap (including its localhost-needs-a-
|
|
37
|
+
* port quirk), case-preserved party segment, `_pk` first in the query. The
|
|
38
|
+
* `handler` param is load-bearing — the proxy reads it for the actor name.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildProxyActorUrl(rawHost: string, actorName: string, instanceId: string, connectionId: string, appId: string, token: string | null | undefined, functionsVersion?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Absolute host for the proxy-fallback actor URL. A relative/empty `serverUrl`
|
|
43
|
+
* can't be dialed (same-origin apps use a relative `/api`, so `serverUrl` is
|
|
44
|
+
* often `""`), so fall back to the page origin.
|
|
20
45
|
*/
|
|
21
46
|
export declare function resolveActorsHost(serverUrl: string, browserOrigin?: string): string;
|
|
22
47
|
export declare function createActorsModule(config: ActorsConfig): {
|
package/dist/modules/actors.js
CHANGED
|
@@ -1,150 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { getAnalyticsSessionId } from "./analytics.js";
|
|
4
|
-
import { generateUuid } from "../utils/common.js";
|
|
5
|
-
import { ActorConnectionError } from "./actors.error.js";
|
|
6
|
-
// Both mirror the Actor Worker dispatcher's own validation (see
|
|
7
|
-
// workers/base44-dispatcher/src/actor-routing.ts) so a request it would reject
|
|
8
|
-
// fails here instead. Rejection happens on the WebSocket *upgrade*, which
|
|
9
|
-
// reaches the browser as an opaque 1006 with no status to act on, so failing
|
|
10
|
-
// fast client-side is the only way to report the real reason.
|
|
11
|
-
const CONNECTION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
12
|
-
const INSTANCE_ID_RE = /^[\x20-\x2e\x30-\x7e]{1,256}$/;
|
|
13
|
-
const RETRYABLE_BOOTSTRAP_STATUSES = new Set([401, 408, 425, 429]);
|
|
14
|
-
// Heartbeat / half-open detection: PartySocket only reconnects on a close/error
|
|
1
|
+
import { WebSocket as ReconnectingWebSocket } from "partysocket";
|
|
2
|
+
// Heartbeat / half-open detection: the socket only reconnects on a close/error
|
|
15
3
|
// event, so ping periodically and force a reconnect if nothing returns in DEAD_MS.
|
|
16
4
|
const PING_MS = 1000;
|
|
17
5
|
const DEAD_MS = 3000;
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
6
|
+
// Mint responses that mean "direct can't serve this connection, the proxy can":
|
|
7
|
+
// 409 = legacy-family actor script, 503 = direct connections not provisioned,
|
|
8
|
+
// 422 = no principal (e.g. anonymous outside a browser) or an id/room only the
|
|
9
|
+
// proxy's looser validation accepts, 405 = a backend that predates the mint
|
|
10
|
+
// endpoint (its actor deploy routes catch the path via `{handler_name:path}`
|
|
11
|
+
// but not the POST method — and the real endpoint never 405s a POST). The
|
|
12
|
+
// proxy serves migrated actors too, so falling back is always safe.
|
|
13
|
+
const PROXY_FALLBACK_STATUSES = new Set([405, 409, 422, 503]);
|
|
14
|
+
// Mint responses no retry can fix (bad request / forbidden / not found): the
|
|
15
|
+
// connection closes instead of re-minting forever; a fresh connect() re-probes.
|
|
16
|
+
// 401 is deliberately absent — the auth token is re-read on every attempt, so a
|
|
17
|
+
// login recovers on the next retry. Disjoint from PROXY_FALLBACK_STATUSES.
|
|
18
|
+
const TERMINAL_MINT_STATUSES = new Set([400, 403, 404]);
|
|
19
|
+
/** The mint's rejection can be anything; a `Base44Error` carries a numeric
|
|
20
|
+
* `.status` (absent for network failures). */
|
|
21
|
+
function mintErrorStatus(err) {
|
|
22
|
+
const status = err && typeof err === "object"
|
|
23
|
+
? err.status
|
|
24
|
+
: undefined;
|
|
25
|
+
return typeof status === "number" ? status : undefined;
|
|
26
|
+
}
|
|
27
|
+
function toError(err) {
|
|
28
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
29
|
+
}
|
|
36
30
|
/**
|
|
37
|
-
* A connection to an actor instance. Only obtainable from
|
|
31
|
+
* A live connection to an actor instance. Only obtainable from
|
|
32
|
+
* {@link ActorRef.connect}, so `subscribe`/`send` are always valid — the socket
|
|
33
|
+
* exists for this object's whole lifetime.
|
|
38
34
|
*/
|
|
39
35
|
class Connection {
|
|
40
|
-
constructor(actorName, instanceId, config, options,
|
|
41
|
-
|
|
42
|
-
this.instanceId = instanceId;
|
|
36
|
+
constructor(actorName, instanceId, config, options, onClose) {
|
|
37
|
+
var _a;
|
|
43
38
|
this.onClose = onClose;
|
|
44
39
|
this.listeners = new Set();
|
|
45
|
-
this.errorListeners = new Set();
|
|
46
40
|
this.heartbeat = null;
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
const
|
|
41
|
+
this.closed = false;
|
|
42
|
+
this.id = (_a = options === null || options === void 0 ? void 0 : options.id) !== null && _a !== void 0 ? _a : crypto.randomUUID();
|
|
43
|
+
// Direct-first with proxy fallback, decided per connection attempt. Once a
|
|
44
|
+
// mint answers with a fallback status the choice is sticky for this
|
|
45
|
+
// socket's lifetime (a fresh connect() after close() probes direct again,
|
|
46
|
+
// picking up actors migrated in the meantime). Any other mint failure
|
|
47
|
+
// rejects, which ReconnectingWebSocket retries with backoff — except the
|
|
48
|
+
// terminal statuses, which close this connection for good.
|
|
49
|
+
let useProxy = config.transport === "proxy";
|
|
50
|
+
const urlProvider = async () => {
|
|
57
51
|
var _a;
|
|
58
|
-
if (this.
|
|
59
|
-
throw
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const status = isAxiosError(error) ? (_a = error.response) === null || _a === void 0 ? void 0 : _a.status : undefined;
|
|
66
|
-
const connectionError = this.createError(error, status);
|
|
67
|
-
bootstrapErrorAwaitingSocketEvent = true;
|
|
68
|
-
if (isTerminalBootstrapError(error)) {
|
|
69
|
-
this.reportError(connectionError, true);
|
|
70
|
-
}
|
|
71
|
-
else if (status === undefined) {
|
|
72
|
-
// The request never reached the server (offline, DNS, aborted), so
|
|
73
|
-
// it consumes nothing and will succeed once connectivity returns.
|
|
74
|
-
// Charging the budget here would kill the connection over a tunnel.
|
|
75
|
-
this.reportError(connectionError);
|
|
52
|
+
if (this.closed)
|
|
53
|
+
throw new Error("Actor connection is closed");
|
|
54
|
+
if (!useProxy) {
|
|
55
|
+
try {
|
|
56
|
+
const { websocket_url, token } = await config.mintConnectionToken(actorName, instanceId, this.id);
|
|
57
|
+
const sep = websocket_url.includes("?") ? "&" : "?";
|
|
58
|
+
return `${websocket_url}${sep}token=${encodeURIComponent(token)}`;
|
|
76
59
|
}
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
catch (err) {
|
|
61
|
+
const status = mintErrorStatus(err);
|
|
62
|
+
const isFallback = config.transport !== "direct" &&
|
|
63
|
+
status !== undefined &&
|
|
64
|
+
PROXY_FALLBACK_STATUSES.has(status);
|
|
65
|
+
if (!isFallback) {
|
|
66
|
+
if (status !== undefined && TERMINAL_MINT_STATUSES.has(status)) {
|
|
67
|
+
// close() before notifying: ws.close() stops the redial the
|
|
68
|
+
// rethrow below would otherwise schedule, and a handler that
|
|
69
|
+
// immediately calls connect() gets a clean new connection.
|
|
70
|
+
this.close();
|
|
71
|
+
}
|
|
72
|
+
// Reported from here because the socket's error event only
|
|
73
|
+
// preserves `err.message`, never `.status`.
|
|
74
|
+
try {
|
|
75
|
+
(_a = config.onMintError) === null || _a === void 0 ? void 0 : _a.call(config, toError(err));
|
|
76
|
+
}
|
|
77
|
+
catch (_b) {
|
|
78
|
+
// an app handler must not break the dial loop or mask `err`
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
useProxy = true;
|
|
79
83
|
}
|
|
80
|
-
throw connectionError;
|
|
81
84
|
}
|
|
82
|
-
|
|
85
|
+
// Rebuilt per attempt so a login/logout is picked up on reconnect.
|
|
86
|
+
return buildProxyActorUrl(config.host, actorName, instanceId, this.id, config.appId, config.getAuthToken(), config.functionsVersion);
|
|
87
|
+
};
|
|
88
|
+
const ws = new ReconnectingWebSocket(urlProvider);
|
|
83
89
|
this.ws = ws;
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/** Watchdog trips with no inbound frame since: gates the one-per-outage
|
|
88
|
-
* silence report and widens the dead window. Reset by any message. */
|
|
89
|
-
let silentReconnects = 0;
|
|
90
|
-
let closeSequence = 0;
|
|
91
|
-
let suppressedCloseSequence = 0;
|
|
92
|
-
const markAlive = () => { lastActivityAt = Date.now(); };
|
|
93
|
-
const markDisconnected = () => { lastActivityAt = null; };
|
|
94
|
-
ws.addEventListener("open", () => {
|
|
95
|
-
socketOpened = true;
|
|
96
|
-
markAlive();
|
|
97
|
-
this.armStability();
|
|
98
|
-
});
|
|
99
|
-
ws.addEventListener("close", (event) => {
|
|
100
|
-
const closedSocketWasOpen = socketOpened;
|
|
101
|
-
socketOpened = false;
|
|
102
|
-
markDisconnected();
|
|
103
|
-
this.clearStability();
|
|
104
|
-
const currentCloseSequence = ++closeSequence;
|
|
105
|
-
// A heartbeat-driven reconnect is not a failed attempt: a legacy Actor
|
|
106
|
-
// without the __pong shim reconnects every DEAD_MS by design, so charging
|
|
107
|
-
// the budget for those would tear down every legacy connection.
|
|
108
|
-
if (this.isClosed || reconnectingForHeartbeat || !closedSocketWasOpen)
|
|
109
|
-
return;
|
|
110
|
-
// PartySocket emits a synthetic close before its error event. Defer so
|
|
111
|
-
// that transport errors are reported once with their real cause.
|
|
112
|
-
queueMicrotask(() => {
|
|
113
|
-
if (this.isClosed || suppressedCloseSequence === currentCloseSequence)
|
|
114
|
-
return;
|
|
115
|
-
const reason = event.reason
|
|
116
|
-
? `WebSocket closed with code ${event.code}: ${event.reason}`
|
|
117
|
-
: `WebSocket closed with code ${event.code}`;
|
|
118
|
-
const connectionError = this.createError(new Error(reason), undefined, event.code, event.reason || undefined);
|
|
119
|
-
if (isTerminalSocketCloseCode(event.code)) {
|
|
120
|
-
this.reportError(connectionError, true);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
this.reportAttemptFailure(connectionError);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
ws.addEventListener("error", (event) => {
|
|
128
|
-
markDisconnected();
|
|
129
|
-
suppressedCloseSequence = closeSequence;
|
|
130
|
-
if (this.isClosed)
|
|
131
|
-
return;
|
|
132
|
-
if (bootstrapErrorAwaitingSocketEvent) {
|
|
133
|
-
bootstrapErrorAwaitingSocketEvent = false;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const cause = event.error instanceof Error
|
|
137
|
-
? event.error
|
|
138
|
-
: new Error(event.message || "Actor WebSocket error");
|
|
139
|
-
// Also the only signal for a refused *upgrade* (bad token, unknown or
|
|
140
|
-
// throwing Actor): the dispatcher answers over HTTP, the socket never
|
|
141
|
-
// opens, and the close listener above skips it — so the budget applied
|
|
142
|
-
// here is what stops a permanently unroutable connection.
|
|
143
|
-
this.reportAttemptFailure(this.createError(cause));
|
|
144
|
-
});
|
|
90
|
+
let lastMsg = Date.now();
|
|
91
|
+
const bumpAlive = () => { lastMsg = Date.now(); };
|
|
92
|
+
ws.addEventListener("open", bumpAlive);
|
|
145
93
|
ws.addEventListener("message", (ev) => {
|
|
146
|
-
|
|
147
|
-
silentReconnects = 0;
|
|
94
|
+
bumpAlive();
|
|
148
95
|
let data;
|
|
149
96
|
try {
|
|
150
97
|
data = JSON.parse(ev.data);
|
|
@@ -159,176 +106,58 @@ class Connection {
|
|
|
159
106
|
listener(data);
|
|
160
107
|
});
|
|
161
108
|
this.heartbeat = setInterval(() => {
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (silentReconnects === 0) {
|
|
170
|
-
this.reportError(this.createError(new Error("Actor WebSocket stopped responding")));
|
|
171
|
-
}
|
|
172
|
-
silentReconnects += 1;
|
|
173
|
-
reconnectingForHeartbeat = true;
|
|
174
|
-
try {
|
|
175
|
-
ws.reconnect(1000, "heartbeat timeout");
|
|
176
|
-
}
|
|
177
|
-
finally {
|
|
178
|
-
reconnectingForHeartbeat = false;
|
|
179
|
-
}
|
|
109
|
+
if (Date.now() - lastMsg > DEAD_MS) {
|
|
110
|
+
bumpAlive(); // avoid a reconnect storm while the new socket comes up
|
|
111
|
+
// Only kick a half-open socket (OPEN but silent). When it isn't open
|
|
112
|
+
// the socket is already redialing with backoff, and reconnect() would
|
|
113
|
+
// reset that backoff into a mint call every DEAD_MS.
|
|
114
|
+
if (ws.readyState === ws.OPEN)
|
|
115
|
+
ws.reconnect();
|
|
180
116
|
return;
|
|
181
117
|
}
|
|
182
118
|
try {
|
|
183
119
|
// The deployed shim echoes __ping → __pong (base44-userapp-bundler
|
|
184
|
-
// shim/actor.ts); without that, an idle room reconnects
|
|
185
|
-
// interval (DEAD_MS up to MAX_DEAD_MS).
|
|
120
|
+
// shim/actor.ts); without that, an idle room reconnects every DEAD_MS.
|
|
186
121
|
ws.send(JSON.stringify({ type: "__ping" }));
|
|
187
122
|
}
|
|
188
123
|
catch (_a) {
|
|
189
|
-
//
|
|
124
|
+
// not open; the watchdog above will reconnect
|
|
190
125
|
}
|
|
191
126
|
}, PING_MS);
|
|
192
|
-
ws.reconnect();
|
|
193
127
|
}
|
|
194
128
|
subscribe(callback) {
|
|
195
|
-
this.assertNotClosed();
|
|
196
129
|
this.listeners.add(callback);
|
|
197
130
|
return {
|
|
198
131
|
unsubscribe: () => { this.listeners.delete(callback); },
|
|
199
132
|
};
|
|
200
133
|
}
|
|
201
134
|
send(data) {
|
|
202
|
-
|
|
135
|
+
// after close() the socket would buffer forever (unbounded enqueue)
|
|
136
|
+
if (this.closed)
|
|
137
|
+
return;
|
|
203
138
|
this.ws.send(JSON.stringify(data));
|
|
204
139
|
}
|
|
205
|
-
get closed() {
|
|
206
|
-
return this.isClosed;
|
|
207
|
-
}
|
|
208
|
-
addErrorListener(listener) {
|
|
209
|
-
if (!listener)
|
|
210
|
-
return { unsubscribe: () => { } };
|
|
211
|
-
this.assertNotClosed();
|
|
212
|
-
this.errorListeners.add(listener);
|
|
213
|
-
return {
|
|
214
|
-
unsubscribe: () => { this.errorListeners.delete(listener); },
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
140
|
close() {
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
assertNotClosed() {
|
|
221
|
-
if (!this.closedError)
|
|
222
|
-
return;
|
|
223
|
-
throw this.createError(this.closedError.cause, this.closedError.status, this.closedError.closeCode, this.closedError.closeReason);
|
|
224
|
-
}
|
|
225
|
-
createError(cause, status, closeCode, closeReason) {
|
|
226
|
-
return new ActorConnectionError(this.actorName, this.instanceId, this.id, cause, status, closeCode, closeReason);
|
|
227
|
-
}
|
|
228
|
-
reportError(error, terminal = false) {
|
|
229
|
-
const handlers = new Set(this.errorListeners);
|
|
230
|
-
if (this.globalOnError)
|
|
231
|
-
handlers.add(this.globalOnError);
|
|
232
|
-
if (terminal)
|
|
233
|
-
this.teardown(error);
|
|
234
|
-
for (const handler of handlers) {
|
|
235
|
-
try {
|
|
236
|
-
handler(error);
|
|
237
|
-
}
|
|
238
|
-
catch (_a) {
|
|
239
|
-
// Error observers must not replace the failure PartySocket receives.
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Report a failed connection attempt, and give up once the budget is spent.
|
|
245
|
-
* This is the only bound on retries for failures a close code cannot classify
|
|
246
|
-
* — see {@link MAX_CONSECUTIVE_FAILURES}.
|
|
247
|
-
*/
|
|
248
|
-
reportAttemptFailure(error) {
|
|
249
|
-
this.clearStability();
|
|
250
|
-
const exhausted = ++this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES;
|
|
251
|
-
this.reportError(error, exhausted);
|
|
252
|
-
}
|
|
253
|
-
armStability() {
|
|
254
|
-
this.clearStability();
|
|
255
|
-
this.stabilityTimer = setTimeout(() => {
|
|
256
|
-
this.stabilityTimer = null;
|
|
257
|
-
this.consecutiveFailures = 0;
|
|
258
|
-
}, STABLE_MS);
|
|
259
|
-
}
|
|
260
|
-
clearStability() {
|
|
261
|
-
if (this.stabilityTimer) {
|
|
262
|
-
clearTimeout(this.stabilityTimer);
|
|
263
|
-
this.stabilityTimer = null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
teardown(error) {
|
|
267
|
-
if (this.isClosed)
|
|
141
|
+
if (this.closed)
|
|
268
142
|
return;
|
|
269
|
-
this.
|
|
270
|
-
this.closedError = error;
|
|
143
|
+
this.closed = true;
|
|
271
144
|
if (this.heartbeat) {
|
|
272
145
|
clearInterval(this.heartbeat);
|
|
273
146
|
this.heartbeat = null;
|
|
274
147
|
}
|
|
275
148
|
this.listeners.clear();
|
|
276
|
-
this.errorListeners.clear();
|
|
277
|
-
this.clearStability();
|
|
278
149
|
this.ws.close();
|
|
279
150
|
this.onClose();
|
|
280
151
|
}
|
|
281
152
|
}
|
|
282
|
-
/**
|
|
283
|
-
* Whether a close code proves a retry can never succeed.
|
|
284
|
-
*
|
|
285
|
-
* Only 1000 and 3000-4999 can reach us as a deliberate server-side choice — the
|
|
286
|
-
* WebSocket API rejects every other code passed to `close()`, so the rest are
|
|
287
|
-
* generated by the transport (1006 on a dropped link or a refused upgrade, 1001
|
|
288
|
-
* on shutdown, 1011 on a server error) and are all worth retrying.
|
|
289
|
-
*
|
|
290
|
-
* Of those two, only 3000-4999 carries app intent: `conn.reject(code, reason)`
|
|
291
|
-
* picks it, and the Actor's decision will not differ on the next attempt. 1000
|
|
292
|
-
* is ambiguous — `reject(1000, ...)` and a graceful worker shutdown are
|
|
293
|
-
* indistinguishable on the wire — so it is left to the attempt budget, which
|
|
294
|
-
* reconnects through a deploy but still gives up on a rejection loop.
|
|
295
|
-
*/
|
|
296
|
-
function isTerminalSocketCloseCode(code) {
|
|
297
|
-
return code >= 3000 && code <= 4999;
|
|
298
|
-
}
|
|
299
|
-
function isTerminalBootstrapError(error) {
|
|
300
|
-
var _a;
|
|
301
|
-
if (!isAxiosError(error))
|
|
302
|
-
return true;
|
|
303
|
-
const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
304
|
-
return status !== undefined
|
|
305
|
-
&& status >= 400
|
|
306
|
-
&& status < 500
|
|
307
|
-
&& !RETRYABLE_BOOTSTRAP_STATUSES.has(status);
|
|
308
|
-
}
|
|
309
153
|
/** Handle for one actor instance: `connect()` opens the socket (idempotent). */
|
|
310
|
-
function makeActorRef(actorName, instanceId, config,
|
|
311
|
-
assertValidInstanceId(instanceId);
|
|
154
|
+
function makeActorRef(actorName, instanceId, config, connections) {
|
|
312
155
|
let conn = null;
|
|
313
156
|
return {
|
|
314
157
|
connect(options) {
|
|
315
|
-
|
|
316
|
-
// rather than as a mismatch against the live connection's (always valid) id.
|
|
317
|
-
if ((options === null || options === void 0 ? void 0 : options.id) !== undefined)
|
|
318
|
-
assertValidConnectionId(options.id);
|
|
319
|
-
if (conn) {
|
|
320
|
-
if ((options === null || options === void 0 ? void 0 : options.id) !== undefined && options.id !== conn.id) {
|
|
321
|
-
throw new Error(`Actor connection is already open with id "${conn.id}"; cannot reuse it with id "${options.id}"`);
|
|
322
|
-
}
|
|
323
|
-
if (options === null || options === void 0 ? void 0 : options.onError) {
|
|
324
|
-
// Registering here would be unremovable: the subscription that
|
|
325
|
-
// addErrorListener returns has nowhere to go, so a handler recreated
|
|
326
|
-
// per render would accumulate for the connection's whole life.
|
|
327
|
-
throw new Error("Actor connection is already open; use connection.addErrorListener() to add an error handler");
|
|
328
|
-
}
|
|
158
|
+
if (conn)
|
|
329
159
|
return conn;
|
|
330
|
-
|
|
331
|
-
const c = new Connection(actorName, instanceId, config, options, getAnonymousId, () => {
|
|
160
|
+
const c = new Connection(actorName, instanceId, config, options, () => {
|
|
332
161
|
connections.delete(c);
|
|
333
162
|
if (conn === c)
|
|
334
163
|
conn = null; // allow a fresh connect() after close
|
|
@@ -340,91 +169,53 @@ function makeActorRef(actorName, instanceId, config, getAnonymousId, connections
|
|
|
340
169
|
};
|
|
341
170
|
}
|
|
342
171
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
172
|
+
* The legacy platform-proxy URL, byte-for-byte what PartySocket built before
|
|
173
|
+
* the direct path existed: same scheme swap (including its localhost-needs-a-
|
|
174
|
+
* port quirk), case-preserved party segment, `_pk` first in the query. The
|
|
175
|
+
* `handler` param is load-bearing — the proxy reads it for the actor name.
|
|
176
|
+
*/
|
|
177
|
+
export function buildProxyActorUrl(rawHost, actorName, instanceId, connectionId, appId, token, functionsVersion) {
|
|
178
|
+
let host = rawHost.replace(/^(http|https|ws|wss):\/\//, "");
|
|
179
|
+
if (host.endsWith("/"))
|
|
180
|
+
host = host.slice(0, -1);
|
|
181
|
+
const insecure = host.startsWith("localhost:") ||
|
|
182
|
+
host.startsWith("127.0.0.1:") ||
|
|
183
|
+
host.startsWith("192.168.") ||
|
|
184
|
+
host.startsWith("10.") ||
|
|
185
|
+
(host.startsWith("172.") &&
|
|
186
|
+
host.split(".")[1] >= "16" &&
|
|
187
|
+
host.split(".")[1] <= "31") ||
|
|
188
|
+
host.startsWith("[::ffff:7f00:1]:");
|
|
189
|
+
const query = new URLSearchParams([
|
|
190
|
+
["_pk", connectionId],
|
|
191
|
+
["app_id", appId],
|
|
192
|
+
["handler", actorName],
|
|
193
|
+
]);
|
|
194
|
+
if (token)
|
|
195
|
+
query.append("token", token);
|
|
196
|
+
if (functionsVersion)
|
|
197
|
+
query.append("fv", functionsVersion);
|
|
198
|
+
return `${insecure ? "ws" : "wss"}://${host}/parties/${actorName}/${instanceId}?${query}`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Absolute host for the proxy-fallback actor URL. A relative/empty `serverUrl`
|
|
202
|
+
* can't be dialed (same-origin apps use a relative `/api`, so `serverUrl` is
|
|
203
|
+
* often `""`), so fall back to the page origin.
|
|
346
204
|
*/
|
|
347
205
|
export function resolveActorsHost(serverUrl, browserOrigin) {
|
|
348
206
|
return serverUrl && !serverUrl.startsWith("/") ? serverUrl : browserOrigin !== null && browserOrigin !== void 0 ? browserOrigin : serverUrl;
|
|
349
207
|
}
|
|
350
|
-
function assertValidConnectionId(id) {
|
|
351
|
-
if (!CONNECTION_ID_RE.test(id)) {
|
|
352
|
-
throw new Error("Actor connection id must be 1-64 letters, numbers, underscores, or hyphens");
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function assertValidInstanceId(instanceId) {
|
|
356
|
-
if (!INSTANCE_ID_RE.test(instanceId)) {
|
|
357
|
-
throw new Error(`Actor instance id "${instanceId}" must be 1-256 printable ASCII characters and cannot contain "/"`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
function resolveConnectionId(id) {
|
|
361
|
-
var _a, _b, _c;
|
|
362
|
-
if (id !== undefined) {
|
|
363
|
-
assertValidConnectionId(id);
|
|
364
|
-
return id;
|
|
365
|
-
}
|
|
366
|
-
return (_c = (_b = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : generateUuid();
|
|
367
|
-
}
|
|
368
|
-
function legacyActorWebSocketUrl(actorName, instanceId, connectionId, authToken, config) {
|
|
369
|
-
const url = new URL(new PartySocket({
|
|
370
|
-
host: config.host,
|
|
371
|
-
party: actorName,
|
|
372
|
-
room: instanceId,
|
|
373
|
-
id: connectionId,
|
|
374
|
-
startClosed: true,
|
|
375
|
-
}).roomUrl);
|
|
376
|
-
url.searchParams.set("_pk", connectionId);
|
|
377
|
-
url.searchParams.set("app_id", config.appId);
|
|
378
|
-
url.searchParams.set("handler", actorName);
|
|
379
|
-
if (authToken)
|
|
380
|
-
url.searchParams.set("token", authToken);
|
|
381
|
-
if (config.functionsVersion)
|
|
382
|
-
url.searchParams.set("fv", config.functionsVersion);
|
|
383
|
-
return url.toString();
|
|
384
|
-
}
|
|
385
|
-
async function resolveActorWebSocketUrl(actorName, instanceId, connectionId, getAnonymousId, config) {
|
|
386
|
-
var _a, _b;
|
|
387
|
-
const authToken = config.getAuthToken();
|
|
388
|
-
const anonymousId = authToken ? null : getAnonymousId();
|
|
389
|
-
const response = await config.connectionClient.post(`/apps/${encodeURIComponent(config.appId)}/actors/${encodeURIComponent(actorName)}/connection-token`, { room: instanceId, connection_id: connectionId }, {
|
|
390
|
-
headers: {
|
|
391
|
-
Authorization: authToken ? `Bearer ${authToken}` : null,
|
|
392
|
-
"X-Base44-Anonymous-Id": anonymousId,
|
|
393
|
-
...(config.functionsVersion
|
|
394
|
-
? { "Base44-Functions-Version": config.functionsVersion }
|
|
395
|
-
: {}),
|
|
396
|
-
},
|
|
397
|
-
validateStatus: (status) => (status >= 200 && status < 300) || status === 409,
|
|
398
|
-
});
|
|
399
|
-
if (response.status === 409) {
|
|
400
|
-
return legacyActorWebSocketUrl(actorName, instanceId, connectionId, authToken, config);
|
|
401
|
-
}
|
|
402
|
-
const websocketUrl = (_a = response.data) === null || _a === void 0 ? void 0 : _a.websocket_url;
|
|
403
|
-
const token = (_b = response.data) === null || _b === void 0 ? void 0 : _b.token;
|
|
404
|
-
if (typeof websocketUrl !== "string" || !websocketUrl || typeof token !== "string" || !token) {
|
|
405
|
-
throw new Error("Invalid Actor connection response");
|
|
406
|
-
}
|
|
407
|
-
const url = new URL(websocketUrl);
|
|
408
|
-
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
409
|
-
throw new Error("Invalid Actor WebSocket URL");
|
|
410
|
-
}
|
|
411
|
-
url.searchParams.set("_pk", connectionId);
|
|
412
|
-
url.searchParams.set("token", token);
|
|
413
|
-
return url.toString();
|
|
414
|
-
}
|
|
415
208
|
export function createActorsModule(config) {
|
|
416
209
|
// Live connections this client opened, so client.cleanup() can reclaim any the
|
|
417
210
|
// app forgot to close() (each connection removes itself here on close).
|
|
418
211
|
const connections = new Set();
|
|
419
|
-
let anonymousId;
|
|
420
|
-
const getAnonymousId = () => anonymousId !== null && anonymousId !== void 0 ? anonymousId : (anonymousId = getAnalyticsSessionId());
|
|
421
212
|
const module = new Proxy({}, {
|
|
422
213
|
get(_, key) {
|
|
423
214
|
// Symbols and `then` resolve to undefined (so the module isn't mistaken
|
|
424
215
|
// for a thenable when awaited); any string key is an actor name.
|
|
425
216
|
if (typeof key !== "string" || key === "then")
|
|
426
217
|
return undefined;
|
|
427
|
-
return (instanceId) => makeActorRef(key, instanceId, config,
|
|
218
|
+
return (instanceId) => makeActorRef(key, instanceId, config, connections);
|
|
428
219
|
},
|
|
429
220
|
});
|
|
430
221
|
return {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ActorConnectionError } from "./actors.error.js";
|
|
2
1
|
/**
|
|
3
2
|
* Extend this interface to add typed `subscribe` callbacks and `send` payloads
|
|
4
3
|
* for your deployed Actors.
|
|
@@ -37,44 +36,34 @@ type ToServerFor<N extends string> = N extends keyof ActorRegistry ? ActorRegist
|
|
|
37
36
|
export interface ActorConnectOptions {
|
|
38
37
|
/**
|
|
39
38
|
* The connection id — becomes the actor's `conn.id`. Supply a stable value
|
|
40
|
-
* (e.g. persisted per tab)
|
|
41
|
-
*
|
|
42
|
-
* auto-generated per-connection id.
|
|
39
|
+
* (e.g. persisted per tab) so a reconnect reuses the same server-side
|
|
40
|
+
* identity; omit for an auto-generated per-connection id.
|
|
43
41
|
*/
|
|
44
42
|
id?: string;
|
|
45
|
-
/**
|
|
46
|
-
* Called when this connection's bootstrap or WebSocket fails. Retryable
|
|
47
|
-
* failures may be reported more than once — the connection keeps retrying
|
|
48
|
-
* until it either succeeds or exhausts its attempt budget, and the final
|
|
49
|
-
* report before it gives up is the one where {@link Connection.closed}
|
|
50
|
-
* becomes `true`.
|
|
51
|
-
*
|
|
52
|
-
* Accepted only on the call that creates the connection: a later
|
|
53
|
-
* `connect({ onError })` throws, because a handler registered then could
|
|
54
|
-
* never be removed. Use {@link Connection.addErrorListener} for that.
|
|
55
|
-
*/
|
|
56
|
-
onError?: (error: ActorConnectionError) => void;
|
|
57
43
|
}
|
|
58
|
-
/** Handle for one
|
|
44
|
+
/** Handle for one listener registered via {@link Connection.subscribe}. */
|
|
59
45
|
export interface ActorSubscription {
|
|
60
|
-
/** Remove this listener; other listeners and the
|
|
46
|
+
/** Remove this listener; other listeners and the socket stay live. */
|
|
61
47
|
unsubscribe(): void;
|
|
62
48
|
}
|
|
63
49
|
/**
|
|
64
|
-
* A connection to an actor instance, returned by {@link ActorRef.connect}.
|
|
50
|
+
* A live connection to an actor instance, returned by {@link ActorRef.connect}.
|
|
51
|
+
* `subscribe`/`send` are always valid — you only get a `Connection` once the
|
|
52
|
+
* socket has been opened, so there's no pre-connect state to guard against.
|
|
65
53
|
*/
|
|
66
54
|
export interface Connection<N extends string = string> {
|
|
67
55
|
/** The connection id (the value the actor sees as `conn.id`). */
|
|
68
56
|
readonly id: string;
|
|
69
|
-
/**
|
|
70
|
-
readonly closed: boolean;
|
|
71
|
-
/** Register a detachable connection-error listener. Throws if the connection has closed. */
|
|
72
|
-
addErrorListener(listener?: (error: ActorConnectionError) => void): ActorSubscription;
|
|
73
|
-
/** Register a message listener. Throws if the connection has closed. */
|
|
57
|
+
/** Register a message listener. Multiple are allowed; returns a per-listener unsubscribe. */
|
|
74
58
|
subscribe(callback: (data: ToClientFor<N>) => void): ActorSubscription;
|
|
75
|
-
/** Send a message. Buffered until open;
|
|
59
|
+
/** Send a message. Buffered by the socket until it's open; dropped after
|
|
60
|
+
* {@link close}. */
|
|
76
61
|
send(data: ToServerFor<N>): void;
|
|
77
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Tear down the socket, heartbeat, and all listeners. Safe to call more
|
|
64
|
+
* than once. A connection also closes itself when it fails permanently —
|
|
65
|
+
* see {@link ActorRef.connect}.
|
|
66
|
+
*/
|
|
78
67
|
close(): void;
|
|
79
68
|
}
|
|
80
69
|
/**
|
|
@@ -83,9 +72,13 @@ export interface Connection<N extends string = string> {
|
|
|
83
72
|
*/
|
|
84
73
|
export interface ActorRef<N extends string = string> {
|
|
85
74
|
/**
|
|
86
|
-
* Open the WebSocket and return the {@link Connection}.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
75
|
+
* Open the WebSocket and return the {@link Connection}. Idempotent while the
|
|
76
|
+
* connection is open.
|
|
77
|
+
*
|
|
78
|
+
* A connection that fails permanently (for example, the actor doesn't exist
|
|
79
|
+
* or the caller isn't allowed to connect) closes itself and reports the
|
|
80
|
+
* error to the client's `onError` handler. Call `connect()` again after
|
|
81
|
+
* fixing the cause to get a fresh {@link Connection}, and re-subscribe.
|
|
89
82
|
*/
|
|
90
83
|
connect(options?: ActorConnectOptions): Connection<N>;
|
|
91
84
|
}
|
|
@@ -25,6 +25,9 @@ const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, () =
|
|
|
25
25
|
wasInitializationTracked: false,
|
|
26
26
|
sessionContext: null,
|
|
27
27
|
sessionStartTime: null,
|
|
28
|
+
// Memoized session id for when `localStorage` can't persist one — see
|
|
29
|
+
// getAnalyticsSessionId.
|
|
30
|
+
fallbackSessionId: null,
|
|
28
31
|
config: {
|
|
29
32
|
...defaultConfiguration,
|
|
30
33
|
...getAnalyticsConfigFromUrlParams(),
|
|
@@ -216,9 +219,11 @@ function trackSessionDurationEvent(track) {
|
|
|
216
219
|
});
|
|
217
220
|
}
|
|
218
221
|
function getEventIntrinsicData() {
|
|
222
|
+
var _a, _b;
|
|
219
223
|
return {
|
|
220
224
|
timestamp: new Date().toISOString(),
|
|
221
|
-
|
|
225
|
+
// `window.location` is absent on React Native, so read it optionally.
|
|
226
|
+
pageUrl: typeof window !== "undefined" ? (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.pathname) !== null && _b !== void 0 ? _b : null : null,
|
|
222
227
|
};
|
|
223
228
|
}
|
|
224
229
|
function transformEventDataToApiRequestData(sessionContext) {
|
|
@@ -302,9 +307,15 @@ export function getAnalyticsConfigFromUrlParams() {
|
|
|
302
307
|
// return the config object //
|
|
303
308
|
return { enabled: analyticsEnable === "true" };
|
|
304
309
|
}
|
|
310
|
+
// When the id can't be persisted (React Native has no `localStorage`), keep
|
|
311
|
+
// it stable for the process instead of minting a fresh one per call.
|
|
312
|
+
function getFallbackSessionId() {
|
|
313
|
+
var _a;
|
|
314
|
+
return ((_a = analyticsSharedState.fallbackSessionId) !== null && _a !== void 0 ? _a : (analyticsSharedState.fallbackSessionId = generateUuid()));
|
|
315
|
+
}
|
|
305
316
|
export function getAnalyticsSessionId() {
|
|
306
317
|
if (typeof window === "undefined") {
|
|
307
|
-
return
|
|
318
|
+
return getFallbackSessionId();
|
|
308
319
|
}
|
|
309
320
|
try {
|
|
310
321
|
const sessionId = localStorage.getItem(ANALYTICS_SESSION_ID_LOCAL_STORAGE_KEY);
|
|
@@ -316,6 +327,6 @@ export function getAnalyticsSessionId() {
|
|
|
316
327
|
return sessionId;
|
|
317
328
|
}
|
|
318
329
|
catch (_a) {
|
|
319
|
-
return
|
|
330
|
+
return getFallbackSessionId();
|
|
320
331
|
}
|
|
321
332
|
}
|
package/dist/modules/auth.js
CHANGED
|
@@ -81,13 +81,10 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
81
81
|
// Tracked here rather than read off `axios.defaults` so the answer stays tied
|
|
82
82
|
// to the identity transitions below (`setToken`, `logout`) instead of to the
|
|
83
83
|
// header a caller may have set on the instance directly.
|
|
84
|
-
let
|
|
84
|
+
let hasAccessToken = Boolean(options.token);
|
|
85
85
|
return {
|
|
86
86
|
hasToken() {
|
|
87
|
-
return
|
|
88
|
-
},
|
|
89
|
-
getToken() {
|
|
90
|
-
return accessToken;
|
|
87
|
+
return hasAccessToken;
|
|
91
88
|
},
|
|
92
89
|
// Get current user information
|
|
93
90
|
async me() {
|
|
@@ -154,7 +151,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
154
151
|
// flight would otherwise resolve into callers that run after the logout.
|
|
155
152
|
clearPendingMe();
|
|
156
153
|
resetAnalyticsSessionContext();
|
|
157
|
-
|
|
154
|
+
hasAccessToken = false;
|
|
158
155
|
// Only do the rest if in a browser environment
|
|
159
156
|
if (typeof window !== "undefined") {
|
|
160
157
|
// Remove token from localStorage
|
|
@@ -183,7 +180,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
183
180
|
// resolved for the previous one must not be handed to later callers.
|
|
184
181
|
clearPendingMe();
|
|
185
182
|
resetAnalyticsSessionContext();
|
|
186
|
-
|
|
183
|
+
hasAccessToken = true;
|
|
187
184
|
// handle token change for axios clients
|
|
188
185
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
189
186
|
functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
@@ -537,6 +537,4 @@ export interface InternalAuthModule extends AuthModule {
|
|
|
537
537
|
* could not succeed without a session, not to decide that one is valid.
|
|
538
538
|
*/
|
|
539
539
|
hasToken(): boolean;
|
|
540
|
-
/** Current access token used by internal transports. */
|
|
541
|
-
getToken(): string | undefined;
|
|
542
540
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
const CONNECTOR_API_METHODS = new Set([
|
|
2
|
+
"GET",
|
|
3
|
+
"POST",
|
|
4
|
+
"PUT",
|
|
5
|
+
"PATCH",
|
|
6
|
+
"DELETE",
|
|
7
|
+
"HEAD",
|
|
8
|
+
]);
|
|
1
9
|
/**
|
|
2
10
|
* Creates the Connectors module for the Base44 SDK.
|
|
3
11
|
*
|
|
@@ -68,6 +76,56 @@ export function createConnectorsModule(axios, appId) {
|
|
|
68
76
|
connectionConfig: (_a = data.connection_config) !== null && _a !== void 0 ? _a : null,
|
|
69
77
|
};
|
|
70
78
|
},
|
|
79
|
+
async callApi(integrationType, request) {
|
|
80
|
+
assertNonEmptyString(integrationType, "Integration type");
|
|
81
|
+
return proxyCall(axios, `/apps/${appId}/connectors/${integrationType}/call`, request);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function assertNonEmptyString(value, label) {
|
|
86
|
+
if (!value || typeof value !== "string") {
|
|
87
|
+
throw new Error(`${label} is required and must be a string`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* POST a request to the connector proxy and normalize the response.
|
|
92
|
+
*
|
|
93
|
+
* The proxy reports upstream outcomes in the body rather than as HTTP status, so
|
|
94
|
+
* a provider 4xx/5xx arrives here as a resolved response with `success: false` —
|
|
95
|
+
* only Base44-side failures reject through the axios error interceptor.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
async function proxyCall(axios, url, request) {
|
|
100
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
101
|
+
if (!request || typeof request !== "object") {
|
|
102
|
+
throw new Error("Request is required and must be an object");
|
|
103
|
+
}
|
|
104
|
+
assertNonEmptyString(request.path, "Request path");
|
|
105
|
+
const method = (_a = request.method) !== null && _a !== void 0 ? _a : "GET";
|
|
106
|
+
if (!CONNECTOR_API_METHODS.has(method)) {
|
|
107
|
+
throw new Error("Request method must be one of GET, POST, PUT, PATCH, DELETE, or HEAD");
|
|
108
|
+
}
|
|
109
|
+
const response = await axios.post(url, {
|
|
110
|
+
method,
|
|
111
|
+
// Omitted rather than sent as null so the proxy applies the connector's
|
|
112
|
+
// declared default host.
|
|
113
|
+
...(request.host === undefined ? {} : { host: request.host }),
|
|
114
|
+
path: request.path,
|
|
115
|
+
query: (_b = request.query) !== null && _b !== void 0 ? _b : {},
|
|
116
|
+
headers: (_c = request.headers) !== null && _c !== void 0 ? _c : {},
|
|
117
|
+
body: (_d = request.body) !== null && _d !== void 0 ? _d : null,
|
|
118
|
+
});
|
|
119
|
+
const data = response;
|
|
120
|
+
return {
|
|
121
|
+
success: data.success,
|
|
122
|
+
phase: data.phase,
|
|
123
|
+
status: (_e = data.status_code) !== null && _e !== void 0 ? _e : null,
|
|
124
|
+
data: data.data,
|
|
125
|
+
dataBase64: (_f = data.data_base64) !== null && _f !== void 0 ? _f : null,
|
|
126
|
+
contentType: (_g = data.content_type) !== null && _g !== void 0 ? _g : null,
|
|
127
|
+
headers: (_h = data.headers) !== null && _h !== void 0 ? _h : {},
|
|
128
|
+
creditsCharged: (_j = data.credits_charged) !== null && _j !== void 0 ? _j : 0,
|
|
71
129
|
};
|
|
72
130
|
}
|
|
73
131
|
/**
|
|
@@ -41,6 +41,81 @@ export interface AppUserConnectorConnectionResponse {
|
|
|
41
41
|
/** Key-value configuration for the connection, or `null` if the connector does not provide one. */
|
|
42
42
|
connectionConfig: Record<string, string> | null;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* How far a metered connector call progressed through the Base44 proxy.
|
|
46
|
+
*
|
|
47
|
+
* Only `not_sent` proves that the provider did not execute the request.
|
|
48
|
+
* `timed_out` and `sent_unconfirmed` may have executed upstream, so do not
|
|
49
|
+
* automatically retry non-idempotent requests based on those phases.
|
|
50
|
+
*/
|
|
51
|
+
export type ConnectorApiResponsePhase = "not_sent" | "responded" | "timed_out" | "sent_unconfirmed";
|
|
52
|
+
/**
|
|
53
|
+
* A request to forward to a metered connector's API through the Base44 proxy.
|
|
54
|
+
*/
|
|
55
|
+
export interface ConnectorApiRequest {
|
|
56
|
+
/** HTTP method for the upstream request. Defaults to `'GET'`. */
|
|
57
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
58
|
+
/**
|
|
59
|
+
* Which of the connector's API hosts to call, by the name it declares.
|
|
60
|
+
* Omit for its default host (the first one declared). Only relevant for
|
|
61
|
+
* connectors that expose more than one host.
|
|
62
|
+
*/
|
|
63
|
+
host?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Path relative to the connector's API root, starting with `/`, such as `'/2/tweets'`.
|
|
66
|
+
*
|
|
67
|
+
* Must not be an absolute URL. Query parameters may be included here or passed
|
|
68
|
+
* separately as {@link query}; either way they are forwarded and priced identically.
|
|
69
|
+
*/
|
|
70
|
+
path: string;
|
|
71
|
+
/** Query parameters. Merged into the request URL alongside any already present in {@link path}. */
|
|
72
|
+
query?: Record<string, string | number | boolean | Array<string | number>>;
|
|
73
|
+
/** Extra request headers. Only headers the connector explicitly allows are forwarded; the rest are dropped. */
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
/** JSON request body. Ignored for `GET` and `HEAD`. */
|
|
76
|
+
body?: unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The upstream API's response, as returned by the Base44 connector proxy.
|
|
80
|
+
*/
|
|
81
|
+
export interface ConnectorApiResponse<T = unknown> {
|
|
82
|
+
/** `true` only when the upstream API returned a 2xx status. Proxy and upstream errors are `false`. */
|
|
83
|
+
success: boolean;
|
|
84
|
+
/** How far the call progressed. Only `not_sent` proves the provider did not execute it. */
|
|
85
|
+
phase: ConnectorApiResponsePhase;
|
|
86
|
+
/** The upstream HTTP status code, or `null` when no response was received. */
|
|
87
|
+
status: number | null;
|
|
88
|
+
/**
|
|
89
|
+
* The parsed upstream response body, or proxy error details when no response
|
|
90
|
+
* was received. `null` when the response was binary — see {@link dataBase64}.
|
|
91
|
+
*/
|
|
92
|
+
data: T;
|
|
93
|
+
/**
|
|
94
|
+
* The response body base64-encoded, for the media types the connector declares
|
|
95
|
+
* as binary (images, PDFs). Set instead of {@link data}, never alongside it.
|
|
96
|
+
*/
|
|
97
|
+
dataBase64: string | null;
|
|
98
|
+
/** The response media type, set only alongside {@link dataBase64}. */
|
|
99
|
+
contentType: string | null;
|
|
100
|
+
/** The subset of upstream response headers the connector exposes, typically rate-limit counters. */
|
|
101
|
+
headers: Record<string, string>;
|
|
102
|
+
/** Integration credits billed to the workspace for this call. */
|
|
103
|
+
creditsCharged: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Raw proxy response shape. Mapped to {@link ConnectorApiResponse} before being returned.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export interface ConnectorProxyRawResponse {
|
|
110
|
+
success: boolean;
|
|
111
|
+
phase: ConnectorApiResponsePhase;
|
|
112
|
+
status_code: number | null;
|
|
113
|
+
data: unknown;
|
|
114
|
+
data_base64: string | null;
|
|
115
|
+
content_type: string | null;
|
|
116
|
+
headers: Record<string, string>;
|
|
117
|
+
credits_charged: number;
|
|
118
|
+
}
|
|
44
119
|
/**
|
|
45
120
|
* Connectors module for managing OAuth tokens for external services.
|
|
46
121
|
*
|
|
@@ -71,6 +146,18 @@ export interface AppUserConnectorConnectionResponse {
|
|
|
71
146
|
* 3. In a backend function, call {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()} using the service role client (`base44.asServiceRole.connectors`) with the connector ID to retrieve the app user's token.
|
|
72
147
|
* 4. Use the returned `accessToken` to call the external service's API directly. Some connectors also return a `connectionConfig` with additional values such as a subdomain for building the API URL.
|
|
73
148
|
*
|
|
149
|
+
* ## Metered connectors
|
|
150
|
+
*
|
|
151
|
+
* A few [platform connectors](#shared-connectors) are backed by paid third-party APIs that charge Base44 per call. For those, the OAuth token is **not** available to your code — {@linkcode getConnection | getConnection()} rejects with a `403`. Call them with {@linkcode callApi | callApi()} instead: Base44 attaches the credential server-side, forwards the request, and bills your workspace's integration credits for the call.
|
|
152
|
+
*
|
|
153
|
+
* This applies to platform connectors only. A workspace-registered or app user connector runs on **your own** OAuth app, so the provider invoices you directly and there is nothing for Base44 to meter — those keep normal token access via {@linkcode getWorkspaceConnection | getWorkspaceConnection()} and {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()}.
|
|
154
|
+
*
|
|
155
|
+
* Two things to keep in mind when writing against a metered connector:
|
|
156
|
+
*
|
|
157
|
+
* - **Cost varies by endpoint, sometimes sharply.** The same connector can charge two orders of magnitude more for one endpoint than another, so avoid putting an expensive call inside a loop and batch wherever the provider supports it. Each response reports what it actually cost as `creditsCharged`.
|
|
158
|
+
* - **Provider and transport outcomes are returned, not thrown.** A provider `4xx`/`5xx` or a connection failure comes back as `success: false` with its `phase`; authorization, quota, and invalid proxy requests reject the promise.
|
|
159
|
+
* - **Only `phase: 'not_sent'` proves the provider did not execute the request.** A timeout or in-flight failure may have executed upstream, so do not automatically retry a non-idempotent call unless the provider supports an idempotency key.
|
|
160
|
+
*
|
|
74
161
|
* ## Available connectors
|
|
75
162
|
*
|
|
76
163
|
* The connectors below can be used as shared connectors or as app user connectors. For a shared platform connector, pass the integration type string to {@linkcode getConnection | getConnection()}. For a connector you register in Workspace Settings with your own OAuth app, use the connector ID with {@linkcode getWorkspaceConnection | getWorkspaceConnection()} for a shared token, or with {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()} for a per-user token.
|
|
@@ -328,6 +415,41 @@ export interface ConnectorsModule {
|
|
|
328
415
|
* ```
|
|
329
416
|
*/
|
|
330
417
|
getCurrentAppUserConnection(connectorId: string): Promise<AppUserConnectorConnectionResponse>;
|
|
418
|
+
/**
|
|
419
|
+
* Calls a [metered connector's](#metered-connectors) API through the Base44 proxy.
|
|
420
|
+
*
|
|
421
|
+
* Use this for a shared platform connector identified by an integration type. Base44 adds the OAuth credential to the outgoing request, forwards it, and bills the workspace for the call, so you never handle the token yourself.
|
|
422
|
+
*
|
|
423
|
+
* @param integrationType - The type of integration, such as `'x'`. See [Available connectors](#available-connectors).
|
|
424
|
+
* @param request - The upstream request to forward. See {@link ConnectorApiRequest}.
|
|
425
|
+
* @returns Promise resolving to a {@link ConnectorApiResponse}. Note that an upstream error is reported in `success` and `status`, not thrown — only Base44-side failures reject.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* // Post to X
|
|
430
|
+
* const res = await base44.asServiceRole.connectors.callApi('x', {
|
|
431
|
+
* method: 'POST',
|
|
432
|
+
* path: '/2/tweets',
|
|
433
|
+
* body: { text: 'Shipped!' },
|
|
434
|
+
* });
|
|
435
|
+
*
|
|
436
|
+
* if (!res.success) {
|
|
437
|
+
* console.error('X rejected the post', res.status, res.data);
|
|
438
|
+
* }
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```typescript
|
|
443
|
+
* // Read, with query parameters and a look at what the call cost
|
|
444
|
+
* const res = await base44.asServiceRole.connectors.callApi('x', {
|
|
445
|
+
* path: '/2/tweets/search/recent',
|
|
446
|
+
* query: { query: 'base44', max_results: 10 },
|
|
447
|
+
* });
|
|
448
|
+
*
|
|
449
|
+
* console.log(`${res.creditsCharged} credits`, res.data);
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
callApi<T = unknown>(integrationType: ConnectorIntegrationType, request: ConnectorApiRequest): Promise<ConnectorApiResponse<T>>;
|
|
331
453
|
}
|
|
332
454
|
/**
|
|
333
455
|
* User-scoped connectors module for managing app user OAuth connections.
|
|
@@ -132,13 +132,14 @@ export function createAxiosClient({ baseURL, headers = {}, token, interceptRespo
|
|
|
132
132
|
client.interceptors.request.use((config) => {
|
|
133
133
|
// `window.location` is absent on React Native (where `window` still exists),
|
|
134
134
|
// so guard on it before reading `.href`.
|
|
135
|
-
if (typeof window !== "undefined"
|
|
136
|
-
|
|
135
|
+
if (typeof window !== "undefined") {
|
|
136
|
+
if (window.location) {
|
|
137
|
+
config.headers.set("X-Origin-URL", window.location.href);
|
|
138
|
+
}
|
|
137
139
|
// On unauthenticated requests, attach a stable anonymous visitor id so the
|
|
138
140
|
// backend can support anonymous agent access (conversation grouping + ownership).
|
|
139
141
|
// Authenticated requests are identified by their Authorization header instead.
|
|
140
|
-
if (!config.headers.get("Authorization")
|
|
141
|
-
!config.headers.get("X-Base44-Anonymous-Id")) {
|
|
142
|
+
if (!config.headers.get("Authorization")) {
|
|
142
143
|
config.headers.set("X-Base44-Anonymous-Id", getAnalyticsSessionId());
|
|
143
144
|
}
|
|
144
145
|
}
|
|
@@ -186,11 +187,11 @@ export function createAxiosClient({ baseURL, headers = {}, token, interceptRespo
|
|
|
186
187
|
}
|
|
187
188
|
return response.data;
|
|
188
189
|
}, (error) => {
|
|
189
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
190
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
190
191
|
const message = ((_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) ||
|
|
191
192
|
((_d = (_c = error.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.detail) ||
|
|
192
193
|
error.message;
|
|
193
|
-
const base44Error = new Base44Error(message, (_e = error.response) === null || _e === void 0 ? void 0 : _e.status, (_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.data) === null || _g === void 0 ? void 0 : _g.code, (
|
|
194
|
+
const base44Error = new Base44Error(message, (_e = error.response) === null || _e === void 0 ? void 0 : _e.status, (_m = (_h = (_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.data) === null || _g === void 0 ? void 0 : _g.code) !== null && _h !== void 0 ? _h : (_l = (_k = (_j = error.response) === null || _j === void 0 ? void 0 : _j.headers) === null || _k === void 0 ? void 0 : _k.get) === null || _l === void 0 ? void 0 : _l.call(_k, "x-base44-connector-error")) !== null && _m !== void 0 ? _m : (_p = (_o = error.response) === null || _o === void 0 ? void 0 : _o.headers) === null || _p === void 0 ? void 0 : _p["x-base44-connector-error"], (_q = error.response) === null || _q === void 0 ? void 0 : _q.data, error);
|
|
194
195
|
// Log errors in development
|
|
195
196
|
if (process.env.NODE_ENV !== "production") {
|
|
196
197
|
safeErrorLog("[Base44 SDK Error]", base44Error);
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** Contextual error for an Actor bootstrap, WebSocket, or closed-connection failure. */
|
|
2
|
-
export declare class ActorConnectionError extends Error {
|
|
3
|
-
readonly actorName: string;
|
|
4
|
-
readonly instanceId: string;
|
|
5
|
-
readonly connectionId: string;
|
|
6
|
-
readonly cause: unknown;
|
|
7
|
-
/** HTTP response status, when the failure came from the bootstrap request. */
|
|
8
|
-
readonly status?: number;
|
|
9
|
-
/** WebSocket close code, when the far end closed the connection. */
|
|
10
|
-
readonly closeCode?: number;
|
|
11
|
-
/** WebSocket close reason, when the far end supplied one. */
|
|
12
|
-
readonly closeReason?: string;
|
|
13
|
-
constructor(actorName: string, instanceId: string, connectionId: string, cause: unknown, status?: number, closeCode?: number, closeReason?: string);
|
|
14
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/** Contextual error for an Actor bootstrap, WebSocket, or closed-connection failure. */
|
|
2
|
-
export class ActorConnectionError extends Error {
|
|
3
|
-
constructor(actorName, instanceId, connectionId, cause, status, closeCode, closeReason) {
|
|
4
|
-
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
5
|
-
super(`Actor "${actorName}" instance "${instanceId}" connection "${connectionId}": ${causeMessage}`);
|
|
6
|
-
this.actorName = actorName;
|
|
7
|
-
this.instanceId = instanceId;
|
|
8
|
-
this.connectionId = connectionId;
|
|
9
|
-
this.cause = cause;
|
|
10
|
-
this.name = "ActorConnectionError";
|
|
11
|
-
this.status = status;
|
|
12
|
-
this.closeCode = closeCode;
|
|
13
|
-
this.closeReason = closeReason;
|
|
14
|
-
}
|
|
15
|
-
}
|