Skip to content

Commit

Permalink
feat: add etherpad fetch queue (#424)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Chau authored Sep 21, 2023
1 parent e34cc2d commit 2b339f8
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 49 deletions.
68 changes: 68 additions & 0 deletions src/api/etherpad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { EtherpadItemType, Item, UUID } from '@graasp/sdk';

import axios from 'axios';

import { QueryClientConfig } from '../types';
import { verifyAuthentication } from './axios';
import { buildGetEtherpadRoute, buildPostEtherpadRoute } from './routes';

/**
* This is a queue singleton class that manages querying etherpads
* It ensures that they are always retrieved sequentially (i.e. always after the previous is resolved)
* This is required because the etherpad sessions are given in a single cookie which must be constructed cumulatively
* To ensure that there is no race condition between requests in a given browser tab, this queue sends them one after the other
*/
class EtherpadQueue {
/** Ensure singleton with a single instance */
static readonly instance = new EtherpadQueue();

/** A reference to the last promise added to the queue */
private lastPromise = Promise.resolve();

/** Ensure singleton with private constructor */
// eslint-disable-next-line no-useless-constructor, no-empty-function
private constructor() {}

public getEtherpad(
{ itemId, mode }: { itemId: UUID; mode: 'read' | 'write' },
{ API_HOST }: QueryClientConfig,
) {
const doFetch = () =>
axios
.get(`${API_HOST}/${buildGetEtherpadRoute(itemId)}`, {
params: { mode },
})
.then(({ data }) => data);
// The queue is implicitly managed by the nested promises call stack
// We simply schedule this request after the last one that was set
// We CANNOT use this.lastPromise.finally(doFetch)! The finally semantics will return the previous return value, even if failing!
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally#description
// instead we use .then(onResolve, onRejected) with both arguments set to doFetch
const nextPromise = this.lastPromise.then(doFetch, doFetch);
this.lastPromise = nextPromise;
// Retuning the previous reference allows multiple then / catch calls
return nextPromise;
}
}

export const postEtherpad = async (
{
name,
parentId,
}: Pick<Item, 'name'> & {
parentId?: UUID;
},
{ API_HOST }: QueryClientConfig,
): Promise<EtherpadItemType> =>
verifyAuthentication(() =>
axios
.post(`${API_HOST}/${buildPostEtherpadRoute(parentId)}`, {
name: name.trim(),
})
.then(({ data }) => data),
);

export const getEtherpad = (
args: { itemId: UUID; mode: 'read' | 'write' },
queryConfig: QueryClientConfig,
) => EtherpadQueue.instance.getEtherpad(args, queryConfig);
27 changes: 14 additions & 13 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
export * from './action';
export * from './apps';
export * from './member';
export * from './item';
export * from './membership';
export * from './authentication';
export * from './itemTag';
export * from './itemLogin';
export * from './itemFlag';
export * from './chat';
export * from './mentions';
export * from './category';
export * from './search';
export * from './chat';
export * from './etherpad';
export * from './invitation';
export * from './item';
export * from './itemExport';
export * from './itemFavorite';
export * from './itemFlag';
export * from './itemLike';
export * from './itemLogin';
export * from './itemPublish';
export * from './itemTag';
export * from './itemValidation';
export * from './action';
export * from './invitation';
export * from './member';
export * from './membership';
export * from './mentions';
export * from './search';
export * from './subscription';
export * from './itemPublish';
export * from './itemFavorite';
37 changes: 1 addition & 36 deletions src/api/item.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
DiscriminatedItem,
EtherpadItemType,
Item,
ResultOf,
UUID,
} from '@graasp/sdk';
import { DiscriminatedItem, Item, ResultOf, UUID } from '@graasp/sdk';

import { DEFAULT_THUMBNAIL_SIZE } from '../config/constants';
import { QueryClientConfig } from '../types';
Expand All @@ -20,13 +14,11 @@ import {
buildDownloadItemThumbnailRoute,
buildEditItemRoute,
buildGetChildrenRoute,
buildGetEtherpadRoute,
buildGetItemDescendants,
buildGetItemParents,
buildGetItemRoute,
buildGetItemsRoute,
buildMoveItemsRoute,
buildPostEtherpadRoute,
buildPostItemRoute,
buildRecycleItemsRoute,
buildRestoreItemsRoute,
Expand Down Expand Up @@ -253,30 +245,3 @@ export const downloadItemThumbnailUrl = async (
})}`,
)
.then(({ data }) => data);

export const postEtherpad = async (
{
name,
parentId,
}: Pick<Item, 'name'> & {
parentId?: UUID;
},
{ API_HOST }: QueryClientConfig,
): Promise<EtherpadItemType> =>
verifyAuthentication(() =>
axios
.post(`${API_HOST}/${buildPostEtherpadRoute(parentId)}`, {
name: name.trim(),
})
.then(({ data }) => data),
);

export const getEtherpad = (
{ itemId, mode }: { itemId: UUID; mode: 'read' | 'write' },
{ API_HOST }: QueryClientConfig,
) =>
axios
.get(`${API_HOST}/${buildGetEtherpadRoute(itemId)}`, {
params: { mode },
})
.then(({ data }) => data);

0 comments on commit 2b339f8

Please sign in to comment.