Skip to content

Commit

Permalink
Use requestData resources in UserPreferences (#1064)
Browse files Browse the repository at this point in the history
Closes #1044.
  • Loading branch information
matthew-white authored Dec 10, 2024
1 parent f0d4725 commit c9b8bef
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 62 deletions.
10 changes: 6 additions & 4 deletions src/request-data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@ except according to the terms contained in the LICENSE file.
import { computed, reactive, shallowReactive, watchSyncEffect } from 'vue';
import { mergeDeepLeft } from 'ramda';

import UserPreferences from './user-preferences/preferences';
import configDefaults from '../config';
import { computeIfExists, hasVerbs, setupOption, transformForm } from './util';
import { noargs } from '../util/util';
import UserPreferences from './user-preferences/preferences';

export default ({ i18n, http }, createResource) => {
export default (container, createResource) => {
const { i18n } = container;

// Resources related to the session
const session = createResource('session');
createResource('session');
createResource('currentUser', () => ({
transformResponse: ({ data }) => {
/* eslint-disable no-param-reassign */
data.verbs = new Set(data.verbs);
data.can = hasVerbs;
data.preferences = new UserPreferences(data.preferences, session, http);
data.preferences = new UserPreferences(data.preferences, container);
/* eslint-enable no-param-reassign */
return shallowReactive(data);
}
Expand Down
94 changes: 36 additions & 58 deletions src/request-data/user-preferences/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
*/

import { shallowReactive, isReactive } from 'vue';
import { apiPaths, withAuth } from '../../util/request';
import { noop } from '../../util/util';
import { SitePreferenceNormalizer, ProjectPreferenceNormalizer } from './normalizers';

import { SitePreferenceNormalizer, ProjectPreferenceNormalizer } from './normalizers';
import { apiPaths } from '../../util/request';
import { createResource } from '../resource';
import { noop } from '../../util/util';

/*
UserPreferences - for storing user preferences such as the display sort order of listings, etc. The settings are propagated to the
Expand Down Expand Up @@ -49,65 +49,43 @@ mutation. At the backend, the preferences are stored in the `user_site_preferenc
*/

export default class UserPreferences {
#abortControllers;
#instanceID;
#session;
#http;

constructor(preferenceData, session, http) {
this.#abortControllers = {};
this.#instanceID = crypto.randomUUID();
#container;
#resources;

constructor(preferenceData, container) {
this.site = this.#makeSiteProxy(preferenceData.site);
this.projects = this.#makeProjectsProxy(preferenceData.projects);
this.#session = session;
this.#http = http;
this.#container = container;
this.#resources = {};
}

// Creates or deletes a user preference on Backend.
#propagate(k, v, projectId) {
// As we need to be able to have multiple requests in-flight (not canceling eachother), we can't use resource.request() here.
// However, we want to avoid stacking requests for the same key, so we abort preceding requests for the same key, if any.
// Note that because locks are origin-scoped, we use a store instantiation identifier to scope them to this app instance.
const keyLockName = `userPreferences-${this.#instanceID}-keystack-${projectId}-${k}`;
navigator.locks.request(
`userPreferences-${this.instanceID}-lockops`,
() => {
navigator.locks.request(
keyLockName,
{ ifAvailable: true },
(lockForKey) => {
const aborter = new AbortController();
if (!lockForKey) {
// Cancel the preceding HTTP request, a new one supersedes it.
this.#abortControllers[k].abort();
return navigator.locks.request(
keyLockName,
() => {
this.#abortControllers[k] = aborter;
return this.#request(k, v, projectId, aborter);
}
);
}
this.#abortControllers[k] = aborter;
return this.#request(k, v, projectId, aborter);
},
);
return Promise.resolve(); // return asap with a resolved promise so the outer lockops lock gets released; we don't wan't to wait here for the inner keylock-enveloped requests.
}
);
}

#request(k, v, projectId, aborter) {
return this.#http.request(
withAuth(
{
method: (v === null) ? 'DELETE' : 'PUT',
url: (projectId === null) ? apiPaths.userSitePreferences(k) : apiPaths.userProjectPreferences(projectId, k),
data: (v === null) ? undefined : { propertyValue: v },
signal: aborter.signal,
},
this.#session.token
)
).catch(noop); // Preference didn't get persisted to the backend. Too bad! We're not doing any retrying.
// Get or create a resource to send the request. If there is a request in
// progress for the same user preference, then we will reuse that request's
// resource. Doing so will cancel the previous request; the new request
// supersedes it. We want to avoid stacking requests for the same key.
const resourceName = projectId == null
? `userPreference.site.${k}`
: `userPreference.project.${projectId}.${k}`;
if (this.#resources[resourceName] == null)
this.#resources[resourceName] = createResource(this.#container, resourceName);
const resource = this.#resources[resourceName];

resource.request({
method: (v === null) ? 'DELETE' : 'PUT',
url: (projectId === null)
? apiPaths.userSitePreferences(k)
: apiPaths.userProjectPreferences(projectId, k),
data: (v === null) ? undefined : { propertyValue: v },
alert: false
})
.catch(noop) // Preference didn't get persisted to the backend. Too bad! We're not doing any retrying.
.finally(() => {
// Remove the resource from this.#resources unless it is being used for
// a new request.
if (!resource.awaitingResponse) delete this.#resources[resourceName];
});
}

#makeSiteProxy(sitePreferenceData) {
Expand Down

0 comments on commit c9b8bef

Please sign in to comment.