1- import { replacer , reviver , yearsPassed } from '@douglasneuroinformatics/libjs' ;
1+ import { cpus } from 'os' ;
2+ import { dirname , join } from 'path' ;
3+ import { fileURLToPath } from 'url' ;
4+ import { Worker } from 'worker_threads' ;
5+
6+ const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
7+
8+ import { replacer , reviver } from '@douglasneuroinformatics/libjs' ;
29import { InjectModel } from '@douglasneuroinformatics/libnest' ;
310import type { Model } from '@douglasneuroinformatics/libnest' ;
411import { linearRegression } from '@douglasneuroinformatics/libstats' ;
512import { BadRequestException , Injectable , NotFoundException , UnprocessableEntityException } from '@nestjs/common' ;
613import type { Json , ScalarInstrument } from '@opendatacapture/runtime-core' ;
7- import { DEFAULT_GROUP_NAME } from '@opendatacapture/schemas/core' ;
814import { $RecordArrayFieldValue } from '@opendatacapture/schemas/instrument' ;
915import type {
1016 CreateInstrumentRecordData ,
@@ -14,7 +20,6 @@ import type {
1420 LinearRegressionResults ,
1521 UploadInstrumentRecordsData
1622} from '@opendatacapture/schemas/instrument-records' ;
17- import { removeSubjectIdScope } from '@opendatacapture/subject-utils' ;
1823import { Prisma } from '@prisma/client' ;
1924import type { Session } from '@prisma/client' ;
2025import { isNumber , mergeWith , pickBy } from 'lodash-es' ;
@@ -40,6 +45,8 @@ type ExpandDataType =
4045 success : false ;
4146 } ;
4247
48+ type WorkerMessage = { data : InstrumentRecordsExport ; success : true } | { error : string ; success : false } ;
49+
4350@Injectable ( )
4451export class InstrumentRecordsService {
4552 constructor (
@@ -133,7 +140,6 @@ export class InstrumentRecordsService {
133140 }
134141
135142 async exportRecords ( { groupId } : { groupId ?: string } = { } , { ability } : EntityOperationOptions = { } ) {
136- const data : InstrumentRecordsExport = [ ] ;
137143 const records = await this . instrumentRecordModel . findMany ( {
138144 include : {
139145 session : {
@@ -164,66 +170,96 @@ export class InstrumentRecordsService {
164170
165171 const instruments = new Map ( instrumentsArray . map ( ( instrument ) => [ instrument . id , instrument ] ) ) ;
166172
167- const processRecord = ( record : ( typeof records ) [ number ] ) => {
168- if ( ! record . computedMeasures ) return [ ] ;
169-
170- const instrument = instruments . get ( record . instrumentId ) ! ;
171- const rows : InstrumentRecordsExport = [ ] ;
172-
173- for ( const [ measureKey , measureValue ] of Object . entries ( record . computedMeasures ) ) {
174- if ( measureValue == null ) continue ;
175-
176- if ( ! Array . isArray ( measureValue ) ) {
177- rows . push ( {
178- groupId : record . subject . groupIds [ 0 ] ?? DEFAULT_GROUP_NAME ,
179- instrumentEdition : instrument . internal . edition ,
180- instrumentName : instrument . internal . name ,
181- measure : measureKey ,
182- sessionDate : record . session . date . toISOString ( ) ,
183- sessionId : record . session . id ,
184- sessionType : record . session . type ,
185- subjectAge : record . subject . dateOfBirth ? yearsPassed ( record . subject . dateOfBirth ) : null ,
186- subjectId : removeSubjectIdScope ( record . subject . id ) ,
187- subjectSex : record . subject . sex ,
188- timestamp : record . date . toISOString ( ) ,
189- username : record . session . user ?. username ?? 'N/A' ,
190- value : measureValue
191- } ) ;
192- continue ;
193- }
173+ // const processRecord = (record: (typeof records)[number]) => {
174+ // if (!record.computedMeasures) return [];
194175
195- if ( measureValue . length < 1 ) continue ;
176+ // const instrument = instruments.get(record.instrumentId)!;
177+ // const rows: InstrumentRecordsExport = [];
196178
197- const expanded = this . expandData ( measureValue ) ;
198- for ( const entry of expanded ) {
199- if ( ! entry . success ) {
200- throw new Error ( `exportRecords: ${ instrument . internal . name } .${ measureKey } — ${ entry . message } ` ) ;
201- }
202- rows . push ( {
203- groupId : record . subject . groupIds [ 0 ] ?? DEFAULT_GROUP_NAME ,
204- instrumentEdition : instrument . internal . edition ,
205- instrumentName : instrument . internal . name ,
206- measure : `${ measureKey } - ${ entry . measure } ` ,
207- sessionDate : record . session . date . toISOString ( ) ,
208- sessionId : record . session . id ,
209- sessionType : record . session . type ,
210- subjectAge : record . subject . dateOfBirth ? yearsPassed ( record . subject . dateOfBirth ) : null ,
211- subjectId : removeSubjectIdScope ( record . subject . id ) ,
212- subjectSex : record . subject . sex ,
213- timestamp : record . date . toISOString ( ) ,
214- username : record . session . user ?. username ?? 'N/A' ,
215- value : entry . measureValue
216- } ) ;
217- }
218- }
179+ // for (const [measureKey, measureValue] of Object.entries(record.computedMeasures)) {
180+ // if (measureValue == null) continue;
219181
220- return rows ;
221- } ;
182+ // if (!Array.isArray(measureValue)) {
183+ // rows.push({
184+ // groupId: record.subject.groupIds[0] ?? DEFAULT_GROUP_NAME,
185+ // instrumentEdition: instrument.internal.edition,
186+ // instrumentName: instrument.internal.name,
187+ // measure: measureKey,
188+ // sessionDate: record.session.date.toISOString(),
189+ // sessionId: record.session.id,
190+ // sessionType: record.session.type,
191+ // subjectAge: record.subject.dateOfBirth ? yearsPassed(record.subject.dateOfBirth) : null,
192+ // subjectId: removeSubjectIdScope(record.subject.id),
193+ // subjectSex: record.subject.sex,
194+ // timestamp: record.date.toISOString(),
195+ // username: record.session.user?.username ?? 'N/A',
196+ // value: measureValue
197+ // });
198+ // continue;
199+ // }
200+
201+ // if (measureValue.length < 1) continue;
222202
223- const results = await Promise . all ( records . map ( ( record ) => processRecord ( record ) ) ) ;
203+ // const expanded = this.expandData(measureValue);
204+ // for (const entry of expanded) {
205+ // if (!entry.success) {
206+ // throw new Error(`exportRecords: ${instrument.internal.name}.${measureKey} — ${entry.message}`);
207+ // }
208+ // rows.push({
209+ // groupId: record.subject.groupIds[0] ?? DEFAULT_GROUP_NAME,
210+ // instrumentEdition: instrument.internal.edition,
211+ // instrumentName: instrument.internal.name,
212+ // measure: `${measureKey} - ${entry.measure}`,
213+ // sessionDate: record.session.date.toISOString(),
214+ // sessionId: record.session.id,
215+ // sessionType: record.session.type,
216+ // subjectAge: record.subject.dateOfBirth ? yearsPassed(record.subject.dateOfBirth) : null,
217+ // subjectId: removeSubjectIdScope(record.subject.id),
218+ // subjectSex: record.subject.sex,
219+ // timestamp: record.date.toISOString(),
220+ // username: record.session.user?.username ?? 'N/A',
221+ // value: entry.measureValue
222+ // });
223+ // }
224+ // }
225+
226+ // return rows;
227+ // };
228+
229+ // const results = await Promise.all(records.map((record) => processRecord(record)));
230+
231+ // return results.flat();
232+
233+ const numWorkers = Math . min ( cpus ( ) . length , Math . ceil ( records . length / 100 ) ) ; // Use up to CPU count, chunk size 100
234+ const chunkSize = Math . ceil ( records . length / numWorkers ) ;
235+ const chunks = [ ] ;
236+ for ( let i = 0 ; i < records . length ; i += chunkSize ) {
237+ chunks . push ( records . slice ( i , i + chunkSize ) ) ;
238+ }
239+
240+ const workerPromises = chunks . map ( ( chunk ) => {
241+ return new Promise < InstrumentRecordsExport > ( ( resolve , reject ) => {
242+ const worker = new Worker ( join ( __dirname , 'export-worker.ts' ) ) ;
243+ worker . postMessage ( { instruments : Array . from ( instruments . entries ( ) ) , records : chunk } ) ;
244+ worker . on ( 'message' , ( message : WorkerMessage ) => {
245+ if ( message . success ) {
246+ resolve ( message . data ) ;
247+ } else {
248+ reject ( new Error ( message . error ) ) ;
249+ }
250+ void worker . terminate ( ) ;
251+ } ) ;
252+ worker . on ( 'error' , ( error ) => {
253+ reject ( error ) ;
254+ void worker . terminate ( ) ;
255+ } ) ;
256+ } ) ;
257+ } ) ;
224258
259+ const results = await Promise . all ( workerPromises ) ;
225260 return results . flat ( ) ;
226261
262+ // const data: InstrumentRecordsExport = [];
227263 // for (const record of records) {
228264 // if (!record.computedMeasures) {
229265 // continue;
@@ -281,7 +317,7 @@ export class InstrumentRecordsService {
281317 // }
282318 // }
283319
284- return data ;
320+ // return data;
285321 }
286322
287323 async find (
0 commit comments