@golemcloud/golem-ts
Version:
A library that help writing Golem programs by providing higher level wrappers for Golem's runtime APIs, including functions for defining and performing operations transactionally.
163 lines (162 loc) • 5.67 kB
JavaScript
// Copyright 2024-2025 Golem Cloud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { executeWithDrop, markAtomicOperation } from "./guard";
import { getOplogIndex, setOplogIndex } from "./hostapi";
import { Result } from "./result";
/**
* Creates an Operation from the provided execute and compensate functions.
* @param execute - The function to execute the operation.
* @param compensate - The function to compensate the operation in case of failure.
* @returns The created Operation.
*/
export function operation(execute, compensate) {
return new OperationImpl(execute, compensate);
}
class OperationImpl {
constructor(execute, compensate) {
this.execute = execute;
this.compensate = compensate;
}
}
class InfallibleTransaction {
constructor(beginOplogIndex) {
this.beginOplogIndex = beginOplogIndex;
this.compensations = [];
}
/**
* Executes an operation within the infallible transaction.
* @param operation - The operation to execute.
* @param input - The input to the operation.
* @returns The result of the operation.
*/
execute(operation, input) {
const result = operation.execute(input);
if (result.isOk()) {
this.compensations.push(
// Compensations cannot fail in infallible transactions.
() => {
const compensationResult = operation.compensate(input, result.val);
if (compensationResult.isErr()) {
throw new Error("Compensation action failed");
}
});
return result.val;
}
else {
this.retry();
throw new Error("Unreachable code");
}
}
retry() {
// Rollback all the compensations in reverse order
for (let i = this.compensations.length - 1; i >= 0; i--) {
this.compensations[i]();
}
setOplogIndex(this.beginOplogIndex);
}
}
class FallibleTransaction {
constructor() {
this.compensations = [];
}
/**
* Executes an operation within the fallible transaction.
* @param operation - The operation to execute.
* @param input - The input to the operation.
* @returns The result of the operation.
*/
execute(operation, input) {
const result = operation.execute(input);
if (result.isOk()) {
this.compensations.push(() => {
return operation.compensate(input, result.val);
});
return result;
}
else {
return result;
}
}
/**
* Handles the failure of the fallible transaction.
* @param error - The error that caused the failure.
* @returns The transaction failure result.
*/
onFailure(error) {
for (let i = this.compensations.length - 1; i >= 0; i--) {
const compensationResult = this.compensations[i]();
if (compensationResult.isErr()) {
return {
type: "FailedAndRolledBackPartially",
error,
compensationFailure: compensationResult.val,
};
}
}
return {
type: "FailedAndRolledBackCompletely",
error,
};
}
}
/**
* Executes an infallible transaction.
*
* InfallibleTransaction is a sequence of operations that are executed in a way that if any of the
* operations or the underlying Golem executor fails, the whole transaction is going to
* be retried.
*
* In addition to that, **user level failures** (represented by the `Result::Err` value
* of an operation) lead to performing the compensation actions of each already performed operation
* in reverse order.
*
* Fatal errors (panic) and external executor failures currently cannot perform the
* rollback actions.
*
* @param f - The function that defines the transaction.
* @returns The result of the transaction.
*/
export function infallibleTransaction(f) {
const guard = markAtomicOperation();
const beginOplogIndex = getOplogIndex();
const tx = new InfallibleTransaction(beginOplogIndex);
return executeWithDrop([guard], () => f(tx));
}
/**
* Executes a fallible transaction.
*
* FallibleTransaction is a sequence of operations that are executed in a way that if any of the
* operations fails, all the already performed operation's compensation actions get executed in
* reverse order.
*
* In case of fatal errors (panic) and external executor failures, it does not perform the
* compensation actions and the whole transaction gets retried.
*
* @param f - The function that defines the transaction.
* @returns The result of the transaction.
*/
export function fallibleTransaction(f) {
const guard = markAtomicOperation();
const tx = new FallibleTransaction();
const execute = () => {
const result = f(tx);
if (result.isOk()) {
return Result.ok(result.val);
}
else {
return Result.err(tx.onFailure(result.val));
}
};
return executeWithDrop([guard], execute);
}