1import * as Schema from "effect/Schema";
2
3import { decodeIncomingShareDraft, type IncomingShareDraft } from "./incoming-share-model";
4
5const INCOMING_SHARE_DIRECTORY = "incoming-shares";
6
7export class IncomingShareStorageError extends Schema.TaggedError<IncomingShareStorageError>()(
8 "IncomingShareStorageError",
9 {
10 operation: Schema.Literals(["load", "write", "remove"]),
11 shareId: Schema.NullOr(Schema.String),
12 cause: Schema.Defect(),
13 },
14) {
15 override get message(): string {
16 return `Incoming share storage operation ${this.operation} failed for ${this.shareId ?? "unknown"}.`;
17 }
18}
19
20function fileName(shareId: string): string {
21 return `${encodeURIComponent(shareId)}.json`;
22}
23
24async function getDirectory() {
25 const { Directory, Paths } = await import("expo-file-system");
26 const directory = new Directory(Paths.document, INCOMING_SHARE_DIRECTORY);
27 directory.create({ idempotent: true, intermediates: true });
28 return directory;
29}
30
31async function getFile(shareId: string) {
32 const { File } = await import("expo-file-system");
33 return new File(await getDirectory(), fileName(shareId));
34}
35
36export async function loadIncomingShareDrafts(options?: {
37 readonly strict?: boolean;
38}): Promise<ReadonlyArray<IncomingShareDraft>> {
39 try {
40 const { File } = await import("expo-file-system");
41 const drafts: IncomingShareDraft[] = [];
42 for (const entry of (await getDirectory()).list()) {
43 if (!(entry instanceof File) || !entry.name.endsWith(".json")) {
44 continue;
45 }
46 try {
47 drafts.push(decodeIncomingShareDraft(JSON.parse(await entry.text()) as unknown));
48 } catch (cause) {
49 const error = new IncomingShareStorageError({ operation: "load", shareId: null, cause });
50 if (options?.strict) {
51 throw error;
52 }
53 console.warn("[incoming-share] ignored invalid persisted share", error);
54 }
55 }
56 return drafts.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
57 } catch (cause) {
58 if (cause instanceof IncomingShareStorageError) {
59 throw cause;
60 }
61 throw new IncomingShareStorageError({ operation: "load", shareId: null, cause });
62 }
63}
64
65export async function writeIncomingShareDraft(draft: IncomingShareDraft): Promise<void> {
66 try {
67 const file = await getFile(draft.id);
68 if (!file.exists) {
69 file.create({ intermediates: true, overwrite: true });
70 }
71 await file.write(JSON.stringify(draft));
72 } catch (cause) {
73 throw new IncomingShareStorageError({ operation: "write", shareId: draft.id, cause });
74 }
75}
76
77export async function removeIncomingShareDraft(shareId: string): Promise<void> {
78 try {
79 const file = await getFile(shareId);
80 if (file.exists) {
81 file.delete();
82 }
83 } catch (cause) {
84 throw new IncomingShareStorageError({ operation: "remove", shareId, cause });
85 }
86}