@ewanc26/atproto 0.2.9 → 0.2.11

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/src/fetch.ts CHANGED
@@ -18,7 +18,13 @@ import type {
18
18
  SifaProject,
19
19
  SifaLanguage,
20
20
  SifaCertification,
21
- SifaExternalAccount
21
+ SifaExternalAccount,
22
+ SifaPosition,
23
+ SifaEducation,
24
+ SifaVolunteering,
25
+ SifaHonor,
26
+ SifaCourse,
27
+ SifaPublication
22
28
  } from './types.js';
23
29
 
24
30
  export async function fetchProfile(did: string, fetchFn?: typeof fetch): Promise<ProfileData> {
@@ -369,6 +375,295 @@ export async function fetchRecentPopfeedReviews(
369
375
  }
370
376
  }
371
377
 
378
+ export async function fetchSifaPositions(
379
+ did: string,
380
+ fetchFn?: typeof fetch
381
+ ): Promise<SifaPosition[]> {
382
+ const cacheKey = `sifa:positions:${did}`;
383
+ const cached = cache.get<SifaPosition[]>(cacheKey);
384
+ if (cached) return cached;
385
+
386
+ try {
387
+ const records = await withFallback(
388
+ did,
389
+ async (agent) => {
390
+ const response = await agent.com.atproto.repo.listRecords({
391
+ repo: did,
392
+ collection: 'id.sifa.profile.position',
393
+ limit: 100
394
+ });
395
+ return response.data.records;
396
+ },
397
+ true,
398
+ fetchFn
399
+ );
400
+
401
+ const data: SifaPosition[] = records.map((record) => {
402
+ const value = record.value as any;
403
+ return {
404
+ company: value.company,
405
+ companyDid: value.companyDid,
406
+ title: value.title,
407
+ description: value.description,
408
+ employmentType: value.employmentType,
409
+ workplaceType: value.workplaceType,
410
+ location: value.location,
411
+ startedAt: value.startedAt,
412
+ endedAt: value.endedAt,
413
+ skills: value.skills,
414
+ isPrimary: value.isPrimary,
415
+ uri: record.uri
416
+ };
417
+ });
418
+
419
+ data.sort((a, b) => {
420
+ if (!a.startedAt) return 1;
421
+ if (!b.startedAt) return -1;
422
+ return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
423
+ });
424
+
425
+ cache.set(cacheKey, data);
426
+ return data;
427
+ } catch {
428
+ return [];
429
+ }
430
+ }
431
+
432
+ export async function fetchSifaEducation(
433
+ did: string,
434
+ fetchFn?: typeof fetch
435
+ ): Promise<SifaEducation[]> {
436
+ const cacheKey = `sifa:education:${did}`;
437
+ const cached = cache.get<SifaEducation[]>(cacheKey);
438
+ if (cached) return cached;
439
+
440
+ try {
441
+ const records = await withFallback(
442
+ did,
443
+ async (agent) => {
444
+ const response = await agent.com.atproto.repo.listRecords({
445
+ repo: did,
446
+ collection: 'id.sifa.profile.education',
447
+ limit: 100
448
+ });
449
+ return response.data.records;
450
+ },
451
+ true,
452
+ fetchFn
453
+ );
454
+
455
+ const data: SifaEducation[] = records.map((record) => {
456
+ const value = record.value as any;
457
+ return {
458
+ institution: value.institution,
459
+ institutionDid: value.institutionDid,
460
+ degree: value.degree,
461
+ fieldOfStudy: value.fieldOfStudy,
462
+ grade: value.grade,
463
+ activities: value.activities,
464
+ description: value.description,
465
+ location: value.location,
466
+ startedAt: value.startedAt,
467
+ endedAt: value.endedAt,
468
+ uri: record.uri
469
+ };
470
+ });
471
+
472
+ data.sort((a, b) => {
473
+ const aEnd = a.endedAt ? new Date(a.endedAt).getTime() : Date.now();
474
+ const bEnd = b.endedAt ? new Date(b.endedAt).getTime() : Date.now();
475
+ return bEnd - aEnd;
476
+ });
477
+
478
+ cache.set(cacheKey, data);
479
+ return data;
480
+ } catch {
481
+ return [];
482
+ }
483
+ }
484
+
485
+ export async function fetchSifaVolunteering(
486
+ did: string,
487
+ fetchFn?: typeof fetch
488
+ ): Promise<SifaVolunteering[]> {
489
+ const cacheKey = `sifa:volunteering:${did}`;
490
+ const cached = cache.get<SifaVolunteering[]>(cacheKey);
491
+ if (cached) return cached;
492
+
493
+ try {
494
+ const records = await withFallback(
495
+ did,
496
+ async (agent) => {
497
+ const response = await agent.com.atproto.repo.listRecords({
498
+ repo: did,
499
+ collection: 'id.sifa.profile.volunteering',
500
+ limit: 100
501
+ });
502
+ return response.data.records;
503
+ },
504
+ true,
505
+ fetchFn
506
+ );
507
+
508
+ const data: SifaVolunteering[] = records.map((record) => {
509
+ const value = record.value as any;
510
+ return {
511
+ organization: value.organization,
512
+ organizationDid: value.organizationDid,
513
+ role: value.role,
514
+ cause: value.cause,
515
+ description: value.description,
516
+ startedAt: value.startedAt,
517
+ endedAt: value.endedAt,
518
+ uri: record.uri
519
+ };
520
+ });
521
+
522
+ cache.set(cacheKey, data);
523
+ return data;
524
+ } catch {
525
+ return [];
526
+ }
527
+ }
528
+
529
+ export async function fetchSifaHonors(
530
+ did: string,
531
+ fetchFn?: typeof fetch
532
+ ): Promise<SifaHonor[]> {
533
+ const cacheKey = `sifa:honors:${did}`;
534
+ const cached = cache.get<SifaHonor[]>(cacheKey);
535
+ if (cached) return cached;
536
+
537
+ try {
538
+ const records = await withFallback(
539
+ did,
540
+ async (agent) => {
541
+ const response = await agent.com.atproto.repo.listRecords({
542
+ repo: did,
543
+ collection: 'id.sifa.profile.honor',
544
+ limit: 100
545
+ });
546
+ return response.data.records;
547
+ },
548
+ true,
549
+ fetchFn
550
+ );
551
+
552
+ const data: SifaHonor[] = records.map((record) => {
553
+ const value = record.value as any;
554
+ return {
555
+ title: value.title,
556
+ issuer: value.issuer,
557
+ issuerDid: value.issuerDid,
558
+ description: value.description,
559
+ awardedAt: value.awardedAt,
560
+ uri: record.uri
561
+ };
562
+ });
563
+
564
+ data.sort((a, b) => {
565
+ if (!a.awardedAt) return 1;
566
+ if (!b.awardedAt) return -1;
567
+ return new Date(b.awardedAt).getTime() - new Date(a.awardedAt).getTime();
568
+ });
569
+
570
+ cache.set(cacheKey, data);
571
+ return data;
572
+ } catch {
573
+ return [];
574
+ }
575
+ }
576
+
577
+ export async function fetchSifaCourses(
578
+ did: string,
579
+ fetchFn?: typeof fetch
580
+ ): Promise<SifaCourse[]> {
581
+ const cacheKey = `sifa:courses:${did}`;
582
+ const cached = cache.get<SifaCourse[]>(cacheKey);
583
+ if (cached) return cached;
584
+
585
+ try {
586
+ const records = await withFallback(
587
+ did,
588
+ async (agent) => {
589
+ const response = await agent.com.atproto.repo.listRecords({
590
+ repo: did,
591
+ collection: 'id.sifa.profile.course',
592
+ limit: 100
593
+ });
594
+ return response.data.records;
595
+ },
596
+ true,
597
+ fetchFn
598
+ );
599
+
600
+ const data: SifaCourse[] = records.map((record) => {
601
+ const value = record.value as any;
602
+ return {
603
+ name: value.name,
604
+ number: value.number,
605
+ institution: value.institution,
606
+ education: value.education,
607
+ uri: record.uri
608
+ };
609
+ });
610
+
611
+ cache.set(cacheKey, data);
612
+ return data;
613
+ } catch {
614
+ return [];
615
+ }
616
+ }
617
+
618
+ export async function fetchSifaPublications(
619
+ did: string,
620
+ fetchFn?: typeof fetch
621
+ ): Promise<SifaPublication[]> {
622
+ const cacheKey = `sifa:publications:${did}`;
623
+ const cached = cache.get<SifaPublication[]>(cacheKey);
624
+ if (cached) return cached;
625
+
626
+ try {
627
+ const records = await withFallback(
628
+ did,
629
+ async (agent) => {
630
+ const response = await agent.com.atproto.repo.listRecords({
631
+ repo: did,
632
+ collection: 'id.sifa.profile.publication',
633
+ limit: 100
634
+ });
635
+ return response.data.records;
636
+ },
637
+ true,
638
+ fetchFn
639
+ );
640
+
641
+ const data: SifaPublication[] = records.map((record) => {
642
+ const value = record.value as any;
643
+ return {
644
+ title: value.title,
645
+ publisher: value.publisher,
646
+ url: value.url,
647
+ description: value.description,
648
+ authors: value.authors,
649
+ publishedAt: value.publishedAt,
650
+ uri: record.uri
651
+ };
652
+ });
653
+
654
+ data.sort((a, b) => {
655
+ if (!a.publishedAt) return 1;
656
+ if (!b.publishedAt) return -1;
657
+ return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
658
+ });
659
+
660
+ cache.set(cacheKey, data);
661
+ return data;
662
+ } catch {
663
+ return [];
664
+ }
665
+ }
666
+
372
667
  export async function fetchTangledRepos(
373
668
  did: string,
374
669
  fetchFn?: typeof fetch
package/src/index.ts CHANGED
@@ -44,7 +44,14 @@ export type {
44
44
  SifaLanguage,
45
45
  SifaCertification,
46
46
  SifaExternalAccount,
47
- SifaLocation
47
+ SifaLocation,
48
+ SifaPosition,
49
+ SifaEducation,
50
+ SifaVolunteering,
51
+ SifaHonor,
52
+ SifaCourse,
53
+ SifaPublication,
54
+ SifaPublicationAuthor
48
55
  } from './types.js';
49
56
 
50
57
  export {
@@ -60,7 +67,13 @@ export {
60
67
  fetchSifaProjects,
61
68
  fetchSifaLanguages,
62
69
  fetchSifaCertifications,
63
- fetchSifaExternalAccounts
70
+ fetchSifaExternalAccounts,
71
+ fetchSifaPositions,
72
+ fetchSifaEducation,
73
+ fetchSifaVolunteering,
74
+ fetchSifaHonors,
75
+ fetchSifaCourses,
76
+ fetchSifaPublications
64
77
  } from './fetch.js';
65
78
 
66
79
  export {
@@ -1,5 +1,22 @@
1
1
  import { cache } from './cache.js';
2
2
 
3
+ /** Timeout for individual artwork API calls (ms). */
4
+ const ARTWORK_TIMEOUT = 5_000;
5
+
6
+ /**
7
+ * Wraps a promise with a timeout. Rejects with a TimeoutError if the promise
8
+ * doesn't settle within `ms` milliseconds.
9
+ */
10
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
11
+ return new Promise<T>((resolve, reject) => {
12
+ const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
13
+ promise.then(
14
+ (value) => { clearTimeout(timer); resolve(value); },
15
+ (err) => { clearTimeout(timer); reject(err); }
16
+ );
17
+ });
18
+ }
19
+
3
20
  interface MusicBrainzRelease {
4
21
  id: string;
5
22
  score: number;
@@ -147,6 +164,7 @@ export async function searchLastFmArtwork(
147
164
 
148
165
  /**
149
166
  * Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
167
+ * Each step is bounded by ARTWORK_TIMEOUT to prevent serverless function timeouts.
150
168
  */
151
169
  export async function findArtwork(
152
170
  trackName: string,
@@ -161,7 +179,7 @@ export async function findArtwork(
161
179
  if (releaseMbId) {
162
180
  const caaUrl = buildCoverArtUrl(releaseMbId, 500);
163
181
  try {
164
- const res = await _fetch(caaUrl, { method: 'HEAD' });
182
+ const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
165
183
  if (res.ok) return caaUrl;
166
184
  } catch { /* continue */ }
167
185
  }
@@ -171,7 +189,7 @@ export async function findArtwork(
171
189
  if (mbId) {
172
190
  const caaUrl = buildCoverArtUrl(mbId, 500);
173
191
  try {
174
- const res = await _fetch(caaUrl, { method: 'HEAD' });
192
+ const res = await withTimeout(_fetch(caaUrl, { method: 'HEAD' }), ARTWORK_TIMEOUT);
175
193
  if (res.ok) return caaUrl;
176
194
  } catch { /* continue */ }
177
195
  }
package/src/types.ts CHANGED
@@ -377,3 +377,75 @@ export interface SifaExternalAccount {
377
377
  isPrimary?: boolean;
378
378
  uri: string;
379
379
  }
380
+
381
+ export interface SifaPosition {
382
+ company: string;
383
+ companyDid?: string;
384
+ title: string;
385
+ description?: string;
386
+ employmentType?: string;
387
+ workplaceType?: string;
388
+ location?: SifaLocation;
389
+ startedAt: string;
390
+ endedAt?: string;
391
+ skills?: string[];
392
+ isPrimary?: boolean;
393
+ uri: string;
394
+ }
395
+
396
+ export interface SifaEducation {
397
+ institution: string;
398
+ institutionDid?: string;
399
+ degree?: string;
400
+ fieldOfStudy?: string;
401
+ grade?: string;
402
+ activities?: string;
403
+ description?: string;
404
+ location?: SifaLocation;
405
+ startedAt?: string;
406
+ endedAt?: string;
407
+ uri: string;
408
+ }
409
+
410
+ export interface SifaVolunteering {
411
+ organization: string;
412
+ organizationDid?: string;
413
+ role?: string;
414
+ cause?: string;
415
+ description?: string;
416
+ startedAt?: string;
417
+ endedAt?: string;
418
+ uri: string;
419
+ }
420
+
421
+ export interface SifaHonor {
422
+ title: string;
423
+ issuer?: string;
424
+ issuerDid?: string;
425
+ description?: string;
426
+ awardedAt?: string;
427
+ uri: string;
428
+ }
429
+
430
+ export interface SifaCourse {
431
+ name: string;
432
+ number?: string;
433
+ institution?: string;
434
+ education?: string;
435
+ uri: string;
436
+ }
437
+
438
+ export interface SifaPublicationAuthor {
439
+ name: string;
440
+ did?: string;
441
+ }
442
+
443
+ export interface SifaPublication {
444
+ title: string;
445
+ publisher?: string;
446
+ url?: string;
447
+ description?: string;
448
+ authors?: SifaPublicationAuthor[];
449
+ publishedAt?: string;
450
+ uri: string;
451
+ }