@modelhealth/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1016 @@
1
+ /**
2
+ * Model Health SDK Client
3
+ *
4
+ * TypeScript/JavaScript client for the Model Health biomechanics SDK.
5
+ * Provides a clean, typed API over the WASM bindings.
6
+ *
7
+ * @packageDocumentation
8
+ *
9
+ * @example Basic usage
10
+ * ```typescript
11
+ * import { ModelHealthService } from '@modelhealth/sdk';
12
+ *
13
+ * // Create and initialize client with API key
14
+ * const client = new ModelHealthService({ apiKey: "your-api-key-here" });
15
+ * await client.init();
16
+ *
17
+ * // Authenticate (optional - API key already provides authentication)
18
+ * const result = await client.login("user@example.com", "password");
19
+ * if (result === "verification_required") {
20
+ * await client.verify("123456", true);
21
+ * }
22
+ *
23
+ * // Get sessions
24
+ * const sessions = await client.sessionList();
25
+ * ```
26
+ */
27
+ import { MemoryTokenStorage, LocalStorageTokenStorage, } from "./types.js";
28
+ let wasmModule = null;
29
+ let wasmInitialized = false;
30
+ let wasmInitPromise = null;
31
+ /**
32
+ * Initialize the WASM module.
33
+ *
34
+ * This must be called before using the SDK. It loads and initializes
35
+ * the WebAssembly module containing the core SDK functionality.
36
+ *
37
+ * @internal
38
+ */
39
+ async function initWasm() {
40
+ if (wasmInitialized)
41
+ return;
42
+ if (wasmInitPromise)
43
+ return wasmInitPromise;
44
+ wasmInitPromise = (async () => {
45
+ try {
46
+ wasmModule = await import("../wasm/model_health_wasm.js");
47
+ await wasmModule.default();
48
+ await wasmModule.init();
49
+ wasmInitialized = true;
50
+ }
51
+ catch (error) {
52
+ wasmInitPromise = null;
53
+ throw new Error(`Failed to initialize WASM module: ${error}`);
54
+ }
55
+ })();
56
+ return wasmInitPromise;
57
+ }
58
+ /**
59
+ * Model Health SDK Client for biomechanical analysis.
60
+ *
61
+ * Main entry point for interacting with the Model Health SDK.
62
+ * Provides authentication, session management, data download,
63
+ * and analysis capabilities.
64
+ *
65
+ * @example Create with API key
66
+ * ```typescript
67
+ * const client = new ModelHealthService({
68
+ * apiKey: "your-api-key-here"
69
+ * });
70
+ * await client.init();
71
+ *
72
+ * // SDK is ready to use
73
+ * const sessions = await client.sessionList();
74
+ * ```
75
+ *
76
+ * @example With custom configuration
77
+ * ```typescript
78
+ * const client = new ModelHealthService({
79
+ * apiKey: "your-api-key",
80
+ * storage: new LocalStorageTokenStorage()
81
+ * });
82
+ * await client.init();
83
+ * ```
84
+ */
85
+ export class ModelHealthService {
86
+ /**
87
+ * Create a new Model Health client.
88
+ *
89
+ * @param config Configuration options including API key
90
+ * @throws If API key is not provided
91
+ *
92
+ * @example Default configuration
93
+ * ```typescript
94
+ * const client = new ModelHealthService({
95
+ * apiKey: "your-api-key-here"
96
+ * });
97
+ * ```
98
+ *
99
+ * @example Custom configuration
100
+ * ```typescript
101
+ * const client = new ModelHealthService({
102
+ * apiKey: "your-api-key",
103
+ * storage: new LocalStorageTokenStorage(),
104
+ * autoInit: false
105
+ * });
106
+ * ```
107
+ */
108
+ constructor(config) {
109
+ this.wasmClient = null;
110
+ this.initialized = false;
111
+ if (!config.apiKey) {
112
+ throw new Error("API key is required. Provide it in the config: { apiKey: 'your-key' }");
113
+ }
114
+ this.config = {
115
+ apiKey: config.apiKey,
116
+ storage: config.storage ?? new MemoryTokenStorage(),
117
+ autoInit: config.autoInit ?? true,
118
+ };
119
+ this.storage = this.config.storage;
120
+ // Auto-initialize if requested
121
+ if (this.config.autoInit) {
122
+ this.init().catch((error) => {
123
+ console.error("Failed to auto-initialize Model Health client:", error);
124
+ });
125
+ }
126
+ }
127
+ /**
128
+ * Initialize the WASM module and client.
129
+ *
130
+ * Must be called before using any other methods if `autoInit: false`
131
+ * was specified in the configuration. Safe to call multiple times.
132
+ *
133
+ * @throws If WASM initialization fails
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const client = new ModelHealthService({
138
+ * apiKey: "your-key",
139
+ * autoInit: false
140
+ * });
141
+ * await client.init();
142
+ * ```
143
+ */
144
+ async init() {
145
+ if (this.initialized)
146
+ return;
147
+ await initWasm();
148
+ // Create the WASM client with API key
149
+ try {
150
+ this.wasmClient = new wasmModule.ModelHealthService(this.config.apiKey);
151
+ }
152
+ catch (error) {
153
+ throw new Error(`Failed to create Model Health client: ${error}`);
154
+ }
155
+ // Set storage
156
+ this.wasmClient.setStorage(this.storage);
157
+ // Try to restore token
158
+ await this.wasmClient.restoreToken();
159
+ this.initialized = true;
160
+ }
161
+ /**
162
+ * Ensure the client is initialized.
163
+ *
164
+ * @private
165
+ * @throws If client is not initialized
166
+ */
167
+ ensureInitialized() {
168
+ if (!this.initialized) {
169
+ throw new Error("Model Health client not initialized. Call init() before using the client.");
170
+ }
171
+ }
172
+ // MARK: - Authentication
173
+ /**
174
+ * Registers a new user account.
175
+ *
176
+ * Creates a new user account and automatically authenticates the user.
177
+ * After successful registration, the SDK is ready to use immediately
178
+ * without requiring a separate login call.
179
+ *
180
+ * @param parameters Registration details including credentials and user information
181
+ * @throws If registration fails (duplicate username/email, validation errors, etc.)
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const params = {
186
+ * username: "user123",
187
+ * email: "user@example.com",
188
+ * password: "securePassword123456789",
189
+ * first_name: "John",
190
+ * last_name: "Doe",
191
+ * country: "United States",
192
+ * institution: "Example University",
193
+ * profession: "Researcher",
194
+ * reason: "Biomechanical research",
195
+ * language: "en",
196
+ * unit: "metric",
197
+ * newsletter: false
198
+ * };
199
+ *
200
+ * await client.register(params);
201
+ * // User is now authenticated and ready to use SDK
202
+ * ```
203
+ */
204
+ async register(parameters) {
205
+ this.ensureInitialized();
206
+ await this.wasmClient.register(parameters);
207
+ }
208
+ /**
209
+ * Authenticates a user with username and password.
210
+ *
211
+ * This initiates the login process. Depending on the account's security settings
212
+ * and device trust status, either:
213
+ * - Returns `"ok"` if the device is trusted (previously verified with
214
+ * `rememberDevice: true` within the last 90 days)
215
+ * - Returns `"verification_required"` if email verification is needed
216
+ *
217
+ * When verification is required, a code is automatically sent to the user's
218
+ * registered email address. Complete authentication by calling `verify()`.
219
+ *
220
+ * @param username User's email address
221
+ * @param password User's password
222
+ * @returns A `LoginResult` indicating whether verification is required
223
+ * @throws If authentication fails (invalid credentials, network issues, etc.)
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const result = await client.login("user@example.com", "secure_pass");
228
+ *
229
+ * switch (result) {
230
+ * case "ok":
231
+ * // Authentication complete, proceed with SDK usage
232
+ * console.log("Login successful");
233
+ * break;
234
+ *
235
+ * case "verification_required":
236
+ * // Prompt user for email verification code and
237
+ * // trust this device for 90 days
238
+ * const code = await promptUserForCode();
239
+ * await client.verify(code, true);
240
+ * break;
241
+ * }
242
+ * ```
243
+ */
244
+ async login(username, password) {
245
+ this.ensureInitialized();
246
+ const result = await this.wasmClient.login(username, password);
247
+ return result;
248
+ }
249
+ /**
250
+ * Completes authentication by verifying an email code.
251
+ *
252
+ * After `login()` returns `"verification_required"`, call this method with
253
+ * the verification code sent to the user's email.
254
+ *
255
+ * Set `rememberDevice: true` to skip email verification on this device for 90 days.
256
+ * Future login attempts from this device will return `"ok"` directly.
257
+ *
258
+ * @param code 6-digit verification code from email
259
+ * @param rememberDevice If `true`, trust this device for 90 days (default: `false`)
260
+ * @throws If the code is invalid or expired
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // After receiving "verification_required" from login
265
+ * await client.verify("123456", true);
266
+ * // Authentication now complete, SDK ready for use
267
+ * ```
268
+ */
269
+ async verify(code, rememberDevice) {
270
+ this.ensureInitialized();
271
+ await this.wasmClient.verify(code, rememberDevice);
272
+ }
273
+ /**
274
+ * Logs out the current user.
275
+ *
276
+ * After logout, the user must call `login()` or `register()` to use the SDK again.
277
+ *
278
+ * @throws If the logout request fails
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * await client.logout();
283
+ * // User is now logged out
284
+ * ```
285
+ */
286
+ async logout() {
287
+ this.ensureInitialized();
288
+ await this.wasmClient.logout();
289
+ }
290
+ /**
291
+ * Checks if a user is currently authenticated.
292
+ *
293
+ * @returns `true` if authenticated, `false` otherwise
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * if (await client.isAuthenticated()) {
298
+ * // Proceed with authenticated operations
299
+ * const sessions = await client.sessionList();
300
+ * } else {
301
+ * // Show login screen
302
+ * }
303
+ * ```
304
+ */
305
+ async isAuthenticated() {
306
+ this.ensureInitialized();
307
+ return await this.wasmClient.isAuthenticated();
308
+ }
309
+ /**
310
+ * Get the current authentication token.
311
+ *
312
+ * @returns The authentication token string, or null if not authenticated
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const token = client.getToken();
317
+ * if (token) {
318
+ * // Store for later use
319
+ * localStorage.setItem('backup_token', token);
320
+ * }
321
+ * ```
322
+ */
323
+ getToken() {
324
+ this.ensureInitialized();
325
+ return this.wasmClient.getToken() ?? null;
326
+ }
327
+ /**
328
+ * Set authentication token directly.
329
+ *
330
+ * Use this to restore a previously saved session without logging in again.
331
+ *
332
+ * @param token The authentication token to restore
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * const savedToken = localStorage.getItem('backup_token');
337
+ * if (savedToken) {
338
+ * client.setToken(savedToken);
339
+ * }
340
+ * ```
341
+ */
342
+ setToken(token) {
343
+ this.ensureInitialized();
344
+ this.wasmClient.setToken(token);
345
+ }
346
+ // MARK: - Sessions
347
+ /**
348
+ * Retrieves all sessions for the authenticated user.
349
+ *
350
+ * @returns An array of `Session` objects. Returns an empty array if no sessions exist.
351
+ * @throws If the request fails due to network issues, authentication problems,
352
+ * or server errors.
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * try {
357
+ * const sessions = await client.sessionList();
358
+ * console.log(`Found ${sessions.length} sessions`);
359
+ * for (const session of sessions) {
360
+ * console.log(`Session: ${session.id}`);
361
+ * }
362
+ * } catch (error) {
363
+ * console.log(`Failed to fetch sessions: ${error}`);
364
+ * }
365
+ * ```
366
+ */
367
+ async sessionList() {
368
+ this.ensureInitialized();
369
+ const result = await this.wasmClient.sessionList();
370
+ return this.parseResponse(result);
371
+ }
372
+ /**
373
+ * Retrieve a specific session by ID with all activities populated.
374
+ *
375
+ * @param sessionId Unique session identifier
376
+ * @returns The requested session with complete activity data
377
+ * @throws If the session doesn't exist, user lacks access, or request fails
378
+ *
379
+ * @example
380
+ * ```typescript
381
+ * const session = await client.getSession("session-abc123");
382
+ * console.log(`Session has ${session.activities.length} activities`);
383
+ * ```
384
+ */
385
+ async getSession(sessionId) {
386
+ this.ensureInitialized();
387
+ const result = await this.wasmClient.getSession(sessionId);
388
+ return this.parseResponse(result);
389
+ }
390
+ /**
391
+ * Creates a new session.
392
+ *
393
+ * A session is required before performing camera calibration. It represents
394
+ * a single calibration workflow and groups multiple cameras together.
395
+ *
396
+ * After creating a session, use camera calibration methods to calibrate your cameras.
397
+ *
398
+ * @returns A `Session` object with a unique identifier
399
+ * @throws If session creation fails
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * // Create session
404
+ * const session = await client.createSession();
405
+ *
406
+ * // Proceed with calibration
407
+ * const details = {
408
+ * rows: 4,
409
+ * columns: 5,
410
+ * square_size: 35,
411
+ * placement: "perpendicular"
412
+ * };
413
+ * // await client.calibrateCamera(session, details, (status) => { ... });
414
+ * ```
415
+ */
416
+ async createSession() {
417
+ this.ensureInitialized();
418
+ const result = await this.wasmClient.createSession();
419
+ return this.parseResponse(result);
420
+ }
421
+ /**
422
+ * Calibrates a camera using a checkerboard pattern.
423
+ *
424
+ * **Requirements:**
425
+ * - A printed checkerboard pattern
426
+ * - Accurate measurement of square size in millimeters
427
+ * - Multiple views of the checkerboard from different angles
428
+ *
429
+ * The calibration is automated and typically completes in a few seconds
430
+ *
431
+ * @param session The session created with `createSession()`
432
+ * @param checkerboardDetails Configuration of the calibration checkerboard
433
+ * @param statusCallback Callback function called with calibration progress updates
434
+ * @throws If calibration fails (insufficient views, pattern not detected, etc.)
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * const session = await client.createSession();
439
+ *
440
+ * const details = {
441
+ * rows: 4, // Internal corners, not squares (for 5×6 board)
442
+ * columns: 5, // Internal corners, not squares (for 5×6 board)
443
+ * square_size: 35, // Measured in millimeters
444
+ * placement: "perpendicular"
445
+ * };
446
+ *
447
+ * await client.calibrateCamera(session, details, (status) => {
448
+ * console.log("Calibration status:", status);
449
+ * });
450
+ * // Calibration complete, proceed to neutral pose
451
+ * ```
452
+ */
453
+ async calibrateCamera(session, checkerboardDetails, statusCallback) {
454
+ this.ensureInitialized();
455
+ const token = this.wasmClient.getToken();
456
+ if (!token) {
457
+ throw new Error("Not authenticated");
458
+ }
459
+ const jsCallback = (statusJson) => {
460
+ statusCallback(statusJson);
461
+ };
462
+ await wasmModule.calibrateCamera(this.config.apiKey, token, session, checkerboardDetails, jsCallback);
463
+ }
464
+ /**
465
+ * Captures the subject's neutral standing pose for model scaling.
466
+ *
467
+ * This step is required after camera calibration and before recording movement activities.
468
+ * It takes a quick video of the subject standing in a neutral position, which is
469
+ * used to scale the biomechanical model to match the subject's dimensions.
470
+ *
471
+ * **Instructions for subject:**
472
+ * - Stand upright in a relaxed, natural position
473
+ * - Face forward with arms spread slightly at sides
474
+ * - Remain still for a few seconds
475
+ *
476
+ * @param subject The subject to calibrate the neutral pose for
477
+ * @param session The session to perform calibration in
478
+ * @param statusCallback Callback function called with calibration progress updates
479
+ * @throws If pose capture fails (subject not detected, poor lighting, etc.)
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * // After successful camera calibration
484
+ * await client.calibrateNeutralPose(subject, session, (status) => {
485
+ * console.log("Neutral pose status:", status);
486
+ * });
487
+ * // Model now scaled, ready to record movement activities
488
+ * ```
489
+ */
490
+ async calibrateNeutralPose(subject, session, statusCallback) {
491
+ this.ensureInitialized();
492
+ const token = this.wasmClient.getToken();
493
+ if (!token) {
494
+ throw new Error("Not authenticated");
495
+ }
496
+ const jsCallback = (statusJson) => {
497
+ statusCallback(statusJson);
498
+ };
499
+ await wasmModule.calibrateNeutralPose(this.config.apiKey, token, subject, session, jsCallback);
500
+ }
501
+ // MARK: - Subjects
502
+ /**
503
+ * Retrieves all subjects associated with the authenticated account.
504
+ *
505
+ * Subjects represent individuals being monitored or assessed. Each subject
506
+ * contains demographic information, physical measurements, and categorization tags.
507
+ *
508
+ * @returns An array of `Subject` objects
509
+ * @throws If the request fails or authentication has expired
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * const subjects = await client.subjectList();
514
+ * for (const subject of subjects) {
515
+ * console.log(`${subject.name}: ${subject.height ?? 0}cm, ${subject.weight ?? 0}kg`);
516
+ * }
517
+ *
518
+ * // Filter by tags
519
+ * const athletes = subjects.filter(s => s.subject_tags.includes("athlete"));
520
+ * ```
521
+ */
522
+ async subjectList() {
523
+ this.ensureInitialized();
524
+ const result = await this.wasmClient.subjectList();
525
+ return this.parseResponse(result);
526
+ }
527
+ /**
528
+ * Creates a new subject in the system.
529
+ *
530
+ * Subjects represent individuals being monitored or assessed. After creating
531
+ * a subject, they can be associated with sessions for neutral pose calibration
532
+ * and movement activities.
533
+ *
534
+ * @param parameters Subject details including name, measurements, and tags
535
+ * @returns The newly created `Subject` with its assigned ID
536
+ * @throws If creation fails (validation errors, duplicate name, etc.)
537
+ *
538
+ * @example
539
+ * ```typescript
540
+ * const params = {
541
+ * name: "John Doe",
542
+ * weight: 75.0, // kilograms
543
+ * height: 180.0, // centimeters
544
+ * birth_year: 1990,
545
+ * gender: "man",
546
+ * sex_at_birth: "man",
547
+ * characteristics: "Regular training schedule",
548
+ * subject_tags: ["athlete"],
549
+ * terms: true
550
+ * };
551
+ *
552
+ * const subject = await client.createSubject(params);
553
+ * console.log(`Created subject with ID: ${subject.id}`);
554
+ *
555
+ * // Use the subject for calibration
556
+ * // await client.calibrateNeutralPose(subject, session, (status) => { ... });
557
+ * ```
558
+ */
559
+ async createSubject(parameters) {
560
+ this.ensureInitialized();
561
+ const result = await this.wasmClient.createSubject(parameters);
562
+ return this.parseResponse(result);
563
+ }
564
+ // MARK: - Activity Management
565
+ /**
566
+ * Retrieves activities for a specific subject with pagination and sorting.
567
+ *
568
+ * This method allows you to fetch activities associated with a particular subject,
569
+ * with control over pagination and sort order. This is useful for displaying
570
+ * activity history or implementing infinite scroll interfaces.
571
+ *
572
+ * @param subjectId The ID of the subject whose activities to retrieve
573
+ * @param startIndex Zero-based index to start from (for pagination). Use 0 for first page.
574
+ * @param count Number of activities to retrieve per request
575
+ * @param sort Sort order for the results (e.g., "updated_at" for most recent first)
576
+ * @returns An array of activities for the specified subject
577
+ * @throws If the request fails or authentication has expired
578
+ *
579
+ * @example
580
+ * ```typescript
581
+ * // Get the 20 most recent activities for a subject
582
+ * const recentActivities = await client.getActivitiesForSubject(
583
+ * "subject-123",
584
+ * 0,
585
+ * 20,
586
+ * "updated_at"
587
+ * );
588
+ *
589
+ * // Pagination - get the next 20 activities
590
+ * const nextPage = await client.getActivitiesForSubject(
591
+ * "subject-123",
592
+ * 20,
593
+ * 20,
594
+ * "updated_at"
595
+ * );
596
+ * ```
597
+ */
598
+ async getActivitiesForSubject(subjectId, startIndex, count, sort) {
599
+ this.ensureInitialized();
600
+ const result = await this.wasmClient.getActivitiesForSubject(subjectId, startIndex, count, sort);
601
+ return this.parseResponse(result);
602
+ }
603
+ /**
604
+ * Retrieves a specific activity by its ID.
605
+ *
606
+ * Use this method to fetch the complete details of an activity, including
607
+ * its videos, results, and current processing status.
608
+ *
609
+ * @param activityId The unique identifier of the activity
610
+ * @returns The requested activity with all its details
611
+ * @throws If the activity doesn't exist, or if authentication has expired
612
+ *
613
+ * @example
614
+ * ```typescript
615
+ * const activity = await client.getActivity("abc123");
616
+ * console.log(`Activity: ${activity.name ?? "Unnamed"}`);
617
+ * console.log(`Status: ${activity.status}`);
618
+ * console.log(`Videos: ${activity.videos.length}`);
619
+ * ```
620
+ */
621
+ async getActivity(activityId) {
622
+ this.ensureInitialized();
623
+ const result = await this.wasmClient.getActivity(activityId);
624
+ return this.parseResponse(result);
625
+ }
626
+ /**
627
+ * Updates an existing activity.
628
+ *
629
+ * Use this method to modify activity properties such as the name.
630
+ * The activity is updated on the server and the updated version is returned.
631
+ *
632
+ * @param activity The activity to update (with modified properties)
633
+ * @returns The updated activity as stored on the server
634
+ * @throws If the update fails or authentication has expired
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * let activity = await client.getActivity("abc123");
639
+ * // Modify the activity name
640
+ * activity.name = "CMJ Baseline Test";
641
+ * const updated = await client.updateActivity(activity);
642
+ * console.log(`Updated: ${updated.name ?? ""}`);
643
+ * ```
644
+ *
645
+ * @note Not all activity properties can be modified. Only mutable fields
646
+ * (such as `name`) will be updated on the server.
647
+ */
648
+ async updateActivity(activity) {
649
+ this.ensureInitialized();
650
+ const result = await this.wasmClient.updateActivity(activity);
651
+ return this.parseResponse(result);
652
+ }
653
+ /**
654
+ * Deletes an activity from the system.
655
+ *
656
+ * This permanently removes the activity and all its associated data,
657
+ * including videos and analysis results. This action cannot be undone.
658
+ *
659
+ * @param activity The activity to delete
660
+ * @throws If the deletion fails or authentication has expired
661
+ *
662
+ * @example
663
+ * ```typescript
664
+ * const activity = await client.getActivity("abc123");
665
+ * await client.deleteActivity(activity);
666
+ * // Activity and all associated data are now permanently deleted
667
+ * ```
668
+ *
669
+ * @warning This operation is irreversible. All videos, analysis results,
670
+ * and metadata associated with this activity will be permanently lost.
671
+ */
672
+ async deleteActivity(activity) {
673
+ this.ensureInitialized();
674
+ await this.wasmClient.deleteActivity(activity);
675
+ }
676
+ /**
677
+ * Retrieves all available activity tags.
678
+ *
679
+ * Activity tags provide a way to categorize and filter activities.
680
+ * This method returns all tags configured in the system, which can be
681
+ * used for filtering or organizing activities in your application.
682
+ *
683
+ * @returns An array of available activity tags
684
+ * @throws If the request fails or authentication has expired
685
+ *
686
+ * @example
687
+ * ```typescript
688
+ * const tags = await client.getActivityTags();
689
+ * for (const tag of tags) {
690
+ * console.log(`${tag.label}: ${tag.value}`);
691
+ * }
692
+ *
693
+ * // Use tags for filtering or categorization
694
+ * const cmjTag = tags.find(t => t.value === "cmj");
695
+ * ```
696
+ */
697
+ async getActivityTags() {
698
+ this.ensureInitialized();
699
+ const result = await this.wasmClient.getActivityTags();
700
+ return this.parseResponse(result);
701
+ }
702
+ // MARK: - Activities
703
+ /**
704
+ * Retrieves all movement activities associated with the authenticated account.
705
+ *
706
+ * Activities represent individual recording sessions and contain references to
707
+ * captured videos and analysis results. Use this to review past data or
708
+ * fetch analysis for completed activities.
709
+ *
710
+ * @param sessionId Session identifier
711
+ * @returns An array of `Activity` objects
712
+ * @throws If the request fails or authentication has expired
713
+ *
714
+ * @example
715
+ * ```typescript
716
+ * const activities = await client.activityList(session.id);
717
+ *
718
+ * // Find completed activities ready for analysis
719
+ * const completed = activities.filter(t => t.status === "completed");
720
+ *
721
+ * // Access videos and results
722
+ * for (const activity of completed) {
723
+ * console.log(`Activity: ${activity.name ?? activity.id}`);
724
+ * console.log(`Videos: ${activity.videos.length}`);
725
+ * console.log(`Results: ${activity.results.length}`);
726
+ * }
727
+ * ```
728
+ */
729
+ async activityList(sessionId) {
730
+ this.ensureInitialized();
731
+ const result = await this.wasmClient.trialList(sessionId);
732
+ return this.parseResponse(result);
733
+ }
734
+ /**
735
+ * Download video data for a specific activity.
736
+ *
737
+ * Asynchronously fetches all videos associated with a given activity that match the specified type.
738
+ * Videos with invalid URLs or failed downloads are silently excluded from the result.
739
+ *
740
+ * @param activity The activity whose videos should be downloaded
741
+ * @param version The version type of videos to download (default: "synced")
742
+ * @returns An array of video data as Uint8Array. The array may be empty if no valid
743
+ * videos are available or all downloads fail.
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * const activity = // ... obtained activity
748
+ * const videoData = await client.downloadActivityVideos(activity, "raw");
749
+ *
750
+ * for (const data of videoData) {
751
+ * // Process video data
752
+ * }
753
+ * ```
754
+ *
755
+ * @note This method performs concurrent downloads for optimal performance. Individual download
756
+ * failures do not affect other requests.
757
+ */
758
+ async downloadActivityVideos(activity, version = "synced") {
759
+ this.ensureInitialized();
760
+ const result = await this.wasmClient.downloadActivityVideos(activity, version);
761
+ const videos = [];
762
+ for (let i = 0; i < result.length; i++) {
763
+ videos.push(new Uint8Array(result[i]));
764
+ }
765
+ return videos;
766
+ }
767
+ /**
768
+ * Downloads result data files from a processed activity.
769
+ *
770
+ * After an activity completes processing, various result files become available for download.
771
+ * Use this method to retrieve specific types of data (kinematic measurements, visualizations)
772
+ * in their native file formats (JSON, CSV).
773
+ *
774
+ * This method is useful when you need access to raw analysis data rather than the
775
+ * structured metrics provided by analysis result methods.
776
+ *
777
+ * @param activity The completed activity to download data from
778
+ * @param dataTypes The types of result data to download (kinematic, visualization, or both)
779
+ * @returns An array of result files with their formats. Returns an empty array if no
780
+ * results are available or all downloads fail.
781
+ *
782
+ * @example
783
+ * ```typescript
784
+ * // Download kinematic data only
785
+ * const kinematicData = await client.downloadActivityResultData(activity, ["kinematic"]);
786
+ *
787
+ * for (const result of kinematicData) {
788
+ * switch (result.file_type) {
789
+ * case "json":
790
+ * const json = JSON.parse(new TextDecoder().decode(result.data));
791
+ * console.log("Parsed kinematic JSON");
792
+ * break;
793
+ *
794
+ * case "csv":
795
+ * const csvString = new TextDecoder().decode(result.data);
796
+ * console.log(`CSV data:\n${csvString}`);
797
+ * break;
798
+ * }
799
+ * }
800
+ *
801
+ * // Download all available data types
802
+ * const allData = await client.downloadActivityResultData(
803
+ * activity,
804
+ * ["kinematic", "visualization"]
805
+ * );
806
+ * console.log(`Downloaded ${allData.length} result files`);
807
+ * ```
808
+ *
809
+ * @note This method performs concurrent downloads for optimal performance.
810
+ * Individual download failures do not affect other requests and failed downloads
811
+ * are silently excluded from results.
812
+ */
813
+ async downloadActivityResultData(activity, dataTypes) {
814
+ this.ensureInitialized();
815
+ const result = await this.wasmClient.downloadActivityResultData(activity, dataTypes);
816
+ return this.parseResponse(result);
817
+ }
818
+ // MARK: - Recording & Analysis
819
+ /**
820
+ * Starts recording a dynamic movement activity.
821
+ *
822
+ * After completing calibration steps (camera calibration and neutral pose),
823
+ * use this method to begin recording an activity.
824
+ *
825
+ * @param activityName A descriptive name for this activity (e.g., "cmj-test")
826
+ * @param session The session this activity is associated with
827
+ * @returns The newly created activity
828
+ * @throws If recording cannot start (session not calibrated, camera issues, etc.)
829
+ *
830
+ * @example
831
+ * ```typescript
832
+ * // Record a CMJ session
833
+ * const activity = await client.record("cmj-2024", session);
834
+ * // Subject performs CMJ while cameras record
835
+ *
836
+ * // When complete, stop recording
837
+ * await client.stopRecording(session);
838
+ * ```
839
+ */
840
+ async record(activityName, session) {
841
+ this.ensureInitialized();
842
+ const result = await this.wasmClient.record(activityName, session);
843
+ return this.parseResponse(result);
844
+ }
845
+ /**
846
+ * Stops recording of a dynamic movement activity in a session.
847
+ *
848
+ * Call this method when the subject has completed the movement activity.
849
+ *
850
+ * @param session The session to stop recording in
851
+ * @throws If the activity cannot be stopped (invalid session ID, already stopped, etc.)
852
+ *
853
+ * @example
854
+ * ```typescript
855
+ * // After recording is complete
856
+ * await client.stopRecording(session);
857
+ * ```
858
+ */
859
+ async stopRecording(session) {
860
+ this.ensureInitialized();
861
+ await this.wasmClient.stopRecording(session);
862
+ }
863
+ /**
864
+ * Retrieves the current processing status of an activity.
865
+ *
866
+ * Poll this method to determine when an activity is ready for analysis.
867
+ * Activities must complete video upload and processing before analysis can begin.
868
+ *
869
+ * @param activity A completed activity
870
+ * @returns The current processing status
871
+ * @throws Network or authentication errors
872
+ *
873
+ * @example
874
+ * ```typescript
875
+ * const status = await client.getStatus(activity);
876
+ *
877
+ * switch (status.type) {
878
+ * case "ready":
879
+ * console.log("Activity ready for analysis");
880
+ * break;
881
+ * case "processing":
882
+ * console.log("Still processing...");
883
+ * break;
884
+ * case "uploading":
885
+ * console.log(`Uploaded ${status.uploaded}/${status.total} videos`);
886
+ * break;
887
+ * case "failed":
888
+ * console.log("Processing failed");
889
+ * break;
890
+ * }
891
+ * ```
892
+ */
893
+ async getStatus(activity) {
894
+ this.ensureInitialized();
895
+ const result = await this.wasmClient.getStatus(activity);
896
+ return this.parseResponse(result);
897
+ }
898
+ /**
899
+ * Starts an analysis task for a completed activity.
900
+ *
901
+ * The activity must have completed processing (status `.ready`) before analysis can begin.
902
+ * Use the returned `AnalysisTask` to poll for completion.
903
+ *
904
+ * @param analysisType The type of analysis to perform
905
+ * @param activity The activity to analyze
906
+ * @param session The session containing the activity
907
+ * @returns An analysis task for tracking completion
908
+ * @throws Network or authentication errors
909
+ *
910
+ * @example
911
+ * ```typescript
912
+ * const task = await client.startAnalysis(
913
+ * "counter_movement_jump",
914
+ * activity,
915
+ * session
916
+ * );
917
+ *
918
+ * // Poll for completion
919
+ * const status = await client.getAnalysisStatus(task);
920
+ * ```
921
+ */
922
+ async startAnalysis(analysisType, activity, session) {
923
+ this.ensureInitialized();
924
+ const result = await this.wasmClient.startAnalysis(analysisType, activity, session);
925
+ return this.parseResponse(result);
926
+ }
927
+ /**
928
+ * Retrieves the current status of an analysis task.
929
+ *
930
+ * Poll this method to monitor analysis progress. When status is `.completed`,
931
+ * use the returned result tags to download analysis files.
932
+ *
933
+ * @param task The task returned from `startAnalysis`
934
+ * @returns The current analysis status
935
+ * @throws Network or authentication errors
936
+ *
937
+ * @example
938
+ * ```typescript
939
+ * const status = await client.getAnalysisStatus(task);
940
+ *
941
+ * switch (status.type) {
942
+ * case "processing":
943
+ * console.log("Analysis running...");
944
+ * break;
945
+ * case "completed":
946
+ * for (const tag of status.result_tags) {
947
+ * const data = await client.downloadAnalysisResult(activity, tag);
948
+ * }
949
+ * break;
950
+ * case "failed":
951
+ * console.log("Analysis failed");
952
+ * break;
953
+ * }
954
+ * ```
955
+ */
956
+ async getAnalysisStatus(task) {
957
+ this.ensureInitialized();
958
+ const result = await this.wasmClient.getAnalysisStatus(task);
959
+ return this.parseResponse(result);
960
+ }
961
+ /**
962
+ * Downloads an analysis result.
963
+ *
964
+ * Result tags are provided in the `.completed` status from `getAnalysisStatus`.
965
+ * Each tag represents a specific analysis output with structured biomechanical metrics.
966
+ *
967
+ * @param activity The completed and analyzed activity
968
+ * @param resultTag The specific result identifier
969
+ * @returns An `AnalysisResult` containing structured metrics
970
+ * @throws Network or authentication errors
971
+ *
972
+ * @example
973
+ * ```typescript
974
+ * const result = await client.downloadAnalysisResult(
975
+ * activity,
976
+ * "countermovement_jump"
977
+ * );
978
+ *
979
+ * console.log(`Analysis: ${result.analysis_title}`);
980
+ * console.log(`Description: ${result.analysis_description}`);
981
+ *
982
+ * // Access specific metrics
983
+ * if (result.jump_height) {
984
+ * console.log(`Jump Height: ${result.jump_height} cm`);
985
+ * }
986
+ *
987
+ * // Iterate all metrics
988
+ * for (const [key, metric] of Object.entries(result.metrics)) {
989
+ * console.log(`${metric.label}:`, metric.value);
990
+ * }
991
+ * ```
992
+ */
993
+ async downloadAnalysisResult(activity, resultTag) {
994
+ this.ensureInitialized();
995
+ const result = await this.wasmClient.downloadAnalysisResult(activity, resultTag);
996
+ return this.parseResponse(result);
997
+ }
998
+ // MARK: - Utilities
999
+ /**
1000
+ * Parse JSON response from WASM.
1001
+ *
1002
+ * @private
1003
+ * @param value Value from WASM (may be string or object)
1004
+ * @returns Parsed TypeScript object
1005
+ */
1006
+ parseResponse(value) {
1007
+ if (typeof value === "string") {
1008
+ return JSON.parse(value);
1009
+ }
1010
+ return value;
1011
+ }
1012
+ }
1013
+ // MARK: - Exports
1014
+ export * from "./types.js";
1015
+ export { MemoryTokenStorage, LocalStorageTokenStorage };
1016
+ //# sourceMappingURL=index.js.map