Changeset cfe1824 in Klonkt for src/services


Ignore:
Timestamp:
07/01/2026 12:57:32 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
f0807cc
Parents:
e1c4f3e
Message:

fix(federation): serialize featured-pin resync per site (4fk)

resyncFeaturedPins() runs a long Remove-all -> wait 5s -> Add-in-order
sequence and was fired fire-and-forget from /save. Two concurrent saves on
the same site interleaved their Remove/Add sequences and scrambled the
StatusPin order on Mastodon. Wrap it in a per-site serializer: a resync
already in flight coalesces later requests into one rerun after it finishes
(accumulating their extra unpins), so saves never interleave and rapid saves
don't pile up N full resyncs. Renamed the worker to doResyncFeaturedPins();
the exported resyncFeaturedPins() is now the serializing wrapper.

  • src/services/ActivityPubService.js — per-site resync serializer + coalescing

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    re1c4f3e rcfe1824  
    10621062// then ADD in rank-DESCENDING order (rank 1 added LAST → newest StatusPin → shown first,
    10631063// because Mastodon displays pins newest-first). `alsoRemove` = ids to unpin too.
    1064 export async function resyncFeaturedPins(site, alsoRemove = []) {
     1064// Serialize pin-resyncs per site: two concurrent /save calls would otherwise interleave
     1065// their Remove -> wait -> Add sequences and scramble the StatusPin order on Mastodon. A
     1066// resync already in flight for a site coalesces later requests into ONE rerun after it
     1067// finishes (accumulating their extra unpins), so rapid saves don't pile up N full resyncs.
     1068const _pinResync = new Map(); // slug -> { promise, pending, pendingRemove:Set, site }
     1069export function resyncFeaturedPins(site, alsoRemove = []) {
     1070  if (!site || !site.slug) return Promise.resolve();
     1071  const slug = site.slug;
     1072  const running = _pinResync.get(slug);
     1073  if (running) {
     1074    running.pending = true;
     1075    running.site = site; // use the latest site object on the rerun
     1076    for (const id of alsoRemove) running.pendingRemove.add(id);
     1077    return running.promise;
     1078  }
     1079  const state = { promise: null, pending: false, pendingRemove: new Set(), site };
     1080  state.promise = (async () => {
     1081    let extra = alsoRemove;
     1082    for (;;) {
     1083      try { await doResyncFeaturedPins(state.site, extra); }
     1084      catch (e) { console.warn('[AP] pin resync failed:', e.message); }
     1085      if (!state.pending) break;
     1086      state.pending = false;
     1087      extra = [...state.pendingRemove];
     1088      state.pendingRemove = new Set();
     1089    }
     1090    _pinResync.delete(slug);
     1091  })();
     1092  _pinResync.set(slug, state);
     1093  return state.promise;
     1094}
     1095
     1096// The actual resync work — do NOT call directly; go through resyncFeaturedPins() above so
     1097// it stays serialized per site.
     1098async function doResyncFeaturedPins(site, alsoRemove = []) {
    10651099  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    10661100  if (!base || !site || !site.slug) return;
Note: See TracChangeset for help on using the changeset viewer.