Guard dashboard against contributors that fail to load from IPFS
CI / build (16.x) (pull_request) Has been cancelled

This commit is contained in:
2026-07-25 19:06:18 +02:00
parent dbf4709512
commit c74d70040b
4 changed files with 95 additions and 4 deletions
+11 -2
View File
@@ -19,8 +19,17 @@ export default Component.extend({
src: alias('avatarURL'),
title: alias('contributor.name'),
avatarURL: computed('contributor.github_uid', 'size', function() {
const github_uid = this.contributor.github_uid;
// Re-compute whenever the contributor reference itself changes (covers a
// previously-undefined contributor being lazily loaded later), as well as
// the github_uid property when one is present.
avatarURL: computed('contributor', 'contributor.github_uid', 'size', function() {
const contributor = this.contributor;
if (!contributor) {
return '';
}
const github_uid = contributor.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
+37 -2
View File
@@ -36,6 +36,11 @@ export default Service.extend({
reimbursements: null,
githubAccessToken: null,
// IDs of contributors currently being lazy-fetched (see ensureContributorLoaded),
// used to avoid spawning duplicate concurrent requests when many contributions
// for the same missing contributor are loaded in a tight loop.
contributorsFetching: null,
currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'),
@@ -56,6 +61,7 @@ export default Service.extend({
this.set('contributors', []);
this.set('contributions', []);
this.set('reimbursements', []);
this.set('contributorsFetching', new Set());
if (window.ethereum) {
window.ethereum.on('chainChanged', this.handleUserChainChanged);
@@ -187,10 +193,17 @@ export default Service.extend({
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value);
// Drop groups whose contributor isn't loaded yet (e.g. an IPFS profile
// failed to deserialize). They'll be re-included once the contributor
// gets lazily fetched via loadContributionFromData.
const contributionsGroupedKnown = contributionsGrouped.filter(c => {
return this.contributors.findBy('id', c.value);
});
const contributorsWithUnconfirmed = contributionsGroupedKnown.map(c => c.value);
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
const kreditsByContributor = contributionsGrouped.map(c => {
const kreditsByContributor = contributionsGroupedKnown.map(c => {
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
const contributor = this.contributors.findBy('id', c.value);
@@ -302,6 +315,26 @@ export default Service.extend({
.then(data => this.loadContributorFromData(data))
},
// When a contribution/reimbursement references a contributor that isn't in
// the local collection yet (e.g. its IPFS profile failed to deserialize on a
// previous attempt, or it was added out-of-band), lazily fetch it in the
// background. Fire-and-forget: never blocks the calling loop iteration, and
// any rejection is logged so it doesn't break the run loop. The successful
// arrival of the contributor will retrigger dependent computeds
// (kreditsByContributor, UserAvatar) via the `contributors.[]` dependency.
ensureContributorLoaded (id) {
if (!id) return;
if (this.contributors.findBy('id', id)) return;
if (this.contributorsFetching.has(id)) return;
console.debug(`[kredits] Contributor ${id} not loaded yet; fetching in background`);
this.contributorsFetching.add(id);
this.fetchContributor(id)
.then(() => console.debug(`[kredits] Lazy-loaded contributor ${id}`))
.catch(err => console.warn(`[kredits] Failed to lazy-load contributor ${id}:`, err))
.finally(() => this.contributorsFetching.delete(id));
},
fetchContributors () {
console.debug(`[kredits] Fetching all contributors from the network`);
return this.kredits.Contributor.all()
@@ -385,6 +418,7 @@ export default Service.extend({
const loadedContribution = this.contributions.findBy('id', contribution.id);
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
this.contributions.pushObject(contribution);
this.ensureContributorLoaded(data.contributorId);
return contribution;
},
@@ -620,6 +654,7 @@ export default Service.extend({
obj.set('contributor', this.contributors.findBy('id', data.recipientId));
this.removeObjectFromCollectionIfLoaded('reimbursements', obj.id);
this.reimbursements.pushObject(obj);
this.ensureContributorLoaded(data.recipientId);
return obj;
},