Skip to content

Commit 9261118

Browse files
committed
feat: use binary encoding to reduce network latency for large exports
1 parent 1e69ab7 commit 9261118

File tree

4 files changed

+45
-5
lines changed

4 files changed

+45
-5
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Injectable } from '@nestjs/common';
2+
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
3+
import type { FastifyReply } from 'fastify';
4+
import { Packr } from 'msgpackr';
5+
import { Observable } from 'rxjs';
6+
import { map } from 'rxjs/operators';
7+
8+
@Injectable()
9+
export class MsgpackInterceptor implements NestInterceptor {
10+
private packr = new Packr({
11+
useRecords: true
12+
});
13+
14+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
15+
const response = context.switchToHttp().getResponse<FastifyReply>();
16+
17+
return next.handle().pipe(
18+
map((data) => {
19+
response.header('Content-Type', 'application/x-msgpack');
20+
return this.packr.pack(data);
21+
})
22+
);
23+
}
24+
}

apps/api/src/instrument-records/instrument-records.controller.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
/* eslint-disable perfectionist/sort-classes */
22

33
import { CurrentUser, ParseSchemaPipe, ValidObjectIdPipe } from '@douglasneuroinformatics/libnest';
4-
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from '@nestjs/common';
4+
import {
5+
Body,
6+
Controller,
7+
Delete,
8+
Get,
9+
HttpCode,
10+
HttpStatus,
11+
Param,
12+
Patch,
13+
Post,
14+
Query,
15+
UseInterceptors
16+
} from '@nestjs/common';
517
import { ApiOperation, ApiTags } from '@nestjs/swagger';
618
import type { InstrumentKind } from '@opendatacapture/runtime-core';
719
import {
@@ -13,6 +25,7 @@ import { z } from 'zod/v4';
1325

1426
import type { AppAbility } from '@/auth/auth.types';
1527
import { RouteAccess } from '@/core/decorators/route-access.decorator';
28+
import { MsgpackInterceptor } from '@/core/interceptors/msgpack.interceptor';
1629

1730
import { InstrumentRecordsService } from './instrument-records.service';
1831

@@ -67,6 +80,7 @@ export class InstrumentRecordsController {
6780
@ApiOperation({ summary: 'Export Records' })
6881
@Get('export')
6982
@RouteAccess({ action: 'read', subject: 'InstrumentRecord' })
83+
@UseInterceptors(MsgpackInterceptor)
7084
exportRecords(@CurrentUser('ability') ability: AppAbility, @Query('groupId') groupId?: string) {
7185
return this.instrumentRecordsService.exportRecords({ groupId }, { ability });
7286
}

apps/web/src/routes/_app/datahub/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { removeSubjectIdScope } from '@opendatacapture/subject-utils';
1010
import { createFileRoute, useNavigate } from '@tanstack/react-router';
1111
import axios from 'axios';
1212
import { UserSearchIcon } from 'lucide-react';
13+
import { unpack } from 'msgpackr/unpack';
1314
import { unparse } from 'papaparse';
1415

1516
import { IdentificationForm } from '@/components/IdentificationForm';
@@ -50,12 +51,13 @@ const Toggles: React.FC<{ table: TanstackTable.Table<Subject> }> = ({ table }) =
5051
};
5152

5253
const getExportRecords = async () => {
53-
const response = await axios.get<InstrumentRecordsExport>('/v1/instrument-records/export', {
54+
const response = await axios.get<ArrayBuffer>('/v1/instrument-records/export', {
5455
params: {
5556
groupId: currentGroup?.id
56-
}
57+
},
58+
responseType: 'arraybuffer'
5759
});
58-
return response.data;
60+
return unpack(new Uint8Array(response.data)) as InstrumentRecordsExport;
5961
};
6062

6163
const handleExportSelection = (option: 'CSV' | 'Excel' | 'JSON') => {

apps/web/src/services/axios.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ axios.defaults.baseURL = config.setup.apiBaseUrl;
1010
axios.interceptors.request.use((config) => {
1111
const accessToken = useAppStore.getState().accessToken;
1212

13-
config.headers.setAccept('application/json');
13+
config.headers.setAccept(['application/json', 'application/x-msgpack']);
1414

1515
// Do not set timeout for setup (can be CPU intensive, especially on slow server)
1616
if (

0 commit comments

Comments
 (0)