Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: race conditions when fetching subscribed schema

Hugo 69138bac 30832530

+41 -8
+41 -8
app/islands/AutomationForm.tsx
··· 407 407 const [nsidSuggestions, setNsidSuggestions] = useState<string[]>([]); 408 408 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 409 409 const suggestDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 410 + const abortRef = useRef<AbortController | null>(null); 410 411 const lastSuggestPrefix = useRef(""); 411 412 const [datalistId] = useState(() => `nsid-${Math.random().toString(36).slice(2, 8)}`); 412 413 const initialFetched = useRef(false); ··· 414 415 const fetchTargetSchema = useCallback((nsid: string) => { 415 416 if (debounceRef.current) clearTimeout(debounceRef.current); 416 417 if (!nsid || !NSID_RE.test(nsid)) { 418 + abortRef.current?.abort(); 417 419 setTargetSchema(null); 418 420 setTargetSchemaError(""); 419 421 return; 420 422 } 421 423 debounceRef.current = setTimeout(async () => { 424 + abortRef.current?.abort(); 425 + const ctrl = new AbortController(); 426 + abortRef.current = ctrl; 422 427 setTargetSchemaLoading(true); 423 428 setTargetSchemaError(""); 424 429 try { 425 - const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`); 430 + const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`, { 431 + signal: ctrl.signal, 432 + }); 426 433 const data = await res.json(); 427 434 if (!res.ok) { 428 435 setTargetSchemaError(data.error || "Failed to load schema"); ··· 431 438 setTargetSchema(data.record ?? null); 432 439 if (!data.record) setTargetSchemaError("No record schema found for this collection"); 433 440 } 434 - } catch { 441 + } catch (err) { 442 + if ((err as Error).name === "AbortError") return; 435 443 setTargetSchemaError("Failed to fetch target collection schema"); 436 444 setTargetSchema(null); 437 445 } finally { 438 - setTargetSchemaLoading(false); 446 + if (abortRef.current === ctrl) { 447 + abortRef.current = null; 448 + setTargetSchemaLoading(false); 449 + } 439 450 } 440 451 }, 400); 441 452 }, []); ··· 1406 1417 const [triggerRecordSchema, setTriggerRecordSchema] = useState<RecordSchema | null>(null); 1407 1418 const [fetchSchemas, setFetchSchemas] = useState<Record<string, FetchSchemaState>>({}); 1408 1419 const fetchSchemaDebounceRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map()); 1420 + const fetchSchemaAbortRef = useRef<Map<string, AbortController>>(new Map()); 1409 1421 const [conditions, setConditions] = useState<Condition[]>( 1410 1422 initial ? toConditionDrafts(initial.conditions) : [], 1411 1423 ); ··· 1422 1434 const savedRef = useRef(false); 1423 1435 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 1424 1436 const suggestDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 1437 + const abortRef = useRef<AbortController | null>(null); 1425 1438 const lastSuggestPrefix = useRef(""); 1426 1439 const initialFetched = useRef(false); 1427 1440 ··· 1462 1475 const fetchFields = useCallback((nsid: string, updateUrl = true) => { 1463 1476 if (debounceRef.current) clearTimeout(debounceRef.current); 1464 1477 if (!nsid) { 1478 + abortRef.current?.abort(); 1465 1479 setFields([]); 1466 1480 setFieldsError(""); 1467 1481 setSchemaUnresolved(false); ··· 1479 1493 history.replaceState(null, "", url); 1480 1494 } 1481 1495 if (!NSID_RE.test(nsid)) { 1496 + abortRef.current?.abort(); 1482 1497 setFields([]); 1483 1498 setFieldsError(""); 1484 1499 return; 1485 1500 } 1486 1501 debounceRef.current = setTimeout(async () => { 1502 + abortRef.current?.abort(); 1503 + const ctrl = new AbortController(); 1504 + abortRef.current = ctrl; 1487 1505 setFieldsLoading(true); 1488 1506 setFieldsError(""); 1489 1507 try { 1490 - const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`); 1508 + const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`, { 1509 + signal: ctrl.signal, 1510 + }); 1491 1511 const data = await res.json(); 1492 1512 if (!res.ok) { 1493 1513 if (res.status === 404) { ··· 1504 1524 setFields(data.fields || []); 1505 1525 setTriggerRecordSchema(data.record ?? null); 1506 1526 } 1507 - } catch { 1527 + } catch (err) { 1528 + if ((err as Error).name === "AbortError") return; 1508 1529 setSchemaUnresolved(false); 1509 1530 setFieldsError("Failed to fetch lexicon fields"); 1510 1531 setFields([]); 1511 1532 setTriggerRecordSchema(null); 1512 1533 } finally { 1513 - setFieldsLoading(false); 1534 + if (abortRef.current === ctrl) { 1535 + abortRef.current = null; 1536 + setFieldsLoading(false); 1537 + } 1514 1538 } 1515 1539 }, 400); 1516 1540 }, []); ··· 1542 1566 return { ...prev, [nsid]: { loading: true, unresolved: false, error: "", fields: [] } }; 1543 1567 }); 1544 1568 const debounces = fetchSchemaDebounceRef.current!; 1569 + const aborts = fetchSchemaAbortRef.current!; 1545 1570 const existing = debounces.get(nsid); 1546 1571 if (existing) clearTimeout(existing); 1547 1572 const handle = setTimeout(async () => { 1573 + aborts.get(nsid)?.abort(); 1574 + const ctrl = new AbortController(); 1575 + aborts.set(nsid, ctrl); 1548 1576 try { 1549 - const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`); 1577 + const res = await fetch(`/api/lexicons/${encodeURIComponent(nsid)}?schema=record`, { 1578 + signal: ctrl.signal, 1579 + }); 1550 1580 const data = await res.json(); 1551 1581 if (!res.ok) { 1552 1582 setFetchSchemas((prev) => ({ ··· 1570 1600 }, 1571 1601 })); 1572 1602 } 1573 - } catch { 1603 + } catch (err) { 1604 + if ((err as Error).name === "AbortError") return; 1574 1605 setFetchSchemas((prev) => ({ 1575 1606 ...prev, 1576 1607 [nsid]: { ··· 1580 1611 fields: [], 1581 1612 }, 1582 1613 })); 1614 + } finally { 1615 + if (aborts.get(nsid) === ctrl) aborts.delete(nsid); 1583 1616 } 1584 1617 }, 400); 1585 1618 debounces.set(nsid, handle);