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 @@...@@ -1,5 +1,6 @@
1import { useAtomValue } from "@effect/atom-react";1import { useAtomValue } from "@effect/atom-react";
2import type { EnvironmentId } from "@t3tools/contracts";2import type { EnvironmentId } from "@t3tools/contracts";
3import * as Schema from "effect/Schema";
3import { useEffect } from "react";4import { useEffect } from "react";
4import { Atom } from "effect/unstable/reactivity";5import { Atom } from "effect/unstable/reactivity";
56
...@@ -11,6 +12,20 @@ const COMPOSER_DRAFTS_DIRECTORY = "composer-drafts";...@@ -11,6 +12,20 @@ const COMPOSER_DRAFTS_DIRECTORY = "composer-drafts";
11const COMPOSER_DRAFTS_FILE = "drafts.json";12const COMPOSER_DRAFTS_FILE = "drafts.json";
12const PERSIST_DEBOUNCE_MS = 200;13const 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
14export interface ComposerDraft {29export interface ComposerDraft {
15 readonly text: string;30 readonly text: string;
16 readonly attachments: ReadonlyArray<DraftComposerImageAttachment>;31 readonly attachments: ReadonlyArray<DraftComposerImageAttachment>;
...@@ -56,12 +71,16 @@ async function getComposerDraftsFile() {...@@ -56,12 +71,16 @@ async function getComposerDraftsFile() {
56}71}
5772
58async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDraft>> {73async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDraft>> {
74 let operation: ComposerDraftPersistenceError["operation"] = "open";
59 try {75 try {
60 const file = await getComposerDraftsFile();76 const file = await getComposerDraftsFile();
61 if (!file.exists) {77 if (!file.exists) {
62 return {};78 return {};
63 }79 }
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>;
65 if (parsed.schemaVersion !== COMPOSER_DRAFTS_SCHEMA_VERSION || !parsed.drafts) {84 if (parsed.schemaVersion !== COMPOSER_DRAFTS_SCHEMA_VERSION || !parsed.drafts) {
66 return {};85 return {};
67 }86 }
...@@ -75,30 +94,53 @@ async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDra...@@ -75,30 +94,53 @@ async function loadPersistedComposerDrafts(): Promise<Record<string, ComposerDra
75 );94 );
76 }),95 }),
77 );96 );
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 );
79 return {};107 return {};
80 }108 }
81}109}
82110
83async function writePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {111async function writePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {
84 const file = await getComposerDraftsFile();112 let operation: ComposerDraftPersistenceError["operation"] = "open";
85 const nonEmptyDrafts = Object.fromEntries(113 try {
86 Object.entries(drafts).filter(([, draft]) => !isEmptyDraft(draft)),114 const file = await getComposerDraftsFile();
87 );115 operation = "encode";
88 const document: PersistedComposerDrafts = {116 const nonEmptyDrafts = Object.fromEntries(
89 schemaVersion: COMPOSER_DRAFTS_SCHEMA_VERSION,117 Object.entries(drafts).filter(([, draft]) => !isEmptyDraft(draft)),
90 drafts: nonEmptyDrafts,118 );
91 };119 const document: PersistedComposerDrafts = {
92 if (!file.exists) {120 schemaVersion: COMPOSER_DRAFTS_SCHEMA_VERSION,
93 file.create({ intermediates: true, overwrite: true });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 });
94 }136 }
95 file.write(JSON.stringify(document));
96}137}
97138
98async function savePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {139async function savePersistedComposerDrafts(drafts: Record<string, ComposerDraft>): Promise<void> {
99 try {140 try {
100 await writePersistedComposerDrafts(drafts);141 await writePersistedComposerDrafts(drafts);
101 } catch {142 } catch (error) {
143 console.warn("[composer-drafts] failed to persist drafts", error);
102 // Draft persistence is best-effort; in-memory drafts still keep working.144 // Draft persistence is best-effort; in-memory drafts still keep working.
103 }145 }
104}146}
...@@ -128,7 +170,16 @@ export function ensureComposerDraftsLoaded(): void {...@@ -128,7 +170,16 @@ export function ensureComposerDraftsLoaded(): void {
128 ...current,170 ...current,
129 });171 });
130 })172 })
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 );
132 // Draft loading is best-effort; in-memory drafts still keep working.183 // Draft loading is best-effort; in-memory drafts still keep working.
133 });184 });
134}185}