I wrote this code to solve similar problem.
I think it is kind of classic DB transaction model. It implements only lock mechanism, not rollbacks. It works in following way:
- you adding bunch of sync methods in order in which they should be called
- these methods will be called in N milliseconds
- there will be only one get request to get DB data (or any other data that you need to modify) and one request to save DB data
- every method will receive reference to "up to date data", i.e. handler № 2 will receive data object that was changed by handler № 1. Order depends on order of № 1
It works in browsers. It should work in Node.js, but I didn't test it.
Note that because every method will receive reference to object, you should modify actual reference, and if you want to read it and return some value, then copy it, don't return that reference, because values of that reference may be changed in future by future handlers
TRANSACTION
will wait for N ms (defaults to 250) before execution. That defines which methods will be grouped into single transaction. You can also make instant calls.
Here is the code:
let TIMER_ID = 0;
let LOCK = Promise.resolve();
let RESOLVE_LOCK = undefined;
let FUNC_BUFFER = [];
let SUCCESS_BUFFER = [];
let FAIL_BUFFER = [];
/**
* Gets DB data.
*/
async function get() {
return {
key1: "value1",
key2: {
key3: "value2"
}
};
}
/**
* Sets new data in DB.
*/
async function set(value) {
return;
}
/**
* Classic database transaction implementation.
*
* You adding bunch of methods, every method will be
* executed with valid instance of data at call time,
* after all functions end the transaction will end.
* If any method failed, then entire transaction will fail
* and no changes will be written. If current transaction is
* active, new one will be not started until end of previous one.
*
* In short, this transaction have ACID properties.
*
* Operations will be auto grouped into separate transactions
* based on start timeout, which is recreated every time on
* operation call.
*
* @example
* ```
* // Without transaction:
* create("1", 123)
* update("1", 321)
* read("1") => 123 // because `update` is async
*
* // With transaction:
* create("1", 123)
* update("1", 321)
* read("1") => 321
*
* // Without transaction:
* create("v", true)
* update("v", false) // throws internal error,
* // "v" will be created
* read("v") => true // because `update` is async
* update("v", false) // will update because `update` is async
*
* // With transaction:
* create("v", true)
* update("v", false) // throws internal error,
* // entire transaction will throw error,
* // "v" will be not created
* read("v") => true // entire transaction will throw error
* update("v", false) // entire transaction will throw error
* ```
*
* @example
* ```
* // Transaction start
* create()
* update()
* update()
* remove()
* // Transaction end
*
* // Transaction start
* create()
* update()
* sleep(1000)
* // Transaction end
*
* // Transaction start
* update()
* remove()
* // Transaction end
* ```
*/
const TRANSACTION = {
/**
* Adds function in transaction.
*
* NOTE:
* you shouldn't await this function, because
* it only represents transcation lock, not
* entire transaction end.
*
* @param f
* Every function should only modify passed state, don't
* reassign passed state and not save changes manually!
* @param onSuccess
* Will be called on entire transaction success.
* @param onFail
* Will be called on entire transaction fail.
* @param startTimeout
* Passed `f` will be added in current transaction,
* and current transaction will be called after
* `startTimeout` ms if there will be no more `f` passed.
* Default value is recommended.
*/
add: async function(
f,
onSuccess,
onFail,
startTimeout
) {
await LOCK;
window.clearTimeout(TIMER_ID);
FUNC_BUFFER.push(f);
if (onSuccess) {
SUCCESS_BUFFER.push(onSuccess);
}
if (onFail) {
FAIL_BUFFER.push(onFail);
}
if (startTimeout == null) {
startTimeout = 250;
}
TIMER_ID = window.setTimeout(() => {
TRANSACTION.start();
}, startTimeout);
console.debug("Added in transaction");
},
start: async function() {
LOCK = new Promise((resolve) => {
RESOLVE_LOCK = resolve;
});
console.debug("Transaction locked");
let success = true;
try {
await TRANSACTION.run();
} catch (error) {
success = false;
console.error(error);
console.warn("Transaction failed");
}
if (success) {
for (const onSuccess of SUCCESS_BUFFER) {
try {
onSuccess();
} catch (error) {
console.error(error);
}
}
} else {
for (const onFail of FAIL_BUFFER) {
try {
onFail();
} catch (error) {
console.error(error);
}
}
}
FUNC_BUFFER = [];
SUCCESS_BUFFER = [];
FAIL_BUFFER = [];
RESOLVE_LOCK();
console.debug("Transaction unlocked");
},
run: async function() {
const data = await get();
const state = {
value1: data.key1,
value2: data.key2
};
for (const f of FUNC_BUFFER) {
console.debug("Transaction function started");
f(state);
console.debug("Transaction function ended");
}
await set({
key1: state.value1,
key2: state.value2
});
}
}
Example № 1:
/**
* Gets DB data.
*/
async function get() {
return {
key1: "value1",
key2: {
key3: "value2"
}
};
}
/**
* Sets new data in DB.
*/
async function set(value) {
console.debug("Will be set:", value);
return;
}
new Promise(
(resolve) => {
TRANSACTION.add(
(data) => {
data.value2.key3 = "test1";
},
() => console.debug("success № 1")
);
TRANSACTION.add(
(data) => {
const copy = {
...data.value2
};
resolve(copy);
},
() => console.debug("success № 2")
);
TRANSACTION.add(
(data) => {
data.value1 = "test10";
data.value2.key3 = "test2";
},
() => console.debug("success № 3")
);
}
)
.then((value) => {
console.debug("result:", value);
});
/* Output:
Added in transaction
Added in transaction
Added in transaction
Transaction locked
Transaction function started
Transaction function ended
Transaction function started
Transaction function ended
Transaction function started
Transaction function ended
Will be set: {key1: 'test10', key2: {key3: 'test2'}}
result: {key3: 'test1'}
success № 1
success № 2
success № 3
Transaction unlocked
*/
Example № 2:
TRANSACTION.add(
() => {
console.log(1);
}
);
TRANSACTION.add(
() => {
console.log(2);
},
undefined,
undefined,
0 // instant call
);
/* Output:
16:15:34.715 Added in transaction
16:15:34.715 Added in transaction
16:15:34.717 Transaction locked
16:15:34.717 Transaction function started
16:15:34.718 1
16:15:34.718 Transaction function ended
16:15:34.718 Transaction function started
16:15:34.718 2
16:15:34.718 Transaction function ended
16:15:34.719 Transaction unlocked
*/
Example № 3:
TRANSACTION.add(
() => {
console.log(1);
}
);
TRANSACTION.add(
() => {
console.log(2);
},
undefined,
undefined,
0 // instant call
);
await new Promise((resolve) => {
window.setTimeout(() => {
resolve();
}, 1000);
});
TRANSACTION.add(
() => {
console.log(3);
}
);
/* Output:
16:19:56.840 Added in transaction
16:19:56.840 Added in transaction
16:19:56.841 Transaction locked
16:19:56.841 Transaction function started
16:19:56.842 1
16:19:56.842 Transaction function ended
16:19:56.842 Transaction function started
16:19:56.842 2
16:19:56.842 Transaction function ended
16:19:56.842 Transaction unlocked
16:19:57.840 Added in transaction
16:19:58.090 Transaction locked
16:19:58.090 Transaction function started
16:19:58.090 3
16:19:58.091 Transaction function ended
16:19:58.091 Transaction unlocked
*/