Commit 5cfa841c authored by Robert Knight's avatar Robert Knight

Make sidebar/services typecheck with `noImplicitAny`

Fix remaining errors needed to make this directory typecheck with
`noImplicitAny`.

 - Add missing types

 - Use `Object.entries` instead of `for..in` for record iteration in
   several places. Add an `entries` utility to make iteration over
   `Record<Key, Value>` types more convenient where `Key` is more
   specific than just a string.

 - Change localStorage fallback to use a Map rather than object for
   temporary data storage
parent b495a4a0
......@@ -170,6 +170,8 @@ export class AnnotationsService {
/**
* Delete an annotation via the API and update the store.
*
* @param {SavedAnnotation} annotation
*/
async delete(annotation) {
await this._api.annotation.delete({ id: annotation.id });
......@@ -232,6 +234,7 @@ export class AnnotationsService {
eventType = 'update';
}
/** @type {Annotation} */
let savedAnnotation;
this._store.annotationSaveStarted(annotation);
try {
......@@ -241,11 +244,14 @@ export class AnnotationsService {
this._store.annotationSaveFinished(annotation);
}
Object.keys(annotation).forEach(key => {
if (key[0] === '$') {
savedAnnotation[key] = annotation[key];
// Copy local/internal fields from the original annotation to the saved
// version.
for (let [key, value] of Object.entries(annotation)) {
if (key.startsWith('$')) {
const fields = /** @type {Record<string, any>} */ (savedAnnotation);
fields[key] = value;
}
}
});
// Clear out any pending changes (draft)
this._store.removeDraft(annotation);
......
......@@ -85,7 +85,7 @@ function findRouteMetadata(routeMap, route) {
/** @type {RouteMap|RouteMetadata} */
let cursor = routeMap;
for (let segment of route.split('.')) {
cursor = cursor[segment];
cursor = /** @type {RouteMap} */ (cursor)[segment];
if (!cursor) {
break;
}
......
......@@ -33,6 +33,7 @@ export class AuthService extends TinyEmitter {
* @param {import('./api-routes').APIRoutesService} apiRoutes
* @param {import('./local-storage').LocalStorageService} localStorage
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger
* @param {import('../../types/config').SidebarSettings} settings
*/
constructor($window, apiRoutes, localStorage, settings, toastMessenger) {
super();
......@@ -62,6 +63,8 @@ export class AuthService extends TinyEmitter {
/**
* Show an error message telling the user that the access token has expired.
*
* @param {string} message
*/
function showAccessTokenExpiredErrorMessage(message) {
toastMessenger.error(`Hypothesis login lost: ${message}`, {
......@@ -111,6 +114,8 @@ export class AuthService extends TinyEmitter {
/**
* Persist access & refresh tokens for future use.
*
* @param {TokenInfo} token
*/
function saveToken(token) {
localStorage.setObject(storageKey(), token);
......
......@@ -61,6 +61,7 @@ export class GroupsService {
* @param {import('./api').APIService} api
* @param {import('./auth').AuthService} auth
* @param {import('./session').SessionService} session
* @param {import('../../types/config').SidebarSettings} settings
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger
*/
constructor(store, api, auth, session, settings, toastMessenger) {
......@@ -274,6 +275,7 @@ export class GroupsService {
});
}
/** @type {{ authority?: string, expand: string[], document_uri?: string }} */
const listParams = {
expand: expandParam,
};
......@@ -373,6 +375,7 @@ export class GroupsService {
});
let error;
/** @param {string} id */
const tryFetchGroup = async id => {
try {
return await this._fetchGroup(id);
......@@ -382,6 +385,7 @@ export class GroupsService {
}
};
/** @param {string} id */
const getGroup = id =>
userGroups.find(g => g.id === id || g.groupid === id) ||
tryFetchGroup(id);
......@@ -430,6 +434,7 @@ export class GroupsService {
const groupIdsOrPromise = this._serviceConfig?.groups;
if (Array.isArray(groupIdsOrPromise) || isPromise(groupIdsOrPromise)) {
/** @type {string[]} */
let groupIds = [];
try {
groupIds = await groupIdsOrPromise;
......
......@@ -2,6 +2,7 @@ import { isReply } from '../helpers/annotation-metadata';
import { SearchClient } from '../search-client';
/**
* @typedef {import('../../types/api').Annotation} Annotation
* @typedef {import('../search-client').SortBy} SortBy
* @typedef {import('../search-client').SortOrder} SortOrder
*/
......@@ -122,23 +123,33 @@ export class LoadAnnotationsService {
this._searchClient = new SearchClient(this._api.search, searchOptions);
this._searchClient.on('resultCount', resultCount => {
this._store.setAnnotationResultCount(resultCount);
});
this._searchClient.on(
'resultCount',
/** @param {number} count */
count => {
this._store.setAnnotationResultCount(count);
}
);
this._searchClient.on('results', results => {
this._searchClient.on(
'results',
/** @param {Annotation[]} results */ results => {
if (results.length) {
this._store.addAnnotations(results);
}
});
}
);
this._searchClient.on('error', error => {
this._searchClient.on(
'error',
/** @param {Error} error */ error => {
if (typeof onError === 'function') {
onError(error);
} else {
console.error(error);
}
});
}
);
this._searchClient.on('end', () => {
// Remove client as it's no longer active.
......
......@@ -3,19 +3,25 @@
*/
class InMemoryStorage {
constructor() {
this._store = {};
this._store = new Map();
}
/** @param {string} key */
getItem(key) {
return key in this._store ? this._store[key] : null;
return this._store.get(key) ?? null;
}
/**
* @param {string} key
* @param {string} value
*/
setItem(key, value) {
this._store[key] = value;
this._store.set(key, value);
}
/** @param {string} key */
removeItem(key) {
delete this._store[key];
this._store.delete(key);
}
}
......
import { entries } from '../util/collections';
import { watch } from '../util/watch';
/**
......@@ -35,24 +36,27 @@ export class PersistedDefaultsService {
/**
* Store subscribe callback for persisting changes to defaults. It will only
* persist defaults that it "knows about" via `DEFAULT_KEYS`.
*
* @param {Record<Key, any>} defaults
* @param {Record<Key, any>} prevDefaults
*/
const persistChangedDefaults = (defaults, prevDefaults) => {
for (let defaultKey in defaults) {
for (let [defaultKey, newValue] of entries(defaults)) {
if (
prevDefaults[defaultKey] !== defaults[defaultKey] &&
prevDefaults[defaultKey] !== newValue &&
defaultKey in DEFAULT_KEYS
) {
this._storage.setItem(DEFAULT_KEYS[defaultKey], defaults[defaultKey]);
this._storage.setItem(DEFAULT_KEYS[defaultKey], newValue);
}
}
};
// Read persisted defaults into the store
Object.keys(DEFAULT_KEYS).forEach(defaultKey => {
for (let [defaultKey, key] of entries(DEFAULT_KEYS)) {
// `localStorage.getItem` will return `null` for a non-existent key
const defaultValue = this._storage.getItem(DEFAULT_KEYS[defaultKey]);
this._store.setDefault(/** @type {Key} */ (defaultKey), defaultValue);
});
const defaultValue = this._storage.getItem(key);
this._store.setDefault(defaultKey, defaultValue);
}
// Listen for changes to those defaults from the store and persist them
watch(
......
......@@ -17,6 +17,7 @@ export class SessionService {
* @param {import('../store').SidebarStore} store
* @param {import('./api').APIService} api
* @param {import('./auth').AuthService} auth
* @param {import('../../types/config').SidebarSettings} settings
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger
*/
constructor(store, api, auth, settings, toastMessenger) {
......
......@@ -222,8 +222,16 @@ export class StreamerService {
);
}
});
newSocket.on('error', err => this._handleSocketError(websocketURL, err));
newSocket.on('message', event => this._handleSocketMessage(event));
newSocket.on(
'error',
/** @param {ErrorEvent} event */ event =>
this._handleSocketError(websocketURL, event)
);
newSocket.on(
'message',
/** @param {MessageEvent} event */ event =>
this._handleSocketMessage(event)
);
this._socket = newSocket;
// Configure the client ID
......
......@@ -35,6 +35,7 @@ export class TagsService {
* @return {string[]} List of matching tags
*/
filter(query, limit = null) {
/** @type {string[]} */
const savedTags = this._storage.getObject(TAGS_LIST_KEY) || [];
let resultCount = 0;
// Match any tag where the query is a prefix of the tag or a word within the tag.
......
......@@ -39,3 +39,17 @@ export function toTrueMap(arr) {
export function trueKeys(obj) {
return Object.keys(obj).filter(key => obj[key] === true);
}
/**
* Typed version of `Object.entries` for use with objects typed as
* `Record<Key, Value>`.
*
* Unlike `Object.entries`, this preserves the type of the key.
*
* @template {string|number|symbol} Key
* @template Value
* @param {Record<Key, Value>} object
*/
export function entries(object) {
return /** @type {[Key, Value][]} */ (Object.entries(object));
}
......@@ -29,6 +29,7 @@
"shared/**/*.js",
"sidebar/config/*.js",
"sidebar/helpers/*.js",
"sidebar/services/**/*.js",
"sidebar/store/**/*.js",
"sidebar/util/*.js",
"types/*.d.ts"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment