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