@bunbase-ae/react-sdk 1.0.1-next.7.bc3dec2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@bunbase-ae/react-sdk",
3
- "version": "1.0.1-next.7.bc3dec2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "React hooks for BunBase — caching, mutations, auth, realtime",
6
- "homepage": "https://docs-bunbase.palmcode.ae/sdk/react",
7
6
  "files": [
8
7
  "src"
9
8
  ],
@@ -17,7 +16,7 @@
17
16
  "peerDependencies": {
18
17
  "react": ">=19",
19
18
  "react-dom": ">=19",
20
- "@bunbase-ae/js": ">=1.0.0"
19
+ "@bunbase-ae/js": ">=1.1.0"
21
20
  },
22
21
  "devDependencies": {
23
22
  "@types/react": "19.2.14",
package/src/cache.ts CHANGED
@@ -24,18 +24,6 @@ export interface CacheEntry {
24
24
  /** Stored so focus-triggered refetch can re-run without external reference. */
25
25
  fetcher: (() => Promise<unknown>) | null;
26
26
  staleTime: number;
27
- retryableError: boolean;
28
- /** Number of consecutive fetch failures — used to compute exponential backoff. */
29
- consecutiveErrors: number;
30
- /** Epoch ms before which fetch() is a no-op (exponential backoff window). */
31
- backoffUntil: number;
32
- /**
33
- * True when this entry was explicitly invalidated (via `invalidate()`) and has
34
- * not yet been re-fetched. Subscription callbacks use this flag to distinguish
35
- * "app-triggered invalidation → fetch" from "settled fetch → re-render only",
36
- * which prevents error/success notifications from spawning a new fetch cycle.
37
- */
38
- invalidated: boolean;
39
27
  }
40
28
 
41
29
  // ─── Storage adapter ─────────────────────────────────────────────────────────
@@ -171,10 +159,6 @@ export class QueryCache {
171
159
  promise: null,
172
160
  fetcher: null,
173
161
  staleTime: stored.staleTime,
174
- retryableError: false,
175
- consecutiveErrors: 0,
176
- backoffUntil: 0,
177
- invalidated: false,
178
162
  });
179
163
  } catch {
180
164
  // Corrupt entry — skip silently.
@@ -228,18 +212,10 @@ export class QueryCache {
228
212
 
229
213
  isStale(key: string): boolean {
230
214
  const e = this.entries.get(key);
231
- if (!e) return true;
232
- // Errored entries have staleAt = backoffUntil; they are stale once the backoff expires.
233
- if (e.status === "error") return Date.now() >= e.staleAt;
234
- if (e.status !== "success") return true;
215
+ if (!e || e.status !== "success") return true;
235
216
  return Date.now() >= e.staleAt;
236
217
  }
237
218
 
238
- /** True only when this entry was explicitly invalidated and hasn't been re-fetched yet. */
239
- isInvalidated(key: string): boolean {
240
- return this.entries.get(key)?.invalidated === true;
241
- }
242
-
243
219
  // ─── Fetch ──────────────────────────────────────────────────────────────────
244
220
 
245
221
  /**
@@ -259,9 +235,6 @@ export class QueryCache {
259
235
  // Data is fresh — nothing to do.
260
236
  if (existing?.status === "success" && Date.now() < existing.staleAt) return;
261
237
 
262
- // Respect exponential backoff after consecutive errors.
263
- if (existing?.backoffUntil && Date.now() < existing.backoffUntil) return;
264
-
265
238
  // Keep existing data visible while refetching (SWR).
266
239
  const entry: CacheEntry = {
267
240
  data: existing?.data,
@@ -271,10 +244,6 @@ export class QueryCache {
271
244
  promise: null,
272
245
  fetcher,
273
246
  staleTime,
274
- retryableError: false,
275
- consecutiveErrors: existing?.consecutiveErrors ?? 0,
276
- backoffUntil: existing?.backoffUntil ?? 0,
277
- invalidated: false,
278
247
  };
279
248
 
280
249
  const promise: Promise<void> = fetcher().then(
@@ -290,10 +259,6 @@ export class QueryCache {
290
259
  promise: null,
291
260
  fetcher,
292
261
  staleTime,
293
- retryableError: false,
294
- consecutiveErrors: 0,
295
- backoffUntil: 0,
296
- invalidated: false,
297
262
  };
298
263
  this.entries.set(key, next);
299
264
  this.persist(key, next);
@@ -303,22 +268,14 @@ export class QueryCache {
303
268
  (err: unknown) => {
304
269
  const current = this.entries.get(key);
305
270
  if (current?.promise !== promise) return;
306
- const consecutiveErrors = (current.consecutiveErrors ?? 0) + 1;
307
- // Exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s cap.
308
- const backoffMs = Math.min(1_000 * 2 ** (consecutiveErrors - 1), 30_000);
309
- const backoffUntil = Date.now() + backoffMs;
310
271
  this.entries.set(key, {
311
272
  data: current?.data,
312
273
  error: err instanceof Error ? err : new Error(String(err)),
313
274
  status: "error",
314
- staleAt: backoffUntil,
275
+ staleAt: 0,
315
276
  promise: null,
316
277
  fetcher,
317
278
  staleTime,
318
- retryableError: isRetryableError(err),
319
- consecutiveErrors,
320
- backoffUntil,
321
- invalidated: false,
322
279
  });
323
280
  this.notify(key);
324
281
  },
@@ -335,17 +292,7 @@ export class QueryCache {
335
292
  refetch(key: string): void {
336
293
  const entry = this.entries.get(key);
337
294
  if (!entry?.fetcher) return;
338
- // Clear backoff and reset error state so the forced refetch always proceeds.
339
- this.entries.set(key, {
340
- ...entry,
341
- error: null,
342
- status: entry.data !== undefined ? "success" : "loading",
343
- staleAt: 0,
344
- promise: null,
345
- retryableError: false,
346
- backoffUntil: 0,
347
- invalidated: true,
348
- });
295
+ this.entries.set(key, { ...entry, staleAt: 0, promise: null });
349
296
  this.fetch(key, entry.fetcher, entry.staleTime);
350
297
  }
351
298
 
@@ -356,27 +303,13 @@ export class QueryCache {
356
303
  *
357
304
  * Data is KEPT so components remain in a loaded state (not loading) while the
358
305
  * background re-fetch runs. Subscribers should call `cache.fetch()` when they
359
- * receive a notification and find the entry is invalidated.
306
+ * receive a notification and find the entry is stale.
360
307
  */
361
308
  invalidate(prefix: string): void {
362
309
  const toNotify: string[] = [];
363
310
  for (const [key, entry] of this.entries) {
364
311
  if (key.startsWith(prefix)) {
365
- this.entries.set(key, {
366
- ...entry,
367
- error: null,
368
- status:
369
- entry.status === "error"
370
- ? entry.data !== undefined
371
- ? "success"
372
- : "loading"
373
- : entry.status,
374
- staleAt: 0,
375
- promise: null,
376
- retryableError: false,
377
- backoffUntil: 0,
378
- invalidated: true,
379
- });
312
+ this.entries.set(key, { ...entry, staleAt: 0, promise: null });
380
313
  toNotify.push(key);
381
314
  }
382
315
  }
@@ -398,10 +331,6 @@ export class QueryCache {
398
331
  promise: null,
399
332
  fetcher: null,
400
333
  staleTime,
401
- retryableError: false,
402
- consecutiveErrors: 0,
403
- backoffUntil: 0,
404
- invalidated: false,
405
334
  };
406
335
  this.entries.set(key, entry);
407
336
  this.persist(key, entry);
@@ -425,10 +354,6 @@ export class QueryCache {
425
354
  promise: null,
426
355
  fetcher: existing?.fetcher ?? null,
427
356
  staleTime,
428
- retryableError: false,
429
- consecutiveErrors: 0,
430
- backoffUntil: 0,
431
- invalidated: false,
432
357
  };
433
358
  this.entries.set(key, entry);
434
359
  this.persist(key, entry);
@@ -465,20 +390,14 @@ export class QueryCache {
465
390
  }
466
391
  }
467
392
 
468
- /** Re-fetch all stale entries. Called on window focus. */
393
+ /** Re-fetch all stale or errored entries. Called on window focus. */
469
394
  refetchStale(): void {
470
395
  for (const [key, entry] of this.entries) {
471
396
  if (!entry.fetcher) continue;
472
- // Errored entries have staleAt = backoffUntil, so the same check handles them.
473
- if (!entry.promise && Date.now() >= entry.staleAt) {
397
+ const isStale = entry.status === "error" || Date.now() >= entry.staleAt;
398
+ if (isStale && !entry.promise) {
474
399
  this.fetch(key, entry.fetcher, entry.staleTime);
475
400
  }
476
401
  }
477
402
  }
478
403
  }
479
-
480
- function isRetryableError(error: unknown): boolean {
481
- if (!error || typeof error !== "object" || !("status" in error)) return true;
482
- const status = (error as { status?: unknown }).status;
483
- return typeof status !== "number" || status < 400 || status >= 500;
484
- }
package/src/useAuth.ts CHANGED
@@ -170,6 +170,5 @@ export function useAuth<T>(selector?: (s: UseAuthResult) => T): UseAuthResult |
170
170
  return useSyncExternalStore(
171
171
  useCallback((fn) => client.auth.subscribeSnapshot(fn), [client]),
172
172
  getSnapshot,
173
- getSnapshot,
174
173
  ) as UseAuthResult | T;
175
174
  }
package/src/useList.ts CHANGED
@@ -74,10 +74,7 @@ export function useList<
74
74
  useEffect(
75
75
  () =>
76
76
  cache.subscribe(cacheKey, () => {
77
- // Only re-fetch when the entry was explicitly invalidated (invalidate() /
78
- // refetch() / interval). Do NOT re-fetch on error or success notifications —
79
- // that would create a tight retry loop bypassing refetchInterval and backoff.
80
- if (enabledRef.current && cache.isInvalidated(cacheKey)) {
77
+ if (enabledRef.current && cache.isStale(cacheKey)) {
81
78
  cache.fetch(cacheKey, () => fetcherRef.current(), staleTimeRef.current);
82
79
  }
83
80
  rerender();
package/src/useRecord.ts CHANGED
@@ -47,8 +47,7 @@ export function useRecord<
47
47
  useEffect(() => {
48
48
  if (!cacheKey) return;
49
49
  return cache.subscribe(cacheKey, () => {
50
- // Only re-fetch on explicit invalidation — not on error/success notifications.
51
- if (cacheKey && cache.isInvalidated(cacheKey)) {
50
+ if (cacheKey && cache.isStale(cacheKey)) {
52
51
  cache.fetch(cacheKey, () => fetcherRef.current(), staleTimeRef.current);
53
52
  }
54
53
  rerender();