mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-30 10:54:34 +01:00
refactor: add effect-ts and refactor codes
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { claudeCodeViewerCacheDirPath } from "../../service/paths";
|
||||
|
||||
const saveSchema = z.array(z.tuple([z.string(), z.unknown()]));
|
||||
|
||||
export class FileCacheStorage<const T> {
|
||||
private storage = new Map<string, T>();
|
||||
|
||||
private constructor(private readonly key: string) {}
|
||||
|
||||
public static load<const LoadSchema>(
|
||||
key: string,
|
||||
schema: z.ZodType<LoadSchema>,
|
||||
) {
|
||||
const instance = new FileCacheStorage<LoadSchema>(key);
|
||||
|
||||
if (!existsSync(claudeCodeViewerCacheDirPath)) {
|
||||
mkdirSync(claudeCodeViewerCacheDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
if (!existsSync(instance.cacheFilePath)) {
|
||||
writeFileSync(instance.cacheFilePath, "[]");
|
||||
} else {
|
||||
const content = readFileSync(instance.cacheFilePath, "utf-8");
|
||||
const parsed = saveSchema.safeParse(JSON.parse(content));
|
||||
|
||||
if (!parsed.success) {
|
||||
writeFileSync(instance.cacheFilePath, "[]");
|
||||
} else {
|
||||
for (const [key, value] of parsed.data) {
|
||||
const parsedValue = schema.safeParse(value);
|
||||
if (!parsedValue.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
instance.storage.set(key, parsedValue.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private get cacheFilePath() {
|
||||
return resolve(claudeCodeViewerCacheDirPath, `${this.key}.json`);
|
||||
}
|
||||
|
||||
private asSaveFormat() {
|
||||
return JSON.stringify(Array.from(this.storage.entries()));
|
||||
}
|
||||
|
||||
private async syncToFile() {
|
||||
await writeFile(this.cacheFilePath, this.asSaveFormat());
|
||||
}
|
||||
|
||||
public get(key: string) {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
public save(key: string, value: T) {
|
||||
const previous = this.asSaveFormat();
|
||||
this.storage.set(key, value);
|
||||
|
||||
if (previous === this.asSaveFormat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.syncToFile();
|
||||
}
|
||||
|
||||
public invalidate(key: string) {
|
||||
if (!this.storage.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(key);
|
||||
void this.syncToFile();
|
||||
}
|
||||
}
|
||||
64
src/server/lib/storage/FileCacheStorage/PersistantService.ts
Normal file
64
src/server/lib/storage/FileCacheStorage/PersistantService.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { resolve } from "node:path";
|
||||
import { FileSystem } from "@effect/platform";
|
||||
import { Context, Effect, Layer } from "effect";
|
||||
import { z } from "zod";
|
||||
import { claudeCodeViewerCacheDirPath } from "../../../service/paths";
|
||||
|
||||
const saveSchema = z.array(z.tuple([z.string(), z.unknown()]));
|
||||
|
||||
const getCacheFilePath = (key: string) =>
|
||||
resolve(claudeCodeViewerCacheDirPath, `${key}.json`);
|
||||
|
||||
const load = (key: string) => {
|
||||
const cacheFilePath = getCacheFilePath(key);
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem;
|
||||
|
||||
if (!(yield* fs.exists(claudeCodeViewerCacheDirPath))) {
|
||||
yield* fs.makeDirectory(claudeCodeViewerCacheDirPath, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(yield* fs.exists(cacheFilePath))) {
|
||||
yield* fs.writeFileString(cacheFilePath, "[]");
|
||||
} else {
|
||||
const content = yield* fs.readFileString(cacheFilePath);
|
||||
const parsed = saveSchema.safeParse(JSON.parse(content));
|
||||
|
||||
if (!parsed.success) {
|
||||
yield* fs.writeFileString(cacheFilePath, "[]");
|
||||
} else {
|
||||
parsed.data;
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const save = (key: string, entries: readonly [string, unknown][]) => {
|
||||
const cacheFilePath = getCacheFilePath(key);
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem;
|
||||
yield* fs.writeFileString(cacheFilePath, JSON.stringify(entries));
|
||||
});
|
||||
};
|
||||
|
||||
export class PersistentService extends Context.Tag("PersistentService")<
|
||||
PersistentService,
|
||||
{
|
||||
readonly load: typeof load;
|
||||
readonly save: typeof save;
|
||||
}
|
||||
>() {
|
||||
static Live = Layer.succeed(this, {
|
||||
load,
|
||||
save,
|
||||
});
|
||||
}
|
||||
|
||||
export type IPersistentService = Context.Tag.Service<PersistentService>;
|
||||
516
src/server/lib/storage/FileCacheStorage/index.test.ts
Normal file
516
src/server/lib/storage/FileCacheStorage/index.test.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
import { FileSystem } from "@effect/platform";
|
||||
import { Effect, Layer, Ref } from "effect";
|
||||
import { z } from "zod";
|
||||
import { FileCacheStorage, makeFileCacheStorageLayer } from "./index";
|
||||
import { PersistentService } from "./PersistantService";
|
||||
|
||||
// Schema for testing
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
const FileSystemMock = FileSystem.layerNoop({});
|
||||
|
||||
describe("FileCacheStorage", () => {
|
||||
describe("basic operations", () => {
|
||||
it("can save and retrieve data with set and get", async () => {
|
||||
// PersistentService mock (empty data)
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// Save data
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
|
||||
// Retrieve data
|
||||
const user = yield* cache.get("user-1");
|
||||
return user;
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when retrieving non-existent key", async () => {
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
return yield* cache.get("non-existent");
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can delete data with invalidate", async () => {
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// Save data
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
|
||||
// Delete data
|
||||
yield* cache.invalidate("user-1");
|
||||
|
||||
// Returns undefined after deletion
|
||||
return yield* cache.get("user-1");
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getAll ですべてのデータを取得できる", async () => {
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// 複数のデータを保存
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
yield* cache.set("user-2", {
|
||||
id: "user-2",
|
||||
name: "Bob",
|
||||
email: "bob@example.com",
|
||||
});
|
||||
|
||||
// すべてのデータを取得
|
||||
return yield* cache.getAll();
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get("user-1")).toEqual({
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
expect(result.get("user-2")).toEqual({
|
||||
id: "user-2",
|
||||
name: "Bob",
|
||||
email: "bob@example.com",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("永続化データの読み込み", () => {
|
||||
it("初期化時に永続化データを読み込む", async () => {
|
||||
// 永続化データを返すモック
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () =>
|
||||
Effect.succeed([
|
||||
[
|
||||
"user-1",
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
},
|
||||
],
|
||||
[
|
||||
"user-2",
|
||||
{
|
||||
id: "user-2",
|
||||
name: "Bob",
|
||||
email: "bob@example.com",
|
||||
},
|
||||
],
|
||||
] as const),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
return yield* cache.getAll();
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get("user-1")?.name).toBe("Alice");
|
||||
expect(result.get("user-2")?.name).toBe("Bob");
|
||||
});
|
||||
|
||||
it("スキーマバリデーションに失敗したデータは無視される", async () => {
|
||||
// 不正なデータを含む永続化データ
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () =>
|
||||
Effect.succeed([
|
||||
[
|
||||
"user-1",
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
},
|
||||
],
|
||||
[
|
||||
"user-invalid",
|
||||
{
|
||||
id: "invalid",
|
||||
name: "Invalid",
|
||||
// email が無い(バリデーションエラー)
|
||||
},
|
||||
],
|
||||
[
|
||||
"user-2",
|
||||
{
|
||||
id: "user-2",
|
||||
name: "Bob",
|
||||
email: "invalid-email", // 不正なメールアドレス
|
||||
},
|
||||
],
|
||||
] as const),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
return yield* cache.getAll();
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 有効なデータのみ読み込まれる
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get("user-1")?.name).toBe("Alice");
|
||||
expect(result.get("user-invalid")).toBeUndefined();
|
||||
expect(result.get("user-2")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("永続化への同期", () => {
|
||||
it("set でデータを保存すると save が呼ばれる", async () => {
|
||||
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
|
||||
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Ref.update(saveCallsRef, (n) => n + 1);
|
||||
}),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
|
||||
// バックグラウンド実行を待つために少し待機
|
||||
yield* Effect.sleep("10 millis");
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const saveCalls = await Effect.runPromise(Ref.get(saveCallsRef));
|
||||
expect(saveCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("同じ値を set しても save は呼ばれない(差分検出)", async () => {
|
||||
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
|
||||
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () =>
|
||||
Effect.succeed([
|
||||
[
|
||||
"user-1",
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
},
|
||||
],
|
||||
] as const),
|
||||
save: () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Ref.update(saveCallsRef, (n) => n + 1);
|
||||
}),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// 既に存在する同じ値を set
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
|
||||
// バックグラウンド実行を待つために少し待機
|
||||
yield* Effect.sleep("10 millis");
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const saveCalls = await Effect.runPromise(Ref.get(saveCallsRef));
|
||||
// 差分がないので save は呼ばれない
|
||||
expect(saveCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("invalidate でデータを削除すると save が呼ばれる", async () => {
|
||||
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
|
||||
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () =>
|
||||
Effect.succeed([
|
||||
[
|
||||
"user-1",
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
},
|
||||
],
|
||||
] as const),
|
||||
save: () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Ref.update(saveCallsRef, (n) => n + 1);
|
||||
}),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
yield* cache.invalidate("user-1");
|
||||
|
||||
// バックグラウンド実行を待つために少し待機
|
||||
yield* Effect.sleep("10 millis");
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const saveCalls = await Effect.runPromise(Ref.get(saveCallsRef));
|
||||
expect(saveCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("存在しないキーを invalidate しても save は呼ばれない", async () => {
|
||||
const saveCallsRef = await Effect.runPromise(Ref.make<number>(0));
|
||||
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () => Effect.succeed([]),
|
||||
save: () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Ref.update(saveCallsRef, (n) => n + 1);
|
||||
}),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// 存在しないキーを invalidate
|
||||
yield* cache.invalidate("non-existent");
|
||||
|
||||
// バックグラウンド実行を待つために少し待機
|
||||
yield* Effect.sleep("10 millis");
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const saveCalls = await Effect.runPromise(Ref.get(saveCallsRef));
|
||||
// 存在しないキーなので save は呼ばれない
|
||||
expect(saveCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("複雑なシナリオ", () => {
|
||||
it("複数の操作を順次実行できる", async () => {
|
||||
const PersistentServiceMock = Layer.succeed(PersistentService, {
|
||||
load: () =>
|
||||
Effect.succeed([
|
||||
[
|
||||
"user-1",
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
},
|
||||
],
|
||||
] as const),
|
||||
save: () => Effect.void,
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const cache = yield* FileCacheStorage<User>();
|
||||
|
||||
// 初期データの確認
|
||||
const initial = yield* cache.getAll();
|
||||
expect(initial.size).toBe(1);
|
||||
|
||||
// 新しいユーザーを追加
|
||||
yield* cache.set("user-2", {
|
||||
id: "user-2",
|
||||
name: "Bob",
|
||||
email: "bob@example.com",
|
||||
});
|
||||
|
||||
// 既存のユーザーを更新
|
||||
yield* cache.set("user-1", {
|
||||
id: "user-1",
|
||||
name: "Alice Updated",
|
||||
email: "alice.updated@example.com",
|
||||
});
|
||||
|
||||
// すべてのデータを取得
|
||||
const afterUpdate = yield* cache.getAll();
|
||||
expect(afterUpdate.size).toBe(2);
|
||||
expect(afterUpdate.get("user-1")?.name).toBe("Alice Updated");
|
||||
expect(afterUpdate.get("user-2")?.name).toBe("Bob");
|
||||
|
||||
// ユーザーを削除
|
||||
yield* cache.invalidate("user-1");
|
||||
|
||||
// 削除後の確認
|
||||
const afterDelete = yield* cache.getAll();
|
||||
expect(afterDelete.size).toBe(1);
|
||||
expect(afterDelete.get("user-1")).toBeUndefined();
|
||||
expect(afterDelete.get("user-2")?.name).toBe("Bob");
|
||||
});
|
||||
|
||||
await Effect.runPromise(
|
||||
program.pipe(
|
||||
Effect.provide(
|
||||
makeFileCacheStorageLayer("test-users", UserSchema).pipe(
|
||||
Layer.provide(PersistentServiceMock),
|
||||
Layer.provide(FileSystemMock),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
src/server/lib/storage/FileCacheStorage/index.ts
Normal file
94
src/server/lib/storage/FileCacheStorage/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { FileSystem } from "@effect/platform";
|
||||
import { Context, Effect, Layer, Ref, Runtime } from "effect";
|
||||
import type { z } from "zod";
|
||||
import { PersistentService } from "./PersistantService";
|
||||
|
||||
export interface FileCacheStorageService<T> {
|
||||
readonly get: (key: string) => Effect.Effect<T | undefined>;
|
||||
readonly set: (key: string, value: T) => Effect.Effect<void>;
|
||||
readonly invalidate: (key: string) => Effect.Effect<void>;
|
||||
readonly getAll: () => Effect.Effect<Map<string, T>>;
|
||||
}
|
||||
|
||||
export const FileCacheStorage = <T>() =>
|
||||
Context.GenericTag<FileCacheStorageService<T>>("FileCacheStorage");
|
||||
|
||||
export const makeFileCacheStorageLayer = <T>(
|
||||
storageKey: string,
|
||||
schema: z.ZodType<T>,
|
||||
) =>
|
||||
Layer.effect(
|
||||
FileCacheStorage<T>(),
|
||||
Effect.gen(function* () {
|
||||
const persistentService = yield* PersistentService;
|
||||
|
||||
const runtime = yield* Effect.runtime<FileSystem.FileSystem>();
|
||||
|
||||
const storageRef = yield* Effect.gen(function* () {
|
||||
const persistedData = yield* persistentService.load(storageKey);
|
||||
|
||||
const initialMap = new Map<string, T>();
|
||||
for (const [key, value] of persistedData) {
|
||||
const parsed = schema.safeParse(value);
|
||||
if (parsed.success) {
|
||||
initialMap.set(key, parsed.data);
|
||||
}
|
||||
}
|
||||
|
||||
return yield* Ref.make(initialMap);
|
||||
});
|
||||
|
||||
const syncToFile = (entries: readonly [string, T][]) => {
|
||||
Runtime.runFork(runtime)(persistentService.save(storageKey, entries));
|
||||
};
|
||||
|
||||
return {
|
||||
get: (key: string) =>
|
||||
Effect.gen(function* () {
|
||||
const storage = yield* Ref.get(storageRef);
|
||||
return storage.get(key);
|
||||
}),
|
||||
|
||||
set: (key: string, value: T) =>
|
||||
Effect.gen(function* () {
|
||||
const before = yield* Ref.get(storageRef);
|
||||
const beforeString = JSON.stringify(Array.from(before.entries()));
|
||||
|
||||
yield* Ref.update(storageRef, (map) => {
|
||||
map.set(key, value);
|
||||
return map;
|
||||
});
|
||||
|
||||
const after = yield* Ref.get(storageRef);
|
||||
const afterString = JSON.stringify(Array.from(after.entries()));
|
||||
|
||||
if (beforeString !== afterString) {
|
||||
syncToFile(Array.from(after.entries()));
|
||||
}
|
||||
}),
|
||||
|
||||
invalidate: (key: string) =>
|
||||
Effect.gen(function* () {
|
||||
const before = yield* Ref.get(storageRef);
|
||||
|
||||
if (!before.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield* Ref.update(storageRef, (map) => {
|
||||
map.delete(key);
|
||||
return map;
|
||||
});
|
||||
|
||||
const after = yield* Ref.get(storageRef);
|
||||
syncToFile(Array.from(after.entries()));
|
||||
}),
|
||||
|
||||
getAll: () =>
|
||||
Effect.gen(function* () {
|
||||
const storage = yield* Ref.get(storageRef);
|
||||
return new Map(storage);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
export class InMemoryCacheStorage<const T> {
|
||||
private storage = new Map<string, T>();
|
||||
|
||||
public get(key: string) {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
public save(key: string, value: T) {
|
||||
this.storage.set(key, value);
|
||||
}
|
||||
|
||||
public invalidate(key: string) {
|
||||
if (!this.storage.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user