Skip to content

Commit a61cf14

Browse files
committed
Add source-of-truth loading state methods (v1.0.12)
The root issue: isLoading flag gets out of sync with actual in-flight requests, especially with reactive frameworks calling .it() multiple times. Solution: Provide direct access to the source of truth - the in-flight request map. New Public API Methods: - isItemLoading(typeName, id, level): Check if specific item is loading - hasAnyInFlightRequests(): Check if ANY requests are in-flight Key Changes: 1. Added isItemLoading() to check G.inFlightItm map directly 2. Added hasAnyInFlightRequests() for global loading state 3. Updated fetchItem() to sync isLoading with actual in-flight state 4. Added in-flight counts to devtools snapshot 5. Exported both methods in public API 6. Documented usage patterns and examples Why This Fixes the Bug: Instead of relying on state flags (isLoading) that can desync during reactive cycles, developers can now query the actual in-flight request map. This is the single source of truth that can't get out of sync. Usage Pattern (Alpine.js): get isLoadingAuth() { return DL.isItemLoading('me', 'current') // Always accurate! } DevTools Support: const snapshot = DL.devtools() snapshot.inFlight.totalCount // Number of in-flight requests snapshot.inFlight.hasAnyInFlight // Boolean snapshot.inFlight.items // Array of in-flight items snapshot.inFlight.collections // Array of in-flight collections Version: 1.0.11 → 1.0.12
1 parent 3e7d1a7 commit a61cf14

2 files changed

Lines changed: 142 additions & 1 deletion

File tree

docs/reference/core.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,123 @@ await updateInvoice(42, { status: 'paid' })
10281028

10291029
---
10301030

1031+
## `isItemLoading(typeName, id, level)`
1032+
1033+
Checks if a specific item is currently being fetched (has an in-flight request). This is the **source of truth** for loading state, checking the actual in-flight request map rather than relying on the `isLoading` flag.
1034+
1035+
### Signature
1036+
1037+
```typescript
1038+
function isItemLoading(
1039+
typeName: string,
1040+
id: any,
1041+
level?: string | null
1042+
): boolean
1043+
```
1044+
1045+
### Parameters
1046+
1047+
- **`typeName`**: The type name (registered via `createType`)
1048+
- **`id`**: The item identifier
1049+
- **`level`** (optional): The level name (null for default level)
1050+
1051+
### Returns: boolean
1052+
1053+
Returns `true` if there is an active fetch in progress for this exact item/level combination, `false` otherwise.
1054+
1055+
### Example
1056+
1057+
```javascript
1058+
// Check if user is loading
1059+
const loading = DL.isItemLoading('user', 123)
1060+
console.log('User 123 is loading:', loading)
1061+
1062+
// Check specific level
1063+
const detailsLoading = DL.isItemLoading('user', 123, 'detailed')
1064+
console.log('User 123 details loading:', detailsLoading)
1065+
1066+
// Use in reactive getters for accurate loading state
1067+
get isLoadingAuth() {
1068+
return DL.isItemLoading('me', 'current')
1069+
}
1070+
```
1071+
1072+
### Why Use This Instead of `ref.meta.isLoading`?
1073+
1074+
The `isLoading` flag can sometimes be out of sync with reality due to timing issues with reactive frameworks. `isItemLoading()` checks the **actual in-flight request map**, which is the single source of truth.
1075+
1076+
```javascript
1077+
// ❌ May be inaccurate with reactive frameworks
1078+
get isLoadingAuth() {
1079+
const meRef = Alpine.store('lib').it('me', 'current')
1080+
return meRef.meta.isLoading
1081+
}
1082+
1083+
// ✅ Always accurate - checks actual in-flight requests
1084+
get isLoadingAuth() {
1085+
return DL.isItemLoading('me', 'current')
1086+
}
1087+
```
1088+
1089+
---
1090+
1091+
## `hasAnyInFlightRequests()`
1092+
1093+
Checks if there are ANY in-flight requests (items or collections) across the entire application. Useful for global loading indicators.
1094+
1095+
### Signature
1096+
1097+
```typescript
1098+
function hasAnyInFlightRequests(): boolean
1099+
```
1100+
1101+
### Returns: boolean
1102+
1103+
Returns `true` if there are any active fetches in progress, `false` if all requests have completed.
1104+
1105+
### Example
1106+
1107+
```javascript
1108+
// Global loading indicator
1109+
const showGlobalSpinner = DL.hasAnyInFlightRequests()
1110+
1111+
// Alpine.js example
1112+
Alpine.store('app', {
1113+
get isLoading() {
1114+
return DL.hasAnyInFlightRequests()
1115+
}
1116+
})
1117+
1118+
// React example
1119+
function GlobalLoadingIndicator() {
1120+
const [isLoading, setIsLoading] = React.useState(false)
1121+
1122+
React.useEffect(() => {
1123+
const interval = setInterval(() => {
1124+
setIsLoading(DL.hasAnyInFlightRequests())
1125+
}, 100)
1126+
1127+
return () => clearInterval(interval)
1128+
}, [])
1129+
1130+
return isLoading ? <Spinner /> : null
1131+
}
1132+
```
1133+
1134+
### Use in DevTools
1135+
1136+
Both methods are also exposed in the devtools snapshot:
1137+
1138+
```javascript
1139+
const snapshot = DL.devtools()
1140+
console.log('In-flight items:', snapshot.inFlight.items)
1141+
console.log('In-flight collections:', snapshot.inFlight.collections)
1142+
console.log('Total in-flight:', snapshot.inFlight.totalCount)
1143+
console.log('Has any in-flight:', snapshot.inFlight.hasAnyInFlight)
1144+
```
1145+
1146+
---
1147+
10311148
## Summary
10321149

10331150
**Core Methods:**
@@ -1040,10 +1157,13 @@ await updateInvoice(42, { status: 'paid' })
10401157
| `fetchItem()` | Fetch single item and get reactive reference |
10411158
| `fetchCollection()` | Fetch collection IDs and get reactive reference |
10421159
| `applyDirectives()` | Apply server directives to invalidate cache |
1160+
| `isItemLoading()` | Check if specific item has in-flight request (source of truth) |
1161+
| `hasAnyInFlightRequests()` | Check if any requests are in-flight globally |
10431162
| `onChange()` | Subscribe to state changes |
10441163
| `onLifecycle()` | Subscribe to lifecycle events |
10451164
| `clientId()` | Get unique client identifier |
10461165
| `state()` | Get state snapshot (debugging) |
1166+
| `devtools()` | Get detailed devtools snapshot |
10471167

10481168
**Configuration:**
10491169

verity/shared/static/lib/core.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,15 @@ function itemKey(typeName, id, levelName) {
12361236
return `${typeName}:${id}:${toLevelKey(levelName)}`;
12371237
}
12381238

1239+
function isItemLoading(typeName, id, levelName = null) {
1240+
const key = itemKey(typeName, id, levelName);
1241+
return G.inFlightItm.has(key);
1242+
}
1243+
1244+
function hasAnyInFlightRequests() {
1245+
return G.inFlightItm.size > 0 || G.inFlightCol.size > 0;
1246+
}
1247+
12391248
function applyFetchedLevel(T, typeName, id, ref, sourceLevelKey, data, timestamp, qid = null, options = {}) {
12401249
let nextData = { ...(ref.data || {}), ...(data || {}) };
12411250
const nextLevelStamps = { ...ref.meta.levelStamps };
@@ -1698,8 +1707,14 @@ function fetchItem(typeName, id, levelName = null, opts = {}) {
16981707
const ref = ensureItemRef(typeName, id);
16991708
ref.meta.lastUsedAt = nowISO();
17001709
scheduleMemorySweep();
1701-
// Don't set isLoading here - let _startItemFetch handle it based on whether fetch is actually needed
17021710
_startItemFetch(typeName, id, levelName, { loud: !opts.silent, force: !!opts.force });
1711+
1712+
// Set isLoading based on actual in-flight state (source of truth)
1713+
const actuallyLoading = isItemLoading(typeName, id, levelName);
1714+
if (ref.meta.isLoading !== actuallyLoading) {
1715+
assignRef(ref, { meta: { ...ref.meta, isLoading: actuallyLoading } });
1716+
}
1717+
17031718
return ref;
17041719
}
17051720

@@ -2038,6 +2053,8 @@ function devtools() {
20382053
inFlight: {
20392054
collections: inFlightCollections,
20402055
items: inFlightItems,
2056+
totalCount: G.inFlightCol.size + G.inFlightItm.size,
2057+
hasAnyInFlight: hasAnyInFlightRequests(),
20412058
},
20422059
directiveRegistry: {
20432060
ttlMs: G.directiveRegistry.ttlMs,
@@ -2121,6 +2138,8 @@ const DLCore = {
21212138
disconnectDirectiveSource,
21222139
disconnectSse,
21232140
ingestDirectiveEnvelope,
2141+
isItemLoading,
2142+
hasAnyInFlightRequests,
21242143
};
21252144

21262145
// ES MODULE EXPORTS (for modern bundlers and ES imports)
@@ -2144,6 +2163,8 @@ if (typeof exports !== "undefined") {
21442163
exports.disconnectDirectiveSource = disconnectDirectiveSource;
21452164
exports.disconnectSse = disconnectSse;
21462165
exports.ingestDirectiveEnvelope = ingestDirectiveEnvelope;
2166+
exports.isItemLoading = isItemLoading;
2167+
exports.hasAnyInFlightRequests = hasAnyInFlightRequests;
21472168
}
21482169

21492170
// UMD/Browser compatibility (for script tag loading)

0 commit comments

Comments
 (0)