Skip to content

Commit ec10700

Browse files
theunrealclaude
andauthored
feat(entities): log console.error when realtime broadcast is oversize (#167)
* feat(entities): auto-refetch when realtime broadcast is truncated Server-side BASE-40236 slims oversize entity broadcasts to fit under the Redis pubsub cap, signaling the slim with `_truncated: true` on the event data. Customer apps that render the truncated fields would otherwise display placeholder text until refresh. The SDK now detects `_truncated` in `entities.X.subscribe` and transparently refetches the full record over HTTP before invoking the user callback, so deployed customer code keeps working without changes. Concurrent subscribers in the same browser fan out to a single HTTP call via an in-flight debounce keyed by `${entityName}:${id}:${timestamp}`. On refetch failure the SDK falls through with the partial payload and logs a warning, so the failure mode is no worse than today's drop-and-stale. Delete events skip refetch — the record no longer exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(entities): rename _truncated → _oversize for stub flag The wire flag is set only when the server falls back to a stub payload because the original record was too big for realtime transport. Calling it "truncated" was misleading — we don't truncate fields in that path, we replace the whole payload with `{id, _oversize: true}`. `_oversize` names the actual cause and tells the SDK why a refetch is needed. Coordinated with the matching backend rename. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(entities): replace auto-refetch with console.error on _oversize Per PM pushback, drop the HTTP auto-refetch path on oversize broadcasts — even with debouncing, an entity that tens of subscribers all refetch on every broadcast (e.g. a popular Truck record updated every 30 s in a fleet app) shifts realtime load to HTTP at potentially uncapped fan-out. New behavior: when `_oversize: true` is seen, log a single console.error naming the entity, id, and the recommended `entities.X.get(id)` call so the developer is notified at debug time. The user callback still fires with the slimmed payload (big string fields are empty, rest of the record may be the stub) — caller decides what to do. Removed the inflightRefetches debounce map and refetchTruncated helper since they're no longer needed. Tests collapse from 5 refetch scenarios to 3 logging scenarios: - logs and passes the stub through when _oversize is true - does NOT log on delete events even if _oversize is set - does NOT log when _oversize is absent 156/156 pass; lint + build clean. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5db5b5b commit ec10700

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/modules/entities.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,22 @@ function createEntityHandler<T = any>(
197197
return;
198198
}
199199

200+
// Server signals oversize broadcasts with `_oversize: true` on
201+
// `data`. The wire payload was slimmed to fit under the realtime
202+
// transport cap, so big string fields arrive as empty strings (or
203+
// the whole record collapses to a stub). Surface this to the
204+
// developer console so they know to fetch the full record on
205+
// demand (e.g. a follow-up entities.X.get(id) call) instead of
206+
// rendering the slimmed payload directly. Skip on delete events
207+
// — the record no longer exists.
208+
if (event.type !== "delete" && (event.data as any)?._oversize) {
209+
console.error(
210+
`[Base44 SDK] Realtime broadcast for ${entityName}#${event.id} was oversize and got slimmed for transport. ` +
211+
`Fields >10 KB are empty and the rest of the record may be a stub. ` +
212+
`Call \`entities.${entityName}.get("${event.id}")\` to fetch the full record.`
213+
);
214+
}
215+
200216
try {
201217
callback(event);
202218
} catch (error) {

tests/unit/entities-subscribe.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,116 @@ describe("Entities Module - subscribe()", () => {
207207
warnSpy.mockRestore();
208208
});
209209

210+
describe("oversize broadcast handling", () => {
211+
test("logs a console.error and passes the stub through when data._oversize is true", () => {
212+
const mockSocket = createMockSocket();
213+
const mockAxios = createMockAxios();
214+
const entities = createEntitiesModule({
215+
axios: mockAxios as any,
216+
appId,
217+
getSocket: () => mockSocket as any,
218+
});
219+
220+
const callback = vi.fn();
221+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
222+
223+
entities.Todo.subscribe(callback);
224+
225+
mockSocket._simulateMessage(`entities:${appId}:Todo`, {
226+
room: `entities:${appId}:Todo`,
227+
data: JSON.stringify({
228+
type: "update",
229+
data: { id: "123", _oversize: true },
230+
id: "123",
231+
timestamp: "2024-01-01T00:00:00.000Z",
232+
}),
233+
});
234+
235+
// No HTTP call — the SDK never auto-refetches.
236+
expect(mockAxios.get).not.toHaveBeenCalled();
237+
// Developer is notified via console.error.
238+
expect(errorSpy).toHaveBeenCalledWith(
239+
expect.stringContaining("[Base44 SDK] Realtime broadcast for Todo#123 was oversize")
240+
);
241+
// Callback still fires with the slimmed payload — caller decides what to do.
242+
expect(callback).toHaveBeenCalledWith(
243+
expect.objectContaining({
244+
type: "update",
245+
id: "123",
246+
data: { id: "123", _oversize: true },
247+
})
248+
);
249+
250+
errorSpy.mockRestore();
251+
});
252+
253+
test("does NOT log on delete events even if _oversize is set", () => {
254+
const mockSocket = createMockSocket();
255+
const mockAxios = createMockAxios();
256+
const entities = createEntitiesModule({
257+
axios: mockAxios as any,
258+
appId,
259+
getSocket: () => mockSocket as any,
260+
});
261+
262+
const callback = vi.fn();
263+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
264+
265+
entities.Todo.subscribe(callback);
266+
267+
mockSocket._simulateMessage(`entities:${appId}:Todo`, {
268+
room: `entities:${appId}:Todo`,
269+
data: JSON.stringify({
270+
type: "delete",
271+
data: { id: "123", _oversize: true },
272+
id: "123",
273+
timestamp: "2024-01-01T00:00:00.000Z",
274+
}),
275+
});
276+
277+
expect(errorSpy).not.toHaveBeenCalled();
278+
expect(callback).toHaveBeenCalledWith(
279+
expect.objectContaining({ type: "delete", id: "123" })
280+
);
281+
282+
errorSpy.mockRestore();
283+
});
284+
285+
test("does NOT log when data has no _oversize flag", () => {
286+
const mockSocket = createMockSocket();
287+
const mockAxios = createMockAxios();
288+
const entities = createEntitiesModule({
289+
axios: mockAxios as any,
290+
appId,
291+
getSocket: () => mockSocket as any,
292+
});
293+
294+
const callback = vi.fn();
295+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
296+
297+
entities.Todo.subscribe(callback);
298+
299+
mockSocket._simulateMessage(`entities:${appId}:Todo`, {
300+
room: `entities:${appId}:Todo`,
301+
data: JSON.stringify({
302+
type: "update",
303+
data: { id: "123", title: "Normal Todo" },
304+
id: "123",
305+
timestamp: "2024-01-01T00:00:00.000Z",
306+
}),
307+
});
308+
309+
expect(errorSpy).not.toHaveBeenCalled();
310+
expect(callback).toHaveBeenCalledWith(
311+
expect.objectContaining({
312+
data: { id: "123", title: "Normal Todo" },
313+
})
314+
);
315+
316+
errorSpy.mockRestore();
317+
});
318+
});
319+
210320
test("subscribe() should catch and log errors thrown by callback", () => {
211321
const mockSocket = createMockSocket();
212322
const mockAxios = createMockAxios();

0 commit comments

Comments
 (0)