authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:08:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:08:03-07:00
log30a084c463acb7b8b75550a117b9bd82e98c5ac4
tree483bb848903846980cc5e5b4b6b0886078f7946e
parent9d5ca2cb7ceec88e392d5dc3a70a5979953e4e3e
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve mobile composer draft failures (#3348)

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 66 insertions(+), 15 deletions(-)

apps/mobile/src/state/use-composer-drafts.ts+66-15
......@@ -1,5 +1,6 @@
11import { useAtomValue } from "@effect/atom-react";
22import type { EnvironmentId } from "@t3tools/contracts";
3import * as Schema from "effect/Schema";
34import { useEffect } from "react";
45import { Atom } from "effect/unstable/reactivity";
56
......@@ -11,6 +12,20 @@ const COMPOSER_DRAFTS_DIRECTORY = "composer-drafts";
1112const COMPOSER_DRAFTS_FILE = "drafts.json";
1213const PERSIST_DEBOUNCE_MS = 200;
1314
15export class ComposerDraftPersistenceError extends Schema.TaggedErrorClass<ComposerDraftPersistenceError>()(
16 "ComposerDraftPersistenceError",
17 {
18 operation: Schema.Literals(["open", "read", "decode", "encode", "write", "hydrate"]),
19 directory: Schema.String,
20 fileName: Schema.String,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Composer draft persistence operation ${this.operation} failed for ${this.directory}/${this.fileName}.`;
26 }
27}
28
1429export interface ComposerDraft {
1530 readonly text: string;
1631 readonly attachments: ReadonlyArray<DraftComposerImageAttachment>;
......@@ -56,12 +71,16 @@ async function getComposerDraftsFile() {
5671}
5772
5873async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDraft>> {
74 let operation: ComposerDraftPersistenceError["operation"] = "open";
5975 try {
6076 const file = await getComposerDraftsFile();
6177 if (!file.exists) {
6278 return {};
6379 }
64 const parsed = JSON.parse(await file.text()) as Partial<PersistedComposerDrafts>;
80 operation = "read";
81 const raw = await file.text();
82 operation = "decode";
83 const parsed = JSON.parse(raw) as Partial<PersistedComposerDrafts>;
6584 if (parsed.schemaVersion !== COMPOSER_DRAFTS_SCHEMA_VERSION || !parsed.drafts) {
6685 return {};
6786 }
......@@ -75,30 +94,53 @@ async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDra
7594 );
7695 }),
7796 );
78 } catch {
97 } catch (cause) {
98 console.warn(
99 "[composer-drafts] ignored persisted draft failure",
100 new ComposerDraftPersistenceError({
101 operation,
102 directory: COMPOSER_DRAFTS_DIRECTORY,
103 fileName: COMPOSER_DRAFTS_FILE,
104 cause,
105 }),
106 );
79107 return {};
80108 }
81109}
82110
83111async function writePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {
84 const file = await getComposerDraftsFile();
85 const nonEmptyDrafts = Object.fromEntries(
86 Object.entries(drafts).filter(([, draft]) => !isEmptyDraft(draft)),
87 );
88 const document: PersistedComposerDrafts = {
89 schemaVersion: COMPOSER_DRAFTS_SCHEMA_VERSION,
90 drafts: nonEmptyDrafts,
91 };
92 if (!file.exists) {
93 file.create({ intermediates: true, overwrite: true });
112 let operation: ComposerDraftPersistenceError["operation"] = "open";
113 try {
114 const file = await getComposerDraftsFile();
115 operation = "encode";
116 const nonEmptyDrafts = Object.fromEntries(
117 Object.entries(drafts).filter(([, draft]) => !isEmptyDraft(draft)),
118 );
119 const document: PersistedComposerDrafts = {
120 schemaVersion: COMPOSER_DRAFTS_SCHEMA_VERSION,
121 drafts: nonEmptyDrafts,
122 };
123 const encoded = JSON.stringify(document);
124 operation = "write";
125 if (!file.exists) {
126 file.create({ intermediates: true, overwrite: true });
127 }
128 file.write(encoded);
129 } catch (cause) {
130 throw new ComposerDraftPersistenceError({
131 operation,
132 directory: COMPOSER_DRAFTS_DIRECTORY,
133 fileName: COMPOSER_DRAFTS_FILE,
134 cause,
135 });
94136 }
95 file.write(JSON.stringify(document));
96137}
97138
98139async function savePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {
99140 try {
100141 await writePersistedComposerDrafts(drafts);
101 } catch {
142 } catch (error) {
143 console.warn("[composer-drafts] failed to persist drafts", error);
102144 // Draft persistence is best-effort; in-memory drafts still keep working.
103145 }
104146}
......@@ -128,7 +170,16 @@ export function ensureComposerDraftsLoaded(): void {
128170 ...current,
129171 });
130172 })
131 .catch(() => {
173 .catch((cause) => {
174 console.warn(
175 "[composer-drafts] failed to hydrate drafts",
176 new ComposerDraftPersistenceError({
177 operation: "hydrate",
178 directory: COMPOSER_DRAFTS_DIRECTORY,
179 fileName: COMPOSER_DRAFTS_FILE,
180 cause,
181 }),
182 );
132183 // Draft loading is best-effort; in-memory drafts still keep working.
133184 });
134185}