Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions core/src/sessions/base_session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ export interface ListSessionsResponse {
totalPages: number;
}

/**
* The slice bounds and response metadata derived from a `ListSessionsRequest`.
*/
export interface ResolvedPagination {
/** Zero-based index of the first session to return. */
offset: number;
/** Pagination metadata for the response. */
meta: Omit<ListSessionsResponse, 'sessions'>;
}

/**
* Base class for session services.
*
Expand Down Expand Up @@ -258,3 +268,90 @@ export function mergeStates(
}
return merged;
}

/**
* Resolves the pagination parameters of a `ListSessionsRequest` against a known
* total into slice bounds and `ListSessionsResponse` metadata.
*
* Intended for backends that paginate in their storage layer: the returned
* `offset` and the request's `limit` map onto a query's `OFFSET` and `LIMIT`.
* Backends holding the whole result set in memory should use
* `paginateSessions` instead.
*
* Negative and out-of-range values are passed through untouched; no session
* backend validates or clamps them.
*
* @param request The request whose `limit`, `offset` and `page` are read.
* @param totalItems The number of matching sessions, counted before `offset`
* and `limit` are applied.
* @return The slice bounds and the response metadata.
*/
export function resolvePagination(
request: ListSessionsRequest,
totalItems: number,
): ResolvedPagination {
const {limit, page} = request;

if (limit === undefined) {
return {
offset: request.offset ?? 0,
meta: {
page: 1,
limit: totalItems,
totalItems,
totalPages: totalItems === 0 ? 0 : 1,
},
};
}

const offset =
page !== undefined ? (page - 1) * limit : (request.offset ?? 0);

return {
offset,
meta: {
page: page ?? (limit === 0 ? 1 : Math.floor(offset / limit) + 1),
limit,
totalItems,
totalPages: limit === 0 ? 0 : Math.ceil(totalItems / limit),
},
};
}

/**
* Orders, slices and wraps a fully materialised set of sessions into a
* `ListSessionsResponse`.
*
* For backends that hold every matching session in memory. The input array is
* left untouched; ordering is applied to a copy, and only when `request.order`
* is set.
*
* @param sessions Every session matching the request, before pagination.
* @param request The request whose `order`, `limit`, `offset` and `page` are
* read.
* @return The requested page of sessions and its metadata.
*/
export function paginateSessions(
sessions: Session[],
request: ListSessionsRequest,
): ListSessionsResponse {
const {limit, order} = request;
const direction = order === 'asc' ? 1 : -1;
const ordered = order
? [...sessions].sort(
(a, b) =>
direction * (a.lastUpdateTime - b.lastUpdateTime) ||
a.id.localeCompare(b.id),
)
: sessions;

const {offset, meta} = resolvePagination(request, sessions.length);

return {
sessions: ordered.slice(
offset,
limit === undefined ? undefined : offset + limit,
),
...meta,
};
}
63 changes: 18 additions & 45 deletions core/src/sessions/database_session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ListSessionsRequest,
ListSessionsResponse,
mergeStates,
resolvePagination,
trimTempDeltaState,
} from './base_session_service.js';
import {
Expand Down Expand Up @@ -251,14 +252,10 @@ export class DatabaseSessionService extends BaseSessionService {
});
}

async listSessions({
appName,
userId,
limit,
offset,
page,
order,
}: ListSessionsRequest): Promise<ListSessionsResponse> {
async listSessions(
request: ListSessionsRequest,
): Promise<ListSessionsResponse> {
const {appName, userId, limit, offset, order} = request;
await this.init();
const em = this.orm!.em.fork();

Expand All @@ -275,53 +272,29 @@ export class DatabaseSessionService extends BaseSessionService {
: undefined;

let storageSessions;
let paginationMeta: Pick<
ListSessionsResponse,
'page' | 'limit' | 'totalItems' | 'totalPages'
>;
let paginationMeta: Omit<ListSessionsResponse, 'sessions'>;

if (limit !== undefined) {
const totalItems = await em.count(StorageSession, where);
const totalPages = limit === 0 ? 0 : Math.ceil(totalItems / limit);

let effectiveOffset: number;
let effectivePage: number;
if (page !== undefined) {
effectiveOffset = (page - 1) * limit;
effectivePage = page;
} else {
effectiveOffset = offset ?? 0;
effectivePage =
limit === 0 ? 1 : Math.floor(effectiveOffset / limit) + 1;
}
const {offset: effectiveOffset, meta} = resolvePagination(
request,
totalItems,
);

storageSessions = await em.find(StorageSession, where, {
orderBy,
limit,
offset: effectiveOffset,
});
paginationMeta = {page: effectivePage, limit, totalItems, totalPages};
} else if (offset) {
const totalItems = await em.count(StorageSession, where);
storageSessions = await em.find(StorageSession, where, {
orderBy,
offset,
});
paginationMeta = {
page: 1,
limit: totalItems,
totalItems,
totalPages: totalItems === 0 ? 0 : 1,
};
paginationMeta = meta;
} else {
storageSessions = await em.find(StorageSession, where, {orderBy});
const totalItems = storageSessions.length;
paginationMeta = {
page: 1,
limit: totalItems,
totalItems,
totalPages: totalItems === 0 ? 0 : 1,
};
storageSessions = await em.find(StorageSession, where, {orderBy, offset});
// The rows are the whole result set unless an offset skipped some of
// them, in which case the true total costs a separate count.
const totalItems = offset
? await em.count(StorageSession, where)
: storageSessions.length;
paginationMeta = resolvePagination(request, totalItems).meta;
}

const appStateModel = await em.findOne(StorageAppState, {appName});
Expand Down
107 changes: 16 additions & 91 deletions core/src/sessions/in_memory_session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ListSessionsRequest,
ListSessionsResponse,
mergeStates,
paginateSessions,
trimTempState,
} from './base_session_service.js';
import {createSession, Session} from './session.js';
Expand Down Expand Up @@ -133,99 +134,23 @@ export class InMemorySessionService extends BaseSessionService {
return copiedSession;
}

listSessions({
appName,
userId,
limit,
offset,
page,
order,
}: ListSessionsRequest): Promise<ListSessionsResponse> {
if (!this.sessions[appName] || !this.sessions[appName][userId]) {
if (limit !== undefined) {
const effectiveOffset =
page !== undefined ? (page - 1) * limit : (offset ?? 0);
const effectivePage =
page !== undefined
? page
: limit === 0
? 1
: Math.floor(effectiveOffset / limit) + 1;
return Promise.resolve({
sessions: [],
page: effectivePage,
limit,
totalItems: 0,
totalPages: 0,
});
}
return Promise.resolve({
sessions: [],
page: 1,
limit: 0,
totalItems: 0,
totalPages: 0,
});
}

const all: Session[] = Object.values(this.sessions[appName][userId]).map(
(session) =>
createSession({
id: session.id,
appName: session.appName,
userId: session.userId,
state: {},
events: [],
lastUpdateTime: session.lastUpdateTime,
}),
listSessions(request: ListSessionsRequest): Promise<ListSessionsResponse> {
const {appName, userId} = request;

const all: Session[] = Object.values(
this.sessions[appName]?.[userId] ?? {},
).map((session) =>
createSession({
id: session.id,
appName: session.appName,
userId: session.userId,
state: {},
events: [],
lastUpdateTime: session.lastUpdateTime,
}),
);

if (order === 'asc') {
all.sort(
(a, b) =>
a.lastUpdateTime - b.lastUpdateTime || a.id.localeCompare(b.id),
);
} else if (order === 'desc') {
all.sort(
(a, b) =>
b.lastUpdateTime - a.lastUpdateTime || a.id.localeCompare(b.id),
);
}

if (limit === undefined) {
const totalItems = all.length;
const sliced = offset ? all.slice(offset) : all;
return Promise.resolve({
sessions: sliced,
page: 1,
limit: totalItems,
totalItems,
totalPages: totalItems === 0 ? 0 : 1,
});
}

const totalItems = all.length;
const totalPages = limit === 0 ? 0 : Math.ceil(totalItems / limit);

let effectiveOffset: number;
let effectivePage: number;
if (page !== undefined) {
effectiveOffset = (page - 1) * limit;
effectivePage = page;
} else {
effectiveOffset = offset ?? 0;
effectivePage = limit === 0 ? 1 : Math.floor(effectiveOffset / limit) + 1;
}

const paginated = all.slice(effectiveOffset, effectiveOffset + limit);

return Promise.resolve({
sessions: paginated,
page: effectivePage,
limit,
totalItems,
totalPages,
});
return Promise.resolve(paginateSessions(all, request));
}

async deleteSession({
Expand Down
Loading
Loading