@base44-preview/sdk 0.8.48-pr.280.b5c6133 → 0.8.48-pr.280.cc2ee4a

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.js CHANGED
@@ -16,7 +16,6 @@ import { RoomsSocket } from "./utils/socket-utils.js";
16
16
  import { createAnalyticsModule } from "./modules/analytics.js";
17
17
  import { createExperimentsModule } from "./modules/experiments.js";
18
18
  import { createExposureTracker } from "./modules/experiment-exposures.js";
19
- import { EXPERIMENTS_CONTEXT_HEADER, getBrowserExperimentsContext, readExperimentsContext } from "./modules/experiments-context.js";
20
19
  import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
21
20
  /**
22
21
  * Creates a Base44 client.
@@ -56,11 +55,10 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
56
55
  * ```
57
56
  */
58
57
  export function createClient(config) {
59
- var _a, _b, _c, _d, _e, _f, _g;
58
+ var _a, _b, _c, _d, _e;
60
59
  const { serverUrl = "https://base44.app", appId, analytics, token, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
61
60
  // Normalize appBaseUrl to always be a string (empty if not provided or invalid)
62
61
  const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
63
- const experimentsContext = (_a = config.experiments) !== null && _a !== void 0 ? _a : getBrowserExperimentsContext(appId);
64
62
  const socketConfig = {
65
63
  serverUrl,
66
64
  mountPath: "/ws-user-apps/socket.io/",
@@ -77,14 +75,9 @@ export function createClient(config) {
77
75
  }
78
76
  return socket;
79
77
  };
80
- const { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext, ...requestHeaders } = optionalHeaders !== null && optionalHeaders !== void 0 ? optionalHeaders : {};
81
78
  const headers = {
82
- ...requestHeaders,
79
+ ...optionalHeaders,
83
80
  "X-App-Id": String(appId),
84
- ...(experimentsContext ? {
85
- "Base44-Visitor-Id": experimentsContext.identity.visitorId,
86
- "Base44-Experiment-Preview": JSON.stringify((_b = experimentsContext.preview) !== null && _b !== void 0 ? _b : {}),
87
- } : {}),
88
81
  };
89
82
  const functionHeaders = functionsVersion
90
83
  ? {
@@ -130,18 +123,13 @@ export function createClient(config) {
130
123
  baseURL: `${serverUrl}/api`,
131
124
  headers,
132
125
  });
133
- const exposureTracker = createExposureTracker({
134
- axiosClient,
135
- appId,
136
- enabled: (_c = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _c !== void 0 ? _c : true,
137
- source: typeof window === "undefined" ? "backend" : "browser",
138
- pageUrl: experimentsContext === null || experimentsContext === void 0 ? void 0 : experimentsContext.pageUrl,
139
- });
140
126
  const experiments = createExperimentsModule({
141
127
  getAuth: () => userAuthModule,
142
- trackExposure: exposureTracker.track,
143
- flushExposures: exposureTracker.flush,
144
- context: experimentsContext,
128
+ trackExposure: createExposureTracker({
129
+ axiosClient,
130
+ appId,
131
+ enabled: (_a = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _a !== void 0 ? _a : true,
132
+ }).track,
145
133
  });
146
134
  const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
147
135
  appBaseUrl: normalizedAppBaseUrl,
@@ -159,19 +147,11 @@ export function createClient(config) {
159
147
  userAuthModule.setToken(accessToken);
160
148
  }
161
149
  }
162
- if (experimentsContext) {
163
- const { userId, status } = experimentsContext.identity;
164
- // The document's cookie identity may differ from this client's localStorage token.
165
- const needsClientIdentity = typeof window !== "undefined" && userAuthModule.hasToken() &&
166
- experimentsContext.config.experiments.some((experiment) => experiment.assign_by === "user");
167
- experiments.onAuthStateChange(status === "pending" || needsClientIdentity ? { status: "pending" } :
168
- userId ? { status: "authenticated", userId } : { status: "anonymous" });
169
- }
170
150
  const actorsModule = createActorsModule({
171
151
  appId,
172
152
  // serverUrl is often relative/empty (same-origin app); the proxy-fallback
173
153
  // URL needs an absolute host, so fall back to the page origin.
174
- host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_d = window.location) === null || _d === void 0 ? void 0 : _d.origin : undefined),
154
+ host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_b = window.location) === null || _b === void 0 ? void 0 : _b.origin : undefined),
175
155
  functionsVersion,
176
156
  getAuthToken: () => token || getAccessToken(),
177
157
  mintConnectionToken: async (actorName, room, connectionId) => {
@@ -210,7 +190,7 @@ export function createClient(config) {
210
190
  }
211
191
  return headers;
212
192
  },
213
- baseURL: (_e = functionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
193
+ baseURL: (_c = functionsAxiosClient.defaults) === null || _c === void 0 ? void 0 : _c.baseURL,
214
194
  }),
215
195
  agents: createAgentsModule({
216
196
  axios: axiosClient,
@@ -228,8 +208,7 @@ export function createClient(config) {
228
208
  serverUrl,
229
209
  appId,
230
210
  userAuthModule,
231
- enabled: (_f = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _f !== void 0 ? _f : true,
232
- getVisitorId: experiments.visitorId,
211
+ enabled: (_d = analytics === null || analytics === void 0 ? void 0 : analytics.enabled) !== null && _d !== void 0 ? _d : true,
233
212
  }),
234
213
  actors: actorsModule.module,
235
214
  cleanup: () => {
@@ -259,7 +238,7 @@ export function createClient(config) {
259
238
  }
260
239
  return headers;
261
240
  },
262
- baseURL: (_g = serviceRoleFunctionsAxiosClient.defaults) === null || _g === void 0 ? void 0 : _g.baseURL,
241
+ baseURL: (_e = serviceRoleFunctionsAxiosClient.defaults) === null || _e === void 0 ? void 0 : _e.baseURL,
263
242
  }),
264
243
  agents: createAgentsModule({
265
244
  axios: serviceRoleAxiosClient,
@@ -302,10 +281,7 @@ export function createClient(config) {
302
281
  appId: String(appId),
303
282
  serverUrl,
304
283
  functionsVersion,
305
- platformHeaders: {
306
- ...headers,
307
- ...(inheritedExperimentsContext ? { [EXPERIMENTS_CONTEXT_HEADER]: inheritedExperimentsContext } : {}),
308
- },
284
+ platformHeaders: optionalHeaders,
309
285
  }),
310
286
  /**
311
287
  * Sets a new authentication token for all subsequent requests.
@@ -457,10 +433,6 @@ export function createClientFromRequest(request) {
457
433
  }
458
434
  // Prepare additional headers to propagate
459
435
  const additionalHeaders = {};
460
- const encodedExperiments = request.headers.get(EXPERIMENTS_CONTEXT_HEADER);
461
- const experimentsContext = readExperimentsContext(encodedExperiments, appId);
462
- if (experimentsContext && encodedExperiments)
463
- additionalHeaders[EXPERIMENTS_CONTEXT_HEADER] = encodedExperiments;
464
436
  if (stateHeader) {
465
437
  additionalHeaders["Base44-State"] = stateHeader;
466
438
  }
@@ -481,6 +453,5 @@ export function createClientFromRequest(request) {
481
453
  serviceToken: serviceRoleToken,
482
454
  functionsVersion: functionsVersion !== null && functionsVersion !== void 0 ? functionsVersion : undefined,
483
455
  headers: additionalHeaders,
484
- experiments: experimentsContext ? { ...experimentsContext, pageUrl: request.url ? new URL(request.url).pathname : "/" } : undefined,
485
456
  });
486
457
  }
@@ -10,7 +10,6 @@ import type { AppLogsModule } from "./modules/app-logs.types.js";
10
10
  import type { AppModule } from "./modules/app.types.js";
11
11
  import type { AnalyticsModule } from "./modules/analytics.types.js";
12
12
  import type { ExperimentsModule } from "./modules/experiments.types.js";
13
- import type { ExperimentsContext } from "./modules/experiments-config.types.js";
14
13
  import type { ActorsModule } from "./modules/actors.types.js";
15
14
  import type { FetchWithAuthInit } from "./utils/fetch-with-auth.js";
16
15
  /**
@@ -81,12 +80,6 @@ export interface CreateClientConfig {
81
80
  * Omit this option to preserve the default analytics behavior.
82
81
  */
83
82
  analytics?: CreateClientAnalyticsConfig;
84
- /**
85
- * Platform-validated context for local flag evaluation. Request-scoped on servers.
86
- * Automatically read from the platform bootstrap in browsers and trusted headers
87
- * by createClientFromRequest(). Not an authorization credential.
88
- */
89
- experiments?: ExperimentsContext;
90
83
  /**
91
84
  * User authentication token. Used to authenticate as a specific user.
92
85
  *
@@ -142,7 +135,7 @@ export interface Base44Client {
142
135
  connectors: UserConnectorsModule;
143
136
  /** {@link EntitiesModule | Entities module} for CRUD operations on your data models. */
144
137
  entities: EntitiesModule;
145
- /** {@link ExperimentsModule | Experiments module} for local feature flags and exposures. */
138
+ /** {@link ExperimentsModule | Experiments module} for browser feature flags and exposures. */
146
139
  experiments: ExperimentsModule;
147
140
  /** {@link FunctionsModule | Functions module} for invoking custom backend functions. */
148
141
  functions: FunctionsModule;
package/dist/index.d.ts CHANGED
@@ -4,8 +4,6 @@ import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from
4
4
  export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
5
  export type { Base44Client, CreateClientAnalyticsConfig, CreateClientConfig, CreateClientOptions, Base44ErrorJSON, };
6
6
  export * from "./types.js";
7
- export { evaluateExperiments } from "./modules/experiments-evaluator.js";
8
- export type { ExperimentsConfig, ExperimentsContext, ExperimentsIdentity } from "./modules/experiments-config.types.js";
9
7
  export type { ExperimentsModule, ExperimentsSnapshot, } from "./modules/experiments.types.js";
10
8
  export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
11
9
  export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, ChangePasswordParams, ResetPasswordParams, User, } from "./modules/auth.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 { evaluateExperiments } from "./modules/experiments-evaluator.js";
7
6
  export { Actor } from "./actor.js";
@@ -12,11 +12,10 @@ export interface AnalyticsModuleArgs {
12
12
  appId: string;
13
13
  userAuthModule: InternalAuthModule;
14
14
  enabled: boolean;
15
- getVisitorId?: () => string | undefined;
16
15
  }
17
16
  /** @internal */
18
17
  export declare function isAnalyticsEnabled(enabled: boolean): boolean;
19
- export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, getVisitorId, }: AnalyticsModuleArgs) => {
18
+ export declare const createAnalyticsModule: ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }: AnalyticsModuleArgs) => {
20
19
  track: (params: TrackEventParams) => void;
21
20
  cleanup: () => void;
22
21
  };
@@ -38,7 +38,7 @@ const analyticsSharedState = getSharedInstance(ANALYTICS_SHARED_STATE_NAME, () =
38
38
  export function isAnalyticsEnabled(enabled) {
39
39
  return enabled && analyticsSharedState.config.enabled && !isReactNative;
40
40
  }
41
- export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, getVisitorId, }) => {
41
+ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthModule, enabled, }) => {
42
42
  // prevent overflow of events //
43
43
  const { maxQueueSize, throttleTime, batchSize } = analyticsSharedState.config;
44
44
  // Disable analytics on React Native. It defines `window` but not `document`,
@@ -74,17 +74,16 @@ export const createAnalyticsModule = ({ axiosClient, serverUrl, appId, userAuthM
74
74
  }
75
75
  };
76
76
  const flush = async (eventsData, options = {}) => {
77
- var _a;
78
77
  if (eventsData.length === 0)
79
78
  return;
80
79
  const sessionContext_ = await getSessionContext(userAuthModule);
81
- const events = eventsData.map(transformEventDataToApiRequestData({ ...sessionContext_, session_id: (_a = getVisitorId === null || getVisitorId === void 0 ? void 0 : getVisitorId()) !== null && _a !== void 0 ? _a : sessionContext_.session_id }));
80
+ const events = eventsData.map(transformEventDataToApiRequestData(sessionContext_));
82
81
  try {
83
82
  if (!options.isBeacon || !beaconRequest(events)) {
84
83
  await batchRequestFallback(events);
85
84
  }
86
85
  }
87
- catch (_b) {
86
+ catch (_a) {
88
87
  // do nothing
89
88
  }
90
89
  };
@@ -239,8 +239,6 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
239
239
  const { access_token, user } = response;
240
240
  if (access_token) {
241
241
  this.setToken(access_token);
242
- if (typeof (user === null || user === void 0 ? void 0 : user.id) === "string")
243
- notifyAuthState({ status: "authenticated", userId: user.id });
244
242
  }
245
243
  return {
246
244
  access_token,
@@ -1,11 +1,9 @@
1
1
  import type { AxiosInstance } from "axios";
2
2
  /** @internal */
3
- export declare function createExposureTracker({ axiosClient, appId, enabled, source, pageUrl, }: {
3
+ export declare function createExposureTracker({ axiosClient, appId, enabled, }: {
4
4
  axiosClient: AxiosInstance;
5
5
  appId: string;
6
6
  enabled: boolean;
7
- source?: "browser" | "backend";
8
- pageUrl?: string;
9
7
  }): {
10
8
  track(assignment: {
11
9
  experiment_id: string;
@@ -15,5 +13,4 @@ export declare function createExposureTracker({ axiosClient, appId, enabled, sou
15
13
  visitorId: string;
16
14
  userId: string | null;
17
15
  }): void;
18
- flush(): Promise<void>;
19
16
  };
@@ -1,65 +1,45 @@
1
- import { v4 as uuid } from "uuid";
2
1
  import { isAnalyticsEnabled } from "./analytics.js";
3
2
  /** @internal */
4
- export function createExposureTracker({ axiosClient, appId, enabled, source = "browser", pageUrl, }) {
5
- const entries = new Map();
6
- function send(entry) {
7
- if (entry.pending)
8
- return entry.pending;
9
- const pending = (async () => {
10
- for (let attempt = 0;; attempt++) {
11
- try {
12
- const response = await axiosClient.request({
13
- method: "POST",
14
- url: `/apps/${appId}/analytics/track/batch`,
15
- headers: { Authorization: entry.authorization },
16
- data: entry.data,
17
- });
18
- if (response.accepted !== 1)
19
- throw new Error("Experiment exposure was not accepted");
20
- entry.acknowledged = true;
21
- return;
22
- }
23
- catch (error) {
24
- if (attempt === 2)
25
- throw error;
26
- await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 100 : 500));
27
- }
28
- }
29
- })().finally(() => { entry.pending = undefined; });
30
- entry.pending = pending;
31
- // Reads stay synchronous; flush() lets request handlers observe delivery failures.
32
- void pending.catch(() => { });
33
- return pending;
34
- }
3
+ export function createExposureTracker({ axiosClient, appId, enabled, }) {
4
+ const exposed = new Set();
35
5
  return {
36
6
  track(assignment, identity) {
37
- if ((source === "browser" && typeof window === "undefined") || !isAnalyticsEnabled(enabled))
7
+ var _a;
8
+ if (typeof window === "undefined" || !isAnalyticsEnabled(enabled))
38
9
  return;
39
10
  const { experiment_id, run_version, variant_key } = assignment;
40
- const key = JSON.stringify([experiment_id, run_version, variant_key, identity.userId, identity.visitorId]);
41
- let entry = entries.get(key);
42
- if (!entry) {
43
- const authorization = identity.userId ? axiosClient.defaults.headers.common.Authorization : null;
44
- entry = {
45
- acknowledged: false,
46
- authorization: typeof authorization === "string" ? authorization : null,
47
- data: { events: [{
48
- event_id: uuid(),
49
- event_name: "__experiment_exposure__",
50
- timestamp: new Date().toISOString(),
51
- session_id: identity.visitorId,
52
- page_url: pageUrl !== null && pageUrl !== void 0 ? pageUrl : (typeof window === "undefined" ? "/" : window.location.pathname),
53
- properties: { experiment_id, run_version, variant_key, source },
54
- }] },
55
- };
56
- entries.set(key, entry);
57
- }
58
- if (!entry.acknowledged)
59
- void send(entry);
60
- },
61
- async flush() {
62
- await Promise.all([...entries.values()].filter((entry) => !entry.acknowledged).map(send));
11
+ const key = JSON.stringify([
12
+ experiment_id,
13
+ run_version,
14
+ variant_key,
15
+ identity.userId,
16
+ identity.visitorId,
17
+ ]);
18
+ if (exposed.has(key))
19
+ return;
20
+ exposed.add(key);
21
+ // Pin the event's auth before an account change can alter Axios defaults.
22
+ const authorization = identity.userId
23
+ ? ((_a = axiosClient.defaults.headers.common.Authorization) !== null && _a !== void 0 ? _a : null)
24
+ : null;
25
+ void axiosClient
26
+ .request({
27
+ method: "POST",
28
+ url: `/apps/${appId}/analytics/track/batch`,
29
+ headers: { Authorization: authorization },
30
+ data: {
31
+ events: [
32
+ {
33
+ event_name: "__experiment_exposure__",
34
+ timestamp: new Date().toISOString(),
35
+ session_id: identity.visitorId,
36
+ page_url: window.location.pathname,
37
+ properties: { experiment_id, run_version, variant_key },
38
+ },
39
+ ],
40
+ },
41
+ })
42
+ .catch(() => exposed.delete(key));
63
43
  },
64
44
  };
65
45
  }
@@ -1,16 +1,12 @@
1
1
  import type { AuthState, InternalAuthModule } from "./auth.types.js";
2
2
  import type { ExperimentsModule } from "./experiments.types.js";
3
3
  import type { createExposureTracker } from "./experiment-exposures.js";
4
- import type { ExperimentsContext } from "./experiments-config.types.js";
5
4
  /** @internal */
6
- export declare function createExperimentsModule({ getAuth, trackExposure, flushExposures, context, }: {
5
+ export declare function createExperimentsModule({ getAuth, trackExposure, }: {
7
6
  getAuth: () => InternalAuthModule;
8
7
  trackExposure: ReturnType<typeof createExposureTracker>["track"];
9
- flushExposures?: () => Promise<void>;
10
- context?: ExperimentsContext;
11
8
  }): {
12
9
  module: ExperimentsModule;
13
10
  onAuthStateChange: (next: AuthState) => void;
14
- visitorId: () => string | undefined;
15
11
  cleanup(): void;
16
12
  };
@@ -1,28 +1,19 @@
1
1
  import { getExperimentsRuntime, } from "./experiments-runtime.types.js";
2
- import { createExperimentsRuntime } from "./experiments-context.js";
3
2
  const EMPTY = Object.freeze({
4
3
  flags: Object.freeze({}),
5
4
  isLoading: false,
6
5
  });
7
6
  /** @internal */
8
- export function createExperimentsModule({ getAuth, trackExposure, flushExposures = async () => { }, context, }) {
9
- var _a;
10
- let runtime = context ? createExperimentsRuntime(context) : undefined;
11
- let state = context
12
- ? context.identity.status === "pending" ? { status: "pending" }
13
- : context.identity.userId ? { status: "authenticated", userId: context.identity.userId }
14
- : { status: "anonymous" }
15
- : undefined;
7
+ export function createExperimentsModule({ getAuth, trackExposure, }) {
8
+ let runtime;
9
+ let state;
16
10
  let snapshot = EMPTY;
17
11
  let active = false;
18
12
  let disposed = false;
13
+ let generation = 0;
14
+ let pending;
19
15
  const listeners = new Set();
20
16
  const readyWaiters = new Set();
21
- const initial = (_a = context === null || context === void 0 ? void 0 : context.serverSnapshot) !== null && _a !== void 0 ? _a : (context ? {
22
- flags: context.identity.status === "pending" ? {} : runtime.flags,
23
- isLoading: context.identity.status === "pending",
24
- } : EMPTY);
25
- const serverSnapshot = Object.freeze({ ...initial, flags: Object.freeze({ ...initial.flags }) });
26
17
  function settleReady() {
27
18
  if (snapshot.isLoading)
28
19
  return;
@@ -62,12 +53,25 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
62
53
  }
63
54
  publish();
64
55
  }
56
+ function resolveIdentity() {
57
+ if (!runtime || pending || disposed)
58
+ return;
59
+ state = { status: "pending" };
60
+ applyIdentity();
61
+ const currentGeneration = generation;
62
+ pending = getAuth()
63
+ .me()
64
+ .then(() => { }, () => { })
65
+ .finally(() => {
66
+ if (currentGeneration === generation)
67
+ pending = undefined;
68
+ });
69
+ }
65
70
  function activate() {
66
71
  if (disposed)
67
72
  return;
68
73
  active = true;
69
- if (!context)
70
- runtime = getExperimentsRuntime();
74
+ runtime = getExperimentsRuntime();
71
75
  if (!runtime) {
72
76
  publish();
73
77
  return;
@@ -77,16 +81,23 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
77
81
  ? { status: "pending" }
78
82
  : { status: "anonymous" };
79
83
  applyIdentity();
84
+ if (state.status === "pending")
85
+ resolveIdentity();
80
86
  }
81
87
  function onAuthStateChange(next) {
82
88
  if (disposed)
83
89
  return;
84
90
  state = next;
91
+ if (next.status === "pending" || next.status === "anonymous") {
92
+ generation++;
93
+ pending = undefined;
94
+ }
85
95
  if (!active)
86
96
  return;
87
- if (!context)
88
- runtime = getExperimentsRuntime();
97
+ runtime = getExperimentsRuntime();
89
98
  applyIdentity();
99
+ if (next.status === "pending")
100
+ resolveIdentity();
90
101
  }
91
102
  const module = {
92
103
  isEnabled(flagKey, fallback = false) {
@@ -102,7 +113,6 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
102
113
  activate();
103
114
  return snapshot;
104
115
  },
105
- getServerSnapshot: () => serverSnapshot,
106
116
  subscribe(listener) {
107
117
  activate();
108
118
  if (!disposed)
@@ -113,18 +123,24 @@ export function createExperimentsModule({ getAuth, trackExposure, flushExposures
113
123
  },
114
124
  async ready() {
115
125
  activate();
126
+ if ((state === null || state === void 0 ? void 0 : state.status) === "error") {
127
+ // Wait for auth.me() to release its shared, failed request before retrying.
128
+ await pending;
129
+ if ((state === null || state === void 0 ? void 0 : state.status) === "error")
130
+ resolveIdentity();
131
+ }
116
132
  if (snapshot.isLoading)
117
133
  return new Promise((resolve) => readyWaiters.add(resolve));
118
134
  return snapshot;
119
135
  },
120
- flush: flushExposures,
121
136
  };
122
137
  return {
123
138
  module,
124
139
  onAuthStateChange,
125
- visitorId: () => runtime === null || runtime === void 0 ? void 0 : runtime.visitorId,
126
140
  cleanup() {
127
141
  disposed = true;
142
+ generation++;
143
+ pending = undefined;
128
144
  runtime = undefined;
129
145
  snapshot = EMPTY;
130
146
  settleReady();
@@ -6,7 +6,7 @@ export interface ExperimentsSnapshot {
6
6
  readonly isLoading: boolean;
7
7
  }
8
8
  /**
9
- * Evaluates feature flags locally from platform-provided configuration and identity.
9
+ * Reads feature flags evaluated by the Base44 browser runtime.
10
10
  *
11
11
  * - Reads flags and reports experiment exposures when a flag is used.
12
12
  * - Synchronizes assignments with this client's SDK login, token changes, and logout.
@@ -14,10 +14,9 @@ export interface ExperimentsSnapshot {
14
14
  *
15
15
  * Available as `base44.experiments` for anonymous and signed-in app visitors,
16
16
  * not in service role mode. Use one client for the app whose runtime is on the page.
17
- * Browsers read the platform bootstrap. Servers and Workers use the request-scoped
18
- * context passed by createClientFromRequest(), or explicit createClient options.
19
- * Missing context returns fallbacks. For authenticated first render, the platform's
20
- * common auth bootstrap must supply a resolved identity before mounting the app.
17
+ * The platform must inject the Experiments runtime before this module can evaluate
18
+ * flags. Without it, including on servers and Workers, reads return their fallback;
19
+ * this module does not provide server-side evaluation or hydration guarantees.
21
20
  * Goal conversions use the existing {@link AnalyticsModule | analytics module}.
22
21
  * Visitor-keyed conversions share the injected runtime's visitor ID. When browser
23
22
  * storage is blocked, the platform must supply a unique per-page ID; attribution
@@ -25,17 +24,17 @@ export interface ExperimentsSnapshot {
25
24
  */
26
25
  export interface ExperimentsModule {
27
26
  /**
28
- * Reads a flag and queues an acknowledged exposure for its current assignment.
27
+ * Reads a flag and reports a best-effort exposure for its current assignment.
29
28
  *
30
- * Never starts an authentication request. Reads return the fallback while the
31
- * app's normal auth initialization is pending or failed. Supply trusted bootstrap
32
- * identity or let the app's existing auth.me()/login flow resolve it.
29
+ * The first use resolves identity through {@link AuthModule.me | auth.me()}
30
+ * when the client has a token. Reads return the fallback while identity is
31
+ * pending or could not be resolved. Await {@link ExperimentsModule.ready | ready()}
32
+ * or subscribe to updates before displaying authenticated variants.
33
33
  *
34
34
  * Call only where the feature is used: a read counts as exposure, not proof of
35
35
  * visibility. Preview overrides and flags without an assignment are not tracked.
36
36
  * Exposures respect the client's analytics setting, are deduplicated per client,
37
- * experiment run, variant and identity. Failed sends retry up to three attempts
38
- * with the same event ID, timestamp and credentials. Await flush() on servers.
37
+ * experiment run, variant and identity, and retry only on a later read after failure.
39
38
  *
40
39
  * @param flagKey - Feature flag key defined in your app.
41
40
  * @param fallback - Value for an unavailable flag or unresolved identity. Defaults to `false`.
@@ -50,7 +49,7 @@ export interface ExperimentsModule {
50
49
  /**
51
50
  * Returns the current flags and identity-loading state without tracking exposures.
52
51
  *
53
- * Observes identity resolution without starting it. The returned object retains its
52
+ * Starts lazy identity resolution if needed. The returned object retains its
54
53
  * reference until its values change, for use with external-store subscriptions.
55
54
  * Use {@link ExperimentsModule.isEnabled | isEnabled()} at the feature boundary
56
55
  * to record exposure rather than displaying a variant directly from this snapshot.
@@ -62,8 +61,6 @@ export interface ExperimentsModule {
62
61
  * ```
63
62
  */
64
63
  getSnapshot(): ExperimentsSnapshot;
65
- /** Immutable initial platform snapshot for matching server render and hydration. */
66
- getServerSnapshot(): ExperimentsSnapshot;
67
64
  /**
68
65
  * Listens for flag or loading-state changes caused by this client's SDK auth flows.
69
66
  *
@@ -82,10 +79,10 @@ export interface ExperimentsModule {
82
79
  */
83
80
  subscribe(listener: () => void): () => void;
84
81
  /**
85
- * Waits for the app's common auth initialization, including a token change.
82
+ * Waits for the current identity lookup, including a token change during that lookup.
86
83
  *
87
- * Resolves with empty flags after an identity lookup failure. Retrying authentication
88
- * belongs to the normal auth flow. Missing runtimes resolve immediately. This does not wait for a future
84
+ * Resolves with empty flags after an identity lookup failure; calling again retries
85
+ * the lookup. Missing runtimes resolve immediately. This does not wait for a future
89
86
  * runtime injection or for exposure delivery, and never records an exposure itself.
90
87
  *
91
88
  * @returns A snapshot after the current identity lookup settles.
@@ -96,11 +93,4 @@ export interface ExperimentsModule {
96
93
  * ```
97
94
  */
98
95
  ready(): Promise<ExperimentsSnapshot>;
99
- /**
100
- * Waits until queued exposures are acknowledged; rejects after bounded retries.
101
- * Server/Worker handlers must await this before ending the request (or use waitUntil).
102
- * Retries preserve event IDs but raw storage is not exactly-once. Calling again
103
- * retries unacknowledged events with the same IDs. No new exposures are created.
104
- */
105
- flush(): Promise<void>;
106
96
  }
@@ -29,7 +29,6 @@ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl,
29
29
  ? header
30
30
  : null;
31
31
  };
32
- const contextAuthorization = bearer(axios);
33
32
  return async function fetchWithAuth(path, init = {}) {
34
33
  assertOwnOriginPath(path);
35
34
  const { fetch: transport = fetch, ...requestInit } = init;
@@ -50,11 +49,6 @@ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl,
50
49
  inherit("Base44-Functions-Version", functionsVersion);
51
50
  inherit("Base44-State", inherited.get("Base44-State"));
52
51
  inherit("X-Data-Env", inherited.get("X-Data-Env"));
53
- inherit("Base44-Visitor-Id", inherited.get("Base44-Visitor-Id"));
54
- inherit("Base44-Experiment-Preview", inherited.get("Base44-Experiment-Preview"));
55
- if (headers.get("Authorization") === contextAuthorization) {
56
- inherit("Base44-Experiments-Context", inherited.get("Base44-Experiments-Context"));
57
- }
58
52
  // The path is passed through untouched: resolving it here would need a
59
53
  // document, and a root-relative path is already what a runtime that
60
54
  // dispatches in-process (Nitro's `fetch`) expects. `host` is deliberately
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.280.b5c6133",
3
+ "version": "0.8.48-pr.280.cc2ee4a",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,39 +0,0 @@
1
- import type { ExperimentsSnapshot } from "./experiments.types.js";
2
- /** Shared versioned configuration published by the platform, never visitor-specific. */
3
- export interface ExperimentsConfig {
4
- v: 1;
5
- app_id: string;
6
- revision?: number;
7
- flags: {
8
- key: string;
9
- rollout_percentage: number;
10
- }[];
11
- experiments: {
12
- id: string;
13
- flag_key: string;
14
- run_version: number;
15
- assign_by: "visitor" | "user";
16
- traffic_allocation: number;
17
- variants: {
18
- key: string;
19
- value: boolean;
20
- weight: number;
21
- }[];
22
- }[];
23
- }
24
- /** Identity supplied by the platform's normal authenticated request/bootstrap path. */
25
- export interface ExperimentsIdentity {
26
- visitorId: string;
27
- userId: string | null;
28
- status?: "authenticated" | "anonymous" | "pending";
29
- }
30
- /** One request's or browser page's context. Never share it between server requests. */
31
- export interface ExperimentsContext {
32
- config: ExperimentsConfig;
33
- identity: ExperimentsIdentity;
34
- preview?: Readonly<Record<string, boolean>>;
35
- /** Request pathname used for server-side exposure events. */
36
- pageUrl?: string;
37
- /** Exact server-rendered flags, retained for the browser's first hydration render. */
38
- serverSnapshot?: ExperimentsSnapshot;
39
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,10 +0,0 @@
1
- import type { ExperimentsContext } from "./experiments-config.types.js";
2
- import type { ExperimentsRuntime } from "./experiments-runtime.types.js";
3
- /** @internal Platform ingress overwrites this header; it is not authentication. */
4
- export declare const EXPERIMENTS_CONTEXT_HEADER = "Base44-Experiments-Context";
5
- /** @internal */
6
- export declare function readExperimentsContext(encoded: string | null, appId: string): ExperimentsContext | undefined;
7
- /** @internal */
8
- export declare function getBrowserExperimentsContext(appId: string): ExperimentsContext | undefined;
9
- /** One independent evaluator instance for one client/request. @internal */
10
- export declare function createExperimentsRuntime(context: ExperimentsContext): ExperimentsRuntime;
@@ -1,44 +0,0 @@
1
- import { evaluateExperiments } from "./experiments-evaluator.js";
2
- /** @internal Platform ingress overwrites this header; it is not authentication. */
3
- export const EXPERIMENTS_CONTEXT_HEADER = "Base44-Experiments-Context";
4
- /** @internal */
5
- export function readExperimentsContext(encoded, appId) {
6
- if (!encoded || encoded.length > 96 * 1024)
7
- return;
8
- try {
9
- const bytes = Uint8Array.from(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0));
10
- return matchingContext(JSON.parse(new TextDecoder().decode(bytes)), appId);
11
- }
12
- catch (_a) {
13
- return;
14
- }
15
- }
16
- function matchingContext(value, appId) {
17
- var _a, _b;
18
- return ((_a = value === null || value === void 0 ? void 0 : value.config) === null || _a === void 0 ? void 0 : _a.v) === 1 && value.config.app_id === appId &&
19
- typeof ((_b = value.identity) === null || _b === void 0 ? void 0 : _b.visitorId) === "string" && value.identity.visitorId &&
20
- (value.identity.userId === null || typeof value.identity.userId === "string")
21
- ? value : undefined;
22
- }
23
- /** @internal */
24
- export function getBrowserExperimentsContext(appId) {
25
- if (typeof window === "undefined" || typeof document === "undefined")
26
- return;
27
- return matchingContext(window.__B44_EXPERIMENTS_BOOTSTRAP__, appId);
28
- }
29
- /** One independent evaluator instance for one client/request. @internal */
30
- export function createExperimentsRuntime(context) {
31
- const identity = { ...context.identity };
32
- const evaluate = () => evaluateExperiments(context.config, identity, context.preview);
33
- const runtime = {
34
- ...evaluate(),
35
- visitorId: identity.visitorId,
36
- userId: identity.userId,
37
- pendingUser: identity.status === "pending",
38
- setUser(userId) {
39
- identity.userId = userId;
40
- Object.assign(runtime, evaluate(), { userId, pendingUser: false });
41
- },
42
- };
43
- return runtime;
44
- }
@@ -1,11 +0,0 @@
1
- import type { ExperimentAssignment } from "./experiments-runtime.types.js";
2
- import type { ExperimentsConfig, ExperimentsIdentity } from "./experiments-config.types.js";
3
- /**
4
- * Evaluates flags locally without storage, network, clock, or browser globals.
5
- * The same config and identity always produce the same assignments.
6
- * This controls presentation, never authorization or access to data.
7
- */
8
- export declare function evaluateExperiments(config: ExperimentsConfig, identity: ExperimentsIdentity, preview?: Readonly<Record<string, boolean>>): {
9
- flags: Record<string, boolean>;
10
- assignments: ExperimentAssignment[];
11
- };
@@ -1,45 +0,0 @@
1
- function bucket(parts) {
2
- let hash = 0x811c9dc5;
3
- for (const byte of new TextEncoder().encode(parts.join(":"))) {
4
- hash = Math.imul(hash ^ byte, 0x01000193) >>> 0;
5
- }
6
- return hash % 100;
7
- }
8
- /**
9
- * Evaluates flags locally without storage, network, clock, or browser globals.
10
- * The same config and identity always produce the same assignments.
11
- * This controls presentation, never authorization or access to data.
12
- */
13
- export function evaluateExperiments(config, identity, preview = {}) {
14
- const flags = Object.fromEntries(config.flags.map((flag) => [
15
- flag.key,
16
- bucket(["rollout", config.app_id, flag.key, identity.visitorId]) < flag.rollout_percentage,
17
- ]));
18
- const assignments = [];
19
- for (const experiment of config.experiments) {
20
- if (Object.prototype.hasOwnProperty.call(preview, experiment.flag_key))
21
- continue;
22
- const key = experiment.assign_by === "user" ? identity.userId : identity.visitorId;
23
- if (!key || bucket(["enroll", config.app_id, experiment.id, experiment.run_version, key]) >= experiment.traffic_allocation)
24
- continue;
25
- const value = bucket(["variant", config.app_id, experiment.id, experiment.run_version, key]);
26
- let total = 0;
27
- let variant = experiment.variants[experiment.variants.length - 1];
28
- for (const candidate of experiment.variants) {
29
- total += candidate.weight;
30
- if (value < total) {
31
- variant = candidate;
32
- break;
33
- }
34
- }
35
- flags[experiment.flag_key] = variant.value;
36
- assignments.push({
37
- experiment_id: experiment.id,
38
- flag_key: experiment.flag_key,
39
- run_version: experiment.run_version,
40
- variant_key: variant.key,
41
- preview: false,
42
- });
43
- }
44
- return { flags: { ...flags, ...preview }, assignments };
45
- }