Skip to content

Commit 52f74b3

Browse files
committed
MemoryCachedRepository
1 parent b7bc522 commit 52f74b3

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Version 0.3.0
66

77
To be released.
88

9+
- Added `MemoryCachedRepository` class.
10+
911

1012
Version 0.2.0
1113
-------------

docs/concepts/repository.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,31 @@ nodes. No setup is required, making it easy to get started.
9898
> efficient because it doesn't have to go through the [`KvStore`] interface.
9999
100100

101+
`MemoryCachedRepository`
102+
------------------------
103+
104+
*This API is available since BotKit 0.3.0.*
105+
106+
The `MemoryCachedRepository` is a repository decorator that adds an in-memory
107+
cache layer on top of another repository. This is useful for improving
108+
performance by reducing the number of accesses to the underlying persistent
109+
storage, but it increases memory usage. The cache is not persistent and will
110+
be lost when the process exits.
111+
112+
It takes an existing `Repository` instance (like `KvRepository` or even
113+
another `MemoryCachedRepository`) and wraps it. Write operations are performed
114+
on both the underlying repository and the cache. Read operations first check
115+
the cache; if the data is found, it's returned directly. Otherwise, the data
116+
is fetched from the underlying repository, stored in the cache, and then
117+
returned.
118+
119+
> [!NOTE]
120+
> List operations like `getMessages` and `getFollowers`, and count operations
121+
> like `countMessages` and `countFollowers` are not cached due to the
122+
> complexity of handling various filtering and pagination options. These
123+
> operations always delegate directly to the underlying repository.
124+
125+
101126
Implementing a custom repository
102127
--------------------------------
103128

src/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export {
7676
Announce,
7777
Create,
7878
KvRepository,
79+
MemoryCachedRepository,
7980
MemoryRepository,
8081
type Repository,
8182
} from "./repository.ts";

src/repository.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { assertEquals } from "@std/assert/equals";
2828
import { assertFalse } from "@std/assert/false";
2929
import {
3030
KvRepository,
31+
MemoryCachedRepository,
3132
MemoryRepository,
3233
type Repository,
3334
} from "./repository.ts";
@@ -40,9 +41,14 @@ function createMemoryRepository(): Repository {
4041
return new MemoryRepository();
4142
}
4243

44+
function createMemoryCachedRepository(): Repository {
45+
return new MemoryCachedRepository(createKvRepository());
46+
}
47+
4348
const factories: Record<string, () => Repository> = {
4449
KvRepository: createKvRepository,
4550
MemoryRepository: createMemoryRepository,
51+
MemoryCachedRepository: createMemoryCachedRepository,
4652
};
4753

4854
const keyPairs: CryptoKeyPair[] = [

src/repository.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,3 +762,191 @@ export class MemoryRepository implements Repository {
762762
return Promise.resolve(this.followees[followeeId.href]);
763763
}
764764
}
765+
766+
/**
767+
* A repository decorator that adds an in-memory cache layer on top of another
768+
* repository. This is useful for improving performance by reducing the number
769+
* of accesses to the underlying persistent storage, but it increases memory
770+
* usage. The cache is not persistent and will be lost when the process exits.
771+
*
772+
* Note: List operations like `getMessages` and `getFollowers`, and count
773+
* operations like `countMessages` and `countFollowers` are not cached and
774+
* always delegate to the underlying repository.
775+
*/
776+
export class MemoryCachedRepository implements Repository {
777+
private underlying: Repository;
778+
private cache: MemoryRepository;
779+
780+
/**
781+
* Creates a new memory-cached repository.
782+
* @param underlying The underlying repository to cache.
783+
* @param cache An optional `MemoryRepository` instance to use as the cache.
784+
* If not provided, a new one will be created internally.
785+
*/
786+
constructor(underlying: Repository, cache?: MemoryRepository) {
787+
this.underlying = underlying;
788+
this.cache = cache ?? new MemoryRepository();
789+
}
790+
791+
async setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void> {
792+
await this.underlying.setKeyPairs(keyPairs);
793+
await this.cache.setKeyPairs(keyPairs);
794+
}
795+
796+
async getKeyPairs(): Promise<CryptoKeyPair[] | undefined> {
797+
let keyPairs = await this.cache.getKeyPairs();
798+
if (keyPairs === undefined) {
799+
keyPairs = await this.underlying.getKeyPairs();
800+
if (keyPairs !== undefined) await this.cache.setKeyPairs(keyPairs);
801+
}
802+
return keyPairs;
803+
}
804+
805+
async addMessage(id: Uuid, activity: Create | Announce): Promise<void> {
806+
await this.underlying.addMessage(id, activity);
807+
await this.cache.addMessage(id, activity);
808+
}
809+
810+
async updateMessage(
811+
id: Uuid,
812+
updater: (
813+
existing: Create | Announce,
814+
) => Create | Announce | undefined | Promise<Create | Announce | undefined>,
815+
): Promise<boolean> {
816+
// Apply update to underlying first
817+
const updated = await this.underlying.updateMessage(id, updater);
818+
if (updated) {
819+
// If successful, fetch the updated message and update the cache
820+
const updatedMessage = await this.underlying.getMessage(id);
821+
if (updatedMessage) {
822+
await this.cache.addMessage(id, updatedMessage); // Use addMessage which acts like set
823+
} else {
824+
// Should not happen if updateMessage returned true, but handle defensively
825+
await this.cache.removeMessage(id);
826+
}
827+
}
828+
return updated;
829+
}
830+
831+
async removeMessage(id: Uuid): Promise<Create | Announce | undefined> {
832+
const removedActivity = await this.underlying.removeMessage(id);
833+
if (removedActivity !== undefined) {
834+
await this.cache.removeMessage(id);
835+
}
836+
return removedActivity;
837+
}
838+
839+
// getMessages is not cached due to complexity with options
840+
getMessages(
841+
options?: RepositoryGetMessagesOptions,
842+
): AsyncIterable<Create | Announce> {
843+
return this.underlying.getMessages(options);
844+
}
845+
846+
async getMessage(id: Uuid): Promise<Create | Announce | undefined> {
847+
let message = await this.cache.getMessage(id);
848+
if (message === undefined) {
849+
message = await this.underlying.getMessage(id);
850+
if (message !== undefined) {
851+
await this.cache.addMessage(id, message); // Use addMessage which acts like set
852+
}
853+
}
854+
return message;
855+
}
856+
857+
// countMessages is not cached
858+
countMessages(): Promise<number> {
859+
return this.underlying.countMessages();
860+
}
861+
862+
async addFollower(followId: URL, follower: Actor): Promise<void> {
863+
await this.underlying.addFollower(followId, follower);
864+
await this.cache.addFollower(followId, follower);
865+
}
866+
867+
async removeFollower(
868+
followId: URL,
869+
followerId: URL,
870+
): Promise<Actor | undefined> {
871+
const removedFollower = await this.underlying.removeFollower(
872+
followId,
873+
followerId,
874+
);
875+
if (removedFollower !== undefined) {
876+
await this.cache.removeFollower(followId, followerId);
877+
}
878+
return removedFollower;
879+
}
880+
881+
async hasFollower(followerId: URL): Promise<boolean> {
882+
// Check cache first for potentially faster response
883+
if (await this.cache.hasFollower(followerId)) {
884+
return true;
885+
}
886+
// If not in cache, check underlying and update cache if found
887+
const exists = await this.underlying.hasFollower(followerId);
888+
// Note: We don't automatically add to cache here, as we don't have the Actor object
889+
// It will be cached if addFollower is called or if getFollowers iterates over it (though getFollowers isn't cached)
890+
return exists;
891+
}
892+
893+
// getFollowers is not cached due to complexity with options
894+
getFollowers(options?: RepositoryGetFollowersOptions): AsyncIterable<Actor> {
895+
// We could potentially cache followers as they are iterated,
896+
// but for simplicity, delegate directly for now.
897+
return this.underlying.getFollowers(options);
898+
}
899+
900+
// countFollowers is not cached
901+
countFollowers(): Promise<number> {
902+
return this.underlying.countFollowers();
903+
}
904+
905+
async addSentFollow(id: Uuid, follow: Follow): Promise<void> {
906+
await this.underlying.addSentFollow(id, follow);
907+
await this.cache.addSentFollow(id, follow);
908+
}
909+
910+
async removeSentFollow(id: Uuid): Promise<Follow | undefined> {
911+
const removedFollow = await this.underlying.removeSentFollow(id);
912+
if (removedFollow !== undefined) {
913+
await this.cache.removeSentFollow(id);
914+
}
915+
return removedFollow;
916+
}
917+
918+
async getSentFollow(id: Uuid): Promise<Follow | undefined> {
919+
let follow = await this.cache.getSentFollow(id);
920+
if (follow === undefined) {
921+
follow = await this.underlying.getSentFollow(id);
922+
if (follow !== undefined) {
923+
await this.cache.addSentFollow(id, follow);
924+
}
925+
}
926+
return follow;
927+
}
928+
929+
async addFollowee(followeeId: URL, follow: Follow): Promise<void> {
930+
await this.underlying.addFollowee(followeeId, follow);
931+
await this.cache.addFollowee(followeeId, follow);
932+
}
933+
934+
async removeFollowee(followeeId: URL): Promise<Follow | undefined> {
935+
const removedFollow = await this.underlying.removeFollowee(followeeId);
936+
if (removedFollow !== undefined) {
937+
await this.cache.removeFollowee(followeeId);
938+
}
939+
return removedFollow;
940+
}
941+
942+
async getFollowee(followeeId: URL): Promise<Follow | undefined> {
943+
let follow = await this.cache.getFollowee(followeeId);
944+
if (follow === undefined) {
945+
follow = await this.underlying.getFollowee(followeeId);
946+
if (follow !== undefined) {
947+
await this.cache.addFollowee(followeeId, follow);
948+
}
949+
}
950+
return follow;
951+
}
952+
}

0 commit comments

Comments
 (0)