A social knowledge tool for researchers built on ATProto
1import { Result, ok, err } from '../../../../shared/core/Result';
2import {
3 IFeedRepository,
4 FeedQueryOptions,
5 PaginatedFeedResult,
6} from '../../domain/IFeedRepository';
7import { FeedActivity } from '../../domain/FeedActivity';
8import { ActivityId } from '../../domain/value-objects/ActivityId';
9
10export class InMemoryFeedRepository implements IFeedRepository {
11 private static instance: InMemoryFeedRepository | null = null;
12 private activities: FeedActivity[] = [];
13
14 private constructor() {}
15
16 public static getInstance(): InMemoryFeedRepository {
17 if (!InMemoryFeedRepository.instance) {
18 InMemoryFeedRepository.instance = new InMemoryFeedRepository();
19 }
20 return InMemoryFeedRepository.instance;
21 }
22
23 public static resetInstance(): void {
24 InMemoryFeedRepository.instance = null;
25 }
26
27 async addActivity(activity: FeedActivity): Promise<Result<void>> {
28 try {
29 this.activities.push(activity);
30 // Sort by creation time descending
31 this.activities.sort(
32 (a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
33 );
34 return ok(undefined);
35 } catch (error) {
36 return err(error as Error);
37 }
38 }
39
40 async getGlobalFeed(
41 options: FeedQueryOptions,
42 ): Promise<Result<PaginatedFeedResult>> {
43 try {
44 const { page, limit, beforeActivityId } = options;
45 let filteredActivities = [...this.activities];
46
47 // Filter by cursor if provided
48 if (beforeActivityId) {
49 const beforeIndex = filteredActivities.findIndex((activity) =>
50 activity.activityId.equals(beforeActivityId),
51 );
52 if (beforeIndex >= 0) {
53 filteredActivities = filteredActivities.slice(beforeIndex + 1);
54 }
55 }
56
57 // Paginate
58 const offset = (page - 1) * limit;
59 const paginatedActivities = filteredActivities.slice(
60 offset,
61 offset + limit,
62 );
63
64 const totalCount = this.activities.length;
65 const hasMore = offset + paginatedActivities.length < totalCount;
66
67 let nextCursor: ActivityId | undefined;
68 if (hasMore && paginatedActivities.length > 0) {
69 nextCursor =
70 paginatedActivities[paginatedActivities.length - 1]!.activityId;
71 }
72
73 return ok({
74 activities: paginatedActivities,
75 totalCount,
76 hasMore,
77 nextCursor,
78 });
79 } catch (error) {
80 return err(error as Error);
81 }
82 }
83
84 async findById(activityId: ActivityId): Promise<Result<FeedActivity | null>> {
85 try {
86 const activity = this.activities.find((a) =>
87 a.activityId.equals(activityId),
88 );
89 return ok(activity || null);
90 } catch (error) {
91 return err(error as Error);
92 }
93 }
94
95 // Test helper methods
96 clear(): void {
97 this.activities = [];
98 }
99
100 getAll(): FeedActivity[] {
101 return [...this.activities];
102 }
103}