| 1148 | * until the test says so (setup reads pass through untouched), and the test |
| 1149 | * can check that an armed read really started before it was abandoned. */ |
| 1150 | const makeReadFaults = () => { |
| 1151 | let hung: ReadonlySet<string> = new Set(); |
| 1152 | let failed: ReadonlyMap<string, string> = new Map(); |
| 1153 | const started = new Set<string>(); |
| 1154 | return { |
| 1155 | hangReads: (keys: readonly string[]) => { |
| 1156 | hung = new Set(keys); |
| 1157 | }, |
| 1158 | failReads: (byCode: Readonly<Record<string, string>>) => { |
| 1159 | failed = new Map(Object.entries(byCode)); |
| 1160 | }, |
| 1161 | started, |
| 1162 | fault: (key: string): Promise<never> | undefined => { |
| 1163 | if (hung.has(key)) { |
| 1164 | started.add(key); |
| 1165 | // Never settles. The driver promise takes no abort signal, so an |
| 1166 | // interrupted read is abandoned exactly like this in production. |
| 1167 | return new Promise<never>(() => {}); |
| 1168 | } |
| 1169 | const code = failed.get(key); |
| 1170 | if (code === undefined) return undefined; |
| 1171 | started.add(key); |
| 1172 | // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: simulate the raw driver-promise rejection that fumaEffect normalizes into a StorageFailure |
| 1173 | return Promise.reject(Object.assign(new Error(code), { code })); |
| 1174 | }, |
| 1175 | }; |
| 1176 | }; |
| 1177 | |
| 1178 | /** Wrap a test `FumaDb` so armed reads hang or reject. Every other read |
| 1179 | * passes through, deferred by one `setImmediate` tick — the wrapper is an |