@bunbase-ae/react-sdk 1.0.0 → 1.0.1-next.3.ee6c18d
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 +2 -1
- package/src/cache.ts +89 -8
- package/src/useAuth.ts +1 -0
- package/src/useList.ts +4 -1
- package/src/useRecord.ts +2 -1
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunbase-ae/react-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-next.3.ee6c18d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React hooks for BunBase — caching, mutations, auth, realtime",
|
|
6
|
+
"homepage": "https://docs-bunbase.palmcode.ae/sdk/react",
|
|
6
7
|
"files": [
|
|
7
8
|
"src"
|
|
8
9
|
],
|
package/src/cache.ts
CHANGED
|
@@ -24,6 +24,18 @@ 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;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
// ─── Storage adapter ─────────────────────────────────────────────────────────
|
|
@@ -159,6 +171,10 @@ export class QueryCache {
|
|
|
159
171
|
promise: null,
|
|
160
172
|
fetcher: null,
|
|
161
173
|
staleTime: stored.staleTime,
|
|
174
|
+
retryableError: false,
|
|
175
|
+
consecutiveErrors: 0,
|
|
176
|
+
backoffUntil: 0,
|
|
177
|
+
invalidated: false,
|
|
162
178
|
});
|
|
163
179
|
} catch {
|
|
164
180
|
// Corrupt entry — skip silently.
|
|
@@ -212,10 +228,18 @@ export class QueryCache {
|
|
|
212
228
|
|
|
213
229
|
isStale(key: string): boolean {
|
|
214
230
|
const e = this.entries.get(key);
|
|
215
|
-
if (!e
|
|
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;
|
|
216
235
|
return Date.now() >= e.staleAt;
|
|
217
236
|
}
|
|
218
237
|
|
|
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
|
+
|
|
219
243
|
// ─── Fetch ──────────────────────────────────────────────────────────────────
|
|
220
244
|
|
|
221
245
|
/**
|
|
@@ -235,6 +259,9 @@ export class QueryCache {
|
|
|
235
259
|
// Data is fresh — nothing to do.
|
|
236
260
|
if (existing?.status === "success" && Date.now() < existing.staleAt) return;
|
|
237
261
|
|
|
262
|
+
// Respect exponential backoff after consecutive errors.
|
|
263
|
+
if (existing?.backoffUntil && Date.now() < existing.backoffUntil) return;
|
|
264
|
+
|
|
238
265
|
// Keep existing data visible while refetching (SWR).
|
|
239
266
|
const entry: CacheEntry = {
|
|
240
267
|
data: existing?.data,
|
|
@@ -244,6 +271,10 @@ export class QueryCache {
|
|
|
244
271
|
promise: null,
|
|
245
272
|
fetcher,
|
|
246
273
|
staleTime,
|
|
274
|
+
retryableError: false,
|
|
275
|
+
consecutiveErrors: existing?.consecutiveErrors ?? 0,
|
|
276
|
+
backoffUntil: existing?.backoffUntil ?? 0,
|
|
277
|
+
invalidated: false,
|
|
247
278
|
};
|
|
248
279
|
|
|
249
280
|
const promise: Promise<void> = fetcher().then(
|
|
@@ -259,6 +290,10 @@ export class QueryCache {
|
|
|
259
290
|
promise: null,
|
|
260
291
|
fetcher,
|
|
261
292
|
staleTime,
|
|
293
|
+
retryableError: false,
|
|
294
|
+
consecutiveErrors: 0,
|
|
295
|
+
backoffUntil: 0,
|
|
296
|
+
invalidated: false,
|
|
262
297
|
};
|
|
263
298
|
this.entries.set(key, next);
|
|
264
299
|
this.persist(key, next);
|
|
@@ -268,14 +303,22 @@ export class QueryCache {
|
|
|
268
303
|
(err: unknown) => {
|
|
269
304
|
const current = this.entries.get(key);
|
|
270
305
|
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;
|
|
271
310
|
this.entries.set(key, {
|
|
272
311
|
data: current?.data,
|
|
273
312
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
274
313
|
status: "error",
|
|
275
|
-
staleAt:
|
|
314
|
+
staleAt: backoffUntil,
|
|
276
315
|
promise: null,
|
|
277
316
|
fetcher,
|
|
278
317
|
staleTime,
|
|
318
|
+
retryableError: isRetryableError(err),
|
|
319
|
+
consecutiveErrors,
|
|
320
|
+
backoffUntil,
|
|
321
|
+
invalidated: false,
|
|
279
322
|
});
|
|
280
323
|
this.notify(key);
|
|
281
324
|
},
|
|
@@ -292,7 +335,17 @@ export class QueryCache {
|
|
|
292
335
|
refetch(key: string): void {
|
|
293
336
|
const entry = this.entries.get(key);
|
|
294
337
|
if (!entry?.fetcher) return;
|
|
295
|
-
|
|
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
|
+
});
|
|
296
349
|
this.fetch(key, entry.fetcher, entry.staleTime);
|
|
297
350
|
}
|
|
298
351
|
|
|
@@ -303,13 +356,27 @@ export class QueryCache {
|
|
|
303
356
|
*
|
|
304
357
|
* Data is KEPT so components remain in a loaded state (not loading) while the
|
|
305
358
|
* background re-fetch runs. Subscribers should call `cache.fetch()` when they
|
|
306
|
-
* receive a notification and find the entry is
|
|
359
|
+
* receive a notification and find the entry is invalidated.
|
|
307
360
|
*/
|
|
308
361
|
invalidate(prefix: string): void {
|
|
309
362
|
const toNotify: string[] = [];
|
|
310
363
|
for (const [key, entry] of this.entries) {
|
|
311
364
|
if (key.startsWith(prefix)) {
|
|
312
|
-
this.entries.set(key, {
|
|
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
|
+
});
|
|
313
380
|
toNotify.push(key);
|
|
314
381
|
}
|
|
315
382
|
}
|
|
@@ -331,6 +398,10 @@ export class QueryCache {
|
|
|
331
398
|
promise: null,
|
|
332
399
|
fetcher: null,
|
|
333
400
|
staleTime,
|
|
401
|
+
retryableError: false,
|
|
402
|
+
consecutiveErrors: 0,
|
|
403
|
+
backoffUntil: 0,
|
|
404
|
+
invalidated: false,
|
|
334
405
|
};
|
|
335
406
|
this.entries.set(key, entry);
|
|
336
407
|
this.persist(key, entry);
|
|
@@ -354,6 +425,10 @@ export class QueryCache {
|
|
|
354
425
|
promise: null,
|
|
355
426
|
fetcher: existing?.fetcher ?? null,
|
|
356
427
|
staleTime,
|
|
428
|
+
retryableError: false,
|
|
429
|
+
consecutiveErrors: 0,
|
|
430
|
+
backoffUntil: 0,
|
|
431
|
+
invalidated: false,
|
|
357
432
|
};
|
|
358
433
|
this.entries.set(key, entry);
|
|
359
434
|
this.persist(key, entry);
|
|
@@ -390,14 +465,20 @@ export class QueryCache {
|
|
|
390
465
|
}
|
|
391
466
|
}
|
|
392
467
|
|
|
393
|
-
/** Re-fetch all stale
|
|
468
|
+
/** Re-fetch all stale entries. Called on window focus. */
|
|
394
469
|
refetchStale(): void {
|
|
395
470
|
for (const [key, entry] of this.entries) {
|
|
396
471
|
if (!entry.fetcher) continue;
|
|
397
|
-
|
|
398
|
-
if (
|
|
472
|
+
// Errored entries have staleAt = backoffUntil, so the same check handles them.
|
|
473
|
+
if (!entry.promise && Date.now() >= entry.staleAt) {
|
|
399
474
|
this.fetch(key, entry.fetcher, entry.staleTime);
|
|
400
475
|
}
|
|
401
476
|
}
|
|
402
477
|
}
|
|
403
478
|
}
|
|
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
package/src/useList.ts
CHANGED
|
@@ -74,7 +74,10 @@ export function useList<
|
|
|
74
74
|
useEffect(
|
|
75
75
|
() =>
|
|
76
76
|
cache.subscribe(cacheKey, () => {
|
|
77
|
-
|
|
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)) {
|
|
78
81
|
cache.fetch(cacheKey, () => fetcherRef.current(), staleTimeRef.current);
|
|
79
82
|
}
|
|
80
83
|
rerender();
|
package/src/useRecord.ts
CHANGED
|
@@ -47,7 +47,8 @@ export function useRecord<
|
|
|
47
47
|
useEffect(() => {
|
|
48
48
|
if (!cacheKey) return;
|
|
49
49
|
return cache.subscribe(cacheKey, () => {
|
|
50
|
-
|
|
50
|
+
// Only re-fetch on explicit invalidation — not on error/success notifications.
|
|
51
|
+
if (cacheKey && cache.isInvalidated(cacheKey)) {
|
|
51
52
|
cache.fetch(cacheKey, () => fetcherRef.current(), staleTimeRef.current);
|
|
52
53
|
}
|
|
53
54
|
rerender();
|