| 1258 | * until the test says so (setup reads pass through untouched), and the test |
| 1259 | * can check that an armed read really started before it was abandoned. */ |
| 1260 | const makeReadFaults = () => { |
| 1261 | let hung: ReadonlySet<string> = new Set(); |
| 1262 | let failed: ReadonlyMap<string, string> = new Map(); |
| 1263 | const started = new Set<string>(); |
| 1264 | return { |
| 1265 | hangReads: (keys: readonly string[]) => { |
| 1266 | hung = new Set(keys); |
| 1267 | }, |
| 1268 | failReads: (byCode: Readonly<Record<string, string>>) => { |
| 1269 | failed = new Map(Object.entries(byCode)); |
| 1270 | }, |
| 1271 | started, |
| 1272 | fault: (key: string): Promise<never> | undefined => { |
| 1273 | if (hung.has(key)) { |
| 1274 | started.add(key); |
| 1275 | // Never settles. The driver promise takes no abort signal, so an |
| 1276 | // interrupted read is abandoned exactly like this in production. |
| 1277 | return new Promise<never>(() => {}); |
| 1278 | } |
| 1279 | const code = failed.get(key); |
| 1280 | if (code === undefined) return undefined; |
| 1281 | started.add(key); |
| 1282 | // 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 |
| 1283 | return Promise.reject(Object.assign(new Error(code), { code })); |
| 1284 | }, |
| 1285 | }; |
| 1286 | }; |
| 1287 | |
| 1288 | /** Wrap a test `FumaDb` so armed reads hang or reject. Every other read |
| 1289 | * passes through, deferred by one `setImmediate` tick — the wrapper is an |