@base44-preview/sdk 0.8.27-pr.171.bb56537 → 0.8.28-pr.173.186369a

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
@@ -122,11 +122,19 @@ export function createClient(config) {
122
122
  userAuthModule.setToken(accessToken);
123
123
  }
124
124
  }
125
+ const userAnalyticsModule = createAnalyticsModule({
126
+ axiosClient,
127
+ serverUrl,
128
+ appId,
129
+ userAuthModule,
130
+ });
125
131
  const userModules = {
126
132
  entities: createEntitiesModule({
127
133
  axios: axiosClient,
128
134
  appId,
129
135
  getSocket,
136
+ subscriptionOptions: options === null || options === void 0 ? void 0 : options.entitySubscriptions,
137
+ trackSubscriptionEvent: userAnalyticsModule.track,
130
138
  }),
131
139
  integrations: createIntegrationsModule(axiosClient, appId),
132
140
  connectors: createUserConnectorsModule(axiosClient, appId),
@@ -152,12 +160,7 @@ export function createClient(config) {
152
160
  }),
153
161
  appLogs: createAppLogsModule(axiosClient, appId),
154
162
  users: createUsersModule(axiosClient, appId),
155
- analytics: createAnalyticsModule({
156
- axiosClient,
157
- serverUrl,
158
- appId,
159
- userAuthModule,
160
- }),
163
+ analytics: userAnalyticsModule,
161
164
  cleanup: () => {
162
165
  userModules.analytics.cleanup();
163
166
  if (socket) {
@@ -170,6 +173,7 @@ export function createClient(config) {
170
173
  axios: serviceRoleAxiosClient,
171
174
  appId,
172
175
  getSocket,
176
+ subscriptionOptions: options === null || options === void 0 ? void 0 : options.entitySubscriptions,
173
177
  }),
174
178
  integrations: createIntegrationsModule(serviceRoleAxiosClient, appId),
175
179
  sso: createSsoModule(serviceRoleAxiosClient, appId),
@@ -1,4 +1,4 @@
1
- import type { EntitiesModule } from "./modules/entities.types.js";
1
+ import type { EntitiesModule, EntitySubscriptionOptions } from "./modules/entities.types.js";
2
2
  import type { IntegrationsModule } from "./modules/integrations.types.js";
3
3
  import type { AuthModule } from "./modules/auth.types.js";
4
4
  import type { SsoModule } from "./modules/sso.types.js";
@@ -15,6 +15,10 @@ export interface CreateClientOptions {
15
15
  * Optional error handler that will be called whenever an API error occurs.
16
16
  */
17
17
  onError?: (error: Error) => void;
18
+ /**
19
+ * Client-side controls for realtime entity subscriptions.
20
+ */
21
+ entitySubscriptions?: EntitySubscriptionOptions;
18
22
  }
19
23
  /**
20
24
  * Configuration for creating a Base44 client.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from
4
4
  export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
5
  export type { Base44Client, CreateClientConfig, CreateClientOptions, Base44ErrorJSON, };
6
6
  export * from "./types.js";
7
- export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
7
+ export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntitySubscriptionOptions, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
8
8
  export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, ChangePasswordParams, ResetPasswordParams, User, } from "./modules/auth.types.js";
9
9
  export type { IntegrationsModule, IntegrationEndpointFunction, CoreIntegrations, InvokeLLMParams, GenerateImageParams, GenerateImageResult, UploadFileParams, UploadFileResult, SendEmailParams, SendEmailResult, ExtractDataFromUploadedFileParams, ExtractDataFromUploadedFileResult, UploadPrivateFileParams, UploadPrivateFileResult, CreateFileSignedUrlParams, CreateFileSignedUrlResult, } from "./modules/integrations.types.js";
10
10
  export type { FunctionsModule, FunctionName, FunctionNameRegistry, } from "./modules/functions.types.js";
@@ -1,5 +1,6 @@
1
1
  import { AxiosInstance } from "axios";
2
- import { EntitiesModule } from "./entities.types";
2
+ import { EntitiesModule, EntitySubscriptionOptions } from "./entities.types";
3
+ import type { TrackEventParams } from "./analytics.types";
3
4
  import { RoomsSocket } from "../utils/socket-utils.js";
4
5
  /**
5
6
  * Configuration for the entities module.
@@ -9,6 +10,8 @@ export interface EntitiesModuleConfig {
9
10
  axios: AxiosInstance;
10
11
  appId: string;
11
12
  getSocket: () => ReturnType<typeof RoomsSocket>;
13
+ subscriptionOptions?: EntitySubscriptionOptions;
14
+ trackSubscriptionEvent?: (params: TrackEventParams) => void;
12
15
  }
13
16
  /**
14
17
  * Creates the entities module for the Base44 SDK.
@@ -1,3 +1,8 @@
1
+ const DEFAULT_MAX_ACTIVE_ENTITY_SUBSCRIPTIONS = 100;
2
+ const DEFAULT_SUBSCRIPTION_CHURN_WARNING_THRESHOLD = 20;
3
+ const DEFAULT_SUBSCRIPTION_CHURN_WINDOW_MS = 60000;
4
+ const DEFAULT_EMPTY_ROOM_GRACE_MS = 1000;
5
+ const ENTITY_SUBSCRIPTION_WARNING_EVENT_NAME = "__entity_subscription_warning__";
1
6
  /**
2
7
  * Creates the entities module for the Base44 SDK.
3
8
  *
@@ -7,6 +12,13 @@
7
12
  */
8
13
  export function createEntitiesModule(config) {
9
14
  const { axios, appId, getSocket } = config;
15
+ const entityHandlers = new Map();
16
+ const subscriptionManager = createEntitySubscriptionManager({
17
+ appId,
18
+ getSocket,
19
+ options: config.subscriptionOptions,
20
+ trackSubscriptionEvent: config.trackSubscriptionEvent,
21
+ });
10
22
  // Using Proxy to dynamically handle entity names
11
23
  return new Proxy({}, {
12
24
  get(target, entityName) {
@@ -16,8 +28,14 @@ export function createEntitiesModule(config) {
16
28
  entityName.startsWith("_")) {
17
29
  return undefined;
18
30
  }
31
+ const cachedHandler = entityHandlers.get(entityName);
32
+ if (cachedHandler) {
33
+ return cachedHandler;
34
+ }
19
35
  // Create entity handler
20
- return createEntityHandler(axios, appId, entityName, getSocket);
36
+ const handler = createEntityHandler(axios, appId, entityName, subscriptionManager);
37
+ entityHandlers.set(entityName, handler);
38
+ return handler;
21
39
  },
22
40
  });
23
41
  }
@@ -41,17 +59,226 @@ function parseRealtimeMessage(dataStr) {
41
59
  return null;
42
60
  }
43
61
  }
62
+ function normalizeEntitySubscriptionOptions(options) {
63
+ return {
64
+ maxActiveSubscriptions: normalizePositiveInteger(options === null || options === void 0 ? void 0 : options.maxActiveSubscriptions, DEFAULT_MAX_ACTIVE_ENTITY_SUBSCRIPTIONS),
65
+ churnWarningThreshold: normalizePositiveInteger(options === null || options === void 0 ? void 0 : options.churnWarningThreshold, DEFAULT_SUBSCRIPTION_CHURN_WARNING_THRESHOLD),
66
+ churnWindowMs: normalizePositiveInteger(options === null || options === void 0 ? void 0 : options.churnWindowMs, DEFAULT_SUBSCRIPTION_CHURN_WINDOW_MS),
67
+ emptyRoomGraceMs: normalizeNonNegativeInteger(options === null || options === void 0 ? void 0 : options.emptyRoomGraceMs, DEFAULT_EMPTY_ROOM_GRACE_MS),
68
+ };
69
+ }
70
+ function normalizePositiveInteger(value, fallback) {
71
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
72
+ return fallback;
73
+ }
74
+ return Math.floor(value);
75
+ }
76
+ function normalizeNonNegativeInteger(value, fallback) {
77
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
78
+ return fallback;
79
+ }
80
+ return Math.floor(value);
81
+ }
82
+ function createEntitySubscriptionManager({ appId, getSocket, options, trackSubscriptionEvent, }) {
83
+ const normalizedOptions = normalizeEntitySubscriptionOptions(options);
84
+ const activeSubscriptions = new Map();
85
+ const churnByRoom = new Map();
86
+ const roomsWarnedForCap = new Set();
87
+ let nextCallbackId = 1;
88
+ function makeRoom(entityName) {
89
+ return `entities:${appId}:${entityName}`;
90
+ }
91
+ function emitWarning(message, properties) {
92
+ console.warn(message);
93
+ try {
94
+ trackSubscriptionEvent === null || trackSubscriptionEvent === void 0 ? void 0 : trackSubscriptionEvent({
95
+ eventName: ENTITY_SUBSCRIPTION_WARNING_EVENT_NAME,
96
+ properties,
97
+ });
98
+ }
99
+ catch (_a) {
100
+ // Diagnostics should never break application code.
101
+ }
102
+ }
103
+ function recordSubscriptionActivity(room, entityName, action) {
104
+ var _a;
105
+ const now = Date.now();
106
+ const churnState = (_a = churnByRoom.get(room)) !== null && _a !== void 0 ? _a : {
107
+ events: [],
108
+ lastWarningAt: null,
109
+ };
110
+ churnState.events = churnState.events.filter((event) => now - event.timestamp <= normalizedOptions.churnWindowMs);
111
+ churnState.events.push({ action, timestamp: now });
112
+ churnByRoom.set(room, churnState);
113
+ const subscribeCount = churnState.events.filter((event) => event.action === "subscribe").length;
114
+ const unsubscribeCount = churnState.events.length - subscribeCount;
115
+ if (churnState.events.length < normalizedOptions.churnWarningThreshold ||
116
+ subscribeCount === 0 ||
117
+ unsubscribeCount === 0 ||
118
+ (churnState.lastWarningAt !== null &&
119
+ now - churnState.lastWarningAt < normalizedOptions.churnWindowMs)) {
120
+ return;
121
+ }
122
+ churnState.lastWarningAt = now;
123
+ emitWarning(`[Base44 SDK] entities.${entityName}.subscribe() is being created and cleaned up repeatedly ` +
124
+ `(${churnState.events.length} subscribe/unsubscribe operations in ` +
125
+ `${normalizedOptions.churnWindowMs}ms). Keep realtime subscriptions in a stable lifecycle to avoid socket churn.`, {
126
+ reason: "subscription_churn",
127
+ app_id: appId,
128
+ entity: entityName,
129
+ room,
130
+ action,
131
+ activity_count: churnState.events.length,
132
+ subscribe_count: subscribeCount,
133
+ unsubscribe_count: unsubscribeCount,
134
+ churn_window_ms: normalizedOptions.churnWindowMs,
135
+ empty_room_grace_ms: normalizedOptions.emptyRoomGraceMs,
136
+ churn_warning_threshold: normalizedOptions.churnWarningThreshold,
137
+ });
138
+ }
139
+ function warnForSubscriptionCap(room, entityName) {
140
+ if (roomsWarnedForCap.has(room)) {
141
+ return;
142
+ }
143
+ roomsWarnedForCap.add(room);
144
+ emitWarning(`[Base44 SDK] Realtime entity subscription cap reached ` +
145
+ `(${normalizedOptions.maxActiveSubscriptions} active entities per SDK client/tab). ` +
146
+ `Skipping entities.${entityName}.subscribe(). Unsubscribe from unused entity subscriptions before subscribing to more entities.`, {
147
+ reason: "active_subscription_cap",
148
+ app_id: appId,
149
+ entity: entityName,
150
+ room,
151
+ active_subscription_count: activeSubscriptions.size,
152
+ max_active_subscriptions: normalizedOptions.maxActiveSubscriptions,
153
+ });
154
+ }
155
+ function closeRoomSubscription(state) {
156
+ clearPendingClose(state);
157
+ state.unsubscribeFromRoom();
158
+ activeSubscriptions.delete(state.room);
159
+ if (activeSubscriptions.size < normalizedOptions.maxActiveSubscriptions) {
160
+ roomsWarnedForCap.clear();
161
+ }
162
+ }
163
+ function clearPendingClose(state) {
164
+ if (!state.closeTimer) {
165
+ return;
166
+ }
167
+ clearTimeout(state.closeTimer);
168
+ state.closeTimer = null;
169
+ }
170
+ function scheduleRoomClose(state) {
171
+ var _a;
172
+ if (state.closeTimer) {
173
+ return;
174
+ }
175
+ if (normalizedOptions.emptyRoomGraceMs === 0) {
176
+ closeRoomSubscription(state);
177
+ return;
178
+ }
179
+ const closeTimer = setTimeout(() => {
180
+ state.closeTimer = null;
181
+ if (state.callbacks.size === 0 &&
182
+ activeSubscriptions.get(state.room) === state) {
183
+ closeRoomSubscription(state);
184
+ }
185
+ }, normalizedOptions.emptyRoomGraceMs);
186
+ (_a = closeTimer.unref) === null || _a === void 0 ? void 0 : _a.call(closeTimer);
187
+ state.closeTimer = closeTimer;
188
+ }
189
+ function dispatchRealtimeMessage(state, dataStr) {
190
+ var _a;
191
+ if (state.callbacks.size === 0) {
192
+ return;
193
+ }
194
+ const event = parseRealtimeMessage(dataStr);
195
+ if (!event) {
196
+ return;
197
+ }
198
+ // Server signals oversize broadcasts with `_oversize: true` on
199
+ // `data`. The wire payload was slimmed to fit under the realtime
200
+ // transport cap, so big string fields arrive as empty strings (or
201
+ // the whole record collapses to a stub). Surface this to the
202
+ // developer console so they know to fetch the full record on
203
+ // demand (e.g. a follow-up entities.X.get(id) call) instead of
204
+ // rendering the slimmed payload directly. Skip on delete events
205
+ // — the record no longer exists.
206
+ if (event.type !== "delete" && ((_a = event.data) === null || _a === void 0 ? void 0 : _a._oversize)) {
207
+ console.error(`[Base44 SDK] Realtime broadcast for ${state.entityName}#${event.id} was oversize and got slimmed for transport. ` +
208
+ `Fields >10 KB are empty and the rest of the record may be a stub. ` +
209
+ `Call \`entities.${state.entityName}.get("${event.id}")\` to fetch the full record.`);
210
+ }
211
+ Array.from(state.callbacks.values()).forEach((callback) => {
212
+ try {
213
+ callback(event);
214
+ }
215
+ catch (error) {
216
+ console.error("[Base44 SDK] Subscription callback error:", error);
217
+ }
218
+ });
219
+ }
220
+ function openRoomSubscription(entityName, room) {
221
+ const state = {
222
+ room,
223
+ entityName,
224
+ callbacks: new Map(),
225
+ unsubscribeFromRoom: () => { },
226
+ closeTimer: null,
227
+ };
228
+ const socket = getSocket();
229
+ state.unsubscribeFromRoom = socket.subscribeToRoom(room, {
230
+ update_model: (msg) => {
231
+ dispatchRealtimeMessage(state, msg.data);
232
+ },
233
+ });
234
+ activeSubscriptions.set(room, state);
235
+ return state;
236
+ }
237
+ return {
238
+ subscribe(entityName, callback) {
239
+ const room = makeRoom(entityName);
240
+ recordSubscriptionActivity(room, entityName, "subscribe");
241
+ let state = activeSubscriptions.get(room);
242
+ if (!state) {
243
+ if (activeSubscriptions.size >=
244
+ normalizedOptions.maxActiveSubscriptions) {
245
+ warnForSubscriptionCap(room, entityName);
246
+ return () => { };
247
+ }
248
+ state = openRoomSubscription(entityName, room);
249
+ }
250
+ else {
251
+ clearPendingClose(state);
252
+ }
253
+ const callbackId = nextCallbackId++;
254
+ state.callbacks.set(callbackId, callback);
255
+ let unsubscribed = false;
256
+ return () => {
257
+ if (unsubscribed) {
258
+ return;
259
+ }
260
+ unsubscribed = true;
261
+ recordSubscriptionActivity(room, entityName, "unsubscribe");
262
+ state.callbacks.delete(callbackId);
263
+ if (state.callbacks.size === 0 &&
264
+ activeSubscriptions.get(room) === state) {
265
+ scheduleRoomClose(state);
266
+ }
267
+ };
268
+ },
269
+ };
270
+ }
44
271
  /**
45
272
  * Creates a handler for a specific entity.
46
273
  *
47
274
  * @param axios - Axios instance
48
275
  * @param appId - Application ID
49
276
  * @param entityName - Entity name
50
- * @param getSocket - Function to get the socket instance
277
+ * @param subscriptionManager - Shared realtime subscription manager
51
278
  * @returns Entity handler with CRUD methods
52
279
  * @internal
53
280
  */
54
- function createEntityHandler(axios, appId, entityName, getSocket) {
281
+ function createEntityHandler(axios, appId, entityName, subscriptionManager) {
55
282
  const baseURL = `/apps/${appId}/entities/${entityName}`;
56
283
  return {
57
284
  // List entities with optional pagination and sorting
@@ -126,38 +353,7 @@ function createEntityHandler(axios, appId, entityName, getSocket) {
126
353
  },
127
354
  // Subscribe to realtime updates
128
355
  subscribe(callback) {
129
- const room = `entities:${appId}:${entityName}`;
130
- // Get the socket and subscribe to the room
131
- const socket = getSocket();
132
- const unsubscribe = socket.subscribeToRoom(room, {
133
- update_model: (msg) => {
134
- var _a;
135
- const event = parseRealtimeMessage(msg.data);
136
- if (!event) {
137
- return;
138
- }
139
- // Server signals oversize broadcasts with `_oversize: true` on
140
- // `data`. The wire payload was slimmed to fit under the realtime
141
- // transport cap, so big string fields arrive as empty strings (or
142
- // the whole record collapses to a stub). Surface this to the
143
- // developer console so they know to fetch the full record on
144
- // demand (e.g. a follow-up entities.X.get(id) call) instead of
145
- // rendering the slimmed payload directly. Skip on delete events
146
- // — the record no longer exists.
147
- if (event.type !== "delete" && ((_a = event.data) === null || _a === void 0 ? void 0 : _a._oversize)) {
148
- console.error(`[Base44 SDK] Realtime broadcast for ${entityName}#${event.id} was oversize and got slimmed for transport. ` +
149
- `Fields >10 KB are empty and the rest of the record may be a stub. ` +
150
- `Call \`entities.${entityName}.get("${event.id}")\` to fetch the full record.`);
151
- }
152
- try {
153
- callback(event);
154
- }
155
- catch (error) {
156
- console.error("[Base44 SDK] Subscription callback error:", error);
157
- }
158
- },
159
- });
160
- return unsubscribe;
356
+ return subscriptionManager.subscribe(entityName, callback);
161
357
  },
162
358
  };
163
359
  }
@@ -23,6 +23,42 @@ export interface RealtimeEvent<T = any> {
23
23
  * @typeParam T - The entity type for the event data. Defaults to `any`.
24
24
  */
25
25
  export type RealtimeCallback<T = any> = (event: RealtimeEvent<T>) => void;
26
+ /**
27
+ * Client-side controls for realtime entity subscriptions.
28
+ */
29
+ export interface EntitySubscriptionOptions {
30
+ /**
31
+ * Maximum number of distinct active entity realtime subscriptions allowed
32
+ * for one SDK client instance. Repeated subscriptions to the same entity
33
+ * share one active entity subscription and do not count again.
34
+ *
35
+ * @defaultValue `100`
36
+ */
37
+ maxActiveSubscriptions?: number;
38
+ /**
39
+ * Number of subscribe/unsubscribe operations for the same entity within
40
+ * `churnWindowMs` before the SDK logs a warning and emits diagnostic
41
+ * telemetry.
42
+ *
43
+ * @defaultValue `20`
44
+ */
45
+ churnWarningThreshold?: number;
46
+ /**
47
+ * Time window, in milliseconds, used to detect repeated subscribe/unsubscribe
48
+ * churn for one entity.
49
+ *
50
+ * @defaultValue `60000`
51
+ */
52
+ churnWindowMs?: number;
53
+ /**
54
+ * Grace period, in milliseconds, before the SDK leaves an entity realtime
55
+ * room after its last local callback unsubscribes. A new subscription to
56
+ * the same entity during this window reuses the existing room membership.
57
+ *
58
+ * @defaultValue `1000`
59
+ */
60
+ emptyRoomGraceMs?: number;
61
+ }
26
62
  /**
27
63
  * Result returned when deleting a single entity.
28
64
  */
@@ -88,7 +88,8 @@ export function RoomsSocket({ config }) {
88
88
  return (_a = handlers.update_model) === null || _a === void 0 ? void 0 : _a.call(handlers, { room, data: dataStr });
89
89
  }
90
90
  function getListeners(room) {
91
- return roomsToListeners[room];
91
+ var _a;
92
+ return (_a = roomsToListeners[room]) !== null && _a !== void 0 ? _a : [];
92
93
  }
93
94
  const subscribeToRoom = (room, handlers) => {
94
95
  if (!roomsToListeners[room]) {
@@ -102,6 +103,7 @@ export function RoomsSocket({ config }) {
102
103
  (_b = (_a = roomsToListeners[room]) === null || _a === void 0 ? void 0 : _a.filter((listener) => listener !== handlers)) !== null && _b !== void 0 ? _b : [];
103
104
  if (roomsToListeners[room].length === 0) {
104
105
  leaveRoom(room);
106
+ delete roomsToListeners[room];
105
107
  }
106
108
  };
107
109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.27-pr.171.bb56537",
3
+ "version": "0.8.28-pr.173.186369a",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",