@bunbase-ae/react-sdk 1.0.1-next.6.9b19ef6 → 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 +2 -3
- package/src/cache.ts +8 -89
- package/src/useAuth.ts +0 -1
- package/src/useList.ts +1 -4
- package/src/useRecord.ts +1 -2
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunbase-ae/react-sdk",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
if (
|
|
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
package/src/useList.ts
CHANGED
|
@@ -74,10 +74,7 @@ export function useList<
|
|
|
74
74
|
useEffect(
|
|
75
75
|
() =>
|
|
76
76
|
cache.subscribe(cacheKey, () => {
|
|
77
|
-
|
|
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
|
-
|
|
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();
|