(db: FumaDb, options: MakeFumaClientOptions = {})
| 172 | }; |
| 173 | |
| 174 | export const makeFumaClient = (db: FumaDb, options: MakeFumaClientOptions = {}): IFumaClient => { |
| 175 | const use: IFumaClient["use"] = (label, fn) => |
| 176 | Effect.flatMap(Effect.service(activeFumaDbRef), (active) => |
| 177 | fumaEffect(label, () => fn(makeSafeFumaQuery(active ?? db, options))), |
| 178 | ).pipe(Effect.withSpan(`fumadb.${label}`)); |
| 179 | |
| 180 | const transaction = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E | StorageFailure> => |
| 181 | Effect.flatMap(Effect.service(activeFumaDbRef), (active) => { |
| 182 | if (active) return effect as Effect.Effect<unknown, unknown>; |
| 183 | |
| 184 | // The outermost transaction owns the post-commit hook queue; hooks |
| 185 | // queued anywhere inside (including nested pass-through transactions) |
| 186 | // run only after THIS commit, and are discarded on rollback. |
| 187 | const commitHooks: Array<Effect.Effect<void>> = []; |
| 188 | return Effect.tryPromise({ |
| 189 | try: () => |
| 190 | db.transaction(async (transactionDb) => { |
| 191 | const exit = await Effect.runPromiseExit( |
| 192 | effect.pipe( |
| 193 | Effect.provideService(activeFumaDbRef, transactionDb), |
| 194 | Effect.provideService(pendingCommitHooksRef, commitHooks), |
| 195 | ), |
| 196 | ); |
| 197 | if (Exit.isSuccess(exit)) return exit.value; |
| 198 | |
| 199 | const failure = exit.cause.reasons.find(Cause.isFailReason); |
| 200 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: FumaDB transactions roll back when the callback rejects |
| 201 | if (failure) throw new TransactionEffectFailure(failure.error); |
| 202 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: FumaDB transactions roll back when the callback rejects |
| 203 | throw new TransactionEffectDefect(exit.cause); |
| 204 | }), |
| 205 | catch: (cause): E | StorageFailure => { |
| 206 | if (cause instanceof TransactionEffectFailure) return cause.error as E; |
| 207 | if (cause instanceof TransactionEffectDefect) { |
| 208 | return fumaFailureFromCause("transaction", cause.cause); |
| 209 | } |
| 210 | return fumaFailureFromCause("transaction", cause); |
| 211 | }, |
| 212 | }).pipe( |
| 213 | Effect.tap(() => |
| 214 | Effect.forEach(commitHooks, (hook) => hook.pipe(Effect.ignoreCause({ log: false })), { |
| 215 | discard: true, |
| 216 | }), |
| 217 | ), |
| 218 | ); |
| 219 | }).pipe(Effect.withSpan("fumadb.transaction")) as Effect.Effect<A, E | StorageFailure>; |
| 220 | |
| 221 | return { use, transaction }; |
| 222 | }; |
| 223 | |
| 224 | export class FumaClient extends Context.Service<FumaClient, IFumaClient>()("executor/FumaClient") { |
| 225 | static layer = (db: FumaDb) => Layer.succeed(this)(makeFumaClient(db)); |
no outgoing calls
no test coverage detected